Compare commits

...

166 commits

Author SHA1 Message Date
875166b683
Replace WMIC sections
Addresses issue #227
2024-11-23 15:27:17 -08:00
179748c469
Add Windows 11 24H2 to version list 2024-11-22 07:27:44 -08:00
ffcee1156a
Remove stale docopt files 2024-09-30 01:52:07 -07:00
edd944a325
Hide extra partitions when building UFDs
Addresses issue #226
2024-09-28 23:22:31 -07:00
75119c15ad
Fix bug in ddrescue-tui argument parsing 2024-09-11 18:11:12 -07:00
e6db63c8b0
Fix typo 2024-09-09 21:43:28 -07:00
e388b77639
Add OSFMount 2024-09-09 21:40:19 -07:00
bbdef56df2
Add OSFMount 2024-09-09 21:05:36 -07:00
6a5a944ea0
Refactor notepad replacement in WinPE
The registry method has proved problematic.  This DOSKEY method seems
less error prone since it's evaluated at runtime.
2024-09-09 20:12:54 -07:00
3621914312
Remove temporary WinPE WIM file 2024-09-08 13:28:31 -07:00
0ef9412995
Switch back to ConEmu in WinPE 2024-09-08 03:53:48 -07:00
0335797a5d
Update WinPE sections 2024-09-08 03:04:33 -07:00
2a07aebff3
Move macOS icons into a tar archive 2024-09-08 00:13:47 -07:00
244d917c73
Drop remaining docopt references 2024-09-04 01:14:34 -07:00
13b8dc8696
Replace remaining docopt sections 2024-09-04 00:53:42 -07:00
58576cbdb4
Use FAT32 for all UFD partitions
This breaks macOS images but those are deprecated at this point.
2024-09-04 00:24:03 -07:00
a3a7b25512
Fix case_insensitive_search 2024-09-04 00:16:54 -07:00
50033f42f6
Move to argparse for ddrescue-tui and hw-diags 2024-09-03 01:12:00 -07:00
d00d625142
Avoid crash when launching SDIO under Windows 11 2024-06-12 19:26:15 -07:00
97842e82f2
Make AVRemover optional 2024-06-03 21:08:59 -07:00
4a54b6e00c
Improve resolution detection when run in a VM 2024-04-27 20:46:23 -07:00
ee7c7c2448
Allow using the live CD/DVD/etc as a source 2024-03-30 23:04:49 -07:00
3aff533c4d
Reduce size to zero-out
Most tools will use a 1MiB offset for the first partition
2024-03-30 23:04:08 -07:00
a256e6e764
Disable finalization options when imaging 2024-03-25 23:01:23 -07:00
cc3e36c60d
Ensure destination exists in ddrescue-tui image 2024-03-25 22:54:57 -07:00
f91eac3ec3
Add Memtest86+ boot options in Syslinux config 2024-03-24 18:52:33 -07:00
667de2d672
Adjust Linux boot options
- CLI option was dropped in Syslinux due to keep the list to a minimum.
Also, its main use was to avoid display issues and nomodeset is a better
option IMO
2024-03-24 18:47:00 -07:00
272fd3e43f
Update tmux sections to reflect upstream changes 2024-03-23 22:35:12 -07:00
58069f9db2
Added 23H2 to windows builds list 2024-03-21 16:04:54 -07:00
33d266f83e
Update more tool configs 2023-11-11 19:53:22 -08:00
e84feb3160
Add AIDA64 config 2023-11-11 18:58:13 -08:00
243fb78837
Add WizTree config 2023-11-11 16:38:58 -08:00
565ec32294
Enable OpenShell under Win11 2023-11-11 15:52:02 -08:00
694eec911b
Drop unused boot option 2023-11-04 17:53:20 -07:00
44b7f786e7
Add UFD macOS boot icon 2023-11-02 20:52:13 -07:00
f7d212d115
Refactor built-ufd to match new Linux builds 2023-11-01 23:30:27 -07:00
3230984a4f
Remove more Archiso customization sections
This will be added to build-ufd
2023-10-27 00:44:58 -07:00
9f4b0ffa82
Remove ISO bootloader override section 2023-10-26 23:19:30 -07:00
75bad41e93
Revert to upstream Linux ISO boot setup
We'll move the WizardKit customization to build-ufd
2023-10-26 00:08:52 -07:00
8602723adb
Use C.UTF-8 to match upstream 2023-10-24 00:29:05 -07:00
b01cb6ed26
Remove haveged and rngd to match upstream 2023-10-23 23:41:24 -07:00
aaae24b790
Suppress linting message 2023-10-22 19:54:51 -07:00
94c8c2ba01
Fix detection of full clones in zero_fill_gaps()
This prevents using the wrong domain size and crashing ddrescue.
2023-10-22 19:53:04 -07:00
80a0d9874a
Drop TDSSKiller
Kaspersky removed the tool from their site, it's probably best to follow that.
2023-10-22 17:41:27 -07:00
e85d9c220e
Update LICENSE.txt 2023-10-21 23:39:28 -07:00
864a546fe3
Rename SDIO config file 2023-10-21 23:28:17 -07:00
3ce134901a
Add config files for BCUninstaller and DDU 2023-10-21 23:01:43 -07:00
6d62bfeaea
Update FastCopy URL 2023-10-21 22:53:27 -07:00
c18e82af75
Add missing DeviceCleanup files 2023-10-21 19:17:50 -07:00
9f5c097f81
Fix typos 2023-10-21 19:11:37 -07:00
42c72f20f4
Run DeviceCleanup and DDU elevated 2023-10-21 19:03:15 -07:00
4e887c318b
Use full tool names 2023-10-21 18:59:37 -07:00
e39a2acf26
Add missing launchers 2023-10-21 18:54:04 -07:00
8ab56ae9d8
Replace UninstallView with BCUninstaller
Addresses issue #223
2023-10-21 18:50:20 -07:00
1c6f0f5f28
Update tool sources 2023-10-21 18:43:54 -07:00
4aedea65c7
Add DeviceCleanup and DDU
Address issue #203
2023-10-21 18:42:30 -07:00
d7067af522
Add data structure description to Sensors() object 2023-10-07 16:36:17 -07:00
484f13dc29
Update embedded_python_env.py 2023-09-23 16:27:34 -07:00
868932c5e4
Remove duplicate code 2023-09-18 11:33:47 -07:00
17a62d6f36
Merge branch 'dev' of gitea.2shirt.work:2Shirt/WizardKit into dev 2023-09-18 11:32:50 -07:00
b20b612315
Update LibreOffice version 2023-09-18 11:30:21 -07:00
73bd58a973
Store average temps in history for use in reports
Addresses #204
2023-09-17 19:40:06 -07:00
33e9cde0f4
Keep history of sensor temps
Needed for #204
2023-09-17 18:11:09 -07:00
ee9d316217
Include serial in ddrescue map name
Addresses issue #200
2023-08-28 15:14:50 -07:00
f7345a8a54
Improve full_disk_clone definition 2023-08-26 17:39:27 -07:00
075a0d8541
Ensure relocate_backup_gpt() is run last
Addresses issue #220
2023-08-26 17:38:27 -07:00
5147a4105f
Simplify finalization menu
Addresses issue #220
2023-08-26 16:57:33 -07:00
d933ec6415
Ignore some unused function arguments 2023-08-26 16:55:25 -07:00
f5681a93d8
Add finalization menu to ddrescue-tui
Addresses issue #220
2023-08-26 16:54:53 -07:00
dbe4a342cc
Fix source_parts usage
Addresses issue #221
2023-08-26 14:30:22 -07:00
460fd9c952
Fix misc typos 2023-08-20 16:09:47 -07:00
7603b93338
Remove temp variable from set_mode() 2023-08-13 20:47:55 -07:00
42720d322b
Move build_block_pair_report() to block_pair.py 2023-08-13 20:47:15 -07:00
6bef46bb4d
Move more logic into wk/clone/block_pair.py 2023-08-13 20:39:23 -07:00
670619b65e
Use new log.format_log_path sub_dir option
This removes the need to import time
2023-08-13 19:47:32 -07:00
b9c4c9c32f
Add sub_dir option to format_log_path 2023-08-13 19:36:38 -07:00
e7642bdc63
Move image sections to wk/clone/image.py 2023-08-13 17:46:31 -07:00
a12995b37d
Move ddrescue classes to separate files 2023-08-13 17:34:14 -07:00
ee34e692dd
Update source/dest earlier in the workflow
Addresses issue #219
2023-08-13 16:31:23 -07:00
2134536960
Remove journalctl alias 2023-08-13 16:15:29 -07:00
47ccd7dd91
Pre-compile Python scripts in build_linux 2023-08-05 14:50:49 -07:00
24e4f7ddcc
Fix pickling ddrescue State() 2023-07-16 18:07:08 -07:00
bddf47816f
Remove duplicate log setup 2023-07-16 17:46:38 -07:00
0c1c65182c
Fix HW Diags test selections for teststations 2023-07-12 12:48:31 -07:00
cda5aee714
Add option to disable password expiration 2023-07-11 11:12:12 -07:00
7c16d13f65
Generate test maps at runtime in ddrescue-tui 2023-07-09 13:58:59 -07:00
840008d8cd
Specify text encoding for zero-fill map file 2023-07-09 13:23:21 -07:00
090a9f2b96
Update ddrescue-tui logging handling 2023-07-09 00:16:46 -07:00
4467369811
Add finalization options to ddrescue-tui
Specifically to zero-fill any gaps from the clone,
to fill the destination with zeros to the end of the device,
and/or to relocate the GPT to the end of the device.
2023-07-08 23:27:05 -07:00
a20fdf7bd3
Only show destination SMART data if present.
Also avoids crash when imaging
2023-07-08 18:58:05 -07:00
df1d2b713f
Simplify _poweroff_source_drive() 2023-07-08 18:37:42 -07:00
4a34f5477d
Add delay to TUI() initialization
Avoids issue where the main menu is printed before the layout is fully
set causing the first few lines to be hidden by the title pane.
2023-07-08 18:32:31 -07:00
0ace951380
Update run_program to avoid linting warnings 2023-07-08 18:10:59 -07:00
6a1cf98d0b
Terminate ddrescue directly instead 2023-07-08 18:10:14 -07:00
8f14fd2442
Fix SMART attribute tracking
Since we've moved to delayed SMART attribute updates we need to set
initial_attributes after we first check the SMART data instead of at
object creation time.
2023-07-08 18:07:55 -07:00
a78a077bdf
Set max idle cutoff to 70* C
Addresses issue #204
2023-07-05 15:51:50 -07:00
d101ec627f
Fix off-by-one bug in tmux.fix_layout()
If resizing both the title and info groups the second group was starting
at a lower initial width.
2023-07-05 15:48:31 -07:00
408a0c6114
Update TUI layout handling
The right column is now created first so the title, info,
current, and worker panes are all in the same "container"
2023-07-05 15:46:27 -07:00
ebd1bbda18
Show SMART data for both devices in ddrescue-tui 2023-07-05 15:00:01 -07:00
7499639c5c
Drop sat,auto detection for smartctl
This was needed twofold.  First is that it was not working as expected
for some time.  Second is that it conflicts with the delayed attribute
updating needed for faster WKClone menus.
2023-07-05 14:57:54 -07:00
d6f3455236
Misc update 2023-07-03 21:27:41 -07:00
55d752dd8b
Use checkmarks in Menu() under ConEmu 2023-07-03 20:45:44 -07:00
f8fc38a78b
Disable highlighting new apps in OpenShell 2023-07-03 20:31:46 -07:00
895d8d2f0a
Disable Bing search desktop widget 2023-07-03 20:25:23 -07:00
815cfde84a
Refactor check_mprime_results() to use sets 2023-07-03 20:16:37 -07:00
9a7fdba3f9
Add warning if cooldown temp is too high vs idle
Addresses issue #204
2023-07-03 20:15:03 -07:00
f9a6850c1a
Split CPU & Cooling tests into separate functions
Addresses issue #204
2023-07-02 15:10:22 -07:00
172f00e4e9
Adjust type hints for NonBlockingStreamReader() 2023-07-02 14:05:20 -07:00
86203a4b86
Use slots for all dataclasses
The minimum Python version was bumped to 3.10 so this is now safe.
2023-06-29 13:48:34 -06:00
8e234ce0cd
Add menu entry for MS Store updates in Auto Setup
Addresses issue #216
2023-06-26 08:20:15 -07:00
9689dcfeab
Update source URLs 2023-06-25 04:10:08 -07:00
cafa2c24fb
Switch to winget where appropriate in Auto Setup
NOTE: Winget is not used for Firefox, LibreOffice, or Open Shell.
This was done because we need more fine-tuned control of the process.
2023-06-25 02:40:43 -07:00
3ff61e9948
Add winget import support 2023-06-25 02:22:04 -07:00
9980dab27b
Add initial winget support 2023-06-25 02:21:26 -07:00
55ce4d8ded
Fix Python dependencies (again) 2023-06-24 23:04:28 -07:00
662f8c1254
Fix bug when running a PowerShell script elevated 2023-06-24 20:32:55 -07:00
d34df7ae07
Add/Update Python dependencies for build_windows 2023-06-24 19:44:20 -07:00
dfcc717048
Open both Microsoft Store and Windows updates
Addresses issue #216
2023-06-24 19:35:12 -07:00
21cbe5d445
Show filesystem type in select_disk_parts() 2023-06-24 18:58:06 -07:00
d94e9097b7
Reduce ESP size to 260MiB 2023-06-24 18:56:31 -07:00
228a5f640e
Adjust TRIM warning message 2023-06-24 18:56:03 -07:00
acd484f891
Check for TRIM in HW Diagnostics and ddrescue-tui
Addresses issue #212
2023-06-17 20:45:44 -07:00
d958945fe8
Relaunch ddrescueview when resuming clone 2023-06-17 20:13:32 -07:00
3e10f2cb8c
Reset layout when aborting HW diagnostics 2023-06-17 18:56:08 -07:00
9810c630f6
Ensure worker panes are added in the proper order 2023-06-17 18:37:56 -07:00
c3bf5f6730
Avoid mixing types for HW Diags main menu 2023-06-17 18:30:33 -07:00
c63b388f81
Small linting refactor 2023-06-17 18:25:12 -07:00
20a0881421
Refactor tmux.fix_layout()
The new code better determines all sizes with splits taken into account.
The non-perfect divisions are also considered when splitting
horizontally.
2023-06-17 18:23:09 -07:00
203ad715e0
Refactor ddrescue-tui source/dest selection
- Re-enables taking images instead of direct cloning!
- Removed some safety checks for clearer code
- We avoid a second scan by reusing the disk_menu object
2023-06-11 15:48:58 -07:00
986c870090
Move ddrescue-tui menus to a separate file 2023-06-10 21:50:56 -07:00
4feb15182e
Rework SMART self-test sections (again)
- Use results from self-test log rather than self-test details
- Include more result details in more scenarios
- Only add self-test results to the report to avoid
  duplicate/conflicting info
- Add check if test started but didn't finish (again?)
2023-06-10 18:59:19 -07:00
88d3ade64d
Avoid background crash when fixing the tmux layout 2023-06-10 18:05:13 -07:00
4202d3c1dc
Adjust cli.ask() log formatting 2023-06-10 17:58:23 -07:00
bcb9228234
Add missing package 2023-06-10 17:56:21 -07:00
f2ab06374b
Revert "Suppress warnings when using tail in tmux"
This reverts commit 3334638a2c.
2023-06-10 17:55:57 -07:00
a2c41fbaf2
Fix destination selection and title pane handling 2023-06-04 19:24:27 -07:00
7e6cfa1896
Add more type hints to ddrescue-tui 2023-06-04 18:54:16 -07:00
13e14e6734
Avoid dangerous default value 2023-06-04 18:13:18 -07:00
45a7f84e19
Restrict journal messages in ddrescue-tui 2023-06-04 18:11:14 -07:00
86f748c599
Clear ddrescue pane when resizing
This replaces the clear every 30s/60s/etc.  It's only enabled while
ddrescue is running to prevent clearing warning messages if printed.
2023-06-04 18:08:59 -07:00
becc564269
Use new TUI layout in ddrescue-tui 2023-06-04 17:43:02 -07:00
7ab6ccbd36
Avoid setting percent to None in tui.py 2023-06-03 18:58:46 -07:00
8e7d202c32
Add reset_title_pane() to tui 2023-06-03 18:58:29 -07:00
05de5c7294
Add type hints to BlockPair 2023-06-03 18:07:30 -07:00
fc2b90a2c0
Raise CPU_CRITICAL_TEMP to 100*C 2023-05-29 17:48:24 -07:00
de7993c39c
Fix type hint for get_known_disk_attributes() 2023-05-29 17:47:58 -07:00
dbb606601d
Drop test() function 2023-05-29 17:32:05 -07:00
1dc22d5991
Remove unused section in layout_needs_fixed() 2023-05-29 17:31:03 -07:00
f50ea711e6
Refactor wk.clone.ddrescue.get_object() 2023-05-29 17:29:02 -07:00
2cce572acf
Drop OrderedDict usage in favor of standard dicts
Python 3.7+ guarantees insertion order is preserved and we (currently)
require 3.10+
2023-05-29 17:25:48 -07:00
a253fdc80f
Add type hints to auto_repairs and auto_setup 2023-05-29 17:09:32 -07:00
386a8b7000
Merge branch 'type-hinting' into dev 2023-05-29 16:26:16 -07:00
a5eb64a055
Add type hints to class instance variables 2023-05-29 16:25:37 -07:00
c009ab2d41
Add even more type hints to function arguments 2023-05-29 16:04:58 -07:00
1bfdb14be4
Refactor color_string() 2023-05-29 14:49:21 -07:00
bf9d994675
Add more type hints to function arguments 2023-05-29 14:01:29 -07:00
f654052f1d
Fix typo 2023-05-29 13:42:45 -07:00
12326a5e2c
Use new Union syntax
This bumps the minimum Python version to 3.10
2023-05-29 12:35:40 -07:00
171cd0019e
Add type hints to function arguments 2023-05-28 20:50:38 -07:00
62edaac25a
Add type hints to functions 2023-05-28 20:09:54 -07:00
60d08a189d
Merge branch 'dev' into type-hinting 2023-05-27 21:15:23 -07:00
69832eda5d
Remove duplicate function wk.log.get_log_filepath 2023-05-27 21:12:27 -07:00
534f258846
Add some type hints 2023-05-27 20:05:03 -07:00
0126452bf1
Merge branch 'ui-split' into dev 2023-05-27 19:50:49 -07:00
f19c4b2422
Update self-test data before checking result
Addresses #209
2023-05-22 20:59:10 -07:00
59d89575ed
Refactor SMART self-test checks
- Preserve TimedOut status
- Adds last self-test result to notes (if present and result is unknown)
2023-05-21 14:52:28 -07:00
147 changed files with 7714 additions and 7106 deletions

View file

@ -1,4 +1,4 @@
Copyright (c) 2021 Alan Mason Copyright (c) 2023 Alan Mason
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:

View file

@ -242,7 +242,7 @@ if defined L_NCMD (
rem use Powershell's window instead of %CON% rem use Powershell's window instead of %CON%
echo UAC.ShellExecute "%POWERSHELL%", "%ps_args% -File "%script%"", "", "runas", 3 >> "%bin%\tmp\Elevate.vbs" echo UAC.ShellExecute "%POWERSHELL%", "%ps_args% -File "%script%"", "", "runas", 3 >> "%bin%\tmp\Elevate.vbs"
) else ( ) else (
echo UAC.ShellExecute "%CON%", "-run %POWERSHELL% %ps_args% -File "%script%" -new_console:n", "", "runas", 1 >> "%bin%\tmp\Elevate.vbs" echo UAC.ShellExecute "%CON%", "-run %POWERSHELL% %ps_args% -File "^"%script%^"" -new_console:n", "", "runas", 1 >> "%bin%\tmp\Elevate.vbs"
) )
rem Run rem Run

View file

@ -1,6 +1,8 @@
"""WizardKit: Auto Repair Tool""" """WizardKit: Auto Repair Tool"""
# vim: sts=2 sw=2 ts=2 # vim: sts=2 sw=2 ts=2
from typing import Any
import wk import wk
@ -8,9 +10,14 @@ import wk
REBOOT_STR = wk.ui.ansi.color_string('Reboot', 'YELLOW') REBOOT_STR = wk.ui.ansi.color_string('Reboot', 'YELLOW')
class MenuEntry(): class MenuEntry():
"""Simple class to allow cleaner code below.""" """Simple class to allow cleaner code below."""
def __init__(self, name, function=None, selected=True, **kwargs): def __init__(
self.name = name self,
self.details = { name: str,
function: str | None = None,
selected: bool = True,
**kwargs):
self.name: str = name
self.details: dict[str, Any] = {
'Function': function, 'Function': function,
'Selected': selected, 'Selected': selected,
**kwargs, **kwargs,
@ -54,14 +61,14 @@ BASE_MENUS = {
), ),
'Manual Steps': ( 'Manual Steps': (
MenuEntry('AdwCleaner', 'auto_adwcleaner'), MenuEntry('AdwCleaner', 'auto_adwcleaner'),
MenuEntry('UninstallView', 'auto_uninstallview'), MenuEntry('Bulk Crap Uninstaller', 'auto_bcuninstaller'),
MenuEntry('Enable Windows Updates', 'auto_windows_updates_enable'), MenuEntry('Enable Windows Updates', 'auto_windows_updates_enable'),
), ),
}, },
'Options': ( 'Options': (
MenuEntry('Kill Explorer', selected=False), MenuEntry('Kill Explorer', selected=False),
MenuEntry('Run AVRemover (once)'),
MenuEntry('Run RKill'), MenuEntry('Run RKill'),
MenuEntry('Run TDSSKiller (once)'),
MenuEntry('Sync Clock'), MenuEntry('Sync Clock'),
MenuEntry('Use Autologon', selected=False), MenuEntry('Use Autologon', selected=False),
), ),
@ -76,7 +83,6 @@ PRESETS = {
'Default': { # Will be expanded at runtime using BASE_MENUS 'Default': { # Will be expanded at runtime using BASE_MENUS
'Options': ( 'Options': (
'Run RKill', 'Run RKill',
'Run TDSSKiller (once)',
'Sync Clock', 'Sync Clock',
), ),
}, },

View file

@ -1,15 +1,22 @@
"""WizardKit: Auto System Setup Tool""" """WizardKit: Auto System Setup Tool"""
# vim: sts=2 sw=2 ts=2 # vim: sts=2 sw=2 ts=2
from typing import Any
import wk import wk
# Classes # Classes
class MenuEntry(): class MenuEntry():
"""Simple class to allow cleaner code below.""" """Simple class to allow cleaner code below."""
def __init__(self, name, function=None, selected=True, **kwargs): def __init__(
self.name = name self,
self.details = { name: str,
function: str | None = None,
selected: bool = True,
**kwargs):
self.name: str = name
self.details: dict[str, Any] = {
'Function': function, 'Function': function,
'Selected': selected, 'Selected': selected,
**kwargs, **kwargs,
@ -26,14 +33,17 @@ BASE_MENUS = {
MenuEntry('Set Custom Power Plan', 'auto_set_custom_power_plan'), MenuEntry('Set Custom Power Plan', 'auto_set_custom_power_plan'),
), ),
'Install Software': ( 'Install Software': (
MenuEntry('Visual C++ Runtimes', 'auto_install_vcredists'), MenuEntry('Winget', 'auto_install_winget'),
MenuEntry('Firefox', 'auto_install_firefox'), MenuEntry('Firefox', 'auto_install_firefox'),
MenuEntry('LibreOffice', 'auto_install_libreoffice', selected=False), MenuEntry('LibreOffice', 'auto_install_libreoffice', selected=False),
MenuEntry('Open Shell', 'auto_install_open_shell'), MenuEntry('Open Shell', 'auto_install_open_shell'),
MenuEntry('Software Bundle', 'auto_install_software_bundle'), MenuEntry('Software Bundle', 'auto_install_software_bundle'),
MenuEntry('Software Upgrades', 'auto_install_software_upgrades'),
MenuEntry('Visual C++ Runtimes', 'auto_install_vcredists'),
), ),
'Configure System': ( 'Configure System': (
MenuEntry('Open Shell', 'auto_config_open_shell'), MenuEntry('Open Shell', 'auto_config_open_shell'),
MenuEntry('Disable Password Expiration', 'auto_disable_password_expiration'),
MenuEntry('Enable BSoD MiniDumps', 'auto_enable_bsod_minidumps'), MenuEntry('Enable BSoD MiniDumps', 'auto_enable_bsod_minidumps'),
MenuEntry('Enable RegBack', 'auto_enable_regback'), MenuEntry('Enable RegBack', 'auto_enable_regback'),
MenuEntry('Enable System Restore', 'auto_system_restore_enable'), MenuEntry('Enable System Restore', 'auto_system_restore_enable'),
@ -61,6 +71,7 @@ BASE_MENUS = {
'Run Programs': ( 'Run Programs': (
MenuEntry('Device Manager', 'auto_open_device_manager'), MenuEntry('Device Manager', 'auto_open_device_manager'),
MenuEntry('HWiNFO Sensors', 'auto_open_hwinfo_sensors'), MenuEntry('HWiNFO Sensors', 'auto_open_hwinfo_sensors'),
MenuEntry('Microsoft Store Updates', 'auto_open_microsoft_store_updates'),
MenuEntry('Snappy Driver Installer', 'auto_open_snappy_driver_installer_origin'), MenuEntry('Snappy Driver Installer', 'auto_open_snappy_driver_installer_origin'),
MenuEntry('Windows Activation', 'auto_open_windows_activation'), MenuEntry('Windows Activation', 'auto_open_windows_activation'),
MenuEntry('Windows Updates', 'auto_open_windows_updates'), MenuEntry('Windows Updates', 'auto_open_windows_updates'),
@ -90,6 +101,9 @@ PRESETS = {
'Install Software': ( 'Install Software': (
'Firefox', # Needed to handle profile upgrade nonsense 'Firefox', # Needed to handle profile upgrade nonsense
), ),
'Run Programs': (
'Microsoft Store Updates',
),
'System Summary': ( 'System Summary': (
'Operating System', 'Operating System',
'Windows Activation', 'Windows Activation',

13
scripts/check_av.ps1 Normal file
View file

@ -0,0 +1,13 @@
# WizardKit: Check Antivirus
#Requires -Version 3.0
if (Test-Path Env:\DEBUG) {
Set-PSDebug -Trace 1
}
$Host.UI.RawUI.WindowTitle = "WizardKit: Check Antivirus"
$Host.UI.RawUI.BackgroundColor = "black"
$Host.UI.RawUI.ForegroundColor = "white"
$ProgressPreference = "SilentlyContinue"
# Main
Get-CimInstance -Namespace "root\SecurityCenter2" -ClassName AntivirusProduct | select displayName,productState | ConvertTo-Json

View file

@ -0,0 +1,13 @@
# WizardKit: Check Partition Alignment
#Requires -Version 3.0
if (Test-Path Env:\DEBUG) {
Set-PSDebug -Trace 1
}
$Host.UI.RawUI.WindowTitle = "WizardKit: Check Partition Alignment"
$Host.UI.RawUI.BackgroundColor = "black"
$Host.UI.RawUI.ForegroundColor = "white"
$ProgressPreference = "SilentlyContinue"
# Main
Get-CimInstance -Query "Select * from Win32_DiskPartition" | select Name,Size,StartingOffset | ConvertTo-Json

View file

@ -2,19 +2,10 @@
"""WizardKit: ddrescue TUI""" """WizardKit: ddrescue TUI"""
# vim: sts=2 sw=2 ts=2 # vim: sts=2 sw=2 ts=2
from docopt import docopt
import wk import wk
if __name__ == '__main__': if __name__ == '__main__':
try:
docopt(wk.clone.ddrescue.DOCSTRING)
except SystemExit:
print('')
wk.ui.cli.pause('Press Enter to exit...')
raise
try: try:
wk.clone.ddrescue.main() wk.clone.ddrescue.main()
except SystemExit: except SystemExit:

View file

@ -0,0 +1,13 @@
# WizardKit: Disable Password Expiration (Local Accounts)
#Requires -Version 3.0
if (Test-Path Env:\DEBUG) {
Set-PSDebug -Trace 1
}
$Host.UI.RawUI.WindowTitle = "Disable Password Expiration"
$Host.UI.RawUI.BackgroundColor = "black"
$Host.UI.RawUI.ForegroundColor = "white"
$ProgressPreference = "SilentlyContinue"
# Main
Get-LocalUser | Set-LocalUser -PasswordNeverExpires $true

View file

@ -5,9 +5,17 @@ python.exe -i embedded_python_env.py
""" """
# vim: sts=2 sw=2 ts=2 # vim: sts=2 sw=2 ts=2
import pickle
import wk import wk
# Functions
def load_state():
with open('debug/state.pickle', 'rb') as f:
return pickle.load(f)
# Main
wk.ui.cli.print_colored( wk.ui.cli.print_colored(
(wk.cfg.main.KIT_NAME_FULL, ': ', 'Debug Console'), (wk.cfg.main.KIT_NAME_FULL, ': ', 'Debug Console'),
('GREEN', None, 'YELLOW'), ('GREEN', None, 'YELLOW'),

View file

@ -2,19 +2,10 @@
"""WizardKit: Hardware Diagnostics""" """WizardKit: Hardware Diagnostics"""
# vim: sts=2 sw=2 ts=2 # vim: sts=2 sw=2 ts=2
from docopt import docopt
import wk import wk
if __name__ == '__main__': if __name__ == '__main__':
try:
docopt(wk.hw.diags.DOCSTRING)
except SystemExit:
print('')
wk.ui.cli.pause('Press Enter to exit...')
raise
try: try:
wk.hw.diags.main() wk.hw.diags.main()
except SystemExit: except SystemExit:

View file

@ -7,7 +7,7 @@ import platform
import wk import wk
def main(): def main() -> None:
"""Show sensor data on screen.""" """Show sensor data on screen."""
sensors = wk.hw.sensors.Sensors() sensors = wk.hw.sensors.Sensors()
if platform.system() == 'Darwin': if platform.system() == 'Darwin':

View file

@ -0,0 +1,37 @@
# WizardKit: Install winget (if needed)
#Requires -Version 3.0
if (Test-Path Env:\DEBUG) {
Set-PSDebug -Trace 1
}
$Host.UI.RawUI.WindowTitle = "WizardKit: Winget installer"
$Host.UI.RawUI.BackgroundColor = "black"
$Host.UI.RawUI.ForegroundColor = "white"
$ProgressPreference = "SilentlyContinue"
# STATIC VARIABLES
$EXIT_OK = 0
$EXIT_INSTALLED = 1
$EXIT_FAILED_TO_INSTALL = 2
# Main
$NeedsInstalled = $false
try {
$_ = $(winget --version)
}
catch {
$NeedsInstalled = $true
}
# Install
if (! $NeedsInstalled) {
exit $EXIT_INSTALLED
}
try {
Add-AppxPackage -ErrorAction Stop -RegisterByFamilyName -MainPackage Microsoft.DesktopAppInstaller_8wekyb3d8bbwe
}
catch {
exit $EXIT_FAILED_TO_INSTALL
}
exit $EXIT_OK

View file

@ -0,0 +1,7 @@
#!/bin/bash
#
## Monitor journal log for data recovery related events
echo -e 'Monitoring journal output...\n'
journalctl -kf \
| grep -Ei --color=always 'ata|nvme|scsi|sd[a..z]+|usb|comreset|critical|error'

View file

@ -1,6 +1,8 @@
"""WizardKit: Launch Snappy Driver Installer Origin""" """WizardKit: Launch Snappy Driver Installer Origin"""
# vim: sts=2 sw=2 ts=2 # vim: sts=2 sw=2 ts=2
from subprocess import CompletedProcess
import wk import wk
from wk.cfg.net import SDIO_SERVER from wk.cfg.net import SDIO_SERVER
@ -20,7 +22,7 @@ SDIO_REMOTE_PATH = wk.io.get_path_obj(
) )
# Functions # Functions
def try_again(): def try_again() -> bool:
"""Ask to try again or quit.""" """Ask to try again or quit."""
if wk.ui.cli.ask(' Try again?'): if wk.ui.cli.ask(' Try again?'):
return True return True
@ -29,10 +31,10 @@ def try_again():
return False return False
def use_network_sdio(): def use_network_sdio() -> bool:
"""Try to mount SDIO server.""" """Try to mount SDIO server."""
use_network = False use_network = False
def _mount_server(): def _mount_server() -> CompletedProcess:
print('Connecting to server... (Press CTRL+c to use local copy)') print('Connecting to server... (Press CTRL+c to use local copy)')
return wk.net.mount_network_share(SDIO_SERVER, read_write=False) return wk.net.mount_network_share(SDIO_SERVER, read_write=False)
@ -72,6 +74,14 @@ if __name__ == '__main__':
log_dir = wk.log.format_log_path(tool=True).parent log_dir = wk.log.format_log_path(tool=True).parent
USE_NETWORK = False USE_NETWORK = False
# Windows 11 workaround
if wk.os.win.OS_VERSION == 11:
appid_services = ['appid', 'appidsvc', 'applockerfltr']
for svc in appid_services:
wk.os.win.stop_service(svc)
if any([wk.os.win.get_service_status(s) != 'stopped' for s in appid_services]):
raise wk.std.GenericWarning('Failed to stop AppID services')
# Try to mount server # Try to mount server
try: try:
USE_NETWORK = use_network_sdio() USE_NETWORK = use_network_sdio()

View file

@ -5,10 +5,12 @@ import json
import re import re
import subprocess import subprocess
from typing import Any
CPU_REGEX = re.compile(r'(core|k\d+)temp', re.IGNORECASE) CPU_REGEX = re.compile(r'(core|k\d+)temp', re.IGNORECASE)
NON_TEMP_REGEX = re.compile(r'^(fan|in|curr)', re.IGNORECASE) NON_TEMP_REGEX = re.compile(r'^(fan|in|curr)', re.IGNORECASE)
def get_data(): def get_data() -> dict[Any, Any]:
cmd = ('sensors', '-j') cmd = ('sensors', '-j')
data = {} data = {}
raw_data = [] raw_data = []
@ -38,7 +40,7 @@ def get_data():
return data return data
def get_max_temp(data): def get_max_temp(data) -> str:
cpu_temps = [] cpu_temps = []
max_cpu_temp = '??° C' max_cpu_temp = '??° C'
for adapter, sources in data.items(): for adapter, sources in data.items():

View file

@ -8,7 +8,7 @@ import wk
# Functions # Functions
def main(): def main() -> None:
"""Mount all volumes and show results.""" """Mount all volumes and show results."""
wk.ui.cli.print_standard(f'{wk.cfg.main.KIT_NAME_FULL}: Volume mount tool') wk.ui.cli.print_standard(f'{wk.cfg.main.KIT_NAME_FULL}: Volume mount tool')
wk.ui.cli.print_standard(' ') wk.ui.cli.print_standard(' ')

View file

@ -6,7 +6,7 @@ import wk
# Functions # Functions
def main(): def main() -> None:
"""Attempt to mount backup shares and print report.""" """Attempt to mount backup shares and print report."""
wk.ui.cli.print_info('Mounting Backup Shares') wk.ui.cli.print_info('Mounting Backup Shares')
report = wk.net.mount_backup_shares() report = wk.net.mount_backup_shares()

View file

@ -6,7 +6,7 @@ import wk
# Functions # Functions
def main(): def main() -> None:
"""Attempt to mount backup shares and print report.""" """Attempt to mount backup shares and print report."""
wk.ui.cli.print_info('Unmounting Backup Shares') wk.ui.cli.print_info('Unmounting Backup Shares')
report = wk.net.unmount_backup_shares() report = wk.net.unmount_backup_shares()
@ -15,7 +15,7 @@ def main():
line = f' {line}' line = f' {line}'
if 'Not mounted' in line: if 'Not mounted' in line:
color = 'YELLOW' color = 'YELLOW'
print(wk.ansi.color_string(line, color)) print(wk.ui.ansi.color_string(line, color))
if __name__ == '__main__': if __name__ == '__main__':

View file

@ -25,7 +25,7 @@ if PLATFORM not in ('macOS', 'Linux'):
# Functions # Functions
def main(): def main() -> None:
"""Upload logs for review.""" """Upload logs for review."""
lines = [] lines = []
try_and_print = wk.ui.cli.TryAndPrint() try_and_print = wk.ui.cli.TryAndPrint()
@ -60,7 +60,7 @@ def main():
raise SystemExit(1) raise SystemExit(1)
def upload_log_dir(reason='Testing'): def upload_log_dir(reason='Testing') -> None:
"""Upload compressed log_dir to the crash server.""" """Upload compressed log_dir to the crash server."""
server = wk.cfg.net.CRASH_SERVER server = wk.cfg.net.CRASH_SERVER
dest = pathlib.Path(f'~/{reason}_{NOW.strftime("%Y-%m-%dT%H%M%S%z")}.txz') dest = pathlib.Path(f'~/{reason}_{NOW.strftime("%Y-%m-%dT%H%M%S%z")}.txz')

View file

@ -21,7 +21,7 @@ from . import ui
# Check env # Check env
if version_info < (3, 7): if version_info < (3, 10):
# Unsupported # Unsupported
raise RuntimeError( raise RuntimeError(
'This package is unsupported on Python ' 'This package is unsupported on Python '

View file

@ -7,7 +7,6 @@ from . import log
from . import main from . import main
from . import music from . import music
from . import net from . import net
from . import python
from . import repairs from . import repairs
from . import setup from . import setup
from . import sources from . import sources

View file

@ -1,16 +1,14 @@
"""WizardKit: Config - ddrescue""" """WizardKit: Config - ddrescue"""
# vim: sts=2 sw=2 ts=2 # vim: sts=2 sw=2 ts=2
from collections import OrderedDict
# Layout # Layout
TMUX_SIDE_WIDTH = 21 TMUX_SIDE_WIDTH = 21
TMUX_LAYOUT = OrderedDict({ TMUX_LAYOUT = {
'Source': {'height': 2, 'Check': True}, 'Source': {'height': 2, 'Check': True},
'Started': {'width': TMUX_SIDE_WIDTH, 'Check': True}, 'Started': {'width': TMUX_SIDE_WIDTH, 'Check': True},
'Progress': {'width': TMUX_SIDE_WIDTH, 'Check': True}, 'Progress': {'width': TMUX_SIDE_WIDTH, 'Check': True},
}) }
# ddrescue # ddrescue
AUTO_PASS_THRESHOLDS = { AUTO_PASS_THRESHOLDS = {
@ -39,7 +37,7 @@ DDRESCUE_SETTINGS = {
'--retry-passes': {'Selected': True, 'Value': '0', }, '--retry-passes': {'Selected': True, 'Value': '0', },
'--reverse': {'Selected': False, }, '--reverse': {'Selected': False, },
'--skip-size': {'Selected': True, 'Value': '0.001,0.02', }, # Percentages of source size '--skip-size': {'Selected': True, 'Value': '0.001,0.02', }, # Percentages of source size
'--test-mode': {'Selected': False, 'Value': 'test.map', }, '--test-mode': {'Selected': False, },
'--timeout': {'Selected': True, 'Value': '30m', }, '--timeout': {'Selected': True, 'Value': '30m', },
'-vvvv': {'Selected': True, 'Hidden': True, }, '-vvvv': {'Selected': True, 'Hidden': True, },
}, },

View file

@ -20,8 +20,13 @@ BADBLOCKS_REGEX = re.compile(
) )
BADBLOCKS_RESULTS_REGEX = re.compile(r'^(.*?)\x08.*\x08(.*)') BADBLOCKS_RESULTS_REGEX = re.compile(r'^(.*?)\x08.*\x08(.*)')
BADBLOCKS_SKIP_REGEX = re.compile(r'^(Checking|\[)', re.IGNORECASE) BADBLOCKS_SKIP_REGEX = re.compile(r'^(Checking|\[)', re.IGNORECASE)
CPU_CRITICAL_TEMP = 99 CPU_TEMPS = {
CPU_FAILURE_TEMP = 90 'Cooling Delta': 25,
'Cooling Low Cutoff': 50,
'Critical': 100,
'Idle Delta': 25,
'Idle High': 70,
}
CPU_TEST_MINUTES = 7 CPU_TEST_MINUTES = 7
IO_GRAPH_WIDTH = 40 IO_GRAPH_WIDTH = 40
IO_ALT_TEST_SIZE_FACTOR = 0.01 IO_ALT_TEST_SIZE_FACTOR = 0.01

View file

@ -15,11 +15,12 @@ LAUNCHERS = {
'L_ITEM': 'auto_repairs.py', 'L_ITEM': 'auto_repairs.py',
'L_ELEV': 'True', 'L_ELEV': 'True',
}, },
'2) Windows Updates': { '2) Store & Windows Updates': {
'L_TYPE': 'Executable', 'L_TYPE': 'Executable',
'L_PATH': r'%SystemRoot%\System32', 'L_PATH': r'%SystemRoot%\System32',
'L_ITEM': 'control.exe', 'L_ITEM': 'control.exe',
'L_ARGS': 'update', 'L_ARGS': 'update',
'Extra Code': ['explorer ms-windows-store:updates'],
}, },
'3) Snappy Driver Installer Origin': { '3) Snappy Driver Installer Origin': {
'L_TYPE': 'PyScript', 'L_TYPE': 'PyScript',
@ -68,6 +69,12 @@ LAUNCHERS = {
'L_PATH': 'BlueScreenView', 'L_PATH': 'BlueScreenView',
'L_ITEM': 'BlueScreenView.exe', 'L_ITEM': 'BlueScreenView.exe',
}, },
'BCUninstaller': {
'L_TYPE': 'Executable',
'L_PATH': 'BCUninstaller',
'L_ITEM': 'BCUninstaller.exe',
'L_ELEV': 'True',
},
'ConEmu (as ADMIN)': { 'ConEmu (as ADMIN)': {
'L_TYPE': 'Executable', 'L_TYPE': 'Executable',
'L_PATH': 'ConEmu', 'L_PATH': 'ConEmu',
@ -97,6 +104,18 @@ LAUNCHERS = {
'if /i "%PROCESSOR_ARCHITECTURE%" == "AMD64" set "ARCH=64"', 'if /i "%PROCESSOR_ARCHITECTURE%" == "AMD64" set "ARCH=64"',
], ],
}, },
'Device Cleanup': {
'L_TYPE': 'Executable',
'L_PATH': 'DeviceCleanup',
'L_ITEM': 'DeviceCleanup.exe',
'L_ELEV': 'True',
},
'Display Driver Uninstaller': {
'L_TYPE': 'Executable',
'L_PATH': 'DDU',
'L_ITEM': 'Display Driver Uninstaller.exe',
'L_ELEV': 'True',
},
'ERUNT': { 'ERUNT': {
'L_TYPE': 'Executable', 'L_TYPE': 'Executable',
'L_PATH': 'erunt', 'L_PATH': 'erunt',
@ -248,12 +267,6 @@ LAUNCHERS = {
'L_PATH': 'PuTTY', 'L_PATH': 'PuTTY',
'L_ITEM': 'PUTTY.EXE', 'L_ITEM': 'PUTTY.EXE',
}, },
'UninstallView': {
'L_TYPE': 'Executable',
'L_PATH': 'UninstallView',
'L_ITEM': 'UninstallView.exe',
'L_ELEV': 'True',
},
'WizTree': { 'WizTree': {
'L_TYPE': 'Executable', 'L_TYPE': 'Executable',
'L_PATH': 'WizTree', 'L_PATH': 'WizTree',

View file

@ -1,14 +0,0 @@
"""WizardKit: Config - Python"""
# vim: sts=2 sw=2 ts=2
from sys import version_info
DATACLASS_DECORATOR_KWARGS = {}
if version_info.major >= 3 and version_info.minor >= 10:
DATACLASS_DECORATOR_KWARGS['slots'] = True
if __name__ == '__main__':
print("This file is not meant to be called directly.")
# vim: sts=2 sw=2 ts=2

View file

@ -29,6 +29,14 @@ REG_CHROME_UBLOCK_ORIGIN = {
) )
}, },
} }
REG_WINDOWS_BSOD_MINIDUMPS = {
'HKLM': {
# Enable small memory dumps
r'SYSTEM\CurrentControlSet\Control\CrashControl': (
('CrashDumpEnabled', 3, 'DWORD'),
)
}
}
REG_WINDOWS_EXPLORER = { REG_WINDOWS_EXPLORER = {
'HKLM': { 'HKLM': {
# Allow password sign-in for MS accounts # Allow password sign-in for MS accounts
@ -50,6 +58,10 @@ REG_WINDOWS_EXPLORER = {
r'Software\Policies\Microsoft\Windows\DataCollection': ( r'Software\Policies\Microsoft\Windows\DataCollection': (
('AllowTelemetry', 0, 'DWORD'), ('AllowTelemetry', 0, 'DWORD'),
), ),
# Disable floating Bing search widget
r'Software\Policies\Microsoft\Edge': (
('WebWidgetAllowed', 0, 'DWORD'),
),
# Disable Edge first run screen # Disable Edge first run screen
r'Software\Policies\Microsoft\MicrosoftEdge\Main': ( r'Software\Policies\Microsoft\MicrosoftEdge\Main': (
('PreventFirstRunPage', 1, 'DWORD'), ('PreventFirstRunPage', 1, 'DWORD'),
@ -113,6 +125,7 @@ REG_OPEN_SHELL_SETTINGS = {
('ShowedStyle2', 1, 'DWORD'), ('ShowedStyle2', 1, 'DWORD'),
), ),
r'Software\OpenShell\StartMenu\Settings': ( r'Software\OpenShell\StartMenu\Settings': (
('HighlightNew', 0, 'DWORD'),
('MenuStyle', 'Win7', 'SZ'), ('MenuStyle', 'Win7', 'SZ'),
('RecentPrograms', 'Recent', 'SZ'), ('RecentPrograms', 'Recent', 'SZ'),
('SkinW7', 'Fluent-Metro', 'SZ'), ('SkinW7', 'Fluent-Metro', 'SZ'),

View file

@ -22,49 +22,39 @@ SOURCES = {
'RKill': 'https://download.bleepingcomputer.com/grinler/rkill.exe', 'RKill': 'https://download.bleepingcomputer.com/grinler/rkill.exe',
'RegDelNull': 'https://live.sysinternals.com/RegDelNull.exe', 'RegDelNull': 'https://live.sysinternals.com/RegDelNull.exe',
'RegDelNull64': 'https://live.sysinternals.com/RegDelNull64.exe', 'RegDelNull64': 'https://live.sysinternals.com/RegDelNull64.exe',
'Software Bundle': 'https://ninite.com/.net4.8-7zip-chrome-edge-vlc/ninite.exe',
'TDSSKiller': 'https://media.kaspersky.com/utilities/VirusUtilities/EN/tdsskiller.exe',
# Visual C++ Runtimes: https://docs.microsoft.com/en-US/cpp/windows/latest-supported-vc-redist
'VCRedist_2012_x32': 'https://download.microsoft.com/download/1/6/B/16B06F60-3B20-4FF2-B699-5E9B7962F9AE/VSU_4/vcredist_x86.exe',
'VCRedist_2012_x64': 'https://download.microsoft.com/download/1/6/B/16B06F60-3B20-4FF2-B699-5E9B7962F9AE/VSU_4/vcredist_x64.exe',
'VCRedist_2013_x32': 'https://aka.ms/highdpimfc2013x86enu',
'VCRedist_2013_x64': 'https://aka.ms/highdpimfc2013x64enu',
'VCRedist_2022_x32': 'https://aka.ms/vs/17/release/vc_redist.x86.exe',
'VCRedist_2022_x64': 'https://aka.ms/vs/17/release/vc_redist.x64.exe',
# Build Kit # Build Kit
'AIDA64': 'https://download.aida64.com/aida64engineer675.zip', 'AIDA64': 'https://download.aida64.com/aida64engineer692.zip',
'Adobe Reader DC': 'https://ardownload2.adobe.com/pub/adobe/reader/win/AcrobatDC/2200220191/AcroRdrDC2200220191_en_US.exe', 'Adobe Reader DC': 'https://ardownload2.adobe.com/pub/adobe/reader/win/AcrobatDC/2300620360/AcroRdrDC2300620360_en_US.exe',
'Aria2': 'https://github.com/aria2/aria2/releases/download/release-1.36.0/aria2-1.36.0-win-32bit-build1.zip', 'Aria2': 'https://github.com/aria2/aria2/releases/download/release-1.36.0/aria2-1.36.0-win-32bit-build1.zip',
'Autoruns32': 'http://live.sysinternals.com/Autoruns.exe', 'Autoruns32': 'http://live.sysinternals.com/Autoruns.exe',
'Autoruns64': 'http://live.sysinternals.com/Autoruns64.exe', 'Autoruns64': 'http://live.sysinternals.com/Autoruns64.exe',
'BleachBit': 'https://download.bleachbit.org/BleachBit-4.4.2-portable.zip', 'BleachBit': 'https://download.bleachbit.org/BleachBit-4.4.2-portable.zip',
'BlueScreenView32': 'http://www.nirsoft.net/utils/bluescreenview.zip', 'BlueScreenView32': 'http://www.nirsoft.net/utils/bluescreenview.zip',
'BlueScreenView64': 'http://www.nirsoft.net/utils/bluescreenview-x64.zip', 'BlueScreenView64': 'http://www.nirsoft.net/utils/bluescreenview-x64.zip',
'BCUninstaller': 'https://github.com/Klocman/Bulk-Crap-Uninstaller/releases/download/v5.7/BCUninstaller_5.7_portable.zip',
'DDU': 'https://www.wagnardsoft.com/DDU/download/DDU%20v18.0.6.8.exe',
'ERUNT': 'http://www.aumha.org/downloads/erunt.zip', 'ERUNT': 'http://www.aumha.org/downloads/erunt.zip',
'Everything32': 'https://www.voidtools.com/Everything-1.4.1.1020.x86.zip', 'Everything32': 'https://www.voidtools.com/Everything-1.4.1.1024.x86.zip',
'Everything64': 'https://www.voidtools.com/Everything-1.4.1.1020.x64.zip', 'Everything64': 'https://www.voidtools.com/Everything-1.4.1.1024.x64.zip',
'FastCopy': 'https://ftp.vector.co.jp/75/32/2323/FastCopy4.2.0_installer.exe', 'FastCopy': 'https://github.com/FastCopyLab/FastCopyDist2/raw/main/FastCopy5.4.2_installer.exe',
'Fluent-Metro': 'https://github.com/bonzibudd/Fluent-Metro/releases/download/v1.5.3/Fluent-Metro_1.5.3.zip', 'Fluent-Metro': 'https://github.com/bonzibudd/Fluent-Metro/releases/download/v1.5.3/Fluent-Metro_1.5.3.zip',
'FurMark': 'https://geeks3d.com/dl/get/696', 'FurMark': 'https://geeks3d.com/dl/get/728',
'HWiNFO': 'https://www.sac.sk/download/utildiag/hwi_730.zip', 'HWiNFO': 'https://www.sac.sk/download/utildiag/hwi_764.zip',
'LibreOffice32': 'https://download.documentfoundation.org/libreoffice/stable/7.3.6/win/x86/LibreOffice_7.3.6_Win_x86.msi', 'LibreOffice32': 'https://download.documentfoundation.org/libreoffice/stable/7.6.2/win/x86/LibreOffice_7.6.2_Win_x86.msi',
'LibreOffice64': 'https://download.documentfoundation.org/libreoffice/stable/7.3.6/win/x86_64/LibreOffice_7.3.6_Win_x64.msi', 'LibreOffice64': 'https://download.documentfoundation.org/libreoffice/stable/7.6.2/win/x86_64/LibreOffice_7.6.2_Win_x86-64.msi',
'Macs Fan Control': 'https://www.crystalidea.com/downloads/macsfancontrol_setup.exe', 'Macs Fan Control': 'https://www.crystalidea.com/downloads/macsfancontrol_setup.exe',
'Neutron': 'http://keir.net/download/neutron.zip', 'Neutron': 'http://keir.net/download/neutron.zip',
'Notepad++': 'https://github.com/notepad-plus-plus/notepad-plus-plus/releases/download/v8.1.9.3/npp.8.1.9.3.portable.minimalist.7z', 'Notepad++': 'https://github.com/notepad-plus-plus/notepad-plus-plus/releases/download/v8.5.8/npp.8.5.8.portable.minimalist.7z',
'OpenShell': 'https://github.com/Open-Shell/Open-Shell-Menu/releases/download/v4.4.170/OpenShellSetup_4_4_170.exe', 'OpenShell': 'https://github.com/Open-Shell/Open-Shell-Menu/releases/download/v4.4.191/OpenShellSetup_4_4_191.exe',
'PuTTY': 'https://the.earth.li/~sgtatham/putty/latest/w32/putty.zip', 'PuTTY': 'https://the.earth.li/~sgtatham/putty/latest/w32/putty.zip',
'SDIO Torrent': 'https://www.glenn.delahoy.com/downloads/sdio/SDIO_Update.torrent', 'SDIO Torrent': 'https://www.glenn.delahoy.com/downloads/sdio/SDIO_Update.torrent',
'UninstallView32': 'https://www.nirsoft.net/utils/uninstallview.zip', 'WizTree': 'https://diskanalyzer.com/files/wiztree_4_15_portable.zip',
'UninstallView64': 'https://www.nirsoft.net/utils/uninstallview-x64.zip',
'WizTree': 'https://diskanalyzer.com/files/wiztree_4_10_portable.zip',
'XMPlay': 'https://support.xmplay.com/files/20/xmplay385.zip?v=47090', 'XMPlay': 'https://support.xmplay.com/files/20/xmplay385.zip?v=47090',
'XMPlay 7z': 'https://support.xmplay.com/files/16/xmp-7z.zip?v=800962', 'XMPlay 7z': 'https://support.xmplay.com/files/16/xmp-7z.zip?v=800962',
'XMPlay Game': 'https://support.xmplay.com/files/12/xmp-gme.zip?v=515637', 'XMPlay Game': 'https://support.xmplay.com/files/12/xmp-gme.zip?v=515637',
'XMPlay RAR': 'https://support.xmplay.com/files/16/xmp-rar.zip?v=409646', 'XMPlay RAR': 'https://support.xmplay.com/files/16/xmp-rar.zip?v=409646',
'XMPlay Innocuous': 'https://support.xmplay.com/files/10/Innocuous%20(v1.5).zip?v=155959', 'XMPlay Innocuous': 'https://support.xmplay.com/files/10/Innocuous%20(v1.7).zip?v=645281',
} }

View file

@ -1,18 +1,16 @@
"""WizardKit: Config - UFD""" """WizardKit: Config - UFD"""
# vim: sts=2 sw=2 ts=2 # vim: sts=2 sw=2 ts=2
from collections import OrderedDict
from wk.cfg.main import KIT_NAME_FULL from wk.cfg.main import KIT_NAME_FULL
# General # General
SOURCES = OrderedDict({ SOURCES = {
'Linux': {'Arg': '--linux', 'Type': 'ISO'}, 'Linux': {'Arg': '--linux', 'Type': 'ISO'},
'WinPE': {'Arg': '--winpe', 'Type': 'ISO'}, 'WinPE': {'Arg': '--winpe', 'Type': 'ISO'},
'Main Kit': {'Arg': '--main-kit', 'Type': 'KIT'}, 'Main Kit': {'Arg': '--main-kit', 'Type': 'KIT'},
'Extra Dir': {'Arg': '--extra-dir', 'Type': 'DIR'}, 'Extra Dir': {'Arg': '--extra-dir', 'Type': 'DIR'},
}) }
# Definitions: Boot entries # Definitions: Boot entries
BOOT_ENTRIES = { BOOT_ENTRIES = {
@ -39,8 +37,6 @@ ITEMS = {
), ),
'Linux': ( 'Linux': (
('/arch', '/'), ('/arch', '/'),
('/EFI/boot', '/EFI/'),
('/syslinux', '/'),
), ),
'Main Kit': ( 'Main Kit': (
('/', f'/{KIT_NAME_FULL}/'), ('/', f'/{KIT_NAME_FULL}/'),
@ -58,6 +54,25 @@ ITEMS = {
('/sources/boot.wim', '/sources/'), ('/sources/boot.wim', '/sources/'),
), ),
} }
ITEMS_FROM_LIVE = {
'WizardKit UFD base': (
('/usr/share/WizardKit/', '/'),
),
'rEFInd': (
('/usr/share/refind/drivers_x64/', '/EFI/Boot/drivers_x64/'),
('/usr/share/refind/icons/', '/EFI/Boot/icons/'),
('/usr/share/refind/refind_x64.efi', '/EFI/Boot/'),
),
'Syslinux': (
('/usr/lib/syslinux/bios/', '/syslinux/'),
),
'Memtest86': (
('/usr/share/memtest86-efi/', '/EFI/Memtest86/'),
),
'Wimboot': (
('/usr/share/wimboot/', '/syslinux/'),
),
}
ITEMS_HIDDEN = ( ITEMS_HIDDEN = (
# Linux (all versions) # Linux (all versions)
'arch', 'arch',

View file

@ -38,4 +38,6 @@ WINDOWS_BUILDS = {
# Windows 11 # Windows 11
'10.0.22000': '21H2', '10.0.22000': '21H2',
'10.0.22621': '22H2', '10.0.22621': '22H2',
'10.0.22631': '23H2',
'10.0.26100': '24H2',
} }

View file

@ -0,0 +1,32 @@
{
"$schema": "https://aka.ms/winget-packages.schema.2.0.json",
"CreationDate": "2023-06-25T01:40:45.003-00:00",
"Sources": [
{
"Packages": [
{
"PackageIdentifier": "7zip.7zip"
},
{
"PackageIdentifier": "Google.Chrome"
},
{
"PackageIdentifier": "Microsoft.Edge"
},
{
"PackageIdentifier": "Mozilla.Firefox"
},
{
"PackageIdentifier": "VideoLAN.VLC"
}
],
"SourceDetails": {
"Argument": "https://cdn.winget.microsoft.com/cache",
"Identifier": "Microsoft.Winget.Source_8wekyb3d8bbwe",
"Name": "winget",
"Type": "Microsoft.PreIndexed.Package"
}
}
],
"WinGetVersion": "1.4.11071"
}

View file

@ -0,0 +1,29 @@
{
"$schema": "https://aka.ms/winget-packages.schema.2.0.json",
"CreationDate": "2023-06-25T01:40:45.003-00:00",
"Sources": [
{
"Packages": [
{
"PackageIdentifier": "Microsoft.VCRedist.2013.x64"
},
{
"PackageIdentifier": "Microsoft.VCRedist.2013.x86"
},
{
"PackageIdentifier": "Microsoft.VCRedist.2015+.x64"
},
{
"PackageIdentifier": "Microsoft.VCRedist.2015+.x86"
}
],
"SourceDetails": {
"Argument": "https://cdn.winget.microsoft.com/cache",
"Identifier": "Microsoft.Winget.Source_8wekyb3d8bbwe",
"Name": "winget",
"Type": "Microsoft.PreIndexed.Package"
}
}
],
"WinGetVersion": "1.4.11071"
}

View file

@ -1,3 +1,7 @@
"""WizardKit: ddrescue-tui module init""" """WizardKit: ddrescue-tui module init"""
from . import block_pair
from . import ddrescue from . import ddrescue
from . import image
from . import menus
from . import state

View file

@ -0,0 +1,575 @@
"""WizardKit: ddrescue TUI - Block Pairs"""
# vim: sts=2 sw=2 ts=2
import logging
import math
import os
import pathlib
import plistlib
import re
import subprocess
from wk import cfg, exe, std
from wk.clone import menus
from wk.hw import disk as hw_disk
from wk.ui import ansi, cli
# STATIC VARIABLES
LOG = logging.getLogger(__name__)
DDRESCUE_LOG_REGEX = re.compile(
r'^\s*(?P<key>\S+):\s+'
r'(?P<size>\d+)\s+'
r'(?P<unit>[PTGMKB]i?B?)'
r'.*\(\s*(?P<percent>\d+\.?\d*)%\)$',
re.IGNORECASE,
)
# Classes
class BlockPair():
"""Object for tracking source to dest recovery data."""
def __init__(
self,
source_dev: hw_disk.Disk,
destination: pathlib.Path,
working_dir: pathlib.Path,
):
self.sector_size: int = source_dev.phy_sec
self.source: pathlib.Path = pathlib.Path(source_dev.path)
self.destination: pathlib.Path = destination
self.map_data: dict[str, bool | int] = {}
self.map_path: pathlib.Path = pathlib.Path()
self.size: int = source_dev.size
self.status: dict[str, float | int | str] = {
'read-skip': 'Pending',
'read-full': 'Pending',
'trim': 'Pending',
'scrape': 'Pending',
}
self.test_map: pathlib.Path | None = None
self.view_map: bool = 'DISPLAY' in os.environ or 'WAYLAND_DISPLAY' in os.environ
self.view_proc: subprocess.Popen | None = None
# Set map path
# e.g. '(Clone|Image)_Model_Serial[_p#]_Size[_Label].map'
map_name = f'{source_dev.model}_{source_dev.serial}'
if source_dev.bus == 'Image':
map_name = 'Image'
if source_dev.parent:
part_num = re.sub(r"^.*?(\d+)$", r"\1", self.source.name)
map_name += f'_p{part_num}'
size_str = std.bytes_to_string(
size=self.size,
use_binary=False,
)
map_name += f'_{size_str.replace(" ", "")}'
if source_dev.raw_details.get('label', ''):
map_name += f'_{source_dev.raw_details["label"]}'
map_name = map_name.replace(' ', '_')
map_name = map_name.replace('/', '_')
map_name = map_name.replace('\\', '_')
if destination.is_dir():
# Imaging
self.map_path = pathlib.Path(f'{destination}/Image_{map_name}.map')
self.destination = self.map_path.with_suffix('.dd')
self.destination.touch()
else:
# Cloning
self.map_path = pathlib.Path(f'{working_dir}/Clone_{map_name}.map')
# Create map file if needed
# NOTE: We need to set the domain size for --complete-only to work
if not self.map_path.exists():
self.map_path.write_text(
data=cfg.ddrescue.DDRESCUE_MAP_TEMPLATE.format(
name=cfg.main.KIT_NAME_FULL,
size=self.size,
),
encoding='utf-8',
)
# Set initial status
self.set_initial_status()
def __getstate__(self):
"""Override to allow pickling ddrescue.State() objects."""
bp_state = self.__dict__.copy()
del bp_state['view_proc']
return bp_state
def get_error_size(self) -> int:
"""Get error size in bytes, returns int."""
return self.size - self.get_rescued_size()
def get_percent_recovered(self) -> float:
"""Get percent rescued from map_data, returns float."""
return 100 * self.map_data.get('rescued', 0) / self.size
def get_rescued_size(self) -> int:
"""Get rescued size using map data.
NOTE: Returns 0 if no map data is available.
"""
self.load_map_data()
return self.map_data.get('rescued', 0)
def load_map_data(self) -> None:
"""Load map data from file.
NOTE: If the file is missing it is assumed that recovery hasn't
started yet so default values will be returned instead.
"""
data: dict[str, bool | int] = {'full recovery': False, 'pass completed': False}
# Get output from ddrescuelog
cmd = [
'ddrescuelog',
'--binary-prefixes',
'--show-status',
f'--size={self.size}',
self.map_path,
]
proc = exe.run_program(cmd, check=False)
# Parse output
for line in proc.stdout.splitlines():
_r = DDRESCUE_LOG_REGEX.search(line)
if _r:
if _r.group('key') == 'rescued' and _r.group('percent') == '100':
# Fix rounding errors from ddrescuelog output
data['rescued'] = self.size
else:
data[_r.group('key')] = std.string_to_bytes(
f'{_r.group("size")} {_r.group("unit")}',
)
data['pass completed'] = 'current status: finished' in line.lower()
# Check if 100% done (only if map is present and non-zero size
# NOTE: ddrescuelog returns 0 (i.e. 100% done) for empty files
if self.map_path.exists() and self.map_path.stat().st_size != 0:
cmd = [
'ddrescuelog',
'--done-status',
f'--size={self.size}',
self.map_path,
]
proc = exe.run_program(cmd, check=False)
data['full recovery'] = proc.returncode == 0
# Done
self.map_data.update(data)
def pass_complete(self, pass_name) -> bool:
"""Check if pass_name is complete based on map data, returns bool."""
pending_size = self.map_data['non-tried']
# Full recovery
if self.map_data.get('full recovery', False):
return True
# New recovery
if 'non-tried' not in self.map_data:
return False
# Initial read skip pass
if pass_name == 'read-skip':
pass_threshold = cfg.ddrescue.AUTO_PASS_THRESHOLDS[pass_name]
if self.get_percent_recovered() >= pass_threshold:
return True
# Recovery in progress
if pass_name in ('trim', 'scrape'):
pending_size += self.map_data['non-trimmed']
if pass_name == 'scrape':
pending_size += self.map_data['non-scraped']
if pending_size == 0:
# This is true when the previous and current passes are complete
return True
# This should never be reached
return False
def safety_check(self) -> None:
"""Run safety check and abort if necessary."""
# TODO: Expand section to support non-Linux systems
dest_size = -1
if self.destination.is_block_device():
cmd = [
'lsblk', '--bytes', '--json',
'--nodeps', '--noheadings', '--output=size',
self.destination,
]
json_data = exe.get_json_from_command(cmd)
dest_size = json_data['blockdevices'][0]['size']
del json_data
# Check destination size if cloning
if not self.destination.is_file() and dest_size < self.size:
cli.print_error(f'Invalid destination: {self.destination}')
raise std.GenericAbort()
def set_initial_status(self) -> None:
"""Read map data and set initial statuses."""
self.load_map_data()
percent = self.get_percent_recovered()
for name in self.status:
if self.pass_complete(name):
self.status[name] = percent
else:
# Stop checking
if percent > 0:
self.status[name] = percent
break
def skip_pass(self, pass_name) -> None:
"""Mark pass as skipped if applicable."""
if self.status[pass_name] == 'Pending':
self.status[pass_name] = 'Skipped'
def update_progress(self, pass_name) -> None:
"""Update progress via map data."""
self.load_map_data()
# Update status
percent = self.get_percent_recovered()
if percent > 0:
self.status[pass_name] = percent
# Mark future passes as skipped if applicable
if percent == 100:
status_keys = list(self.status.keys())
for pass_n in status_keys[status_keys.index(pass_name)+1:]:
self.status[pass_n] = 'Skipped'
# Functions
def add_clone_block_pairs(state) -> list[hw_disk.Disk]:
"""Add device to device block pairs and set settings if necessary."""
source_sep = get_partition_separator(state.source.path.name)
dest_sep = get_partition_separator(state.destination.path.name)
settings = {}
# Clone settings
settings = state.load_settings(discard_unused_settings=True)
# Add pairs from previous run
if settings['Partition Mapping']:
source_parts = []
for part_map in settings['Partition Mapping']:
bp_source = hw_disk.Disk(
f'{state.source.path}{source_sep}{part_map[0]}',
)
bp_dest = pathlib.Path(
f'{state.destination.path}{dest_sep}{part_map[1]}',
)
source_parts.append(bp_source)
state.add_block_pair(bp_source, bp_dest)
return source_parts
# Add pairs from selection
source_parts = menus.select_disk_parts('Clone', state.source)
if state.source.path.samefile(source_parts[0].path):
# Whole disk (or single partition via args), skip settings
bp_dest = state.destination.path
state.add_block_pair(state.source, bp_dest)
return source_parts
# New run, use new settings file
settings['Needs Format'] = True
offset = 0
user_choice = cli.choice(
'Format clone using GPT, MBR, or match Source type?',
['G', 'M', 'S'],
)
if user_choice == 'G':
settings['Table Type'] = 'GPT'
elif user_choice == 'M':
settings['Table Type'] = 'MBR'
else:
# Match source type
settings['Table Type'] = get_table_type(state.source.path)
if cli.ask('Create an empty Windows boot partition on the clone?'):
settings['Create Boot Partition'] = True
offset = 2 if settings['Table Type'] == 'GPT' else 1
# Add pairs
for dest_num, part in enumerate(source_parts):
dest_num += offset + 1
bp_dest = pathlib.Path(
f'{state.destination.path}{dest_sep}{dest_num}',
)
state.add_block_pair(part, bp_dest)
# Add to settings file
source_num = re.sub(r'^.*?(\d+)$', r'\1', part.path.name)
settings['Partition Mapping'].append([source_num, dest_num])
# Save settings
state.save_settings(settings)
# Done
return source_parts
def add_image_block_pairs(state) -> list[hw_disk.Disk]:
"""Add device to image file block pairs."""
source_parts = menus.select_disk_parts(state.mode, state.source)
for part in source_parts:
state.add_block_pair(part, state.destination)
# Done
return source_parts
def build_block_pair_report(block_pairs, settings) -> list:
"""Build block pair report, returns list."""
report = []
notes = []
if block_pairs:
report.append(ansi.color_string('Block Pairs', 'GREEN'))
else:
# Bail early
return report
# Show block pair mapping
if settings and settings['Create Boot Partition']:
if settings['Table Type'] == 'GPT':
report.append(f'{" —— ":<9} --> EFI System Partition')
report.append(f'{" —— ":<9} --> Microsoft Reserved Partition')
elif settings['Table Type'] == 'MBR':
report.append(f'{" —— ":<9} --> System Reserved')
for pair in block_pairs:
report.append(f'{pair.source.name:<9} --> {pair.destination.name}')
# Show resume messages as necessary
if settings:
if not settings['First Run']:
notes.append(
ansi.color_string(
['NOTE:', 'Clone settings loaded from previous run.'],
['BLUE', None],
),
)
if settings['Needs Format'] and settings['Table Type']:
msg = f'Destination will be formatted using {settings["Table Type"]}'
notes.append(
ansi.color_string(
['NOTE:', msg],
['BLUE', None],
),
)
if any(pair.get_rescued_size() > 0 for pair in block_pairs):
notes.append(
ansi.color_string(
['NOTE:', 'Resume data loaded from map file(s).'],
['BLUE', None],
),
)
# Add notes to report
if notes:
report.append(' ')
report.extend(notes)
# Done
return report
def build_sfdisk_partition_line(table_type, dev_path, size, details) -> str:
"""Build sfdisk partition line using passed details, returns str."""
line = f'{dev_path} : size={size}'
dest_type = ''
source_filesystem = str(details.get('fstype', '')).upper()
source_table_type = ''
source_type = details.get('parttype', '')
# Set dest type
if re.match(r'^0x\w+$', source_type):
# Source is a MBR type
source_table_type = 'MBR'
if table_type == 'MBR':
dest_type = source_type.replace('0x', '').lower()
elif re.match(r'^\w{8}-\w{4}-\w{4}-\w{4}-\w{12}$', source_type):
# Source is a GPT type
source_table_type = 'GPT'
if table_type == 'GPT':
dest_type = source_type.upper()
if not dest_type:
# Assuming changing table types, set based on FS
if source_filesystem in cfg.ddrescue.PARTITION_TYPES.get(table_type, {}):
dest_type = cfg.ddrescue.PARTITION_TYPES[table_type][source_filesystem]
line += f', type={dest_type}'
# Safety Check
if not dest_type:
cli.print_error(f'Failed to determine partition type for: {dev_path}')
raise std.GenericAbort()
# Add extra details
if details.get('partlabel', ''):
line += f', name="{details["partlabel"]}"'
if details.get('partuuid', '') and source_table_type == table_type:
# Only add UUID if source/dest table types match
line += f', uuid={details["partuuid"].upper()}'
# Done
return line
def get_partition_separator(name) -> str:
"""Get partition separator based on device name, returns str."""
separator = ''
if re.search(r'(loop|mmc|nvme)', name, re.IGNORECASE):
separator = 'p'
return separator
def get_table_type(disk_path) -> str:
"""Get disk partition table type, returns str.
NOTE: If resulting table type is not GPT or MBR
then an exception is raised.
"""
disk_path = str(disk_path)
table_type = None
# Linux
if std.PLATFORM == 'Linux':
cmd = f'lsblk --json --output=pttype --nodeps {disk_path}'.split()
json_data = exe.get_json_from_command(cmd)
table_type = json_data['blockdevices'][0].get('pttype', '').upper()
table_type = table_type.replace('DOS', 'MBR')
# macOS
if std.PLATFORM == 'Darwin':
cmd = ['diskutil', 'list', '-plist', disk_path]
proc = exe.run_program(cmd, check=False, encoding=None, errors=None)
try:
plist_data = plistlib.loads(proc.stdout)
except (TypeError, ValueError):
# Invalid / corrupt plist data? return empty dict to avoid crash
pass
else:
disk_details = plist_data.get('AllDisksAndPartitions', [{}])[0]
table_type = disk_details['Content']
table_type = table_type.replace('FDisk_partition_scheme', 'MBR')
table_type = table_type.replace('GUID_partition_scheme', 'GPT')
# Check type
if table_type not in ('GPT', 'MBR'):
cli.print_error(f'Unsupported partition table type: {table_type}')
raise std.GenericAbort()
# Done
return table_type
def prep_destination(
state,
source_parts: list[hw_disk.Disk],
dry_run: bool = True,
) -> None:
"""Prep destination as necessary."""
# TODO: Split into Linux and macOS
# logical sector size is not easily found under macOS
# It might be easier to rewrite this section using macOS tools
dest_prefix = str(state.destination.path)
dest_prefix += get_partition_separator(state.destination.path.name)
esp_type = 'C12A7328-F81F-11D2-BA4B-00A0C93EC93B'
msr_type = 'E3C9E316-0B5C-4DB8-817D-F92DF00215AE'
part_num = 0
sfdisk_script = []
settings = state.load_settings()
# Bail early
if not settings['Needs Format']:
return
# Add partition table settings
if settings['Table Type'] == 'GPT':
sfdisk_script.append('label: gpt')
else:
sfdisk_script.append('label: dos')
sfdisk_script.append('unit: sectors')
sfdisk_script.append('')
# Add boot partition if requested
if settings['Create Boot Partition']:
if settings['Table Type'] == 'GPT':
part_num += 1
sfdisk_script.append(
build_sfdisk_partition_line(
table_type='GPT',
dev_path=f'{dest_prefix}{part_num}',
size='260MiB',
details={'parttype': esp_type, 'partlabel': 'EFI System'},
),
)
part_num += 1
sfdisk_script.append(
build_sfdisk_partition_line(
table_type=settings['Table Type'],
dev_path=f'{dest_prefix}{part_num}',
size='16MiB',
details={'parttype': msr_type, 'partlabel': 'Microsoft Reserved'},
),
)
elif settings['Table Type'] == 'MBR':
part_num += 1
sfdisk_script.append(
build_sfdisk_partition_line(
table_type='MBR',
dev_path=f'{dest_prefix}{part_num}',
size='100MiB',
details={'parttype': '0x7', 'partlabel': 'System Reserved'},
),
)
# Add selected partition(s)
for part in source_parts:
num_sectors = part.size / state.destination.log_sec
num_sectors = math.ceil(num_sectors)
part_num += 1
sfdisk_script.append(
build_sfdisk_partition_line(
table_type=settings['Table Type'],
dev_path=f'{dest_prefix}{part_num}',
size=num_sectors,
details=part.raw_details,
),
)
# Save sfdisk script
script_path = (
f'{state.working_dir}/'
f'sfdisk_{state.destination.path.name}.script'
)
with open(script_path, 'w', encoding='utf-8') as _f:
_f.write('\n'.join(sfdisk_script))
# Skip real format for dry runs
if dry_run:
LOG.info('Dry run, refusing to format destination')
return
# Format disk
LOG.warning('Formatting destination: %s', state.destination.path)
with open(script_path, 'r', encoding='utf-8') as _f:
proc = exe.run_program(
cmd=['sudo', 'sfdisk', state.destination.path],
stdin=_f,
check=False,
)
if proc.returncode != 0:
cli.print_error('Error(s) encoundtered while formatting destination')
raise std.GenericAbort()
# Update settings
settings['Needs Format'] = False
state.save_settings(settings)
if __name__ == '__main__':
print("This file is not meant to be called directly.")

File diff suppressed because it is too large Load diff

109
scripts/wk/clone/image.py Normal file
View file

@ -0,0 +1,109 @@
"""WizardKit: ddrescue TUI - State"""
# vim: sts=2 sw=2 ts=2
import atexit
import logging
import pathlib
import plistlib
import re
from wk import exe
from wk.std import PLATFORM
from wk.ui import cli
# STATIC VARIABLES
LOG = logging.getLogger(__name__)
# Functions
def mount_raw_image(path) -> pathlib.Path:
"""Mount raw image using OS specific methods, returns pathlib.Path."""
loopback_path = None
if PLATFORM == 'Darwin':
loopback_path = mount_raw_image_macos(path)
elif PLATFORM == 'Linux':
loopback_path = mount_raw_image_linux(path)
# Check
if not loopback_path:
cli.print_error(f'Failed to mount image: {path}')
# Register unmount atexit
atexit.register(unmount_loopback_device, loopback_path)
# Done
return loopback_path
def mount_raw_image_linux(path) -> pathlib.Path:
"""Mount raw image using losetup, returns pathlib.Path."""
loopback_path = None
# Mount using losetup
cmd = [
'sudo',
'losetup',
'--find',
'--partscan',
'--show',
path,
]
proc = exe.run_program(cmd, check=False)
# Check result
if proc.returncode == 0:
loopback_path = proc.stdout.strip()
# Done
return loopback_path
def mount_raw_image_macos(path) -> pathlib.Path:
"""Mount raw image using hdiutil, returns pathlib.Path."""
loopback_path = None
plist_data = {}
# Mount using hdiutil
# plistdata['system-entities'][{}...]
cmd = [
'hdiutil', 'attach',
'-imagekey', 'diskimage-class=CRawDiskImage',
'-nomount',
'-plist',
'-readonly',
path,
]
proc = exe.run_program(cmd, check=False, encoding=None, errors=None)
# Check result
try:
plist_data = plistlib.loads(proc.stdout)
except plistlib.InvalidFileException:
return None
for dev in plist_data.get('system-entities', []):
dev_path = dev.get('dev-entry', '')
if re.match(r'^/dev/disk\d+$', dev_path):
loopback_path = dev_path
# Done
return loopback_path
def unmount_loopback_device(path) -> None:
"""Unmount loopback device using OS specific methods."""
cmd = []
# Build OS specific cmd
if PLATFORM == 'Darwin':
cmd = ['hdiutil', 'detach', path]
elif PLATFORM == 'Linux':
cmd = ['sudo', 'losetup', '--detach', path]
# Unmount loopback device
exe.run_program(cmd, check=False)
if __name__ == '__main__':
print("This file is not meant to be called directly.")

273
scripts/wk/clone/menus.py Normal file
View file

@ -0,0 +1,273 @@
"""WizardKit: ddrescue TUI - Menus"""
# vim: sts=2 sw=2 ts=2
import logging
import pathlib
from wk.cfg.ddrescue import DDRESCUE_SETTINGS
from wk.hw.disk import Disk, get_disks
from wk.std import GenericAbort, PLATFORM, bytes_to_string
from wk.ui import ansi, cli
# STATIC VARIABLES
LOG = logging.getLogger(__name__)
CLONE_SETTINGS = {
'Source': None,
'Destination': None,
'Create Boot Partition': False,
'First Run': True,
'Needs Format': False,
'Table Type': None,
'Partition Mapping': [
# (5, 1) ## Clone source partition #5 to destination partition #1
],
}
if PLATFORM == 'Darwin':
# TODO: Direct I/O needs more testing under macOS
DDRESCUE_SETTINGS['Default']['--idirect'] = {'Selected': False, 'Hidden': True}
DDRESCUE_SETTINGS['Default']['--odirect'] = {'Selected': False, 'Hidden': True}
MENU_ACTIONS = (
'Start',
f'Change settings {ansi.color_string("(experts only)", "YELLOW")}',
f'Detect drives {ansi.color_string("(experts only)", "YELLOW")}',
'Quit')
MENU_TOGGLES = {
'Auto continue (if recovery % over threshold)': True,
'Retry (mark non-rescued sectors "non-tried")': False,
}
SETTING_PRESETS = (
'Default',
'Fast',
'Safe',
)
# Functions
def main() -> cli.Menu:
"""Main menu, returns wk.ui.cli.Menu."""
menu = cli.Menu(title=ansi.color_string('ddrescue TUI: Main Menu', 'GREEN'))
menu.separator = ' '
# Add actions, options, etc
for action in MENU_ACTIONS:
if not (PLATFORM == 'Darwin' and 'Detect drives' in action):
menu.add_action(action)
for toggle, selected in MENU_TOGGLES.items():
menu.add_toggle(toggle, {'Selected': selected})
# Done
return menu
def settings(mode: str, silent: bool = True) -> cli.Menu:
"""Settings menu, returns wk.ui.cli.Menu."""
title_text = [
ansi.color_string('ddrescue TUI: Expert Settings', 'GREEN'),
' ',
ansi.color_string(
['These settings can cause', 'MAJOR DAMAGE', 'to drives'],
['YELLOW', 'RED', 'YELLOW'],
),
'Please read the manual before making changes',
]
menu = cli.Menu(title='\n'.join(title_text))
menu.separator = ' '
preset = 'Default'
if not silent:
# Ask which preset to use
cli.print_standard(
f'Available ddrescue presets: {" / ".join(SETTING_PRESETS)}'
)
preset = cli.choice('Please select a preset:', SETTING_PRESETS)
# Fix selection
for _p in SETTING_PRESETS:
if _p.startswith(preset):
preset = _p
# Add default settings
menu.add_action('Load Preset')
menu.add_action('Main Menu')
for name, details in DDRESCUE_SETTINGS['Default'].items():
menu.add_option(name, details.copy())
# Update settings using preset
if preset != 'Default':
for name, details in DDRESCUE_SETTINGS[preset].items():
menu.options[name].update(details.copy())
# Disable direct output when saving to an image
if mode == 'Image':
menu.options['--odirect']['Disabled'] = True
menu.options['--odirect']['Selected'] = False
# Done
return menu
def disks() -> cli.Menu:
"""Disk menu, returns wk.ui.cli.Menu()."""
cli.print_info('Scanning disks...')
available_disks = get_disks()
menu = cli.Menu('ddrescue TUI: Disk selection')
menu.disabled_str = 'Already selected'
menu.separator = ' '
menu.add_action('Quit')
for disk in available_disks:
menu.add_option(
name=(
f'{str(disk.path):<12} '
f'{disk.bus:<5} '
f'{bytes_to_string(disk.size, decimals=1, use_binary=False):<8} '
f'{disk.model} '
f'{disk.serial}'
),
details={'Object': disk},
)
# Done
return menu
def select_disk(prompt_msg: str, menu: cli.Menu) -> Disk:
"""Select disk from provided Menu, returns Disk()."""
menu.title = ansi.color_string(
f'ddrescue TUI: {prompt_msg} Selection', 'GREEN',
)
# Get selection
selection = menu.simple_select()
if 'Quit' in selection:
raise GenericAbort()
# Disable selected disk's menu entry
menu.options[selection[0]]['Disabled'] = True
# Update details to include child devices
selected_disk = selection[-1]['Object']
selected_disk.update_details(skip_children=False)
# Done
return selected_disk
def select_disk_parts(prompt_msg, disk) -> list[Disk]:
"""Select disk parts from list, returns list of Disk()."""
title = ansi.color_string('ddrescue TUI: Partition Selection', 'GREEN')
title += f'\n\nDisk: {disk.path} {disk.description}'
menu = cli.Menu(title)
menu.separator = ' '
menu.add_action('All')
menu.add_action('None')
menu.add_action('Proceed', {'Separator': True})
menu.add_action('Quit')
object_list = []
def _select_parts(menu) -> None:
"""Loop over selection menu until at least one partition selected."""
while True:
selection = menu.advanced_select(
f'Please select the parts to {prompt_msg.lower()}: ',
)
if 'All' in selection:
for option in menu.options.values():
option['Selected'] = True
elif 'None' in selection:
for option in menu.options.values():
option['Selected'] = False
elif 'Proceed' in selection:
if any(option['Selected'] for option in menu.options.values()):
# At least one partition/device selected/device selected
break
elif 'Quit' in selection:
raise GenericAbort()
# Bail early if running under macOS
if PLATFORM == 'Darwin':
return [disk]
# Bail early if child device selected
if disk.parent:
return [disk]
# Add parts
whole_disk_str = f'{str(disk.path):<14} (Whole device)'
for part in disk.children:
fstype = part.get('fstype', '')
fstype = str(fstype) if fstype else ''
size = part["size"]
name = (
f'{str(part["path"]):<14} '
f'{fstype.upper():<5} '
f'({bytes_to_string(size, decimals=1, use_binary=True):>6})'
)
menu.add_option(name, details={'Selected': True, 'pathlib.Path': part['path']})
# Add whole disk if necessary
if not menu.options:
menu.add_option(whole_disk_str, {'Selected': True, 'pathlib.Path': disk.path})
menu.title += '\n\n'
menu.title += ansi.color_string(' No partitions detected.', 'YELLOW')
# Get selection
_select_parts(menu)
# Build list of Disk() object_list
for option in menu.options.values():
if option['Selected']:
object_list.append(option['pathlib.Path'])
# Check if whole disk selected
if len(object_list) == len(disk.children):
# NOTE: This is not true if the disk has no partitions
msg = f'Preserve partition table and unused space in {prompt_msg.lower()}?'
if cli.ask(msg):
# Replace part list with whole disk obj
object_list = [disk.path]
# Convert object_list to Disk() objects
cli.print_standard(' ')
cli.print_info('Getting disk/partition details...')
object_list = [Disk(path) for path in object_list]
# Done
return object_list
def select_path(prompt_msg) -> pathlib.Path:
"""Select path, returns pathlib.Path."""
invalid = False
menu = cli.Menu(
title=ansi.color_string(f'ddrescue TUI: {prompt_msg} Path Selection', 'GREEN'),
)
menu.separator = ' '
menu.add_action('Quit')
menu.add_option('Current directory')
menu.add_option('Enter manually')
path = pathlib.Path.cwd()
# Make selection
selection = menu.simple_select()
if 'Current directory' in selection:
pass
elif 'Enter manually' in selection:
path = pathlib.Path(cli.input_text('Please enter path: '))
elif 'Quit' in selection:
raise GenericAbort()
# Check
try:
path = path.resolve()
except TypeError:
invalid = True
if invalid or not path.is_dir():
cli.print_error(f'Invalid path: {path}')
raise GenericAbort()
# Done
return path
if __name__ == '__main__':
print("This file is not meant to be called directly.")

1068
scripts/wk/clone/state.py Normal file

File diff suppressed because it is too large Load diff

View file

@ -5,6 +5,7 @@ import inspect
import logging import logging
import lzma import lzma
import os import os
import pathlib
import pickle import pickle
import platform import platform
import re import re
@ -12,15 +13,17 @@ import socket
import sys import sys
import time import time
from typing import Any
import requests import requests
from wk.cfg.net import CRASH_SERVER from wk.cfg.net import CRASH_SERVER
from wk.log import get_log_filepath, get_root_logger_path from wk.log import get_root_logger_path
# Classes # Classes
class Debug(): class Debug():
"""Object used when dumping debug data.""" """Object used when dumping debug data."""
def method(self): def method(self) -> None:
"""Dummy method used to identify functions vs data.""" """Dummy method used to identify functions vs data."""
@ -31,7 +34,7 @@ METHOD_TYPE = type(DEBUG_CLASS.method)
# Functions # Functions
def generate_debug_report(): def generate_debug_report() -> str:
"""Generate debug report, returns str.""" """Generate debug report, returns str."""
platform_function_list = ( platform_function_list = (
'architecture', 'architecture',
@ -42,8 +45,12 @@ def generate_debug_report():
report = [] report = []
# Logging data # Logging data
log_path = get_log_filepath() try:
if log_path: log_path = get_root_logger_path()
except RuntimeError:
# Assuming logging wasn't started
pass
else:
report.append('------ Start Log -------') report.append('------ Start Log -------')
report.append('') report.append('')
with open(log_path, 'r', encoding='utf-8') as log_file: with open(log_path, 'r', encoding='utf-8') as log_file:
@ -74,7 +81,7 @@ def generate_debug_report():
return '\n'.join(report) return '\n'.join(report)
def generate_object_report(obj, indent=0): def generate_object_report(obj: Any, indent: int = 0) -> list[str]:
"""Generate debug report for obj, returns list.""" """Generate debug report for obj, returns list."""
report = [] report = []
attr_list = [] attr_list = []
@ -105,7 +112,10 @@ def generate_object_report(obj, indent=0):
return report return report
def save_pickles(obj_dict, out_path=None): def save_pickles(
obj_dict: dict[Any, Any],
out_path: pathlib.Path | str | None = None,
) -> None:
"""Save dict of objects using pickle.""" """Save dict of objects using pickle."""
LOG.info('Saving pickles') LOG.info('Saving pickles')
@ -125,7 +135,11 @@ def save_pickles(obj_dict, out_path=None):
LOG.error('Failed to save all the pickles', exc_info=True) LOG.error('Failed to save all the pickles', exc_info=True)
def upload_debug_report(report, compress=True, reason='DEBUG'): def upload_debug_report(
report: str,
compress: bool = True,
reason: str = 'DEBUG',
) -> None:
"""Upload debug report to CRASH_SERVER as specified in wk.cfg.main.""" """Upload debug report to CRASH_SERVER as specified in wk.cfg.main."""
LOG.info('Uploading debug report to %s', CRASH_SERVER.get('Name', '?')) LOG.info('Uploading debug report to %s', CRASH_SERVER.get('Name', '?'))
headers = CRASH_SERVER.get('Headers', {'X-Requested-With': 'XMLHttpRequest'}) headers = CRASH_SERVER.get('Headers', {'X-Requested-With': 'XMLHttpRequest'})
@ -140,8 +154,12 @@ def upload_debug_report(report, compress=True, reason='DEBUG'):
# Set filename (based on the logging config if possible) # Set filename (based on the logging config if possible)
filename = 'Unknown' filename = 'Unknown'
log_path = get_log_filepath() try:
if log_path: log_path = get_root_logger_path()
except RuntimeError:
# Assuming logging wasn't started
pass
else:
# Strip everything but the prefix # Strip everything but the prefix
filename = re.sub(r'^(.*)_(\d{4}-\d{2}-\d{2}.*)', r'\1', log_path.name) filename = re.sub(r'^(.*)_(\d{4}-\d{2}-\d{2}.*)', r'\1', log_path.name)
filename = f'{filename}_{reason}_{time.strftime("%Y-%m-%d_%H%M%S%z")}.log' filename = f'{filename}_{reason}_{time.strftime("%Y-%m-%d_%H%M%S%z")}.log'

View file

@ -4,12 +4,15 @@
import json import json
import logging import logging
import os import os
import pathlib
import re import re
import subprocess import subprocess
import time import time
from threading import Thread from io import IOBase
from queue import Queue, Empty from queue import Queue, Empty
from threading import Thread
from typing import Any, Callable, Iterable
import psutil import psutil
@ -25,11 +28,11 @@ class NonBlockingStreamReader():
## https://gist.github.com/EyalAr/7915597 ## https://gist.github.com/EyalAr/7915597
## https://stackoverflow.com/a/4896288 ## https://stackoverflow.com/a/4896288
def __init__(self, stream): def __init__(self, stream: IOBase):
self.stream = stream self.stream: IOBase = stream
self.queue = Queue() self.queue: Queue = Queue()
def populate_queue(stream, queue): def populate_queue(stream: IOBase, queue: Queue) -> None:
"""Collect lines from stream and put them in queue.""" """Collect lines from stream and put them in queue."""
while not stream.closed: while not stream.closed:
try: try:
@ -45,18 +48,18 @@ class NonBlockingStreamReader():
args=(self.stream, self.queue), args=(self.stream, self.queue),
) )
def stop(self): def stop(self) -> None:
"""Stop reading from input stream.""" """Stop reading from input stream."""
self.stream.close() self.stream.close()
def read(self, timeout=None): def read(self, timeout: float | int | None = None) -> Any:
"""Read from queue if possible, returns item from queue.""" """Read from queue if possible, returns item from queue."""
try: try:
return self.queue.get(block=timeout is not None, timeout=timeout) return self.queue.get(block=timeout is not None, timeout=timeout)
except Empty: except Empty:
return None return None
def save_to_file(self, proc, out_path): def save_to_file(self, proc: subprocess.Popen, out_path: pathlib.Path | str) -> None:
"""Continuously save output to file while proc is running.""" """Continuously save output to file while proc is running."""
LOG.debug('Saving process %s output to %s', proc, out_path) LOG.debug('Saving process %s output to %s', proc, out_path)
while proc.poll() is None: while proc.poll() is None:
@ -74,7 +77,12 @@ class NonBlockingStreamReader():
# Functions # Functions
def build_cmd_kwargs(cmd, minimized=False, pipe=True, shell=False, **kwargs): def build_cmd_kwargs(
cmd: list[str],
minimized: bool = False,
pipe: bool = True,
shell: bool = False,
**kwargs) -> dict[str, Any]:
"""Build kwargs for use by subprocess functions, returns dict. """Build kwargs for use by subprocess functions, returns dict.
Specifically subprocess.run() and subprocess.Popen(). Specifically subprocess.run() and subprocess.Popen().
@ -122,7 +130,12 @@ def build_cmd_kwargs(cmd, minimized=False, pipe=True, shell=False, **kwargs):
return cmd_kwargs return cmd_kwargs
def get_json_from_command(cmd, check=True, encoding='utf-8', errors='ignore'): def get_json_from_command(
cmd: list[str],
check: bool = True,
encoding: str = 'utf-8',
errors: str = 'ignore',
) -> dict[Any, Any]:
"""Capture JSON content from cmd output, returns dict. """Capture JSON content from cmd output, returns dict.
If the data can't be decoded then either an exception is raised If the data can't be decoded then either an exception is raised
@ -141,7 +154,11 @@ def get_json_from_command(cmd, check=True, encoding='utf-8', errors='ignore'):
return json_data return json_data
def get_procs(name, exact=True, try_again=True): def get_procs(
name: str,
exact: bool = True,
try_again: bool = True,
) -> list[psutil.Process]:
"""Get process object(s) based on name, returns list of proc objects.""" """Get process object(s) based on name, returns list of proc objects."""
LOG.debug('name: %s, exact: %s', name, exact) LOG.debug('name: %s, exact: %s', name, exact)
processes = [] processes = []
@ -161,7 +178,12 @@ def get_procs(name, exact=True, try_again=True):
return processes return processes
def kill_procs(name, exact=True, force=False, timeout=30): def kill_procs(
name: str,
exact: bool = True,
force: bool = False,
timeout: float | int = 30,
) -> None:
"""Kill all processes matching name (case-insensitively). """Kill all processes matching name (case-insensitively).
NOTE: Under Posix systems this will send SIGINT to allow processes NOTE: Under Posix systems this will send SIGINT to allow processes
@ -185,7 +207,13 @@ def kill_procs(name, exact=True, force=False, timeout=30):
proc.kill() proc.kill()
def popen_program(cmd, minimized=False, pipe=False, shell=False, **kwargs): def popen_program(
cmd: list[str],
minimized: bool = False,
pipe: bool = False,
shell: bool = False,
**kwargs,
) -> subprocess.Popen:
"""Run program and return a subprocess.Popen object.""" """Run program and return a subprocess.Popen object."""
LOG.debug( LOG.debug(
'cmd: %s, minimized: %s, pipe: %s, shell: %s', 'cmd: %s, minimized: %s, pipe: %s, shell: %s',
@ -209,7 +237,13 @@ def popen_program(cmd, minimized=False, pipe=False, shell=False, **kwargs):
return proc return proc
def run_program(cmd, check=True, pipe=True, shell=False, **kwargs): def run_program(
cmd: list[str],
check: bool = True,
pipe: bool = True,
shell: bool = False,
**kwargs,
) -> subprocess.CompletedProcess:
"""Run program and return a subprocess.CompletedProcess object.""" """Run program and return a subprocess.CompletedProcess object."""
LOG.debug( LOG.debug(
'cmd: %s, check: %s, pipe: %s, shell: %s', 'cmd: %s, check: %s, pipe: %s, shell: %s',
@ -222,8 +256,9 @@ def run_program(cmd, check=True, pipe=True, shell=False, **kwargs):
pipe=pipe, pipe=pipe,
shell=shell, shell=shell,
**kwargs) **kwargs)
check = cmd_kwargs.pop('check', True) # Avoids linting warning
try: try:
proc = subprocess.run(**cmd_kwargs) proc = subprocess.run(check=check, **cmd_kwargs)
except FileNotFoundError: except FileNotFoundError:
LOG.error('Command not found: %s', cmd) LOG.error('Command not found: %s', cmd)
raise raise
@ -233,7 +268,11 @@ def run_program(cmd, check=True, pipe=True, shell=False, **kwargs):
return proc return proc
def start_thread(function, args=None, daemon=True): def start_thread(
function: Callable,
args: Iterable[Any] | None = None,
daemon: bool = True,
) -> Thread:
"""Run function as thread in background, returns Thread object.""" """Run function as thread in background, returns Thread object."""
LOG.debug( LOG.debug(
'Starting background thread for function: %s, args: %s, daemon: %s', 'Starting background thread for function: %s, args: %s, daemon: %s',
@ -245,7 +284,7 @@ def start_thread(function, args=None, daemon=True):
return thread return thread
def stop_process(proc, graceful=True): def stop_process(proc: subprocess.Popen, graceful: bool = True) -> None:
"""Stop process. """Stop process.
NOTES: proc should be a subprocess.Popen obj. NOTES: proc should be a subprocess.Popen obj.
@ -267,7 +306,11 @@ def stop_process(proc, graceful=True):
proc.kill() proc.kill()
def wait_for_procs(name, exact=True, timeout=None): def wait_for_procs(
name: str,
exact: bool = True,
timeout: float | int | None = None,
) -> None:
"""Wait for all process matching name.""" """Wait for all process matching name."""
LOG.debug('name: %s, exact: %s, timeout: %s', name, exact, timeout) LOG.debug('name: %s, exact: %s, timeout: %s', name, exact, timeout)
target_procs = get_procs(name, exact=exact) target_procs = get_procs(name, exact=exact)

View file

@ -33,7 +33,10 @@ THRESH_GREAT = 750 * 1024**2
# Functions # Functions
def generate_horizontal_graph(rate_list, graph_width=40, oneline=False): def generate_horizontal_graph(
rate_list: list[float],
graph_width: int = 40,
oneline: bool = False) -> list[str]:
"""Generate horizontal graph from rate_list, returns list.""" """Generate horizontal graph from rate_list, returns list."""
graph = ['', '', '', ''] graph = ['', '', '', '']
scale = 8 if oneline else 32 scale = 8 if oneline else 32
@ -80,7 +83,7 @@ def generate_horizontal_graph(rate_list, graph_width=40, oneline=False):
return graph return graph
def get_graph_step(rate, scale=16): def get_graph_step(rate: float, scale: int = 16) -> int:
"""Get graph step based on rate and scale, returns int.""" """Get graph step based on rate and scale, returns int."""
rate_in_mb = rate / (1024**2) rate_in_mb = rate / (1024**2)
step = 0 step = 0
@ -95,14 +98,17 @@ def get_graph_step(rate, scale=16):
return step return step
def merge_rates(rates, graph_width=40): def merge_rates(
rates: list[float],
graph_width: int = 40,
) -> list[int | float]:
"""Merge rates to have entries equal to the width, returns list.""" """Merge rates to have entries equal to the width, returns list."""
merged_rates = [] merged_rates = []
offset = 0 offset = 0
slice_width = int(len(rates) / graph_width) slice_width = int(len(rates) / graph_width)
# Merge rates # Merge rates
for _i in range(graph_width): for _ in range(graph_width):
merged_rates.append(sum(rates[offset:offset+slice_width])/slice_width) merged_rates.append(sum(rates[offset:offset+slice_width])/slice_width)
offset += slice_width offset += slice_width
@ -110,7 +116,7 @@ def merge_rates(rates, graph_width=40):
return merged_rates return merged_rates
def vertical_graph_line(percent, rate, scale=32): def vertical_graph_line(percent: float, rate: float, scale: int = 32) -> str:
"""Build colored graph string using thresholds, returns str.""" """Build colored graph string using thresholds, returns str."""
color_bar = None color_bar = None
color_rate = None color_rate = None

View file

@ -8,7 +8,7 @@ import subprocess
from typing import TextIO from typing import TextIO
from wk import exe from wk import exe
from wk.cfg.hw import CPU_FAILURE_TEMP from wk.cfg.hw import CPU_TEMPS
from wk.os.mac import set_fans as macos_set_fans from wk.os.mac import set_fans as macos_set_fans
from wk.std import PLATFORM from wk.std import PLATFORM
from wk.ui import ansi from wk.ui import ansi
@ -20,32 +20,75 @@ SysbenchType = tuple[subprocess.Popen, TextIO]
# Functions # Functions
def check_cooling_results(test_obj, sensors, run_sysbench=False) -> None: def check_cooling_results(sensors, test_object) -> None:
"""Check cooling results and update test_obj.""" """Check cooling result via sensor data."""
max_temp = sensors.cpu_max_temp() idle_temp = sensors.get_cpu_temp('Idle')
temp_labels = ['Idle', 'Max', 'Cooldown'] cooldown_temp = sensors.get_cpu_temp('Cooldown')
if run_sysbench: max_temp = sensors.get_cpu_temp('Max')
temp_labels.append('Sysbench') test_object.report.append(ansi.color_string('Temps', 'BLUE'))
# Check temps # Check temps
if not max_temp: if max_temp > CPU_TEMPS['Critical']:
test_obj.set_status('Unknown') test_object.failed = True
elif max_temp >= CPU_FAILURE_TEMP: test_object.set_status('Failed')
test_obj.failed = True test_object.report.extend([
test_obj.set_status('Failed') ansi.color_string(
elif 'Aborted' not in test_obj.status: f' WARNING: Critical CPU temp of {CPU_TEMPS["Critical"]} exceeded.',
test_obj.passed = True 'RED',
test_obj.set_status('Passed') ),
'',
])
elif idle_temp >= CPU_TEMPS['Idle High']:
test_object.failed = True
test_object.set_status('Failed')
test_object.report.extend([
ansi.color_string(
f' WARNING: Max idle temp of {CPU_TEMPS["Idle High"]} exceeded.',
'YELLOW',
),
'',
])
elif (
cooldown_temp <= CPU_TEMPS['Cooling Low Cutoff']
or max_temp - cooldown_temp >= CPU_TEMPS['Cooling Delta']
):
test_object.passed = True
test_object.set_status('Passed')
else:
test_object.passed = False
test_object.set_status('Unknown')
if cooldown_temp - idle_temp >= CPU_TEMPS['Idle Delta']:
test_object.report.extend([
ansi.color_string(
f' WARNING: Cooldown temp at least {CPU_TEMPS["Idle Delta"]}° over idle.',
'YELLOW',
),
'',
])
# Add temps to report # Build report
for line in sensors.generate_report(*temp_labels, only_cpu=True): report_labels = ['Idle']
test_obj.report.append(f' {line}') average_labels = []
if 'Sysbench' in sensors.temp_labels:
average_labels.append('Sysbench')
report_labels.extend(['Sysbench', 'Cooldown'])
if 'Prime95' in sensors.temp_labels:
average_labels.append('Prime95')
report_labels.append('Prime95')
if 'Cooldown' not in report_labels:
report_labels.append('Cooldown')
if len(sensors.temp_labels.intersection(['Prime95', 'Sysbench'])) < 1:
# Include overall max temp if needed
report_labels.append('Max')
for line in sensors.generate_report(
*report_labels, only_cpu=True, include_avg_for=average_labels):
test_object.report.append(f' {line}')
def check_mprime_results(test_obj, working_dir) -> None: def check_mprime_results(test_obj, working_dir) -> None:
"""Check mprime log files and update test_obj.""" """Check mprime log files and update test_obj."""
passing_lines = {} passing_lines = set()
warning_lines = {} warning_lines = set()
def _read_file(log_name) -> list[str]: def _read_file(log_name) -> list[str]:
"""Read file and split into lines, returns list.""" """Read file and split into lines, returns list."""
@ -63,7 +106,7 @@ def check_mprime_results(test_obj, working_dir) -> None:
for line in _read_file('results.txt'): for line in _read_file('results.txt'):
line = line.strip() line = line.strip()
if re.search(r'(error|fail)', line, re.IGNORECASE): if re.search(r'(error|fail)', line, re.IGNORECASE):
warning_lines[line] = None warning_lines.add(line)
# prime.log (check if passed) # prime.log (check if passed)
for line in _read_file('prime.log'): for line in _read_file('prime.log'):
@ -73,10 +116,10 @@ def check_mprime_results(test_obj, working_dir) -> None:
if match: if match:
if int(match.group(2)) + int(match.group(3)) > 0: if int(match.group(2)) + int(match.group(3)) > 0:
# Errors and/or warnings encountered # Errors and/or warnings encountered
warning_lines[match.group(1).capitalize()] = None warning_lines.add(match.group(1).capitalize())
else: else:
# No errors/warnings # No errors/warnings
passing_lines[match.group(1).capitalize()] = None passing_lines.add(match.group(1).capitalize())
# Update status # Update status
if warning_lines: if warning_lines:
@ -112,9 +155,11 @@ def start_mprime(working_dir, log_path) -> subprocess.Popen:
stdout=subprocess.PIPE, stdout=subprocess.PIPE,
) )
proc_mprime.stdout.close() # type: ignore[reportOptionalMemberAccess] proc_mprime.stdout.close() # type: ignore[reportOptionalMemberAccess]
save_nsbr = exe.NonBlockingStreamReader(proc_grep.stdout) save_nbsr = exe.NonBlockingStreamReader(
proc_grep.stdout, # type: ignore[reportGeneralTypeIssues]
)
exe.start_thread( exe.start_thread(
save_nsbr.save_to_file, save_nbsr.save_to_file,
args=(proc_grep, log_path), args=(proc_grep, log_path),
) )
@ -122,35 +167,6 @@ def start_mprime(working_dir, log_path) -> subprocess.Popen:
return proc_mprime return proc_mprime
def start_sysbench(sensors, sensors_out, log_path) -> SysbenchType:
"""Start sysbench, returns tuple with Popen object and file handle."""
set_apple_fan_speed('max')
sysbench_cmd = [
'sysbench',
f'--threads={exe.psutil.cpu_count()}',
'--cpu-max-prime=1000000000',
'cpu',
'run',
]
# Restart background monitor for Sysbench
sensors.stop_background_monitor()
sensors.start_background_monitor(
sensors_out,
alt_max='Sysbench',
thermal_action=('killall', 'sysbench', '-INT'),
)
# Start sysbench
filehandle_sysbench = open(
log_path, 'a', encoding='utf-8',
)
proc_sysbench = exe.popen_program(sysbench_cmd, stdout=filehandle_sysbench)
# Done
return (proc_sysbench, filehandle_sysbench)
def set_apple_fan_speed(speed) -> None: def set_apple_fan_speed(speed) -> None:
"""Set Apple fan speed.""" """Set Apple fan speed."""
cmd = None cmd = None
@ -174,6 +190,27 @@ def set_apple_fan_speed(speed) -> None:
exe.run_program(cmd, check=False) exe.run_program(cmd, check=False)
def start_sysbench(log_path) -> SysbenchType:
"""Start sysbench, returns tuple with Popen object and file handle."""
set_apple_fan_speed('max')
cmd = [
'sysbench',
f'--threads={exe.psutil.cpu_count()}',
'--cpu-max-prime=1000000000',
'cpu',
'run',
]
# Start sysbench
filehandle = open(
log_path, 'a', encoding='utf-8',
)
proc = exe.popen_program(cmd, stdout=filehandle)
# Done
return (proc, filehandle)
def stop_mprime(proc_mprime) -> None: def stop_mprime(proc_mprime) -> None:
"""Stop mprime gracefully, then forcefully as needed.""" """Stop mprime gracefully, then forcefully as needed."""
proc_mprime.terminate() proc_mprime.terminate()

View file

@ -1,14 +1,12 @@
"""WizardKit: Hardware diagnostics""" """WizardKit: Hardware diagnostics"""
# vim: sts=2 sw=2 ts=2 # vim: sts=2 sw=2 ts=2
import argparse
import atexit import atexit
import logging import logging
import os import os
import pathlib import pathlib
import subprocess import subprocess
import time
from docopt import docopt
from wk import cfg, debug, exe, log, std from wk import cfg, debug, exe, log, std
from wk.cfg.hw import STATUS_COLORS from wk.cfg.hw import STATUS_COLORS
@ -29,23 +27,13 @@ from wk.ui import ansi, cli, tui
# STATIC VARIABLES # STATIC VARIABLES
DOCSTRING = f'''{cfg.main.KIT_NAME_FULL}: Hardware Diagnostics
Usage:
hw-diags [options]
hw-diags (-h | --help)
Options:
-c --cli Force CLI mode
-h --help Show this page
-q --quick Skip menu and perform a quick check
-t --test-mode Run diags in test mode
'''
LOG = logging.getLogger(__name__) LOG = logging.getLogger(__name__)
TEST_GROUPS = { TEST_GROUPS = {
# Also used to build the menu options # Also used to build the menu options
## NOTE: This needs to be above MENU_SETS ## NOTE: This needs to be above MENU_SETS
'CPU & Cooling': 'cpu_stress_tests', 'CPU (Sysbench)': 'cpu_test_sysbench',
'CPU (Prime95)': 'cpu_test_mprime',
'CPU (Cooling)': 'cpu_test_cooling',
'Disk Attributes': 'disk_attribute_check', 'Disk Attributes': 'disk_attribute_check',
'Disk Self-Test': 'disk_self_test', 'Disk Self-Test': 'disk_self_test',
'Disk Surface Scan': 'disk_surface_scan', 'Disk Surface Scan': 'disk_surface_scan',
@ -65,6 +53,7 @@ MENU_ACTIONS_SECRET = (
MENU_OPTIONS_QUICK = ('Disk Attributes',) MENU_OPTIONS_QUICK = ('Disk Attributes',)
MENU_SETS = { MENU_SETS = {
'Full Diagnostic': (*TEST_GROUPS,), 'Full Diagnostic': (*TEST_GROUPS,),
'CPU Diagnostic': (*[group for group in TEST_GROUPS if group.startswith('CPU')],),
'Disk Diagnostic': ( 'Disk Diagnostic': (
'Disk Attributes', 'Disk Attributes',
'Disk Self-Test', 'Disk Self-Test',
@ -82,15 +71,16 @@ PLATFORM = std.PLATFORM
class State(): class State():
"""Object for tracking hardware diagnostic data.""" """Object for tracking hardware diagnostic data."""
def __init__(self, test_mode=False): def __init__(self, test_mode=False):
self.disks = [] self.disks: list[hw_disk.Disk] = []
self.log_dir = None self.log_dir: pathlib.Path | None = None
self.progress_file = None self.progress_file: pathlib.Path | None = None
self.system = None self.sensors: hw_sensors.Sensors = hw_sensors.Sensors()
self.test_groups = [] self.system: hw_system.System | None = None
self.title_text = ansi.color_string('Hardware Diagnostics', 'GREEN') self.test_groups: list[TestGroup] = []
self.title_text: str = ansi.color_string('Hardware Diagnostics', 'GREEN')
if test_mode: if test_mode:
self.title_text += ansi.color_string(' (Test Mode)', 'YELLOW') self.title_text += ansi.color_string(' (Test Mode)', 'YELLOW')
self.ui = tui.TUI(f'{self.title_text}\nMain Menu') self.ui: tui.TUI = tui.TUI(f'{self.title_text}\nMain Menu')
def abort_testing(self) -> None: def abort_testing(self) -> None:
"""Set unfinished tests as aborted and cleanup panes.""" """Set unfinished tests as aborted and cleanup panes."""
@ -100,15 +90,20 @@ class State():
test.set_status('Aborted') test.set_status('Aborted')
# Cleanup panes # Cleanup panes
self.ui.remove_all_info_panes() self.reset_layout()
self.ui.remove_all_worker_panes()
def disk_safety_checks(self) -> None: def disk_safety_checks(self) -> None:
"""Check for mid-run SMART failures and failed test(s).""" """Check for mid-run SMART failures and failed test(s)."""
for dev in self.disks: for dev in self.disks:
disk_smart_status_check(dev, mid_run=True) disk_smart_status_check(dev, mid_run=True)
for test in dev.tests: for test in dev.tests:
if test.failed and 'Attributes' not in test.name: if test.failed:
# Skip acceptable failure states
if 'Attributes' in test.name:
continue
if 'Self-Test' in test.name and 'TimedOut' in test.status:
continue
# Disable remaining tests
dev.disable_disk_tests() dev.disable_disk_tests()
break break
@ -117,20 +112,21 @@ class State():
# Reset objects # Reset objects
self.disks.clear() self.disks.clear()
self.sensors = hw_sensors.Sensors()
self.test_groups.clear() self.test_groups.clear()
# Set log # Set log
self.log_dir = log.format_log_path() self.log_dir = log.format_log_path(
self.log_dir = pathlib.Path( log_name='main',
f'{self.log_dir.parent}/' sub_dir='Hardware-Diagnostics',
f'Hardware-Diagnostics_{time.strftime("%Y-%m-%d_%H%M%S%z")}/'
) )
log.update_log_path( log.update_log_path(
dest_dir=self.log_dir, dest_dir=self.log_dir.parent,
dest_name='main', dest_name=self.log_dir.stem,
keep_history=False, keep_history=False,
timestamp=False, timestamp=False,
) )
self.log_dir = self.log_dir.parent
cli.clear_screen() cli.clear_screen()
cli.print_info('Initializing...') cli.print_info('Initializing...')
@ -153,21 +149,9 @@ class State():
continue continue
if 'CPU' in name: if 'CPU' in name:
# Create two Test objects which will both be used by cpu_stress_tests
# NOTE: Prime95 should be added first
self.system.tests.append( self.system.tests.append(
Test(dev=self.system, label='Prime95', name=name), Test(dev=self.system, label=name[5:-1], name=name),
) )
self.system.tests.append(
Test(dev=self.system, label='Cooling', name=name),
)
self.test_groups.append(
TestGroup(
name=name,
function=globals()[TEST_GROUPS[name]],
test_objects=self.system.tests,
),
)
if 'Disk' in name: if 'Disk' in name:
test_group = TestGroup( test_group = TestGroup(
@ -179,6 +163,23 @@ class State():
test_group.test_objects.append(test_obj) test_group.test_objects.append(test_obj)
self.test_groups.append(test_group) self.test_groups.append(test_group)
# Group CPU tests
if self.system.tests:
self.test_groups.insert(
0,
TestGroup(
name='CPU & Cooling',
function=run_cpu_tests,
test_objects=self.system.tests,
),
)
def reset_layout(self) -> None:
"""Reset layout to avoid flickering."""
self.ui.clear_current_pane_height()
self.ui.remove_all_info_panes()
self.ui.remove_all_worker_panes()
def save_debug_reports(self) -> None: def save_debug_reports(self) -> None:
"""Save debug reports to disk.""" """Save debug reports to disk."""
LOG.info('Saving debug reports') LOG.info('Saving debug reports')
@ -212,7 +213,7 @@ class State():
proc = exe.run_program(['smc', '-l']) proc = exe.run_program(['smc', '-l'])
data.extend(proc.stdout.splitlines()) data.extend(proc.stdout.splitlines())
except Exception: except Exception:
LOG.ERROR('Error(s) encountered while exporting SMC data') LOG.error('Error(s) encountered while exporting SMC data')
data = [line.strip() for line in data] data = [line.strip() for line in data]
with open(f'{debug_dir}/smc.data', 'a', encoding='utf-8') as _f: with open(f'{debug_dir}/smc.data', 'a', encoding='utf-8') as _f:
_f.write('\n'.join(data)) _f.write('\n'.join(data))
@ -250,9 +251,39 @@ class State():
# Functions # Functions
def argparse_helper() -> dict[str, bool]:
"""Helper function to setup and return args, returns dict.
NOTE: A dict is used to match the legacy code.
"""
parser = argparse.ArgumentParser(
prog='hw-diags',
description=f'{cfg.main.KIT_NAME_FULL}: Hardware Diagnostics',
)
parser.add_argument(
'-c', '--cli', action='store_true',
help='Force CLI mode',
)
parser.add_argument(
'-q', '--quick', action='store_true',
help='Skip menu and perform a quick check',
)
parser.add_argument(
'-t', '--test-mode', action='store_true',
help='Run diags in test mode',
)
args = parser.parse_args()
legacy_args = {
'--cli': args.cli,
'--quick': args.quick,
'--test-mode': args.test_mode,
}
return legacy_args
def build_menu(cli_mode=False, quick_mode=False) -> cli.Menu: def build_menu(cli_mode=False, quick_mode=False) -> cli.Menu:
"""Build main menu, returns wk.ui.cli.Menu.""" """Build main menu, returns wk.ui.cli.Menu."""
menu = cli.Menu(title=None) menu = cli.Menu(title='')
# Add actions, options, etc # Add actions, options, etc
for action in MENU_ACTIONS: for action in MENU_ACTIONS:
@ -269,13 +300,15 @@ def build_menu(cli_mode=False, quick_mode=False) -> cli.Menu:
# Update default selections for quick mode if necessary # Update default selections for quick mode if necessary
if quick_mode: if quick_mode:
for name in menu.options: for name, details in menu.options.items():
# Only select quick option(s) # Only select quick option(s)
menu.options[name]['Selected'] = name in MENU_OPTIONS_QUICK details['Selected'] = name in MENU_OPTIONS_QUICK
# Skip CPU tests for TestStations # Skip CPU tests for TestStations
if os.path.exists(cfg.hw.TESTSTATION_FILE): if os.path.exists(cfg.hw.TESTSTATION_FILE):
menu.options['CPU & Cooling']['Selected'] = False menu.options['CPU (Sysbench)']['Selected'] = False
menu.options['CPU (Prime95)']['Selected'] = False
menu.options['CPU (Cooling)']['Selected'] = False
# Add CLI actions if necessary # Add CLI actions if necessary
if cli_mode or 'DISPLAY' not in os.environ: if cli_mode or 'DISPLAY' not in os.environ:
@ -301,140 +334,213 @@ def build_menu(cli_mode=False, quick_mode=False) -> cli.Menu:
return menu return menu
def cpu_stress_tests(state, test_objects, test_mode=False) -> None: def cpu_tests_init(state: State) -> None:
"""CPU & cooling check using Prime95 and Sysbench.""" """Initialize CPU tests."""
LOG.info('CPU Test (Prime95)')
aborted = False
prime_log = pathlib.Path(f'{state.log_dir}/prime.log')
run_sysbench = False
sensors_out = pathlib.Path(f'{state.log_dir}/sensors.out') sensors_out = pathlib.Path(f'{state.log_dir}/sensors.out')
test_minutes = cfg.hw.CPU_TEST_MINUTES state.update_title_text(state.system.cpu_description)
if test_mode:
test_minutes = cfg.hw.TEST_MODE_CPU_LIMIT
test_mprime_obj, test_cooling_obj = test_objects
# Bail early # Start monitor
if test_cooling_obj.disabled or test_mprime_obj.disabled:
return
# Prep
state.update_title_text(test_mprime_obj.dev.cpu_description)
test_cooling_obj.set_status('Working')
test_mprime_obj.set_status('Working')
# Start sensors monitor
sensors = hw_sensors.Sensors()
sensors.start_background_monitor(
sensors_out,
thermal_action=('killall', 'mprime', '-INT'),
)
# Create monitor and worker panes
state.update_progress_file()
state.ui.add_worker_pane(lines=10, watch_cmd='tail', watch_file=prime_log)
if PLATFORM == 'Darwin': if PLATFORM == 'Darwin':
state.ui.add_info_pane( state.ui.add_info_pane(
percent=80, cmd='./hw-sensors', update_layout=False, percent=80, cmd='./hw-sensors', update_layout=False,
) )
elif PLATFORM == 'Linux': elif PLATFORM == 'Linux':
state.ui.add_info_pane( state.ui.add_info_pane(
percent=80, watch_file=sensors_out, update_layout=False, percent=80,
watch_file=pathlib.Path(f'{state.log_dir}/sensors.out'),
update_layout=False,
) )
state.sensors.start_background_monitor(sensors_out)
state.ui.set_current_pane_height(3) state.ui.set_current_pane_height(3)
# Get idle temps # Save idle temps
cli.print_standard('Saving idle temps...') cli.print_standard('Saving idle temps...')
sensors.save_average_temps(temp_label='Idle', seconds=5) state.sensors.save_average_temps(temp_label='Idle', seconds=5, save_history=False)
# Stress CPU
cli.print_info('Running stress test')
hw_cpu.set_apple_fan_speed('max')
proc_mprime = hw_cpu.start_mprime(state.log_dir, prime_log)
# Show countdown
print('')
try:
print_countdown(proc=proc_mprime, seconds=test_minutes*60)
except KeyboardInterrupt:
aborted = True
# Stop Prime95
hw_cpu.stop_mprime(proc_mprime)
# Update progress if necessary
if sensors.cpu_reached_critical_temp() or aborted:
test_cooling_obj.set_status('Aborted')
test_mprime_obj.set_status('Aborted')
state.update_progress_file()
# Get cooldown temp
state.ui.clear_current_pane()
cli.print_standard('Letting CPU cooldown...')
std.sleep(5)
cli.print_standard('Saving cooldown temps...')
sensors.save_average_temps(temp_label='Cooldown', seconds=5)
# Check Prime95 results
test_mprime_obj.report.append(ansi.color_string('Prime95', 'BLUE'))
hw_cpu.check_mprime_results(
test_obj=test_mprime_obj, working_dir=state.log_dir,
)
# Run Sysbench test if necessary
run_sysbench = (
not aborted and sensors.cpu_max_temp() >= cfg.hw.CPU_FAILURE_TEMP
)
if run_sysbench:
LOG.info('CPU Test (Sysbench)')
cli.print_standard('Letting CPU cooldown more...')
std.sleep(10)
state.ui.clear_current_pane()
cli.print_info('Running alternate stress test')
print('')
sysbench_log = prime_log.with_name('sysbench.log')
sysbench_log.touch()
state.ui.remove_all_worker_panes()
state.ui.add_worker_pane(lines=10, watch_cmd='tail', watch_file=sysbench_log)
proc_sysbench, filehandle_sysbench = hw_cpu.start_sysbench(
sensors,
sensors_out,
log_path=sysbench_log,
)
try:
print_countdown(proc=proc_sysbench, seconds=test_minutes*60)
except AttributeError:
# Assuming the sysbench process wasn't found and proc was set to None
LOG.error('Failed to find sysbench process', exc_info=True)
except KeyboardInterrupt:
aborted = True
hw_cpu.stop_sysbench(proc_sysbench, filehandle_sysbench)
# Update progress
# NOTE: CPU critical temp check isn't really necessary
# Hard to imagine it wasn't hit during Prime95 but was in sysbench
if sensors.cpu_reached_critical_temp() or aborted:
test_cooling_obj.set_status('Aborted')
test_mprime_obj.set_status('Aborted')
state.update_progress_file()
# Check Cooling results
test_cooling_obj.report.append(ansi.color_string('Temps', 'BLUE'))
hw_cpu.check_cooling_results(test_cooling_obj, sensors, run_sysbench)
def cpu_tests_end(state: State) -> None:
"""End CPU tests."""
# Cleanup # Cleanup
state.update_progress_file() state.sensors.clear_temps(next_label='Done')
sensors.stop_background_monitor() state.sensors.stop_background_monitor()
state.ui.clear_current_pane_height() state.ui.clear_current_pane_height()
state.ui.remove_all_info_panes() state.ui.remove_all_info_panes()
state.ui.remove_all_worker_panes() state.ui.remove_all_worker_panes()
def cpu_test_cooling(state: State, test_object, test_mode=False) -> None:
"""CPU cooling test via sensor data assessment."""
_ = test_mode
LOG.info('CPU Test (Cooling)')
# Bail early
if test_object.disabled:
return
hw_cpu.check_cooling_results(state.sensors, test_object)
state.update_progress_file()
def cpu_test_mprime(state: State, test_object, test_mode=False) -> None:
"""CPU stress test using mprime."""
LOG.info('CPU Test (Prime95)')
aborted = False
log_path = pathlib.Path(f'{state.log_dir}/prime.log')
sensors_out = pathlib.Path(f'{state.log_dir}/sensors.out')
test_minutes = cfg.hw.CPU_TEST_MINUTES
if test_mode:
test_minutes = cfg.hw.TEST_MODE_CPU_LIMIT
# Bail early
if test_object.disabled:
return
if state.sensors.cpu_reached_critical_temp():
test_object.set_status('Denied')
test_object.disabled = True
return
# Prep
test_object.set_status('Working')
state.update_progress_file()
state.ui.clear_current_pane()
cli.print_info('Running stress test')
print('')
# Start sensors monitor
state.sensors.clear_temps(next_label='Prime95')
state.sensors.stop_background_monitor()
state.sensors.start_background_monitor(
sensors_out,
alt_max='Prime95',
thermal_action=('killall', '-INT', 'mprime'),
)
# Run Prime95
hw_cpu.set_apple_fan_speed('max')
proc = hw_cpu.start_mprime(state.log_dir, log_path)
state.ui.add_worker_pane(lines=10, watch_cmd='tail', watch_file=log_path)
try:
print_countdown(proc=proc, seconds=test_minutes*60)
except KeyboardInterrupt:
aborted = True
# Stop Prime95
hw_cpu.stop_mprime(proc)
# Get cooldown temp
if 'Cooldown' in state.sensors.temp_labels:
# Give Prime95 time to save the results
std.sleep(1)
state.sensors.clear_temps(next_label='Cooldown')
else:
# Save cooldown temp
state.ui.clear_current_pane()
cli.print_standard('Letting CPU cooldown...')
std.sleep(5)
cli.print_standard('Saving cooldown temps...')
state.sensors.save_average_temps(temp_label='Cooldown', seconds=5)
# Check Prime95 results
test_object.report.append(ansi.color_string('Prime95', 'BLUE'))
hw_cpu.check_mprime_results(test_obj=test_object, working_dir=state.log_dir)
# Update progress
if state.sensors.cpu_reached_critical_temp() or aborted:
test_object.set_status('Aborted')
state.update_progress_file()
# Done # Done
state.ui.remove_all_worker_panes()
if aborted: if aborted:
cpu_tests_end(state)
raise std.GenericAbort('Aborted') raise std.GenericAbort('Aborted')
def disk_attribute_check(state, test_objects, test_mode=False) -> None: def cpu_test_sysbench(state: State, test_object, test_mode=False) -> None:
"""CPU stress test using Sysbench."""
LOG.info('CPU Test (Sysbench)')
aborted = False
log_path = pathlib.Path(f'{state.log_dir}/sysbench.log')
sensors_out = pathlib.Path(f'{state.log_dir}/sensors.out')
test_minutes = cfg.hw.CPU_TEST_MINUTES
if test_mode:
test_minutes = cfg.hw.TEST_MODE_CPU_LIMIT
# Bail early
if test_object.disabled:
return
# Prep
test_object.set_status('Working')
state.update_progress_file()
state.ui.clear_current_pane()
cli.print_info('Running stress test')
print('')
# Start sensors monitor
state.sensors.clear_temps(next_label='Sysbench')
state.sensors.stop_background_monitor()
state.sensors.start_background_monitor(
sensors_out,
alt_max='Sysbench',
thermal_action=('killall', '-INT', 'sysbench'),
)
# Run sysbench
state.ui.add_worker_pane(lines=10, watch_cmd='tail', watch_file=log_path)
proc, filehandle = hw_cpu.start_sysbench(log_path=log_path)
try:
print_countdown(proc=proc, seconds=test_minutes*60)
except AttributeError:
# Assuming the sysbench process wasn't found and proc was set to None
LOG.error('Failed to find sysbench process', exc_info=True)
except KeyboardInterrupt:
aborted = True
hw_cpu.stop_sysbench(proc, filehandle)
# Get cooldown temp
if 'Cooldown' in state.sensors.temp_labels:
state.sensors.clear_temps(next_label='Cooldown')
else:
state.ui.clear_current_pane()
cli.print_standard('Letting CPU cooldown...')
std.sleep(5)
cli.print_standard('Saving cooldown temps...')
state.sensors.save_average_temps(temp_label='Cooldown', seconds=5)
# Update progress
test_object.report.append(ansi.color_string('Sysbench', 'BLUE'))
if aborted:
test_object.set_status('Aborted')
test_object.report.append(ansi.color_string(' Aborted.', 'YELLOW'))
state.update_progress_file()
elif state.sensors.cpu_reached_critical_temp():
test_object.set_status('Aborted')
test_object.report.append(
ansi.color_string(' Aborted due to temps.', 'YELLOW'),
)
elif proc.returncode not in (-15, -2, 0):
# NOTE: Return codes:
# 0 == Completed w/out issue
# -2 == Stopped with INT signal
# -15 == Stopped with TERM signal
test_object.set_status('Failed')
test_object.report.append(f' Failed with return code: {proc.returncode}')
else:
test_object.set_status('Passed')
test_object.report.append(' Completed without issue.')
state.update_progress_file()
# Done
state.ui.remove_all_worker_panes()
if aborted:
cpu_tests_end(state)
raise std.GenericAbort('Aborted')
def disk_attribute_check(state: State, test_objects, test_mode=False) -> None:
"""Disk attribute check.""" """Disk attribute check."""
_ = test_mode
LOG.info('Disk Attribute Check') LOG.info('Disk Attribute Check')
for test in test_objects: for test in test_objects:
disk_smart_status_check(test.dev, mid_run=False) disk_smart_status_check(test.dev, mid_run=False)
@ -510,8 +616,9 @@ def disk_io_benchmark(
raise std.GenericAbort('Aborted') raise std.GenericAbort('Aborted')
def disk_self_test(state, test_objects, test_mode=False) -> None: def disk_self_test(state: State, test_objects, test_mode=False) -> None:
"""Disk self-test if available.""" """Disk self-test if available."""
_ = test_mode
LOG.info('Disk Self-Test(s)') LOG.info('Disk Self-Test(s)')
aborted = False aborted = False
threads = [] threads = []
@ -534,7 +641,7 @@ def disk_self_test(state, test_objects, test_mode=False) -> None:
# Show progress # Show progress
if threads[-1].is_alive(): if threads[-1].is_alive():
state.ui.add_worker_pane(lines=4, watch_cmd='tail', watch_file=test_log) state.ui.add_worker_pane(lines=4, watch_file=test_log)
# Wait for all tests to complete # Wait for all tests to complete
state.update_progress_file() state.update_progress_file()
@ -602,7 +709,7 @@ def disk_smart_status_check(dev, mid_run=True) -> None:
dev.disable_disk_tests() dev.disable_disk_tests()
def disk_surface_scan(state, test_objects, test_mode=False) -> None: def disk_surface_scan(state: State, test_objects, test_mode=False) -> None:
"""Read-only disk surface scan using badblocks.""" """Read-only disk surface scan using badblocks."""
LOG.info('Disk Surface Scan (badblocks)') LOG.info('Disk Surface Scan (badblocks)')
aborted = False aborted = False
@ -658,7 +765,12 @@ def disk_surface_scan(state, test_objects, test_mode=False) -> None:
def main() -> None: def main() -> None:
"""Main function for hardware diagnostics.""" """Main function for hardware diagnostics."""
args = docopt(DOCSTRING) try:
args = argparse_helper()
except SystemExit:
print('')
cli.pause('Press Enter to exit...')
raise
log.update_log_path(dest_name='Hardware-Diagnostics', timestamp=True) log.update_log_path(dest_name='Hardware-Diagnostics', timestamp=True)
# Safety check # Safety check
@ -754,8 +866,18 @@ def print_countdown(proc, seconds) -> None:
# Done # Done
print('') print('')
def run_cpu_tests(state: State, test_objects, test_mode=False) -> None:
"""Run selected CPU test(s)."""
state.update_progress_file()
cpu_tests_init(state)
for obj in test_objects:
func = globals()[TEST_GROUPS[obj.name]]
func(state, obj, test_mode=test_mode)
cpu_tests_end(state)
state.update_progress_file()
def run_diags(state, menu, quick_mode=False, test_mode=False) -> None:
def run_diags(state: State, menu, quick_mode=False, test_mode=False) -> None:
"""Run selected diagnostics.""" """Run selected diagnostics."""
aborted = False aborted = False
atexit.register(state.save_debug_reports) atexit.register(state.save_debug_reports)
@ -782,6 +904,7 @@ def run_diags(state, menu, quick_mode=False, test_mode=False) -> None:
aborted = True aborted = True
state.abort_testing() state.abort_testing()
state.update_progress_file() state.update_progress_file()
state.reset_layout()
break break
else: else:
# Run safety checks after disk tests # Run safety checks after disk tests
@ -807,7 +930,7 @@ def run_diags(state, menu, quick_mode=False, test_mode=False) -> None:
cli.pause('Press Enter to return to main menu...') cli.pause('Press Enter to return to main menu...')
def show_failed_attributes(state) -> None: def show_failed_attributes(state: State) -> None:
"""Show failed attributes for all disks.""" """Show failed attributes for all disks."""
for dev in state.disks: for dev in state.disks:
cli.print_colored([dev.name, dev.description], ['CYAN', None]) cli.print_colored([dev.name, dev.description], ['CYAN', None])
@ -817,7 +940,7 @@ def show_failed_attributes(state) -> None:
cli.print_standard('') cli.print_standard('')
def show_results(state) -> None: def show_results(state: State) -> None:
"""Show test results by device.""" """Show test results by device."""
std.sleep(0.5) std.sleep(0.5)
state.ui.clear_current_pane() state.ui.clear_current_pane()

View file

@ -1,7 +1,6 @@
"""WizardKit: Disk object and functions""" """WizardKit: Disk object and functions"""
# vim: sts=2 sw=2 ts=2 # vim: sts=2 sw=2 ts=2
import copy
import logging import logging
import pathlib import pathlib
import platform import platform
@ -9,10 +8,9 @@ import plistlib
import re import re
from dataclasses import dataclass, field from dataclasses import dataclass, field
from typing import Any, Union from typing import Any
from wk.cfg.main import KIT_NAME_SHORT from wk.cfg.main import KIT_NAME_SHORT
from wk.cfg.python import DATACLASS_DECORATOR_KWARGS
from wk.exe import get_json_from_command, run_program from wk.exe import get_json_from_command, run_program
from wk.hw.test import Test from wk.hw.test import Test
from wk.hw.smart import ( from wk.hw.smart import (
@ -32,7 +30,7 @@ WK_LABEL_REGEX = re.compile(
# Classes # Classes
@dataclass(**DATACLASS_DECORATOR_KWARGS) @dataclass(slots=True)
class Disk: class Disk:
"""Object for tracking disk specific data.""" """Object for tracking disk specific data."""
attributes: dict[Any, dict] = field(init=False, default_factory=dict) attributes: dict[Any, dict] = field(init=False, default_factory=dict)
@ -40,34 +38,29 @@ class Disk:
children: list[dict] = field(init=False, default_factory=list) children: list[dict] = field(init=False, default_factory=list)
description: str = field(init=False) description: str = field(init=False)
filesystem: str = field(init=False) filesystem: str = field(init=False)
initial_attributes: dict[Any, dict] = field(init=False) initial_attributes: dict[Any, dict] = field(init=False, default_factory=dict)
known_attributes: dict[Any, dict] = field(init=False, default_factory=dict) known_attributes: dict[Any, dict] = field(init=False, default_factory=dict)
log_sec: int = field(init=False) log_sec: int = field(init=False)
model: str = field(init=False) model: str = field(init=False)
name: str = field(init=False) name: str = field(init=False)
notes: list[str] = field(init=False, default_factory=list) notes: list[str] = field(init=False, default_factory=list)
path: Union[pathlib.Path, str] path: pathlib.Path = field(init=False)
path_str: pathlib.Path | str
parent: str = field(init=False) parent: str = field(init=False)
phy_sec: int = field(init=False) phy_sec: int = field(init=False)
raw_details: dict[str, Any] = field(init=False) raw_details: dict[str, Any] = field(init=False)
raw_smartctl: dict[str, Any] = field(init=False) raw_smartctl: dict[str, Any] = field(init=False, default_factory=dict)
serial: str = field(init=False) serial: str = field(init=False)
size: int = field(init=False) size: int = field(init=False)
ssd: bool = field(init=False) ssd: bool = field(init=False)
tests: list[Test] = field(init=False, default_factory=list) tests: list[Test] = field(init=False, default_factory=list)
use_sat: bool = field(init=False, default=False) trim: bool = field(init=False)
def __post_init__(self) -> None: def __post_init__(self):
self.path = pathlib.Path(self.path).resolve() self.path = pathlib.Path(self.path_str).resolve()
self.update_details() self.update_details()
self.set_description() self.set_description()
self.known_attributes = get_known_disk_attributes(self.model) self.known_attributes = get_known_disk_attributes(self.model)
if not self.attributes and self.bus == 'USB':
# Try using SAT
LOG.warning('Using SAT for smartctl for %s', self.path)
self.notes = []
self.use_sat = True
self.initial_attributes = copy.deepcopy(self.attributes)
if not self.is_4k_aligned(): if not self.is_4k_aligned():
self.add_note('One or more partitions are not 4K aligned', 'YELLOW') self.add_note('One or more partitions are not 4K aligned', 'YELLOW')
@ -212,6 +205,7 @@ class Disk:
self.serial = self.raw_details.get('serial', 'Unknown Serial') self.serial = self.raw_details.get('serial', 'Unknown Serial')
self.size = self.raw_details.get('size', -1) self.size = self.raw_details.get('size', -1)
self.ssd = self.raw_details.get('ssd', False) self.ssd = self.raw_details.get('ssd', False)
self.trim = self.raw_details.get('trim', False)
# Ensure certain attributes types # Ensure certain attributes types
## NOTE: This is ugly, deal. ## NOTE: This is ugly, deal.
@ -225,6 +219,10 @@ class Disk:
if attr == 'size': if attr == 'size':
setattr(self, attr, -1) setattr(self, attr, -1)
# Add TRIM note
if self.trim:
self.add_note('TRIM support detected', 'YELLOW')
# Functions # Functions
def get_disk_details_linux(disk_path, skip_children=True) -> dict[Any, Any]: def get_disk_details_linux(disk_path, skip_children=True) -> dict[Any, Any]:
@ -248,10 +246,12 @@ def get_disk_details_linux(disk_path, skip_children=True) -> dict[Any, Any]:
dev['bus'] = dev.pop('tran', '???') dev['bus'] = dev.pop('tran', '???')
dev['parent'] = dev.pop('pkname', None) dev['parent'] = dev.pop('pkname', None)
dev['ssd'] = not dev.pop('rota', True) dev['ssd'] = not dev.pop('rota', True)
dev['trim'] = bool(dev.pop('disc-max', 0))
if 'loop' in str(disk_path) and dev['bus'] is None: if 'loop' in str(disk_path) and dev['bus'] is None:
dev['bus'] = 'Image' dev['bus'] = 'Image'
dev['model'] = '' dev['model'] = ''
dev['serial'] = '' dev['serial'] = ''
dev['trim'] = False # NOTE: This check is just for physical devices
# Convert to dict # Convert to dict
details = dev_list.pop(0) details = dev_list.pop(0)
@ -309,6 +309,7 @@ def get_disk_details_macos(disk_path, skip_children=True) -> dict:
dev['serial'] = get_disk_serial_macos(dev['path']) dev['serial'] = get_disk_serial_macos(dev['path'])
dev['size'] = dev.pop('Size', -1) dev['size'] = dev.pop('Size', -1)
dev['ssd'] = dev.pop('SolidState', False) dev['ssd'] = dev.pop('SolidState', False)
dev['trim'] = False # TODO: ACtually check for TRIM
dev['vendor'] = '' dev['vendor'] = ''
if dev.get('WholeDisk', True): if dev.get('WholeDisk', True):
dev['parent'] = None dev['parent'] = None

View file

@ -6,10 +6,12 @@ import logging
import pathlib import pathlib
import re import re
from copy import deepcopy
from subprocess import CalledProcessError from subprocess import CalledProcessError
from threading import Thread
from typing import Any from typing import Any
from wk.cfg.hw import CPU_CRITICAL_TEMP, SMC_IDS, TEMP_COLORS from wk.cfg.hw import CPU_TEMPS, SMC_IDS, TEMP_COLORS
from wk.exe import run_program, start_thread from wk.exe import run_program, start_thread
from wk.io import non_clobber_path from wk.io import non_clobber_path
from wk.std import PLATFORM, sleep from wk.std import PLATFORM, sleep
@ -35,39 +37,83 @@ class ThermalLimitReachedError(RuntimeError):
# Classes # Classes
class Sensors(): class Sensors():
"""Class for holding sensor specific data.""" """Class for holding sensor specific data.
def __init__(self):
self.background_thread = None
self.data = get_sensor_data()
self.out_path = None
def clear_temps(self) -> None: # Sensor data structure
#
# Section # CPUTemps / Other
# Adapters # coretemp / acpi / nvme / etc
# Sources # Core 1 / SODIMM / Sensor X / etc
# Label # temp1_input / etc (i.e. lm_sensor label)
# Max # 99.0
# X # 55.0 (where X is Idle/Current/Sysbench/etc)
# Temps # [39.0, 38.0, 40.0, 39.0, 38.0, ...]
#
# e.g.
# { 'CPUTemps': { 'coretemp-isa-0000': { 'Core 0': { 'Average': 44.5,
# 'Current': 44.0,
# 'Idle': 44.5,
# 'Label': 'temp2_input',
# 'Max': 45.0,
# 'Temps': [ 45.0,
# 45.0,
# ...,
# 42.0]}}}}
#
# Sensor history data structure
# [ ('Name of "run"', sensor_data_structure_described_above), ]
#
# e.g.
# [
# ( 'Idle',
# { 'CPUTemps': { 'coretemp-isa-0000': { 'Core 0': { 'Max': 45.0, ..., }}}}
# ),
# ( 'Sysbench',
# { 'CPUTemps': { 'coretemp-isa-0000': { 'Core 0': { 'Max': 85.0, ..., }}}}
# ),
# ]
"""
def __init__(self):
self.background_thread: Thread | None = None
self.data: dict[Any, Any] = get_sensor_data()
self.history: list[tuple[str, dict]] = []
self.history_index: dict[str, int] = {}
self.history_next_label: str = 'Idle'
self.out_path: pathlib.Path | str | None = None
self.temp_labels: set = set(['Current', 'Max'])
def clear_temps(self, next_label: str, save_history: bool = True) -> None:
"""Clear saved temps but keep structure""" """Clear saved temps but keep structure"""
prev_label = self.history_next_label
self.history_next_label = next_label
# Save history
if save_history:
cur_data = deepcopy(self.data)
# Calculate averages
for adapters in cur_data.values():
for sources in adapters.values():
for name in sources:
temp_list = sources[name]['Temps']
try:
sources[name]['Average'] = sum(temp_list) / len(temp_list)
except ZeroDivisionError:
LOG.error('Failed to calculate averate temp for %s', name)
sources[name]['Average'] = 0
# Add to history
self.history.append((prev_label, cur_data))
self.history_index[prev_label] = len(self.history) - 1
# Clear data
for adapters in self.data.values(): for adapters in self.data.values():
for sources in adapters.values(): for sources in adapters.values():
for source_data in sources.values(): for source_data in sources.values():
source_data['Temps'] = [] source_data['Temps'] = []
def cpu_max_temp(self) -> float:
"""Get max temp from any CPU source, returns float.
NOTE: If no temps are found this returns zero.
"""
max_temp = 0.0
# Check all CPU Temps
for section, adapters in self.data.items():
if not section.startswith('CPU'):
continue
for sources in adapters.values():
for source_data in sources.values():
max_temp = max(max_temp, source_data.get('Max', 0))
# Done
return max_temp
def cpu_reached_critical_temp(self) -> bool: def cpu_reached_critical_temp(self) -> bool:
"""Check if CPU reached CPU_CRITICAL_TEMP, returns bool.""" """Check if CPU exceeded critical temp, returns bool."""
for section, adapters in self.data.items(): for section, adapters in self.data.items():
if not section.startswith('CPU'): if not section.startswith('CPU'):
# Limit to CPU temps # Limit to CPU temps
@ -76,16 +122,22 @@ class Sensors():
# Ugly section # Ugly section
for sources in adapters.values(): for sources in adapters.values():
for source_data in sources.values(): for source_data in sources.values():
if source_data.get('Max', -1) >= CPU_CRITICAL_TEMP: if source_data.get('Max', -1) > CPU_TEMPS['Critical']:
return True return True
# Didn't return above so temps are within the threshold # Didn't return above so temps are within the threshold
return False return False
def generate_report( def generate_report(
self, *temp_labels, colored=True, only_cpu=False) -> list[str]: self,
*temp_labels: str,
colored: bool = True,
only_cpu: bool = False,
include_avg_for: list[str] | None = None,
) -> list[str]:
"""Generate report based on given temp_labels, returns list.""" """Generate report based on given temp_labels, returns list."""
report = [] report = []
include_avg_for = include_avg_for if include_avg_for else []
for section, adapters in sorted(self.data.items()): for section, adapters in sorted(self.data.items()):
if only_cpu and not section.startswith('CPU'): if only_cpu and not section.startswith('CPU'):
@ -99,6 +151,10 @@ class Sensors():
for label in temp_labels: for label in temp_labels:
if label != 'Current': if label != 'Current':
line += f' {label.lower()}: ' line += f' {label.lower()}: '
if label in include_avg_for:
avg_temp = self.get_avg_temp(
label, section, adapter, source, colored)
line += f'{avg_temp} / '
line += get_temp_str( line += get_temp_str(
source_data.get(label, '???'), source_data.get(label, '???'),
colored=colored, colored=colored,
@ -118,6 +174,32 @@ class Sensors():
# Done # Done
return report return report
def get_avg_temp(self, label, section, adapter, source, colored) -> str:
"""Get average temp from history, return str."""
# NOTE: This is Super-ugly
label_index = self.history_index[label]
avg_temp = self.history[label_index][1][section][adapter][source]['Average']
return get_temp_str(avg_temp, colored=colored)
def get_cpu_temp(self, label) -> float:
"""Get temp for label from any CPU source, returns float.
NOTE: This returns the highest value for the label.
NOTE 2: If no temps are found this returns zero.
"""
max_temp = 0.0
# Check all CPU Temps
for section, adapters in self.data.items():
if not section.startswith('CPU'):
continue
for sources in adapters.values():
for source_data in sources.values():
max_temp = max(max_temp, source_data.get(label, 0))
# Done
return float(max_temp)
def monitor_to_file( def monitor_to_file(
self, out_path, alt_max=None, self, out_path, alt_max=None,
exit_on_thermal_limit=True, temp_labels=None, exit_on_thermal_limit=True, temp_labels=None,
@ -135,6 +217,7 @@ class Sensors():
temp_labels = ['Current', 'Max'] temp_labels = ['Current', 'Max']
if alt_max: if alt_max:
temp_labels.append(alt_max) temp_labels.append(alt_max)
self.temp_labels.add(alt_max)
# Start loop # Start loop
while True: while True:
@ -154,9 +237,15 @@ class Sensors():
# Sleep before next loop # Sleep before next loop
sleep(0.5) sleep(0.5)
def save_average_temps(self, temp_label, seconds=10) -> None: def save_average_temps(
self,
temp_label: str,
seconds: int = 10,
save_history: bool = True,
) -> None:
"""Save average temps under temp_label over provided seconds..""" """Save average temps under temp_label over provided seconds.."""
self.clear_temps() self.clear_temps(next_label=temp_label, save_history=save_history)
self.temp_labels.add(temp_label)
# Get temps # Get temps
for _ in range(seconds): for _ in range(seconds):
@ -199,6 +288,10 @@ class Sensors():
def stop_background_monitor(self) -> None: def stop_background_monitor(self) -> None:
"""Stop background thread.""" """Stop background thread."""
# Bail early
if self.background_thread is None:
return
self.out_path.with_suffix('.stop').touch() self.out_path.with_suffix('.stop').touch()
self.background_thread.join() self.background_thread.join()
@ -209,6 +302,8 @@ class Sensors():
def update_sensor_data( def update_sensor_data(
self, alt_max=None, exit_on_thermal_limit=True) -> None: self, alt_max=None, exit_on_thermal_limit=True) -> None:
"""Update sensor data via OS-specific means.""" """Update sensor data via OS-specific means."""
if alt_max:
self.temp_labels.add(alt_max)
if PLATFORM == 'Darwin': if PLATFORM == 'Darwin':
self.update_sensor_data_macos(alt_max, exit_on_thermal_limit) self.update_sensor_data_macos(alt_max, exit_on_thermal_limit)
elif PLATFORM == 'Linux': elif PLATFORM == 'Linux':
@ -235,7 +330,7 @@ class Sensors():
# Raise exception if thermal limit reached # Raise exception if thermal limit reached
if exit_on_thermal_limit and section == 'CPUTemps': if exit_on_thermal_limit and section == 'CPUTemps':
if source_data['Current'] >= CPU_CRITICAL_TEMP: if source_data['Current'] > CPU_TEMPS['Critical']:
raise ThermalLimitReachedError('CPU temps reached limit') raise ThermalLimitReachedError('CPU temps reached limit')
def update_sensor_data_macos( def update_sensor_data_macos(
@ -262,7 +357,7 @@ class Sensors():
# Raise exception if thermal limit reached # Raise exception if thermal limit reached
if exit_on_thermal_limit and section == 'CPUTemps': if exit_on_thermal_limit and section == 'CPUTemps':
if source_data['Current'] >= CPU_CRITICAL_TEMP: if source_data['Current'] > CPU_TEMPS['Critical']:
raise ThermalLimitReachedError('CPU temps reached limit') raise ThermalLimitReachedError('CPU temps reached limit')
@ -419,7 +514,7 @@ def get_sensor_data_macos() -> dict[Any, Any]:
def get_temp_str(temp, colored=True) -> str: def get_temp_str(temp, colored=True) -> str:
"""Get colored string based on temp, returns str.""" """Get colored string based on temp, returns str."""
temp_color = None temp_color = ''
# Safety check # Safety check
try: try:

View file

@ -42,25 +42,24 @@ def build_self_test_report(test_obj, aborted=False) -> None:
last known progress instead of just "was aborted by host." last known progress instead of just "was aborted by host."
""" """
report = [ansi.color_string('Self-Test', 'BLUE')] report = [ansi.color_string('Self-Test', 'BLUE')]
test_details = get_smart_self_test_details(test_obj.dev) test_result = get_smart_self_test_last_result(test_obj.dev)
test_result = test_details.get('status', {}).get('string', 'Unknown')
# Build report # Build report
if test_obj.disabled or test_obj.status == 'Denied': if test_obj.disabled or test_obj.status == 'Denied':
report.append(ansi.color_string(f' {test_obj.status}', 'RED')) report.append(ansi.color_string(f' {test_obj.status}', 'RED'))
elif test_obj.status == 'N/A' or not test_obj.dev.attributes: elif test_obj.status == 'N/A' or not test_obj.dev.attributes:
report.append(ansi.color_string(f' {test_obj.status}', 'YELLOW')) report.append(ansi.color_string(f' {test_obj.status}', 'YELLOW'))
elif test_obj.status == 'TestInProgress':
report.append(ansi.color_string(' Failed to stop previous test', 'RED'))
test_obj.set_status('Failed')
else: else:
# Other cases include self-test result string # Other cases include self-test result string
report.append(f' {test_result.capitalize()}') if test_obj.status == 'TestInProgress':
if aborted and not (test_obj.passed or test_obj.failed): report.append(ansi.color_string(' Failed to stop previous test', 'RED'))
report.append(ansi.color_string(' Aborted', 'YELLOW')) test_obj.set_status('Failed')
test_obj.set_status('Aborted')
elif test_obj.status == 'TimedOut': elif test_obj.status == 'TimedOut':
report.append(ansi.color_string(' TimedOut', 'YELLOW')) report.append(ansi.color_string(' TimedOut', 'YELLOW'))
elif aborted and not (test_obj.passed or test_obj.failed):
report.append(ansi.color_string(' Aborted', 'YELLOW'))
test_obj.set_status('Aborted')
report.append(f' {test_result}')
# Done # Done
test_obj.report.extend(report) test_obj.report.extend(report)
@ -105,7 +104,7 @@ def enable_smart(dev) -> None:
cmd = [ cmd = [
'sudo', 'sudo',
'smartctl', 'smartctl',
f'--device={"sat,auto" if dev.use_sat else "auto"}', '--device=auto',
'--tolerance=permissive', '--tolerance=permissive',
'--smart=on', '--smart=on',
dev.path, dev.path,
@ -201,7 +200,7 @@ def get_attribute_value_string(dev, attr) -> str:
return value_str return value_str
def get_known_disk_attributes(model) -> None: def get_known_disk_attributes(model) -> dict[str | int, dict[str, Any]]:
"""Get known disk attributes based on the device model.""" """Get known disk attributes based on the device model."""
known_attributes = copy.deepcopy(KNOWN_DISK_ATTRIBUTES) known_attributes = copy.deepcopy(KNOWN_DISK_ATTRIBUTES)
@ -219,7 +218,7 @@ def get_known_disk_attributes(model) -> None:
return known_attributes return known_attributes
def get_smart_self_test_details(dev) -> dict[Any, Any]: def get_smart_self_test_details(dev) -> dict[str, Any]:
"""Shorthand to get deeply nested self-test details, returns dict.""" """Shorthand to get deeply nested self-test details, returns dict."""
details = {} details = {}
try: try:
@ -232,6 +231,33 @@ def get_smart_self_test_details(dev) -> dict[Any, Any]:
return details return details
def get_smart_self_test_last_result(dev) -> str:
"""Get last SMART self-test result, returns str."""
result = 'Unknown'
# Parse SMART data
data = dev.raw_smartctl.get(
'ata_smart_self_test_log', {}).get(
'standard', {}).get(
'table', [])
try:
data = data[0]
except IndexError:
# No results found
return result
# Build result string
result = (
f'Power-on hours: {data.get("lifetime_hours", "?")}'
f', Type: {data.get("type", {}).get("string", "?")}'
f', Passed: {data.get("status", {}).get("passed", "?")}'
f', Result: {data.get("status", {}).get("string", "?")}'
)
# Done
return result
def monitor_smart_self_test(test_obj, header_str, log_path) -> bool: def monitor_smart_self_test(test_obj, header_str, log_path) -> bool:
"""Monitor SMART self-test status and update test_obj, returns bool.""" """Monitor SMART self-test status and update test_obj, returns bool."""
started = False started = False
@ -263,6 +289,9 @@ def monitor_smart_self_test(test_obj, header_str, log_path) -> bool:
if _i * 5 >= SMART_SELF_TEST_START_TIMEOUT_IN_SECONDS: if _i * 5 >= SMART_SELF_TEST_START_TIMEOUT_IN_SECONDS:
# Test didn't start within limit, stop waiting # Test didn't start within limit, stop waiting
abort_self_test(test_obj.dev) abort_self_test(test_obj.dev)
result = get_smart_self_test_last_result(test_obj.dev)
if result == 'Unknown':
result = 'SMART self-test failed to start'
test_obj.failed = True test_obj.failed = True
test_obj.set_status('TimedOut') test_obj.set_status('TimedOut')
break break
@ -278,6 +307,11 @@ def monitor_smart_self_test(test_obj, header_str, log_path) -> bool:
finished = True finished = True
break break
# Check if timed out
if started and not finished:
test_obj.failed = True
test_obj.set_status('TimedOut')
# Done # Done
return finished return finished
@ -291,8 +325,8 @@ def run_self_test(test_obj, log_path) -> None:
run_smart_self_test(test_obj, log_path) run_smart_self_test(test_obj, log_path)
def run_smart_self_test(test_obj, log_path) -> bool: def run_smart_self_test(test_obj, log_path) -> None:
"""Run SMART self-test and check if it passed, returns bool. """Run SMART self-test and check if it passed, returns None.
NOTE: An exception will be raised if the disk lacks SMART support. NOTE: An exception will be raised if the disk lacks SMART support.
""" """
@ -349,11 +383,15 @@ def run_smart_self_test(test_obj, log_path) -> bool:
# Check result # Check result
if finished: if finished:
test_details = get_smart_self_test_details(test_obj.dev)
test_obj.passed = test_details.get('status', {}).get('passed', False) test_obj.passed = test_details.get('status', {}).get('passed', False)
test_obj.failed = test_obj.failed or not test_obj.passed test_obj.failed = test_obj.failed or not test_obj.passed
# Set status # Set status
if test_obj.failed and test_obj.status != 'TimedOut': if test_obj.status == 'TimedOut':
# Preserve TimedOut status
pass
elif test_obj.failed:
test_obj.set_status('Failed') test_obj.set_status('Failed')
elif test_obj.passed: elif test_obj.passed:
test_obj.set_status('Passed') test_obj.set_status('Passed')
@ -423,7 +461,7 @@ def update_smart_details(dev) -> None:
cmd = [ cmd = [
'sudo', 'sudo',
'smartctl', 'smartctl',
f'--device={"sat,auto" if dev.use_sat else "auto"}', '--device=auto',
'--tolerance=verypermissive', '--tolerance=verypermissive',
'--all', '--all',
'--json', '--json',
@ -468,6 +506,10 @@ def update_smart_details(dev) -> None:
if not updated_attributes: if not updated_attributes:
dev.add_note('No NVMe or SMART data available', 'YELLOW') dev.add_note('No NVMe or SMART data available', 'YELLOW')
# Update iniital_attributes if needed
if not dev.initial_attributes:
dev.initial_attributes = copy.deepcopy(updated_attributes)
# Done # Done
dev.attributes.update(updated_attributes) dev.attributes.update(updated_attributes)

View file

@ -9,7 +9,6 @@ from dataclasses import dataclass, field
from typing import Any from typing import Any
from wk.cfg.hw import KNOWN_RAM_VENDOR_IDS from wk.cfg.hw import KNOWN_RAM_VENDOR_IDS
from wk.cfg.python import DATACLASS_DECORATOR_KWARGS
from wk.exe import get_json_from_command, run_program from wk.exe import get_json_from_command, run_program
from wk.hw.test import Test from wk.hw.test import Test
from wk.std import PLATFORM, bytes_to_string, string_to_bytes from wk.std import PLATFORM, bytes_to_string, string_to_bytes
@ -20,7 +19,7 @@ from wk.ui import ansi
LOG = logging.getLogger(__name__) LOG = logging.getLogger(__name__)
@dataclass(**DATACLASS_DECORATOR_KWARGS) @dataclass(slots=True)
class System: class System:
"""Object for tracking system specific hardware data.""" """Object for tracking system specific hardware data."""
cpu_description: str = field(init=False) cpu_description: str = field(init=False)

View file

@ -4,9 +4,7 @@
from dataclasses import dataclass, field from dataclasses import dataclass, field
from typing import Any, Callable from typing import Any, Callable
from wk.cfg.python import DATACLASS_DECORATOR_KWARGS @dataclass(slots=True)
@dataclass(**DATACLASS_DECORATOR_KWARGS)
class Test: class Test:
"""Object for tracking test specific data.""" """Object for tracking test specific data."""
dev: Any dev: Any
@ -14,7 +12,6 @@ class Test:
name: str name: str
disabled: bool = field(init=False, default=False) disabled: bool = field(init=False, default=False)
failed: bool = field(init=False, default=False) failed: bool = field(init=False, default=False)
hidden: bool = False
passed: bool = field(init=False, default=False) passed: bool = field(init=False, default=False)
report: list[str] = field(init=False, default_factory=list) report: list[str] = field(init=False, default_factory=list)
status: str = field(init=False, default='Pending') status: str = field(init=False, default='Pending')
@ -28,7 +25,7 @@ class Test:
self.status = status self.status = status
@dataclass(**DATACLASS_DECORATOR_KWARGS) @dataclass(slots=True)
class TestGroup: class TestGroup:
"""Object for tracking groups of tests.""" """Object for tracking groups of tests."""
name: str name: str

View file

@ -13,7 +13,7 @@ LOG = logging.getLogger(__name__)
# Functions # Functions
def case_insensitive_path(path): def case_insensitive_path(path: pathlib.Path | str) -> pathlib.Path:
"""Find path case-insensitively, returns pathlib.Path obj.""" """Find path case-insensitively, returns pathlib.Path obj."""
given_path = pathlib.Path(path).resolve() given_path = pathlib.Path(path).resolve()
real_path = None real_path = None
@ -37,12 +37,13 @@ def case_insensitive_path(path):
return real_path return real_path
def case_insensitive_search(path, item): def case_insensitive_search(
path: pathlib.Path | str, item: str) -> pathlib.Path:
"""Search path for item case insensitively, returns pathlib.Path obj.""" """Search path for item case insensitively, returns pathlib.Path obj."""
path = pathlib.Path(path).resolve() path = pathlib.Path(path).resolve()
given_path = path.joinpath(item) given_path = path.joinpath(item)
real_path = None real_path = None
regex = fr'^{item}' regex = fr'^{item}$'
# Quick check # Quick check
if given_path.exists(): if given_path.exists():
@ -61,7 +62,10 @@ def case_insensitive_search(path, item):
return real_path return real_path
def copy_file(source, dest, overwrite=False): def copy_file(
source: pathlib.Path | str,
dest: pathlib.Path | str,
overwrite: bool = False) -> None:
"""Copy file and optionally overwrite the destination.""" """Copy file and optionally overwrite the destination."""
source = case_insensitive_path(source) source = case_insensitive_path(source)
dest = pathlib.Path(dest).resolve() dest = pathlib.Path(dest).resolve()
@ -72,7 +76,7 @@ def copy_file(source, dest, overwrite=False):
shutil.copy2(source, dest) shutil.copy2(source, dest)
def delete_empty_folders(path): def delete_empty_folders(path: pathlib.Path | str) -> None:
"""Recursively delete all empty folders in path.""" """Recursively delete all empty folders in path."""
LOG.debug('path: %s', path) LOG.debug('path: %s', path)
@ -89,7 +93,11 @@ def delete_empty_folders(path):
pass pass
def delete_folder(path, force=False, ignore_errors=False): def delete_folder(
path: pathlib.Path | str,
force: bool = False,
ignore_errors: bool = False,
) -> None:
"""Delete folder if empty or if forced. """Delete folder if empty or if forced.
NOTE: Exceptions are not caught by this function, NOTE: Exceptions are not caught by this function,
@ -106,7 +114,11 @@ def delete_folder(path, force=False, ignore_errors=False):
os.rmdir(path) os.rmdir(path)
def delete_item(path, force=False, ignore_errors=False): def delete_item(
path: pathlib.Path | str,
force: bool = False,
ignore_errors: bool = False,
) -> None:
"""Delete file or folder, optionally recursively. """Delete file or folder, optionally recursively.
NOTE: Exceptions are not caught by this function, NOTE: Exceptions are not caught by this function,
@ -124,7 +136,11 @@ def delete_item(path, force=False, ignore_errors=False):
os.remove(path) os.remove(path)
def get_path_obj(path, expanduser=True, resolve=True): def get_path_obj(
path: pathlib.Path | str,
expanduser: bool = True,
resolve: bool = True,
) -> pathlib.Path:
"""Get based on path, returns pathlib.Path.""" """Get based on path, returns pathlib.Path."""
path = pathlib.Path(path) path = pathlib.Path(path)
if expanduser: if expanduser:
@ -134,7 +150,7 @@ def get_path_obj(path, expanduser=True, resolve=True):
return path return path
def non_clobber_path(path): def non_clobber_path(path: pathlib.Path | str) -> pathlib.Path:
"""Update path as needed to non-existing path, returns pathlib.Path.""" """Update path as needed to non-existing path, returns pathlib.Path."""
LOG.debug('path: %s', path) LOG.debug('path: %s', path)
path = pathlib.Path(path) path = pathlib.Path(path)
@ -163,7 +179,10 @@ def non_clobber_path(path):
return new_path return new_path
def recursive_copy(source, dest, overwrite=False): def recursive_copy(
source: pathlib.Path | str,
dest: pathlib.Path | str,
overwrite: bool = False) -> None:
"""Copy source to dest recursively. """Copy source to dest recursively.
NOTE: This uses rsync style source/dest syntax. NOTE: This uses rsync style source/dest syntax.
@ -213,7 +232,10 @@ def recursive_copy(source, dest, overwrite=False):
raise FileExistsError(f'Refusing to delete file: {dest}') raise FileExistsError(f'Refusing to delete file: {dest}')
def rename_item(path, new_path): def rename_item(
path: pathlib.Path | str,
new_path: pathlib.Path | str,
) -> pathlib.Path:
"""Rename item, returns pathlib.Path.""" """Rename item, returns pathlib.Path."""
path = pathlib.Path(path) path = pathlib.Path(path)
return path.rename(new_path) return path.rename(new_path)

View file

@ -6,6 +6,7 @@ NOTE: This script is meant to be called from within a new kit in ConEmu.
import logging import logging
import os import os
import pathlib
import re import re
from wk.cfg.launchers import LAUNCHERS from wk.cfg.launchers import LAUNCHERS
@ -44,7 +45,7 @@ WIDTH = 50
# Functions # Functions
def compress_cbin_dirs(): def compress_cbin_dirs() -> None:
"""Compress CBIN_DIR items using ARCHIVE_PASSWORD.""" """Compress CBIN_DIR items using ARCHIVE_PASSWORD."""
current_dir = os.getcwd() current_dir = os.getcwd()
for item in CBIN_DIR.iterdir(): for item in CBIN_DIR.iterdir():
@ -62,25 +63,25 @@ def compress_cbin_dirs():
delete_item(item, force=True, ignore_errors=True) delete_item(item, force=True, ignore_errors=True)
def delete_from_temp(item_path): def delete_from_temp(item_path) -> None:
"""Delete item from temp.""" """Delete item from temp."""
delete_item(TMP_DIR.joinpath(item_path), force=True, ignore_errors=True) delete_item(TMP_DIR.joinpath(item_path), force=True, ignore_errors=True)
def download_to_temp(filename, source_url, referer=None): def download_to_temp(filename, source_url, referer=None) -> pathlib.Path:
"""Download file to temp dir, returns pathlib.Path.""" """Download file to temp dir, returns pathlib.Path."""
out_path = TMP_DIR.joinpath(filename) out_path = TMP_DIR.joinpath(filename)
download_file(out_path, source_url, referer=referer) download_file(out_path, source_url, referer=referer)
return out_path return out_path
def extract_to_bin(archive, folder): def extract_to_bin(archive, folder) -> None:
"""Extract archive to folder under BIN_DIR.""" """Extract archive to folder under BIN_DIR."""
out_path = BIN_DIR.joinpath(folder) out_path = BIN_DIR.joinpath(folder)
extract_archive(archive, out_path) extract_archive(archive, out_path)
def generate_launcher(section, name, options): def generate_launcher(section, name, options) -> None:
"""Generate launcher script.""" """Generate launcher script."""
dest = ROOT_DIR.joinpath(f'{section+"/" if section else ""}{name}.cmd') dest = ROOT_DIR.joinpath(f'{section+"/" if section else ""}{name}.cmd')
out_text = [] out_text = []
@ -107,27 +108,27 @@ def generate_launcher(section, name, options):
# Download functions # Download functions
def download_adobe_reader(): def download_adobe_reader() -> None:
"""Download Adobe Reader.""" """Download Adobe Reader."""
out_path = INSTALLERS_DIR.joinpath('Adobe Reader DC.exe') out_path = INSTALLERS_DIR.joinpath('Adobe Reader DC.exe')
download_file(out_path, SOURCES['Adobe Reader DC']) download_file(out_path, SOURCES['Adobe Reader DC'])
def download_aida64(): def download_aida64() -> None:
"""Download AIDA64.""" """Download AIDA64."""
archive = download_to_temp('AIDA64.zip', SOURCES['AIDA64']) archive = download_to_temp('AIDA64.zip', SOURCES['AIDA64'])
extract_to_bin(archive, 'AIDA64') extract_to_bin(archive, 'AIDA64')
delete_from_temp('AIDA64.zip') delete_from_temp('AIDA64.zip')
def download_autoruns(): def download_autoruns() -> None:
"""Download Autoruns.""" """Download Autoruns."""
for item in ('Autoruns32', 'Autoruns64'): for item in ('Autoruns32', 'Autoruns64'):
out_path = BIN_DIR.joinpath(f'Sysinternals/{item}.exe') out_path = BIN_DIR.joinpath(f'Sysinternals/{item}.exe')
download_file(out_path, SOURCES[item]) download_file(out_path, SOURCES[item])
def download_bleachbit(): def download_bleachbit() -> None:
"""Download BleachBit.""" """Download BleachBit."""
out_path = BIN_DIR.joinpath('BleachBit') out_path = BIN_DIR.joinpath('BleachBit')
archive = download_to_temp('BleachBit.zip', SOURCES['BleachBit']) archive = download_to_temp('BleachBit.zip', SOURCES['BleachBit'])
@ -142,7 +143,7 @@ def download_bleachbit():
delete_from_temp('BleachBit.zip') delete_from_temp('BleachBit.zip')
def download_bluescreenview(): def download_bluescreenview() -> None:
"""Download BlueScreenView.""" """Download BlueScreenView."""
archive_32 = download_to_temp( archive_32 = download_to_temp(
'bluescreenview32.zip', SOURCES['BlueScreenView32'], 'bluescreenview32.zip', SOURCES['BlueScreenView32'],
@ -161,14 +162,37 @@ def download_bluescreenview():
delete_from_temp('bluescreenview64.zip') delete_from_temp('bluescreenview64.zip')
def download_erunt(): def download_ddu() -> None:
"""Download Display Driver Uninstaller."""
archive = download_to_temp('DDU.exe', SOURCES['DDU'])
out_path = BIN_DIR.joinpath('DDU')
extract_archive(archive, out_path, 'DDU*/*.*', mode='e')
out_path = out_path.joinpath('Settings')
for item in ('AMD', 'INTEL', 'Languages', 'NVIDIA', 'REALTEK'):
extract_archive(
archive,
out_path.joinpath(item),
f'DDU*/Settings/{item}/*',
mode='e',
)
delete_from_temp('DDU.exe')
def download_bcuninstaller() -> None:
"""Download Bulk Crap Uninstaller."""
archive = download_to_temp('BCU.zip', SOURCES['BCUninstaller'])
extract_to_bin(archive, 'BCUninstaller')
delete_from_temp('BCU.zip')
def download_erunt() -> None:
"""Download ERUNT.""" """Download ERUNT."""
archive = download_to_temp('erunt.zip', SOURCES['ERUNT']) archive = download_to_temp('erunt.zip', SOURCES['ERUNT'])
extract_to_bin(archive, 'ERUNT') extract_to_bin(archive, 'ERUNT')
delete_from_temp('erunt.zip') delete_from_temp('erunt.zip')
def download_everything(): def download_everything() -> None:
"""Download Everything.""" """Download Everything."""
archive_32 = download_to_temp('everything32.zip', SOURCES['Everything32']) archive_32 = download_to_temp('everything32.zip', SOURCES['Everything32'])
archive_64 = download_to_temp('everything64.zip', SOURCES['Everything64']) archive_64 = download_to_temp('everything64.zip', SOURCES['Everything64'])
@ -183,7 +207,7 @@ def download_everything():
delete_from_temp('everything64.zip') delete_from_temp('everything64.zip')
def download_fastcopy(): def download_fastcopy() -> None:
"""Download FastCopy.""" """Download FastCopy."""
installer = download_to_temp('FastCopyInstaller.exe', SOURCES['FastCopy']) installer = download_to_temp('FastCopyInstaller.exe', SOURCES['FastCopy'])
out_path = BIN_DIR.joinpath('FastCopy') out_path = BIN_DIR.joinpath('FastCopy')
@ -199,7 +223,7 @@ def download_fastcopy():
delete_item(BIN_DIR.joinpath('FastCopy/setup.exe')) delete_item(BIN_DIR.joinpath('FastCopy/setup.exe'))
def download_furmark(): def download_furmark() -> None:
"""Download FurMark.""" """Download FurMark."""
installer = download_to_temp( installer = download_to_temp(
'FurMark_Setup.exe', 'FurMark_Setup.exe',
@ -219,28 +243,32 @@ def download_furmark():
delete_from_temp('FurMarkInstall') delete_from_temp('FurMarkInstall')
def download_hwinfo(): def download_hwinfo() -> None:
"""Download HWiNFO.""" """Download HWiNFO."""
archive = download_to_temp('HWiNFO.zip', SOURCES['HWiNFO']) archive = download_to_temp('HWiNFO.zip', SOURCES['HWiNFO'])
extract_to_bin(archive, 'HWiNFO') extract_to_bin(archive, 'HWiNFO')
delete_from_temp('HWiNFO.zip') delete_from_temp('HWiNFO.zip')
def download_macs_fan_control(): def download_macs_fan_control() -> None:
"""Download Macs Fan Control.""" """Download Macs Fan Control."""
out_path = INSTALLERS_DIR.joinpath('Macs Fan Control.exe') out_path = INSTALLERS_DIR.joinpath('Macs Fan Control.exe')
download_file(out_path, SOURCES['Macs Fan Control']) download_file(out_path, SOURCES['Macs Fan Control'])
def download_libreoffice(): def download_libreoffice() -> None:
"""Download LibreOffice.""" """Download LibreOffice."""
for arch in 32, 64: for arch in 32, 64:
out_path = INSTALLERS_DIR.joinpath(f'LibreOffice{arch}.msi') out_path = INSTALLERS_DIR.joinpath(f'LibreOffice{arch}.msi')
download_file(out_path, SOURCES[f'LibreOffice{arch}']) download_file(
out_path,
SOURCES[f'LibreOffice{arch}'],
referer='https://www.libreoffice.org/download/download-libreoffice/',
)
ui.sleep(1) ui.sleep(1)
def download_neutron(): def download_neutron() -> None:
"""Download Neutron.""" """Download Neutron."""
archive = download_to_temp('neutron.zip', SOURCES['Neutron']) archive = download_to_temp('neutron.zip', SOURCES['Neutron'])
out_path = BIN_DIR.joinpath('Neutron') out_path = BIN_DIR.joinpath('Neutron')
@ -248,7 +276,7 @@ def download_neutron():
delete_from_temp('neutron.zip') delete_from_temp('neutron.zip')
def download_notepad_plus_plus(): def download_notepad_plus_plus() -> None:
"""Download Notepad++.""" """Download Notepad++."""
archive = download_to_temp('npp.7z', SOURCES['Notepad++']) archive = download_to_temp('npp.7z', SOURCES['Notepad++'])
extract_to_bin(archive, 'NotepadPlusPlus') extract_to_bin(archive, 'NotepadPlusPlus')
@ -260,21 +288,21 @@ def download_notepad_plus_plus():
delete_from_temp('npp.7z') delete_from_temp('npp.7z')
def download_openshell(): def download_openshell() -> None:
"""Download OpenShell installer and Fluent-Metro skin.""" """Download OpenShell installer and Fluent-Metro skin."""
for name in ('OpenShell.exe', 'Fluent-Metro.zip'): for name in ('OpenShell.exe', 'Fluent-Metro.zip'):
out_path = BIN_DIR.joinpath(f'OpenShell/{name}') out_path = BIN_DIR.joinpath(f'OpenShell/{name}')
download_file(out_path, SOURCES[name[:-4]]) download_file(out_path, SOURCES[name[:-4]])
def download_putty(): def download_putty() -> None:
"""Download PuTTY.""" """Download PuTTY."""
archive = download_to_temp('putty.zip', SOURCES['PuTTY']) archive = download_to_temp('putty.zip', SOURCES['PuTTY'])
extract_to_bin(archive, 'PuTTY') extract_to_bin(archive, 'PuTTY')
delete_from_temp('putty.zip') delete_from_temp('putty.zip')
def download_snappy_driver_installer_origin(): def download_snappy_driver_installer_origin() -> None:
"""Download Snappy Driver Installer Origin.""" """Download Snappy Driver Installer Origin."""
archive = download_to_temp('aria2.zip', SOURCES['Aria2']) archive = download_to_temp('aria2.zip', SOURCES['Aria2'])
aria2c = TMP_DIR.joinpath('aria2/aria2c.exe') aria2c = TMP_DIR.joinpath('aria2/aria2c.exe')
@ -344,29 +372,14 @@ def download_snappy_driver_installer_origin():
delete_from_temp('fake.7z') delete_from_temp('fake.7z')
def download_uninstallview(): def download_wiztree() -> None:
"""Download UninstallView."""
archive_32 = download_to_temp('uninstallview32.zip', SOURCES['UninstallView32'])
archive_64 = download_to_temp('uninstallview64.zip', SOURCES['UninstallView64'])
out_path = BIN_DIR.joinpath('UninstallView')
extract_archive(archive_64, out_path, 'UninstallView.exe')
rename_item(
out_path.joinpath('UninstallView.exe'),
out_path.joinpath('UninstallView64.exe'),
)
extract_archive(archive_32, out_path)
delete_from_temp('uninstallview32.zip')
delete_from_temp('uninstallview64.zip')
def download_wiztree():
"""Download WizTree.""" """Download WizTree."""
archive = download_to_temp('wiztree.zip', SOURCES['WizTree']) archive = download_to_temp('wiztree.zip', SOURCES['WizTree'])
extract_to_bin(archive, 'WizTree') extract_to_bin(archive, 'WizTree')
delete_from_temp('wiztree.zip') delete_from_temp('wiztree.zip')
def download_xmplay(): def download_xmplay() -> None:
"""Download XMPlay.""" """Download XMPlay."""
archives = [ archives = [
download_to_temp('xmplay.zip', SOURCES['XMPlay']), download_to_temp('xmplay.zip', SOURCES['XMPlay']),
@ -382,7 +395,7 @@ def download_xmplay():
args = [archive, BIN_DIR.joinpath('XMPlay/plugins')] args = [archive, BIN_DIR.joinpath('XMPlay/plugins')]
if archive.name == 'Innocuous.zip': if archive.name == 'Innocuous.zip':
args.append( args.append(
'Innocuous (v1.5)/Innocuous (Hue Shifted)/' 'Innocuous (v1.7)/Innocuous (Hue Shifted)/'
'Innocuous (Dark Skies - Purple-80) [L1].xmpskin' 'Innocuous (Dark Skies - Purple-80) [L1].xmpskin'
) )
extract_archive(*args, mode='e') extract_archive(*args, mode='e')
@ -394,7 +407,7 @@ def download_xmplay():
delete_from_temp('xmp-rar.zip') delete_from_temp('xmp-rar.zip')
delete_from_temp('Innocuous.zip') delete_from_temp('Innocuous.zip')
def download_xmplay_music(): def download_xmplay_music() -> None:
"""Download XMPlay Music.""" """Download XMPlay Music."""
music_tmp = TMP_DIR.joinpath('music') music_tmp = TMP_DIR.joinpath('music')
music_tmp.mkdir(exist_ok=True) music_tmp.mkdir(exist_ok=True)
@ -447,7 +460,7 @@ def download_xmplay_music():
# "Main" Function # "Main" Function
def build_kit(): def build_kit() -> None:
"""Build Kit.""" """Build Kit."""
update_log_path(dest_name='Build Tool', timestamp=True) update_log_path(dest_name='Build Tool', timestamp=True)
title = f'{KIT_NAME_FULL}: Build Tool' title = f'{KIT_NAME_FULL}: Build Tool'
@ -470,6 +483,8 @@ def build_kit():
try_print.run('BleachBit...', download_bleachbit) try_print.run('BleachBit...', download_bleachbit)
try_print.run('BlueScreenView...', download_bluescreenview) try_print.run('BlueScreenView...', download_bluescreenview)
try_print.run('ERUNT...', download_erunt) try_print.run('ERUNT...', download_erunt)
try_print.run('BulkCrapUninstaller...', download_bcuninstaller)
try_print.run('DDU...', download_ddu)
try_print.run('Everything...', download_everything) try_print.run('Everything...', download_everything)
try_print.run('FastCopy...', download_fastcopy) try_print.run('FastCopy...', download_fastcopy)
try_print.run('FurMark...', download_furmark) try_print.run('FurMark...', download_furmark)
@ -481,7 +496,6 @@ def build_kit():
try_print.run('OpenShell...', download_openshell) try_print.run('OpenShell...', download_openshell)
try_print.run('PuTTY...', download_putty) try_print.run('PuTTY...', download_putty)
try_print.run('Snappy Driver Installer...', download_snappy_driver_installer_origin) try_print.run('Snappy Driver Installer...', download_snappy_driver_installer_origin)
try_print.run('UninstallView...', download_uninstallview)
try_print.run('WizTree...', download_wiztree) try_print.run('WizTree...', download_wiztree)
try_print.run('XMPlay...', download_xmplay) try_print.run('XMPlay...', download_xmplay)
try_print.run('XMPlay Music...', download_xmplay_music) try_print.run('XMPlay Music...', download_xmplay_music)

View file

@ -1,11 +1,13 @@
"""WizardKit: Tool Functions""" """WizardKit: Tool Functions"""
# vim: sts=2 sw=2 ts=2 # vim: sts=2 sw=2 ts=2
from datetime import datetime, timedelta
import logging import logging
import pathlib import pathlib
import platform import platform
from datetime import datetime, timedelta
from subprocess import CompletedProcess, Popen
import requests import requests
from wk.cfg.main import ARCHIVE_PASSWORD from wk.cfg.main import ARCHIVE_PASSWORD
@ -30,7 +32,9 @@ CACHED_DIRS = {}
# Functions # Functions
def download_file(out_path, source_url, as_new=False, overwrite=False, referer=None): def download_file(
out_path, source_url,
as_new=False, overwrite=False, referer=None) -> pathlib.Path:
"""Download a file using requests, returns pathlib.Path.""" """Download a file using requests, returns pathlib.Path."""
out_path = pathlib.Path(out_path).resolve() out_path = pathlib.Path(out_path).resolve()
name = out_path.name name = out_path.name
@ -95,7 +99,7 @@ def download_file(out_path, source_url, as_new=False, overwrite=False, referer=N
return out_path return out_path
def download_tool(folder, name, suffix=None): def download_tool(folder, name, suffix=None) -> None:
"""Download tool.""" """Download tool."""
name_arch = f'{name}{ARCH}' name_arch = f'{name}{ARCH}'
out_path = get_tool_path(folder, name, check=False, suffix=suffix) out_path = get_tool_path(folder, name, check=False, suffix=suffix)
@ -130,7 +134,7 @@ def download_tool(folder, name, suffix=None):
raise raise
def extract_archive(archive, out_path, *args, mode='x', silent=True): def extract_archive(archive, out_path, *args, mode='x', silent=True) -> None:
"""Extract an archive to out_path.""" """Extract an archive to out_path."""
out_path = pathlib.Path(out_path).resolve() out_path = pathlib.Path(out_path).resolve()
out_path.parent.mkdir(parents=True, exist_ok=True) out_path.parent.mkdir(parents=True, exist_ok=True)
@ -142,7 +146,7 @@ def extract_archive(archive, out_path, *args, mode='x', silent=True):
run_program(cmd) run_program(cmd)
def extract_tool(folder): def extract_tool(folder) -> None:
"""Extract tool.""" """Extract tool."""
extract_archive( extract_archive(
find_kit_dir('.cbin').joinpath(folder).with_suffix('.7z'), find_kit_dir('.cbin').joinpath(folder).with_suffix('.7z'),
@ -151,7 +155,7 @@ def extract_tool(folder):
) )
def find_kit_dir(name=None): def find_kit_dir(name=None) -> pathlib.Path:
"""Find folder in kit, returns pathlib.Path. """Find folder in kit, returns pathlib.Path.
Search is performed in the script's path and then recursively upwards. Search is performed in the script's path and then recursively upwards.
@ -178,7 +182,7 @@ def find_kit_dir(name=None):
return cur_path return cur_path
def get_tool_path(folder, name, check=True, suffix=None): def get_tool_path(folder, name, check=True, suffix=None) -> pathlib.Path:
"""Get tool path, returns pathlib.Path""" """Get tool path, returns pathlib.Path"""
bin_dir = find_kit_dir('.bin') bin_dir = find_kit_dir('.bin')
if not suffix: if not suffix:
@ -203,7 +207,7 @@ def run_tool(
folder, name, *run_args, folder, name, *run_args,
cbin=False, cwd=False, download=False, popen=False, cbin=False, cwd=False, download=False, popen=False,
**run_kwargs, **run_kwargs,
): ) -> CompletedProcess | Popen:
"""Run tool from the kit or the Internet, returns proc obj. """Run tool from the kit or the Internet, returns proc obj.
proc will be either subprocess.CompletedProcess or subprocess.Popen.""" proc will be either subprocess.CompletedProcess or subprocess.Popen."""

View file

@ -1,15 +1,15 @@
"""WizardKit: UFD Functions""" """WizardKit: UFD Functions"""
# vim: sts=2 sw=2 ts=2 # vim: sts=2 sw=2 ts=2
import argparse
import logging import logging
import math import math
import os import os
import pathlib
import re
import shutil import shutil
from subprocess import CalledProcessError from subprocess import CalledProcessError
from collections import OrderedDict
from docopt import docopt
from wk import io, log from wk import io, log
from wk.cfg.main import KIT_NAME_FULL, KIT_NAME_SHORT from wk.cfg.main import KIT_NAME_FULL, KIT_NAME_SHORT
from wk.cfg.ufd import ( from wk.cfg.ufd import (
@ -17,6 +17,7 @@ from wk.cfg.ufd import (
BOOT_FILES, BOOT_FILES,
IMAGE_BOOT_ENTRIES, IMAGE_BOOT_ENTRIES,
ITEMS, ITEMS,
ITEMS_FROM_LIVE,
ITEMS_HIDDEN, ITEMS_HIDDEN,
SOURCES, SOURCES,
) )
@ -27,30 +28,6 @@ from wk.ui import cli as ui
# STATIC VARIABLES # STATIC VARIABLES
DOCSTRING = '''WizardKit: Build UFD
Usage:
build-ufd [options] --ufd-device PATH
[--linux PATH]
[--main-kit PATH]
[--winpe PATH]
[--extra-dir PATH]
[EXTRA_IMAGES...]
build-ufd (-h | --help)
Options:
-e PATH, --extra-dir PATH
-k PATH, --main-kit PATH
-l PATH, --linux PATH
-u PATH, --ufd-device PATH
-w PATH, --winpe PATH
-d --debug Enable debug mode
-h --help Show this page
-M --use-mbr Use real MBR instead of GPT w/ Protective MBR
-F --force Bypass all confirmation messages. USE WITH EXTREME CAUTION!
-U --update Don't format device, just update
'''
LOG = logging.getLogger(__name__) LOG = logging.getLogger(__name__)
EXTRA_IMAGES_LIST = '/mnt/UFD/arch/extra_images.list' EXTRA_IMAGES_LIST = '/mnt/UFD/arch/extra_images.list'
MIB = 1024 ** 2 MIB = 1024 ** 2
@ -59,7 +36,55 @@ UFD_LABEL = f'{KIT_NAME_SHORT}_UFD'
# Functions # Functions
def apply_image(part_path, image_path, hide_macos_boot=True): def argparse_helper() -> dict[str, None|bool|str]:
"""Helper function to setup and return args, returns dict.
NOTE: A dict is used to match the legacy code.
"""
parser = argparse.ArgumentParser(
prog='build-ufd',
description=f'{KIT_NAME_FULL}: Build UFD',
)
parser.add_argument('-u', '--ufd-device', required=True)
parser.add_argument('-l', '--linux', required=False)
parser.add_argument('-e', '--extra-dir', required=False)
parser.add_argument('-k', '--main-kit', required=False)
parser.add_argument('-w', '--winpe', required=False)
parser.add_argument(
'-d', '--debug', action='store_true',
help='Enable debug mode',
)
parser.add_argument(
'-M', '--use-mbr', action='store_true',
help='Use real MBR instead of GPT w/ Protective MBR',
)
parser.add_argument(
'-F', '--force', action='store_true',
help='Bypass all confirmation messages. USE WITH EXTREME CAUTION!',
)
parser.add_argument(
'-U', '--update', action='store_true',
help="Don't format device, just update",
)
parser.add_argument(
'EXTRA_IMAGES', nargs='*',
)
args = parser.parse_args()
legacy_args = {
'--debug': args.debug,
'--extra-dir': args.extra_dir,
'--force': args.force,
'--linux': args.linux,
'--main-kit': args.main_kit,
'--ufd-device': args.ufd_device,
'--update': args.update,
'--use-mbr': args.use_mbr,
'--winpe': args.winpe,
'EXTRA_IMAGES': args.EXTRA_IMAGES,
}
return legacy_args
def apply_image(part_path, image_path, hide_macos_boot=True) -> None:
"""Apply raw image to dev_path using dd.""" """Apply raw image to dev_path using dd."""
cmd = [ cmd = [
'sudo', 'sudo',
@ -89,9 +114,14 @@ def apply_image(part_path, image_path, hide_macos_boot=True):
linux.unmount(source_or_mountpoint='/mnt/TMP') linux.unmount(source_or_mountpoint='/mnt/TMP')
def build_ufd(): def build_ufd() -> None:
"""Build UFD using selected sources.""" """Build UFD using selected sources."""
args = docopt(DOCSTRING) try:
args = argparse_helper()
except SystemExit:
print('')
ui.pause('Press Enter to exit...')
raise
if args['--debug']: if args['--debug']:
log.enable_debug_mode() log.enable_debug_mode()
if args['--update'] and args['EXTRA_IMAGES']: if args['--update'] and args['EXTRA_IMAGES']:
@ -122,7 +152,7 @@ def build_ufd():
if not args['--update']: if not args['--update']:
ui.print_info('Prep UFD') ui.print_info('Prep UFD')
try_print.run( try_print.run(
message='Zeroing first 64MiB...', message='Zeroing first 1MiB...',
function=zero_device, function=zero_device,
dev_path=ufd_dev, dev_path=ufd_dev,
) )
@ -145,6 +175,13 @@ def build_ufd():
dev_path=ufd_dev, dev_path=ufd_dev,
label=UFD_LABEL, label=UFD_LABEL,
) )
try_print.run(
message='Hiding extra partition(s)...',
function=hide_extra_partitions,
dev_path=ufd_dev,
num_parts=len(extra_images),
use_mbr=args['--use-mbr'],
)
ufd_dev_first_partition = find_first_partition(ufd_dev) ufd_dev_first_partition = find_first_partition(ufd_dev)
# Mount UFD # Mount UFD
@ -170,14 +207,25 @@ def build_ufd():
message='Removing Linux...', message='Removing Linux...',
function=remove_arch, function=remove_arch,
) )
# Copy boot files
ui.print_standard(' ')
ui.print_info('Boot Files')
for s_section, s_items in ITEMS_FROM_LIVE.items():
s_section = pathlib.Path(s_section)
try_print.run(
message=f'Copying {s_section}...',
function=copy_source,
source=s_section,
items=s_items,
from_live=True,
overwrite=True,
)
os.rename('/mnt/UFD/EFI/Boot/refind_x64.efi', '/mnt/UFD/EFI/Boot/bootx64.efi')
# Copy sources # Copy sources
ui.print_standard(' ') ui.print_standard(' ')
ui.print_info('Copy Sources') ui.print_info('Copy Sources')
try_print.run(
'Copying Memtest86...', io.recursive_copy,
'/usr/share/memtest86-efi/', '/mnt/UFD/EFI/Memtest86/', overwrite=True,
)
for s_label, s_path in sources.items(): for s_label, s_path in sources.items():
try_print.run( try_print.run(
message=f'Copying {s_label}...', message=f'Copying {s_label}...',
@ -252,7 +300,7 @@ def build_ufd():
ui.pause('Press Enter to exit...') ui.pause('Press Enter to exit...')
def confirm_selections(update=False): def confirm_selections(update=False) -> None:
"""Ask tech to confirm selections, twice if necessary.""" """Ask tech to confirm selections, twice if necessary."""
if not ui.ask('Is the above information correct?'): if not ui.ask('Is the above information correct?'):
ui.abort() ui.abort()
@ -273,9 +321,9 @@ def confirm_selections(update=False):
ui.print_standard(' ') ui.print_standard(' ')
def copy_source(source, items, overwrite=False): def copy_source(source, items, from_live=False, overwrite=False) -> None:
"""Copy source items to /mnt/UFD.""" """Copy source items to /mnt/UFD."""
is_image = source.is_file() is_image = not from_live and (source.is_file() or source.is_block_device())
items_not_found = False items_not_found = False
# Mount source if necessary # Mount source if necessary
@ -284,7 +332,14 @@ def copy_source(source, items, overwrite=False):
# Copy items # Copy items
for i_source, i_dest in items: for i_source, i_dest in items:
i_source = f'{"/mnt/Source" if is_image else source}{i_source}' if from_live:
# Don't prepend source
pass
elif is_image:
i_source = f'/mnt/Source{i_source}'
else:
# Prepend source
i_source = f'{source}{i_source}'
i_dest = f'/mnt/UFD{i_dest}' i_dest = f'/mnt/UFD{i_dest}'
try: try:
io.recursive_copy(i_source, i_dest, overwrite=overwrite) io.recursive_copy(i_source, i_dest, overwrite=overwrite)
@ -300,7 +355,7 @@ def copy_source(source, items, overwrite=False):
raise FileNotFoundError('One or more items not found') raise FileNotFoundError('One or more items not found')
def create_table(dev_path, use_mbr=False, images=None): def create_table(dev_path, use_mbr=False, images=None) -> None:
"""Create GPT or DOS partition table.""" """Create GPT or DOS partition table."""
cmd = [ cmd = [
'sudo', 'sudo',
@ -330,7 +385,7 @@ def create_table(dev_path, use_mbr=False, images=None):
for part, real in zip(part_sizes, images): for part, real in zip(part_sizes, images):
end = start + real end = start + real
cmd.append( cmd.append(
f'mkpart primary {"fat32" if start==MIB else "hfs+"} {start}B {end-1}B', f'mkpart primary "fat32" {start}B {end-1}B',
) )
start += part start += part
@ -338,7 +393,7 @@ def create_table(dev_path, use_mbr=False, images=None):
run_program(cmd) run_program(cmd)
def find_first_partition(dev_path): def find_first_partition(dev_path) -> str:
"""Find path to first partition of dev, returns str.""" """Find path to first partition of dev, returns str."""
cmd = [ cmd = [
'lsblk', 'lsblk',
@ -357,7 +412,7 @@ def find_first_partition(dev_path):
return part_path return part_path
def format_partition(dev_path, label): def format_partition(dev_path, label) -> None:
"""Format first partition on device FAT32.""" """Format first partition on device FAT32."""
cmd = [ cmd = [
'sudo', 'sudo',
@ -369,7 +424,7 @@ def format_partition(dev_path, label):
run_program(cmd) run_program(cmd)
def get_block_device_size(dev_path): def get_block_device_size(dev_path) -> int:
"""Get block device size via lsblk, returns int.""" """Get block device size via lsblk, returns int."""
cmd = [ cmd = [
'lsblk', 'lsblk',
@ -388,7 +443,7 @@ def get_block_device_size(dev_path):
return int(proc.stdout.strip()) return int(proc.stdout.strip())
def get_uuid(path): def get_uuid(path) -> str:
"""Get filesystem UUID via findmnt, returns str.""" """Get filesystem UUID via findmnt, returns str."""
cmd = [ cmd = [
'findmnt', 'findmnt',
@ -404,7 +459,7 @@ def get_uuid(path):
return proc.stdout.strip() return proc.stdout.strip()
def hide_items(ufd_dev_first_partition, items): def hide_items(ufd_dev_first_partition, items) -> None:
"""Set FAT32 hidden flag for items.""" """Set FAT32 hidden flag for items."""
with open('/root/.mtoolsrc', 'w', encoding='utf-8') as _f: with open('/root/.mtoolsrc', 'w', encoding='utf-8') as _f:
_f.write(f'drive U: file="{ufd_dev_first_partition}"\n') _f.write(f'drive U: file="{ufd_dev_first_partition}"\n')
@ -416,7 +471,18 @@ def hide_items(ufd_dev_first_partition, items):
run_program(cmd, shell=True, check=False) run_program(cmd, shell=True, check=False)
def install_syslinux_to_dev(ufd_dev, use_mbr): def hide_extra_partitions(dev_path, num_parts, use_mbr) -> None:
if use_mbr:
# Bail early
return
for part_id in range(num_parts):
part_id += 2 # Extra partitions start at 2
cmd = ['sfdisk', '--part-attrs', dev_path, str(part_id), 'RequiredPartition,62,63']
run_program(cmd, check=False)
def install_syslinux_to_dev(ufd_dev, use_mbr) -> None:
"""Install Syslinux to UFD (dev).""" """Install Syslinux to UFD (dev)."""
cmd = [ cmd = [
'sudo', 'sudo',
@ -429,7 +495,7 @@ def install_syslinux_to_dev(ufd_dev, use_mbr):
run_program(cmd) run_program(cmd)
def install_syslinux_to_partition(partition): def install_syslinux_to_partition(partition) -> None:
"""Install Syslinux to UFD (partition).""" """Install Syslinux to UFD (partition)."""
cmd = [ cmd = [
'sudo', 'sudo',
@ -442,7 +508,7 @@ def install_syslinux_to_partition(partition):
run_program(cmd) run_program(cmd)
def is_valid_path(path_obj, path_type): def is_valid_path(path_obj, path_type) -> bool:
"""Verify path_obj is valid by type, returns bool.""" """Verify path_obj is valid by type, returns bool."""
valid_path = False valid_path = False
if path_type == 'DIR': if path_type == 'DIR':
@ -453,13 +519,14 @@ def is_valid_path(path_obj, path_type):
valid_path = path_obj.is_file() and path_obj.suffix.lower() == '.img' valid_path = path_obj.is_file() and path_obj.suffix.lower() == '.img'
elif path_type == 'ISO': elif path_type == 'ISO':
valid_path = path_obj.is_file() and path_obj.suffix.lower() == '.iso' valid_path = path_obj.is_file() and path_obj.suffix.lower() == '.iso'
valid_path = valid_path or re.match(r'^/dev/sr\d+$', str(path_obj))
elif path_type == 'UFD': elif path_type == 'UFD':
valid_path = path_obj.is_block_device() valid_path = path_obj.is_block_device()
return valid_path return valid_path
def set_boot_flag(dev_path, use_mbr=False): def set_boot_flag(dev_path, use_mbr=False) -> None:
"""Set modern or legacy boot flag.""" """Set modern or legacy boot flag."""
cmd = [ cmd = [
'sudo', 'sudo',
@ -471,7 +538,7 @@ def set_boot_flag(dev_path, use_mbr=False):
run_program(cmd) run_program(cmd)
def remove_arch(): def remove_arch() -> None:
"""Remove arch dir from UFD. """Remove arch dir from UFD.
This ensures a clean installation to the UFD and resets the boot files This ensures a clean installation to the UFD and resets the boot files
@ -479,7 +546,7 @@ def remove_arch():
shutil.rmtree(io.case_insensitive_path('/mnt/UFD/arch')) shutil.rmtree(io.case_insensitive_path('/mnt/UFD/arch'))
def show_selections(args, sources, ufd_dev, ufd_sources, extra_images): def show_selections(args, sources, ufd_dev, ufd_sources, extra_images) -> None:
"""Show selections including non-specified options.""" """Show selections including non-specified options."""
# Sources # Sources
@ -526,7 +593,7 @@ def show_selections(args, sources, ufd_dev, ufd_sources, extra_images):
ui.print_standard(' ') ui.print_standard(' ')
def update_boot_entries(ufd_dev, images=None): def update_boot_entries(ufd_dev, images=None) -> None:
"""Update boot files for UFD usage""" """Update boot files for UFD usage"""
configs = [] configs = []
uuids = [get_uuid('/mnt/UFD')] uuids = [get_uuid('/mnt/UFD')]
@ -548,7 +615,7 @@ def update_boot_entries(ufd_dev, images=None):
'sed', 'sed',
'--in-place', '--in-place',
'--regexp-extended', '--regexp-extended',
f's#archisolabel={ISO_LABEL}#archisodevice=/dev/disk/by-uuid/{uuids[0]}#', f's/___+/{uuids[0]}/',
*configs, *configs,
] ]
run_program(cmd) run_program(cmd)
@ -613,9 +680,9 @@ def update_boot_entries(ufd_dev, images=None):
break break
def verify_sources(args, ufd_sources): def verify_sources(args, ufd_sources) -> dict[str, pathlib.Path]:
"""Check all sources and abort if necessary, returns dict.""" """Check all sources and abort if necessary, returns dict."""
sources = OrderedDict() sources = {}
for label, data in ufd_sources.items(): for label, data in ufd_sources.items():
s_path = args[data['Arg']] s_path = args[data['Arg']]
@ -625,15 +692,16 @@ def verify_sources(args, ufd_sources):
except FileNotFoundError: except FileNotFoundError:
ui.print_error(f'ERROR: {label} not found: {s_path}') ui.print_error(f'ERROR: {label} not found: {s_path}')
ui.abort() ui.abort()
if not is_valid_path(s_path_obj, data['Type']): else:
ui.print_error(f'ERROR: Invalid {label} source: {s_path}') if not is_valid_path(s_path_obj, data['Type']):
ui.abort() ui.print_error(f'ERROR: Invalid {label} source: {s_path}')
sources[label] = s_path_obj ui.abort()
sources[label] = s_path_obj
return sources return sources
def verify_ufd(dev_path): def verify_ufd(dev_path) -> pathlib.Path:
"""Check that dev_path is a valid UFD, returns pathlib.Path obj.""" """Check that dev_path is a valid UFD, returns pathlib.Path obj."""
ufd_dev = None ufd_dev = None
@ -647,16 +715,16 @@ def verify_ufd(dev_path):
ui.print_error(f'ERROR: Invalid UFD device: {ufd_dev}') ui.print_error(f'ERROR: Invalid UFD device: {ufd_dev}')
ui.abort() ui.abort()
return ufd_dev return ufd_dev # type: ignore[reportGeneralTypeIssues]
def zero_device(dev_path): def zero_device(dev_path) -> None:
"""Zero-out first 64MB of device.""" """Zero-out first 1MB of device."""
cmd = [ cmd = [
'sudo', 'sudo',
'dd', 'dd',
'bs=4M', 'bs=1M',
'count=16', 'count=1',
'if=/dev/zero', 'if=/dev/zero',
f'of={dev_path}', f'of={dev_path}',
] ]

View file

@ -26,7 +26,7 @@ DEFAULT_LOG_NAME = cfg.main.KIT_NAME_FULL
# Functions # Functions
def enable_debug_mode(): def enable_debug_mode() -> None:
"""Configures logging for better debugging.""" """Configures logging for better debugging."""
root_logger = logging.getLogger() root_logger = logging.getLogger()
for handler in root_logger.handlers: for handler in root_logger.handlers:
@ -39,13 +39,21 @@ def enable_debug_mode():
def format_log_path( def format_log_path(
log_dir=None, log_name=None, timestamp=False, log_dir: pathlib.Path | str | None = None,
kit=False, tool=False, append=False): log_name: str | None = None,
append: bool = False,
kit: bool = False,
sub_dir: str | None = None,
timestamp: bool = False,
tool: bool = False,
) -> pathlib.Path:
"""Format path based on args passed, returns pathlib.Path obj.""" """Format path based on args passed, returns pathlib.Path obj."""
log_path = pathlib.Path( log_path = pathlib.Path(
f'{log_dir if log_dir else DEFAULT_LOG_DIR}/' f'{log_dir if log_dir else DEFAULT_LOG_DIR}/'
f'{cfg.main.KIT_NAME_FULL+"/" if kit else ""}' f'{cfg.main.KIT_NAME_FULL+"/" if kit else ""}'
f'{"Tools/" if tool else ""}' f'{"Tools/" if tool else ""}'
f'{sub_dir+"_" if sub_dir else ""}'
f'{time.strftime("%Y-%m-%d_%H%M%S%z") if sub_dir else ""}/'
f'{log_name if log_name else DEFAULT_LOG_NAME}' f'{log_name if log_name else DEFAULT_LOG_NAME}'
f'{"_" if timestamp else ""}' f'{"_" if timestamp else ""}'
f'{time.strftime("%Y-%m-%d_%H%M%S%z") if timestamp else ""}' f'{time.strftime("%Y-%m-%d_%H%M%S%z") if timestamp else ""}'
@ -61,40 +69,24 @@ def format_log_path(
return log_path return log_path
def get_log_filepath(): def get_root_logger_path() -> pathlib.Path:
"""Get the log filepath from the root logger, returns pathlib.Path obj. """Get the log filepath from the root logger, returns pathlib.Path obj.
NOTE: This will use the first handler baseFilename it finds (if any). NOTE: This will use the first handler baseFilename it finds (if any).
""" """
log_filepath = None
root_logger = logging.getLogger() root_logger = logging.getLogger()
# Check handlers # Check handlers
for handler in root_logger.handlers: for handler in root_logger.handlers:
if hasattr(handler, 'baseFilename'): if hasattr(handler, 'baseFilename'):
log_filepath = pathlib.Path(handler.baseFilename).resolve() log_file = handler.baseFilename # type: ignore[reportGeneralTypeIssues]
break return pathlib.Path(log_file).resolve()
# Done # No log file found
return log_filepath raise RuntimeError('Log path not found.')
def get_root_logger_path(): def remove_empty_log(log_path: None | pathlib.Path = None) -> None:
"""Get path to log file from root logger, returns pathlib.Path obj."""
log_path = None
root_logger = logging.getLogger()
# Check all handlers and use the first fileHandler found
for handler in root_logger.handlers:
if isinstance(handler, logging.FileHandler):
log_path = pathlib.Path(handler.baseFilename).resolve()
break
# Done
return log_path
def remove_empty_log(log_path=None):
"""Remove log if empty. """Remove log if empty.
NOTE: Under Windows an empty log is 2 bytes long. NOTE: Under Windows an empty log is 2 bytes long.
@ -117,7 +109,7 @@ def remove_empty_log(log_path=None):
log_path.unlink() log_path.unlink()
def start(config=None): def start(config: dict[str, str] | None = None) -> None:
"""Configure and start logging using safe defaults.""" """Configure and start logging using safe defaults."""
log_path = format_log_path(timestamp=os.name != 'nt') log_path = format_log_path(timestamp=os.name != 'nt')
root_logger = logging.getLogger() root_logger = logging.getLogger()
@ -140,7 +132,12 @@ def start(config=None):
def update_log_path( def update_log_path(
dest_dir=None, dest_name=None, keep_history=True, timestamp=True, append=False): dest_dir: None | pathlib.Path | str = None,
dest_name: None | str = None,
append: bool = False,
keep_history: bool = True,
timestamp: bool = True,
) -> None:
"""Moves current log file to new path and updates the root logger.""" """Moves current log file to new path and updates the root logger."""
root_logger = logging.getLogger() root_logger = logging.getLogger()
new_path = format_log_path(dest_dir, dest_name, timestamp=timestamp, append=append) new_path = format_log_path(dest_dir, dest_name, timestamp=timestamp, append=append)

View file

@ -5,6 +5,9 @@ import os
import pathlib import pathlib
import re import re
from subprocess import CompletedProcess
from typing import Any
import psutil import psutil
from wk.exe import get_json_from_command, run_program from wk.exe import get_json_from_command, run_program
@ -23,7 +26,7 @@ REGEX_VALID_IP = re.compile(
# Functions # Functions
def connected_to_private_network(raise_on_error=False): def connected_to_private_network(raise_on_error: bool = False) -> bool:
"""Check if connected to a private network, returns bool. """Check if connected to a private network, returns bool.
This checks for a valid private IP assigned to this system. This checks for a valid private IP assigned to this system.
@ -49,12 +52,10 @@ def connected_to_private_network(raise_on_error=False):
raise GenericError('Not connected to a network') raise GenericError('Not connected to a network')
# Done # Done
if raise_on_error:
connected = None
return connected return connected
def mount_backup_shares(read_write=False): def mount_backup_shares(read_write: bool = False) -> list[str]:
"""Mount backup shares using OS specific methods.""" """Mount backup shares using OS specific methods."""
report = [] report = []
for name, details in BACKUP_SERVERS.items(): for name, details in BACKUP_SERVERS.items():
@ -97,7 +98,10 @@ def mount_backup_shares(read_write=False):
return report return report
def mount_network_share(details, mount_point=None, read_write=False): def mount_network_share(
details: dict[str, Any],
mount_point: None | pathlib.Path | str = None,
read_write: bool = False) -> CompletedProcess:
"""Mount network share using OS specific methods.""" """Mount network share using OS specific methods."""
cmd = None cmd = None
address = details['Address'] address = details['Address']
@ -148,7 +152,7 @@ def mount_network_share(details, mount_point=None, read_write=False):
return run_program(cmd, check=False) return run_program(cmd, check=False)
def ping(addr='google.com'): def ping(addr: str = 'google.com') -> None:
"""Attempt to ping addr.""" """Attempt to ping addr."""
cmd = ( cmd = (
'ping', 'ping',
@ -159,7 +163,7 @@ def ping(addr='google.com'):
run_program(cmd) run_program(cmd)
def share_is_mounted(details): def share_is_mounted(details: dict[str, Any]) -> bool:
"""Check if dev/share/etc is mounted, returns bool.""" """Check if dev/share/etc is mounted, returns bool."""
mounted = False mounted = False
@ -193,8 +197,9 @@ def share_is_mounted(details):
return mounted return mounted
def show_valid_addresses(): def show_valid_addresses() -> None:
"""Show all valid private IP addresses assigned to the system.""" """Show all valid private IP addresses assigned to the system."""
# TODO: Refactor to remove ui dependancy
devs = psutil.net_if_addrs() devs = psutil.net_if_addrs()
for dev, families in sorted(devs.items()): for dev, families in sorted(devs.items()):
for family in families: for family in families:
@ -203,8 +208,9 @@ def show_valid_addresses():
ui.show_data(message=dev, data=family.address) ui.show_data(message=dev, data=family.address)
def speedtest(): def speedtest() -> list[str]:
"""Run a network speedtest using speedtest-cli.""" """Run a network speedtest using speedtest-cli."""
# TODO: Refactor to use speedtest-cli's JSON output
cmd = ['speedtest-cli', '--simple'] cmd = ['speedtest-cli', '--simple']
proc = run_program(cmd, check=False) proc = run_program(cmd, check=False)
output = [line.strip() for line in proc.stdout.splitlines() if line.strip()] output = [line.strip() for line in proc.stdout.splitlines() if line.strip()]
@ -213,7 +219,7 @@ def speedtest():
return [f'{a:<10}{b:6.2f} {c}' for a, b, c in output] return [f'{a:<10}{b:6.2f} {c}' for a, b, c in output]
def unmount_backup_shares(): def unmount_backup_shares() -> list[str]:
"""Unmount backup shares.""" """Unmount backup shares."""
report = [] report = []
for name, details in BACKUP_SERVERS.items(): for name, details in BACKUP_SERVERS.items():
@ -242,7 +248,10 @@ def unmount_backup_shares():
return report return report
def unmount_network_share(details=None, mount_point=None): def unmount_network_share(
details: dict[str, Any] | None = None,
mount_point: None | pathlib.Path | str = None,
) -> CompletedProcess:
"""Unmount network share""" """Unmount network share"""
cmd = [] cmd = []

View file

@ -20,12 +20,12 @@ UUID_CORESTORAGE = '53746f72-6167-11aa-aa11-00306543ecac'
# Functions # Functions
def build_volume_report(device_path=None) -> list: def build_volume_report(device_path=None) -> list[str]:
"""Build volume report using lsblk, returns list. """Build volume report using lsblk, returns list.
If device_path is provided the report is limited to that device. If device_path is provided the report is limited to that device.
""" """
def _get_volumes(dev, indent=0) -> list: def _get_volumes(dev, indent=0) -> list[dict]:
"""Convert lsblk JSON tree to a flat list of items, returns list.""" """Convert lsblk JSON tree to a flat list of items, returns list."""
dev['name'] = f'{" "*indent}{dev["name"]}' dev['name'] = f'{" "*indent}{dev["name"]}'
volumes = [dev] volumes = [dev]
@ -108,7 +108,7 @@ def build_volume_report(device_path=None) -> list:
return report return report
def get_user_home(user): def get_user_home(user) -> pathlib.Path:
"""Get path to user's home dir, returns pathlib.Path obj.""" """Get path to user's home dir, returns pathlib.Path obj."""
home = None home = None
@ -129,7 +129,7 @@ def get_user_home(user):
return pathlib.Path(home) return pathlib.Path(home)
def get_user_name(): def get_user_name() -> str:
"""Get real user name, returns str.""" """Get real user name, returns str."""
user = None user = None
@ -146,7 +146,7 @@ def get_user_name():
return user return user
def make_temp_file(suffix=None): def make_temp_file(suffix=None) -> pathlib.Path:
"""Make temporary file, returns pathlib.Path() obj.""" """Make temporary file, returns pathlib.Path() obj."""
cmd = ['mktemp'] cmd = ['mktemp']
if suffix: if suffix:
@ -155,7 +155,7 @@ def make_temp_file(suffix=None):
return pathlib.Path(proc.stdout.strip()) return pathlib.Path(proc.stdout.strip())
def mount(source, mount_point=None, read_write=False): def mount(source, mount_point=None, read_write=False) -> None:
"""Mount source (on mount_point if provided). """Mount source (on mount_point if provided).
NOTE: If not running_as_root() then udevil will be used. NOTE: If not running_as_root() then udevil will be used.
@ -178,13 +178,13 @@ def mount(source, mount_point=None, read_write=False):
raise RuntimeError(f'Failed to mount: {source} on {mount_point}') raise RuntimeError(f'Failed to mount: {source} on {mount_point}')
def mount_volumes(device_path=None, read_write=False, scan_corestorage=False): def mount_volumes(device_path=None, read_write=False, scan_corestorage=False) -> None:
"""Mount all detected volumes. """Mount all detected volumes.
NOTE: If device_path is specified then only volumes NOTE: If device_path is specified then only volumes
under that path will be mounted. under that path will be mounted.
""" """
def _get_volumes(dev) -> list: def _get_volumes(dev) -> list[dict]:
"""Convert lsblk JSON tree to a flat list of items, returns list.""" """Convert lsblk JSON tree to a flat list of items, returns list."""
volumes = [dev] volumes = [dev]
for child in dev.get('children', []): for child in dev.get('children', []):
@ -233,12 +233,12 @@ def mount_volumes(device_path=None, read_write=False, scan_corestorage=False):
pass pass
def running_as_root(): def running_as_root() -> bool:
"""Check if running with effective UID of 0, returns bool.""" """Check if running with effective UID of 0, returns bool."""
return os.geteuid() == 0 return os.geteuid() == 0
def scan_corestorage_container(container, timeout=300): def scan_corestorage_container(container, timeout=300) -> list[dict]:
"""Scan CoreStorage container for inner volumes, returns list.""" """Scan CoreStorage container for inner volumes, returns list."""
container_path = pathlib.Path(container) container_path = pathlib.Path(container)
detected_volumes = {} detected_volumes = {}
@ -285,7 +285,7 @@ def scan_corestorage_container(container, timeout=300):
return inner_volumes return inner_volumes
def unmount(source_or_mountpoint): def unmount(source_or_mountpoint) -> None:
"""Unmount source_or_mountpoint. """Unmount source_or_mountpoint.
NOTE: If not running_as_root() then udevil will be used. NOTE: If not running_as_root() then udevil will be used.

View file

@ -13,7 +13,7 @@ REGEX_FANS = re.compile(r'^.*\(bytes (?P<bytes>.*)\)$')
# Functions # Functions
def decode_smc_bytes(text): def decode_smc_bytes(text) -> int:
"""Decode SMC bytes, returns int.""" """Decode SMC bytes, returns int."""
result = None result = None
@ -32,7 +32,7 @@ def decode_smc_bytes(text):
return result return result
def set_fans(mode): def set_fans(mode) -> None:
"""Set fans to auto or max.""" """Set fans to auto or max."""
if mode == 'auto': if mode == 'auto':
set_fans_auto() set_fans_auto()
@ -42,14 +42,14 @@ def set_fans(mode):
raise RuntimeError(f'Invalid fan mode: {mode}') raise RuntimeError(f'Invalid fan mode: {mode}')
def set_fans_auto(): def set_fans_auto() -> None:
"""Set fans to auto.""" """Set fans to auto."""
LOG.info('Setting fans to auto') LOG.info('Setting fans to auto')
cmd = ['sudo', 'smc', '-k', 'FS! ', '-w', '0000'] cmd = ['sudo', 'smc', '-k', 'FS! ', '-w', '0000']
run_program(cmd) run_program(cmd)
def set_fans_max(): def set_fans_max() -> None:
"""Set fans to their max speeds.""" """Set fans to their max speeds."""
LOG.info('Setting fans to max') LOG.info('Setting fans to max')
num_fans = 0 num_fans = 0

View file

@ -6,9 +6,10 @@ import logging
import os import os
import pathlib import pathlib
import platform import platform
import re
from contextlib import suppress from contextlib import suppress
from typing import Any
import psutil import psutil
try: try:
@ -24,7 +25,7 @@ from wk.cfg.windows_builds import (
OUTDATED_BUILD_NUMBERS, OUTDATED_BUILD_NUMBERS,
WINDOWS_BUILDS, WINDOWS_BUILDS,
) )
from wk.exe import get_json_from_command, run_program from wk.exe import get_json_from_command, run_program, wait_for_procs
from wk.kit.tools import find_kit_dir from wk.kit.tools import find_kit_dir
from wk.std import ( from wk.std import (
GenericError, GenericError,
@ -72,9 +73,6 @@ KNOWN_HIVE_NAMES = {
RAM_OK = 5.5 * 1024**3 # ~6 GiB assuming a bit of shared memory RAM_OK = 5.5 * 1024**3 # ~6 GiB assuming a bit of shared memory
RAM_WARNING = 3.5 * 1024**3 # ~4 GiB assuming a bit of shared memory RAM_WARNING = 3.5 * 1024**3 # ~4 GiB assuming a bit of shared memory
REG_MSISERVER = r'HKLM\SYSTEM\CurrentControlSet\Control\SafeBoot\Network\MSIServer' REG_MSISERVER = r'HKLM\SYSTEM\CurrentControlSet\Control\SafeBoot\Network\MSIServer'
REGEX_4K_ALIGNMENT = re.compile(
r'^(?P<description>.*?)\s+(?P<size>\d+)\s+(?P<offset>\d+)',
)
SLMGR = pathlib.Path(f'{os.environ.get("SYSTEMROOT")}/System32/slmgr.vbs') SLMGR = pathlib.Path(f'{os.environ.get("SYSTEMROOT")}/System32/slmgr.vbs')
SYSTEMDRIVE = os.environ.get('SYSTEMDRIVE') SYSTEMDRIVE = os.environ.get('SYSTEMDRIVE')
@ -92,7 +90,7 @@ else:
# Activation Functions # Activation Functions
def activate_with_bios(): def activate_with_bios() -> None:
"""Attempt to activate Windows with a key stored in the BIOS.""" """Attempt to activate Windows with a key stored in the BIOS."""
# Code borrowed from https://github.com/aeruder/get_win8key # Code borrowed from https://github.com/aeruder/get_win8key
##################################################### #####################################################
@ -132,7 +130,7 @@ def activate_with_bios():
raise GenericError('Activation Failed') raise GenericError('Activation Failed')
def get_activation_string(): def get_activation_string() -> str:
"""Get activation status, returns str.""" """Get activation status, returns str."""
cmd = ['cscript', '//nologo', SLMGR, '/xpr'] cmd = ['cscript', '//nologo', SLMGR, '/xpr']
proc = run_program(cmd, check=False) proc = run_program(cmd, check=False)
@ -142,7 +140,7 @@ def get_activation_string():
return act_str return act_str
def is_activated(): def is_activated() -> bool:
"""Check if Windows is activated via slmgr.vbs and return bool.""" """Check if Windows is activated via slmgr.vbs and return bool."""
act_str = get_activation_string() act_str = get_activation_string()
@ -151,45 +149,39 @@ def is_activated():
# Date / Time functions # Date / Time functions
def get_timezone(): def get_timezone() -> str:
"""Get current timezone using tzutil, returns str.""" """Get current timezone using tzutil, returns str."""
cmd = ['tzutil', '/g'] cmd = ['tzutil', '/g']
proc = run_program(cmd, check=False) proc = run_program(cmd, check=False)
return proc.stdout return proc.stdout
def set_timezone(zone): def set_timezone(zone) -> None:
"""Set current timezone using tzutil.""" """Set current timezone using tzutil."""
cmd = ['tzutil', '/s', zone] cmd = ['tzutil', '/s', zone]
run_program(cmd, check=False) run_program(cmd, check=False)
# Info Functions # Info Functions
def check_4k_alignment(show_alert=False): def check_4k_alignment(show_alert=False) -> list[str]:
"""Check if all partitions are 4K aligned, returns book.""" """Check if all partitions are 4K aligned, returns list."""
cmd = ['WMIC', 'partition', 'get', 'Caption,Size,StartingOffset'] script_path = find_kit_dir('Scripts').joinpath('check_partition_alignment.ps1')
cmd = ['PowerShell', '-ExecutionPolicy', 'Bypass', '-File', script_path]
json_data = get_json_from_command(cmd)
report = [] report = []
show_alert = False show_alert = False
# Check offsets # Check offsets
proc = run_program(cmd) for part in json_data:
for line in proc.stdout.splitlines(): if part['StartingOffset'] % 4096 != 0:
line = line.strip() report.append(
if not line or not line.startswith('Disk'): ansi.color_string(
continue f'{part["Name"]}'
match = REGEX_4K_ALIGNMENT.match(line) f' ({bytes_to_string(part["Size"], decimals=1)})'
if not match: ,
LOG.error('Failed to parse partition info for: %s', line) 'RED'
continue )
if int(match.group('offset')) % 4096 != 0: )
report.append(
ansi.color_string(
f'{match.group("description")}'
f' ({bytes_to_string(match.group("size"), decimals=1)})'
,
'RED'
)
)
# Show alert # Show alert
if show_alert: if show_alert:
@ -201,11 +193,12 @@ def check_4k_alignment(show_alert=False):
0, 0,
ansi.color_string('One or more partitions not 4K aligned', 'YELLOW'), ansi.color_string('One or more partitions not 4K aligned', 'YELLOW'),
) )
report.sort()
return report return report
def export_bitlocker_info(): def export_bitlocker_info() -> None:
"""Get Bitlocker info and save to the current directory.""" """Get Bitlocker info and save to the base directory of the kit."""
commands = [ commands = [
['manage-bde', '-status', SYSTEMDRIVE], ['manage-bde', '-status', SYSTEMDRIVE],
['manage-bde', '-protectors', '-get', SYSTEMDRIVE], ['manage-bde', '-protectors', '-get', SYSTEMDRIVE],
@ -222,49 +215,56 @@ def export_bitlocker_info():
_f.write(f'{proc.stdout}\n\n') _f.write(f'{proc.stdout}\n\n')
def get_installed_antivirus(): def get_installed_antivirus() -> dict[str, dict]:
"""Get list of installed antivirus programs, returns list.""" """Get installed antivirus products and their status, returns dict."""
cmd = [ script_path = find_kit_dir('Scripts').joinpath('check_av.ps1')
'WMIC', r'/namespace:\\root\SecurityCenter2', cmd = ['PowerShell', '-ExecutionPolicy', 'Bypass', '-File', script_path]
'path', 'AntivirusProduct', json_data = get_json_from_command(cmd)
'get', 'displayName', '/value', products = {}
]
products = []
report = []
# Get list of products # Check state and build dict
proc = run_program(cmd) for p in json_data:
for line in proc.stdout.splitlines(): name = p['displayName']
line = line.strip() state = p['productState']
if '=' in line: enabled = ((state>>8) & 0x11) in (0x10, 0x11) # middle two hex digits
products.append(line.split('=')[1]) outdated = (state & 0x11) != 0x00 # last two hex digits
products[name] = {
'Enabled': enabled,
'Outdated': outdated,
'State': state,
}
return products
def list_installed_antivirus() -> list[str]:
"""Get list of installed antivirus programs, returns list."""
products = get_installed_antivirus()
products_active = []
products_inactive = []
# Check product(s) status # Check product(s) status
for product in sorted(products): for name, details in products.items():
cmd = [ if details['Enabled']:
'WMIC', r'/namespace:\\root\SecurityCenter2', if details['Outdated']:
'path', 'AntivirusProduct', products_active.append(ansi.color_string(f'{name} [OUTDATED]', 'YELLOW'))
'where', f'displayName="{product}"', else:
'get', 'productState', '/value', products_active.append(name)
]
proc = run_program(cmd)
state = proc.stdout.split('=')[1]
state = hex(int(state))
if str(state)[3:5] not in ['10', '11']:
report.append(ansi.color_string(f'[Disabled] {product}', 'YELLOW'))
else: else:
report.append(product) # Disabled
products_inactive.append(ansi.color_string(f'[Disabled] {name}', 'YELLOW'))
# Final check # Final check
if not report: if not (products_active or products_inactive):
report.append(ansi.color_string('No products detected', 'RED')) products_inactive.append(ansi.color_string('No products detected', 'RED'))
# Done # Done
return report products_active.sort()
products_inactive.sort()
return products_active + products_inactive
def get_installed_ram(as_list=False, raise_exceptions=False): def get_installed_ram(as_list=False, raise_exceptions=False) -> list | str:
"""Get installed RAM.""" """Get installed RAM, returns list or str."""
mem = psutil.virtual_memory() mem = psutil.virtual_memory()
mem_str = bytes_to_string(mem.total, decimals=1) mem_str = bytes_to_string(mem.total, decimals=1)
@ -279,8 +279,8 @@ def get_installed_ram(as_list=False, raise_exceptions=False):
return [mem_str] if as_list else mem_str return [mem_str] if as_list else mem_str
def get_os_activation(as_list=False, check=True): def get_os_activation(as_list=False, check=True) -> list | str:
"""Get OS activation status, returns str. """Get OS activation status, returns list or str.
NOTE: If check=True then raise an exception if OS isn't activated. NOTE: If check=True then raise an exception if OS isn't activated.
""" """
@ -296,7 +296,7 @@ def get_os_activation(as_list=False, check=True):
return [act_str] if as_list else act_str return [act_str] if as_list else act_str
def get_os_name(as_list=False, check=True): def get_os_name(as_list=False, check=True) -> str:
"""Build OS display name, returns str. """Build OS display name, returns str.
NOTE: If check=True then an exception is raised if the OS version is NOTE: If check=True then an exception is raised if the OS version is
@ -322,7 +322,7 @@ def get_os_name(as_list=False, check=True):
return [display_name] if as_list else display_name return [display_name] if as_list else display_name
def get_raw_disks(): def get_raw_disks() -> list[str]:
"""Get all disks without a partiton table, returns list.""" """Get all disks without a partiton table, returns list."""
script_path = find_kit_dir('Scripts').joinpath('get_raw_disks.ps1') script_path = find_kit_dir('Scripts').joinpath('get_raw_disks.ps1')
cmd = ['PowerShell', '-ExecutionPolicy', 'Bypass', '-File', script_path] cmd = ['PowerShell', '-ExecutionPolicy', 'Bypass', '-File', script_path]
@ -347,7 +347,7 @@ def get_raw_disks():
return raw_disks return raw_disks
def get_volume_usage(use_colors=False): def get_volume_usage(use_colors=False) -> list[str]:
"""Get space usage info for all fixed volumes, returns list.""" """Get space usage info for all fixed volumes, returns list."""
report = [] report = []
for disk in psutil.disk_partitions(): for disk in psutil.disk_partitions():
@ -371,7 +371,7 @@ def get_volume_usage(use_colors=False):
return report return report
def show_alert_box(message, title=None): def show_alert_box(message, title=None) -> None:
"""Show Windows alert box with message.""" """Show Windows alert box with message."""
title = title if title else f'{KIT_NAME_FULL} Warning' title = title if title else f'{KIT_NAME_FULL} Warning'
message_box = ctypes.windll.user32.MessageBoxW message_box = ctypes.windll.user32.MessageBoxW
@ -379,7 +379,7 @@ def show_alert_box(message, title=None):
# Registry Functions # Registry Functions
def reg_delete_key(hive, key, recurse=False): def reg_delete_key(hive, key, recurse=False) -> None:
"""Delete a key from the registry. """Delete a key from the registry.
NOTE: If recurse is False then it will only work on empty keys. NOTE: If recurse is False then it will only work on empty keys.
@ -401,7 +401,7 @@ def reg_delete_key(hive, key, recurse=False):
except FileNotFoundError: except FileNotFoundError:
# Ignore # Ignore
pass pass
except PermissionError: except PermissionError as _e:
LOG.error(r'Failed to delete registry key: %s\%s', hive_name, key) LOG.error(r'Failed to delete registry key: %s\%s', hive_name, key)
if recurse: if recurse:
# Re-raise exception # Re-raise exception
@ -409,10 +409,10 @@ def reg_delete_key(hive, key, recurse=False):
# recurse is not True so assuming we tried to remove a non-empty key # recurse is not True so assuming we tried to remove a non-empty key
msg = fr'Refusing to remove non-empty key: {hive_name}\{key}' msg = fr'Refusing to remove non-empty key: {hive_name}\{key}'
raise FileExistsError(msg) raise FileExistsError(msg) from _e
def reg_delete_value(hive, key, value): def reg_delete_value(hive, key, value) -> None:
"""Delete a value from the registry.""" """Delete a value from the registry."""
access = winreg.KEY_ALL_ACCESS access = winreg.KEY_ALL_ACCESS
hive = reg_get_hive(hive) hive = reg_get_hive(hive)
@ -436,8 +436,9 @@ def reg_delete_value(hive, key, value):
raise raise
def reg_get_hive(hive): def reg_get_hive(hive) -> Any:
"""Get winreg HKEY constant from string, returns HKEY constant.""" """Get winreg HKEY constant from string, returns HKEY constant."""
# TODO: Fix type hint
if isinstance(hive, int): if isinstance(hive, int):
# Assuming we're already a winreg HKEY constant # Assuming we're already a winreg HKEY constant
pass pass
@ -448,8 +449,9 @@ def reg_get_hive(hive):
return hive return hive
def reg_get_data_type(data_type): def reg_get_data_type(data_type) -> Any:
"""Get registry data type from string, returns winreg constant.""" """Get registry data type from string, returns winreg constant."""
# TODO: Fix type hint
if isinstance(data_type, int): if isinstance(data_type, int):
# Assuming we're already a winreg value type constant # Assuming we're already a winreg value type constant
pass pass
@ -460,7 +462,7 @@ def reg_get_data_type(data_type):
return data_type return data_type
def reg_key_exists(hive, key): def reg_key_exists(hive, key) -> bool:
"""Test if the specified hive/key exists, returns bool.""" """Test if the specified hive/key exists, returns bool."""
exists = False exists = False
hive = reg_get_hive(hive) hive = reg_get_hive(hive)
@ -478,7 +480,7 @@ def reg_key_exists(hive, key):
return exists return exists
def reg_read_value(hive, key, value, force_32=False, force_64=False): def reg_read_value(hive, key, value, force_32=False, force_64=False) -> Any:
"""Query value from hive/hey, returns multiple types. """Query value from hive/hey, returns multiple types.
NOTE: Set value='' to read the default value. NOTE: Set value='' to read the default value.
@ -502,7 +504,7 @@ def reg_read_value(hive, key, value, force_32=False, force_64=False):
return data return data
def reg_write_settings(settings): def reg_write_settings(settings) -> None:
"""Set registry values in bulk from a custom data structure. """Set registry values in bulk from a custom data structure.
Data structure should be as follows: Data structure should be as follows:
@ -542,7 +544,7 @@ def reg_write_settings(settings):
reg_set_value(hive, key, *value) reg_set_value(hive, key, *value)
def reg_set_value(hive, key, name, data, data_type, option=None): def reg_set_value(hive, key, name, data, data_type, option=None) -> None:
"""Set value for hive/key.""" """Set value for hive/key."""
access = winreg.KEY_WRITE access = winreg.KEY_WRITE
data_type = reg_get_data_type(data_type) data_type = reg_get_data_type(data_type)
@ -574,25 +576,25 @@ def reg_set_value(hive, key, name, data, data_type, option=None):
# Safe Mode Functions # Safe Mode Functions
def disable_safemode(): def disable_safemode() -> None:
"""Edit BCD to remove safeboot value.""" """Edit BCD to remove safeboot value."""
cmd = ['bcdedit', '/deletevalue', '{default}', 'safeboot'] cmd = ['bcdedit', '/deletevalue', '{default}', 'safeboot']
run_program(cmd) run_program(cmd)
def disable_safemode_msi(): def disable_safemode_msi() -> None:
"""Disable MSI access under safemode.""" """Disable MSI access under safemode."""
cmd = ['reg', 'delete', REG_MSISERVER, '/f'] cmd = ['reg', 'delete', REG_MSISERVER, '/f']
run_program(cmd) run_program(cmd)
def enable_safemode(): def enable_safemode() -> None:
"""Edit BCD to set safeboot as default.""" """Edit BCD to set safeboot as default."""
cmd = ['bcdedit', '/set', '{default}', 'safeboot', 'network'] cmd = ['bcdedit', '/set', '{default}', 'safeboot', 'network']
run_program(cmd) run_program(cmd)
def enable_safemode_msi(): def enable_safemode_msi() -> None:
"""Enable MSI access under safemode.""" """Enable MSI access under safemode."""
cmd = ['reg', 'add', REG_MSISERVER, '/f'] cmd = ['reg', 'add', REG_MSISERVER, '/f']
run_program(cmd) run_program(cmd)
@ -605,7 +607,7 @@ def enable_safemode_msi():
# Secure Boot Functions # Secure Boot Functions
def is_booted_uefi(): def is_booted_uefi() -> bool:
"""Check if booted UEFI or legacy, returns bool.""" """Check if booted UEFI or legacy, returns bool."""
kernel = ctypes.windll.kernel32 kernel = ctypes.windll.kernel32
firmware_type = ctypes.c_uint() firmware_type = ctypes.c_uint()
@ -621,7 +623,7 @@ def is_booted_uefi():
return firmware_type.value == 2 return firmware_type.value == 2
def is_secure_boot_enabled(raise_exceptions=False, show_alert=False): def is_secure_boot_enabled(raise_exceptions=False, show_alert=False) -> bool:
"""Check if Secure Boot is enabled, returns bool. """Check if Secure Boot is enabled, returns bool.
If raise_exceptions is True then an exception is raised with details. If raise_exceptions is True then an exception is raised with details.
@ -671,7 +673,7 @@ def is_secure_boot_enabled(raise_exceptions=False, show_alert=False):
# Service Functions # Service Functions
def disable_service(service_name): def disable_service(service_name) -> None:
"""Set service startup to disabled.""" """Set service startup to disabled."""
cmd = ['sc', 'config', service_name, 'start=', 'disabled'] cmd = ['sc', 'config', service_name, 'start=', 'disabled']
run_program(cmd, check=False) run_program(cmd, check=False)
@ -681,7 +683,7 @@ def disable_service(service_name):
raise GenericError(f'Failed to disable service {service_name}') raise GenericError(f'Failed to disable service {service_name}')
def enable_service(service_name, start_type='auto'): def enable_service(service_name, start_type='auto') -> None:
"""Enable service by setting start type.""" """Enable service by setting start type."""
cmd = ['sc', 'config', service_name, 'start=', start_type] cmd = ['sc', 'config', service_name, 'start=', start_type]
psutil_type = 'automatic' psutil_type = 'automatic'
@ -696,7 +698,7 @@ def enable_service(service_name, start_type='auto'):
raise GenericError(f'Failed to enable service {service_name}') raise GenericError(f'Failed to enable service {service_name}')
def get_service_status(service_name): def get_service_status(service_name) -> str:
"""Get service status using psutil, returns str.""" """Get service status using psutil, returns str."""
status = 'unknown' status = 'unknown'
try: try:
@ -708,7 +710,7 @@ def get_service_status(service_name):
return status return status
def get_service_start_type(service_name): def get_service_start_type(service_name) -> str:
"""Get service startup type using psutil, returns str.""" """Get service startup type using psutil, returns str."""
start_type = 'unknown' start_type = 'unknown'
try: try:
@ -720,7 +722,7 @@ def get_service_start_type(service_name):
return start_type return start_type
def start_service(service_name): def start_service(service_name) -> None:
"""Stop service.""" """Stop service."""
cmd = ['net', 'start', service_name] cmd = ['net', 'start', service_name]
run_program(cmd, check=False) run_program(cmd, check=False)
@ -730,7 +732,7 @@ def start_service(service_name):
raise GenericError(f'Failed to start service {service_name}') raise GenericError(f'Failed to start service {service_name}')
def stop_service(service_name): def stop_service(service_name) -> None:
"""Stop service.""" """Stop service."""
cmd = ['net', 'stop', service_name] cmd = ['net', 'stop', service_name]
run_program(cmd, check=False) run_program(cmd, check=False)
@ -740,5 +742,62 @@ def stop_service(service_name):
raise GenericError(f'Failed to stop service {service_name}') raise GenericError(f'Failed to stop service {service_name}')
# Winget Functions
def winget_check(raise_exceptions: bool = False) -> None:
"""Check if winget is present, install if not."""
cmd = [
'powershell',
'-ExecutionPolicy', 'bypass',
'-File', find_kit_dir('Scripts').joinpath('install_winget.ps1'),
]
proc = run_program(cmd, check=False)
# Raise exception if requested
if raise_exceptions:
if proc.returncode == 1:
raise GenericWarning('Already installed')
if proc.returncode == 2:
raise GenericError('Failed to install')
def winget_import(group_name: str = 'default') -> None:
"""Use winget to import a set of applications.
group_name should be the name of a JSON file exported from winget.
NOTE: The path is relative to .bin/Scripts/wk/cfg/winget/
"""
cmd = [
'winget',
'import', '--import-file',
str(find_kit_dir('Scripts').joinpath(f'wk/cfg/winget/{group_name}.json')),
]
tmp_file = fr'{os.environ.get("TMP")}\run_winget.cmd'
if CONEMU:
with open(tmp_file, 'w', encoding='utf-8') as _f:
_f.write('@echo off\n')
_f.write(" ".join(cmd))
cmd = ('cmd', '/c', tmp_file, '-new_console:n', '-new_console:s33V')
run_program(cmd, check=False, pipe=False)
sleep(1)
wait_for_procs('winget.exe')
def winget_upgrade() -> None:
"""Upgrade all supported programs with winget, returns subprocess.Popen."""
cmd = ['winget', 'upgrade', '--all']
# Adjust if running inside ConEmu
tmp_file = fr'{os.environ.get("TMP")}\run_winget.cmd'
if CONEMU:
with open(tmp_file, 'w', encoding='utf-8') as _f:
_f.write('@echo off\n')
_f.write(" ".join(cmd))
cmd = ('cmd', '/c', tmp_file, '-new_console:n', '-new_console:s33V')
run_program(cmd, check=False, pipe=False)
sleep(1)
wait_for_procs('winget.exe')
if __name__ == '__main__': if __name__ == '__main__':
print("This file is not meant to be called directly.") print("This file is not meant to be called directly.")

View file

@ -4,11 +4,13 @@
import atexit import atexit
import logging import logging
import os import os
import pathlib
import re import re
import sys import sys
import time import time
from subprocess import CalledProcessError, DEVNULL from subprocess import CalledProcessError, DEVNULL
from typing import Any
from xml.dom.minidom import parse as xml_parse from xml.dom.minidom import parse as xml_parse
from wk.cfg.main import KIT_NAME_FULL, KIT_NAME_SHORT, WINDOWS_TIME_ZONE from wk.cfg.main import KIT_NAME_FULL, KIT_NAME_SHORT, WINDOWS_TIME_ZONE
@ -102,7 +104,7 @@ for error in ('CalledProcessError', 'FileNotFoundError'):
# Auto Repairs # Auto Repairs
def build_menus(base_menus, title, presets): def build_menus(base_menus, title, presets) -> dict[str, ui.Menu]:
"""Build menus, returns dict.""" """Build menus, returns dict."""
menus = {} menus = {}
menus['Main'] = ui.Menu(title=f'{title}\n{ansi.color_string("Main Menu", "GREEN")}') menus['Main'] = ui.Menu(title=f'{title}\n{ansi.color_string("Main Menu", "GREEN")}')
@ -169,7 +171,7 @@ def build_menus(base_menus, title, presets):
return menus return menus
def update_scheduled_task(): def update_scheduled_task() -> None:
"""Create (or update) scheduled task to start repairs.""" """Create (or update) scheduled task to start repairs."""
cmd = [ cmd = [
'schtasks', '/create', '/f', 'schtasks', '/create', '/f',
@ -183,7 +185,7 @@ def update_scheduled_task():
run_program(cmd) run_program(cmd)
def end_session(): def end_session() -> None:
"""End Auto Repairs session.""" """End Auto Repairs session."""
# Remove logon task # Remove logon task
cmd = [ cmd = [
@ -222,7 +224,7 @@ def end_session():
LOG.error('Failed to remove Auto Repairs session settings') LOG.error('Failed to remove Auto Repairs session settings')
def get_entry_settings(group, name): def get_entry_settings(group, name) -> dict[str, Any]:
"""Get menu entry settings from the registry, returns dict.""" """Get menu entry settings from the registry, returns dict."""
key_path = fr'{AUTO_REPAIR_KEY}\{group}\{name}' key_path = fr'{AUTO_REPAIR_KEY}\{group}\{name}'
settings = {} settings = {}
@ -241,7 +243,7 @@ def get_entry_settings(group, name):
return settings return settings
def init(menus, presets): def init(menus, presets) -> None:
"""Initialize Auto Repairs.""" """Initialize Auto Repairs."""
session_started = is_session_started() session_started = is_session_started()
@ -267,7 +269,7 @@ def init(menus, presets):
print('') print('')
def init_run(options): def init_run(options) -> None:
"""Initialize Auto Repairs Run.""" """Initialize Auto Repairs Run."""
update_scheduled_task() update_scheduled_task()
if options['Kill Explorer']['Selected']: if options['Kill Explorer']['Selected']:
@ -294,8 +296,9 @@ def init_run(options):
TRY_PRINT.run('Running RKill...', run_rkill, msg_good='DONE') TRY_PRINT.run('Running RKill...', run_rkill, msg_good='DONE')
def init_session(options): def init_session(options) -> None:
"""Initialize Auto Repairs session.""" """Initialize Auto Repairs session."""
_ = options # Suppress linting error and reserve for furture use
reg_set_value('HKCU', AUTO_REPAIR_KEY, 'SessionStarted', 1, 'DWORD') reg_set_value('HKCU', AUTO_REPAIR_KEY, 'SessionStarted', 1, 'DWORD')
reg_set_value('HKCU', AUTO_REPAIR_KEY, 'LogName', get_root_logger_path().stem, 'SZ') reg_set_value('HKCU', AUTO_REPAIR_KEY, 'LogName', get_root_logger_path().stem, 'SZ')
@ -309,17 +312,16 @@ def init_session(options):
set_timezone(WINDOWS_TIME_ZONE) set_timezone(WINDOWS_TIME_ZONE)
# One-time tasks # One-time tasks
TRY_PRINT.run( if options['Run AVRemover (once)']['Selected']:
'Run AVRemover...', run_tool, 'AVRemover', 'AVRemover', TRY_PRINT.run(
download=True, msg_good='DONE', 'Run AVRemover...', run_tool, 'AVRemover', 'AVRemover',
) download=True, msg_good='DONE',
if options['Run TDSSKiller (once)']['Selected']: )
TRY_PRINT.run('Running TDSSKiller...', run_tdsskiller, msg_good='DONE')
print('') print('')
reboot(30) reboot(30)
def is_autologon_enabled(): def is_autologon_enabled() -> bool:
"""Check if Autologon is enabled, returns bool.""" """Check if Autologon is enabled, returns bool."""
auto_admin_logon = False auto_admin_logon = False
try: try:
@ -337,7 +339,7 @@ def is_autologon_enabled():
return auto_admin_logon return auto_admin_logon
def is_session_started(): def is_session_started() -> bool:
"""Check if session was started, returns bool.""" """Check if session was started, returns bool."""
session_started = False session_started = False
try: try:
@ -349,7 +351,7 @@ def is_session_started():
return session_started return session_started
def load_preset(menus, presets, enable_menu_exit=True): def load_preset(menus, presets, enable_menu_exit=True) -> None:
"""Load menu settings from preset and ask selection question(s).""" """Load menu settings from preset and ask selection question(s)."""
if not enable_menu_exit: if not enable_menu_exit:
MENU_PRESETS.actions['Main Menu'].update({'Disabled':True, 'Hidden':True}) MENU_PRESETS.actions['Main Menu'].update({'Disabled':True, 'Hidden':True})
@ -375,7 +377,7 @@ def load_preset(menus, presets, enable_menu_exit=True):
MENU_PRESETS.actions['Main Menu'].update({'Disabled':False, 'Hidden':False}) MENU_PRESETS.actions['Main Menu'].update({'Disabled':False, 'Hidden':False})
def load_settings(menus): def load_settings(menus) -> None:
"""Load session settings from the registry.""" """Load session settings from the registry."""
for group, menu in menus.items(): for group, menu in menus.items():
if group == 'Main': if group == 'Main':
@ -384,7 +386,7 @@ def load_settings(menus):
menu.options[name].update(get_entry_settings(group, ansi.strip_colors(name))) menu.options[name].update(get_entry_settings(group, ansi.strip_colors(name)))
def run_auto_repairs(base_menus, presets): def run_auto_repairs(base_menus, presets) -> None:
"""Run Auto Repairs.""" """Run Auto Repairs."""
set_log_path() set_log_path()
title = f'{KIT_NAME_FULL}: Auto Repairs' title = f'{KIT_NAME_FULL}: Auto Repairs'
@ -443,7 +445,7 @@ def run_auto_repairs(base_menus, presets):
ui.pause('Press Enter to exit...') ui.pause('Press Enter to exit...')
def run_group(group, menu): def run_group(group, menu) -> None:
"""Run entries in group if appropriate.""" """Run entries in group if appropriate."""
ui.print_info(f' {group}') ui.print_info(f' {group}')
for name, details in menu.options.items(): for name, details in menu.options.items():
@ -487,7 +489,7 @@ def run_group(group, menu):
details['Function'](group, name) details['Function'](group, name)
def save_selection_settings(menus): def save_selection_settings(menus) -> None:
"""Save selections in the registry.""" """Save selections in the registry."""
for group, menu in menus.items(): for group, menu in menus.items():
if group == 'Main': if group == 'Main':
@ -500,7 +502,7 @@ def save_selection_settings(menus):
) )
def save_settings(group, name, result=None, **kwargs): def save_settings(group, name, result=None, **kwargs) -> None:
"""Save entry settings in the registry.""" """Save entry settings in the registry."""
key_path = fr'{AUTO_REPAIR_KEY}\{group}\{ansi.strip_colors(name)}' key_path = fr'{AUTO_REPAIR_KEY}\{group}\{ansi.strip_colors(name)}'
@ -528,7 +530,7 @@ def save_settings(group, name, result=None, **kwargs):
reg_set_value('HKCU', key_path, value_name, data, data_type) reg_set_value('HKCU', key_path, value_name, data, data_type)
def set_log_path(): def set_log_path() -> None:
"""Set log name using defaults or the saved registry value.""" """Set log name using defaults or the saved registry value."""
try: try:
log_path = reg_read_value('HKCU', AUTO_REPAIR_KEY, 'LogName') log_path = reg_read_value('HKCU', AUTO_REPAIR_KEY, 'LogName')
@ -544,7 +546,7 @@ def set_log_path():
) )
def show_main_menu(base_menus, menus, presets, title): def show_main_menu(base_menus, menus, presets, title) -> None:
"""Show main menu and handle actions.""" """Show main menu and handle actions."""
while True: while True:
update_main_menu(menus) update_main_menu(menus)
@ -559,7 +561,7 @@ def show_main_menu(base_menus, menus, presets, title):
raise SystemExit raise SystemExit
def show_sub_menu(menu): def show_sub_menu(menu) -> None:
"""Show sub-menu and handle sub-menu actions.""" """Show sub-menu and handle sub-menu actions."""
while True: while True:
selection = menu.advanced_select() selection = menu.advanced_select()
@ -590,7 +592,7 @@ def show_sub_menu(menu):
menu.options[name][key] = value menu.options[name][key] = value
def update_main_menu(menus): def update_main_menu(menus) -> None:
"""Update main menu based on current selections.""" """Update main menu based on current selections."""
index = 1 index = 1
skip = 'Reboot' skip = 'Reboot'
@ -609,7 +611,7 @@ def update_main_menu(menus):
# Auto Repairs: Wrapper Functions # Auto Repairs: Wrapper Functions
def auto_adwcleaner(group, name): def auto_adwcleaner(group, name) -> None:
"""Run AdwCleaner scan. """Run AdwCleaner scan.
save_settings() is called first since AdwCleaner may kill this script. save_settings() is called first since AdwCleaner may kill this script.
@ -621,25 +623,25 @@ def auto_adwcleaner(group, name):
save_settings(group, name, result=result) save_settings(group, name, result=result)
def auto_backup_browser_profiles(group, name): def auto_backup_browser_profiles(group, name) -> None:
"""Backup browser profiles.""" """Backup browser profiles."""
backup_all_browser_profiles(use_try_print=True) backup_all_browser_profiles(use_try_print=True)
save_settings(group, name, done=True, failed=False, message='DONE') save_settings(group, name, done=True, failed=False, message='DONE')
def auto_backup_power_plans(group, name): def auto_backup_power_plans(group, name) -> None:
"""Backup power plans.""" """Backup power plans."""
result = TRY_PRINT.run('Backup Power Plans...', export_power_plans) result = TRY_PRINT.run('Backup Power Plans...', export_power_plans)
save_settings(group, name, result=result) save_settings(group, name, result=result)
def auto_backup_registry(group, name): def auto_backup_registry(group, name) -> None:
"""Backup registry.""" """Backup registry."""
result = TRY_PRINT.run('Backup Registry...', backup_registry) result = TRY_PRINT.run('Backup Registry...', backup_registry)
save_settings(group, name, result=result) save_settings(group, name, result=result)
def auto_bleachbit(group, name): def auto_bleachbit(group, name) -> None:
"""Run BleachBit to clean files.""" """Run BleachBit to clean files."""
result = TRY_PRINT.run( result = TRY_PRINT.run(
'BleachBit...', run_bleachbit, BLEACH_BIT_CLEANERS, msg_good='DONE', 'BleachBit...', run_bleachbit, BLEACH_BIT_CLEANERS, msg_good='DONE',
@ -647,7 +649,15 @@ def auto_bleachbit(group, name):
save_settings(group, name, result=result) save_settings(group, name, result=result)
def auto_chkdsk(group, name): def auto_bcuninstaller(group, name) -> None:
"""Run BCUninstaller."""
result = TRY_PRINT.run(
'BCUninstaller...', run_bcuninstaller, msg_good='DONE',
)
save_settings(group, name, result=result)
def auto_chkdsk(group, name) -> None:
"""Run CHKDSK repairs.""" """Run CHKDSK repairs."""
needs_reboot = False needs_reboot = False
result = TRY_PRINT.run(f'CHKDSK ({SYSTEMDRIVE})...', run_chkdsk_online) result = TRY_PRINT.run(f'CHKDSK ({SYSTEMDRIVE})...', run_chkdsk_online)
@ -671,7 +681,7 @@ def auto_chkdsk(group, name):
reboot() reboot()
def auto_disable_pending_renames(group, name): def auto_disable_pending_renames(group, name) -> None:
"""Disable pending renames.""" """Disable pending renames."""
result = TRY_PRINT.run( result = TRY_PRINT.run(
'Disabling pending renames...', disable_pending_renames, 'Disabling pending renames...', disable_pending_renames,
@ -679,7 +689,7 @@ def auto_disable_pending_renames(group, name):
save_settings(group, name, result=result) save_settings(group, name, result=result)
def auto_dism(group, name): def auto_dism(group, name) -> None:
"""Run DISM repairs.""" """Run DISM repairs."""
needs_reboot = False needs_reboot = False
result = TRY_PRINT.run('DISM (RestoreHealth)...', run_dism) result = TRY_PRINT.run('DISM (RestoreHealth)...', run_dism)
@ -704,7 +714,7 @@ def auto_dism(group, name):
reboot() reboot()
def auto_enable_regback(group, name): def auto_enable_regback(group, name) -> None:
"""Enable RegBack.""" """Enable RegBack."""
result = TRY_PRINT.run( result = TRY_PRINT.run(
'Enable RegBack...', reg_set_value, 'HKLM', 'Enable RegBack...', reg_set_value, 'HKLM',
@ -714,19 +724,19 @@ def auto_enable_regback(group, name):
save_settings(group, name, result=result) save_settings(group, name, result=result)
def auto_hitmanpro(group, name): def auto_hitmanpro(group, name) -> None:
"""Run HitmanPro scan.""" """Run HitmanPro scan."""
result = TRY_PRINT.run('HitmanPro...', run_hitmanpro, msg_good='DONE') result = TRY_PRINT.run('HitmanPro...', run_hitmanpro, msg_good='DONE')
save_settings(group, name, result=result) save_settings(group, name, result=result)
def auto_kvrt(group, name): def auto_kvrt(group, name) -> None:
"""Run KVRT scan.""" """Run KVRT scan."""
result = TRY_PRINT.run('KVRT...', run_kvrt, msg_good='DONE') result = TRY_PRINT.run('KVRT...', run_kvrt, msg_good='DONE')
save_settings(group, name, result=result) save_settings(group, name, result=result)
def auto_microsoft_defender(group, name): def auto_microsoft_defender(group, name) -> None:
"""Run Microsoft Defender scan.""" """Run Microsoft Defender scan."""
result = TRY_PRINT.run( result = TRY_PRINT.run(
'Microsoft Defender...', run_microsoft_defender, msg_good='DONE', 'Microsoft Defender...', run_microsoft_defender, msg_good='DONE',
@ -734,14 +744,14 @@ def auto_microsoft_defender(group, name):
save_settings(group, name, result=result) save_settings(group, name, result=result)
def auto_reboot(group, name): def auto_reboot(group, name) -> None:
"""Reboot the system.""" """Reboot the system."""
save_settings(group, name, done=True, failed=False, message='DONE') save_settings(group, name, done=True, failed=False, message='DONE')
print('') print('')
reboot(30) reboot(30)
def auto_remove_power_plan(group, name): def auto_remove_power_plan(group, name) -> None:
"""Remove custom power plan and set to Balanced.""" """Remove custom power plan and set to Balanced."""
result = TRY_PRINT.run( result = TRY_PRINT.run(
'Remove Custom Power Plan...', remove_custom_power_plan, 'Remove Custom Power Plan...', remove_custom_power_plan,
@ -749,7 +759,7 @@ def auto_remove_power_plan(group, name):
save_settings(group, name, result=result) save_settings(group, name, result=result)
def auto_repair_registry(group, name): def auto_repair_registry(group, name) -> None:
"""Delete registry keys with embedded null characters.""" """Delete registry keys with embedded null characters."""
result = TRY_PRINT.run( result = TRY_PRINT.run(
'Running Registry repairs...', delete_registry_null_keys, 'Running Registry repairs...', delete_registry_null_keys,
@ -757,19 +767,19 @@ def auto_repair_registry(group, name):
save_settings(group, name, result=result) save_settings(group, name, result=result)
def auto_reset_power_plans(group, name): def auto_reset_power_plans(group, name) -> None:
"""Reset power plans.""" """Reset power plans."""
result = TRY_PRINT.run('Reset Power Plans...', reset_power_plans) result = TRY_PRINT.run('Reset Power Plans...', reset_power_plans)
save_settings(group, name, result=result) save_settings(group, name, result=result)
def auto_reset_proxy(group, name): def auto_reset_proxy(group, name) -> None:
"""Reset proxy settings.""" """Reset proxy settings."""
result = TRY_PRINT.run('Clearing proxy settings...', reset_proxy) result = TRY_PRINT.run('Clearing proxy settings...', reset_proxy)
save_settings(group, name, result=result) save_settings(group, name, result=result)
def auto_reset_windows_policies(group, name): def auto_reset_windows_policies(group, name) -> None:
"""Reset Windows policies to defaults.""" """Reset Windows policies to defaults."""
result = TRY_PRINT.run( result = TRY_PRINT.run(
'Resetting Windows policies...', reset_windows_policies, 'Resetting Windows policies...', reset_windows_policies,
@ -777,13 +787,13 @@ def auto_reset_windows_policies(group, name):
save_settings(group, name, result=result) save_settings(group, name, result=result)
def auto_restore_uac_defaults(group, name): def auto_restore_uac_defaults(group, name) -> None:
"""Restore UAC default settings.""" """Restore UAC default settings."""
result = TRY_PRINT.run('Restoring UAC defaults...', restore_uac_defaults) result = TRY_PRINT.run('Restoring UAC defaults...', restore_uac_defaults)
save_settings(group, name, result=result) save_settings(group, name, result=result)
def auto_set_custom_power_plan(group, name): def auto_set_custom_power_plan(group, name) -> None:
"""Set custom power plan.""" """Set custom power plan."""
result = TRY_PRINT.run( result = TRY_PRINT.run(
'Set Custom Power Plan...', create_custom_power_plan, 'Set Custom Power Plan...', create_custom_power_plan,
@ -793,13 +803,13 @@ def auto_set_custom_power_plan(group, name):
save_settings(group, name, result=result) save_settings(group, name, result=result)
def auto_sfc(group, name): def auto_sfc(group, name) -> None:
"""Run SFC repairs.""" """Run SFC repairs."""
result = TRY_PRINT.run('SFC Scan...', run_sfc_scan) result = TRY_PRINT.run('SFC Scan...', run_sfc_scan)
save_settings(group, name, result=result) save_settings(group, name, result=result)
def auto_system_restore_create(group, name): def auto_system_restore_create(group, name) -> None:
"""Create System Restore point.""" """Create System Restore point."""
result = TRY_PRINT.run( result = TRY_PRINT.run(
'Create System Restore...', create_system_restore_point, 'Create System Restore...', create_system_restore_point,
@ -807,7 +817,7 @@ def auto_system_restore_create(group, name):
save_settings(group, name, result=result) save_settings(group, name, result=result)
def auto_system_restore_enable(group, name): def auto_system_restore_enable(group, name) -> None:
"""Enable System Restore.""" """Enable System Restore."""
cmd = [ cmd = [
'powershell', '-Command', 'Enable-ComputerRestore', 'powershell', '-Command', 'Enable-ComputerRestore',
@ -817,21 +827,13 @@ def auto_system_restore_enable(group, name):
save_settings(group, name, result=result) save_settings(group, name, result=result)
def auto_system_restore_set_size(group, name): def auto_system_restore_set_size(group, name) -> None:
"""Set System Restore size.""" """Set System Restore size."""
result = TRY_PRINT.run('Set System Restore Size...', set_system_restore_size) result = TRY_PRINT.run('Set System Restore Size...', set_system_restore_size)
save_settings(group, name, result=result) save_settings(group, name, result=result)
def auto_uninstallview(group, name): def auto_windows_updates_disable(group, name) -> None:
"""Run UninstallView."""
result = TRY_PRINT.run(
'UninstallView...', run_uninstallview, msg_good='DONE',
)
save_settings(group, name, result=result)
def auto_windows_updates_disable(group, name):
"""Disable Windows Updates.""" """Disable Windows Updates."""
result = TRY_PRINT.run('Disable Windows Updates...', disable_windows_updates) result = TRY_PRINT.run('Disable Windows Updates...', disable_windows_updates)
if result['Failed']: if result['Failed']:
@ -840,13 +842,13 @@ def auto_windows_updates_disable(group, name):
save_settings(group, name, result=result) save_settings(group, name, result=result)
def auto_windows_updates_enable(group, name): def auto_windows_updates_enable(group, name) -> None:
"""Enable Windows Updates.""" """Enable Windows Updates."""
result = TRY_PRINT.run('Enable Windows Updates...', enable_windows_updates) result = TRY_PRINT.run('Enable Windows Updates...', enable_windows_updates)
save_settings(group, name, result=result) save_settings(group, name, result=result)
def auto_windows_updates_reset(group, name): def auto_windows_updates_reset(group, name) -> None:
"""Reset Windows Updates.""" """Reset Windows Updates."""
result = TRY_PRINT.run('Reset Windows Updates...', reset_windows_updates) result = TRY_PRINT.run('Reset Windows Updates...', reset_windows_updates)
if result['Failed']: if result['Failed']:
@ -856,12 +858,12 @@ def auto_windows_updates_reset(group, name):
# Misc Functions # Misc Functions
def set_backup_path(name, date=False): def set_backup_path(name, date=False) -> pathlib.Path:
"""Set backup path, returns pathlib.Path.""" """Set backup path, returns pathlib.Path."""
return set_local_storage_path('Backups', name, date) return set_local_storage_path('Backups', name, date)
def set_local_storage_path(folder, name, date=False): def set_local_storage_path(folder, name, date=False) -> pathlib.Path:
"""Get path for local storage, returns pathlib.Path.""" """Get path for local storage, returns pathlib.Path."""
local_path = get_path_obj(f'{SYSTEMDRIVE}/{KIT_NAME_SHORT}/{folder}/{name}') local_path = get_path_obj(f'{SYSTEMDRIVE}/{KIT_NAME_SHORT}/{folder}/{name}')
if date: if date:
@ -869,13 +871,13 @@ def set_local_storage_path(folder, name, date=False):
return local_path return local_path
def set_quarantine_path(name, date=False): def set_quarantine_path(name, date=False) -> pathlib.Path:
"""Set quarantine path, returns pathlib.Path.""" """Set quarantine path, returns pathlib.Path."""
return set_local_storage_path('Quarantine', name, date) return set_local_storage_path('Quarantine', name, date)
# Tool Functions # Tool Functions
def backup_all_browser_profiles(use_try_print=False): def backup_all_browser_profiles(use_try_print=False) -> None:
"""Backup browser profiles for all users.""" """Backup browser profiles for all users."""
users = get_path_obj(f'{SYSTEMDRIVE}/Users') users = get_path_obj(f'{SYSTEMDRIVE}/Users')
for userprofile in users.iterdir(): for userprofile in users.iterdir():
@ -884,7 +886,7 @@ def backup_all_browser_profiles(use_try_print=False):
backup_browser_profiles(userprofile, use_try_print) backup_browser_profiles(userprofile, use_try_print)
def backup_browser_chromium(backup_path, browser, search_path, use_try_print): def backup_browser_chromium(backup_path, browser, search_path, use_try_print) -> None:
"""Backup Chromium-based browser profile.""" """Backup Chromium-based browser profile."""
for item in search_path.iterdir(): for item in search_path.iterdir():
match = re.match(r'^(Default|Profile).*', item.name, re.IGNORECASE) match = re.match(r'^(Default|Profile).*', item.name, re.IGNORECASE)
@ -914,7 +916,7 @@ def backup_browser_chromium(backup_path, browser, search_path, use_try_print):
run_program(cmd, check=False) run_program(cmd, check=False)
def backup_browser_firefox(backup_path, search_path, use_try_print): def backup_browser_firefox(backup_path, search_path, use_try_print) -> None:
"""Backup Firefox browser profile.""" """Backup Firefox browser profile."""
output_path = backup_path.joinpath('Firefox.7z') output_path = backup_path.joinpath('Firefox.7z')
@ -939,7 +941,7 @@ def backup_browser_firefox(backup_path, search_path, use_try_print):
run_program(cmd, check=False) run_program(cmd, check=False)
def backup_browser_profiles(userprofile, use_try_print=False): def backup_browser_profiles(userprofile, use_try_print=False) -> None:
"""Backup browser profiles for userprofile.""" """Backup browser profiles for userprofile."""
backup_path = set_backup_path('Browsers', date=True) backup_path = set_backup_path('Browsers', date=True)
backup_path = backup_path.joinpath(userprofile.name) backup_path = backup_path.joinpath(userprofile.name)
@ -968,7 +970,7 @@ def backup_browser_profiles(userprofile, use_try_print=False):
pass pass
def backup_registry(): def backup_registry() -> None:
"""Backup Registry.""" """Backup Registry."""
backup_path = set_backup_path('Registry', date=True) backup_path = set_backup_path('Registry', date=True)
backup_path.parent.mkdir(parents=True, exist_ok=True) backup_path.parent.mkdir(parents=True, exist_ok=True)
@ -981,12 +983,12 @@ def backup_registry():
run_tool('ERUNT', 'ERUNT', backup_path, 'sysreg', 'curuser', 'otherusers') run_tool('ERUNT', 'ERUNT', backup_path, 'sysreg', 'curuser', 'otherusers')
def delete_registry_null_keys(): def delete_registry_null_keys() -> None:
"""Delete registry keys with embedded null characters.""" """Delete registry keys with embedded null characters."""
run_tool('RegDelNull', 'RegDelNull', '-s', '-y', download=True) run_tool('RegDelNull', 'RegDelNull', '-s', '-y', download=True)
def log_kvrt_results(log_path, report_path): def log_kvrt_results(log_path, report_path) -> None:
"""Parse KVRT report and log results in plain text.""" """Parse KVRT report and log results in plain text."""
log_text = '' log_text = ''
report_file = None report_file = None
@ -1027,7 +1029,7 @@ def log_kvrt_results(log_path, report_path):
log_path.write_text(log_text, encoding='utf-8') log_path.write_text(log_text, encoding='utf-8')
def run_adwcleaner(): def run_adwcleaner() -> None:
"""Run AdwCleaner.""" """Run AdwCleaner."""
settings_path = get_tool_path('AdwCleaner', 'AdwCleaner', check=False) settings_path = get_tool_path('AdwCleaner', 'AdwCleaner', check=False)
settings_path = settings_path.with_name('settings') settings_path = settings_path.with_name('settings')
@ -1037,7 +1039,7 @@ def run_adwcleaner():
run_tool('AdwCleaner', 'AdwCleaner', download=True) run_tool('AdwCleaner', 'AdwCleaner', download=True)
def run_bleachbit(cleaners, preview=True): def run_bleachbit(cleaners, preview=True) -> None:
"""Run BleachBit to either clean or preview files.""" """Run BleachBit to either clean or preview files."""
cmd_args = ( cmd_args = (
'--preview' if preview else '--clean', '--preview' if preview else '--clean',
@ -1048,11 +1050,20 @@ def run_bleachbit(cleaners, preview=True):
proc = run_tool('BleachBit', 'bleachbit_console', *cmd_args) proc = run_tool('BleachBit', 'bleachbit_console', *cmd_args)
# Save logs # Save logs
log_path.write_text(proc.stdout, encoding='utf-8') log_path.write_text(
log_path.with_suffix('.err').write_text(proc.stderr, encoding='utf-8') proc.stdout, encoding='utf-8', # type: ignore[reportGeneralTypeIssues]
)
log_path.with_suffix('.err').write_text(
proc.stderr, encoding='utf-8', # type: ignore[reportGeneralTypeIssues]
)
def run_hitmanpro(): def run_bcuninstaller() -> None:
"""Run BCUninstaller."""
run_tool('BCUninstaller', 'BCUninstaller')
def run_hitmanpro() -> None:
"""Run HitmanPro scan.""" """Run HitmanPro scan."""
log_path = format_log_path(log_name='HitmanPro', timestamp=True, tool=True) log_path = format_log_path(log_name='HitmanPro', timestamp=True, tool=True)
log_path = log_path.with_suffix('.xml') log_path = log_path.with_suffix('.xml')
@ -1061,7 +1072,7 @@ def run_hitmanpro():
run_tool('HitmanPro', 'HitmanPro', *cmd_args, download=True) run_tool('HitmanPro', 'HitmanPro', *cmd_args, download=True)
def run_kvrt(): def run_kvrt() -> None:
"""Run KVRT scan.""" """Run KVRT scan."""
log_path = format_log_path(log_name='KVRT', timestamp=True, tool=True) log_path = format_log_path(log_name='KVRT', timestamp=True, tool=True)
log_path.parent.mkdir(parents=True, exist_ok=True) log_path.parent.mkdir(parents=True, exist_ok=True)
@ -1102,11 +1113,11 @@ def run_kvrt():
log_kvrt_results(log_path, report_path) log_kvrt_results(log_path, report_path)
def run_microsoft_defender(full=True): def run_microsoft_defender(full=True) -> None:
"""Run Microsoft Defender scan.""" """Run Microsoft Defender scan."""
reg_key = r'Software\Microsoft\Windows Defender' reg_key = r'Software\Microsoft\Windows Defender'
def _get_defender_path(): def _get_defender_path() -> str:
install_path = reg_read_value('HKLM', reg_key, 'InstallLocation') install_path = reg_read_value('HKLM', reg_key, 'InstallLocation')
return fr'{install_path}\MpCmdRun.exe' return fr'{install_path}\MpCmdRun.exe'
@ -1147,7 +1158,7 @@ def run_microsoft_defender(full=True):
raise GenericError('Failed to run scan or clean items.') raise GenericError('Failed to run scan or clean items.')
def run_rkill(): def run_rkill() -> None:
"""Run RKill scan.""" """Run RKill scan."""
log_path = format_log_path(log_name='RKill', timestamp=True, tool=True) log_path = format_log_path(log_name='RKill', timestamp=True, tool=True)
log_path.parent.mkdir(parents=True, exist_ok=True) log_path.parent.mkdir(parents=True, exist_ok=True)
@ -1161,31 +1172,8 @@ def run_rkill():
run_tool('RKill', 'RKill', *cmd_args, download=True) run_tool('RKill', 'RKill', *cmd_args, download=True)
def run_tdsskiller():
"""Run TDSSKiller scan."""
log_path = format_log_path(log_name='TDSSKiller', timestamp=True, tool=True)
log_path.parent.mkdir(parents=True, exist_ok=True)
quarantine_path = set_quarantine_path('TDSSKiller')
quarantine_path.mkdir(parents=True, exist_ok=True)
cmd_args = (
'-accepteula',
'-accepteulaksn',
'-l', log_path,
'-qpath', quarantine_path,
'-qsus',
'-dcexact',
'-silent',
)
run_tool('TDSSKiller', 'TDSSKiller', *cmd_args, download=True)
def run_uninstallview():
"""Run UninstallView."""
run_tool('UninstallView', 'UninstallView')
# OS Built-in Functions # OS Built-in Functions
def create_custom_power_plan(enable_sleep=True, keep_display_on=False): def create_custom_power_plan(enable_sleep=True, keep_display_on=False) -> None:
"""Create new power plan and set as active.""" """Create new power plan and set as active."""
custom_guid = POWER_PLANS['Custom'] custom_guid = POWER_PLANS['Custom']
sleep_timeouts = POWER_PLAN_SLEEP_TIMEOUTS['High Performance'] sleep_timeouts = POWER_PLAN_SLEEP_TIMEOUTS['High Performance']
@ -1234,7 +1222,7 @@ def create_custom_power_plan(enable_sleep=True, keep_display_on=False):
run_program(cmd) run_program(cmd)
def create_system_restore_point(): def create_system_restore_point() -> None:
"""Create System Restore point.""" """Create System Restore point."""
cmd = [ cmd = [
'powershell', '-Command', 'Checkpoint-Computer', 'powershell', '-Command', 'Checkpoint-Computer',
@ -1249,7 +1237,7 @@ def create_system_restore_point():
raise GenericWarning('Skipped, a restore point was created too recently') raise GenericWarning('Skipped, a restore point was created too recently')
def disable_pending_renames(): def disable_pending_renames() -> None:
"""Disable pending renames.""" """Disable pending renames."""
reg_set_value( reg_set_value(
'HKLM', r'SYSTEM\CurrentControlSet\Control\Session Manager', 'HKLM', r'SYSTEM\CurrentControlSet\Control\Session Manager',
@ -1257,18 +1245,18 @@ def disable_pending_renames():
) )
def disable_windows_updates(): def disable_windows_updates() -> None:
"""Disable and stop Windows Updates.""" """Disable and stop Windows Updates."""
disable_service('wuauserv') disable_service('wuauserv')
stop_service('wuauserv') stop_service('wuauserv')
def enable_windows_updates(): def enable_windows_updates() -> None:
"""Enable Windows Updates.""" """Enable Windows Updates."""
enable_service('wuauserv', 'demand') enable_service('wuauserv', 'demand')
def export_power_plans(): def export_power_plans() -> None:
"""Export existing power plans.""" """Export existing power plans."""
backup_path = set_backup_path('Power Plans', date=True) backup_path = set_backup_path('Power Plans', date=True)
@ -1299,13 +1287,13 @@ def export_power_plans():
run_program(cmd) run_program(cmd)
def kill_explorer(): def kill_explorer() -> None:
"""Kill all Explorer processes.""" """Kill all Explorer processes."""
cmd = ['taskkill', '/im', 'explorer.exe', '/f'] cmd = ['taskkill', '/im', 'explorer.exe', '/f']
run_program(cmd, check=False) run_program(cmd, check=False)
def reboot(timeout=10): def reboot(timeout=10) -> None:
"""Reboot the system.""" """Reboot the system."""
atexit.unregister(start_explorer) atexit.unregister(start_explorer)
ui.print_warning(f'Rebooting the system in {timeout} seconds...') ui.print_warning(f'Rebooting the system in {timeout} seconds...')
@ -1315,7 +1303,7 @@ def reboot(timeout=10):
raise SystemExit raise SystemExit
def remove_custom_power_plan(high_performance=False): def remove_custom_power_plan(high_performance=False) -> None:
"""Remove custom power plan and set to a built-in plan. """Remove custom power plan and set to a built-in plan.
If high_performance is True then set to High Performance and set If high_performance is True then set to High Performance and set
@ -1342,13 +1330,13 @@ def remove_custom_power_plan(high_performance=False):
run_program(cmd) run_program(cmd)
def reset_power_plans(): def reset_power_plans() -> None:
"""Reset power plans to their default settings.""" """Reset power plans to their default settings."""
cmd = ['powercfg', '-RestoreDefaultSchemes'] cmd = ['powercfg', '-RestoreDefaultSchemes']
run_program(cmd) run_program(cmd)
def reset_proxy(): def reset_proxy() -> None:
"""Reset WinHTTP proxy settings.""" """Reset WinHTTP proxy settings."""
cmd = ['netsh', 'winhttp', 'reset', 'proxy'] cmd = ['netsh', 'winhttp', 'reset', 'proxy']
proc = run_program(cmd, check=False) proc = run_program(cmd, check=False)
@ -1358,7 +1346,7 @@ def reset_proxy():
raise GenericError('Failed to reset proxy settings.') raise GenericError('Failed to reset proxy settings.')
def reset_windows_policies(): def reset_windows_policies() -> None:
"""Reset Windows policies to defaults.""" """Reset Windows policies to defaults."""
cmd = ['gpupdate', '/force'] cmd = ['gpupdate', '/force']
proc = run_program(cmd, check=False) proc = run_program(cmd, check=False)
@ -1368,7 +1356,7 @@ def reset_windows_policies():
raise GenericError('Failed to reset one or more policies.') raise GenericError('Failed to reset one or more policies.')
def reset_windows_updates(): def reset_windows_updates() -> None:
"""Reset Windows Updates.""" """Reset Windows Updates."""
system_root = os.environ.get('SYSTEMROOT', 'C:/Windows') system_root = os.environ.get('SYSTEMROOT', 'C:/Windows')
src_path = f'{system_root}/SoftwareDistribution' src_path = f'{system_root}/SoftwareDistribution'
@ -1381,7 +1369,7 @@ def reset_windows_updates():
pass pass
def restore_uac_defaults(): def restore_uac_defaults() -> None:
"""Restore UAC default settings.""" """Restore UAC default settings."""
settings = REG_UAC_DEFAULTS_WIN10 settings = REG_UAC_DEFAULTS_WIN10
if OS_VERSION in (7, 8, 8.1): if OS_VERSION in (7, 8, 8.1):
@ -1390,7 +1378,7 @@ def restore_uac_defaults():
reg_write_settings(settings) reg_write_settings(settings)
def run_chkdsk_offline(): def run_chkdsk_offline() -> None:
"""Set filesystem 'dirty bit' to force a CHKDSK during startup.""" """Set filesystem 'dirty bit' to force a CHKDSK during startup."""
cmd = ['fsutil', 'dirty', 'set', SYSTEMDRIVE] cmd = ['fsutil', 'dirty', 'set', SYSTEMDRIVE]
proc = run_program(cmd, check=False) proc = run_program(cmd, check=False)
@ -1400,7 +1388,7 @@ def run_chkdsk_offline():
raise GenericError('Failed to set dirty bit.') raise GenericError('Failed to set dirty bit.')
def run_chkdsk_online(): def run_chkdsk_online() -> None:
"""Run CHKDSK. """Run CHKDSK.
NOTE: If run on Windows 8+ online repairs are attempted. NOTE: If run on Windows 8+ online repairs are attempted.
@ -1440,7 +1428,7 @@ def run_chkdsk_online():
raise GenericError('Issue(s) detected') raise GenericError('Issue(s) detected')
def run_dism(repair=True): def run_dism(repair=True) -> None:
"""Run DISM to either scan or repair component store health.""" """Run DISM to either scan or repair component store health."""
conemu_args = ['-new_console:nb', '-new_console:s33V'] if IN_CONEMU else [] conemu_args = ['-new_console:nb', '-new_console:s33V'] if IN_CONEMU else []
@ -1479,7 +1467,7 @@ def run_dism(repair=True):
raise GenericError('Issue(s) detected') raise GenericError('Issue(s) detected')
def run_sfc_scan(): def run_sfc_scan() -> None:
"""Run SFC and save results.""" """Run SFC and save results."""
cmd = ['sfc', '/scannow'] cmd = ['sfc', '/scannow']
log_path = format_log_path(log_name='SFC', timestamp=True, tool=True) log_path = format_log_path(log_name='SFC', timestamp=True, tool=True)
@ -1506,7 +1494,7 @@ def run_sfc_scan():
raise OSError raise OSError
def set_system_restore_size(size=8): def set_system_restore_size(size=8) -> None:
"""Set System Restore size.""" """Set System Restore size."""
cmd = [ cmd = [
'vssadmin', 'Resize', 'ShadowStorage', 'vssadmin', 'Resize', 'ShadowStorage',
@ -1515,7 +1503,7 @@ def set_system_restore_size(size=8):
run_program(cmd, pipe=False, stderr=DEVNULL, stdout=DEVNULL) run_program(cmd, pipe=False, stderr=DEVNULL, stdout=DEVNULL)
def start_explorer(): def start_explorer() -> None:
"""Start Explorer.""" """Start Explorer."""
popen_program(['explorer.exe']) popen_program(['explorer.exe'])

View file

@ -8,6 +8,8 @@ import os
import re import re
import sys import sys
from typing import Any
from wk.cfg.main import KIT_NAME_FULL from wk.cfg.main import KIT_NAME_FULL
from wk.cfg.setup import ( from wk.cfg.setup import (
BROWSER_PATHS, BROWSER_PATHS,
@ -17,13 +19,13 @@ from wk.cfg.setup import (
REG_WINDOWS_EXPLORER, REG_WINDOWS_EXPLORER,
REG_OPEN_SHELL_SETTINGS, REG_OPEN_SHELL_SETTINGS,
REG_OPEN_SHELL_LOW_POWER_IDLE, REG_OPEN_SHELL_LOW_POWER_IDLE,
REG_WINDOWS_BSOD_MINIDUMPS,
UBLOCK_ORIGIN_URLS, UBLOCK_ORIGIN_URLS,
) )
from wk.exe import kill_procs, run_program, popen_program from wk.exe import kill_procs, run_program, popen_program
from wk.io import case_insensitive_path, get_path_obj from wk.io import case_insensitive_path, get_path_obj
from wk.kit.tools import ( from wk.kit.tools import (
ARCH, ARCH,
download_tool,
extract_archive, extract_archive,
extract_tool, extract_tool,
find_kit_dir, find_kit_dir,
@ -35,16 +37,21 @@ from wk.os.win import (
OS_VERSION, OS_VERSION,
activate_with_bios, activate_with_bios,
check_4k_alignment, check_4k_alignment,
get_installed_antivirus,
get_installed_ram, get_installed_ram,
get_os_activation, get_os_activation,
get_os_name, get_os_name,
get_raw_disks, get_raw_disks,
get_service_status,
get_volume_usage, get_volume_usage,
is_activated, is_activated,
is_secure_boot_enabled, is_secure_boot_enabled,
list_installed_antivirus,
reg_set_value, reg_set_value,
reg_write_settings, reg_write_settings,
stop_service,
winget_check,
winget_import,
winget_upgrade,
) )
from wk.repairs.win import ( from wk.repairs.win import (
WIDTH, WIDTH,
@ -99,7 +106,7 @@ for error in ('CalledProcessError', 'FileNotFoundError'):
# Auto Setup # Auto Setup
def build_menus(base_menus, title, presets): def build_menus(base_menus, title, presets) -> dict[str, ui.Menu]:
"""Build menus, returns dict.""" """Build menus, returns dict."""
menus = {} menus = {}
menus['Main'] = ui.Menu(title=f'{title}\n{ansi.color_string("Main Menu", "GREEN")}') menus['Main'] = ui.Menu(title=f'{title}\n{ansi.color_string("Main Menu", "GREEN")}')
@ -154,7 +161,7 @@ def build_menus(base_menus, title, presets):
return menus return menus
def check_os_and_set_menu_title(title): def check_os_and_set_menu_title(title) -> str:
"""Check OS version and update title for menus, returns str.""" """Check OS version and update title for menus, returns str."""
color = None color = None
os_name = get_os_name(check=False) os_name = get_os_name(check=False)
@ -180,7 +187,7 @@ def check_os_and_set_menu_title(title):
return f'{title} ({ansi.color_string(os_name, color)})' return f'{title} ({ansi.color_string(os_name, color)})'
def load_preset(menus, presets, title, enable_menu_exit=True): def load_preset(menus, presets, title, enable_menu_exit=True) -> None:
"""Load menu settings from preset and ask selection question(s).""" """Load menu settings from preset and ask selection question(s)."""
if not enable_menu_exit: if not enable_menu_exit:
MENU_PRESETS.actions['Main Menu'].update({'Disabled':True, 'Hidden':True}) MENU_PRESETS.actions['Main Menu'].update({'Disabled':True, 'Hidden':True})
@ -219,7 +226,7 @@ def load_preset(menus, presets, title, enable_menu_exit=True):
menus[group_name].options[entry_name]['Selected'] = False menus[group_name].options[entry_name]['Selected'] = False
def run_auto_setup(base_menus, presets): def run_auto_setup(base_menus, presets) -> None:
"""Run Auto Setup.""" """Run Auto Setup."""
update_log_path(dest_name='Auto Setup', timestamp=True) update_log_path(dest_name='Auto Setup', timestamp=True)
title = f'{KIT_NAME_FULL}: Auto Setup' title = f'{KIT_NAME_FULL}: Auto Setup'
@ -261,7 +268,7 @@ def run_auto_setup(base_menus, presets):
ui.pause('Press Enter to exit...') ui.pause('Press Enter to exit...')
def run_group(group, menu): def run_group(group, menu) -> None:
"""Run entries in group if appropriate.""" """Run entries in group if appropriate."""
ui.print_info(f' {group}') ui.print_info(f' {group}')
for name, details in menu.options.items(): for name, details in menu.options.items():
@ -276,7 +283,7 @@ def run_group(group, menu):
details['Function']() details['Function']()
def show_main_menu(base_menus, menus, presets, title): def show_main_menu(base_menus, menus, presets, title) -> None:
"""Show main menu and handle actions.""" """Show main menu and handle actions."""
while True: while True:
update_main_menu(menus) update_main_menu(menus)
@ -291,7 +298,7 @@ def show_main_menu(base_menus, menus, presets, title):
raise SystemExit raise SystemExit
def show_sub_menu(menu): def show_sub_menu(menu) -> None:
"""Show sub-menu and handle sub-menu actions.""" """Show sub-menu and handle sub-menu actions."""
while True: while True:
selection = menu.advanced_select() selection = menu.advanced_select()
@ -307,7 +314,7 @@ def show_sub_menu(menu):
menu.options[name]['Selected'] = value menu.options[name]['Selected'] = value
def update_main_menu(menus): def update_main_menu(menus) -> None:
"""Update main menu based on current selections.""" """Update main menu based on current selections."""
index = 1 index = 1
skip = 'Reboot' skip = 'Reboot'
@ -326,37 +333,37 @@ def update_main_menu(menus):
# Auto Repairs: Wrapper Functions # Auto Repairs: Wrapper Functions
def auto_backup_registry(): def auto_backup_registry() -> None:
"""Backup registry.""" """Backup registry."""
TRY_PRINT.run('Backup Registry...', backup_registry) TRY_PRINT.run('Backup Registry...', backup_registry)
def auto_backup_browser_profiles(): def auto_backup_browser_profiles() -> None:
"""Backup browser profiles.""" """Backup browser profiles."""
backup_all_browser_profiles(use_try_print=True) backup_all_browser_profiles(use_try_print=True)
def auto_backup_power_plans(): def auto_backup_power_plans() -> None:
"""Backup power plans.""" """Backup power plans."""
TRY_PRINT.run('Backup Power Plans...', export_power_plans) TRY_PRINT.run('Backup Power Plans...', export_power_plans)
def auto_reset_power_plans(): def auto_reset_power_plans() -> None:
"""Reset power plans.""" """Reset power plans."""
TRY_PRINT.run('Reset Power Plans...', reset_power_plans) TRY_PRINT.run('Reset Power Plans...', reset_power_plans)
def auto_set_custom_power_plan(): def auto_set_custom_power_plan() -> None:
"""Set custom power plan.""" """Set custom power plan."""
TRY_PRINT.run('Set Custom Power Plan...', create_custom_power_plan) TRY_PRINT.run('Set Custom Power Plan...', create_custom_power_plan)
def auto_enable_bsod_minidumps(): def auto_enable_bsod_minidumps() -> None:
"""Enable saving minidumps during BSoDs.""" """Enable saving minidumps during BSoDs."""
TRY_PRINT.run('Enable BSoD mini dumps...', enable_bsod_minidumps) TRY_PRINT.run('Enable BSoD mini dumps...', enable_bsod_minidumps)
def auto_enable_regback(): def auto_enable_regback() -> None:
"""Enable RegBack.""" """Enable RegBack."""
TRY_PRINT.run( TRY_PRINT.run(
'Enable RegBack...', reg_set_value, 'HKLM', 'Enable RegBack...', reg_set_value, 'HKLM',
@ -365,7 +372,7 @@ def auto_enable_regback():
) )
def auto_system_restore_enable(): def auto_system_restore_enable() -> None:
"""Enable System Restore.""" """Enable System Restore."""
cmd = [ cmd = [
'powershell', '-Command', 'Enable-ComputerRestore', 'powershell', '-Command', 'Enable-ComputerRestore',
@ -374,28 +381,28 @@ def auto_system_restore_enable():
TRY_PRINT.run('Enable System Restore...', run_program, cmd=cmd) TRY_PRINT.run('Enable System Restore...', run_program, cmd=cmd)
def auto_system_restore_set_size(): def auto_system_restore_set_size() -> None:
"""Set System Restore size.""" """Set System Restore size."""
TRY_PRINT.run('Set System Restore Size...', set_system_restore_size) TRY_PRINT.run('Set System Restore Size...', set_system_restore_size)
def auto_system_restore_create(): def auto_system_restore_create() -> None:
"""Create System Restore point.""" """Create System Restore point."""
TRY_PRINT.run('Create System Restore...', create_system_restore_point) TRY_PRINT.run('Create System Restore...', create_system_restore_point)
def auto_windows_updates_enable(): def auto_windows_updates_enable() -> None:
"""Enable Windows Updates.""" """Enable Windows Updates."""
TRY_PRINT.run('Enable Windows Updates...', enable_windows_updates) TRY_PRINT.run('Enable Windows Updates...', enable_windows_updates)
# Auto Setup: Wrapper Functions # Auto Setup: Wrapper Functions
def auto_activate_windows(): def auto_activate_windows() -> None:
"""Attempt to activate Windows using BIOS key.""" """Attempt to activate Windows using BIOS key."""
TRY_PRINT.run('Windows Activation...', activate_with_bios) TRY_PRINT.run('Windows Activation...', activate_with_bios)
def auto_config_browsers(): def auto_config_browsers() -> None:
"""Configure Browsers.""" """Configure Browsers."""
prompt = ' Press Enter to continue...' prompt = ' Press Enter to continue...'
TRY_PRINT.run('Chrome Notifications...', disable_chrome_notifications) TRY_PRINT.run('Chrome Notifications...', disable_chrome_notifications)
@ -406,33 +413,38 @@ def auto_config_browsers():
'Set default browser...', set_default_browser, msg_good='STARTED', 'Set default browser...', set_default_browser, msg_good='STARTED',
) )
print(prompt, end='', flush=True) print(prompt, end='', flush=True)
ui.pause('') ui.pause(' ')
# Move cursor to beginning of the previous line and clear prompt # Move cursor to beginning of the previous line and clear prompt
print(f'\033[F\r{" "*len(prompt)}\r', end='', flush=True) print(f'\033[F\r{" "*len(prompt)}\r', end='', flush=True)
def auto_config_explorer(): def auto_config_explorer() -> None:
"""Configure Windows Explorer and restart the process.""" """Configure Windows Explorer and restart the process."""
TRY_PRINT.run('Windows Explorer...', config_explorer) TRY_PRINT.run('Windows Explorer...', config_explorer)
def auto_config_open_shell(): def auto_config_open_shell() -> None:
"""Configure Open Shell.""" """Configure Open Shell."""
TRY_PRINT.run('Open Shell...', config_open_shell) TRY_PRINT.run('Open Shell...', config_open_shell)
def auto_export_aida64_report(): def auto_disable_password_expiration() -> None:
"""Disable password expiration for all users."""
TRY_PRINT.run('Disable password expiration...', disable_password_expiration)
def auto_export_aida64_report() -> None:
"""Export AIDA64 reports.""" """Export AIDA64 reports."""
TRY_PRINT.run('AIDA64 Report...', export_aida64_report) TRY_PRINT.run('AIDA64 Report...', export_aida64_report)
def auto_install_firefox(): def auto_install_firefox() -> None:
"""Install Firefox.""" """Install Firefox."""
TRY_PRINT.run('Firefox...', install_firefox) TRY_PRINT.run('Firefox...', install_firefox)
def auto_install_libreoffice(): def auto_install_libreoffice() -> None:
"""Install LibreOffice. """Install LibreOffice.
NOTE: It is assumed that auto_install_vcredists() will be run NOTE: It is assumed that auto_install_vcredists() will be run
@ -441,105 +453,120 @@ def auto_install_libreoffice():
TRY_PRINT.run('LibreOffice...', install_libreoffice, vcredist=False) TRY_PRINT.run('LibreOffice...', install_libreoffice, vcredist=False)
def auto_install_open_shell(): def auto_install_open_shell() -> None:
"""Install Open Shell.""" """Install Open Shell."""
TRY_PRINT.run('Open Shell...', install_open_shell) TRY_PRINT.run('Open Shell...', install_open_shell)
def auto_install_software_bundle(): def auto_install_software_bundle() -> None:
"""Install standard software bundle.""" """Install standard software bundle."""
TRY_PRINT.run('Software Bundle...', install_software_bundle) TRY_PRINT.run('Software Bundle...', winget_import, group_name='default')
def auto_install_vcredists(): def auto_install_software_upgrades() -> None:
"""Upgrade all supported installed software."""
TRY_PRINT.run('Software Upgrades...', winget_upgrade)
def auto_install_vcredists() -> None:
"""Install latest supported Visual C++ runtimes.""" """Install latest supported Visual C++ runtimes."""
TRY_PRINT.run('Visual C++ Runtimes...', install_vcredists) TRY_PRINT.run('Visual C++ Runtimes...', winget_import, group_name='vcredists')
def auto_open_device_manager(): def auto_install_winget() -> None:
"""Install winget if needed."""
TRY_PRINT.run('Winget...', winget_check, raise_exceptions=True)
def auto_open_device_manager() -> None:
"""Open Device Manager.""" """Open Device Manager."""
TRY_PRINT.run('Device Manager...', open_device_manager) TRY_PRINT.run('Device Manager...', open_device_manager)
def auto_open_hwinfo_sensors(): def auto_open_hwinfo_sensors() -> None:
"""Open HWiNFO Sensors.""" """Open HWiNFO Sensors."""
TRY_PRINT.run('HWiNFO Sensors...', open_hwinfo_sensors) TRY_PRINT.run('HWiNFO Sensors...', open_hwinfo_sensors)
def auto_open_snappy_driver_installer_origin(): def auto_open_microsoft_store_updates() -> None:
"""Opem Microsoft Store Updates."""
TRY_PRINT.run('Microsoft Store Updates...', open_microsoft_store_updates)
def auto_open_snappy_driver_installer_origin() -> None:
"""Open Snappy Driver Installer Origin.""" """Open Snappy Driver Installer Origin."""
TRY_PRINT.run('Snappy Driver Installer...', open_snappy_driver_installer_origin) TRY_PRINT.run('Snappy Driver Installer...', open_snappy_driver_installer_origin)
def auto_open_windows_activation(): def auto_open_windows_activation() -> None:
"""Open Windows Activation.""" """Open Windows Activation."""
if not is_activated(): if not is_activated():
TRY_PRINT.run('Windows Activation...', open_windows_activation) TRY_PRINT.run('Windows Activation...', open_windows_activation)
def auto_open_windows_updates(): def auto_open_windows_updates() -> None:
"""Open Windows Updates.""" """Open Windows Updates."""
TRY_PRINT.run('Windows Updates...', open_windows_updates) TRY_PRINT.run('Windows Updates...', open_windows_updates)
def auto_open_xmplay(): def auto_open_xmplay() -> None:
"""Open XMPlay.""" """Open XMPlay."""
TRY_PRINT.run('XMPlay...', open_xmplay) TRY_PRINT.run('XMPlay...', open_xmplay)
def auto_show_4k_alignment_check(): def auto_show_4k_alignment_check() -> None:
"""Display 4K alignment check.""" """Display 4K alignment check."""
TRY_PRINT.run('4K alignment Check...', check_4k_alignment, show_alert=True) TRY_PRINT.run('4K alignment Check...', check_4k_alignment, show_alert=True)
def auto_show_installed_antivirus(): def auto_show_installed_antivirus() -> None:
"""Display installed antivirus.""" """Display installed antivirus."""
TRY_PRINT.run('Virus Protection...', get_installed_antivirus) TRY_PRINT.run('Virus Protection...', list_installed_antivirus)
def auto_show_installed_ram(): def auto_show_installed_ram() -> None:
"""Display installed RAM.""" """Display installed RAM."""
TRY_PRINT.run('Installed RAM...', get_installed_ram, TRY_PRINT.run('Installed RAM...', get_installed_ram,
as_list=True, raise_exceptions=True, as_list=True, raise_exceptions=True,
) )
def auto_show_os_activation(): def auto_show_os_activation() -> None:
"""Display OS activation status.""" """Display OS activation status."""
TRY_PRINT.run('Activation...', get_os_activation, as_list=True) TRY_PRINT.run('Activation...', get_os_activation, as_list=True)
def auto_show_os_name(): def auto_show_os_name() -> None:
"""Display OS Name.""" """Display OS Name."""
TRY_PRINT.run('Operating System...', get_os_name, as_list=True) TRY_PRINT.run('Operating System...', get_os_name, as_list=True)
def auto_show_secure_boot_status(): def auto_show_secure_boot_status() -> None:
"""Display Secure Boot status.""" """Display Secure Boot status."""
TRY_PRINT.run( TRY_PRINT.run(
'Secure Boot...', check_secure_boot_status, msg_good='Enabled', 'Secure Boot...', check_secure_boot_status, msg_good='Enabled',
) )
def auto_show_storage_status(): def auto_show_storage_status() -> None:
"""Display storage status.""" """Display storage status."""
TRY_PRINT.run('Storage Status...', get_storage_status) TRY_PRINT.run('Storage Status...', get_storage_status)
def auto_windows_temp_fix(): def auto_windows_temp_fix() -> None:
"""Restore default ACLs for Windows\\Temp.""" """Restore default ACLs for Windows\\Temp."""
TRY_PRINT.run(r'Windows\Temp fix...', fix_windows_temp) TRY_PRINT.run(r'Windows\Temp fix...', fix_windows_temp)
# Configure Functions # Configure Functions
def config_explorer(): def config_explorer() -> None:
"""Configure Windows Explorer and restart the process.""" """Configure Windows Explorer and restart the process."""
reg_write_settings(REG_WINDOWS_EXPLORER) reg_write_settings(REG_WINDOWS_EXPLORER)
kill_procs('explorer.exe', force=True) kill_procs('explorer.exe', force=True)
popen_program(['explorer.exe']) popen_program(['explorer.exe'])
def config_open_shell(): def config_open_shell() -> None:
"""Configure Open Shell.""" """Configure Open Shell."""
has_low_power_idle = False has_low_power_idle = False
@ -559,7 +586,7 @@ def config_open_shell():
reg_write_settings(REG_OPEN_SHELL_LOW_POWER_IDLE) reg_write_settings(REG_OPEN_SHELL_LOW_POWER_IDLE)
def disable_chrome_notifications(): def disable_chrome_notifications() -> None:
"""Disable notifications in Google Chrome.""" """Disable notifications in Google Chrome."""
defaults_key = 'default_content_setting_values' defaults_key = 'default_content_setting_values'
profiles = [] profiles = []
@ -601,13 +628,19 @@ def disable_chrome_notifications():
pref_file.write_text(json.dumps(pref_data, separators=(',', ':'))) pref_file.write_text(json.dumps(pref_data, separators=(',', ':')))
def enable_bsod_minidumps(): def disable_password_expiration() -> None:
"""Enable saving minidumps during BSoDs.""" """Disable password expiration for all users."""
cmd = ['wmic', 'RECOVEROS', 'set', 'DebugInfoType', '=', '3'] script_path = find_kit_dir('Scripts').joinpath('disable_password_expiration.ps1')
cmd = ['PowerShell', '-ExecutionPolicy', 'Bypass', '-File', script_path]
run_program(cmd) run_program(cmd)
def enable_ublock_origin(): def enable_bsod_minidumps() -> None:
"""Enable saving minidumps during BSoDs."""
reg_write_settings(REG_WINDOWS_BSOD_MINIDUMPS)
def enable_ublock_origin() -> None:
"""Enable uBlock Origin in supported browsers.""" """Enable uBlock Origin in supported browsers."""
base_paths = [ base_paths = [
PROGRAMFILES_64, PROGRAMFILES_32, os.environ.get('LOCALAPPDATA'), PROGRAMFILES_64, PROGRAMFILES_32, os.environ.get('LOCALAPPDATA'),
@ -637,7 +670,7 @@ def enable_ublock_origin():
popen_program(cmd, pipe=True) popen_program(cmd, pipe=True)
def fix_windows_temp(): def fix_windows_temp() -> None:
"""Restore default permissions for Windows\\Temp.""" """Restore default permissions for Windows\\Temp."""
permissions = ( permissions = (
'Users:(CI)(X,WD,AD)', 'Users:(CI)(X,WD,AD)',
@ -649,7 +682,7 @@ def fix_windows_temp():
# Install Functions # Install Functions
def install_firefox(): def install_firefox() -> None:
"""Install Firefox. """Install Firefox.
As far as I can tell if you use the EXE installers then it will use As far as I can tell if you use the EXE installers then it will use
@ -754,12 +787,12 @@ def install_libreoffice(
run_program(cmd) run_program(cmd)
def install_open_shell(): def install_open_shell() -> None:
"""Install Open Shell (just the Start Menu).""" """Install Open Shell (just the Start Menu)."""
skin_zip = get_tool_path('OpenShell', 'Fluent-Metro', suffix='zip') skin_zip = get_tool_path('OpenShell', 'Fluent-Metro', suffix='zip')
# Bail early # Bail early
if OS_VERSION != 10: if OS_VERSION < 10:
raise GenericWarning('Unsupported OS') raise GenericWarning('Unsupported OS')
# Install OpenShell # Install OpenShell
@ -784,49 +817,7 @@ def install_open_shell():
run_program(cmd) run_program(cmd)
def install_software_bundle(): def uninstall_firefox() -> None:
"""Install standard software bundle."""
download_tool('Ninite', 'Software Bundle')
installer = get_tool_path('Ninite', 'Software Bundle')
msg = 'Waiting for installations to finish...'
warning = 'NOTE: Press CTRL+c to manually resume if it gets stuck...'
# Start installations and wait for them to finish
ui.print_standard(msg)
ui.print_warning(warning, end='', flush=True)
proc = popen_program([installer])
try:
proc.wait()
except KeyboardInterrupt:
# Assuming user-forced continue
pass
# Clear info lines
print(
'\r\033[0K' # Cursor to start of current line and clear to end of line
'\033[F\033[54C' # Cursor to start of prev line and then move 54 right
'\033[0K', # Clear from cursor to end of line
end='', flush=True)
def install_vcredists():
"""Install latest supported Visual C++ runtimes."""
for year in (2012, 2013, 2022):
cmd_args = ['/install', '/passive', '/norestart']
if year == 2012:
cmd_args.pop(0)
name = f'VCRedist_{year}_x32'
download_tool('VCRedist', name)
installer = get_tool_path('VCRedist', name)
run_program([installer, *cmd_args])
if ARCH == '64':
name = f'{name[:-2]}64'
download_tool('VCRedist', name)
installer = get_tool_path('VCRedist', name)
run_program([installer, *cmd_args])
def uninstall_firefox():
"""Uninstall all copies of Firefox.""" """Uninstall all copies of Firefox."""
json_file = format_log_path(log_name='Installed Programs', timestamp=True) json_file = format_log_path(log_name='Installed Programs', timestamp=True)
json_file = json_file.with_name(f'{json_file.stem}.json') json_file = json_file.with_name(f'{json_file.stem}.json')
@ -847,13 +838,14 @@ def uninstall_firefox():
# Misc Functions # Misc Functions
def check_secure_boot_status(): def check_secure_boot_status() -> None:
"""Check Secure Boot status.""" """Check Secure Boot status."""
is_secure_boot_enabled(raise_exceptions=True, show_alert=True) is_secure_boot_enabled(raise_exceptions=True, show_alert=True)
def get_firefox_default_profile(profiles_ini): def get_firefox_default_profile(profiles_ini) -> Any:
"""Get Firefox default profile, returns(pathlib.Path, encoding) or None.""" """Get Firefox default profile, returns(pathlib.Path, encoding) or None."""
# TODO: Refactor to remove dependancy on Any
default_profile = None default_profile = None
encoding = None encoding = None
parser = None parser = None
@ -890,7 +882,7 @@ def get_firefox_default_profile(profiles_ini):
return (default_profile, encoding) return (default_profile, encoding)
def get_storage_status(): def get_storage_status() -> list[str]:
"""Get storage status for fixed disks, returns list.""" """Get storage status for fixed disks, returns list."""
report = get_volume_usage(use_colors=True) report = get_volume_usage(use_colors=True)
for disk in get_raw_disks(): for disk in get_raw_disks():
@ -900,14 +892,14 @@ def get_storage_status():
return report return report
def set_default_browser(): def set_default_browser() -> None:
"""Open Windows Settings to the default apps section.""" """Open Windows Settings to the default apps section."""
cmd = ['start', '', 'ms-settings:defaultapps'] cmd = ['start', '', 'ms-settings:defaultapps']
popen_program(cmd, shell=True) popen_program(cmd, shell=True)
# Tool Functions # Tool Functions
def export_aida64_report(): def export_aida64_report() -> None:
"""Export AIDA64 report.""" """Export AIDA64 report."""
report_path = format_log_path( report_path = format_log_path(
log_name='AIDA64 System Report', log_name='AIDA64 System Report',
@ -928,12 +920,12 @@ def export_aida64_report():
raise GenericError('Error(s) encountered exporting report.') raise GenericError('Error(s) encountered exporting report.')
def open_device_manager(): def open_device_manager() -> None:
"""Open Device Manager.""" """Open Device Manager."""
popen_program(['mmc', 'devmgmt.msc']) popen_program(['mmc', 'devmgmt.msc'])
def open_hwinfo_sensors(): def open_hwinfo_sensors() -> None:
"""Open HWiNFO sensors.""" """Open HWiNFO sensors."""
hwinfo_path = get_tool_path('HWiNFO', 'HWiNFO') hwinfo_path = get_tool_path('HWiNFO', 'HWiNFO')
base_config = hwinfo_path.with_name('general.ini') base_config = hwinfo_path.with_name('general.ini')
@ -949,22 +941,33 @@ def open_hwinfo_sensors():
run_tool('HWiNFO', 'HWiNFO', popen=True) run_tool('HWiNFO', 'HWiNFO', popen=True)
def open_snappy_driver_installer_origin(): def open_microsoft_store_updates() -> None:
"""Open Microsoft Store to the updates page."""
popen_program(['explorer', 'ms-windows-store:updates'])
def open_snappy_driver_installer_origin() -> None:
"""Open Snappy Driver Installer Origin.""" """Open Snappy Driver Installer Origin."""
if OS_VERSION == 11:
appid_services = ['appid', 'appidsvc', 'applockerfltr']
for svc in appid_services:
stop_service(svc)
if any([get_service_status(s) != 'stopped' for s in appid_services]):
raise GenericWarning('Failed to stop AppID services')
run_tool('SDIO', 'SDIO', cwd=True, pipe=True, popen=True) run_tool('SDIO', 'SDIO', cwd=True, pipe=True, popen=True)
def open_windows_activation(): def open_windows_activation() -> None:
"""Open Windows Activation.""" """Open Windows Activation."""
popen_program(['slui']) popen_program(['slui'])
def open_windows_updates(): def open_windows_updates() -> None:
"""Open Windows Updates.""" """Open Windows Updates."""
popen_program(['control', '/name', 'Microsoft.WindowsUpdate']) popen_program(['control', '/name', 'Microsoft.WindowsUpdate'])
def open_xmplay(): def open_xmplay() -> None:
"""Open XMPlay.""" """Open XMPlay."""
sleep(2) sleep(2)
run_tool('XMPlay', 'XMPlay', 'music.7z', cwd=True, popen=True) run_tool('XMPlay', 'XMPlay', 'music.7z', cwd=True, popen=True)

View file

@ -31,7 +31,10 @@ class GenericWarning(Exception):
# Functions # Functions
def bytes_to_string(size, decimals=0, use_binary=True): def bytes_to_string(
size: float | int,
decimals: int = 0,
use_binary: bool = True) -> str:
"""Convert size into a human-readable format, returns str. """Convert size into a human-readable format, returns str.
[Doctest] [Doctest]
@ -73,13 +76,13 @@ def bytes_to_string(size, decimals=0, use_binary=True):
return size_str return size_str
def sleep(seconds=2): def sleep(seconds: int | float = 2) -> None:
"""Simple wrapper for time.sleep.""" """Simple wrapper for time.sleep."""
time.sleep(seconds) time.sleep(seconds)
def string_to_bytes(size, assume_binary=False): def string_to_bytes(size: float | int | str, assume_binary: bool = False) -> int:
"""Convert human-readable size str to bytes and return an int.""" """Convert human-readable size to bytes and return an int."""
LOG.debug('size: %s, assume_binary: %s', size, assume_binary) LOG.debug('size: %s, assume_binary: %s', size, assume_binary)
scale = 1000 scale = 1000
size = str(size) size = str(size)

View file

@ -3,7 +3,8 @@
import itertools import itertools
import logging import logging
import pathlib
from typing import Iterable
# STATIC VARIABLES # STATIC VARIABLES
LOG = logging.getLogger(__name__) LOG = logging.getLogger(__name__)
@ -23,44 +24,41 @@ COLORS = {
# Functions # Functions
def clear_screen(): def clear_screen() -> None:
"""Clear screen using ANSI escape.""" """Clear screen using ANSI escape."""
print('\033c', end='', flush=True) print('\033c', end='', flush=True)
def color_string(strings, colors, sep=' '): def color_string(
strings: Iterable[str] | str,
colors: Iterable[str | None] | str,
sep=' ',
) -> str:
"""Build colored string using ANSI escapes, returns str.""" """Build colored string using ANSI escapes, returns str."""
clear_code = COLORS['CLEAR'] data = {'strings': strings, 'colors': colors}
msg = [] msg = []
# Convert to tuples if necessary # Convert input to tuples of strings
if isinstance(strings, (str, pathlib.Path)): for k, v in data.items():
strings = (strings,) if isinstance(v, str):
if isinstance(colors, (str, pathlib.Path)): # Avoid splitting string into a list of characters
colors = (colors,) data[k] = (v,)
try:
# Convert to strings if necessary iter(v)
try: except TypeError:
iter(strings) # Assuming single element passed, convert to string
except TypeError: data[k] = (str(v),)
# Assuming single element passed, convert to string
strings = (str(strings),)
try:
iter(colors)
except TypeError:
# Assuming single element passed, convert to string
colors = (str(colors),)
# Build new string with color escapes added # Build new string with color escapes added
for string, color in itertools.zip_longest(strings, colors): for string, color in itertools.zip_longest(data['strings'], data['colors']):
color_code = COLORS.get(color, clear_code) color_code = COLORS.get(str(color), COLORS['CLEAR'])
msg.append(f'{color_code}{string}{clear_code}') msg.append(f'{color_code}{string}{COLORS["CLEAR"]}')
# Done # Done
return sep.join(msg) return sep.join(msg)
def strip_colors(string): def strip_colors(string: str) -> str:
"""Strip known ANSI color escapes from string, returns str.""" """Strip known ANSI color escapes from string, returns str."""
LOG.debug('string: %s', string) LOG.debug('string: %s', string)
for color in COLORS.values(): for color in COLORS.values():

View file

@ -9,8 +9,10 @@ import subprocess
import sys import sys
import traceback import traceback
from collections import OrderedDict from typing import Any, Callable, Iterable
from prompt_toolkit import prompt from prompt_toolkit import prompt
from prompt_toolkit.document import Document
from prompt_toolkit.validation import Validator, ValidationError from prompt_toolkit.validation import Validator, ValidationError
try: try:
@ -36,12 +38,12 @@ PLATFORM = platform.system()
# Classes # Classes
class InputChoiceValidator(Validator): class InputChoiceValidator(Validator):
"""Validate that input is one of the provided choices.""" """Validate that input is one of the provided choices."""
def __init__(self, choices, allow_empty=False): def __init__(self, choices: Iterable[str], allow_empty: bool = False):
self.allow_empty = allow_empty self.allow_empty: bool = allow_empty
self.choices = [str(c).upper() for c in choices] self.choices: list[str] = [str(c).upper() for c in choices]
super().__init__() super().__init__()
def validate(self, document): def validate(self, document: Document) -> None:
text = document.text text = document.text
if not (text or self.allow_empty): if not (text or self.allow_empty):
raise ValidationError( raise ValidationError(
@ -56,7 +58,7 @@ class InputChoiceValidator(Validator):
class InputNotEmptyValidator(Validator): class InputNotEmptyValidator(Validator):
"""Validate that input is not empty.""" """Validate that input is not empty."""
def validate(self, document): def validate(self, document: Document) -> None:
text = document.text text = document.text
if not text: if not text:
raise ValidationError( raise ValidationError(
@ -66,11 +68,11 @@ class InputNotEmptyValidator(Validator):
class InputTicketIDValidator(Validator): class InputTicketIDValidator(Validator):
"""Validate that input resembles a ticket ID.""" """Validate that input resembles a ticket ID."""
def __init__(self, allow_empty=False): def __init__(self, allow_empty: bool = False):
self.allow_empty = allow_empty self.allow_empty: bool = allow_empty
super().__init__() super().__init__()
def validate(self, document): def validate(self, document: Document) -> None:
text = document.text text = document.text
if not (text or self.allow_empty): if not (text or self.allow_empty):
raise ValidationError( raise ValidationError(
@ -85,11 +87,11 @@ class InputTicketIDValidator(Validator):
class InputYesNoValidator(Validator): class InputYesNoValidator(Validator):
"""Validate that input is a yes or no.""" """Validate that input is a yes or no."""
def __init__(self, allow_empty=False): def __init__(self, allow_empty: bool = False):
self.allow_empty = allow_empty self.allow_empty: bool = allow_empty
super().__init__() super().__init__()
def validate(self, document): def validate(self, document: Document) -> None:
text = document.text text = document.text
if not (text or self.allow_empty): if not (text or self.allow_empty):
raise ValidationError( raise ValidationError(
@ -105,22 +107,20 @@ class InputYesNoValidator(Validator):
class Menu(): class Menu():
"""Object for tracking menu specific data and methods. """Object for tracking menu specific data and methods.
Menu items are added to an OrderedDict so the order is preserved.
ASSUMPTIONS: ASSUMPTIONS:
1. All entry names are unique. 1. All entry names are unique.
2. All action entry names start with different letters. 2. All action entry names start with different letters.
""" """
def __init__(self, title='[Untitled Menu]'): def __init__(self, title: str = '[Untitled Menu]'):
self.actions = OrderedDict() self.actions: dict[str, dict[Any, Any]] = {}
self.options = OrderedDict() self.options: dict[str, dict[Any, Any]] = {}
self.sets = OrderedDict() self.sets: dict[str, dict[Any, Any]] = {}
self.toggles = OrderedDict() self.toggles: dict[str, dict[Any, Any]] = {}
self.disabled_str = 'Disabled' self.disabled_str: str = 'Disabled'
self.separator = '' self.separator: str = ''
self.title = title self.title: str = title
def _generate_menu_text(self): def _generate_menu_text(self) -> str:
"""Generate menu text, returns str.""" """Generate menu text, returns str."""
separator_string = self._get_separator_string() separator_string = self._get_separator_string()
menu_lines = [self.title, separator_string] if self.title else [] menu_lines = [self.title, separator_string] if self.title else []
@ -161,14 +161,14 @@ class Menu():
def _get_display_name( def _get_display_name(
self, name, details, self, name, details,
index=None, no_checkboxes=True, setting_item=False): index=None, no_checkboxes=True, setting_item=False) -> str:
"""Format display name based on details and args, returns str.""" """Format display name based on details and args, returns str."""
disabled = details.get('Disabled', False) disabled = details.get('Disabled', False)
if setting_item and not details['Selected']: if setting_item and not details['Selected']:
# Display item in YELLOW # Display item in YELLOW
disabled = True disabled = True
checkmark = '*' checkmark = '*'
if 'DISPLAY' in os.environ or PLATFORM == 'Darwin': if 'CONEMUPID' in os.environ or 'DISPLAY' in os.environ or PLATFORM == 'Darwin':
checkmark = '' checkmark = ''
display_name = f'{index if index else name[:1].upper()}: ' display_name = f'{index if index else name[:1].upper()}: '
if not (index and index >= 10): if not (index and index >= 10):
@ -189,7 +189,7 @@ class Menu():
# Done # Done
return display_name return display_name
def _get_separator_string(self): def _get_separator_string(self) -> str:
"""Format separator length based on name lengths, returns str.""" """Format separator length based on name lengths, returns str."""
separator_length = 0 separator_length = 0
@ -211,7 +211,7 @@ class Menu():
# Done # Done
return self.separator * separator_length return self.separator * separator_length
def _get_valid_answers(self): def _get_valid_answers(self) -> list[str]:
"""Get valid answers based on menu items, returns list.""" """Get valid answers based on menu items, returns list."""
valid_answers = [] valid_answers = []
@ -234,10 +234,10 @@ class Menu():
# Done # Done
return valid_answers return valid_answers
def _resolve_selection(self, selection): def _resolve_selection(self, selection: str) -> tuple[str, dict[Any, Any]]:
"""Get menu item based on user selection, returns tuple.""" """Get menu item based on user selection, returns tuple."""
offset = 1 offset = 1
resolved_selection = None resolved_selection = tuple()
if selection.isnumeric(): if selection.isnumeric():
# Enumerate over numbered entries # Enumerate over numbered entries
entries = [ entries = [
@ -249,6 +249,10 @@ class Menu():
if details[1].get('Hidden', False): if details[1].get('Hidden', False):
offset -= 1 offset -= 1
elif str(_i+offset) == selection: elif str(_i+offset) == selection:
# TODO: Fix this typo!
# It was discovered after being in production for SEVERAL YEARS!
# Extra testing is needed to verify any calls to this function still
# depend on this functionality
resolved_selection = (details) resolved_selection = (details)
break break
else: else:
@ -261,7 +265,7 @@ class Menu():
# Done # Done
return resolved_selection return resolved_selection
def _update(self, single_selection=True, settings_mode=False): def _update(self, single_selection: bool = True, settings_mode: bool = False) -> None:
"""Update menu items in preparation for printing to screen.""" """Update menu items in preparation for printing to screen."""
index = 0 index = 0
@ -299,7 +303,8 @@ class Menu():
no_checkboxes=True, no_checkboxes=True,
) )
def _update_entry_selection_status(self, entry, toggle=True, status=None): def _update_entry_selection_status(
self, entry: str, toggle: bool = True, status: bool = False) -> None:
"""Update entry selection status either directly or by toggling.""" """Update entry selection status either directly or by toggling."""
if entry in self.sets: if entry in self.sets:
# Update targets not the set itself # Update targets not the set itself
@ -313,14 +318,14 @@ class Menu():
else: else:
section[entry]['Selected'] = status section[entry]['Selected'] = status
def _update_set_selection_status(self, targets, status): def _update_set_selection_status(self, targets: Iterable[str], status: bool) -> None:
"""Select or deselect options based on targets and status.""" """Select or deselect options based on targets and status."""
for option, details in self.options.items(): for option, details in self.options.items():
# If (new) status is True and this option is a target then select # If (new) status is True and this option is a target then select
# Otherwise deselect # Otherwise deselect
details['Selected'] = status and option in targets details['Selected'] = status and option in targets
def _user_select(self, prompt_msg): def _user_select(self, prompt_msg: str) -> str:
"""Show menu and select an entry, returns str.""" """Show menu and select an entry, returns str."""
menu_text = self._generate_menu_text() menu_text = self._generate_menu_text()
valid_answers = self._get_valid_answers() valid_answers = self._get_valid_answers()
@ -337,19 +342,19 @@ class Menu():
# Done # Done
return answer return answer
def add_action(self, name, details=None): def add_action(self, name: str, details: dict[Any, Any] | None = None) -> None:
"""Add action to menu.""" """Add action to menu."""
details = details if details else {} details = details if details else {}
details['Selected'] = details.get('Selected', False) details['Selected'] = details.get('Selected', False)
self.actions[name] = details self.actions[name] = details
def add_option(self, name, details=None): def add_option(self, name: str, details: dict[Any, Any] | None = None) -> None:
"""Add option to menu.""" """Add option to menu."""
details = details if details else {} details = details if details else {}
details['Selected'] = details.get('Selected', False) details['Selected'] = details.get('Selected', False)
self.options[name] = details self.options[name] = details
def add_set(self, name, details=None): def add_set(self, name: str, details: dict[Any, Any] | None = None) -> None:
"""Add set to menu.""" """Add set to menu."""
details = details if details else {} details = details if details else {}
details['Selected'] = details.get('Selected', False) details['Selected'] = details.get('Selected', False)
@ -361,13 +366,16 @@ class Menu():
# Add set # Add set
self.sets[name] = details self.sets[name] = details
def add_toggle(self, name, details=None): def add_toggle(self, name: str, details: dict[Any, Any] | None = None) -> None:
"""Add toggle to menu.""" """Add toggle to menu."""
details = details if details else {} details = details if details else {}
details['Selected'] = details.get('Selected', False) details['Selected'] = details.get('Selected', False)
self.toggles[name] = details self.toggles[name] = details
def advanced_select(self, prompt_msg='Please make a selection: '): def advanced_select(
self,
prompt_msg: str = 'Please make a selection: ',
) -> tuple[str, dict[Any, Any]]:
"""Display menu and make multiple selections, returns tuple. """Display menu and make multiple selections, returns tuple.
NOTE: Menu is displayed until an action entry is selected. NOTE: Menu is displayed until an action entry is selected.
@ -386,7 +394,10 @@ class Menu():
# Done # Done
return selected_entry return selected_entry
def settings_select(self, prompt_msg='Please make a selection: '): def settings_select(
self,
prompt_msg: str = 'Please make a selection: ',
) -> tuple[str, dict[Any, Any]]:
"""Display menu and make multiple selections, returns tuple. """Display menu and make multiple selections, returns tuple.
NOTE: Menu is displayed until an action entry is selected. NOTE: Menu is displayed until an action entry is selected.
@ -414,14 +425,18 @@ class Menu():
# Done # Done
return selected_entry return selected_entry
def simple_select(self, prompt_msg='Please make a selection: ', update=True): def simple_select(
self,
prompt_msg: str = 'Please make a selection: ',
update: bool = True,
) -> tuple[str, dict[Any, Any]]:
"""Display menu and make a single selection, returns tuple.""" """Display menu and make a single selection, returns tuple."""
if update: if update:
self._update() self._update()
user_selection = self._user_select(prompt_msg) user_selection = self._user_select(prompt_msg)
return self._resolve_selection(user_selection) return self._resolve_selection(user_selection)
def update(self): def update(self) -> None:
"""Update menu with default settings.""" """Update menu with default settings."""
self._update() self._update()
@ -431,17 +446,17 @@ class TryAndPrint():
The errors and warning attributes are used to allow fine-tuned results The errors and warning attributes are used to allow fine-tuned results
based on exception names. based on exception names.
""" """
def __init__(self, msg_bad='FAILED', msg_good='SUCCESS'): def __init__(self, msg_bad: str = 'FAILED', msg_good: str = 'SUCCESS'):
self.catch_all = True self.catch_all : bool = True
self.indent = INDENT self.indent: int = INDENT
self.list_errors = ['GenericError'] self.list_errors: list[str] = ['GenericError']
self.list_warnings = ['GenericWarning'] self.list_warnings: list[str] = ['GenericWarning']
self.msg_bad = msg_bad self.msg_bad: str = msg_bad
self.msg_good = msg_good self.msg_good: str = msg_good
self.verbose = False self.verbose : bool = False
self.width = WIDTH self.width: int = WIDTH
def _format_exception_message(self, _exception): def _format_exception_message(self, _exception: Exception) -> str:
"""Format using the exception's args or name, returns str.""" """Format using the exception's args or name, returns str."""
LOG.debug( LOG.debug(
'Formatting exception: %s, %s', 'Formatting exception: %s, %s',
@ -488,7 +503,11 @@ class TryAndPrint():
# Done # Done
return message return message
def _format_function_output(self, output, msg_good): def _format_function_output(
self,
output: list | subprocess.CompletedProcess,
msg_good: str,
) -> str:
"""Format function output for use in try_and_print(), returns str.""" """Format function output for use in try_and_print(), returns str."""
LOG.debug('Formatting output: %s', output) LOG.debug('Formatting output: %s', output)
@ -526,26 +545,33 @@ class TryAndPrint():
# Done # Done
return result_msg return result_msg
def _log_result(self, message, result_msg): def _log_result(self, message: str, result_msg: str) -> None:
"""Log result text without color formatting.""" """Log result text without color formatting."""
log_text = f'{" "*self.indent}{message:<{self.width}}{result_msg}' log_text = f'{" "*self.indent}{message:<{self.width}}{result_msg}'
for line in log_text.splitlines(): for line in log_text.splitlines():
line = strip_colors(line) line = strip_colors(line)
LOG.info(line) LOG.info(line)
def add_error(self, exception_name): def add_error(self, exception_name: str) -> None:
"""Add exception name to error list.""" """Add exception name to error list."""
if exception_name not in self.list_errors: if exception_name not in self.list_errors:
self.list_errors.append(exception_name) self.list_errors.append(exception_name)
def add_warning(self, exception_name): def add_warning(self, exception_name: str) -> None:
"""Add exception name to warning list.""" """Add exception name to warning list."""
if exception_name not in self.list_warnings: if exception_name not in self.list_warnings:
self.list_warnings.append(exception_name) self.list_warnings.append(exception_name)
def run( def run(
self, message, function, *args, self,
catch_all=None, msg_good=None, verbose=None, **kwargs): message: str,
function: Callable,
*args: Iterable[Any],
catch_all: bool | None = None,
msg_good: str | None = None,
verbose: bool | None = None,
**kwargs,
) -> dict[str, Any]:
"""Run a function and print the results, returns results as dict. """Run a function and print the results, returns results as dict.
If catch_all is True then (nearly) all exceptions will be caught. If catch_all is True then (nearly) all exceptions will be caught.
@ -556,7 +582,7 @@ class TryAndPrint():
msg_bad, or exception text. msg_bad, or exception text.
The output should be a list or a subprocess.CompletedProcess object. The output should be a list or a subprocess.CompletedProcess object.
If msg_good is passed it will override self.msg_good for this call. If msg_good is passed it will override self.msg_good.
If verbose is True then exception names or messages will be used for If verbose is True then exception names or messages will be used for
the result message. Otherwise it will simply be set to result_bad. the result message. Otherwise it will simply be set to result_bad.
@ -583,8 +609,8 @@ class TryAndPrint():
verbose = verbose if verbose is not None else self.verbose verbose = verbose if verbose is not None else self.verbose
# Build exception tuples # Build exception tuples
e_exceptions = tuple(get_exception(e) for e in self.list_errors) e_exceptions: tuple = tuple(get_exception(e) for e in self.list_errors)
w_exceptions = tuple(get_exception(e) for e in self.list_warnings) w_exceptions: tuple = tuple(get_exception(e) for e in self.list_warnings)
# Run function and catch exceptions # Run function and catch exceptions
print(f'{" "*self.indent}{message:<{self.width}}', end='', flush=True) print(f'{" "*self.indent}{message:<{self.width}}', end='', flush=True)
@ -632,7 +658,11 @@ class TryAndPrint():
# Functions # Functions
def abort(prompt_msg='Aborted.', show_prompt_msg=True, return_code=1): def abort(
prompt_msg: str = 'Aborted.',
show_prompt_msg: bool = True,
return_code: int = 1,
) -> None:
"""Abort script.""" """Abort script."""
print_warning(prompt_msg) print_warning(prompt_msg)
if show_prompt_msg: if show_prompt_msg:
@ -641,23 +671,24 @@ def abort(prompt_msg='Aborted.', show_prompt_msg=True, return_code=1):
sys.exit(return_code) sys.exit(return_code)
def ask(prompt_msg): def ask(prompt_msg: str) -> bool:
"""Prompt the user with a Y/N question, returns bool.""" """Prompt the user with a Y/N question, returns bool."""
validator = InputYesNoValidator() validator = InputYesNoValidator()
# Show prompt # Show prompt
response = input_text(f'{prompt_msg} [Y/N]: ', validator=validator) response = input_text(f'{prompt_msg} [Y/N]: ', validator=validator)
if response.upper().startswith('Y'): if response.upper().startswith('Y'):
answer = True LOG.info('%s Yes', prompt_msg)
elif response.upper().startswith('N'): return True
answer = False if response.upper().startswith('N'):
LOG.info('%s No', prompt_msg)
return False
# Done # This shouldn't ever be reached
LOG.info('%s%s', prompt_msg, 'Yes' if answer else 'No') raise ValueError(f'Invalid answer given: {response}')
return answer
def beep(repeat=1): def beep(repeat: int = 1) -> None:
"""Play system bell with optional repeat.""" """Play system bell with optional repeat."""
while repeat >= 1: while repeat >= 1:
# Print bell char without a newline # Print bell char without a newline
@ -666,7 +697,7 @@ def beep(repeat=1):
repeat -= 1 repeat -= 1
def choice(prompt_msg, choices): def choice(prompt_msg: str, choices: Iterable[str]) -> str:
"""Choose an option from a provided list, returns str. """Choose an option from a provided list, returns str.
Choices provided will be converted to uppercase and returned as such. Choices provided will be converted to uppercase and returned as such.
@ -684,7 +715,7 @@ def choice(prompt_msg, choices):
return response.upper() return response.upper()
def fix_prompt(message): def fix_prompt(message: str) -> str:
"""Fix prompt, returns str.""" """Fix prompt, returns str."""
if not message: if not message:
message = 'Input text: ' message = 'Input text: '
@ -695,7 +726,7 @@ def fix_prompt(message):
@cache @cache
def get_exception(name): def get_exception(name: str) -> Exception:
"""Get exception by name, returns exception object. """Get exception by name, returns exception object.
[Doctest] [Doctest]
@ -731,7 +762,7 @@ def get_exception(name):
return obj return obj
def get_ticket_id(): def get_ticket_id() -> str:
"""Get ticket ID, returns str.""" """Get ticket ID, returns str."""
prompt_msg = 'Please enter ticket ID:' prompt_msg = 'Please enter ticket ID:'
validator = InputTicketIDValidator() validator = InputTicketIDValidator()
@ -744,7 +775,9 @@ def get_ticket_id():
def input_text( def input_text(
prompt_msg='Enter text: ', allow_empty=False, validator=None, prompt_msg: str = 'Enter text: ',
allow_empty: bool = False,
validator: Validator | None = None,
) -> str: ) -> str:
"""Get input from user, returns str.""" """Get input from user, returns str."""
prompt_msg = fix_prompt(prompt_msg) prompt_msg = fix_prompt(prompt_msg)
@ -766,7 +799,7 @@ def input_text(
return result return result
def major_exception(): def major_exception() -> None:
"""Display traceback, optionally upload detailes, and exit.""" """Display traceback, optionally upload detailes, and exit."""
LOG.critical('Major exception encountered', exc_info=True) LOG.critical('Major exception encountered', exc_info=True)
print_error('Major exception', log=False) print_error('Major exception', log=False)
@ -780,12 +813,18 @@ def major_exception():
raise SystemExit(1) raise SystemExit(1)
def pause(prompt_msg='Press Enter to continue... '): def pause(prompt_msg: str = 'Press Enter to continue... ') -> None:
"""Simple pause implementation.""" """Simple pause implementation."""
input_text(prompt_msg, allow_empty=True) input_text(prompt_msg, allow_empty=True)
def print_colored(strings, colors, log=False, sep=' ', **kwargs): def print_colored(
strings: Iterable[str] | str,
colors: Iterable[str | None] | str,
log: bool = False,
sep: str = ' ',
**kwargs,
) -> None:
"""Prints strings in the colors specified.""" """Prints strings in the colors specified."""
LOG.debug( LOG.debug(
'strings: %s, colors: %s, sep: %s, kwargs: %s', 'strings: %s, colors: %s, sep: %s, kwargs: %s',
@ -803,7 +842,7 @@ def print_colored(strings, colors, log=False, sep=' ', **kwargs):
LOG.info(strip_colors(msg)) LOG.info(strip_colors(msg))
def print_error(msg, log=True, **kwargs): def print_error(msg: str, log: bool = True, **kwargs) -> None:
"""Prints message in RED and log as ERROR.""" """Prints message in RED and log as ERROR."""
if 'file' not in kwargs: if 'file' not in kwargs:
# Only set if not specified # Only set if not specified
@ -813,14 +852,14 @@ def print_error(msg, log=True, **kwargs):
LOG.error(msg) LOG.error(msg)
def print_info(msg, log=True, **kwargs): def print_info(msg: str, log: bool = True, **kwargs) -> None:
"""Prints message in BLUE and log as INFO.""" """Prints message in BLUE and log as INFO."""
print_colored(msg, 'BLUE', **kwargs) print_colored(msg, 'BLUE', **kwargs)
if log: if log:
LOG.info(msg) LOG.info(msg)
def print_report(report, indent=None, log=True): def print_report(report: list[str], indent=None, log: bool = True) -> None:
"""Print report to screen and optionally to log.""" """Print report to screen and optionally to log."""
for line in report: for line in report:
if indent: if indent:
@ -830,21 +869,21 @@ def print_report(report, indent=None, log=True):
LOG.info(strip_colors(line)) LOG.info(strip_colors(line))
def print_standard(msg, log=True, **kwargs): def print_standard(msg: str, log: bool = True, **kwargs) -> None:
"""Prints message and log as INFO.""" """Prints message and log as INFO."""
print(msg, **kwargs) print(msg, **kwargs)
if log: if log:
LOG.info(msg) LOG.info(msg)
def print_success(msg, log=True, **kwargs): def print_success(msg: str, log: bool = True, **kwargs) -> None:
"""Prints message in GREEN and log as INFO.""" """Prints message in GREEN and log as INFO."""
print_colored(msg, 'GREEN', **kwargs) print_colored(msg, 'GREEN', **kwargs)
if log: if log:
LOG.info(msg) LOG.info(msg)
def print_warning(msg, log=True, **kwargs): def print_warning(msg: str, log: bool = True, **kwargs) -> None:
"""Prints message in YELLOW and log as WARNING.""" """Prints message in YELLOW and log as WARNING."""
if 'file' not in kwargs: if 'file' not in kwargs:
# Only set if not specified # Only set if not specified
@ -854,7 +893,7 @@ def print_warning(msg, log=True, **kwargs):
LOG.warning(msg) LOG.warning(msg)
def set_title(title): def set_title(title: str) -> None:
"""Set window title.""" """Set window title."""
LOG.debug('title: %s', title) LOG.debug('title: %s', title)
if os.name == 'nt': if os.name == 'nt':
@ -863,14 +902,19 @@ def set_title(title):
print_error('Setting the title is only supported under Windows.') print_error('Setting the title is only supported under Windows.')
def show_data(message, data, color=None, indent=None, width=None): def show_data(
message: str,
data: Any,
color: str | None = None,
indent: int | None = None,
width: int | None = None,
) -> None:
"""Display info using default or provided indent and width.""" """Display info using default or provided indent and width."""
colors = (None, color if color else None)
indent = INDENT if indent is None else indent indent = INDENT if indent is None else indent
width = WIDTH if width is None else width width = WIDTH if width is None else width
print_colored( print_colored(
(f'{" "*indent}{message:<{width}}', data), (f'{" "*indent}{message:<{width}}', data),
colors, (None, color if color else None),
log=True, log=True,
sep='', sep='',
) )

View file

@ -4,6 +4,8 @@
import logging import logging
import pathlib import pathlib
from typing import Any
from wk.exe import run_program from wk.exe import run_program
from wk.std import PLATFORM from wk.std import PLATFORM
@ -13,7 +15,7 @@ LOG = logging.getLogger(__name__)
# Functions # Functions
def capture_pane(pane_id=None): def capture_pane(pane_id: str | None = None) -> str:
"""Capture text from current or target pane, returns str.""" """Capture text from current or target pane, returns str."""
cmd = ['tmux', 'capture-pane', '-p'] cmd = ['tmux', 'capture-pane', '-p']
if pane_id: if pane_id:
@ -24,7 +26,7 @@ def capture_pane(pane_id=None):
return proc.stdout.strip() return proc.stdout.strip()
def clear_pane(pane_id=None): def clear_pane(pane_id: str | None = None) -> None:
"""Clear pane buffer for current or target pane.""" """Clear pane buffer for current or target pane."""
commands = [ commands = [
['tmux', 'send-keys', '-R'], ['tmux', 'send-keys', '-R'],
@ -38,8 +40,15 @@ def clear_pane(pane_id=None):
run_program(cmd, check=False) run_program(cmd, check=False)
def fix_layout(layout, forced=False): def fix_layout(
"""Fix pane sizes based on layout.""" layout: dict[str, dict[str, Any]],
clear_on_resize: bool = False,
forced: bool = False,
) -> None:
"""Fix pane sizes based on layout.
NOTE: The magic +/- 1 values are for the split rows/columns.
"""
resize_kwargs = [] resize_kwargs = []
# Bail early # Bail early
@ -47,37 +56,58 @@ def fix_layout(layout, forced=False):
# Layout should be fine # Layout should be fine
return return
# Clear current pane if needed
if clear_on_resize:
clear_pane()
# Remove closed panes # Remove closed panes
for data in layout.values(): for data in layout.values():
data['Panes'] = [pane for pane in data['Panes'] if poll_pane(pane)] data['Panes'] = [pane for pane in data['Panes'] if poll_pane(pane)]
# Calc height for "floating" row # Calculate constraints
# NOTE: We start with height +1 to account for the splits (i.e. splits = num rows - 1) avail_horizontal, avail_vertical = get_window_size()
floating_height = 1 + get_window_size()[1] avail_vertical -= layout['Current'].get('height', 0)
for group in ('Title', 'Info', 'Current', 'Workers'): for group in ('Title', 'Info'):
if layout[group]['Panes']: if not layout[group]['Panes']:
group_height = 1 + layout[group].get('height', 0)
if group == 'Workers':
group_height *= len(layout[group]['Panes'])
floating_height -= group_height
# Update main panes
for section, data in layout.items():
# "Floating" pane(s)
if 'height' not in data and section in ('Info', 'Current', 'Workers'):
for pane_id in data['Panes']:
resize_kwargs.append({'pane_id': pane_id, 'height': floating_height})
# Rest of the panes
if section == 'Workers':
# Skip for now
continue continue
if 'height' in data: avail_vertical -= layout[group].get('height', 0) + 1
for pane_id in data['Panes']: num_workers = len(layout['Workers']['Panes'])
resize_kwargs.append({'pane_id': pane_id, 'height': data['height']}) avail_vertical -= num_workers * (layout['Workers'].get('height', 0) + 1)
if 'width' in data: avail_horizontal -= layout['Progress']['width'] + 1
for pane_id in data['Panes']:
resize_kwargs.append({'pane_id': pane_id, 'width': data['width']}) # Fix heights
for group, data in layout.items():
if not data['Panes'] or group in ('Started', 'Progress'):
continue
resize_kwargs.append(
{'pane_id': data['Panes'][0], 'height': data.get('height', avail_vertical)}
)
if group == 'Workers' and len(data['Panes']) > 1:
for pane_id in data['Panes'][1:]:
resize_kwargs.append(
{'pane_id': pane_id, 'height': data.get('height', avail_vertical)}
)
# Fix widths
resize_kwargs.append(
{'pane_id': layout['Progress']['Panes'][0], 'width': layout['Progress']['width']}
)
resize_kwargs.append(
{'pane_id': layout['Started']['Panes'][0], 'height': layout['Started']['height']}
)
for group, data in layout.items():
num_panes = len(data['Panes'])
if num_panes < 2 or group not in ('Title', 'Info'):
continue
pane_width, remainder = divmod(avail_horizontal - (num_panes-1), num_panes)
for pane_id in data['Panes']:
new_width = pane_width
if remainder > 0:
new_width += 1
remainder -= 1
resize_kwargs.append({'pane_id': pane_id, 'width': new_width})
# Resize panes
for kwargs in resize_kwargs: for kwargs in resize_kwargs:
try: try:
resize_pane(**kwargs) resize_pane(**kwargs)
@ -85,41 +115,22 @@ def fix_layout(layout, forced=False):
# Assuming pane was closed just before resizing # Assuming pane was closed just before resizing
pass pass
# Update "group" panes widths
for group in ('Title', 'Info'):
num_panes = len(layout[group]['Panes'])
if num_panes <= 1:
continue
width = int( (get_pane_size()[0] - (1 - num_panes)) / num_panes )
for pane_id in layout[group]['Panes']:
resize_pane(pane_id, width=width)
if group == 'Title':
# (re)fix Started pane
resize_pane(layout['Started']['Panes'][0], width=layout['Started']['width'])
# Bail early def get_pane_size(pane_id: str | None = None) -> tuple[int, int]:
if not (
layout['Workers']['Panes']
and 'height' in layout['Workers']
and floating_height > 0
):
return
# Update worker heights
for worker in reversed(layout['Workers']['Panes']):
resize_pane(worker, height=layout['Workers']['height'])
def get_pane_size(pane_id=None):
"""Get current or target pane size, returns tuple.""" """Get current or target pane size, returns tuple."""
cmd = ['tmux', 'display', '-p'] cmd = ['tmux', 'display-message', '-p']
if pane_id: if pane_id:
cmd.extend(['-t', pane_id]) cmd.extend(['-t', pane_id])
cmd.append('#{pane_width} #{pane_height}') cmd.append('#{pane_width} #{pane_height}')
# Get resolution # Get resolution
proc = run_program(cmd, check=False) proc = run_program(cmd, check=False)
width, height = proc.stdout.strip().split() try:
width, height = proc.stdout.strip().split()
except ValueError:
# Assuming this is a race condition as it usually happens inside the
# background fix layout loop
return 0, 0
width = int(width) width = int(width)
height = int(height) height = int(height)
@ -127,9 +138,9 @@ def get_pane_size(pane_id=None):
return (width, height) return (width, height)
def get_window_size(): def get_window_size() -> tuple[int, int]:
"""Get current window size, returns tuple.""" """Get current window size, returns tuple."""
cmd = ['tmux', 'display', '-p', '#{window_width} #{window_height}'] cmd = ['tmux', 'display-message', '-p', '#{window_width} #{window_height}']
# Get resolution # Get resolution
proc = run_program(cmd, check=False) proc = run_program(cmd, check=False)
@ -141,7 +152,7 @@ def get_window_size():
return (width, height) return (width, height)
def kill_all_panes(pane_id=None): def kill_all_panes(pane_id: str | None = None) -> None:
"""Kill all panes except for the current or target pane.""" """Kill all panes except for the current or target pane."""
cmd = ['tmux', 'kill-pane', '-a'] cmd = ['tmux', 'kill-pane', '-a']
if pane_id: if pane_id:
@ -151,7 +162,7 @@ def kill_all_panes(pane_id=None):
run_program(cmd, check=False) run_program(cmd, check=False)
def kill_pane(*pane_ids): def kill_pane(*pane_ids: str) -> None:
"""Kill pane(s) by id.""" """Kill pane(s) by id."""
cmd = ['tmux', 'kill-pane', '-t'] cmd = ['tmux', 'kill-pane', '-t']
@ -160,7 +171,7 @@ def kill_pane(*pane_ids):
run_program(cmd+[pane_id], check=False) run_program(cmd+[pane_id], check=False)
def layout_needs_fixed(layout): def layout_needs_fixed(layout: dict[str, dict[str, Any]]) -> bool:
"""Check if layout needs fixed, returns bool.""" """Check if layout needs fixed, returns bool."""
needs_fixed = False needs_fixed = False
@ -175,23 +186,13 @@ def layout_needs_fixed(layout):
get_pane_size(pane)[0] != data['width'] for pane in data['Panes'] get_pane_size(pane)[0] != data['width'] for pane in data['Panes']
) )
# TODO: Re-enable?
## Group panes
#for group in ('Title', 'Info'):
# num_panes = len(layout[group]['Panes'])
# if num_panes <= 1:
# continue
# width = int( (get_pane_size()[0] - (1 - num_panes)) / num_panes )
# for pane in layout[group]['Panes']:
# needs_fixed = needs_fixed or abs(get_pane_size(pane)[0] - width) > 2
# Done # Done
return needs_fixed return needs_fixed
def poll_pane(pane_id): def poll_pane(pane_id: str) -> bool:
"""Check if pane exists, returns bool.""" """Check if pane exists, returns bool."""
cmd = ['tmux', 'list-panes', '-F', '#D'] cmd = ['tmux', 'list-panes', '-F', '#{pane_id}']
# Get list of panes # Get list of panes
proc = run_program(cmd, check=False) proc = run_program(cmd, check=False)
@ -202,7 +203,12 @@ def poll_pane(pane_id):
def prep_action( def prep_action(
cmd=None, working_dir=None, text=None, watch_file=None, watch_cmd='cat'): cmd: str | None = None,
working_dir: pathlib.Path | str | None = None,
text: str | None = None,
watch_file: pathlib.Path | str | None = None,
watch_cmd: str = 'cat',
) -> list[str]:
"""Prep action to perform during a tmux call, returns list. """Prep action to perform during a tmux call, returns list.
This will prep for running a basic command, displaying text on screen, This will prep for running a basic command, displaying text on screen,
@ -242,7 +248,7 @@ def prep_action(
'cat', 'cat',
]) ])
elif watch_cmd == 'tail': elif watch_cmd == 'tail':
action_cmd.extend(['tail', '-q', '-f']) action_cmd.extend(['tail', '-f'])
action_cmd.append(watch_file) action_cmd.append(watch_file)
else: else:
LOG.error('No action specified') LOG.error('No action specified')
@ -252,7 +258,7 @@ def prep_action(
return action_cmd return action_cmd
def prep_file(path): def prep_file(path: pathlib.Path | str) -> None:
"""Check if file exists and create empty file if not.""" """Check if file exists and create empty file if not."""
path = pathlib.Path(path).resolve() path = pathlib.Path(path).resolve()
try: try:
@ -262,7 +268,11 @@ def prep_file(path):
pass pass
def resize_pane(pane_id=None, width=None, height=None): def resize_pane(
pane_id: str | None = None,
width: int | None = None,
height: int | None = None,
) -> None:
"""Resize current or target pane. """Resize current or target pane.
NOTE: kwargs is only here to make calling this function easier NOTE: kwargs is only here to make calling this function easier
@ -287,7 +297,7 @@ def resize_pane(pane_id=None, width=None, height=None):
run_program(cmd, check=False) run_program(cmd, check=False)
def respawn_pane(pane_id, **action): def respawn_pane(pane_id: str, **action) -> None:
"""Respawn pane with action.""" """Respawn pane with action."""
cmd = ['tmux', 'respawn-pane', '-k', '-t', pane_id] cmd = ['tmux', 'respawn-pane', '-k', '-t', pane_id]
cmd.extend(prep_action(**action)) cmd.extend(prep_action(**action))
@ -297,11 +307,14 @@ def respawn_pane(pane_id, **action):
def split_window( def split_window(
lines=None, percent=None, lines: int | None = None,
behind=False, vertical=False, percent: int | None = None,
target_id=None, **action): behind: bool = False,
vertical: bool = False,
target_id: str | None = None,
**action) -> str:
"""Split tmux window, run action, and return pane_id as str.""" """Split tmux window, run action, and return pane_id as str."""
cmd = ['tmux', 'split-window', '-d', '-PF', '#D'] cmd = ['tmux', 'split-window', '-d', '-PF', '#{pane_id}']
# Safety checks # Safety checks
if not (lines or percent): if not (lines or percent):
@ -322,7 +335,7 @@ def split_window(
if lines: if lines:
cmd.extend(['-l', str(lines)]) cmd.extend(['-l', str(lines)])
elif percent: elif percent:
cmd.extend(['-p', str(percent)]) cmd.extend(['-l', f'{percent}%'])
# New pane action # New pane action
cmd.extend(prep_action(**action)) cmd.extend(prep_action(**action))
@ -332,7 +345,7 @@ def split_window(
return proc.stdout.strip() return proc.stdout.strip()
def zoom_pane(pane_id=None): def zoom_pane(pane_id: str | None = None) -> None:
"""Toggle zoom status for current or target pane.""" """Toggle zoom status for current or target pane."""
cmd = ['tmux', 'resize-pane', '-Z'] cmd = ['tmux', 'resize-pane', '-Z']
if pane_id: if pane_id:

View file

@ -7,6 +7,7 @@ import time
from copy import deepcopy from copy import deepcopy
from os import environ from os import environ
from typing import Any
from wk.exe import start_thread from wk.exe import start_thread
from wk.std import sleep from wk.std import sleep
@ -21,7 +22,7 @@ TMUX_LAYOUT = { # NOTE: This needs to be in order from top to bottom
'Info': {'Panes': []}, 'Info': {'Panes': []},
'Current': {'Panes': [environ.get('TMUX_PANE', None)]}, 'Current': {'Panes': [environ.get('TMUX_PANE', None)]},
'Workers': {'Panes': []}, 'Workers': {'Panes': []},
'Started': {'Panes': [], 'width': TMUX_SIDE_WIDTH}, 'Started': {'Panes': [], 'height': TMUX_TITLE_HEIGHT},
'Progress': {'Panes': [], 'width': TMUX_SIDE_WIDTH}, 'Progress': {'Panes': [], 'width': TMUX_SIDE_WIDTH},
} }
@ -29,12 +30,13 @@ TMUX_LAYOUT = { # NOTE: This needs to be in order from top to bottom
# Classes # Classes
class TUI(): class TUI():
"""Object for tracking TUI elements.""" """Object for tracking TUI elements."""
def __init__(self, title_text=None) -> None: def __init__(self, title_text: str | None = None):
self.layout = deepcopy(TMUX_LAYOUT) self.clear_on_resize = False
self.side_width = TMUX_SIDE_WIDTH self.layout: dict[str, dict[str, Any]] = deepcopy(TMUX_LAYOUT)
self.title_text = title_text if title_text else 'Title Text' self.side_width: int = TMUX_SIDE_WIDTH
self.title_text_line2 = '' self.title_text: str = title_text if title_text else 'Title Text'
self.title_colors = ['BLUE', None] self.title_text_line2: str = ''
self.title_colors: list[str] = ['BLUE', '']
# Init tmux and start a background process to maintain layout # Init tmux and start a background process to maintain layout
self.init_tmux() self.init_tmux()
@ -44,7 +46,11 @@ class TUI():
atexit.register(tmux.kill_all_panes) atexit.register(tmux.kill_all_panes)
def add_info_pane( def add_info_pane(
self, lines=None, percent=None, update_layout=True, **tmux_args, self,
lines: int | None = None,
percent: int = 0,
update_layout: bool = True,
**tmux_args,
) -> None: ) -> None:
"""Add info pane.""" """Add info pane."""
if not (lines or percent): if not (lines or percent):
@ -78,7 +84,12 @@ class TUI():
# Add pane # Add pane
self.layout['Info']['Panes'].append(tmux.split_window(**tmux_args)) self.layout['Info']['Panes'].append(tmux.split_window(**tmux_args))
def add_title_pane(self, line1, line2=None, colors=None) -> None: def add_title_pane(
self,
line1: str,
line2: str | None = None,
colors: list[str] | None = None,
) -> None:
"""Add pane to title row.""" """Add pane to title row."""
lines = [line1, line2] lines = [line1, line2]
colors = colors if colors else self.title_colors.copy() colors = colors if colors else self.title_colors.copy()
@ -105,7 +116,11 @@ class TUI():
self.layout['Title']['Panes'].append(tmux.split_window(**tmux_args)) self.layout['Title']['Panes'].append(tmux.split_window(**tmux_args))
def add_worker_pane( def add_worker_pane(
self, lines=None, percent=None, update_layout=True, **tmux_args, self,
lines: int | None = None,
percent: int = 0,
update_layout: bool = True,
**tmux_args,
) -> None: ) -> None:
"""Add worker pane.""" """Add worker pane."""
height = lines height = lines
@ -122,7 +137,7 @@ class TUI():
tmux_args.update({ tmux_args.update({
'behind': False, 'behind': False,
'lines': lines, 'lines': lines,
'percent': percent, 'percent': percent if percent else None,
'target_id': None, 'target_id': None,
'vertical': True, 'vertical': True,
}) })
@ -131,8 +146,8 @@ class TUI():
if update_layout: if update_layout:
self.layout['Workers']['height'] = height self.layout['Workers']['height'] = height
# Add pane # Add pane (ensure panes are sorted top to bottom)
self.layout['Workers']['Panes'].append(tmux.split_window(**tmux_args)) self.layout['Workers']['Panes'].insert(0, tmux.split_window(**tmux_args))
def clear_current_pane(self) -> None: def clear_current_pane(self) -> None:
"""Clear screen and history for current pane.""" """Clear screen and history for current pane."""
@ -142,10 +157,10 @@ class TUI():
"""Clear current pane height and update layout.""" """Clear current pane height and update layout."""
self.layout['Current'].pop('height', None) self.layout['Current'].pop('height', None)
def fix_layout(self, forced=True) -> None: def fix_layout(self, forced: bool = True) -> None:
"""Fix tmux layout based on self.layout.""" """Fix tmux layout based on self.layout."""
try: try:
tmux.fix_layout(self.layout, forced=forced) tmux.fix_layout(self.layout, clear_on_resize=self.clear_on_resize, forced=forced)
except RuntimeError: except RuntimeError:
# Assuming self.panes changed while running # Assuming self.panes changed while running
pass pass
@ -165,6 +180,25 @@ class TUI():
self.layout.clear() self.layout.clear()
self.layout.update(deepcopy(TMUX_LAYOUT)) self.layout.update(deepcopy(TMUX_LAYOUT))
# Progress
self.layout['Progress']['Panes'].append(tmux.split_window(
lines=TMUX_SIDE_WIDTH,
text=' ',
))
# Started
self.layout['Started']['Panes'].append(tmux.split_window(
behind=True,
lines=2,
target_id=self.layout['Progress']['Panes'][0],
text=ansi.color_string(
['Started', time.strftime("%Y-%m-%d %H:%M %Z")],
['BLUE', None],
sep='\n',
),
vertical=True,
))
# Title # Title
self.layout['Title']['Panes'].append(tmux.split_window( self.layout['Title']['Panes'].append(tmux.split_window(
behind=True, behind=True,
@ -177,22 +211,8 @@ class TUI():
), ),
)) ))
# Started # Done
self.layout['Started']['Panes'].append(tmux.split_window( sleep(0.2)
lines=TMUX_SIDE_WIDTH,
target_id=self.layout['Title']['Panes'][0],
text=ansi.color_string(
['Started', time.strftime("%Y-%m-%d %H:%M %Z")],
['BLUE', None],
sep='\n',
),
))
# Progress
self.layout['Progress']['Panes'].append(tmux.split_window(
lines=TMUX_SIDE_WIDTH,
text=' ',
))
def remove_all_info_panes(self) -> None: def remove_all_info_panes(self) -> None:
"""Remove all info panes and update layout.""" """Remove all info panes and update layout."""
@ -208,19 +228,38 @@ class TUI():
self.layout['Workers']['Panes'].clear() self.layout['Workers']['Panes'].clear()
tmux.kill_pane(*panes) tmux.kill_pane(*panes)
def set_current_pane_height(self, height) -> None: def reset_title_pane(
self,
line1: str = 'Title Text',
line2: str = '',
colors: list[str] | None = None,
) -> None:
"""Remove all extra title panes, reset main title pane, and update layout."""
colors = self.title_colors if colors is None else colors
panes = self.layout['Title']['Panes'].copy()
if len(panes) > 1:
tmux.kill_pane(*panes[1:])
self.layout['Title']['Panes'] = panes[:1]
self.set_title(line1, line2, colors)
def set_current_pane_height(self, height: int) -> None:
"""Set current pane height and update layout.""" """Set current pane height and update layout."""
self.layout['Current']['height'] = height self.layout['Current']['height'] = height
tmux.resize_pane(height=height) tmux.resize_pane(height=height)
def set_progress_file(self, progress_file) -> None: def set_progress_file(self, progress_file: str) -> None:
"""Set the file to use for the progresse pane.""" """Set the file to use for the progresse pane."""
tmux.respawn_pane( tmux.respawn_pane(
pane_id=self.layout['Progress']['Panes'][0], pane_id=self.layout['Progress']['Panes'][0],
watch_file=progress_file, watch_file=progress_file,
) )
def set_title(self, line1, line2=None, colors=None) -> None: def set_title(
self,
line1: str,
line2: str | None = None,
colors: list[str] | None = None,
) -> None:
"""Set title text.""" """Set title text."""
self.title_text = line1 self.title_text = line1
self.title_text_line2 = line2 if line2 else '' self.title_text_line2 = line2 if line2 else ''
@ -251,7 +290,7 @@ class TUI():
# Functions # Functions
def fix_layout(layout, forced=False): def fix_layout(layout, forced: bool = False) -> None:
"""Fix pane sizes based on layout.""" """Fix pane sizes based on layout."""
resize_kwargs = [] resize_kwargs = []
@ -320,7 +359,7 @@ def fix_layout(layout, forced=False):
tmux.resize_pane(workers[1], height=next_height) tmux.resize_pane(workers[1], height=next_height)
workers.pop(0) workers.pop(0)
def layout_needs_fixed(layout): def layout_needs_fixed(layout) -> bool:
"""Check if layout needs fixed, returns bool.""" """Check if layout needs fixed, returns bool."""
needs_fixed = False needs_fixed = False
@ -338,18 +377,6 @@ def layout_needs_fixed(layout):
# Done # Done
return needs_fixed return needs_fixed
def test():
"""TODO: Deleteme"""
ui = TUI()
ui.add_info_pane(lines=10, text='Info One')
ui.add_info_pane(lines=10, text='Info Two')
ui.add_info_pane(lines=10, text='Info Three')
ui.add_worker_pane(lines=3, text='Work One')
ui.add_worker_pane(lines=3, text='Work Two')
ui.add_worker_pane(lines=3, text='Work Three')
ui.fix_layout()
return ui
if __name__ == '__main__': if __name__ == '__main__':
print("This file is not meant to be called directly.") print("This file is not meant to be called directly.")

View file

@ -80,6 +80,21 @@ function copy_live_env() {
mkdir -p "$PROFILE_DIR/airootfs/usr/local/bin" mkdir -p "$PROFILE_DIR/airootfs/usr/local/bin"
rsync -aI "$ROOT_DIR/scripts/" "$PROFILE_DIR/airootfs/usr/local/bin/" rsync -aI "$ROOT_DIR/scripts/" "$PROFILE_DIR/airootfs/usr/local/bin/"
echo "Copying WizardKit UFD files..."
rsync -aI --exclude="macOS-boot-icon.tar" "$ROOT_DIR/setup/ufd/" "$PROFILE_DIR/airootfs/usr/share/WizardKit/"
tar xaf "$ROOT_DIR/setup/ufd/macOS-boot-icon.tar" -C "$PROFILE_DIR/airootfs/usr/share/WizardKit"
cp "$ROOT_DIR/images/rEFInd.png" "$PROFILE_DIR/airootfs/usr/share/WizardKit/EFI/Boot/rEFInd.png"
cp "$ROOT_DIR/images/Syslinux.png" "$PROFILE_DIR/airootfs/usr/share/WizardKit/syslinux/syslinux.png"
echo "Copying Memtest86+ files..."
rsync -aI "/boot/memtest86+/memtest.bin" "$PROFILE_DIR/airootfs/usr/share/WizardKit/syslinux/"
rsync -aI "/boot/memtest86+/memtest.efi" "$PROFILE_DIR/airootfs/usr/share/WizardKit/EFI/Memtest86+/"
mv "$PROFILE_DIR/airootfs/usr/share/WizardKit/EFI/Memtest86+"/{memtest.efi,bootx64.efi}
# Pre-compile Python scripts
unset PYTHONPYCACHEPREFIX
python -m compileall "$PROFILE_DIR/airootfs/usr/local/bin/"
# Update profiledef.sh to set proper permissions for executable files # Update profiledef.sh to set proper permissions for executable files
for _file in $(find "$PROFILE_DIR/airootfs" -executable -type f | sed "s%$PROFILE_DIR/airootfs%%" | sort); do for _file in $(find "$PROFILE_DIR/airootfs" -executable -type f | sed "s%$PROFILE_DIR/airootfs%%" | sort); do
sed -i "\$i\ [\"$_file\"]=\"0:0:755\"" "$PROFILE_DIR/profiledef.sh" sed -i "\$i\ [\"$_file\"]=\"0:0:755\"" "$PROFILE_DIR/profiledef.sh"
@ -112,48 +127,15 @@ function update_live_env() {
username="tech" username="tech"
label="${KIT_NAME_SHORT}_LINUX" label="${KIT_NAME_SHORT}_LINUX"
# Boot config
cp "$ROOT_DIR/images/Syslinux.png" "$PROFILE_DIR/syslinux/splash.png"
sed -i -r "s/___+/${KIT_NAME_FULL}/" "$PROFILE_DIR/airootfs/usr/share/WizardKit/syslinux/syslinux.cfg"
# MOTD # MOTD
sed -i -r "s/KIT_NAME_SHORT/$KIT_NAME_SHORT/" "$PROFILE_DIR/profiledef.sh" sed -i -r "s/KIT_NAME_SHORT/$KIT_NAME_SHORT/" "$PROFILE_DIR/profiledef.sh"
sed -i -r "s/KIT_NAME_FULL/$KIT_NAME_SHORT/" "$PROFILE_DIR/profiledef.sh" sed -i -r "s/KIT_NAME_FULL/$KIT_NAME_SHORT/" "$PROFILE_DIR/profiledef.sh"
sed -i -r "s/SUPPORT_URL/$KIT_NAME_SHORT/" "$PROFILE_DIR/profiledef.sh" sed -i -r "s/SUPPORT_URL/$KIT_NAME_SHORT/" "$PROFILE_DIR/profiledef.sh"
# Boot config (legacy)
mkdir -p "$TEMP_DIR" 2>/dev/null
git clone --depth=1 https://github.com/ipxe/wimboot "$TEMP_DIR/wimboot"
rsync -aI "$TEMP_DIR/wimboot"/{LICENSE.txt,README.md,wimboot} "$PROFILE_DIR/syslinux/wimboot/"
cp "$ROOT_DIR/images/Pxelinux.png" "$PROFILE_DIR/syslinux/pxelinux.png"
cp "$ROOT_DIR/images/Syslinux.png" "$PROFILE_DIR/syslinux/syslinux.png"
sed -i -r "s/__+/$KIT_NAME_FULL/" "$PROFILE_DIR/syslinux/syslinux.cfg"
# Boot config (UEFI)
curl -Lo "$TEMP_DIR/refind.zip" "https://sourceforge.net/projects/refind/files/latest/download"
7z x -aoa "$TEMP_DIR/refind.zip" -o"$TEMP_DIR/refind"
cp "$ROOT_DIR/images/rEFInd.png" "$PROFILE_DIR/EFI/boot/rEFInd.png"
cp "$TEMP_DIR/refind"/refind*/"refind/refind_x64.efi" "$PROFILE_DIR/EFI/boot/bootx64.efi"
rsync -aI "$TEMP_DIR/refind"/refind*/refind/drivers_x64/ "$PROFILE_DIR/EFI/boot/drivers_x64/"
rsync -aI "$TEMP_DIR/refind"/refind*/refind/icons/ "$PROFILE_DIR/EFI/boot/icons/"
sed -i "s/%ARCHISO_LABEL%/${label}/" "$PROFILE_DIR/EFI/boot/refind.conf"
# Memtest86+ (Open Source)
mkdir -p "$PROFILE_DIR/EFI/memtest86+"
mkdir -p "$TEMP_DIR/memtest86+"
curl -Lo "$TEMP_DIR/memtest86+/memtest86-binaries.zip" "https://memtest.org/download/v6.10/mt86plus_6.10.binaries.zip"
7z e "$TEMP_DIR/memtest86+/memtest86-binaries.zip" -o"$TEMP_DIR/memtest86+" "memtest64.efi"
mv "$TEMP_DIR/memtest86+/memtest64.efi" "$PROFILE_DIR/EFI/memtest86+/bootx64.efi"
# Memtest86 (Passmark)
mkdir -p "$PROFILE_DIR/EFI/memtest86/Benchmark"
mkdir -p "$TEMP_DIR/memtest86"
curl -Lo "$TEMP_DIR/memtest86/memtest86-usb.zip" "https://www.memtest86.com/downloads/memtest86-usb.zip"
7z e -aoa "$TEMP_DIR/memtest86/memtest86-usb.zip" -o"$TEMP_DIR/memtest86" "memtest86-usb.img"
7z e -aoa "$TEMP_DIR/memtest86/memtest86-usb.img" -o"$TEMP_DIR/memtest86" "MemTest86.img"
7z x -aoa "$TEMP_DIR/memtest86/MemTest86.img" -o"$TEMP_DIR/memtest86"
rm "$TEMP_DIR/memtest86/EFI/BOOT/BOOTIA32.efi"
mv "$TEMP_DIR/memtest86/EFI/BOOT/BOOTX64.efi" "$PROFILE_DIR/EFI/memtest86/bootx64.efi"
mv "$TEMP_DIR/memtest86/EFI/BOOT"/* "$PROFILE_DIR/EFI/memtest86"/
mv "$TEMP_DIR/memtest86/help"/* "$PROFILE_DIR/EFI/memtest86"/
mv "$TEMP_DIR/memtest86/license.rtf" "$PROFILE_DIR/EFI/memtest86"/
# Hostname # Hostname
echo "$hostname" > "$PROFILE_DIR/airootfs/etc/hostname" echo "$hostname" > "$PROFILE_DIR/airootfs/etc/hostname"
echo "127.0.1.1 $hostname.localdomain $hostname" >> "$PROFILE_DIR/airootfs/etc/hosts" echo "127.0.1.1 $hostname.localdomain $hostname" >> "$PROFILE_DIR/airootfs/etc/hosts"
@ -172,9 +154,6 @@ function update_live_env() {
# MOTD # MOTD
sed -i -r "s/_+/$KIT_NAME_FULL Linux Environment/" "$PROFILE_DIR/airootfs/etc/motd" sed -i -r "s/_+/$KIT_NAME_FULL Linux Environment/" "$PROFILE_DIR/airootfs/etc/motd"
# Network
ln -s "/run/systemd/resolve/stub-resolv.conf" "$PROFILE_DIR/airootfs/etc/resolv.conf"
# Oh My ZSH # Oh My ZSH
git clone --depth=1 https://github.com/robbyrussell/oh-my-zsh.git "$SKEL_DIR/.oh-my-zsh" git clone --depth=1 https://github.com/robbyrussell/oh-my-zsh.git "$SKEL_DIR/.oh-my-zsh"
rm -Rf "$SKEL_DIR/.oh-my-zsh/.git" rm -Rf "$SKEL_DIR/.oh-my-zsh/.git"
@ -347,16 +326,6 @@ function build_iso() {
-v "$PROFILE_DIR" \ -v "$PROFILE_DIR" \
| tee -a "$LOG_DIR/$DATETIME.log" | tee -a "$LOG_DIR/$DATETIME.log"
# Build better ISO
rsync -aI "$PROFILE_DIR/EFI/" "${ISO_DIR:-safety}/EFI/"
rsync -aI --ignore-existing "$PROFILE_DIR/syslinux/" "${ISO_DIR:-safety}/syslinux/"
## Sketchy bit ##
. /usr/bin/mkarchiso -o "${OUT_DIR}" -w "${WORK_DIR}" "${PROFILE_DIR}"
isofs_dir="${ISO_DIR}"
image_name="${KIT_NAME_SHORT}-Linux-${DATE}-x86_64.iso"
rm "${OUT_DIR}/${image_name}"
_build_iso_image
# Cleanup # Cleanup
echo "Removing temp files..." echo "Removing temp files..."
rm "$TEMP_DIR/Linux" -Rf | tee -a "$LOG_DIR/$DATETIME.log" rm "$TEMP_DIR/Linux" -Rf | tee -a "$LOG_DIR/$DATETIME.log"

View file

@ -6,10 +6,6 @@
setlocal EnableDelayedExpansion setlocal EnableDelayedExpansion
title WizardKit: Build Tool title WizardKit: Build Tool
call :CheckFlags %* call :CheckFlags %*
rem TODO: Remove warning
echo "Windows PE build is currently under development"
echo " Proceeding will likely result in errors so be warned"
pause
call :CheckElevation || goto Exit call :CheckElevation || goto Exit
call :FindKitsRoot || goto ErrorKitNotFound call :FindKitsRoot || goto ErrorKitNotFound
@ -19,14 +15,8 @@ set "dandi_set_env=%adk_root%\Deployment Tools\DandISetEnv.bat"
if not exist "%dandi_set_env%" (goto ErrorKitNotFound) if not exist "%dandi_set_env%" (goto ErrorKitNotFound)
call "%dandi_set_env%" || goto ErrorUnknown call "%dandi_set_env%" || goto ErrorUnknown
:EnsureCRLF
rem Rewrite main.py using PowerShell to have CRLF/`r`n lineendings
set "script=%~dp0\.bin\Scripts\borrowed\set-eol.ps1"
set "main=%~dp0\.bin\Scripts\settings\main.py"
powershell -executionpolicy bypass -noprofile -file %script% -lineEnding win -file %main% || goto ErrorUnknown
:Launch :Launch
set "script=%~dp0\.bin\Scripts\build_pe.ps1" set "script=%~dp0\pe\build_pe.ps1"
powershell -executionpolicy bypass -noprofile -file %script% || goto ErrorUnknown powershell -executionpolicy bypass -noprofile -file %script% || goto ErrorUnknown
goto Exit goto Exit

View file

@ -11,3 +11,4 @@ smartmontools-svn
ttf-font-awesome-4 ttf-font-awesome-4
udevil udevil
wd719x-firmware wd719x-firmware
wimboot-bin

View file

@ -10,6 +10,7 @@ bc
bind bind
bluez bluez
bluez-utils bluez-utils
bolt
btrfs-progs btrfs-progs
cbatticon cbatticon
chntpw chntpw
@ -31,15 +32,21 @@ dosfstools
dunst dunst
e2fsprogs e2fsprogs
edk2-shell edk2-shell
efibootmgr
evince evince
exfatprogs exfatprogs
f2fs-tools
fatresize
feh feh
ffmpeg ffmpeg
firefox firefox
foot-terminfo
gnome-keyring gnome-keyring
gnu-netcat
gparted gparted
gpicview gpicview
gptfdisk gptfdisk
grub
gsmartcontrol gsmartcontrol
hardinfo-gtk3 hardinfo-gtk3
hexedit hexedit
@ -50,6 +57,7 @@ intel-ucode
iwd iwd
iwgtk iwgtk
jfsutils jfsutils
kitty-terminfo
ldns ldns
leafpad leafpad
less less
@ -57,65 +65,77 @@ lha
libewf libewf
libinput libinput
libldm libldm
libusb-compat
libxft libxft
linux linux
linux-firmware linux-firmware
linux-firmware-marvell
lm_sensors lm_sensors
lsscsi
lvm2 lvm2
lzip lzip
man-db man-db
man-pages man-pages
mdadm mdadm
mediainfo mediainfo
memtest86+
memtest86-efi memtest86-efi
mesa-demos mesa-demos
mesa-utils mesa-utils
mkinitcpio mkinitcpio
mkinitcpio-archiso mkinitcpio-archiso
mkinitcpio-nfs-utils
mkvtoolnix-cli mkvtoolnix-cli
mprime-bin mprime-bin
mpv mpv
mtools mtools
nano nano
nbd
ncdu ncdu
ndisc6
nfs-utils
nmap
noto-fonts noto-fonts
noto-fonts-cjk noto-fonts-cjk
ntfs-3g
numlockx numlockx
nvme-cli nvme-cli
open-iscsi
openbox openbox
openssh openssh
opensuperclone-git opensuperclone-git
otf-font-awesome-4 otf-font-awesome-4
p7zip p7zip
papirus-icon-theme papirus-icon-theme
parted
perl perl
picom picom
pipes.sh pipes.sh
pv pv
python python
python-docopt python-prompt_toolkit
python-psutil python-psutil
python-pytz python-pytz
python-requests python-requests
qemu-guest-agent qemu-guest-agent
qemu-guest-agent refind
reiserfsprogs
reiserfsprogs reiserfsprogs
rfkill rfkill
rng-tools
rofi rofi
rsync rsync
rxvt-unicode rxvt-unicode
rxvt-unicode-terminfo rxvt-unicode-terminfo
sdparm
smartmontools-svn smartmontools-svn
sof-firmware
speedtest-cli speedtest-cli
spice-vdagent spice-vdagent
squashfs-tools
st st
sudo sudo
sysbench sysbench
sysfsutils sysfsutils
syslinux syslinux
systemd-resolvconf
systemd-sysvcompat systemd-sysvcompat
terminus-font terminus-font
testdisk testdisk
@ -125,6 +145,8 @@ tigervnc
tint2 tint2
tk tk
tmux tmux
tpm2-tools
tpm2-tss
tree tree
ttf-font-awesome-4 ttf-font-awesome-4
ttf-hack ttf-hack
@ -135,6 +157,8 @@ ufw
unarj unarj
unrar unrar
unzip unzip
usb_modeswitch
usbmuxd
usbutils usbutils
util-linux util-linux
veracrypt veracrypt
@ -142,7 +166,9 @@ vim
virtualbox-guest-utils virtualbox-guest-utils
volumeicon volumeicon
wd719x-firmware wd719x-firmware
wezterm-terminfo
which which
wimboot-bin
wimlib wimlib
wmctrl wmctrl
xarchiver xarchiver
@ -150,6 +176,7 @@ xf86-input-libinput
xf86-video-amdgpu xf86-video-amdgpu
xf86-video-fbdev xf86-video-fbdev
xf86-video-nouveau xf86-video-nouveau
xf86-video-qxl
xf86-video-vesa xf86-video-vesa
xfsprogs xfsprogs
xorg-server xorg-server

View file

@ -5,6 +5,8 @@ curl
dos2unix dos2unix
git git
gtk3 gtk3
memtest86+
memtest86+-efi
p7zip p7zip
perl-rename perl-rename
pv pv

View file

@ -1 +1 @@
LANG=en_US.UTF-8 LANG=C.UTF-8

View file

@ -1,70 +0,0 @@
#
# SPDX-License-Identifier: GPL-3.0-or-later
# vim:set ft=sh
# MODULES
# The following modules are loaded before any boot hooks are
# run. Advanced users may wish to specify all system modules
# in this array. For instance:
# MODULES=(piix ide_disk reiserfs)
MODULES=()
# BINARIES
# This setting includes any additional binaries a given user may
# wish into the CPIO image. This is run last, so it may be used to
# override the actual binaries included by a given hook
# BINARIES are dependency parsed, so you may safely ignore libraries
BINARIES=()
# FILES
# This setting is similar to BINARIES above, however, files are added
# as-is and are not parsed in any way. This is useful for config files.
FILES=()
# HOOKS
# This is the most important setting in this file. The HOOKS control the
# modules and scripts added to the image, and what happens at boot time.
# Order is important, and it is recommended that you do not change the
# order in which HOOKS are added. Run 'mkinitcpio -H <hook name>' for
# help on a given hook.
# 'base' is _required_ unless you know precisely what you are doing.
# 'udev' is _required_ in order to automatically load modules
# 'filesystems' is _required_ unless you specify your fs modules in MODULES
# Examples:
## This setup specifies all modules in the MODULES setting above.
## No raid, lvm2, or encrypted root is needed.
# HOOKS=(base)
#
## This setup will autodetect all modules for your system and should
## work as a sane default
# HOOKS=(base udev autodetect block filesystems)
#
## This setup will generate a 'full' image which supports most systems.
## No autodetection is done.
# HOOKS=(base udev block filesystems)
#
## This setup assembles a pata mdadm array with an encrypted root FS.
## Note: See 'mkinitcpio -H mdadm' for more information on raid devices.
# HOOKS=(base udev block mdadm encrypt filesystems)
#
## This setup loads an lvm2 volume group on a usb device.
# HOOKS=(base udev block lvm2 filesystems)
#
## NOTE: If you have /usr on a separate partition, you MUST include the
# usr, fsck and shutdown hooks.
HOOKS=(base udev modconf memdisk archiso_shutdown archiso archiso_loop_mnt archiso_pxe_common archiso_pxe_nbd archiso_pxe_http archiso_pxe_nfs archiso_kms block filesystems keyboard)
# COMPRESSION
# Use this to compress the initramfs image. By default, gzip compression
# is used. Use 'cat' to create an uncompressed image.
#COMPRESSION="gzip"
#COMPRESSION="bzip2"
#COMPRESSION="lzma"
COMPRESSION="xz"
#COMPRESSION="lzop"
#COMPRESSION="lz4"
#COMPRESSION="zstd"
# COMPRESSION_OPTIONS
# Additional options for the compressor
#COMPRESSION_OPTIONS=()

View file

@ -0,0 +1,2 @@
HOOKS=(base udev modconf kms memdisk archiso archiso_loop_mnt archiso_pxe_common archiso_pxe_nbd archiso_pxe_http archiso_pxe_nfs block filesystems keyboard)
COMPRESSION="xz"

View file

@ -1,8 +0,0 @@
# mkinitcpio preset file for the 'linux' package on archiso
PRESETS=('archiso')
ALL_kver='/boot/vmlinuz-linux'
ALL_config='/etc/mkinitcpio.conf'
archiso_image="/boot/initramfs-linux.img"

View file

@ -1,13 +0,0 @@
# remove from airootfs!
[Trigger]
Operation = Install
Type = Package
Target = glibc
[Action]
Description = Uncommenting en_US.UTF-8 locale and running locale-gen...
When = PostTransaction
Depends = glibc
Depends = sed
Depends = sh
Exec = /bin/sh -c "sed -i 's/#\(en_US\.UTF-8\)/\1/' /etc/locale.gen && locale-gen"

View file

@ -0,0 +1,23 @@
# This is /run/systemd/resolve/stub-resolv.conf managed by man:systemd-resolved(8).
# Do not edit.
#
# This file might be symlinked as /etc/resolv.conf. If you're looking at
# /etc/resolv.conf and seeing this text, you have followed the symlink.
#
# This is a dynamic resolv.conf file for connecting local clients to the
# internal DNS stub resolver of systemd-resolved. This file lists all
# configured search domains.
#
# Run "resolvectl status" to see details about the uplink DNS servers
# currently in use.
#
# Third party programs should typically not access this file directly, but only
# through the symlink at /etc/resolv.conf. To manage man:resolv.conf(5) in a
# different way, replace this symlink by a static file or a different symlink.
#
# See man:systemd-resolved.service(8) for details about the supported modes of
# operation for /etc/resolv.conf.
nameserver 127.0.0.53
options edns0 trust-ad
search .

View file

@ -12,7 +12,6 @@ alias fix-perms='find -type d -exec chmod 755 "{}" \; && find -type f -exec chmo
alias hexedit='hexedit --color' alias hexedit='hexedit --color'
alias hw-info='sudo hw-info | less -S' alias hw-info='sudo hw-info | less -S'
alias ip='ip -br -c' alias ip='ip -br -c'
alias journalctl-datarec="echo -e 'Monitoring journal output...\n' && journalctl -kf | grep -Ei 'ata|nvme|scsi|sd[a..z]+|usb|comreset|critical|error'"
alias less='less -S' alias less='less -S'
alias ls='ls --color=auto' alias ls='ls --color=auto'
alias mkdir='mkdir -p' alias mkdir='mkdir -p'

View file

@ -2,8 +2,17 @@
# #
## Calculate DPI, update settings if necessary, then start desktop apps ## Calculate DPI, update settings if necessary, then start desktop apps
MONITOR=$(xrandr --listmonitors | grep -E '^\s+[0-9]' | head -1 | sed -r 's/^.*\s+(.*)$/\1/')
REGEX_XRANDR='^.* ([0-9]+)x([0-9]+)\+[0-9]+\+[0-9]+.* ([0-9]+)mm x ([0-9]+)mm.*$' REGEX_XRANDR='^.* ([0-9]+)x([0-9]+)\+[0-9]+\+[0-9]+.* ([0-9]+)mm x ([0-9]+)mm.*$'
# Resize screen in VMs
if lsmod | grep -Eq 'qxl|virtio_gpu'; then
echo -n "Starting VM guest services..."
spice-vdagent
sleep 0.5
xrandr --output "${MONITOR}" --auto
fi
echo -n "Getting display details... " echo -n "Getting display details... "
# Get screen data # Get screen data

View file

@ -1,5 +1,10 @@
setterm -blank 0 -powerdown 0 2>/dev/null setterm -blank 0 -powerdown 0 2>/dev/null
if [ "$(fgconsole 2>/dev/null)" -eq "1" ]; then if [ "$(fgconsole 2>/dev/null)" -eq "1" ]; then
# VM guest init
if lsmod | grep -Eq 'qxl|virtio_gpu'; then
systemctl start spice-vdagentd.service
fi
# Set up teststation details # Set up teststation details
$HOME/.setup_teststation $HOME/.setup_teststation

View file

@ -0,0 +1,2 @@
[Network]
IPv6PrivacyExtensions=yes

View file

@ -1,10 +1,21 @@
[Match] [Match]
# Matching with "Type=ether" causes issues with containers because it also matches virtual Ethernet interfaces (veth*).
# See https://bugs.archlinux.org/task/70892
# Instead match by globbing the network interface name.
Name=en* Name=en*
Name=eth* Name=eth*
[Network] [Network]
DHCP=yes DHCP=yes
IPv6PrivacyExtensions=yes MulticastDNS=yes
[DHCP] # systemd-networkd does not set per-interface-type default route metrics
RouteMetric=512 # https://github.com/systemd/systemd/issues/17698
# Explicitly set route metric, so that Ethernet is preferred over Wi-Fi and Wi-Fi is preferred over mobile broadband.
# Use values from NetworkManager. From nm_device_get_route_metric_default in
# https://gitlab.freedesktop.org/NetworkManager/NetworkManager/-/blob/main/src/core/devices/nm-device.c
[DHCPv4]
RouteMetric=100
[IPv6AcceptRA]
RouteMetric=100

View file

@ -1,10 +0,0 @@
[Match]
Name=wlp*
Name=wlan*
[Network]
DHCP=yes
IPv6PrivacyExtensions=yes
[DHCP]
RouteMetric=1024

View file

@ -0,0 +1,17 @@
[Match]
Name=wl*
[Network]
DHCP=yes
MulticastDNS=yes
# systemd-networkd does not set per-interface-type default route metrics
# https://github.com/systemd/systemd/issues/17698
# Explicitly set route metric, so that Ethernet is preferred over Wi-Fi and Wi-Fi is preferred over mobile broadband.
# Use values from NetworkManager. From nm_device_get_route_metric_default in
# https://gitlab.freedesktop.org/NetworkManager/NetworkManager/-/blob/main/src/core/devices/nm-device.c
[DHCPv4]
RouteMetric=600
[IPv6AcceptRA]
RouteMetric=600

View file

@ -0,0 +1,16 @@
[Match]
Name=ww*
[Network]
DHCP=yes
# systemd-networkd does not set per-interface-type default route metrics
# https://github.com/systemd/systemd/issues/17698
# Explicitly set route metric, so that Ethernet is preferred over Wi-Fi and Wi-Fi is preferred over mobile broadband.
# Use values from NetworkManager. From nm_device_get_route_metric_default in
# https://gitlab.freedesktop.org/NetworkManager/NetworkManager/-/blob/main/src/core/devices/nm-device.c
[DHCPv4]
RouteMetric=700
[IPv6AcceptRA]
RouteMetric=700

View file

@ -0,0 +1,4 @@
# Default systemd-resolved configuration for archiso
[Resolve]
MulticastDNS=yes

View file

@ -5,4 +5,4 @@ Description=Temporary /etc/pacman.d/gnupg directory
What=tmpfs What=tmpfs
Where=/etc/pacman.d/gnupg Where=/etc/pacman.d/gnupg
Type=tmpfs Type=tmpfs
Options=mode=0755 Options=mode=0755,noswap

View file

@ -1 +0,0 @@
/usr/lib/systemd/system/rngd.service

View file

@ -1,15 +1,15 @@
[Unit] [Unit]
Description=Initializes Pacman keyring Description=Initializes Pacman keyring
Wants=haveged.service
After=haveged.service
Requires=etc-pacman.d-gnupg.mount Requires=etc-pacman.d-gnupg.mount
After=etc-pacman.d-gnupg.mount After=etc-pacman.d-gnupg.mount time-sync.target
BindsTo=etc-pacman.d-gnupg.mount
Before=archlinux-keyring-wkd-sync.service
[Service] [Service]
Type=oneshot Type=oneshot
RemainAfterExit=yes RemainAfterExit=yes
ExecStart=/usr/bin/pacman-key --init ExecStart=/usr/bin/pacman-key --init
ExecStart=/usr/bin/pacman-key --populate archlinux ExecStart=/usr/bin/pacman-key --populate
[Install] [Install]
WantedBy=multi-user.target WantedBy=multi-user.target

View file

@ -0,0 +1,4 @@
disable-ccid
disable-pinpad
pcsc-driver /usr/lib/libpcsclite.so
pcsc-shared

View file

@ -1,7 +1,7 @@
title %ARCHISO_LABEL% title %ARCHISO_LABEL%
sort-key 01 sort-key 01
linux /%INSTALL_DIR%/boot/x86_64/vmlinuz-linux linux /%INSTALL_DIR%/boot/x86_64/vmlinuz-linux
initrd /%INSTALL_DIR%/boot/intel-ucode.img initrd /%INSTALL_DIR%/boot/intel-ucode.img
initrd /%INSTALL_DIR%/boot/amd-ucode.img initrd /%INSTALL_DIR%/boot/amd-ucode.img
initrd /%INSTALL_DIR%/boot/x86_64/initramfs-linux.img initrd /%INSTALL_DIR%/boot/x86_64/initramfs-linux.img
options archisobasedir=%INSTALL_DIR% archisolabel=%ARCHISO_LABEL% options archisobasedir=%INSTALL_DIR% archisodevice=UUID=%ARCHISO_UUID%

View file

@ -1,7 +0,0 @@
title %ARCHISO_LABEL% (Copy to RAM)
sort-key 02
linux /%INSTALL_DIR%/boot/x86_64/vmlinuz-linux
initrd /%INSTALL_DIR%/boot/intel-ucode.img
initrd /%INSTALL_DIR%/boot/amd-ucode.img
initrd /%INSTALL_DIR%/boot/x86_64/initramfs-linux.img
options archisobasedir=%INSTALL_DIR% archisolabel=%ARCHISO_LABEL% copytoram

View file

@ -0,0 +1,109 @@
# Load partition table and file system modules
insmod part_gpt
insmod part_msdos
insmod fat
insmod iso9660
insmod ntfs
insmod ntfscomp
insmod exfat
insmod udf
# Use graphics-mode output
if loadfont "${prefix}/fonts/unicode.pf2" ; then
insmod all_video
set gfxmode="auto"
terminal_input console
terminal_output console
fi
# Enable serial console
insmod serial
insmod usbserial_common
insmod usbserial_ftdi
insmod usbserial_pl2303
insmod usbserial_usbdebug
if serial --unit=0 --speed=115200; then
terminal_input --append serial
terminal_output --append serial
fi
# Search for the ISO volume
if [ -z "${ARCHISO_UUID}" ]; then
if [ -z "${ARCHISO_HINT}" ]; then
regexp --set=1:ARCHISO_HINT '^\(([^)]+)\)' "${cmdpath}"
fi
search --no-floppy --set=root --file '%ARCHISO_SEARCH_FILENAME%' --hint "${ARCHISO_HINT}"
probe --set ARCHISO_UUID --fs-uuid "${root}"
fi
# Get a human readable platform identifier
if [ "${grub_platform}" == 'efi' ]; then
archiso_platform='UEFI'
if [ "${grub_cpu}" == 'x86_64' ]; then
archiso_platform="x64 ${archiso_platform}"
elif [ "${grub_cpu}" == 'i386' ]; then
archiso_platform="IA32 ${archiso_platform}"
else
archiso_platform="${grub_cpu} ${archiso_platform}"
fi
elif [ "${grub_platform}" == 'pc' ]; then
archiso_platform='BIOS'
else
archiso_platform="${grub_cpu} ${grub_platform}"
fi
# Set default menu entry
default=archlinux
timeout=15
timeout_style=menu
# Menu entries
menuentry "%ARCHISO_LABEL% (${archiso_platform})" --class arch --class gnu-linux --class gnu --class os --id 'archlinux' {
set gfxpayload=keep
linux /%INSTALL_DIR%/boot/%ARCH%/vmlinuz-linux archisobasedir=%INSTALL_DIR% archisodevice=UUID=${ARCHISO_UUID}
initrd /%INSTALL_DIR%/boot/intel-ucode.img /%INSTALL_DIR%/boot/amd-ucode.img /%INSTALL_DIR%/boot/%ARCH%/initramfs-linux.img
}
if [ "${grub_platform}" == 'efi' -a "${grub_cpu}" == 'x86_64' -a -f '/boot/memtest86+/memtest.efi' ]; then
menuentry 'Run Memtest86+ (RAM test)' --class memtest86 --class memtest --class gnu --class tool {
set gfxpayload=800x600,1024x768
linux /boot/memtest86+/memtest.efi
}
fi
if [ "${grub_platform}" == 'pc' -a -f '/boot/memtest86+/memtest' ]; then
menuentry 'Run Memtest86+ (RAM test)' --class memtest86 --class memtest --class gnu --class tool {
set gfxpayload=800x600,1024x768
linux /boot/memtest86+/memtest
}
fi
if [ "${grub_platform}" == 'efi' ]; then
if [ "${grub_cpu}" == 'x86_64' -a -f '/shellx64.efi' ]; then
menuentry 'UEFI Shell' --class efi {
chainloader /shellx64.efi
}
elif [ "${grub_cpu}" == "i386" -a -f '/shellia32.efi' ]; then
menuentry 'UEFI Shell' --class efi {
chainloader /shellia32.efi
}
fi
menuentry 'UEFI Firmware Settings' --id 'uefi-firmware' {
fwsetup
}
fi
menuentry 'System shutdown' --class shutdown --class poweroff {
echo 'System shutting down...'
halt
}
menuentry 'System restart' --class reboot --class restart {
echo 'System rebooting...'
reboot
}
# GRUB init tune for accessibility
play 600 988 1 1319 4

View file

@ -0,0 +1,73 @@
# https://www.supergrubdisk.org/wiki/Loopback.cfg
# Search for the ISO volume
search --no-floppy --set=archiso_img_dev --file "${iso_path}"
probe --set archiso_img_dev_uuid --fs-uuid "${archiso_img_dev}"
# Get a human readable platform identifier
if [ "${grub_platform}" == 'efi' ]; then
archiso_platform='UEFI'
if [ "${grub_cpu}" == 'x86_64' ]; then
archiso_platform="x64 ${archiso_platform}"
elif [ "${grub_cpu}" == 'i386' ]; then
archiso_platform="IA32 ${archiso_platform}"
else
archiso_platform="${grub_cpu} ${archiso_platform}"
fi
elif [ "${grub_platform}" == 'pc' ]; then
archiso_platform='BIOS'
else
archiso_platform="${grub_cpu} ${grub_platform}"
fi
# Set default menu entry
default=archlinux
timeout=15
timeout_style=menu
# Menu entries
menuentry "%ARCHISO_LABEL% (${archiso_platform})" --class arch --class gnu-linux --class gnu --class os --id 'archlinux' {
set gfxpayload=keep
linux /%INSTALL_DIR%/boot/%ARCH%/vmlinuz-linux archisobasedir=%INSTALL_DIR% img_dev=UUID=${archiso_img_dev_uuid} img_loop="${iso_path}"
initrd /%INSTALL_DIR%/boot/intel-ucode.img /%INSTALL_DIR%/boot/amd-ucode.img /%INSTALL_DIR%/boot/%ARCH%/initramfs-linux.img
}
if [ "${grub_platform}" == 'efi' -a "${grub_cpu}" == 'x86_64' -a -f '/boot/memtest86+/memtest.efi' ]; then
menuentry 'Run Memtest86+ (RAM test)' --class memtest86 --class memtest --class gnu --class tool {
set gfxpayload=800x600,1024x768
linux /boot/memtest86+/memtest.efi
}
fi
if [ "${grub_platform}" == 'pc' -a -f '/boot/memtest86+/memtest' ]; then
menuentry 'Run Memtest86+ (RAM test)' --class memtest86 --class memtest --class gnu --class tool {
set gfxpayload=800x600,1024x768
linux /boot/memtest86+/memtest
}
fi
if [ "${grub_platform}" == 'efi' ]; then
if [ "${grub_cpu}" == 'x86_64' -a -f '/shellx64.efi' ]; then
menuentry 'UEFI Shell' --class efi {
chainloader /shellx64.efi
}
elif [ "${grub_cpu}" == "i386" -a -f '/shellia32.efi' ]; then
menuentry 'UEFI Shell' --class efi {
chainloader /shellia32.efi
}
fi
menuentry 'UEFI Firmware Settings' --id 'uefi-firmware' {
fwsetup
}
fi
menuentry 'System shutdown' --class shutdown --class poweroff {
echo 'System shutting down...'
halt
}
menuentry 'System restart' --class reboot --class restart {
echo 'System rebooting...'
reboot
}

View file

@ -8,16 +8,19 @@ iso_application="KIT_NAME_FULL Linux Environment"
iso_version="$(date +%Y-%m-%d)" iso_version="$(date +%Y-%m-%d)"
install_dir="arch" install_dir="arch"
buildmodes=('iso') buildmodes=('iso')
bootmodes=('bios.syslinux.mbr' 'bios.syslinux.eltorito') bootmodes=('bios.syslinux.mbr' 'bios.syslinux.eltorito'
'uefi-ia32.grub.esp' 'uefi-x64.grub.esp'
'uefi-ia32.grub.eltorito' 'uefi-x64.grub.eltorito')
arch="x86_64" arch="x86_64"
pacman_conf="pacman.conf" pacman_conf="pacman.conf"
airootfs_image_type="squashfs" airootfs_image_type="squashfs"
airootfs_image_tool_options=('-comp' 'xz' '-Xbcj' 'x86' '-b' '1M' '-Xdict-size' '1M') airootfs_image_tool_options=('-comp' 'xz' '-Xbcj' 'x86' '-b' '1M' '-Xdict-size' '1M')
file_permissions=( file_permissions=(
["/root"]="0:0:750"
["/etc/shadow"]="0:0:400"
["/etc/gshadow"]="0:0:400" ["/etc/gshadow"]="0:0:400"
["/etc/shadow"]="0:0:400"
["/etc/skel/.ssh"]="0:0:700" ["/etc/skel/.ssh"]="0:0:700"
["/etc/skel/.ssh/authorized_keys"]="0:0:600" ["/etc/skel/.ssh/authorized_keys"]="0:0:600"
["/etc/skel/.ssh/id_rsa"]="0:0:600" ["/etc/skel/.ssh/id_rsa"]="0:0:600"
["/root"]="0:0:750"
["/root/.gnupg"]="0:0:700"
) )

View file

@ -0,0 +1,28 @@
SERIAL 0 115200
UI vesamenu.c32
MENU TITLE Arch Linux
MENU BACKGROUND splash.png
MENU WIDTH 78
MENU MARGIN 4
MENU ROWS 7
MENU VSHIFT 10
MENU TABMSGROW 14
MENU CMDLINEROW 14
MENU HELPMSGROW 16
MENU HELPMSGENDROW 29
# Refer to https://wiki.syslinux.org/wiki/index.php/Comboot/menu.c32
MENU COLOR border 30;44 #40ffffff #a0000000 std
MENU COLOR title 1;36;44 #9033ccff #a0000000 std
MENU COLOR sel 7;37;40 #e0ffffff #20ffffff all
MENU COLOR unsel 37;44 #50ffffff #a0000000 std
MENU COLOR help 37;40 #c0ffffff #a0000000 std
MENU COLOR timeout_msg 37;40 #80ffffff #00000000 std
MENU COLOR timeout 1;37;40 #c0ffffff #00000000 std
MENU COLOR msg07 37;40 #90ffffff #a0000000 std
MENU COLOR tabmsg 31;40 #30ffffff #00000000 std
MENU CLEAR
MENU IMMEDIATE

View file

@ -0,0 +1,32 @@
LABEL arch64_nbd
TEXT HELP
Boot %ARCHISO_LABEL% using NBD.
* HW diagnostics, file-based backups, data recovery, etc
ENDTEXT
MENU LABEL %ARCHISO_LABEL% (NBD)
LINUX ::/%INSTALL_DIR%/boot/x86_64/vmlinuz-linux
INITRD ::/%INSTALL_DIR%/boot/intel-ucode.img,::/%INSTALL_DIR%/boot/amd-ucode.img,::/%INSTALL_DIR%/boot/x86_64/initramfs-linux.img
APPEND archisobasedir=%INSTALL_DIR% archisodevice=UUID=%ARCHISO_UUID% archiso_nbd_srv=${pxeserver} cms_verify=y
SYSAPPEND 3
LABEL arch64_nfs
TEXT HELP
Boot %ARCHISO_LABEL% using NFS.
* HW diagnostics, file-based backups, data recovery, etc
ENDTEXT
MENU LABEL %ARCHISO_LABEL% (NFS)
LINUX ::/%INSTALL_DIR%/boot/x86_64/vmlinuz-linux
INITRD ::/%INSTALL_DIR%/boot/intel-ucode.img,::/%INSTALL_DIR%/boot/amd-ucode.img,::/%INSTALL_DIR%/boot/x86_64/initramfs-linux.img
APPEND archisobasedir=%INSTALL_DIR% archiso_nfs_srv=${pxeserver}:/run/archiso/bootmnt cms_verify=y
SYSAPPEND 3
LABEL arch64_http
TEXT HELP
Boot %ARCHISO_LABEL% using HTTP.
* HW diagnostics, file-based backups, data recovery, etc
ENDTEXT
MENU LABEL %ARCHISO_LABEL% (HTTP)
LINUX ::/%INSTALL_DIR%/boot/x86_64/vmlinuz-linux
INITRD ::/%INSTALL_DIR%/boot/intel-ucode.img,::/%INSTALL_DIR%/boot/amd-ucode.img,::/%INSTALL_DIR%/boot/x86_64/initramfs-linux.img
APPEND archisobasedir=%INSTALL_DIR% archiso_http_srv=http://${pxeserver}/ cms_verify=y
SYSAPPEND 3

View file

@ -0,0 +1,5 @@
INCLUDE archiso_head.cfg
INCLUDE archiso_pxe-linux.cfg
INCLUDE archiso_tail.cfg

View file

@ -0,0 +1,9 @@
LABEL arch64
TEXT HELP
Boot %ARCHISO_LABEL% on BIOS.
* HW diagnostics, file-based backups, data recovery, etc
ENDTEXT
MENU LABEL %ARCHISO_LABEL% (BIOS)
LINUX /%INSTALL_DIR%/boot/x86_64/vmlinuz-linux
INITRD /%INSTALL_DIR%/boot/intel-ucode.img,/%INSTALL_DIR%/boot/amd-ucode.img,/%INSTALL_DIR%/boot/x86_64/initramfs-linux.img
APPEND archisobasedir=%INSTALL_DIR% archisodevice=UUID=%ARCHISO_UUID%

View file

@ -0,0 +1,8 @@
INCLUDE archiso_head.cfg
DEFAULT arch64
TIMEOUT 150
INCLUDE archiso_sys-linux.cfg
INCLUDE archiso_tail.cfg

View file

@ -0,0 +1,35 @@
LABEL existing
TEXT HELP
Boot an existing operating system.
Press TAB to edit the disk and partition number to boot.
ENDTEXT
MENU LABEL Boot existing OS
COM32 chain.c32
APPEND hd0 0
# https://www.memtest.org/
LABEL memtest
MENU LABEL Run Memtest86+ (RAM test)
LINUX /boot/memtest86+/memtest
# https://wiki.syslinux.org/wiki/index.php/Hdt_(Hardware_Detection_Tool)
LABEL hdt
MENU LABEL Hardware Information (HDT)
COM32 hdt.c32
APPEND modules_alias=hdt/modalias.gz pciids=hdt/pciids.gz
LABEL reboot
TEXT HELP
Reboot computer.
The computer's firmware must support APM.
ENDTEXT
MENU LABEL Reboot
COM32 reboot.c32
LABEL poweroff
TEXT HELP
Power off computer.
The computer's firmware must support APM.
ENDTEXT
MENU LABEL Power Off
COM32 poweroff.c32

View file

@ -1,21 +0,0 @@
LABEL wk_linux
TEXT HELP
A live Linux environment
* HW diagnostics, file-based backups, data recovery, etc
ENDTEXT
MENU LABEL Linux
LINUX /%INSTALL_DIR%/boot/%ARCH%/vmlinuz-linux
INITRD /%INSTALL_DIR%/boot/intel-ucode.img,/%INSTALL_DIR%/boot/amd-ucode.img,/%INSTALL_DIR%/boot/x86_64/initramfs-linux.img
APPEND archisobasedir=%INSTALL_DIR% archisolabel=%ARCHISO_LABEL% copytoram loglevel=3
LABEL wk_linux_cli
TEXT HELP
A live Linux environment (CLI)
* HW diagnostics, file-based backups, data recovery, etc
ENDTEXT
MENU LABEL Linux (CLI)
LINUX /%INSTALL_DIR%/boot/x86_64/vmlinuz-linux
INITRD /%INSTALL_DIR%/boot/intel-ucode.img,/%INSTALL_DIR%/boot/amd-ucode.img,/%INSTALL_DIR%/boot/x86_64/initramfs-linux.img
APPEND archisobasedir=%INSTALL_DIR% archisolabel=%ARCHISO_LABEL% copytoram nox
SYSAPPEND 3

Some files were not shown because too many files have changed in this diff Show more