Compare commits

...

287 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
3334638a2c
Suppress warnings when using tail in tmux 2023-05-27 19:48:18 -07:00
cb012423bb
Refactor hardware diagnostics to use new TUI 2023-05-27 19:47:26 -07:00
4c76e59238
Add get_window_size() 2023-05-27 19:22:24 -07:00
ba69773fba
Clear history when clearing a pane
Helpful if the pane is resized to prevent cleared lines from returning.
2023-05-27 19:08:48 -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
9678f143c7
Misc 2023-05-14 21:59:34 -07:00
7aafcd7c01
Update add_title_pane() to match set_title() 2023-04-09 16:33:11 -07:00
b834be9f00
Close all panes atexit 2023-04-09 16:32:13 -07:00
ba3bf480f7
BREAKING Add wk/ui/tui.py 2023-04-09 15:59:34 -07:00
f9bcd534d4
Move ansi code into wk/ui 2023-04-08 19:20:22 -07:00
d302be2d7c
Use prompt_toolkit for CLI input 2023-04-08 16:26:51 -07:00
13fc64e6ab
Remove unneeded wk.ui calls 2023-04-08 14:16:41 -07:00
44ddb3c258
Replace clear_screen() function
This is now part of wk.ansi and is fully cross-compatible.
2023-04-08 12:45:45 -07:00
95d7159414
Move ANSI color escape sections to their own file 2023-04-02 20:46:54 -07:00
03a143488c
Adjust ui imports and calls 2023-04-02 20:12:18 -07:00
6efc970374
Move tmux sections into wk.ui 2023-04-01 22:36:59 -07:00
e3ebc2d1b8
Merge branch 'dev' into ui-split 2023-04-01 22:28:32 -07:00
96136e8e46
Overwrite .new files if needed
Addresses issue #213
2023-04-01 22:25:11 -07:00
ddb9c4041b
Suppress output when configuring browsers
Addresses #214
2023-04-01 22:20:33 -07:00
9228137187
Fix check_4k_alignment()
Addresses #215
2023-04-01 22:16:38 -07:00
89fd647792
Split wk.std into debug, std, and ui sections 2023-04-01 22:14:03 -07:00
9f66b151af
Replace more pylint sections with ruff 2023-03-26 23:52:52 -07:00
08294caffc
Drop photorec-sort script 2023-03-26 23:09:53 -07:00
7c66eb5e99
Allow password sign-in for MS accounts
Addresses issue #210
2023-03-26 23:04:45 -07:00
d5bc74d21b
Show partition info in 4K alignment check 2023-03-25 20:24:17 -07:00
4f6a07c449
Set Linux as the default boot option 2023-03-19 20:49:32 -07:00
606e657591
Switch to Memtest86+ under UEFI
Memtest86 (by Passmark) still available as an alternate option.
2023-03-19 20:47:36 -07:00
2717ad1a88
Drop pylint reference 2023-03-19 20:21:12 -07:00
10400ec2c3
Add pre-commit hook 2023-03-19 20:13:50 -07:00
bbdc10e0f2
Merge branch 'dev' into ruff-test 2023-03-19 19:45:26 -07:00
16ee95b1d9
Adjust Python module path handling
Simplifies top-level scripts and removes linting exceptions.
2023-03-19 19:42:54 -07:00
e00920f24a
Switch to ruff linting 2023-03-19 18:47:46 -07:00
dad1d3e7f9
Switch to Github release for wimboot 2023-03-18 19:09:02 -07:00
a124b0002b
Remove unused proc var 2023-02-18 14:58:44 -08:00
c888adc1e0
Revert "Fix type causing wrong type to be returned"
This reverts commit 4cc6a5ecd1.
2023-02-16 16:48:01 -08:00
4cc6a5ecd1
Fix type causing wrong type to be returned 2023-02-15 17:36:18 -08:00
ee1f41a1bb
Add VCRedist link 2023-02-07 16:09:33 -08:00
ba1191e425
Add desktop file for OSCViewer 2023-02-04 17:29:38 -08:00
fd9d506ce5
Adjust package lists for OpenSuperClone 2023-02-04 17:29:09 -08:00
9d76502421
Parse KVRT report to create human readable log 2023-02-04 17:22:00 -08:00
f371a4cb83
Limit KVRT scan to Users and ProgramData folders 2023-02-04 17:20:06 -08:00
4f89656edc
Merge branch 'opensuperclone' into dev 2023-01-31 22:41:51 -08:00
4a3bb2cc3f
Revert "Switch to LTS kernel"
This reverts commit f21b95b090.
2023-01-31 22:36:42 -08:00
302ad58814
Remove /utf8 arg from FastCopy launchers
I seems that the FastCopy docs are outdated?
2023-01-02 13:22:07 -08:00
6e4bc11a7e
(Re)add mesa-utils
Needed by the hw-info script
2022-12-23 21:02:13 -08:00
c5b3df3e03
Revert "Update Memtest86 section"
This reverts commit 4a7762f751.
2022-12-23 20:50:12 -08:00
5e95cee2f3
Support kit dir to be at the root of a volume 2022-12-23 19:09:16 -08:00
c235926930
Remove Linux Minimal build
- Merge archiso profiles
- Merge package lists
- Merge full/minimal sections in build_linux
- Remove minimal boot entries
- Remove minimal from build-ufd config and scripts
- Update Linux README.md

Addresses #207
2022-12-17 23:07:35 -08:00
4a7762f751
Update Memtest86 section 2022-12-17 21:44:38 -08:00
3e7d074ca3
Update st package 2022-12-17 20:37:30 -08:00
0e2d51926c
Fix Auto Repairs menu
Reboot entries weren't selected in the default profile due to having
ASCII color escapes.
2022-12-15 20:41:05 -08:00
0ce8169e6b
Skip the main menu when resuming to Auto Repairs
Fixes issue #205
2022-12-15 20:03:05 -08:00
f21b95b090
Switch to LTS kernel 2022-12-15 17:05:29 -08:00
b136462c75
Add dependencies for opensuperclone 2022-12-15 17:04:52 -08:00
137c3ec5e5
Add label for test mode in hardware diagnostics 2022-12-11 21:44:20 -08:00
1dfc7a0243
Add missing build dependencies 2022-12-11 20:56:24 -08:00
786660a625
Add option to install packages in pacinit 2022-12-11 19:08:06 -08:00
a96eacab06
Replace compton (again) 2022-12-03 22:04:26 -08:00
89de2a7679
Avoid crash for devices with bad volume names 2022-11-16 22:41:16 -08:00
2d353cf4e6
Add OpenSuperClone to Linux 2022-11-15 03:35:43 -08:00
76e7994aaa
Switch to mprime-bin
The compiled version is a couple versions out of date?
2022-11-15 03:34:48 -08:00
902ccc7989
Use upstream, precompiled rEFInd binaries
- Needed to allow booting under some MacBook models
2022-11-15 03:33:43 -08:00
e8f86196c8
Refactor Linux dependencies
- Support comments in the package list
- Remove extraneous dependencies
2022-11-15 03:32:30 -08:00
93e4a2e50c
Replace using picom settings 2022-11-12 23:54:11 -08:00
e583929498
Reduce time needed for get_disks() 2022-11-12 23:21:23 -08:00
3922ed08a8
Fix pylint warning W3101 2022-11-12 22:32:25 -08:00
71bbf6ed57
Replace testdisk-wip with testdisk
Needed for dependency issues
2022-11-12 22:20:25 -08:00
f85a03a712
Fix .zlogin in full build 2022-10-30 21:25:02 -07:00
d306a4c4e6
Improve RAM model reporting 2022-10-29 12:06:37 -07:00
b36847fa5c
Skip elevate for Windows Updates 2022-10-23 17:47:07 -07:00
94c1f282f5
Fix reboot entries in Auto Repairs presets 2022-10-23 17:09:08 -07:00
d1ff7a391b
Fix typo 2022-10-23 16:49:42 -07:00
b9202c0ba2
Fix Auto Repairs presets 2022-10-23 16:48:03 -07:00
166ad3198c
Add badblocks support for drives over 16TB 2022-10-23 13:31:37 -07:00
2706d1a9a5
Show failed SMART attributes during disk tests 2022-10-23 13:18:43 -07:00
5c6c123daa
Fix SDIO launcher when SDIO_SERVER not defined 2022-10-22 20:35:05 -07:00
1f984f5b77
Add journalctl-datarec alias 2022-10-22 20:20:17 -07:00
25b64d6852
Limit badblocks scan to minimum in test_mode 2022-10-22 19:41:27 -07:00
a324e71ba9
Open Snappy Driver Instller Origin in AutoSetup
Address issue #202
2022-10-22 18:49:58 -07:00
31de1a20d9
Configure browsers in Additional Users preset.
Addresses issue #202
2022-10-22 18:45:04 -07:00
3d90adf4b2
Add option to run SDIO from network share 2022-10-22 18:24:48 -07:00
eab7a03f70
Update launchers.py 2022-10-19 23:34:51 -07:00
df6d3d4a24
Fix Auto Setup selections under Windows 11 2022-10-19 23:31:20 -07:00
8c03530ec5
Update details before confirming selection 2022-10-19 23:21:23 -07:00
abaed514fb
Fix wk-debug 2022-10-09 00:52:29 -07:00
3d799c2b62
Fix test mode in I/O benchmark test 2022-10-08 22:05:54 -07:00
49ed3c2919
Drop test station name detection via DNS 2022-10-08 20:03:40 -07:00
7dd35e3572
Replace egrep with grep -E 2022-10-08 19:38:45 -07:00
7714b3436f
Track initial and current SMART attributes
Addresses issue #194
2022-10-08 19:26:20 -07:00
4465caa9fd
Skip empty devices 2022-10-08 18:45:31 -07:00
a6a774beae
Update Disk details before checking labels 2022-10-08 18:44:56 -07:00
bb43c7447d
Add wk_debug.py 2022-10-08 18:28:17 -07:00
2c9e56e830
Improve device size reporting in the description
i.e. support 512GB SSDs, 1.5TB HDDs, etc
Addresses issue #199
2022-10-08 16:33:50 -07:00
fc8f81b66d
Open ddrescueview only once per BlockPair 2022-10-08 15:41:54 -07:00
6880a353cc
Set known_attributes when intializing Disk()
This new design uses copy.deepcopy() to avoid erroneous thresholds being
applied to drives during diags.  This also reduces the number of lookups
to one per Disk.
2022-10-08 14:15:32 -07:00
c08ad2b1fb
Avoid crash when saving debug info 2022-10-08 14:00:03 -07:00
591fb8e138
Skip installing Open Shell under Windows 11 2022-10-01 19:15:15 -07:00
a9581c9152
Update OS version sections to support Windows 11 2022-10-01 18:43:44 -07:00
dc2c9955e6
Bump Windows tool versions 2022-09-28 09:32:52 -07:00
5a61ea9abf
Bump psutil version 2022-09-28 09:27:40 -07:00
926b32b574
Clean badblocks results to remove backspaces 2022-09-28 00:33:15 -07:00
5aa49fe5e5
Stomp bugs and typos 2022-09-28 00:33:15 -07:00
d51bf9fe63
Add presets to AutoRepairs 2022-09-28 00:33:15 -07:00
3602665438
Refactor auto_setup.py
Combined the two STATIC VARIABLES sections
2022-09-28 00:33:15 -07:00
e55cc41e2b
Update the AutoRepairs scheduled task every run
This is useful if the kit is being run from a different port and the
letter changed between runs.
2022-09-28 00:33:15 -07:00
2df0bb504f
Reorganize kit layout and update launchers 2022-09-28 00:33:15 -07:00
82e0de422b
Add export_bitlocker.py script 2022-09-28 00:33:15 -07:00
cafdf42a6d
Use same log file for full AutoRepairs session 2022-09-28 00:33:15 -07:00
3b89f1eabc
Support download referer headers and redirects 2022-09-28 00:33:15 -07:00
9f52daeec3
Reduce AutoRepair countdown
Addresses issue #197
2022-09-28 00:33:15 -07:00
dd59f94f85
Replace IOBit with UninstallView 2022-09-28 00:33:15 -07:00
2b42bf9a8c
Update sources 2022-09-28 00:33:14 -07:00
a86649cb29
Update main settings 2022-09-28 00:33:14 -07:00
f008546565
Avoid crashing if a device disconnects mid-diags 2022-09-24 19:58:41 -07:00
21da84e5e2
Update Archiso profile to match upstream changes 2022-09-23 18:25:57 -07:00
a2e1c1fad3
Remove deprecated egrep calls 2022-09-23 18:19:41 -07:00
a97a71a24a
Fix Windows 11 version reporting 2022-09-04 18:10:54 -07:00
5f69e23887
Address Pylint error W1518 2022-09-04 17:44:58 -07:00
d36589751e
Fix mount_volumes() under Linux 2022-09-04 16:57:55 -07:00
5e1cc683fe
Disable Edge first run screen
Addresses issue #193
2022-07-10 15:55:25 -07:00
72640686da
Disable "Let's make Windows even better" screens
Addresses issue #191
2022-07-10 15:48:57 -07:00
df85d3049e
Disable Windows search highlights
Addresses issue #190
2022-07-10 15:42:53 -07:00
573 changed files with 10396 additions and 9659 deletions

6
.pre-commit-config.yaml Normal file
View file

@ -0,0 +1,6 @@
repos:
- repo: https://github.com/charliermarsh/ruff-pre-commit
# Ruff version.
rev: 'v0.0.257'
hooks:
- id: ruff

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:

View file

@ -242,7 +242,7 @@ if defined L_NCMD (
rem use Powershell's window instead of %CON%
echo UAC.ShellExecute "%POWERSHELL%", "%ps_args% -File "%script%"", "", "runas", 3 >> "%bin%\tmp\Elevate.vbs"
) 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

View file

@ -1,8 +1,2 @@
# WizardKit: Scripts #
## pylint ##
These scripts use two spaces per indent instead of the default four. As such you will need to update your pylintrc file or run like this:
`pylint --indent-after-paren=2 --indent-string=' ' wk`

View file

@ -1,33 +1,25 @@
"""WizardKit: Auto Repair Tool"""
# vim: sts=2 sw=2 ts=2
import os
import sys
from typing import Any
os.chdir(os.path.dirname(os.path.realpath(__file__)))
sys.path.append(os.getcwd())
import wk # pylint: disable=wrong-import-position
import wk
# Classes
REBOOT_STR = wk.ui.ansi.color_string('Reboot', 'YELLOW')
class MenuEntry():
# pylint: disable=too-few-public-methods
"""Simple class to allow cleaner code below."""
def __init__(self, name, function=None, **kwargs):
self.name = name
# Color reboot entries
if name == 'Reboot':
self.name = wk.std.color_string(
['Reboot', ' ', '(Forced)'],
['YELLOW', None, 'ORANGE'],
sep='',
)
# Set details
self.details = {
def __init__(
self,
name: str,
function: str | None = None,
selected: bool = True,
**kwargs):
self.name: str = name
self.details: dict[str, Any] = {
'Function': function,
'Selected': True,
'Selected': selected,
**kwargs,
}
@ -49,7 +41,7 @@ BASE_MENUS = {
'Windows Repairs': (
MenuEntry('Disable Windows Updates', 'auto_windows_updates_disable'),
MenuEntry('Reset Windows Updates', 'auto_windows_updates_reset'),
MenuEntry('Reboot', 'auto_reboot'),
MenuEntry(REBOOT_STR, 'auto_reboot'),
MenuEntry('CHKDSK', 'auto_chkdsk'),
MenuEntry('DISM RestoreHealth', 'auto_dism'),
MenuEntry('SFC Scan', 'auto_sfc'),
@ -65,35 +57,44 @@ BASE_MENUS = {
MenuEntry('KVRT', 'auto_kvrt'),
MenuEntry('Windows Defender', 'auto_microsoft_defender'),
MenuEntry('Remove Custom Power Plan', 'auto_remove_power_plan'),
MenuEntry('Reboot', 'auto_reboot'),
MenuEntry(REBOOT_STR, 'auto_reboot'),
),
'Manual Steps': (
MenuEntry('AdwCleaner', 'auto_adwcleaner'),
MenuEntry('IO Bit Uninstaller', 'auto_iobit_uninstaller'),
MenuEntry('Bulk Crap Uninstaller', 'auto_bcuninstaller'),
MenuEntry('Enable Windows Updates', 'auto_windows_updates_enable'),
),
},
'Options': (
MenuEntry('Kill Explorer'),
MenuEntry('Kill Explorer', selected=False),
MenuEntry('Run AVRemover (once)'),
MenuEntry('Run RKill'),
MenuEntry('Run TDSSKiller (once)'),
MenuEntry('Sync Clock'),
MenuEntry('Use Autologon'),
MenuEntry('Use Autologon', selected=False),
),
'Actions': (
MenuEntry('Load Preset'),
MenuEntry('Options'),
MenuEntry('Start', Separator=True),
MenuEntry('Quit'),
),
}
PRESETS = {
'Default': { # Will be expanded at runtime using BASE_MENUS
'Options': (
'Run RKill',
'Sync Clock',
),
},
'Custom': {}, # Will remain empty at runtime
}
if __name__ == '__main__':
try:
wk.repairs.win.run_auto_repairs(BASE_MENUS)
wk.repairs.win.run_auto_repairs(BASE_MENUS, PRESETS)
except KeyboardInterrupt:
wk.std.abort()
wk.ui.cli.abort()
except SystemExit:
raise
except: #pylint: disable=bare-except
wk.std.major_exception()
except: # noqa: E722
wk.ui.cli.major_exception()

View file

@ -1,20 +1,94 @@
"""WizardKit: Auto System Setup Tool"""
# vim: sts=2 sw=2 ts=2
import os
import sys
from typing import Any
os.chdir(os.path.dirname(os.path.realpath(__file__)))
sys.path.append(os.getcwd())
import wk # pylint: disable=wrong-import-position
import wk
# Classes
class MenuEntry():
"""Simple class to allow cleaner code below."""
def __init__(
self,
name: str,
function: str | None = None,
selected: bool = True,
**kwargs):
self.name: str = name
self.details: dict[str, Any] = {
'Function': function,
'Selected': selected,
**kwargs,
}
# STATIC VARIABLES
BASE_MENUS = {
'Groups': {
'Backup Settings': (
MenuEntry('Backup Browsers', 'auto_backup_browser_profiles'),
MenuEntry('Backup Power Plans', 'auto_backup_power_plans'),
MenuEntry('Reset Power Plans', 'auto_reset_power_plans'),
MenuEntry('Set Custom Power Plan', 'auto_set_custom_power_plan'),
),
'Install Software': (
MenuEntry('Winget', 'auto_install_winget'),
MenuEntry('Firefox', 'auto_install_firefox'),
MenuEntry('LibreOffice', 'auto_install_libreoffice', selected=False),
MenuEntry('Open Shell', 'auto_install_open_shell'),
MenuEntry('Software Bundle', 'auto_install_software_bundle'),
MenuEntry('Software Upgrades', 'auto_install_software_upgrades'),
MenuEntry('Visual C++ Runtimes', 'auto_install_vcredists'),
),
'Configure System': (
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 RegBack', 'auto_enable_regback'),
MenuEntry('Enable System Restore', 'auto_system_restore_enable'),
MenuEntry('Set System Restore Size', 'auto_system_restore_set_size'),
MenuEntry('Enable Windows Updates', 'auto_windows_updates_enable'),
MenuEntry('Windows Activation', 'auto_activate_windows'),
MenuEntry('Windows Explorer', 'auto_config_explorer'),
MenuEntry(r'Windows\Temp Fix', 'auto_windows_temp_fix'),
MenuEntry('Configure Browsers', 'auto_config_browsers'),
MenuEntry('Create System Restore', 'auto_system_restore_create'),
),
'System Information': (
MenuEntry('AIDA64 Report', 'auto_export_aida64_report'),
MenuEntry('Backup Registry', 'auto_backup_registry'),
),
'System Summary': (
MenuEntry('Operating System', 'auto_show_os_name'),
MenuEntry('Windows Activation', 'auto_show_os_activation'),
MenuEntry('Secure Boot', 'auto_show_secure_boot_status'),
MenuEntry('Installed RAM', 'auto_show_installed_ram'),
MenuEntry('Storage Status', 'auto_show_storage_status'),
MenuEntry('Virus Protection', 'auto_show_installed_antivirus'),
MenuEntry('Partitions 4K Aligned', 'auto_show_4k_alignment_check'),
),
'Run Programs': (
MenuEntry('Device Manager', 'auto_open_device_manager'),
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('Windows Activation', 'auto_open_windows_activation'),
MenuEntry('Windows Updates', 'auto_open_windows_updates'),
MenuEntry('XMPlay', 'auto_open_xmplay'),
),
},
'Actions': (
MenuEntry('Load Preset'),
MenuEntry('Start', Separator=True),
MenuEntry('Quit'),
),
}
PRESETS = {
'Default': {}, # Will be built at runtime using BASE_MENUS
'Additional User': {
'Configure System': (
'Chrome Notifications',
'Configure Browsers',
'Open Shell',
'uBlock Origin',
'Enable BSoD MiniDumps',
@ -27,6 +101,9 @@ PRESETS = {
'Install Software': (
'Firefox', # Needed to handle profile upgrade nonsense
),
'Run Programs': (
'Microsoft Store Updates',
),
'System Summary': (
'Operating System',
'Windows Activation',
@ -86,83 +163,13 @@ PRESETS = {
'Custom': {}, # Will remain empty at runtime
}
# Classes
class MenuEntry():
# pylint: disable=too-few-public-methods
"""Simple class to allow cleaner code below."""
def __init__(self, name, function=None, selected=True, **kwargs):
self.name = name
self.details = {
'Function': function,
'Selected': selected,
**kwargs,
}
# STATIC VARIABLES
BASE_MENUS = {
'Groups': {
'Backup Settings': (
MenuEntry('Backup Browsers', 'auto_backup_browser_profiles'),
MenuEntry('Backup Power Plans', 'auto_backup_power_plans'),
MenuEntry('Reset Power Plans', 'auto_reset_power_plans'),
MenuEntry('Set Custom Power Plan', 'auto_set_custom_power_plan'),
),
'Install Software': (
MenuEntry('Visual C++ Runtimes', 'auto_install_vcredists'),
MenuEntry('Firefox', 'auto_install_firefox'),
MenuEntry('LibreOffice', 'auto_install_libreoffice', selected=False),
MenuEntry('Open Shell', 'auto_install_open_shell'),
MenuEntry('Software Bundle', 'auto_install_software_bundle'),
),
'Configure System': (
MenuEntry('Configure Browsers', 'auto_config_browsers'),
MenuEntry('Open Shell', 'auto_config_open_shell'),
MenuEntry('Enable BSoD MiniDumps', 'auto_enable_bsod_minidumps'),
MenuEntry('Enable RegBack', 'auto_enable_regback'),
MenuEntry('Enable System Restore', 'auto_system_restore_enable'),
MenuEntry('Set System Restore Size', 'auto_system_restore_set_size'),
MenuEntry('Enable Windows Updates', 'auto_windows_updates_enable'),
MenuEntry('Windows Activation', 'auto_activate_windows'),
MenuEntry('Windows Explorer', 'auto_config_explorer'),
MenuEntry(r'Windows\Temp Fix', 'auto_windows_temp_fix'),
MenuEntry('Create System Restore', 'auto_system_restore_create'),
),
'System Information': (
MenuEntry('AIDA64 Report', 'auto_export_aida64_report'),
MenuEntry('Backup Registry', 'auto_backup_registry'),
),
'System Summary': (
MenuEntry('Operating System', 'auto_show_os_name'),
MenuEntry('Windows Activation', 'auto_show_os_activation'),
MenuEntry('Secure Boot', 'auto_show_secure_boot_status'),
MenuEntry('Installed RAM', 'auto_show_installed_ram'),
MenuEntry('Storage Status', 'auto_show_storage_status'),
MenuEntry('Virus Protection', 'auto_show_installed_antivirus'),
MenuEntry('Partitions 4K Aligned', 'auto_show_4k_alignment_check'),
),
'Run Programs': (
MenuEntry('Device Manager', 'auto_open_device_manager'),
MenuEntry('HWiNFO Sensors', 'auto_open_hwinfo_sensors'),
MenuEntry('Windows Activation', 'auto_open_windows_activation'),
MenuEntry('Windows Updates', 'auto_open_windows_updates'),
MenuEntry('XMPlay', 'auto_open_xmplay'),
),
},
'Actions': (
MenuEntry('Load Preset'),
MenuEntry('Start', Separator=True),
MenuEntry('Quit'),
),
}
if __name__ == '__main__':
try:
wk.setup.win.run_auto_setup(BASE_MENUS, PRESETS)
except KeyboardInterrupt:
wk.std.abort()
wk.ui.cli.abort()
except SystemExit:
raise
except: #pylint: disable=bare-except
wk.std.major_exception()
except: # noqa: E722
wk.ui.cli.major_exception()

View file

@ -10,5 +10,5 @@ if __name__ == '__main__':
wk.kit.ufd.build_ufd()
except SystemExit:
raise
except: #pylint: disable=bare-except
wk.std.major_exception()
except: # noqa: E722
wk.ui.cli.major_exception()

View file

@ -1,20 +1,15 @@
"""WizardKit: Build Kit (Windows)."""
# vim: sts=2 sw=2 ts=2
import os
import sys
os.chdir(os.path.dirname(os.path.realpath(__file__)))
sys.path.append(os.getcwd())
import wk # pylint: disable=wrong-import-position
import wk
if __name__ == '__main__':
try:
wk.kit.build.build_kit()
except KeyboardInterrupt:
wk.std.abort()
wk.ui.cli.abort()
except SystemExit:
raise
except: #pylint: disable=bare-except
wk.std.major_exception()
except: # noqa: E722
wk.ui.cli.major_exception()

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

@ -1,24 +1,14 @@
#!/usr/bin/env python3
"""WizardKit: ddrescue TUI"""
# pylint: disable=invalid-name
# vim: sts=2 sw=2 ts=2
from docopt import docopt
import wk
if __name__ == '__main__':
try:
docopt(wk.clone.ddrescue.DOCSTRING)
except SystemExit:
print('')
wk.std.pause('Press Enter to exit...')
raise
try:
wk.clone.ddrescue.main()
except SystemExit:
raise
except: #pylint: disable=bare-except
wk.std.major_exception()
except: # noqa: E722
wk.ui.cli.major_exception()

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,13 +5,18 @@ python.exe -i embedded_python_env.py
"""
# vim: sts=2 sw=2 ts=2
import os
import sys
import pickle
import wk
os.chdir(os.path.dirname(os.path.realpath(__file__)))
sys.path.append(os.getcwd())
import wk # pylint: disable=wrong-import-position
wk.std.print_colored(
# Functions
def load_state():
with open('debug/state.pickle', 'rb') as f:
return pickle.load(f)
# Main
wk.ui.cli.print_colored(
(wk.cfg.main.KIT_NAME_FULL, ': ', 'Debug Console'),
('GREEN', None, 'YELLOW'),
sep='',

View file

@ -0,0 +1,6 @@
"""WizardKit: Export Bitlocker Tool"""
# vim: sts=2 sw=2 ts=2
import wk
wk.os.win.export_bitlocker_info()

View file

@ -1,24 +1,14 @@
#!/usr/bin/env python3
"""WizardKit: Hardware Diagnostics"""
# pylint: disable=invalid-name
# vim: sts=2 sw=2 ts=2
from docopt import docopt
import wk
if __name__ == '__main__':
try:
docopt(wk.hw.diags.DOCSTRING)
except SystemExit:
print('')
wk.std.pause('Press Enter to exit...')
raise
try:
wk.hw.diags.main()
except SystemExit:
raise
except: #pylint: disable=bare-except
wk.std.major_exception()
except: # noqa: E722
wk.ui.cli.major_exception()

View file

@ -7,15 +7,15 @@ import platform
import wk
def main():
def main() -> None:
"""Show sensor data on screen."""
sensors = wk.hw.sensors.Sensors()
if platform.system() == 'Darwin':
wk.std.clear_screen()
wk.ui.cli.clear_screen()
while True:
print('\033[100A', end='')
sensors.update_sensor_data()
wk.std.print_report(sensors.generate_report('Current', 'Max'))
wk.ui.cli.print_report(sensors.generate_report('Current', 'Max'))
wk.std.sleep(1)
elif platform.system() == 'Linux':
proc = wk.exe.run_program(cmd=['mktemp'])
@ -42,5 +42,5 @@ if __name__ == '__main__':
pass
except SystemExit:
raise
except: #pylint: disable=bare-except
wk.std.major_exception()
except: # noqa: E722
wk.ui.cli.major_exception()

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'

99
scripts/launch_sdio.py Normal file
View file

@ -0,0 +1,99 @@
"""WizardKit: Launch Snappy Driver Installer Origin"""
# vim: sts=2 sw=2 ts=2
from subprocess import CompletedProcess
import wk
from wk.cfg.net import SDIO_SERVER
# STATIC VARIABLES
MOUNT_EXCEPTIONS = (
RuntimeError,
wk.exe.subprocess.CalledProcessError,
)
SDIO_LOCAL_PATH = wk.kit.tools.get_tool_path("SDIO", "SDIO")
SDIO_REMOTE_PATH = wk.io.get_path_obj(
(
fr'\\{SDIO_SERVER["Address"]}\{SDIO_SERVER["Share"]}\{SDIO_SERVER["Path"]}'
fr'\SDIO{"64" if wk.os.win.ARCH == "64" else ""}.exe'
),
resolve=False,
)
# Functions
def try_again() -> bool:
"""Ask to try again or quit."""
if wk.ui.cli.ask(' Try again?'):
return True
if not wk.ui.cli.ask(' Use local version?'):
wk.ui.cli.abort()
return False
def use_network_sdio() -> bool:
"""Try to mount SDIO server."""
use_network = False
def _mount_server() -> CompletedProcess:
print('Connecting to server... (Press CTRL+c to use local copy)')
return wk.net.mount_network_share(SDIO_SERVER, read_write=False)
# Bail early
if not SDIO_SERVER['Address']:
return use_network
# Main loop
while True:
try:
proc = _mount_server()
except KeyboardInterrupt:
break
except MOUNT_EXCEPTIONS as err:
wk.ui.cli.print_error(f' {err}')
if not try_again():
break
else:
if proc.returncode == 0:
# Network copy available
use_network = True
break
# Failed to mount
wk.ui.cli.print_error(' Failed to mount server')
if not try_again():
break
# Done
return use_network
if __name__ == '__main__':
wk.ui.cli.set_title(
f'{wk.cfg.main.KIT_NAME_FULL}: Snappy Driver Installer Origin Launcher',
)
log_dir = wk.log.format_log_path(tool=True).parent
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:
USE_NETWORK = use_network_sdio()
except KeyboardInterrupt:
wk.ui.cli.abort()
# Run SDIO
EXE_PATH = SDIO_LOCAL_PATH
if USE_NETWORK:
EXE_PATH = SDIO_REMOTE_PATH
print('Using network copy!')
else:
print('Using local copy!')
cmd = [EXE_PATH, '-log_dir', log_dir]
wk.exe.run_program(cmd, check=False, cwd=EXE_PATH.parent)

View file

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

View file

@ -8,34 +8,34 @@ import wk
# Functions
def main():
def main() -> None:
"""Mount all volumes and show results."""
wk.std.print_standard(f'{wk.cfg.main.KIT_NAME_FULL}: Volume mount tool')
wk.std.print_standard(' ')
wk.ui.cli.print_standard(f'{wk.cfg.main.KIT_NAME_FULL}: Volume mount tool')
wk.ui.cli.print_standard(' ')
# Mount volumes and get report
wk.std.print_standard('Mounting volumes...')
wk.ui.cli.print_standard('Mounting volumes...')
wk.os.linux.mount_volumes()
report = wk.os.linux.build_volume_report()
# Show results
wk.std.print_info('Results')
wk.std.print_report(report)
wk.ui.cli.print_info('Results')
wk.ui.cli.print_report(report)
# GUI mode
if 'gui' in sys.argv:
wk.std.pause('Press Enter to exit...')
wk.ui.cli.pause('Press Enter to exit...')
wk.exe.popen_program(['nohup', 'thunar', '/media'])
if __name__ == '__main__':
if wk.std.PLATFORM != 'Linux':
os_name = wk.std.PLATFORM.replace('Darwin', 'macOS')
wk.std.print_error(f'This script is not supported under {os_name}.')
wk.std.abort()
wk.ui.cli.print_error(f'This script is not supported under {os_name}.')
wk.ui.cli.abort()
try:
main()
except SystemExit:
raise
except: #pylint: disable=bare-except
wk.std.major_exception()
except: # noqa: E722
wk.ui.cli.major_exception()

View file

@ -1,15 +1,14 @@
#!/usr/bin/env python3
"""WizardKit: Mount Backup Shares"""
# pylint: disable=invalid-name
# vim: sts=2 sw=2 ts=2
import wk
# Functions
def main():
def main() -> None:
"""Attempt to mount backup shares and print report."""
wk.std.print_info('Mounting Backup Shares')
wk.ui.cli.print_info('Mounting Backup Shares')
report = wk.net.mount_backup_shares()
for line in report:
color = 'GREEN'
@ -18,7 +17,7 @@ def main():
color = 'RED'
elif 'Already' in line:
color = 'YELLOW'
print(wk.std.color_string(line, color))
print(wk.ansi.color_string(line, color))
if __name__ == '__main__':
@ -26,5 +25,5 @@ if __name__ == '__main__':
main()
except SystemExit:
raise
except: #pylint: disable=bare-except
wk.std.major_exception()
except: # noqa: E722
wk.ui.cli.major_exception()

View file

@ -6,6 +6,8 @@ import os
import re
import sys
import wk
# STATIC VARIABLES
SCANDIR = os.getcwd()
USAGE = '''Usage: {script} <search-terms>...
@ -13,12 +15,6 @@ USAGE = '''Usage: {script} <search-terms>...
This script will search all doc/docx files below the current directory for
the search-terms provided (case-insensitive).'''.format(script=__file__)
# Init
sys.path.append(os.path.dirname(os.path.realpath(__file__)))
from functions.network import *
init_global_vars()
REGEX_DOC_FILES = re.compile(r'\.docx?$', re.IGNORECASE)
@ -34,10 +30,10 @@ def scan_file(file_path, search):
match = False
try:
if entry.name.lower().endswith('.docx'):
result = run_program(['unzip', '-p', entry.path])
result = wk.exe.run_program(['unzip', '-p', entry.path])
else:
# Assuming .doc
result = run_program(['antiword', entry.path])
result = wk.exe.run_program(['antiword', entry.path])
out = result.stdout.decode()
match = re.search(search, out, re.IGNORECASE)
except Exception:
@ -50,13 +46,13 @@ def scan_file(file_path, search):
if __name__ == '__main__':
try:
# Prep
clear_screen()
wk.ui.cli.clear_screen()
terms = [re.sub(r'\s+', r'\s*', t) for t in sys.argv[1:]]
search = '({})'.format('|'.join(terms))
if len(sys.argv) == 1:
# Print usage
print_standard(USAGE)
wk.ui.cli.print_standard(USAGE)
else:
matches = []
for entry in scan_for_docs(SCANDIR):
@ -64,21 +60,20 @@ if __name__ == '__main__':
# Strip None values (i.e. non-matching entries)
matches = [m for m in matches if m]
if matches:
print_success('Found {} {}:'.format(
wk.ui.cli.print_success('Found {} {}:'.format(
len(matches),
'Matches' if len(matches) > 1 else 'Match'))
for match in matches:
print_standard(match)
wk.ui.cli.print_standard(match)
else:
print_error('No matches found.')
wk.ui.cli.print_error('No matches found.')
# Done
print_standard('\nDone.')
wk.ui.cli.print_standard('\nDone.')
#pause("Press Enter to exit...")
exit_script()
except SystemExit as sys_exit:
exit_script(sys_exit.code)
except:
major_exception()
except SystemExit:
raise
except: # noqa: E722
wk.ui.cli.major_exception()
# vim: sts=2 sw=2 ts=2

View file

@ -13,6 +13,6 @@ sudo sed -i -r "s/^SigLevel.*/SigLevel = Never/" /etc/pacman.conf
# Init Pacman keyring
sudo systemctl start pacman-init.service
# Refresh package databases
sudo pacman -Sy
# Refresh package databases and install packages (if provided)
sudo pacman -Sy "$@"

View file

@ -1,150 +0,0 @@
#!/bin/bash
#
## sort photorec results into something usefull
## Set paths
recup_dir="${1%/}"
[ -n "$recup_dir" ] || recup_dir="."
recup_dir="$(realpath "$recup_dir")"
out_dir="$recup_dir/Recovered"
bad_dir="$recup_dir/Corrupt"
## Test path before starting (using current dir if not specified)
for d in $recup_dir/recup*; do
### Source: http://stackoverflow.com/a/6364244
## Check if the glob gets expanded to existing files.
## If not, f here will be exactly the pattern above
## and the exists test will evaluate to false.
[ -e "$d" ] && echo "Found recup folder(s)" || {
echo "ERROR: No recup folders found"
echo "Usage: $0 recup_dir"
exit 1
}
## This is all we needed to know, so we can break after the first iteration
break
done
# Hard link files into folders by type
for d in $recup_dir/recup*; do
if [ -d "$d" ]; then
echo "Linking $d"
pushd $d >/dev/null
find -type f | while read k; do
file="$(basename "$k")"
src="$(realpath "$k")"
ext="$(echo "${file##*.}" | tr '[:upper:]' '[:lower:]')"
ext_dir="$out_dir/$ext"
if [ "${file##*.}" = "$file" ]; then
ext_dir="$out_dir/_MISC_"
elif [ "$ext" = "jpg" ] && [ "${file:0:1}" = "t" ]; then
ext_dir="$out_dir/jpg-thumbnail"
fi
#echo " $file -> $ext_dir"
[ -d "$ext_dir" ] || mkdir -p "$ext_dir"
ln "$src" "$ext_dir"
done
popd >/dev/null
else
echo "ERROR: '$d' not a directory"
fi
done
## Check the files output by photorec for corruption
pushd "$out_dir" >/dev/null
# Check archives with 7-Zip
#for d in 7z bz2 gz lzh lzo rar tar xz zip; do
# if [ -d "$d" ]; then
# echo "Checking $d files"
# pushd "$d" >/dev/null
# for f in *; do
# if ! 7z t "$f" >/dev/null 2>&1; then
# #echo " BAD: $f"
# [ -d "$bad_dir/$d" ] || mkdir -p "$bad_dir/$d"
# mv -n "$f" "$bad_dir/$d/$f"
# fi
# done
# popd >/dev/null
# fi
#done
# Check Audio/Video files with ffprobe
for d in avi flac flv m4a m4p m4v mkv mid mov mp2 mp3 mp4 mpg mpg2 ogg ts vob wav; do
if [ -d "$d" ]; then
echo "Checking $d files"
pushd "$d" >/dev/null
for f in *; do
if ! ffprobe "$f" >/dev/null 2>&1; then
#echo " BAD: $f"
[ -d "$bad_dir/$d" ] || mkdir -p "$bad_dir/$d"
mv -n "$f" "$bad_dir/$d/$f"
fi
done
popd >/dev/null
fi
done
# Check .doc files with antiword
if [ -d "doc" ]; then
echo "Checking doc files"
pushd "doc" >/dev/null
for f in *doc; do
if ! antiword "$f" >/dev/null 2>&1; then
#echo " BAD: $f"
[ -d "$bad_dir/doc" ] || mkdir -p "$bad_dir/doc"
mv -n "$f" "$bad_dir/doc/$f"
fi
done
popd >/dev/null
fi
# Check .docx files with 7z and grep
if [ -d "docx" ]; then
echo "Checking docx files"
pushd "docx" >/dev/null
for f in *docx; do
if ! 7z l "$f" | grep -q -s "word/document.xml"; then
#echo " BAD: $f"
[ -d "$bad_dir/docx" ] || mkdir -p "$bad_dir/docx"
mv -n "$f" "$bad_dir/docx/$f"
fi
done
popd >/dev/null
fi
# Sort pictures by date (only for common camera formats)
for d in jpg mrw orf raf raw rw2 tif x3f; do
if [ -d "$d" ]; then
echo "Sorting $d files by date"
pushd "$d" >/dev/null
for f in *; do
date_dir="$(date -d "$(stat -c %y "$f")" +"%F")"
[ -d "$date_dir" ] || mkdir "$date_dir"
mv -n "$f" "$date_dir/"
done
popd >/dev/null
fi
done
# Sort mov files by encoded date
if [ -d "mov" ]; then
echo "Sorting mov files by date"
pushd "mov" >/dev/null
for f in *mov; do
enc_date="$(mediainfo "$f" | grep -i "Encoded date" | head -1 | sed -r 's/.*: //')"
date_dir="$(date -d "$enc_date" +"%F")"
echo "$date_dir" | grep -E -q -s '^[0-9]{4}-[0-9]{2}-[0-9]{2}$' || date_dir="Unknown Date"
[ -d "$date_dir" ] || mkdir "$date_dir"
mv -n "$f" "$date_dir/"
done
popd >/dev/null
fi
## sort audio files by tags
## sort matroska files by metadata
## return to original dir
popd >/dev/null

19
scripts/pyproject.toml Normal file
View file

@ -0,0 +1,19 @@
[tool.ruff.per-file-ignores]
# Init files
"wk/__init__.py" = ["F401"]
"wk/cfg/__init__.py" = ["F401"]
"wk/clone/__init__.py" = ["F401"]
"wk/hw/__init__.py" = ["F401"]
"wk/kit/__init__.py" = ["F401"]
"wk/os/__init__.py" = ["F401"]
"wk/repairs/__init__.py" = ["F401"]
"wk/setup/__init__.py" = ["F401"]
"wk/ui/__init__.py" = ["F401"]
# Long lines
"wk/borrowed/acpi.py" = ["E501", "F841"]
"wk/cfg/ddrescue.py" = ["E501"]
"wk/cfg/hw.py" = ["E501"]
"wk/cfg/launchers.py" = ["E501"]
"wk/cfg/setup.py" = ["E501"]
"wk/cfg/sources.py" = ["E501"]

View file

@ -1,22 +1,21 @@
#!/usr/bin/env python3
"""WizardKit: Unmount Backup Shares"""
# pylint: disable=invalid-name
# vim: sts=2 sw=2 ts=2
import wk
# Functions
def main():
def main() -> None:
"""Attempt to mount backup shares and print report."""
wk.std.print_info('Unmounting Backup Shares')
wk.ui.cli.print_info('Unmounting Backup Shares')
report = wk.net.unmount_backup_shares()
for line in report:
color = 'GREEN'
line = f' {line}'
if 'Not mounted' in line:
color = 'YELLOW'
print(wk.std.color_string(line, color))
print(wk.ui.ansi.color_string(line, color))
if __name__ == '__main__':
@ -24,5 +23,5 @@ if __name__ == '__main__':
main()
except SystemExit:
raise
except: #pylint: disable=bare-except
wk.std.major_exception()
except: # noqa: E722
wk.ui.cli.major_exception()

View file

@ -25,24 +25,24 @@ if PLATFORM not in ('macOS', 'Linux'):
# Functions
def main():
def main() -> None:
"""Upload logs for review."""
lines = []
try_and_print = wk.std.TryAndPrint()
try_and_print = wk.ui.cli.TryAndPrint()
# Set log
wk.log.update_log_path(dest_name='Upload-Logs', timestamp=True)
# Instructions
wk.std.print_success(f'{wk.cfg.main.KIT_NAME_FULL}: Upload Logs')
wk.std.print_standard('')
wk.std.print_standard('Please state the reason for the review.')
wk.std.print_info(' End note with an empty line.')
wk.std.print_standard('')
wk.ui.cli.print_success(f'{wk.cfg.main.KIT_NAME_FULL}: Upload Logs')
wk.ui.cli.print_standard('')
wk.ui.cli.print_standard('Please state the reason for the review.')
wk.ui.cli.print_info(' End note with an empty line.')
wk.ui.cli.print_standard('')
# Get reason note
while True:
text = wk.std.input_text('> ')
text = wk.ui.cli.input_text('> ')
if not text:
lines.append('')
break
@ -60,17 +60,16 @@ def main():
raise SystemExit(1)
def upload_log_dir(reason='Testing'):
def upload_log_dir(reason='Testing') -> None:
"""Upload compressed log_dir to the crash server."""
server = wk.cfg.net.CRASH_SERVER
dest = pathlib.Path(f'~/{reason}_{NOW.strftime("%Y-%m-%dT%H%M%S%z")}.txz')
dest = dest.expanduser().resolve()
data = None
# Compress LOG_DIR (relative to parent dir)
os.chdir(LOG_DIR.parent)
cmd = ['tar', 'caf', dest.name, LOG_DIR.name]
proc = wk.exe.run_program(cmd, check=False)
wk.exe.run_program(cmd, check=False)
# Upload compressed data
url = f'{server["Url"]}/{dest.name}'

5
scripts/wk-debug Executable file
View file

@ -0,0 +1,5 @@
#!/bin/bash
#
## WizardKit: Debug Launcher
python3 -i /usr/local/bin/wk_debug.py

View file

@ -1,7 +1,7 @@
"""WizardKit: wk module init"""
# vim: sts=2 sw=2 ts=2
from sys import version_info as version
from sys import stderr, version_info
from . import cfg
from . import clone
@ -17,21 +17,22 @@ from . import os
from . import repairs
from . import setup
from . import std
from . import tmux
from . import ui
# Check env
if version < (3, 7):
if version_info < (3, 10):
# Unsupported
raise RuntimeError(
f'This package is unsupported on Python {version.major}.{version.minor}'
'This package is unsupported on Python '
f'{version_info.major}.{version_info.minor}'
)
# Init
try:
log.start()
except UserWarning as err:
std.print_warning(err)
print(err, file=stderr)
if __name__ == '__main__':

View file

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

View file

@ -1,17 +1,14 @@
"""WizardKit: Config - ddrescue"""
# pylint: disable=line-too-long
# vim: sts=2 sw=2 ts=2
from collections import OrderedDict
# Layout
TMUX_SIDE_WIDTH = 21
TMUX_LAYOUT = OrderedDict({
TMUX_LAYOUT = {
'Source': {'height': 2, 'Check': True},
'Started': {'width': TMUX_SIDE_WIDTH, 'Check': True},
'Progress': {'width': TMUX_SIDE_WIDTH, 'Check': True},
})
}
# ddrescue
AUTO_PASS_THRESHOLDS = {
@ -40,7 +37,7 @@ DDRESCUE_SETTINGS = {
'--retry-passes': {'Selected': True, 'Value': '0', },
'--reverse': {'Selected': False, },
'--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', },
'-vvvv': {'Selected': True, 'Hidden': True, },
},

View file

@ -1,11 +1,8 @@
"""WizardKit: Config - Hardware"""
# pylint: disable=line-too-long
# vim: sts=2 sw=2 ts=2
import re
from collections import OrderedDict
# STATIC VARIABLES
ATTRIBUTE_COLORS = (
@ -15,14 +12,21 @@ ATTRIBUTE_COLORS = (
('Maximum', 'PURPLE'),
)
# NOTE: Force 4K read block size for disks >= 3TB
BADBLOCKS_EXTRA_LARGE_DISK = 15 * 1024**4
BADBLOCKS_LARGE_DISK = 3 * 1024**4
BADBLOCKS_REGEX = re.compile(
r'^Pass completed, (\d+) bad blocks found. .(\d+)/(\d+)/(\d+) errors',
re.IGNORECASE,
)
BADBLOCKS_RESULTS_REGEX = re.compile(r'^(.*?)\x08.*\x08(.*)')
BADBLOCKS_SKIP_REGEX = re.compile(r'^(Checking|\[)', re.IGNORECASE)
CPU_CRITICAL_TEMP = 99
CPU_FAILURE_TEMP = 90
CPU_TEMPS = {
'Cooling Delta': 25,
'Cooling Low Cutoff': 50,
'Critical': 100,
'Idle Delta': 25,
'Idle High': 70,
}
CPU_TEST_MINUTES = 7
IO_GRAPH_WIDTH = 40
IO_ALT_TEST_SIZE_FACTOR = 0.01
@ -160,18 +164,6 @@ THRESH_HDD_AVG_LOW = 65 * 1024**2
THRESH_SSD_MIN = 90 * 1024**2
THRESH_SSD_AVG_HIGH = 135 * 1024**2
THRESH_SSD_AVG_LOW = 100 * 1024**2
TMUX_SIDE_WIDTH = 20
TMUX_LAYOUT = OrderedDict({
'Top': {'height': 2, 'Check': True},
'Started': {'width': TMUX_SIDE_WIDTH, 'Check': True},
'Progress': {'width': TMUX_SIDE_WIDTH, 'Check': True},
# Testing panes
'Temps': {'height': 1000, 'Check': False},
'Prime95': {'height': 11, 'Check': False},
'SMART': {'height': 4, 'Check': True},
'badblocks': {'height': 5, 'Check': True},
'I/O Benchmark': {'height': 1000, 'Check': False},
})
# VOLUME THRESHOLDS in percent
VOLUME_WARNING_THRESHOLD = 70
VOLUME_FAILURE_THRESHOLD = 85

View file

@ -1,45 +1,138 @@
"""WizardKit: Config - Launchers (Windows)"""
# pylint: disable=line-too-long
# vim: sts=2 sw=2 ts=2
LAUNCHERS = {
r'': { # Root Dir
'Auto Repairs': {
'0) Export BitLocker': {
'L_TYPE': 'PyScript',
'L_PATH': 'Scripts',
'L_ITEM': 'export_bitlocker.py',
'L_ELEV': 'True',
},
'1) Auto Repairs': {
'L_TYPE': 'PyScript',
'L_PATH': 'Scripts',
'L_ITEM': 'auto_repairs.py',
'L_ELEV': 'True',
},
'Auto Setup': {
'2) Store & Windows Updates': {
'L_TYPE': 'Executable',
'L_PATH': r'%SystemRoot%\System32',
'L_ITEM': 'control.exe',
'L_ARGS': 'update',
'Extra Code': ['explorer ms-windows-store:updates'],
},
'3) Snappy Driver Installer Origin': {
'L_TYPE': 'PyScript',
'L_PATH': 'Scripts',
'L_ITEM': 'launch_sdio.py',
'L_ELEV': 'True',
},
'4) Auto Setup': {
'L_TYPE': 'PyScript',
'L_PATH': 'Scripts',
'L_ITEM': 'auto_setup.py',
'L_ELEV': 'True',
},
},
r'Data Recovery': {
'PhotoRec (CLI)': {
r'Tools': {
'AIDA64': {
'L_TYPE': 'Executable',
'L_PATH': 'TestDisk',
'L_ITEM': 'photorec_win.exe',
'L_ELEV': 'True',
'L__CLI': 'True',
'L_PATH': 'AIDA64',
'L_ITEM': 'aida64.exe',
},
'PhotoRec': {
'Autoruns (with VirusTotal Scan)': {
'L_TYPE': 'Executable',
'L_PATH': 'TestDisk',
'L_ITEM': 'qphotorec_win.exe',
'L_PATH': 'Sysinternals',
'L_ITEM': 'Autoruns.exe',
'L_ARGS': '-e',
'Extra Code': [
r'reg add HKCU\Software\Sysinternals\AutoRuns /v checkvirustotal /t REG_DWORD /d 1 /f >nul',
r'reg add HKCU\Software\Sysinternals\AutoRuns /v EulaAccepted /t REG_DWORD /d 1 /f >nul',
r'reg add HKCU\Software\Sysinternals\AutoRuns /v shownomicrosoft /t REG_DWORD /d 1 /f >nul',
r'reg add HKCU\Software\Sysinternals\AutoRuns /v shownowindows /t REG_DWORD /d 1 /f >nul',
r'reg add HKCU\Software\Sysinternals\AutoRuns /v showonlyvirustotal /t REG_DWORD /d 1 /f >nul',
r'reg add HKCU\Software\Sysinternals\AutoRuns /v submitvirustotal /t REG_DWORD /d 0 /f >nul',
r'reg add HKCU\Software\Sysinternals\AutoRuns /v verifysignatures /t REG_DWORD /d 1 /f >nul',
r'reg add HKCU\Software\Sysinternals\AutoRuns\SigCheck /v EulaAccepted /t REG_DWORD /d 1 /f >nul',
r'reg add HKCU\Software\Sysinternals\AutoRuns\Streams /v EulaAccepted /t REG_DWORD /d 1 /f >nul',
r'reg add HKCU\Software\Sysinternals\AutoRuns\VirusTotal /v VirusTotalTermsAccepted /t REG_DWORD /d 1 /f >nul',
],
},
'BleachBit': {
'L_TYPE': 'Executable',
'L_PATH': 'BleachBit',
'L_ITEM': 'bleachbit.exe',
},
'BlueScreenView': {
'L_TYPE': 'Executable',
'L_PATH': 'BlueScreenView',
'L_ITEM': 'BlueScreenView.exe',
},
'BCUninstaller': {
'L_TYPE': 'Executable',
'L_PATH': 'BCUninstaller',
'L_ITEM': 'BCUninstaller.exe',
'L_ELEV': 'True',
},
'TestDisk': {
'ConEmu (as ADMIN)': {
'L_TYPE': 'Executable',
'L_PATH': 'TestDisk',
'L_ITEM': 'testdisk_win.exe',
'L_PATH': 'ConEmu',
'L_ITEM': 'ConEmu.exe',
'L_ELEV': 'True',
},
'ConEmu': {
'L_TYPE': 'Executable',
'L_PATH': 'ConEmu',
'L_ITEM': 'ConEmu.exe',
},
'Debug Console (Command Prompt)': {
'L_TYPE': 'Executable',
'L_PATH': 'ConEmu',
'L_ITEM': 'ConEmu.exe',
'L_ARGS': r'-Dir %bin%\Scripts',
'L_ELEV': 'True',
},
'Debug Console (Python)': {
'L_TYPE': 'Executable',
'L_PATH': 'ConEmu',
'L_ITEM': 'ConEmu.exe',
'L_ARGS': r'-Dir %bin%\Scripts -Run ..\Python\x%ARCH%\python.exe -i embedded_python_env.py',
'L_ELEV': 'True',
'Extra Code': [
'set ARCH=32',
'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': {
'L_TYPE': 'Executable',
'L_PATH': 'erunt',
'L_ITEM': 'ERUNT.EXE',
'L_ARGS': r'%client_dir%\Backups\Registry\%iso_date% sysreg curuser otherusers',
'L_ELEV': 'True',
'Extra Code': [
r'call "%bin%\Scripts\init_client_dir.cmd" /Logs',
],
},
'Everything': {
'L_TYPE': 'Executable',
'L_PATH': 'Everything',
'L_ITEM': 'Everything.exe',
'L_ARGS': '-nodb',
'L_ELEV': 'True',
'L__CLI': 'True',
},
},
r'Data Transfers': {
'FastCopy (as ADMIN)': {
'L_TYPE': 'Executable',
'L_PATH': 'FastCopy',
@ -48,7 +141,6 @@ LAUNCHERS = {
r' /logfile=%log_dir%\Tools\FastCopy.log'
r' /acl'
r' /cmd=noexist_only'
r' /utf8'
r' /skip_empty_dir'
r' /linkdest'
r' /exclude='
@ -97,7 +189,6 @@ LAUNCHERS = {
r' /logfile=%log_dir%\Tools\FastCopy.log'
r' /acl'
r' /cmd=noexist_only'
r' /utf8'
r' /skip_empty_dir'
r' /linkdest'
r' /exclude='
@ -137,51 +228,6 @@ LAUNCHERS = {
r'call "%bin%\Scripts\init_client_dir.cmd" /Logs /Transfer',
],
},
},
r'Diagnostics': {
'AIDA64': {
'L_TYPE': 'Executable',
'L_PATH': 'AIDA64',
'L_ITEM': 'aida64.exe',
},
'Autoruns (with VirusTotal Scan)': {
'L_TYPE': 'Executable',
'L_PATH': 'Sysinternals',
'L_ITEM': 'Autoruns.exe',
'L_ARGS': '-e',
'Extra Code': [
r'reg add HKCU\Software\Sysinternals\AutoRuns /v checkvirustotal /t REG_DWORD /d 1 /f >nul',
r'reg add HKCU\Software\Sysinternals\AutoRuns /v EulaAccepted /t REG_DWORD /d 1 /f >nul',
r'reg add HKCU\Software\Sysinternals\AutoRuns /v shownomicrosoft /t REG_DWORD /d 1 /f >nul',
r'reg add HKCU\Software\Sysinternals\AutoRuns /v shownowindows /t REG_DWORD /d 1 /f >nul',
r'reg add HKCU\Software\Sysinternals\AutoRuns /v showonlyvirustotal /t REG_DWORD /d 1 /f >nul',
r'reg add HKCU\Software\Sysinternals\AutoRuns /v submitvirustotal /t REG_DWORD /d 0 /f >nul',
r'reg add HKCU\Software\Sysinternals\AutoRuns /v verifysignatures /t REG_DWORD /d 1 /f >nul',
r'reg add HKCU\Software\Sysinternals\AutoRuns\SigCheck /v EulaAccepted /t REG_DWORD /d 1 /f >nul',
r'reg add HKCU\Software\Sysinternals\AutoRuns\Streams /v EulaAccepted /t REG_DWORD /d 1 /f >nul',
r'reg add HKCU\Software\Sysinternals\AutoRuns\VirusTotal /v VirusTotalTermsAccepted /t REG_DWORD /d 1 /f >nul',
],
},
'BleachBit': {
'L_TYPE': 'Executable',
'L_PATH': 'BleachBit',
'L_ITEM': 'bleachbit.exe',
},
'BlueScreenView': {
'L_TYPE': 'Executable',
'L_PATH': 'BlueScreenView',
'L_ITEM': 'BlueScreenView.exe',
},
'ERUNT': {
'L_TYPE': 'Executable',
'L_PATH': 'erunt',
'L_ITEM': 'ERUNT.EXE',
'L_ARGS': r'%client_dir%\Backups\Registry\%iso_date% sysreg curuser otherusers',
'L_ELEV': 'True',
'Extra Code': [
r'call "%bin%\Scripts\init_client_dir.cmd" /Logs',
],
},
'FurMark': {
'L_TYPE': 'Executable',
'L_PATH': 'FurMark',
@ -211,31 +257,6 @@ LAUNCHERS = {
r')',
],
},
'Snappy Driver Installer Origin': {
'L_TYPE': 'Executable',
'L_PATH': 'SDIO',
'L_ITEM': 'SDIO.exe',
},
},
r'Misc': {
'ConEmu (as ADMIN)': {
'L_TYPE': 'Executable',
'L_PATH': 'ConEmu',
'L_ITEM': 'ConEmu.exe',
'L_ELEV': 'True',
},
'ConEmu': {
'L_TYPE': 'Executable',
'L_PATH': 'ConEmu',
'L_ITEM': 'ConEmu.exe',
},
'Everything': {
'L_TYPE': 'Executable',
'L_PATH': 'Everything',
'L_ITEM': 'Everything.exe',
'L_ARGS': '-nodb',
'L_ELEV': 'True',
},
'Notepad++': {
'L_TYPE': 'Executable',
'L_PATH': 'notepadplusplus',
@ -246,17 +267,6 @@ LAUNCHERS = {
'L_PATH': 'PuTTY',
'L_ITEM': 'PUTTY.EXE',
},
'WizardKit Debug Console': {
'L_TYPE': 'Executable',
'L_PATH': 'ConEmu',
'L_ITEM': 'ConEmu.exe',
'L_ARGS': r'-Dir %bin%\Scripts -Run ..\Python\x%ARCH%\python.exe -i embedded_python_env.py',
'L_ELEV': 'True',
'Extra Code': [
'set ARCH=32',
'if /i "%PROCESSOR_ARCHITECTURE%" == "AMD64" set "ARCH=64"',
],
},
'WizTree': {
'L_TYPE': 'Executable',
'L_PATH': 'WizTree',
@ -270,13 +280,6 @@ LAUNCHERS = {
'L_ARGS': r'"%bin%\XMPlay\music.7z"',
},
},
r'Uninstallers': {
'IObit Uninstaller': {
'L_TYPE': 'Executable',
'L_PATH': 'IObitUninstallerPortable',
'L_ITEM': 'IObitUninstallerPortable.exe',
},
},
}

View file

@ -14,7 +14,7 @@ ENABLED_UPLOAD_DATA=False
ARCHIVE_PASSWORD='Abracadabra'
KIT_NAME_FULL='WizardKit'
KIT_NAME_SHORT='WK'
SUPPORT_MESSAGE='Please let 2Shirt know by opening an issue on GitHub'
SUPPORT_MESSAGE='Please let 2Shirt know by opening an issue on Gitea'
# Text Formatting
INDENT=4
@ -27,8 +27,8 @@ TECH_PASSWORD='Abracadabra'
# Time Zones
## See 'timedatectl list-timezones' for valid Linux values
## See 'tzutil /l' for valid Windows values
LINUX_TIME_ZONE='America/Denver'
WINDOWS_TIME_ZONE='Mountain Standard Time'
LINUX_TIME_ZONE='America/Los_Angeles'
WINDOWS_TIME_ZONE='Pacific Standard Time'
if __name__ == '__main__':

View file

@ -28,6 +28,13 @@ CRASH_SERVER = {
#'Pass': '',
#'Headers': {'X-Requested-With': 'XMLHttpRequest'},
}
SDIO_SERVER = {
'Address': '',
'Share': '',
'Path': '',
'RO-User': '',
'RO-Pass': '',
}
if __name__ == '__main__':

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

@ -3,7 +3,7 @@
from wk.cfg.main import KIT_NAME_FULL
AUTO_REPAIR_DELAY_IN_SECONDS = 30
AUTO_REPAIR_DELAY_IN_SECONDS = 3
AUTO_REPAIR_KEY = fr'Software\{KIT_NAME_FULL}\Auto Repairs'
BLEACH_BIT_CLEANERS = (
# Applications

View file

@ -1,5 +1,4 @@
"""WizardKit: Config - Setup"""
# pylint: disable=line-too-long
# vim: sts=2 sw=2 ts=2
@ -10,6 +9,11 @@ BROWSER_PATHS = {
'Microsoft Edge': 'Microsoft/Edge/Application/msedge.exe',
'Opera': 'Opera/launcher.exe',
}
DISABLED_ENTRIES_WINDOWS_11 = {
# Group Name: Option Name
'Install Software': 'Open Shell',
'Configure System': 'Open Shell',
}
LIBREOFFICE_XCU_DATA = '''<?xml version="1.0" encoding="UTF-8"?>
<oor:items xmlns:oor="http://openoffice.org/2001/registry" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<item oor:path="/org.openoffice.Setup/Office/Factories/org.openoffice.Setup:Factory['com.sun.star.presentation.PresentationDocument']"><prop oor:name="ooSetupFactoryDefaultFilter" oor:op="fuse"><value>Impress MS PowerPoint 2007 XML</value></prop></item>
@ -25,9 +29,20 @@ REG_CHROME_UBLOCK_ORIGIN = {
)
},
}
REG_WINDOWS_EXPLORER = {
# pylint: disable=line-too-long
REG_WINDOWS_BSOD_MINIDUMPS = {
'HKLM': {
# Enable small memory dumps
r'SYSTEM\CurrentControlSet\Control\CrashControl': (
('CrashDumpEnabled', 3, 'DWORD'),
)
}
}
REG_WINDOWS_EXPLORER = {
'HKLM': {
# Allow password sign-in for MS accounts
r'Software\Microsoft\Windows NT\CurrentVersion\PasswordLess\Device': (
('DevicePasswordLessBuildVersion', 0, 'DWORD'),
),
# Disable Location Tracking
r'Software\Microsoft\Windows NT\CurrentVersion\Sensor\Overrides\{BFA794E4-F964-4FDB-90F6-51056BFE4B44}': (
('SensorPermissionState', 0, 'DWORD'),
@ -43,6 +58,14 @@ REG_WINDOWS_EXPLORER = {
r'Software\Policies\Microsoft\Windows\DataCollection': (
('AllowTelemetry', 0, 'DWORD'),
),
# Disable floating Bing search widget
r'Software\Policies\Microsoft\Edge': (
('WebWidgetAllowed', 0, 'DWORD'),
),
# Disable Edge first run screen
r'Software\Policies\Microsoft\MicrosoftEdge\Main': (
('PreventFirstRunPage', 1, 'DWORD'),
),
# Disable Wi-Fi Sense
r'Software\Microsoft\PolicyManager\default\WiFi\AllowWiFiHotSpotReporting': (
('Value', 0, 'DWORD'),
@ -68,6 +91,10 @@ REG_WINDOWS_EXPLORER = {
r'Software\Microsoft\Windows\CurrentVersion\Feeds': (
('ShellFeedsTaskbarOpenOnHover', 0, 'DWORD'),
),
# Disable search highlights
r'Software\Microsoft\Windows\CurrentVersion\Feeds\DSB': (
('ShowDynamicContent', 0, 'DWORD'),
),
# File Explorer
r'Software\Microsoft\Windows\CurrentVersion\Explorer\Advanced': (
# Change default Explorer view to "Computer"
@ -82,6 +109,14 @@ REG_WINDOWS_EXPLORER = {
r'Software\Microsoft\Windows\CurrentVersion\Search': (
('SearchboxTaskbarMode', 1, 'DWORD'),
),
# Disable search highlights from opening on hover
r'Software\Microsoft\Windows\CurrentVersion\SearchSettings': (
('IsDynamicSearchBoxEnabled', 0, 'DWORD'),
),
# Disable "Let's make Windows even better" screens
r'Software\Microsoft\Windows\CurrentVersion\UserProfileEngagement': (
('ScoobeSystemSettingEnabled', 0, 'DWORD'),
),
},
}
REG_OPEN_SHELL_SETTINGS = {
@ -90,6 +125,7 @@ REG_OPEN_SHELL_SETTINGS = {
('ShowedStyle2', 1, 'DWORD'),
),
r'Software\OpenShell\StartMenu\Settings': (
('HighlightNew', 0, 'DWORD'),
('MenuStyle', 'Win7', 'SZ'),
('RecentPrograms', 'Recent', 'SZ'),
('SkinW7', 'Fluent-Metro', 'SZ'),

View file

@ -1,5 +1,4 @@
"""WizardKit: Config - Tool Sources"""
# pylint: disable=line-too-long
# vim: sts=2 sw=2 ts=2
@ -23,47 +22,39 @@ SOURCES = {
'RKill': 'https://download.bleepingcomputer.com/grinler/rkill.exe',
'RegDelNull': 'https://live.sysinternals.com/RegDelNull.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',
'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_2019_x32': 'https://aka.ms/vs/16/release/vc_redist.x86.exe',
'VCRedist_2019_x64': 'https://aka.ms/vs/16/release/vc_redist.x64.exe',
# Build Kit
'AIDA64': 'https://download.aida64.com/aida64engineer660.zip',
'Adobe Reader DC': 'https://ardownload2.adobe.com/pub/adobe/reader/win/AcrobatDC/2101120039/AcroRdrDC2101120039_en_US.exe',
'AIDA64': 'https://download.aida64.com/aida64engineer692.zip',
'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',
'Autoruns32': 'http://live.sysinternals.com/Autoruns.exe',
'Autoruns64': 'http://live.sysinternals.com/Autoruns64.exe',
'BleachBit': 'https://download.bleachbit.org/BleachBit-4.4.2-portable.zip',
'BlueScreenView32': 'http://www.nirsoft.net/utils/bluescreenview.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',
'Everything32': 'https://www.voidtools.com/Everything-1.4.1.1009.x86.en-US.zip',
'Everything64': 'https://www.voidtools.com/Everything-1.4.1.1009.x64.en-US.zip',
'FastCopy': 'https://ftp.vector.co.jp/73/10/2323/FastCopy392_installer.exe',
'Fluent-Metro': 'https://github.com/bonzibudd/Fluent-Metro/releases/download/v1.5.2/Fluent-Metro_1.5.2.zip',
'FurMark': 'https://geeks3d.com/dl/get/569',
'HWiNFO': 'https://www.sac.sk/download/utildiag/hwi_712.zip',
'IOBit Uninstaller': 'https://portableapps.com/redirect/?a=IObitUninstallerPortable&s=s&d=pa&f=IObitUninstallerPortable_7.5.0.7.paf.exe',
'LibreOffice32': 'https://download.documentfoundation.org/libreoffice/stable/7.3.0/win/x86/LibreOffice_7.3.0_Win_x86.msi',
'LibreOffice64': 'https://download.documentfoundation.org/libreoffice/stable/7.3.0/win/x86_64/LibreOffice_7.3.0_Win_x64.msi',
'Everything32': 'https://www.voidtools.com/Everything-1.4.1.1024.x86.zip',
'Everything64': 'https://www.voidtools.com/Everything-1.4.1.1024.x64.zip',
'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',
'FurMark': 'https://geeks3d.com/dl/get/728',
'HWiNFO': 'https://www.sac.sk/download/utildiag/hwi_764.zip',
'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.6.2/win/x86_64/LibreOffice_7.6.2_Win_x86-64.msi',
'Macs Fan Control': 'https://www.crystalidea.com/downloads/macsfancontrol_setup.exe',
'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',
'OpenShell': 'https://github.com/Open-Shell/Open-Shell-Menu/releases/download/v4.4.160/OpenShellSetup_4_4_160.exe',
'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.191/OpenShellSetup_4_4_191.exe',
'PuTTY': 'https://the.earth.li/~sgtatham/putty/latest/w32/putty.zip',
'SDIO Torrent': 'https://www.snappy-driver-installer.org/downloads/SDIO_Update.torrent',
'TestDisk': 'https://www.cgsecurity.org/testdisk-7.2-WIP.win.zip',
'WizTree': 'https://diskanalyzer.com/files/wiztree_4_07_portable.zip',
'SDIO Torrent': 'https://www.glenn.delahoy.com/downloads/sdio/SDIO_Update.torrent',
'WizTree': 'https://diskanalyzer.com/files/wiztree_4_15_portable.zip',
'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 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 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,24 +1,20 @@
"""WizardKit: Config - UFD"""
# vim: sts=2 sw=2 ts=2
from collections import OrderedDict
from wk.cfg.main import KIT_NAME_FULL
# General
SOURCES = OrderedDict({
SOURCES = {
'Linux': {'Arg': '--linux', 'Type': 'ISO'},
'Linux (Minimal)': {'Arg': '--linux-minimal', 'Type': 'ISO'},
'WinPE': {'Arg': '--winpe', 'Type': 'ISO'},
'Main Kit': {'Arg': '--main-kit', 'Type': 'KIT'},
'Extra Dir': {'Arg': '--extra-dir', 'Type': 'DIR'},
})
}
# Definitions: Boot entries
BOOT_ENTRIES = {
# Path to check: Comment to remove
'/arch_minimal': 'UFD-MINIMAL',
'/sources/boot.wim': 'UFD-WINPE',
}
BOOT_FILES = {
@ -41,14 +37,6 @@ ITEMS = {
),
'Linux': (
('/arch', '/'),
('/EFI/boot', '/EFI/'),
('/syslinux', '/'),
),
'Linux (Minimal)': (
('/arch/boot/x86_64/initramfs-linux.img', '/arch_minimal/'),
('/arch/boot/x86_64/vmlinuz-linux', '/arch_minimal/'),
('/arch/pkglist.x86_64.txt', '/arch_minimal/'),
('/arch/x86_64', '/arch_minimal/'),
),
'Main Kit': (
('/', f'/{KIT_NAME_FULL}/'),
@ -66,10 +54,28 @@ ITEMS = {
('/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 = (
# Linux (all versions)
'arch',
'arch_minimal',
'EFI',
'syslinux',
# Main Kit

View file

@ -33,4 +33,11 @@ WINDOWS_BUILDS = {
'10.0.19042': '20H2',
'10.0.19043': '21H1',
'10.0.19044': '21H2',
'10.0.19045': '22H2',
# Windows 11
'10.0.22000': '21H2',
'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"""
from . import block_pair
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

@ -1,23 +1,87 @@
"""WizardKit: Debug Functions"""
# pylint: disable=invalid-name
# vim: sts=2 sw=2 ts=2
import inspect
import logging
import lzma
import os
import pathlib
import pickle
import platform
import re
import socket
import sys
import time
from typing import Any
import requests
from wk.cfg.net import CRASH_SERVER
from wk.log import get_root_logger_path
# Classes
class Debug():
# pylint: disable=too-few-public-methods
"""Object used when dumping debug data."""
def method(self):
def method(self) -> None:
"""Dummy method used to identify functions vs data."""
# STATIC VARIABLES
LOG = logging.getLogger(__name__)
DEBUG_CLASS = Debug()
METHOD_TYPE = type(DEBUG_CLASS.method)
# Functions
def generate_object_report(obj, indent=0):
def generate_debug_report() -> str:
"""Generate debug report, returns str."""
platform_function_list = (
'architecture',
'machine',
'platform',
'python_version',
)
report = []
# Logging data
try:
log_path = get_root_logger_path()
except RuntimeError:
# Assuming logging wasn't started
pass
else:
report.append('------ Start Log -------')
report.append('')
with open(log_path, 'r', encoding='utf-8') as log_file:
report.extend(log_file.read().splitlines())
report.append('')
report.append('------- End Log --------')
# System
report.append('--- Start debug info ---')
report.append('')
report.append('[System]')
report.append(f' {"FQDN":<24} {socket.getfqdn()}')
for func in platform_function_list:
func_name = func.replace('_', ' ').capitalize()
func_result = getattr(platform, func)()
report.append(f' {func_name:<24} {func_result}')
report.append(f' {"Python sys.argv":<24} {sys.argv}')
report.append('')
# Environment
report.append('[Environment Variables]')
for key, value in sorted(os.environ.items()):
report.append(f' {key:<24} {value}')
report.append('')
# Done
report.append('---- End debug info ----')
return '\n'.join(report)
def generate_object_report(obj: Any, indent: int = 0) -> list[str]:
"""Generate debug report for obj, returns list."""
report = []
attr_list = []
@ -39,7 +103,7 @@ def generate_object_report(obj, indent=0):
# Add attribute to report (expanded if necessary)
if isinstance(attr, dict):
report.append(f'{name}:')
for key, value in sorted(attr.items()):
for key, value in attr.items():
report.append(f'{" "*(indent+1)}{key}: {str(value)}')
else:
report.append(f'{" "*indent}{name}: {str(attr)}')
@ -48,5 +112,78 @@ def generate_object_report(obj, indent=0):
return report
def save_pickles(
obj_dict: dict[Any, Any],
out_path: pathlib.Path | str | None = None,
) -> None:
"""Save dict of objects using pickle."""
LOG.info('Saving pickles')
# Set path
if not out_path:
out_path = get_root_logger_path()
out_path = out_path.parent.joinpath('../debug').resolve()
# Save pickles
try:
for name, obj in obj_dict.copy().items():
if name.startswith('__') or inspect.ismodule(obj):
continue
with open(f'{out_path}/{name}.pickle', 'wb') as _f:
pickle.dump(obj, _f, protocol=pickle.HIGHEST_PROTOCOL)
except Exception:
LOG.error('Failed to save all the pickles', exc_info=True)
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."""
LOG.info('Uploading debug report to %s', CRASH_SERVER.get('Name', '?'))
headers = CRASH_SERVER.get('Headers', {'X-Requested-With': 'XMLHttpRequest'})
if compress:
headers['Content-Type'] = 'application/octet-stream'
# Check if the required server details are available
if not all(CRASH_SERVER.get(key, False) for key in ('Name', 'Url', 'User')):
msg = 'Server details missing, aborting upload.'
print(msg)
raise RuntimeError(msg)
# Set filename (based on the logging config if possible)
filename = 'Unknown'
try:
log_path = get_root_logger_path()
except RuntimeError:
# Assuming logging wasn't started
pass
else:
# Strip everything but the prefix
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'
LOG.debug('filename: %s', filename)
# Compress report
if compress:
filename += '.xz'
xz_report = lzma.compress(report.encode('utf8'))
# Upload report
url = f'{CRASH_SERVER["Url"]}/{filename}'
response = requests.put(
url,
auth=(CRASH_SERVER['User'], CRASH_SERVER.get('Pass', '')),
data=xz_report if compress else report,
headers=headers,
timeout=60,
)
# Check response
if not response.ok:
raise RuntimeError('Failed to upload report')
if __name__ == '__main__':
print("This file is not meant to be called directly.")

View file

@ -1,15 +1,18 @@
"""WizardKit: Execution functions"""
#vim: sts=2 sw=2 ts=2
# vim: sts=2 sw=2 ts=2
import json
import logging
import os
import pathlib
import re
import subprocess
import time
from threading import Thread
from io import IOBase
from queue import Queue, Empty
from threading import Thread
from typing import Any, Callable, Iterable
import psutil
@ -21,16 +24,15 @@ LOG = logging.getLogger(__name__)
# Classes
class NonBlockingStreamReader():
"""Class to allow non-blocking reads from a stream."""
# pylint: disable=too-few-public-methods
# Credits:
## https://gist.github.com/EyalAr/7915597
## https://stackoverflow.com/a/4896288
def __init__(self, stream):
self.stream = stream
self.queue = Queue()
def __init__(self, stream: IOBase):
self.stream: IOBase = stream
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."""
while not stream.closed:
try:
@ -46,18 +48,18 @@ class NonBlockingStreamReader():
args=(self.stream, self.queue),
)
def stop(self):
def stop(self) -> None:
"""Stop reading from input stream."""
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."""
try:
return self.queue.get(block=timeout is not None, timeout=timeout)
except Empty:
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."""
LOG.debug('Saving process %s output to %s', proc, out_path)
while proc.poll() is None:
@ -75,7 +77,12 @@ class NonBlockingStreamReader():
# 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.
Specifically subprocess.run() and subprocess.Popen().
@ -92,7 +99,7 @@ def build_cmd_kwargs(cmd, minimized=False, pipe=True, shell=False, **kwargs):
# Strip sudo if appropriate
if cmd[0] == 'sudo':
if os.name == 'posix' and os.geteuid() == 0: # pylint: disable=no-member
if os.name == 'posix' and os.geteuid() == 0:
cmd.pop(0)
# Add additional kwargs if applicable
@ -107,8 +114,8 @@ def build_cmd_kwargs(cmd, minimized=False, pipe=True, shell=False, **kwargs):
# Start minimized
if minimized:
startupinfo = subprocess.STARTUPINFO()
startupinfo.dwFlags = subprocess.STARTF_USESHOWWINDOW
startupinfo = subprocess.STARTUPINFO() # type: ignore
startupinfo.dwFlags = subprocess.STARTF_USESHOWWINDOW # type: ignore
startupinfo.wShowWindow = 6
cmd_kwargs['startupinfo'] = startupinfo
@ -123,7 +130,12 @@ def build_cmd_kwargs(cmd, minimized=False, pipe=True, shell=False, **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.
If the data can't be decoded then either an exception is raised
@ -142,7 +154,11 @@ def get_json_from_command(cmd, check=True, encoding='utf-8', errors='ignore'):
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."""
LOG.debug('name: %s, exact: %s', name, exact)
processes = []
@ -162,7 +178,12 @@ def get_procs(name, exact=True, try_again=True):
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).
NOTE: Under Posix systems this will send SIGINT to allow processes
@ -186,7 +207,13 @@ def kill_procs(name, exact=True, force=False, timeout=30):
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."""
LOG.debug(
'cmd: %s, minimized: %s, pipe: %s, shell: %s',
@ -200,7 +227,6 @@ def popen_program(cmd, minimized=False, pipe=False, shell=False, **kwargs):
shell=shell,
**kwargs)
try:
# pylint: disable=consider-using-with
proc = subprocess.Popen(**cmd_kwargs)
except FileNotFoundError:
LOG.error('Command not found: %s', cmd)
@ -211,8 +237,13 @@ def popen_program(cmd, minimized=False, pipe=False, shell=False, **kwargs):
return proc
def run_program(cmd, check=True, pipe=True, shell=False, **kwargs):
# pylint: disable=subprocess-run-check
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."""
LOG.debug(
'cmd: %s, check: %s, pipe: %s, shell: %s',
@ -225,8 +256,9 @@ def run_program(cmd, check=True, pipe=True, shell=False, **kwargs):
pipe=pipe,
shell=shell,
**kwargs)
check = cmd_kwargs.pop('check', True) # Avoids linting warning
try:
proc = subprocess.run(**cmd_kwargs)
proc = subprocess.run(check=check, **cmd_kwargs)
except FileNotFoundError:
LOG.error('Command not found: %s', cmd)
raise
@ -236,7 +268,11 @@ def run_program(cmd, check=True, pipe=True, shell=False, **kwargs):
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."""
LOG.debug(
'Starting background thread for function: %s, args: %s, daemon: %s',
@ -248,7 +284,7 @@ def start_thread(function, args=None, daemon=True):
return thread
def stop_process(proc, graceful=True):
def stop_process(proc: subprocess.Popen, graceful: bool = True) -> None:
"""Stop process.
NOTES: proc should be a subprocess.Popen obj.
@ -257,20 +293,24 @@ def stop_process(proc, graceful=True):
# Graceful exit
if graceful:
if os.name == 'posix' and os.geteuid() != 0: # pylint: disable=no-member
if os.name == 'posix' and os.geteuid() != 0:
run_program(['sudo', 'kill', str(proc.pid)], check=False)
else:
proc.terminate()
time.sleep(2)
# Force exit
if os.name == 'posix' and os.geteuid() != 0: # pylint: disable=no-member
if os.name == 'posix' and os.geteuid() != 0:
run_program(['sudo', 'kill', '-9', str(proc.pid)], check=False)
else:
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."""
LOG.debug('name: %s, exact: %s, timeout: %s', name, exact, timeout)
target_procs = get_procs(name, exact=exact)

View file

@ -3,7 +3,7 @@
import logging
from wk.std import color_string
from wk.ui import ansi
# STATIC VARIABLES
@ -33,7 +33,10 @@ THRESH_GREAT = 750 * 1024**2
# 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."""
graph = ['', '', '', '']
scale = 8 if oneline else 32
@ -52,27 +55,27 @@ def generate_horizontal_graph(rate_list, graph_width=40, oneline=False):
rate_color = 'GREEN'
# Build graph
full_block = color_string((GRAPH_HORIZONTAL[-1],), (rate_color,))
full_block = ansi.color_string((GRAPH_HORIZONTAL[-1],), (rate_color,))
if step >= 24:
graph[0] += color_string((GRAPH_HORIZONTAL[step-24],), (rate_color,))
graph[0] += ansi.color_string((GRAPH_HORIZONTAL[step-24],), (rate_color,))
graph[1] += full_block
graph[2] += full_block
graph[3] += full_block
elif step >= 16:
graph[0] += ' '
graph[1] += color_string((GRAPH_HORIZONTAL[step-16],), (rate_color,))
graph[1] += ansi.color_string((GRAPH_HORIZONTAL[step-16],), (rate_color,))
graph[2] += full_block
graph[3] += full_block
elif step >= 8:
graph[0] += ' '
graph[1] += ' '
graph[2] += color_string((GRAPH_HORIZONTAL[step-8],), (rate_color,))
graph[2] += ansi.color_string((GRAPH_HORIZONTAL[step-8],), (rate_color,))
graph[3] += full_block
else:
graph[0] += ' '
graph[1] += ' '
graph[2] += ' '
graph[3] += color_string((GRAPH_HORIZONTAL[step],), (rate_color,))
graph[3] += ansi.color_string((GRAPH_HORIZONTAL[step],), (rate_color,))
# Done
if oneline:
@ -80,7 +83,7 @@ def generate_horizontal_graph(rate_list, graph_width=40, oneline=False):
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."""
rate_in_mb = rate / (1024**2)
step = 0
@ -95,14 +98,17 @@ def get_graph_step(rate, scale=16):
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."""
merged_rates = []
offset = 0
slice_width = int(len(rates) / graph_width)
# Merge rates
for _i in range(graph_width):
for _ in range(graph_width):
merged_rates.append(sum(rates[offset:offset+slice_width])/slice_width)
offset += slice_width
@ -110,7 +116,7 @@ def merge_rates(rates, graph_width=40):
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."""
color_bar = None
color_rate = None
@ -128,7 +134,7 @@ def vertical_graph_line(percent, rate, scale=32):
color_rate = 'GREEN'
# Build string
line = color_string(
line = ansi.color_string(
strings=(
f'{percent:5.1f}%',
f'{GRAPH_VERTICAL[step]:<4}',

View file

@ -21,11 +21,8 @@ from wk.cfg.hw import (
THRESH_SSD_MIN,
)
from wk.exe import run_program
from wk.std import (
PLATFORM,
strip_colors,
color_string,
)
from wk.std import PLATFORM
from wk.ui import ansi
# STATIC VARIABLES
@ -66,8 +63,8 @@ def calc_io_dd_values(dev_size, test_mode=False) -> dict[str, int]:
test_mode limits the benchmark to IO_MINIMUM_TEST_SIZE (if possible)
"""
if test_mode:
dev_size = min(IO_MINIMUM_TEST_SIZE, dev_size)
if test_mode and dev_size > IO_MINIMUM_TEST_SIZE:
dev_size = IO_MINIMUM_TEST_SIZE
read_total = min(IO_MINIMUM_TEST_SIZE, dev_size)
read_total = max(read_total, dev_size*IO_ALT_TEST_SIZE_FACTOR)
read_chunks = int(read_total // IO_CHUNK_SIZE)
@ -85,6 +82,12 @@ def calc_io_dd_values(dev_size, test_mode=False) -> dict[str, int]:
# skip_extra_rate == 0 is fine
pass
# Test mode
if test_mode:
read_chunks_limit = int(read_chunks * 0.1)
read_chunks_limit = (read_chunks_limit // IO_GRAPH_WIDTH) * IO_GRAPH_WIDTH
read_chunks = max(IO_GRAPH_WIDTH, read_chunks_limit)
# Done
return {
'Read Chunks': read_chunks,
@ -110,7 +113,7 @@ def check_io_results(test_obj, rate_list, graph_width) -> None:
# Add horizontal graph to report
for line in graph.generate_horizontal_graph(rate_list, graph_width):
if not strip_colors(line).strip():
if not ansi.strip_colors(line).strip():
# Skip empty lines
continue
test_obj.report.append(line)
@ -148,7 +151,7 @@ def run_io_test(test_obj, log_path, test_mode=False) -> None:
LOG.info('Using %s for better performance', dev_path)
offset = 0
read_rates = []
test_obj.report.append(color_string('I/O Benchmark', 'BLUE'))
test_obj.report.append(ansi.color_string('I/O Benchmark', 'BLUE'))
# Get dd values or bail
try:
@ -156,7 +159,7 @@ def run_io_test(test_obj, log_path, test_mode=False) -> None:
except DeviceTooSmallError:
test_obj.set_status('N/A')
test_obj.report.append(
color_string('Disk too small to test', 'YELLOW'),
ansi.color_string('Disk too small to test', 'YELLOW'),
)
return

View file

@ -8,15 +8,10 @@ import subprocess
from typing import TextIO
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.std import (
PLATFORM,
color_string,
print_error,
print_warning,
)
from wk.tmux import respawn_pane as tmux_respawn_pane
from wk.std import PLATFORM
from wk.ui import ansi
# STATIC VARIABLES
@ -25,32 +20,75 @@ SysbenchType = tuple[subprocess.Popen, TextIO]
# Functions
def check_cooling_results(test_obj, sensors, run_sysbench=False) -> None:
"""Check cooling results and update test_obj."""
max_temp = sensors.cpu_max_temp()
temp_labels = ['Idle', 'Max', 'Cooldown']
if run_sysbench:
temp_labels.append('Sysbench')
def check_cooling_results(sensors, test_object) -> None:
"""Check cooling result via sensor data."""
idle_temp = sensors.get_cpu_temp('Idle')
cooldown_temp = sensors.get_cpu_temp('Cooldown')
max_temp = sensors.get_cpu_temp('Max')
test_object.report.append(ansi.color_string('Temps', 'BLUE'))
# Check temps
if not max_temp:
test_obj.set_status('Unknown')
elif max_temp >= CPU_FAILURE_TEMP:
test_obj.failed = True
test_obj.set_status('Failed')
elif 'Aborted' not in test_obj.status:
test_obj.passed = True
test_obj.set_status('Passed')
if max_temp > CPU_TEMPS['Critical']:
test_object.failed = True
test_object.set_status('Failed')
test_object.report.extend([
ansi.color_string(
f' WARNING: Critical CPU temp of {CPU_TEMPS["Critical"]} exceeded.',
'RED',
),
'',
])
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
for line in sensors.generate_report(*temp_labels, only_cpu=True):
test_obj.report.append(f' {line}')
# Build report
report_labels = ['Idle']
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:
"""Check mprime log files and update test_obj."""
passing_lines = {}
warning_lines = {}
passing_lines = set()
warning_lines = set()
def _read_file(log_name) -> list[str]:
"""Read file and split into lines, returns list."""
@ -68,9 +106,9 @@ def check_mprime_results(test_obj, working_dir) -> None:
for line in _read_file('results.txt'):
line = line.strip()
if re.search(r'(error|fail)', line, re.IGNORECASE):
warning_lines[line] = None
warning_lines.add(line)
# print.log (check if passed)
# prime.log (check if passed)
for line in _read_file('prime.log'):
line = line.strip()
match = re.search(
@ -78,10 +116,10 @@ def check_mprime_results(test_obj, working_dir) -> None:
if match:
if int(match.group(2)) + int(match.group(3)) > 0:
# Errors and/or warnings encountered
warning_lines[match.group(1).capitalize()] = None
warning_lines.add(match.group(1).capitalize())
else:
# No errors/warnings
passing_lines[match.group(1).capitalize()] = None
passing_lines.add(match.group(1).capitalize())
# Update status
if warning_lines:
@ -97,29 +135,31 @@ def check_mprime_results(test_obj, working_dir) -> None:
for line in passing_lines:
test_obj.report.append(f' {line}')
for line in warning_lines:
test_obj.report.append(color_string(f' {line}', 'YELLOW'))
test_obj.report.append(ansi.color_string(f' {line}', 'YELLOW'))
if not (passing_lines or warning_lines):
test_obj.report.append(color_string(' Unknown result', 'YELLOW'))
test_obj.report.append(ansi.color_string(' Unknown result', 'YELLOW'))
def start_mprime(working_dir, log_path) -> subprocess.Popen:
"""Start mprime and save filtered output to log, returns Popen object."""
set_apple_fan_speed('max')
proc_mprime = subprocess.Popen( # pylint: disable=consider-using-with
proc_mprime = subprocess.Popen(
['mprime', '-t'],
cwd=working_dir,
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
)
proc_grep = subprocess.Popen( # pylint: disable=consider-using-with
proc_grep = subprocess.Popen(
'grep --ignore-case --invert-match --line-buffered stress.txt'.split(),
stdin=proc_mprime.stdout,
stdout=subprocess.PIPE,
)
proc_mprime.stdout.close()
save_nsbr = exe.NonBlockingStreamReader(proc_grep.stdout)
proc_mprime.stdout.close() # type: ignore[reportOptionalMemberAccess]
save_nbsr = exe.NonBlockingStreamReader(
proc_grep.stdout, # type: ignore[reportGeneralTypeIssues]
)
exe.start_thread(
save_nsbr.save_to_file,
save_nbsr.save_to_file,
args=(proc_grep, log_path),
)
@ -127,38 +167,6 @@ def start_mprime(working_dir, log_path) -> subprocess.Popen:
return proc_mprime
def start_sysbench(sensors, sensors_out, log_path, pane) -> 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'),
)
# Update bottom pane
tmux_respawn_pane(pane, watch_file=log_path, watch_cmd='tail')
# Start sysbench
filehandle_sysbench = open( # pylint: disable=consider-using-with
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:
"""Set Apple fan speed."""
cmd = None
@ -174,14 +182,35 @@ def set_apple_fan_speed(speed) -> None:
except (RuntimeError, ValueError, subprocess.CalledProcessError) as err:
LOG.error('Failed to set fans to %s', speed)
LOG.error('Error: %s', err)
print_error(f'Failed to set fans to {speed}')
for line in str(err).splitlines():
print_warning(f' {line.strip()}')
#ui.print_error(f'Failed to set fans to {speed}')
#for line in str(err).splitlines():
# ui.print_warning(f' {line.strip()}')
elif PLATFORM == 'Linux':
cmd = ['apple-fans', speed]
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:
"""Stop mprime gracefully, then forcefully as needed."""
proc_mprime.terminate()

File diff suppressed because it is too large Load diff

View file

@ -8,18 +8,17 @@ import plistlib
import re
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.python import DATACLASS_DECORATOR_KWARGS
from wk.exe import get_json_from_command, run_program
from wk.hw.test import Test
from wk.hw.smart import (
enable_smart,
generate_attribute_report,
update_smart_details,
get_known_disk_attributes,
)
from wk.std import PLATFORM, bytes_to_string, color_string, strip_colors
from wk.std import PLATFORM
from wk.ui import ansi
# STATIC VARIABLES
@ -31,48 +30,44 @@ WK_LABEL_REGEX = re.compile(
# Classes
@dataclass(**DATACLASS_DECORATOR_KWARGS)
@dataclass(slots=True)
class Disk:
# pylint: disable=too-many-instance-attributes
"""Object for tracking disk specific data."""
attributes: dict[Any, dict] = field(init=False, default_factory=dict)
bus: str = field(init=False)
children: list[dict] = field(init=False, default_factory=list)
filesystem: str = field(init=False)
log_sec: int = field(init=False)
model: str = field(init=False)
name: str = field(init=False)
notes: list[str] = field(init=False, default_factory=list)
path: Union[pathlib.Path, str]
parent: str = field(init=False)
phy_sec: int = field(init=False)
raw_details: dict[str, Any] = field(init=False)
raw_smartctl: dict[str, Any] = field(init=False)
serial: str = field(init=False)
size: int = field(init=False)
ssd: bool = field(init=False)
tests: list[Test] = field(init=False, default_factory=list)
use_sat: bool = field(init=False, default=False)
attributes: dict[Any, dict] = field(init=False, default_factory=dict)
bus: str = field(init=False)
children: list[dict] = field(init=False, default_factory=list)
description: str = field(init=False)
filesystem: str = 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)
log_sec: int = field(init=False)
model: str = field(init=False)
name: str = field(init=False)
notes: list[str] = field(init=False, default_factory=list)
path: pathlib.Path = field(init=False)
path_str: pathlib.Path | str
parent: str = field(init=False)
phy_sec: int = field(init=False)
raw_details: dict[str, Any] = field(init=False)
raw_smartctl: dict[str, Any] = field(init=False, default_factory=dict)
serial: str = field(init=False)
size: int = field(init=False)
ssd: bool = field(init=False)
tests: list[Test] = field(init=False, default_factory=list)
trim: bool = field(init=False)
def __post_init__(self) -> None:
self.path = pathlib.Path(self.path).resolve()
def __post_init__(self):
self.path = pathlib.Path(self.path_str).resolve()
self.update_details()
enable_smart(self)
update_smart_details(self)
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
enable_smart(self)
update_smart_details(self)
self.set_description()
self.known_attributes = get_known_disk_attributes(self.model)
if not self.is_4k_aligned():
self.add_note('One or more partitions are not 4K aligned', 'YELLOW')
def add_note(self, note, color=None) -> None:
"""Add note that will be included in the disk report."""
if color:
note = color_string(note, color)
note = ansi.color_string(note, color)
if note not in self.notes:
self.notes.append(note)
self.notes.sort()
@ -81,18 +76,10 @@ class Disk:
"""Check if note is already present."""
present = False
for note in self.notes:
if note_str == strip_colors(note):
if note_str == ansi.strip_colors(note):
present = True
return present
@property
def description(self) -> str:
"""Get disk description from details."""
return (
f'{bytes_to_string(self.size, use_binary=False)}'
f' ({self.bus}) {self.model} {self.serial}'
)
def disable_disk_tests(self) -> None:
"""Disable all tests."""
LOG.warning('Disabling all tests for: %s', self.path)
@ -105,18 +92,18 @@ class Disk:
"""Generate Disk report, returns list."""
report = []
if header:
report.append(color_string(f'Device ({self.path.name})', 'BLUE'))
report.append(ansi.color_string(f'Device ({self.path.name})', 'BLUE'))
report.append(f' {self.description}')
# Attributes
if self.attributes:
if header:
report.append(color_string('Attributes', 'BLUE'))
report.append(ansi.color_string('Attributes', 'BLUE'))
report.extend(generate_attribute_report(self))
# Notes
if self.notes:
report.append(color_string('Notes', 'BLUE'))
report.append(ansi.color_string('Notes', 'BLUE'))
for note in self.notes:
report.append(f' {note}')
@ -151,12 +138,50 @@ class Disk:
return aligned
@property
def present(self) -> bool:
"""Verify this device is still present, returns bool."""
if not self.path.exists():
self.add_note('Device disconnected', 'RED')
return False
return True
def set_description(self) -> None:
"""Set disk description from details."""
decimals = 1
suffix = ' '
# Set size_str (try binary scale first)
for scale in (1024, 1000):
size = float(self.size)
units = list('KMGTPEZY')
while units:
if abs(size) < scale:
break
size /= scale
suffix = units.pop(0)
size = ((size * 10) // 1) / 10
if size % 1 == 0:
# Found an exact whole number, drop the decimal
decimals = 0
break
if size % 1 == 0.5:
break
# Done
self.description = (
f'{size:0.{decimals}f} {suffix}B ({self.bus}) {self.model} {self.serial}'
)
def update_details(self, skip_children=True) -> None:
"""Update disk details using OS specific methods.
Required details default to generic descriptions
and are converted to the correct type.
"""
if not self.present:
return
if PLATFORM == 'Darwin':
self.raw_details = get_disk_details_macos(
self.path, skip_children=skip_children,
@ -180,6 +205,7 @@ class Disk:
self.serial = self.raw_details.get('serial', 'Unknown Serial')
self.size = self.raw_details.get('size', -1)
self.ssd = self.raw_details.get('ssd', False)
self.trim = self.raw_details.get('trim', False)
# Ensure certain attributes types
## NOTE: This is ugly, deal.
@ -193,6 +219,10 @@ class Disk:
if attr == 'size':
setattr(self, attr, -1)
# Add TRIM note
if self.trim:
self.add_note('TRIM support detected', 'YELLOW')
# Functions
def get_disk_details_linux(disk_path, skip_children=True) -> dict[Any, Any]:
@ -216,10 +246,12 @@ def get_disk_details_linux(disk_path, skip_children=True) -> dict[Any, Any]:
dev['bus'] = dev.pop('tran', '???')
dev['parent'] = dev.pop('pkname', None)
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:
dev['bus'] = 'Image'
dev['model'] = ''
dev['serial'] = ''
dev['trim'] = False # NOTE: This check is just for physical devices
# Convert to dict
details = dev_list.pop(0)
@ -277,6 +309,7 @@ def get_disk_details_macos(disk_path, skip_children=True) -> dict:
dev['serial'] = get_disk_serial_macos(dev['path'])
dev['size'] = dev.pop('Size', -1)
dev['ssd'] = dev.pop('SolidState', False)
dev['trim'] = False # TODO: ACtually check for TRIM
dev['vendor'] = ''
if dev.get('WholeDisk', True):
dev['parent'] = None
@ -311,6 +344,8 @@ def get_disks(skip_kits=False) -> list[Disk]:
# Skip WK disks
if skip_kits:
for disk in disks:
disk.update_details(skip_children=False)
disks = [
disk_obj for disk_obj in disks
if not any(
@ -336,6 +371,10 @@ def get_disks_linux() -> list[Disk]:
if disk_obj.raw_details.get('type', '???') != 'disk':
continue
# Skip empty devices (usually card readers)
if disk_obj.size <= 0:
continue
# Add disk
disks.append(disk_obj)

View file

@ -4,7 +4,7 @@
import logging
from wk.exe import run_program
from wk.std import PLATFORM, print_warning
from wk.std import PLATFORM
# STATIC VARIABLES
@ -17,7 +17,8 @@ def keyboard_test() -> None:
if PLATFORM == 'Linux':
run_xev()
else:
print_warning(f'Not supported under this OS: {PLATFORM}')
LOG.error('Not supported under this OS: %s', PLATFORM)
raise NotImplementedError(f'Not supported under this OS: {PLATFORM}')
def run_xev() -> None:

View file

@ -9,11 +9,7 @@ from wk.net import (
show_valid_addresses,
speedtest,
)
from wk.std import (
TryAndPrint,
pause,
print_warning,
)
from wk.ui import cli as ui
# STATIC VARIABLES
@ -24,7 +20,7 @@ LOG = logging.getLogger(__name__)
def network_test() -> None:
"""Run network tests."""
LOG.info('Network Test')
try_and_print = TryAndPrint()
try_and_print = ui.TryAndPrint()
result = try_and_print.run(
message='Network connection...',
function=connected_to_private_network,
@ -34,8 +30,8 @@ def network_test() -> None:
# Bail if not connected
if result['Failed']:
print_warning('Please connect to a network and try again')
pause('Press Enter to return to main menu...')
ui.print_warning('Please connect to a network and try again')
ui.pause('Press Enter to return to main menu...')
return
# Show IP address(es)
@ -51,7 +47,7 @@ def network_test() -> None:
try_and_print.run('Speedtest...', speedtest)
# Done
pause('Press Enter to return to main menu...')
ui.pause('Press Enter to return to main menu...')
if __name__ == '__main__':

View file

@ -6,7 +6,7 @@ import logging
from subprocess import PIPE
from wk.exe import run_program
from wk.tmux import zoom_pane as tmux_zoom_pane
from wk.ui import tmux
# STATIC VARIABLES
@ -31,9 +31,9 @@ def screensaver(name) -> None:
]
# Switch pane to fullscreen and start screensaver
tmux_zoom_pane()
tmux.zoom_pane()
run_program(cmd, check=False, pipe=False, stderr=PIPE)
tmux_zoom_pane()
tmux.zoom_pane()
if __name__ == '__main__':

View file

@ -6,13 +6,16 @@ import logging
import pathlib
import re
from copy import deepcopy
from subprocess import CalledProcessError
from threading import Thread
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.io import non_clobber_path
from wk.std import PLATFORM, color_string, sleep
from wk.std import PLATFORM, sleep
from wk.ui import ansi
# STATIC VARIABLES
@ -34,39 +37,83 @@ class ThermalLimitReachedError(RuntimeError):
# Classes
class Sensors():
"""Class for holding sensor specific data."""
def __init__(self):
self.background_thread = None
self.data = get_sensor_data()
self.out_path = None
"""Class for holding sensor specific data.
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"""
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 sources in adapters.values():
for source_data in sources.values():
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:
"""Check if CPU reached CPU_CRITICAL_TEMP, returns bool."""
"""Check if CPU exceeded critical temp, returns bool."""
for section, adapters in self.data.items():
if not section.startswith('CPU'):
# Limit to CPU temps
@ -75,16 +122,22 @@ class Sensors():
# Ugly section
for sources in adapters.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
# Didn't return above so temps are within the threshold
return False
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."""
report = []
include_avg_for = include_avg_for if include_avg_for else []
for section, adapters in sorted(self.data.items()):
if only_cpu and not section.startswith('CPU'):
@ -98,6 +151,10 @@ class Sensors():
for label in temp_labels:
if label != 'Current':
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(
source_data.get(label, '???'),
colored=colored,
@ -109,7 +166,7 @@ class Sensors():
# Handle empty reports
if not report:
report = [
color_string('WARNING: No sensors found', 'YELLOW'),
ansi.color_string('WARNING: No sensors found', 'YELLOW'),
'',
'Please monitor temps manually',
]
@ -117,11 +174,36 @@ class Sensors():
# Done
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(
self, out_path, alt_max=None,
exit_on_thermal_limit=True, temp_labels=None,
thermal_action=None) -> None:
# pylint: disable=too-many-arguments
"""Write report to path every second until stopped.
thermal_action is a cmd to run if ThermalLimitReachedError is caught.
@ -135,6 +217,7 @@ class Sensors():
temp_labels = ['Current', 'Max']
if alt_max:
temp_labels.append(alt_max)
self.temp_labels.add(alt_max)
# Start loop
while True:
@ -154,9 +237,15 @@ class Sensors():
# Sleep before next loop
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.."""
self.clear_temps()
self.clear_temps(next_label=temp_label, save_history=save_history)
self.temp_labels.add(temp_label)
# Get temps
for _ in range(seconds):
@ -182,7 +271,6 @@ class Sensors():
self, out_path, alt_max=None,
exit_on_thermal_limit=True, temp_labels=None,
thermal_action=None) -> None:
# pylint: disable=too-many-arguments
"""Start background thread to save report to file.
thermal_action is a cmd to run if ThermalLimitReachedError is caught.
@ -200,6 +288,10 @@ class Sensors():
def stop_background_monitor(self) -> None:
"""Stop background thread."""
# Bail early
if self.background_thread is None:
return
self.out_path.with_suffix('.stop').touch()
self.background_thread.join()
@ -210,6 +302,8 @@ class Sensors():
def update_sensor_data(
self, alt_max=None, exit_on_thermal_limit=True) -> None:
"""Update sensor data via OS-specific means."""
if alt_max:
self.temp_labels.add(alt_max)
if PLATFORM == 'Darwin':
self.update_sensor_data_macos(alt_max, exit_on_thermal_limit)
elif PLATFORM == 'Linux':
@ -236,7 +330,7 @@ class Sensors():
# Raise exception if thermal limit reached
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')
def update_sensor_data_macos(
@ -263,7 +357,7 @@ class Sensors():
# Raise exception if thermal limit reached
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')
@ -315,7 +409,11 @@ def get_sensor_data_linux() -> dict[Any, Any]:
## current temp is labeled xxxx_input
for source, labels in sources.items():
for label, temp in labels.items():
if label.startswith('fan') or label.startswith('in') or label.startswith('curr'):
if (
label.startswith('fan')
or label.startswith('in')
or label.startswith('curr')
):
# Skip fan RPMs and voltages
continue
if 'input' in label:
@ -416,14 +514,14 @@ def get_sensor_data_macos() -> dict[Any, Any]:
def get_temp_str(temp, colored=True) -> str:
"""Get colored string based on temp, returns str."""
temp_color = None
temp_color = ''
# Safety check
try:
temp = float(temp)
except (TypeError, ValueError):
# Invalid temp?
return color_string(temp, 'PURPLE')
return ansi.color_string(temp, 'PURPLE')
# Determine color
if colored:
@ -433,7 +531,7 @@ def get_temp_str(temp, colored=True) -> str:
break
# Done
return color_string(f'{"-" if temp < 0 else ""}{temp:2.0f}°C', temp_color)
return ansi.color_string(f'{"-" if temp < 0 else ""}{temp:2.0f}°C', temp_color)

View file

@ -1,6 +1,7 @@
"""WizardKit: SMART test functions"""
# vim: sts=2 sw=2 ts=2
import copy
import logging
import re
@ -17,7 +18,8 @@ from wk.cfg.hw import (
SMART_SELF_TEST_START_TIMEOUT_IN_SECONDS,
)
from wk.exe import get_json_from_command, run_program
from wk.std import bytes_to_string, color_string, sleep
from wk.std import bytes_to_string, sleep
from wk.ui import ansi
# STATIC VARIABLES
@ -39,26 +41,25 @@ def build_self_test_report(test_obj, aborted=False) -> None:
For instance if the test was aborted the report should include the
last known progress instead of just "was aborted by host."
"""
report = [color_string('Self-Test', 'BLUE')]
test_details = get_smart_self_test_details(test_obj.dev)
test_result = test_details.get('status', {}).get('string', 'Unknown')
report = [ansi.color_string('Self-Test', 'BLUE')]
test_result = get_smart_self_test_last_result(test_obj.dev)
# Build report
if test_obj.disabled or test_obj.status == 'Denied':
report.append(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:
report.append(color_string(f' {test_obj.status}', 'YELLOW'))
elif test_obj.status == 'TestInProgress':
report.append(color_string(' Failed to stop previous test', 'RED'))
test_obj.set_status('Failed')
report.append(ansi.color_string(f' {test_obj.status}', 'YELLOW'))
else:
# Other cases include self-test result string
report.append(f' {test_result.capitalize()}')
if aborted and not (test_obj.passed or test_obj.failed):
report.append(color_string(' Aborted', 'YELLOW'))
test_obj.set_status('Aborted')
if test_obj.status == 'TestInProgress':
report.append(ansi.color_string(' Failed to stop previous test', 'RED'))
test_obj.set_status('Failed')
elif test_obj.status == 'TimedOut':
report.append(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
test_obj.report.extend(report)
@ -67,16 +68,15 @@ def build_self_test_report(test_obj, aborted=False) -> None:
def check_attributes(dev, only_blocking=False) -> bool:
"""Check if any known attributes are failing, returns bool."""
attributes_ok = True
known_attributes = get_known_disk_attributes(dev.model)
for attr, value in dev.attributes.items():
# Skip unknown attributes
if attr not in known_attributes:
if attr not in dev.known_attributes:
continue
# Get thresholds
blocking_attribute = known_attributes[attr].get('Blocking', False)
err_thresh = known_attributes[attr].get('Error', None)
max_thresh = known_attributes[attr].get('Maximum', None)
blocking_attribute = dev.known_attributes[attr].get('Blocking', False)
err_thresh = dev.known_attributes[attr].get('Error', None)
max_thresh = dev.known_attributes[attr].get('Maximum', None)
if not max_thresh:
max_thresh = float('inf')
@ -89,7 +89,7 @@ def check_attributes(dev, only_blocking=False) -> bool:
continue
# Check attribute
if known_attributes[attr].get('PercentageLife', False):
if dev.known_attributes[attr].get('PercentageLife', False):
if 0 <= value['raw'] <= err_thresh:
attributes_ok = False
elif err_thresh <= value['raw'] < max_thresh:
@ -104,7 +104,7 @@ def enable_smart(dev) -> None:
cmd = [
'sudo',
'smartctl',
f'--device={"sat,auto" if dev.use_sat else "auto"}',
'--device=auto',
'--tolerance=permissive',
'--smart=on',
dev.path,
@ -112,21 +112,14 @@ def enable_smart(dev) -> None:
run_program(cmd, check=False)
def generate_attribute_report(dev) -> list[str]:
def generate_attribute_report(dev, only_failed=False) -> list[str]:
"""Generate attribute report, returns list."""
known_attributes = get_known_disk_attributes(dev.model)
report = []
for attr, value in sorted(dev.attributes.items()):
note = ''
value_color = 'GREEN'
# Skip attributes not in our list
if attr not in known_attributes:
if attr not in dev.known_attributes:
continue
# Check for attribute note
note = known_attributes[attr].get('Note', '')
# ID / Name
label = f'{attr:>3}'
if isinstance(attr, int):
@ -134,32 +127,17 @@ def generate_attribute_report(dev) -> list[str]:
label += f' / {str(hex(attr))[2:].upper():0>2}: {value["name"]}'
label = f' {label.replace("_", " "):38}'
# Value color
if known_attributes[attr].get('PercentageLife', False):
# PercentageLife values
if 0 <= value['raw'] <= known_attributes[attr]['Error']:
value_color = 'RED'
note = '(failed, % life remaining)'
elif value['raw'] < 0 or value['raw'] > 100:
value_color = 'PURPLE'
note = '(invalid?)'
else:
for threshold, color in ATTRIBUTE_COLORS:
threshold_val = known_attributes[attr].get(threshold, None)
if threshold_val and value['raw'] >= threshold_val:
value_color = color
if threshold == 'Error':
note = '(failed)'
elif threshold == 'Maximum':
note = '(invalid?)'
# Color & Note
value_color, note = get_attribute_value_color_and_note(dev, attr, value)
# 199/C7 warning
if str(attr) == '199' and value['raw'] > 0:
note = '(bad cable?)'
# Skip non-failing attributes if requested
## NOTE: This is a naive test and will include 'invalid' attributes
if only_failed and not note:
continue
# Build colored string and append to report
line = color_string(
[label, value['raw_str'], note],
line = ansi.color_string(
[label, get_attribute_value_string(dev, attr), note],
[None, value_color, 'YELLOW'],
)
report.append(line)
@ -168,24 +146,79 @@ def generate_attribute_report(dev) -> list[str]:
return report
def get_known_disk_attributes(model) -> dict[Any, dict]:
"""Get known NVMe/SMART attributes (model specific), returns dict."""
known_attributes = KNOWN_DISK_ATTRIBUTES.copy()
def get_attribute_value_color_and_note(dev, attr, value) -> tuple[str, str]:
"""Get attribute color and note based on SMART data."""
value_color = 'GREEN'
note = dev.known_attributes[attr].get('Note', '')
# Value value_color
if dev.known_attributes[attr].get('PercentageLife', False):
# PercentageLife values
if 0 <= value['raw'] <= dev.known_attributes[attr]['Error']:
value_color = 'RED'
note = '(failed, % life remaining)'
elif value['raw'] < 0 or value['raw'] > 100:
value_color = 'PURPLE'
note = '(invalid?)'
else:
for threshold, color in ATTRIBUTE_COLORS:
threshold_val = dev.known_attributes[attr].get(threshold, None)
if threshold_val and value['raw'] >= threshold_val:
value_color = color
if threshold == 'Error':
note = '(failed)'
elif threshold == 'Maximum':
note = '(invalid?)'
# 199/C7 warning
if str(attr) == '199' and value['raw'] > 0:
note = '(bad cable?)'
# Done
return (value_color, note)
def get_attribute_value_string(dev, attr) -> str:
"""Get attribute value string and report if it has changed."""
current_value = dev.attributes.get(attr, {})
initial_value = dev.initial_attributes.get(attr, {})
value_str = current_value.get('raw_str', '')
# Compare current value against initial value
if (
current_value.get('raw', None) is None
or initial_value.get('raw', None) is None
):
return value_str
if current_value['raw'] != initial_value['raw']:
value_str = (
f'{initial_value.get("raw_str", "?")} --> '
f'{current_value.get("raw_str", "?")}'
)
# Done
return value_str
def get_known_disk_attributes(model) -> dict[str | int, dict[str, Any]]:
"""Get known disk attributes based on the device model."""
known_attributes = copy.deepcopy(KNOWN_DISK_ATTRIBUTES)
# Apply model-specific data
for regex, data in KNOWN_DISK_MODELS.items():
if re.search(regex, model):
for attr, thresholds in data.items():
if attr in known_attributes:
known_attributes[attr].update(thresholds)
else:
known_attributes[attr] = thresholds
if not re.search(regex, model):
continue
for attr, thresholds in data.items():
if attr in known_attributes:
known_attributes[attr].update(thresholds)
else:
known_attributes[attr] = copy.deepcopy(thresholds)
# Done
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."""
details = {}
try:
@ -198,6 +231,33 @@ def get_smart_self_test_details(dev) -> dict[Any, Any]:
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:
"""Monitor SMART self-test status and update test_obj, returns bool."""
started = False
@ -229,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:
# Test didn't start within limit, stop waiting
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.set_status('TimedOut')
break
@ -244,6 +307,11 @@ def monitor_smart_self_test(test_obj, header_str, log_path) -> bool:
finished = True
break
# Check if timed out
if started and not finished:
test_obj.failed = True
test_obj.set_status('TimedOut')
# Done
return finished
@ -257,15 +325,15 @@ def run_self_test(test_obj, log_path) -> None:
run_smart_self_test(test_obj, log_path)
def run_smart_self_test(test_obj, log_path) -> bool:
"""Run SMART self-test and check if it passed, returns bool.
def run_smart_self_test(test_obj, log_path) -> None:
"""Run SMART self-test and check if it passed, returns None.
NOTE: An exception will be raised if the disk lacks SMART support.
"""
finished = False
test_details = get_smart_self_test_details(test_obj.dev)
size_str = bytes_to_string(test_obj.dev.size, use_binary=False)
header_str = color_string(
header_str = ansi.color_string(
['[', test_obj.dev.path.name, ' ', size_str, ']'],
[None, 'BLUE', None, 'CYAN', None],
sep='',
@ -315,11 +383,15 @@ def run_smart_self_test(test_obj, log_path) -> bool:
# Check result
if finished:
test_details = get_smart_self_test_details(test_obj.dev)
test_obj.passed = test_details.get('status', {}).get('passed', False)
test_obj.failed = test_obj.failed or not test_obj.passed
# 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')
elif test_obj.passed:
test_obj.set_status('Passed')
@ -380,11 +452,16 @@ def update_smart_details(dev) -> None:
"""Update SMART details via smartctl."""
updated_attributes = {}
# Bail if device was disconnected
if not dev.present:
dev.add_note('Device disconnected', 'RED')
return
# Get SMART data
cmd = [
'sudo',
'smartctl',
f'--device={"sat,auto" if dev.use_sat else "auto"}',
'--device=auto',
'--tolerance=verypermissive',
'--all',
'--json',
@ -429,6 +506,10 @@ def update_smart_details(dev) -> None:
if not updated_attributes:
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
dev.attributes.update(updated_attributes)

View file

@ -6,18 +6,16 @@ import logging
from subprocess import STDOUT
from wk.cfg.hw import (
BADBLOCKS_EXTRA_LARGE_DISK,
BADBLOCKS_LARGE_DISK,
BADBLOCKS_REGEX,
BADBLOCKS_RESULTS_REGEX,
BADBLOCKS_SKIP_REGEX,
TEST_MODE_BADBLOCKS_LIMIT,
)
from wk.exe import run_program
from wk.std import (
PLATFORM,
bytes_to_string,
color_string,
strip_colors,
)
from wk.std import PLATFORM, bytes_to_string
from wk.ui import ansi
# STATIC VARIABLES
@ -29,10 +27,15 @@ def check_surface_scan_results(test_obj, log_path) -> None:
"""Check results and set test status."""
with open(log_path, 'r', encoding='utf-8') as _f:
for line in _f.readlines():
line = strip_colors(line.strip())
line = ansi.strip_colors(line.strip())
if not line or BADBLOCKS_SKIP_REGEX.match(line):
# Skip
continue
# Clean line by removing backspaces/etc
line = BADBLOCKS_RESULTS_REGEX.sub(r'\1 \2', line)
# Add to report
match = BADBLOCKS_REGEX.search(line)
if match:
if all(s == '0' for s in match.groups()):
@ -41,10 +44,10 @@ def check_surface_scan_results(test_obj, log_path) -> None:
test_obj.set_status('Passed')
else:
test_obj.failed = True
test_obj.report.append(f' {color_string(line, "YELLOW")}')
test_obj.report.append(f' {ansi.color_string(line, "YELLOW")}')
test_obj.set_status('Failed')
else:
test_obj.report.append(f' {color_string(line, "YELLOW")}')
test_obj.report.append(f' {ansi.color_string(line, "YELLOW")}')
if not (test_obj.passed or test_obj.failed):
test_obj.set_status('Unknown')
@ -58,10 +61,12 @@ def run_scan(test_obj, log_path, test_mode=False) -> None:
# Use "RAW" disks under macOS
dev_path = dev_path.with_name(f'r{dev_path.name}')
LOG.info('Using %s for better performance', dev_path)
test_obj.report.append(color_string('badblocks', 'BLUE'))
test_obj.report.append(ansi.color_string('badblocks', 'BLUE'))
test_obj.set_status('Working')
# Increase block size if necessary
if dev.size >= BADBLOCKS_EXTRA_LARGE_DISK:
block_size = '8192'
if (dev.phy_sec == 4096
or dev.size >= BADBLOCKS_LARGE_DISK):
block_size = '4096'
@ -75,7 +80,7 @@ def run_scan(test_obj, log_path, test_mode=False) -> None:
with open(log_path, 'a', encoding='utf-8') as _f:
size_str = bytes_to_string(dev.size, use_binary=False)
_f.write(
color_string(
ansi.color_string(
['[', dev.path.name, ' ', size_str, ']\n'],
[None, 'BLUE', None, 'CYAN', None],
sep='',

View file

@ -9,22 +9,17 @@ from dataclasses import dataclass, field
from typing import Any
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.hw.test import Test
from wk.std import (
PLATFORM,
bytes_to_string,
color_string,
string_to_bytes,
)
from wk.std import PLATFORM, bytes_to_string, string_to_bytes
from wk.ui import ansi
# STATIC VARIABLES
LOG = logging.getLogger(__name__)
@dataclass(**DATACLASS_DECORATOR_KWARGS)
@dataclass(slots=True)
class System:
"""Object for tracking system specific hardware data."""
cpu_description: str = field(init=False)
@ -41,11 +36,11 @@ class System:
def generate_report(self) -> list[str]:
"""Generate CPU & RAM report, returns list."""
report = []
report.append(color_string('Device', 'BLUE'))
report.append(ansi.color_string('Device', 'BLUE'))
report.append(f' {self.cpu_description}')
# Include RAM details
report.append(color_string('RAM', 'BLUE'))
report.append(ansi.color_string('RAM', 'BLUE'))
report.append(f' {self.ram_total} ({", ".join(self.ram_dimms)})')
# Tests
@ -115,6 +110,7 @@ def get_ram_list_linux() -> list[list]:
cmd = ['sudo', 'dmidecode', '--type', 'memory']
dimm_list = []
manufacturer = 'Unknown'
part_number = 'Unknown'
size = 0
# Get DMI data
@ -125,8 +121,12 @@ def get_ram_list_linux() -> list[list]:
for line in dmi_data:
line = line.strip()
if line == 'Memory Device':
# Add to list
if size and (manufacturer or part_number):
dimm_list.append([size, manufacturer, part_number])
# Reset vars
manufacturer = 'Unknown'
part_number = 'Unknown'
size = 0
elif line.startswith('Size:'):
size = line.replace('Size: ', '')
@ -137,7 +137,18 @@ def get_ram_list_linux() -> list[list]:
size = 0
elif line.startswith('Manufacturer:'):
manufacturer = line.replace('Manufacturer: ', '')
dimm_list.append([size, manufacturer])
elif line.startswith('Part Number: '):
part_number = line.replace('Part Number: ', '')
# Add last DIMM
if size and (manufacturer or part_number):
dimm_list.append([size, manufacturer, part_number])
# Cleanup list
dimm_list = [
[dimm[0], dimm[1] if dimm[1] != 'Unknown' else dimm[2]]
for dimm in dimm_list
]
# Save details
return dimm_list

View file

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

View file

@ -13,7 +13,7 @@ LOG = logging.getLogger(__name__)
# Functions
def case_insensitive_path(path):
def case_insensitive_path(path: pathlib.Path | str) -> pathlib.Path:
"""Find path case-insensitively, returns pathlib.Path obj."""
given_path = pathlib.Path(path).resolve()
real_path = None
@ -37,12 +37,13 @@ def case_insensitive_path(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."""
path = pathlib.Path(path).resolve()
given_path = path.joinpath(item)
real_path = None
regex = fr'^{item}'
regex = fr'^{item}$'
# Quick check
if given_path.exists():
@ -61,7 +62,10 @@ def case_insensitive_search(path, item):
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."""
source = case_insensitive_path(source)
dest = pathlib.Path(dest).resolve()
@ -72,7 +76,7 @@ def copy_file(source, dest, overwrite=False):
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."""
LOG.debug('path: %s', path)
@ -89,7 +93,11 @@ def delete_empty_folders(path):
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.
NOTE: Exceptions are not caught by this function,
@ -106,7 +114,11 @@ def delete_folder(path, force=False, ignore_errors=False):
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.
NOTE: Exceptions are not caught by this function,
@ -124,7 +136,11 @@ def delete_item(path, force=False, ignore_errors=False):
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."""
path = pathlib.Path(path)
if expanduser:
@ -134,7 +150,7 @@ def get_path_obj(path, expanduser=True, resolve=True):
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."""
LOG.debug('path: %s', path)
path = pathlib.Path(path)
@ -163,7 +179,10 @@ def non_clobber_path(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.
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}')
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."""
path = pathlib.Path(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 os
import pathlib
import re
from wk.cfg.launchers import LAUNCHERS
@ -21,16 +22,8 @@ from wk.kit.tools import (
get_tool_path,
)
from wk.log import update_log_path
from wk.std import (
GenericError,
TryAndPrint,
clear_screen,
pause,
print_info,
print_success,
set_title,
sleep,
)
from wk.std import GenericError
from wk.ui import cli as ui
# STATIC VARIABLES
@ -52,7 +45,7 @@ WIDTH = 50
# Functions
def compress_cbin_dirs():
def compress_cbin_dirs() -> None:
"""Compress CBIN_DIR items using ARCHIVE_PASSWORD."""
current_dir = os.getcwd()
for item in CBIN_DIR.iterdir():
@ -70,25 +63,25 @@ def compress_cbin_dirs():
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(TMP_DIR.joinpath(item_path), force=True, ignore_errors=True)
def download_to_temp(filename, source_url):
def download_to_temp(filename, source_url, referer=None) -> pathlib.Path:
"""Download file to temp dir, returns pathlib.Path."""
out_path = TMP_DIR.joinpath(filename)
download_file(out_path, source_url)
download_file(out_path, source_url, referer=referer)
return out_path
def extract_to_bin(archive, folder):
def extract_to_bin(archive, folder) -> None:
"""Extract archive to folder under BIN_DIR."""
out_path = BIN_DIR.joinpath(folder)
extract_archive(archive, out_path)
def generate_launcher(section, name, options):
def generate_launcher(section, name, options) -> None:
"""Generate launcher script."""
dest = ROOT_DIR.joinpath(f'{section+"/" if section else ""}{name}.cmd')
out_text = []
@ -115,27 +108,27 @@ def generate_launcher(section, name, options):
# Download functions
def download_adobe_reader():
def download_adobe_reader() -> None:
"""Download Adobe Reader."""
out_path = INSTALLERS_DIR.joinpath('Adobe Reader DC.exe')
download_file(out_path, SOURCES['Adobe Reader DC'])
def download_aida64():
def download_aida64() -> None:
"""Download AIDA64."""
archive = download_to_temp('AIDA64.zip', SOURCES['AIDA64'])
extract_to_bin(archive, 'AIDA64')
delete_from_temp('AIDA64.zip')
def download_autoruns():
def download_autoruns() -> None:
"""Download Autoruns."""
for item in ('Autoruns32', 'Autoruns64'):
out_path = BIN_DIR.joinpath(f'Sysinternals/{item}.exe')
download_file(out_path, SOURCES[item])
def download_bleachbit():
def download_bleachbit() -> None:
"""Download BleachBit."""
out_path = BIN_DIR.joinpath('BleachBit')
archive = download_to_temp('BleachBit.zip', SOURCES['BleachBit'])
@ -150,7 +143,7 @@ def download_bleachbit():
delete_from_temp('BleachBit.zip')
def download_bluescreenview():
def download_bluescreenview() -> None:
"""Download BlueScreenView."""
archive_32 = download_to_temp(
'bluescreenview32.zip', SOURCES['BlueScreenView32'],
@ -169,14 +162,37 @@ def download_bluescreenview():
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."""
archive = download_to_temp('erunt.zip', SOURCES['ERUNT'])
extract_to_bin(archive, 'ERUNT')
delete_from_temp('erunt.zip')
def download_everything():
def download_everything() -> None:
"""Download Everything."""
archive_32 = download_to_temp('everything32.zip', SOURCES['Everything32'])
archive_64 = download_to_temp('everything64.zip', SOURCES['Everything64'])
@ -191,7 +207,7 @@ def download_everything():
delete_from_temp('everything64.zip')
def download_fastcopy():
def download_fastcopy() -> None:
"""Download FastCopy."""
installer = download_to_temp('FastCopyInstaller.exe', SOURCES['FastCopy'])
out_path = BIN_DIR.joinpath('FastCopy')
@ -207,9 +223,13 @@ def download_fastcopy():
delete_item(BIN_DIR.joinpath('FastCopy/setup.exe'))
def download_furmark():
def download_furmark() -> None:
"""Download FurMark."""
installer = download_to_temp('FurMark_Setup.exe', SOURCES['FurMark'])
installer = download_to_temp(
'FurMark_Setup.exe',
SOURCES['FurMark'],
referer=SOURCES['FurMark'],
)
out_path = BIN_DIR.joinpath('FurMark')
tmp_path = TMP_DIR.joinpath('FurMarkInstall')
run_program([installer, f'/DIR={tmp_path}', '/SILENT'])
@ -223,37 +243,32 @@ def download_furmark():
delete_from_temp('FurMarkInstall')
def download_hwinfo():
def download_hwinfo() -> None:
"""Download HWiNFO."""
archive = download_to_temp('HWiNFO.zip', SOURCES['HWiNFO'])
extract_to_bin(archive, 'HWiNFO')
delete_from_temp('HWiNFO.zip')
def download_iobit_uninstaller():
"""Download IOBit Uninstaller."""
installer = CBIN_DIR.joinpath('IObitUninstallerPortable.exe')
download_file(installer, SOURCES['IOBit Uninstaller'])
popen_program([installer])
sleep(1)
wait_for_procs('IObitUninstallerPortable.exe')
delete_item(installer)
def download_macs_fan_control():
def download_macs_fan_control() -> None:
"""Download Macs Fan Control."""
out_path = INSTALLERS_DIR.joinpath('Macs Fan Control.exe')
download_file(out_path, SOURCES['Macs Fan Control'])
def download_libreoffice():
def download_libreoffice() -> None:
"""Download LibreOffice."""
for arch in 32, 64:
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)
def download_neutron():
def download_neutron() -> None:
"""Download Neutron."""
archive = download_to_temp('neutron.zip', SOURCES['Neutron'])
out_path = BIN_DIR.joinpath('Neutron')
@ -261,7 +276,7 @@ def download_neutron():
delete_from_temp('neutron.zip')
def download_notepad_plus_plus():
def download_notepad_plus_plus() -> None:
"""Download Notepad++."""
archive = download_to_temp('npp.7z', SOURCES['Notepad++'])
extract_to_bin(archive, 'NotepadPlusPlus')
@ -273,21 +288,21 @@ def download_notepad_plus_plus():
delete_from_temp('npp.7z')
def download_openshell():
def download_openshell() -> None:
"""Download OpenShell installer and Fluent-Metro skin."""
for name in ('OpenShell.exe', 'Fluent-Metro.zip'):
out_path = BIN_DIR.joinpath(f'OpenShell/{name}')
download_file(out_path, SOURCES[name[:-4]])
def download_putty():
def download_putty() -> None:
"""Download PuTTY."""
archive = download_to_temp('putty.zip', SOURCES['PuTTY'])
extract_to_bin(archive, 'PuTTY')
delete_from_temp('putty.zip')
def download_snappy_driver_installer_origin():
def download_snappy_driver_installer_origin() -> None:
"""Download Snappy Driver Installer Origin."""
archive = download_to_temp('aria2.zip', SOURCES['Aria2'])
aria2c = TMP_DIR.joinpath('aria2/aria2c.exe')
@ -320,7 +335,7 @@ def download_snappy_driver_installer_origin():
cmd.append('-new_console:n')
cmd.append('-new_console:s33V')
popen_program(cmd, cwd=aria2c.parent)
sleep(1)
ui.sleep(1)
wait_for_procs('aria2c.exe')
else:
run_program(cmd)
@ -357,25 +372,14 @@ def download_snappy_driver_installer_origin():
delete_from_temp('fake.7z')
def download_testdisk():
"""Download TestDisk."""
archive = download_to_temp('testdisk_wip.zip', SOURCES['TestDisk'])
out_path = BIN_DIR.joinpath('TestDisk')
tmp_path = TMP_DIR.joinpath('TestDisk')
extract_archive(archive, tmp_path)
rename_item(tmp_path.joinpath('testdisk-7.2-WIP'), out_path)
delete_from_temp('TestDisk')
delete_from_temp('testdisk_wip.zip')
def download_wiztree():
def download_wiztree() -> None:
"""Download WizTree."""
archive = download_to_temp('wiztree.zip', SOURCES['WizTree'])
extract_to_bin(archive, 'WizTree')
delete_from_temp('wiztree.zip')
def download_xmplay():
def download_xmplay() -> None:
"""Download XMPlay."""
archives = [
download_to_temp('xmplay.zip', SOURCES['XMPlay']),
@ -391,7 +395,7 @@ def download_xmplay():
args = [archive, BIN_DIR.joinpath('XMPlay/plugins')]
if archive.name == 'Innocuous.zip':
args.append(
'Innocuous (v1.5)/Innocuous (Hue Shifted)/'
'Innocuous (v1.7)/Innocuous (Hue Shifted)/'
'Innocuous (Dark Skies - Purple-80) [L1].xmpskin'
)
extract_archive(*args, mode='e')
@ -403,7 +407,7 @@ def download_xmplay():
delete_from_temp('xmp-rar.zip')
delete_from_temp('Innocuous.zip')
def download_xmplay_music():
def download_xmplay_music() -> None:
"""Download XMPlay Music."""
music_tmp = TMP_DIR.joinpath('music')
music_tmp.mkdir(exist_ok=True)
@ -456,17 +460,17 @@ def download_xmplay_music():
# "Main" Function
def build_kit():
def build_kit() -> None:
"""Build Kit."""
update_log_path(dest_name='Build Tool', timestamp=True)
title = f'{KIT_NAME_FULL}: Build Tool'
clear_screen()
set_title(title)
print_info(title)
ui.clear_screen()
ui.set_title(title)
ui.print_info(title)
print('')
# Set up TryAndPrint
try_print = TryAndPrint()
try_print = ui.TryAndPrint()
try_print.width = WIDTH
try_print.verbose = True
for error in ('CalledProcessError', 'FileNotFoundError'):
@ -479,11 +483,12 @@ def build_kit():
try_print.run('BleachBit...', download_bleachbit)
try_print.run('BlueScreenView...', download_bluescreenview)
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('FastCopy...', download_fastcopy)
try_print.run('FurMark...', download_furmark)
try_print.run('HWiNFO...', download_hwinfo)
try_print.run('IOBit Uninstaller...', download_iobit_uninstaller)
try_print.run('LibreOffice...', download_libreoffice)
try_print.run('Macs Fan Control...', download_macs_fan_control)
try_print.run('Neutron...', download_neutron)
@ -491,22 +496,21 @@ def build_kit():
try_print.run('OpenShell...', download_openshell)
try_print.run('PuTTY...', download_putty)
try_print.run('Snappy Driver Installer...', download_snappy_driver_installer_origin)
try_print.run('TestDisk...', download_testdisk)
try_print.run('WizTree...', download_wiztree)
try_print.run('XMPlay...', download_xmplay)
try_print.run('XMPlay Music...', download_xmplay_music)
# Pause
print('', flush=True)
pause('Please review and press Enter to continue...')
ui.pause('Please review and press Enter to continue...')
# Compress .cbin
try_print.run('Compress cbin...', compress_cbin_dirs)
# Generate launcher scripts
print_success('Generating launchers')
ui.print_success('Generating launchers')
for section, launchers in sorted(LAUNCHERS.items()):
print_info(f' {section if section else "(Root)"}')
ui.print_info(f' {section if section else "(Root)"}')
for name, options in sorted(launchers.items()):
try_print.run(
f' {name}...', generate_launcher,
@ -516,7 +520,7 @@ def build_kit():
# Done
print('')
print('Done.')
pause('Press Enter to exit...')
ui.pause('Press Enter to exit...')
if __name__ == '__main__':

View file

@ -1,17 +1,19 @@
"""WizardKit: Tool Functions"""
# vim: sts=2 sw=2 ts=2
from datetime import datetime, timedelta
import logging
import pathlib
import platform
from datetime import datetime, timedelta
from subprocess import CompletedProcess, Popen
import requests
from wk.cfg.main import ARCHIVE_PASSWORD
from wk.cfg.sources import DOWNLOAD_FREQUENCY, SOURCES
from wk.exe import popen_program, run_program
from wk.std import GenericError
from wk.std import GenericError, sleep
# STATIC VARIABLES
@ -30,7 +32,9 @@ CACHED_DIRS = {}
# Functions
def download_file(out_path, source_url, as_new=False, overwrite=False):
def download_file(
out_path, source_url,
as_new=False, overwrite=False, referer=None) -> pathlib.Path:
"""Download a file using requests, returns pathlib.Path."""
out_path = pathlib.Path(out_path).resolve()
name = out_path.name
@ -38,6 +42,7 @@ def download_file(out_path, source_url, as_new=False, overwrite=False):
download_msg = f'Downloading {name}...'
if as_new:
out_path = out_path.with_suffix(f'{out_path.suffix}.new')
overwrite = True
print(download_msg, end='', flush=True)
# Avoid clobbering
@ -47,12 +52,34 @@ def download_file(out_path, source_url, as_new=False, overwrite=False):
# Create destination directory
out_path.parent.mkdir(parents=True, exist_ok=True)
# Update headers
headers = HEADERS.copy()
if referer:
headers['referer'] = referer
# Request download
with requests.Session() as session:
try:
response = session.get(source_url, headers=HEADERS, stream=True)
except requests.RequestException as _err:
download_failed = _err
response = session.get(
source_url,
allow_redirects=True,
headers=headers,
stream=True,
)
except requests.RequestException:
try:
sleep(1)
response = session.get(
source_url,
allow_redirects=True,
headers=headers,
stream=True,
)
except requests.RequestException as _err:
download_failed = _err
else:
if not response.ok:
download_failed = response
else:
if not response.ok:
download_failed = response
@ -72,7 +99,7 @@ def download_file(out_path, source_url, as_new=False, overwrite=False):
return out_path
def download_tool(folder, name, suffix=None):
def download_tool(folder, name, suffix=None) -> None:
"""Download tool."""
name_arch = f'{name}{ARCH}'
out_path = get_tool_path(folder, name, check=False, suffix=suffix)
@ -107,7 +134,7 @@ def download_tool(folder, name, suffix=None):
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."""
out_path = pathlib.Path(out_path).resolve()
out_path.parent.mkdir(parents=True, exist_ok=True)
@ -119,7 +146,7 @@ def extract_archive(archive, out_path, *args, mode='x', silent=True):
run_program(cmd)
def extract_tool(folder):
def extract_tool(folder) -> None:
"""Extract tool."""
extract_archive(
find_kit_dir('.cbin').joinpath(folder).with_suffix('.7z'),
@ -128,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.
Search is performed in the script's path and then recursively upwards.
@ -145,7 +172,7 @@ def find_kit_dir(name=None):
cur_path = cur_path.parent
# Check
if cur_path.match(cur_path.anchor):
if not cur_path.joinpath(search).exists():
raise FileNotFoundError(f'Failed to find kit dir, {name=}')
if name:
cur_path = cur_path.joinpath(name)
@ -155,7 +182,7 @@ def find_kit_dir(name=None):
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"""
bin_dir = find_kit_dir('.bin')
if not suffix:
@ -180,7 +207,7 @@ def run_tool(
folder, name, *run_args,
cbin=False, cwd=False, download=False, popen=False,
**run_kwargs,
):
) -> CompletedProcess | Popen:
"""Run tool from the kit or the Internet, returns proc obj.
proc will be either subprocess.CompletedProcess or subprocess.Popen."""

View file

@ -1,56 +1,33 @@
"""WizardKit: UFD Functions"""
# vim: sts=2 sw=2 ts=2
import argparse
import logging
import math
import os
import pathlib
import re
import shutil
from subprocess import CalledProcessError
from collections import OrderedDict
from docopt import docopt
from wk import io, log, std
from wk import io, log
from wk.cfg.main import KIT_NAME_FULL, KIT_NAME_SHORT
from wk.cfg.ufd import (
BOOT_ENTRIES,
BOOT_FILES,
IMAGE_BOOT_ENTRIES,
ITEMS,
ITEMS_FROM_LIVE,
ITEMS_HIDDEN,
SOURCES,
)
from wk.exe import get_json_from_command, run_program
from wk.os import linux
from wk.ui import cli as ui
# STATIC VARIABLES
DOCSTRING = '''WizardKit: Build UFD
Usage:
build-ufd [options] --ufd-device PATH
[--linux PATH]
[--linux-minimal 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
-m PATH, --linux-minimal 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__)
EXTRA_IMAGES_LIST = '/mnt/UFD/arch/extra_images.list'
MIB = 1024 ** 2
@ -59,7 +36,55 @@ UFD_LABEL = f'{KIT_NAME_SHORT}_UFD'
# 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."""
cmd = [
'sudo',
@ -89,17 +114,21 @@ def apply_image(part_path, image_path, hide_macos_boot=True):
linux.unmount(source_or_mountpoint='/mnt/TMP')
def build_ufd():
# pylint: disable=too-many-statements
def build_ufd() -> None:
"""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']:
log.enable_debug_mode()
if args['--update'] and args['EXTRA_IMAGES']:
std.print_warning('Extra images are ignored when updating')
ui.print_warning('Extra images are ignored when updating')
args['EXTRA_IMAGES'] = []
log.update_log_path(dest_name='build-ufd', timestamp=True)
try_print = std.TryAndPrint()
try_print = ui.TryAndPrint()
try_print.add_error('FileNotFoundError')
try_print.catch_all = False
try_print.indent = 2
@ -107,9 +136,9 @@ def build_ufd():
try_print.width = 64
# Show header
std.print_success(KIT_NAME_FULL)
std.print_warning('UFD Build Tool')
std.print_warning(' ')
ui.print_success(KIT_NAME_FULL)
ui.print_warning('UFD Build Tool')
ui.print_warning(' ')
# Verify selections
ufd_dev = verify_ufd(args['--ufd-device'])
@ -121,9 +150,9 @@ def build_ufd():
# Prep UFD
if not args['--update']:
std.print_info('Prep UFD')
ui.print_info('Prep UFD')
try_print.run(
message='Zeroing first 64MiB...',
message='Zeroing first 1MiB...',
function=zero_device,
dev_path=ufd_dev,
)
@ -146,6 +175,13 @@ def build_ufd():
dev_path=ufd_dev,
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)
# Mount UFD
@ -171,14 +207,25 @@ def build_ufd():
message='Removing Linux...',
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
std.print_standard(' ')
std.print_info('Copy Sources')
try_print.run(
'Copying Memtest86...', io.recursive_copy,
'/usr/share/memtest86-efi/', '/mnt/UFD/EFI/Memtest86/', overwrite=True,
)
ui.print_standard(' ')
ui.print_info('Copy Sources')
for s_label, s_path in sources.items():
try_print.run(
message=f'Copying {s_label}...',
@ -190,8 +237,8 @@ def build_ufd():
# Apply extra images
if not args['--update']:
std.print_standard(' ')
std.print_info('Apply Extra Images')
ui.print_standard(' ')
ui.print_info('Apply Extra Images')
for part_num, image_path in enumerate(extra_images):
try_print.run(
message=f'Applying {image_path.name}...',
@ -206,8 +253,8 @@ def build_ufd():
_f.write('\n'.join([image.name for image in extra_images]))
# Update boot entries
std.print_standard(' ')
std.print_info('Boot Setup')
ui.print_standard(' ')
ui.print_info('Boot Setup')
try_print.run(
message='Updating boot entries...',
function=update_boot_entries,
@ -238,8 +285,8 @@ def build_ufd():
)
# Hide items
std.print_standard(' ')
std.print_info('Final Touches')
ui.print_standard(' ')
ui.print_info('Final Touches')
try_print.run(
message='Hiding items...',
function=hide_items,
@ -248,35 +295,35 @@ def build_ufd():
)
# Done
std.print_standard('\nDone.')
ui.print_standard('\nDone.')
if not args['--force']:
std.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."""
if not std.ask('Is the above information correct?'):
std.abort()
if not ui.ask('Is the above information correct?'):
ui.abort()
# Safety check
if not update:
std.print_standard(' ')
std.print_warning('SAFETY CHECK')
std.print_standard(
ui.print_standard(' ')
ui.print_warning('SAFETY CHECK')
ui.print_standard(
'All data will be DELETED from the disk and partition(s) listed above.')
std.print_colored(
ui.print_colored(
['This is irreversible and will lead to', 'DATA LOSS'],
[None, 'RED'],
)
if not std.ask('Asking again to confirm, is this correct?'):
std.abort()
if not ui.ask('Asking again to confirm, is this correct?'):
ui.abort()
std.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."""
is_image = source.is_file()
is_image = not from_live and (source.is_file() or source.is_block_device())
items_not_found = False
# Mount source if necessary
@ -285,7 +332,14 @@ def copy_source(source, items, overwrite=False):
# Copy 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}'
try:
io.recursive_copy(i_source, i_dest, overwrite=overwrite)
@ -301,7 +355,7 @@ def copy_source(source, items, overwrite=False):
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."""
cmd = [
'sudo',
@ -331,7 +385,7 @@ def create_table(dev_path, use_mbr=False, images=None):
for part, real in zip(part_sizes, images):
end = start + real
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
@ -339,7 +393,7 @@ def create_table(dev_path, use_mbr=False, images=None):
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."""
cmd = [
'lsblk',
@ -358,7 +412,7 @@ def find_first_partition(dev_path):
return part_path
def format_partition(dev_path, label):
def format_partition(dev_path, label) -> None:
"""Format first partition on device FAT32."""
cmd = [
'sudo',
@ -370,7 +424,7 @@ def format_partition(dev_path, label):
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."""
cmd = [
'lsblk',
@ -389,7 +443,7 @@ def get_block_device_size(dev_path):
return int(proc.stdout.strip())
def get_uuid(path):
def get_uuid(path) -> str:
"""Get filesystem UUID via findmnt, returns str."""
cmd = [
'findmnt',
@ -405,7 +459,7 @@ def get_uuid(path):
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."""
with open('/root/.mtoolsrc', 'w', encoding='utf-8') as _f:
_f.write(f'drive U: file="{ufd_dev_first_partition}"\n')
@ -417,7 +471,18 @@ def hide_items(ufd_dev_first_partition, items):
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)."""
cmd = [
'sudo',
@ -430,7 +495,7 @@ def install_syslinux_to_dev(ufd_dev, use_mbr):
run_program(cmd)
def install_syslinux_to_partition(partition):
def install_syslinux_to_partition(partition) -> None:
"""Install Syslinux to UFD (partition)."""
cmd = [
'sudo',
@ -443,7 +508,7 @@ def install_syslinux_to_partition(partition):
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."""
valid_path = False
if path_type == 'DIR':
@ -454,13 +519,14 @@ def is_valid_path(path_obj, path_type):
valid_path = path_obj.is_file() and path_obj.suffix.lower() == '.img'
elif path_type == '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':
valid_path = path_obj.is_block_device()
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."""
cmd = [
'sudo',
@ -472,7 +538,7 @@ def set_boot_flag(dev_path, use_mbr=False):
run_program(cmd)
def remove_arch():
def remove_arch() -> None:
"""Remove arch dir from UFD.
This ensures a clean installation to the UFD and resets the boot files
@ -480,16 +546,16 @@ def remove_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."""
# Sources
std.print_info('Sources')
ui.print_info('Sources')
for label in ufd_sources.keys():
if label in sources:
std.print_standard(f' {label+":":<18} {sources[label]}')
ui.print_standard(f' {label+":":<18} {sources[label]}')
else:
std.print_colored(
ui.print_colored(
[f' {label+":":<18}', 'Not Specified'],
[None, 'YELLOW'],
)
@ -501,15 +567,15 @@ def show_selections(args, sources, ufd_dev, ufd_sources, extra_images):
print(f' {" ":<18} {image}')
# Destination
std.print_standard(' ')
std.print_info('Destination')
ui.print_standard(' ')
ui.print_info('Destination')
cmd = [
'lsblk', '--nodeps', '--noheadings', '--paths',
'--output', 'NAME,FSTYPE,TRAN,SIZE,VENDOR,MODEL,SERIAL',
ufd_dev,
]
proc = run_program(cmd, check=False)
std.print_standard(proc.stdout.strip())
ui.print_standard(proc.stdout.strip())
cmd = [
'lsblk', '--noheadings', '--paths',
'--output', 'NAME,SIZE,FSTYPE,LABEL,MOUNTPOINT',
@ -517,17 +583,17 @@ def show_selections(args, sources, ufd_dev, ufd_sources, extra_images):
]
proc = run_program(cmd, check=False)
for line in proc.stdout.splitlines()[1:]:
std.print_standard(line)
ui.print_standard(line)
# Notes
if args['--update']:
std.print_warning('Updating kit in-place')
ui.print_warning('Updating kit in-place')
elif args['--use-mbr']:
std.print_warning('Formatting using legacy MBR')
std.print_standard(' ')
ui.print_warning('Formatting using legacy MBR')
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"""
configs = []
uuids = [get_uuid('/mnt/UFD')]
@ -549,7 +615,7 @@ def update_boot_entries(ufd_dev, images=None):
'sed',
'--in-place',
'--regexp-extended',
f's#archisolabel={ISO_LABEL}#archisodevice=/dev/disk/by-uuid/{uuids[0]}#',
f's/___+/{uuids[0]}/',
*configs,
]
run_program(cmd)
@ -614,9 +680,9 @@ def update_boot_entries(ufd_dev, images=None):
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."""
sources = OrderedDict()
sources = {}
for label, data in ufd_sources.items():
s_path = args[data['Arg']]
@ -624,40 +690,41 @@ def verify_sources(args, ufd_sources):
try:
s_path_obj = io.case_insensitive_path(s_path)
except FileNotFoundError:
std.print_error(f'ERROR: {label} not found: {s_path}')
std.abort()
if not is_valid_path(s_path_obj, data['Type']):
std.print_error(f'ERROR: Invalid {label} source: {s_path}')
std.abort()
sources[label] = s_path_obj
ui.print_error(f'ERROR: {label} not found: {s_path}')
ui.abort()
else:
if not is_valid_path(s_path_obj, data['Type']):
ui.print_error(f'ERROR: Invalid {label} source: {s_path}')
ui.abort()
sources[label] = s_path_obj
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."""
ufd_dev = None
try:
ufd_dev = io.case_insensitive_path(dev_path)
except FileNotFoundError:
std.print_error(f'ERROR: UFD device not found: {dev_path}')
std.abort()
ui.print_error(f'ERROR: UFD device not found: {dev_path}')
ui.abort()
if not is_valid_path(ufd_dev, 'UFD'):
std.print_error(f'ERROR: Invalid UFD device: {ufd_dev}')
std.abort()
ui.print_error(f'ERROR: Invalid UFD device: {ufd_dev}')
ui.abort()
return ufd_dev
return ufd_dev # type: ignore[reportGeneralTypeIssues]
def zero_device(dev_path):
"""Zero-out first 64MB of device."""
def zero_device(dev_path) -> None:
"""Zero-out first 1MB of device."""
cmd = [
'sudo',
'dd',
'bs=4M',
'count=16',
'bs=1M',
'count=1',
'if=/dev/zero',
f'of={dev_path}',
]

View file

@ -26,7 +26,7 @@ DEFAULT_LOG_NAME = cfg.main.KIT_NAME_FULL
# Functions
def enable_debug_mode():
def enable_debug_mode() -> None:
"""Configures logging for better debugging."""
root_logger = logging.getLogger()
for handler in root_logger.handlers:
@ -39,13 +39,21 @@ def enable_debug_mode():
def format_log_path(
log_dir=None, log_name=None, timestamp=False,
kit=False, tool=False):
log_dir: pathlib.Path | str | None = None,
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."""
log_path = pathlib.Path(
f'{log_dir if log_dir else DEFAULT_LOG_DIR}/'
f'{cfg.main.KIT_NAME_FULL+"/" if kit 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'{"_" if timestamp else ""}'
f'{time.strftime("%Y-%m-%d_%H%M%S%z") if timestamp else ""}'
@ -54,28 +62,31 @@ def format_log_path(
log_path = log_path.resolve()
# Avoid clobbering
log_path = non_clobber_path(log_path)
if not append:
log_path = non_clobber_path(log_path)
# Done
return log_path
def get_root_logger_path():
"""Get path to log file from root logger, returns pathlib.Path obj."""
log_path = None
def get_root_logger_path() -> pathlib.Path:
"""Get the log filepath from the root logger, returns pathlib.Path obj.
NOTE: This will use the first handler baseFilename it finds (if any).
"""
root_logger = logging.getLogger()
# Check all handlers and use the first fileHandler found
# Check handlers
for handler in root_logger.handlers:
if isinstance(handler, logging.FileHandler):
log_path = pathlib.Path(handler.baseFilename).resolve()
break
if hasattr(handler, 'baseFilename'):
log_file = handler.baseFilename # type: ignore[reportGeneralTypeIssues]
return pathlib.Path(log_file).resolve()
# Done
return log_path
# No log file found
raise RuntimeError('Log path not found.')
def remove_empty_log(log_path=None):
def remove_empty_log(log_path: None | pathlib.Path = None) -> None:
"""Remove log if empty.
NOTE: Under Windows an empty log is 2 bytes long.
@ -98,7 +109,7 @@ def remove_empty_log(log_path=None):
log_path.unlink()
def start(config=None):
def start(config: dict[str, str] | None = None) -> None:
"""Configure and start logging using safe defaults."""
log_path = format_log_path(timestamp=os.name != 'nt')
root_logger = logging.getLogger()
@ -121,10 +132,15 @@ def start(config=None):
def update_log_path(
dest_dir=None, dest_name=None, keep_history=True, timestamp=True):
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."""
root_logger = logging.getLogger()
new_path = format_log_path(dest_dir, dest_name, timestamp=timestamp)
new_path = format_log_path(dest_dir, dest_name, timestamp=timestamp, append=append)
old_handler = None
old_path = get_root_logger_path()
os.makedirs(new_path.parent, exist_ok=True)
@ -147,10 +163,15 @@ def update_log_path(
new_handler = logging.FileHandler(new_path, mode='a')
new_handler.setFormatter(old_handler.formatter)
# Remove old_handler and log if empty
# Remove old_handler
root_logger.removeHandler(old_handler)
old_handler.close()
remove_empty_log(old_path)
# Delete orignal log if needed
if keep_history:
remove_empty_log(old_path)
else:
old_path.unlink()
# Add new handler
root_logger.addHandler(new_handler)

View file

@ -5,12 +5,16 @@ import os
import pathlib
import re
from subprocess import CompletedProcess
from typing import Any
import psutil
from wk.exe import get_json_from_command, run_program
from wk.std import PLATFORM, GenericError, show_data
from wk.std import PLATFORM, GenericError
from wk.cfg.net import BACKUP_SERVERS
from wk.ui import cli as ui
# REGEX
@ -22,7 +26,7 @@ REGEX_VALID_IP = re.compile(
# 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.
This checks for a valid private IP assigned to this system.
@ -48,12 +52,10 @@ def connected_to_private_network(raise_on_error=False):
raise GenericError('Not connected to a network')
# Done
if raise_on_error:
connected = None
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."""
report = []
for name, details in BACKUP_SERVERS.items():
@ -96,7 +98,10 @@ def mount_backup_shares(read_write=False):
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."""
cmd = None
address = details['Address']
@ -127,8 +132,8 @@ def mount_network_share(details, mount_point=None, read_write=False):
'-t', 'cifs',
'-o', (
f'{"rw" if read_write else "ro"}'
f',uid={os.getuid()}' # pylint: disable=no-member
f',gid={os.getgid()}' # pylint: disable=no-member
f',uid={os.getuid()}'
f',gid={os.getgid()}'
f',username={username}'
f',{"password=" if password else "guest"}{password}'
),
@ -147,7 +152,7 @@ def mount_network_share(details, mount_point=None, read_write=False):
return run_program(cmd, check=False)
def ping(addr='google.com'):
def ping(addr: str = 'google.com') -> None:
"""Attempt to ping addr."""
cmd = (
'ping',
@ -158,7 +163,7 @@ def ping(addr='google.com'):
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."""
mounted = False
@ -192,18 +197,20 @@ def share_is_mounted(details):
return mounted
def show_valid_addresses():
def show_valid_addresses() -> None:
"""Show all valid private IP addresses assigned to the system."""
# TODO: Refactor to remove ui dependancy
devs = psutil.net_if_addrs()
for dev, families in sorted(devs.items()):
for family in families:
if REGEX_VALID_IP.search(family.address):
# Valid IP found
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."""
# TODO: Refactor to use speedtest-cli's JSON output
cmd = ['speedtest-cli', '--simple']
proc = run_program(cmd, check=False)
output = [line.strip() for line in proc.stdout.splitlines() if line.strip()]
@ -212,7 +219,7 @@ def speedtest():
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."""
report = []
for name, details in BACKUP_SERVERS.items():
@ -241,7 +248,10 @@ def unmount_backup_shares():
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"""
cmd = []

View file

@ -10,7 +10,8 @@ import subprocess
from wk.cfg.hw import VOLUME_FAILURE_THRESHOLD, VOLUME_WARNING_THRESHOLD
from wk.exe import get_json_from_command, popen_program, run_program
from wk.log import format_log_path
from wk.std import bytes_to_string, color_string
from wk.std import bytes_to_string
from wk.ui import ansi
# STATIC VARIABLES
@ -19,12 +20,12 @@ UUID_CORESTORAGE = '53746f72-6167-11aa-aa11-00306543ecac'
# 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.
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."""
dev['name'] = f'{" "*indent}{dev["name"]}'
volumes = [dev]
@ -82,20 +83,20 @@ def build_volume_report(device_path=None) -> list:
vol['mountpoint'] = f'Mounted on {vol["mountpoint"]}'
# Name and size
line = color_string(
line = ansi.color_string(
[f'{vol["name"]:<20}', f'{vol["size"]:>9}'],
[None, 'CYAN'],
)
# Mountpoint and type
line = color_string(
line = ansi.color_string(
[line, f'{vol["mountpoint"]:<{m_width}}', f'{vol["fstype"]:<11}'],
[None, None, 'BLUE'],
)
# Used and free
if any([vol['fsused'], vol['fsavail']]):
line = color_string(
line = ansi.color_string(
[line, f'({vol["fsused"]:>9} used, {vol["fsavail"]:>9} free)'],
[None, size_color],
)
@ -107,7 +108,7 @@ def build_volume_report(device_path=None) -> list:
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."""
home = None
@ -128,7 +129,7 @@ def get_user_home(user):
return pathlib.Path(home)
def get_user_name():
def get_user_name() -> str:
"""Get real user name, returns str."""
user = None
@ -145,7 +146,7 @@ def get_user_name():
return user
def make_temp_file(suffix=None):
def make_temp_file(suffix=None) -> pathlib.Path:
"""Make temporary file, returns pathlib.Path() obj."""
cmd = ['mktemp']
if suffix:
@ -154,7 +155,7 @@ def make_temp_file(suffix=None):
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).
NOTE: If not running_as_root() then udevil will be used.
@ -177,14 +178,13 @@ def mount(source, mount_point=None, read_write=False):
raise RuntimeError(f'Failed to mount: {source} on {mount_point}')
def mount_volumes(device_path=None, read_write=False, scan_corestorage=False):
# pylint: disable=too-many-branches
def mount_volumes(device_path=None, read_write=False, scan_corestorage=False) -> None:
"""Mount all detected volumes.
NOTE: If device_path is specified then only volumes
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."""
volumes = [dev]
for child in dev.get('children', []):
@ -205,9 +205,13 @@ def mount_volumes(device_path=None, read_write=False, scan_corestorage=False):
cmd.append(device_path)
json_data = get_json_from_command(cmd)
# Bail if json_data is empty
if not json_data:
return
# Build list of volumes
for dev in _get_volumes(json_data.get('blockdevices', [{}])[0]):
volumes.append(dev)
for dev in json_data.get('blockdevices', [{}]):
volumes.extend(_get_volumes(dev))
if dev.get('parttype', '') == UUID_CORESTORAGE:
containers.append(dev['name'])
@ -229,12 +233,12 @@ def mount_volumes(device_path=None, read_write=False, scan_corestorage=False):
pass
def running_as_root():
def running_as_root() -> bool:
"""Check if running with effective UID of 0, returns bool."""
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."""
container_path = pathlib.Path(container)
detected_volumes = {}
@ -281,7 +285,7 @@ def scan_corestorage_container(container, timeout=300):
return inner_volumes
def unmount(source_or_mountpoint):
def unmount(source_or_mountpoint) -> None:
"""Unmount source_or_mountpoint.
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
def decode_smc_bytes(text):
def decode_smc_bytes(text) -> int:
"""Decode SMC bytes, returns int."""
result = None
@ -32,7 +32,7 @@ def decode_smc_bytes(text):
return result
def set_fans(mode):
def set_fans(mode) -> None:
"""Set fans to auto or max."""
if mode == 'auto':
set_fans_auto()
@ -42,14 +42,14 @@ def set_fans(mode):
raise RuntimeError(f'Invalid fan mode: {mode}')
def set_fans_auto():
def set_fans_auto() -> None:
"""Set fans to auto."""
LOG.info('Setting fans to auto')
cmd = ['sudo', 'smc', '-k', 'FS! ', '-w', '0000']
run_program(cmd)
def set_fans_max():
def set_fans_max() -> None:
"""Set fans to their max speeds."""
LOG.info('Setting fans to max')
num_fans = 0

View file

@ -8,6 +8,8 @@ import pathlib
import platform
from contextlib import suppress
from typing import Any
import psutil
try:
@ -23,15 +25,16 @@ from wk.cfg.windows_builds import (
OUTDATED_BUILD_NUMBERS,
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.std import (
GenericError,
GenericWarning,
bytes_to_string,
color_string,
sleep,
)
from wk.ui import cli as ui
from wk.ui import ansi
# STATIC VARIABLES
@ -67,16 +70,27 @@ KNOWN_HIVE_NAMES = {
winreg.HKEY_LOCAL_MACHINE: 'HKLM',
winreg.HKEY_USERS: 'HKU',
}
OS_VERSION = platform.win32_ver()[0]
OS_VERSION = 8.1 if OS_VERSION == '8.1' else int(OS_VERSION)
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
REG_MSISERVER = r'HKLM\SYSTEM\CurrentControlSet\Control\SafeBoot\Network\MSIServer'
SLMGR = pathlib.Path(f'{os.environ.get("SYSTEMROOT")}/System32/slmgr.vbs')
SYSTEMDRIVE = os.environ.get('SYSTEMDRIVE')
# STATIC OS VARIABLES
WIN32_VER = platform.win32_ver()
OS_BUILD_VERSION_FULL = WIN32_VER[1]
OS_BUILD_VERSION = int(OS_BUILD_VERSION_FULL.split('.')[2])
PLATFORM_VER = WIN32_VER[0]
if PLATFORM_VER == '8.1':
OS_VERSION = 8.1
elif OS_BUILD_VERSION >= 22000:
OS_VERSION = 11
else:
OS_VERSION = int(PLATFORM_VER)
# Activation Functions
def activate_with_bios():
def activate_with_bios() -> None:
"""Attempt to activate Windows with a key stored in the BIOS."""
# Code borrowed from https://github.com/aeruder/get_win8key
#####################################################
@ -116,7 +130,7 @@ def activate_with_bios():
raise GenericError('Activation Failed')
def get_activation_string():
def get_activation_string() -> str:
"""Get activation status, returns str."""
cmd = ['cscript', '//nologo', SLMGR, '/xpr']
proc = run_program(cmd, check=False)
@ -126,7 +140,7 @@ def get_activation_string():
return act_str
def is_activated():
def is_activated() -> bool:
"""Check if Windows is activated via slmgr.vbs and return bool."""
act_str = get_activation_string()
@ -135,80 +149,122 @@ def is_activated():
# Date / Time functions
def get_timezone():
def get_timezone() -> str:
"""Get current timezone using tzutil, returns str."""
cmd = ['tzutil', '/g']
proc = run_program(cmd, check=False)
return proc.stdout
def set_timezone(zone):
def set_timezone(zone) -> None:
"""Set current timezone using tzutil."""
cmd = ['tzutil', '/s', zone]
run_program(cmd, check=False)
# Info Functions
def check_4k_alignment(show_alert=False):
"""Check if all partitions are 4K aligned, returns book."""
cmd = ['WMIC', 'partition', 'get', 'StartingOffset']
def check_4k_alignment(show_alert=False) -> list[str]:
"""Check if all partitions are 4K aligned, returns list."""
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 = []
show_alert = False
# Check offsets
proc = run_program(cmd)
for offset in proc.stdout.splitlines():
offset = offset.strip()
if not offset.isnumeric():
continue
if int(offset) % 4096 != 0:
# Not aligned
if show_alert:
show_alert_box('One or more partitions are not 4K aligned')
raise GenericError('One or more partitions are not 4K aligned')
for part in json_data:
if part['StartingOffset'] % 4096 != 0:
report.append(
ansi.color_string(
f'{part["Name"]}'
f' ({bytes_to_string(part["Size"], decimals=1)})'
,
'RED'
)
)
def get_installed_antivirus():
"""Get list of installed antivirus programs, returns list."""
cmd = [
'WMIC', r'/namespace:\\root\SecurityCenter2',
'path', 'AntivirusProduct',
'get', 'displayName', '/value',
]
products = []
report = []
# Get list of products
proc = run_program(cmd)
for line in proc.stdout.splitlines():
line = line.strip()
if '=' in line:
products.append(line.split('=')[1])
# Check product(s) status
for product in sorted(products):
cmd = [
'WMIC', r'/namespace:\\root\SecurityCenter2',
'path', 'AntivirusProduct',
'where', f'displayName="{product}"',
'get', 'productState', '/value',
]
proc = run_program(cmd)
state = proc.stdout.split('=')[1]
state = hex(int(state))
if str(state)[3:5] not in ['10', '11']:
report.append(color_string(f'[Disabled] {product}', 'YELLOW'))
else:
report.append(product)
# Final check
if not report:
report.append(color_string('No products detected', 'RED'))
# Show alert
if show_alert:
show_alert_box('One or more partitions not 4K aligned')
# Done
if report:
report.insert(
0,
ansi.color_string('One or more partitions not 4K aligned', 'YELLOW'),
)
report.sort()
return report
def get_installed_ram(as_list=False, raise_exceptions=False):
"""Get installed RAM."""
def export_bitlocker_info() -> None:
"""Get Bitlocker info and save to the base directory of the kit."""
commands = [
['manage-bde', '-status', SYSTEMDRIVE],
['manage-bde', '-protectors', '-get', SYSTEMDRIVE],
]
# Get filename
file_name = ui.input_text(prompt_msg='Enter filename')
file_path = pathlib.Path(f'../../Bitlocker_{file_name}.txt').resolve()
# Save info
with open(file_path, 'a', encoding='utf-8') as _f:
for cmd in commands:
proc = run_program(cmd, check=False)
_f.write(f'{proc.stdout}\n\n')
def get_installed_antivirus() -> dict[str, dict]:
"""Get installed antivirus products and their status, returns dict."""
script_path = find_kit_dir('Scripts').joinpath('check_av.ps1')
cmd = ['PowerShell', '-ExecutionPolicy', 'Bypass', '-File', script_path]
json_data = get_json_from_command(cmd)
products = {}
# Check state and build dict
for p in json_data:
name = p['displayName']
state = p['productState']
enabled = ((state>>8) & 0x11) in (0x10, 0x11) # middle two hex digits
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
for name, details in products.items():
if details['Enabled']:
if details['Outdated']:
products_active.append(ansi.color_string(f'{name} [OUTDATED]', 'YELLOW'))
else:
products_active.append(name)
else:
# Disabled
products_inactive.append(ansi.color_string(f'[Disabled] {name}', 'YELLOW'))
# Final check
if not (products_active or products_inactive):
products_inactive.append(ansi.color_string('No products detected', 'RED'))
# Done
products_active.sort()
products_inactive.sort()
return products_active + products_inactive
def get_installed_ram(as_list=False, raise_exceptions=False) -> list | str:
"""Get installed RAM, returns list or str."""
mem = psutil.virtual_memory()
mem_str = bytes_to_string(mem.total, decimals=1)
@ -223,8 +279,8 @@ def get_installed_ram(as_list=False, raise_exceptions=False):
return [mem_str] if as_list else mem_str
def get_os_activation(as_list=False, check=True):
"""Get OS activation status, returns str.
def get_os_activation(as_list=False, check=True) -> list | str:
"""Get OS activation status, returns list or str.
NOTE: If check=True then raise an exception if OS isn't activated.
"""
@ -240,33 +296,33 @@ def get_os_activation(as_list=False, check=True):
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.
NOTE: If check=True then an exception is raised if the OS version is
outdated or unsupported.
"""
key = r'SOFTWARE\Microsoft\Windows NT\CurrentVersion'
build_version = int(reg_read_value("HKLM", key, "CurrentBuild"))
build_version_full = platform.win32_ver()[1]
details = WINDOWS_BUILDS.get(build_version_full, f'Build {build_version}')
details = WINDOWS_BUILDS.get(OS_BUILD_VERSION_FULL, f'Build {OS_BUILD_VERSION}')
display_name = (
f'{reg_read_value("HKLM", key, "ProductName")} {ARCH}-bit {details}'
)
if OS_BUILD_VERSION >= 22000:
display_name = display_name.replace('Windows 10', 'Windows 11')
# Check for support issues
if check:
if build_version in OUTDATED_BUILD_NUMBERS:
if OS_BUILD_VERSION in OUTDATED_BUILD_NUMBERS:
raise GenericWarning(f'{display_name} (outdated)')
if build_version < OLDEST_SUPPORTED_BUILD:
if OS_BUILD_VERSION < OLDEST_SUPPORTED_BUILD:
raise GenericError(f'{display_name} (unsupported)')
# Done
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."""
script_path = find_kit_dir('Scripts').joinpath('get_raw_disks.ps1')
cmd = ['PowerShell', '-ExecutionPolicy', 'Bypass', '-File', script_path]
@ -291,7 +347,7 @@ def get_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."""
report = []
for disk in psutil.disk_partitions():
@ -308,14 +364,14 @@ def get_volume_usage(use_colors=False):
f' ({bytes_to_string(free, 2):>10} / {bytes_to_string(total, 2):>10})'
)
if use_colors:
display_str = color_string(display_str, color)
display_str = ansi.color_string(display_str, color)
report.append(f'{disk.device} {display_str}')
# Done
return report
def show_alert_box(message, title=None):
def show_alert_box(message, title=None) -> None:
"""Show Windows alert box with message."""
title = title if title else f'{KIT_NAME_FULL} Warning'
message_box = ctypes.windll.user32.MessageBoxW
@ -323,8 +379,7 @@ def show_alert_box(message, title=None):
# Registry Functions
def reg_delete_key(hive, key, recurse=False):
# pylint: disable=raise-missing-from
def reg_delete_key(hive, key, recurse=False) -> None:
"""Delete a key from the registry.
NOTE: If recurse is False then it will only work on empty keys.
@ -346,7 +401,7 @@ def reg_delete_key(hive, key, recurse=False):
except FileNotFoundError:
# Ignore
pass
except PermissionError:
except PermissionError as _e:
LOG.error(r'Failed to delete registry key: %s\%s', hive_name, key)
if recurse:
# Re-raise exception
@ -354,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
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."""
access = winreg.KEY_ALL_ACCESS
hive = reg_get_hive(hive)
@ -381,8 +436,9 @@ def reg_delete_value(hive, key, value):
raise
def reg_get_hive(hive):
def reg_get_hive(hive) -> Any:
"""Get winreg HKEY constant from string, returns HKEY constant."""
# TODO: Fix type hint
if isinstance(hive, int):
# Assuming we're already a winreg HKEY constant
pass
@ -393,8 +449,9 @@ def reg_get_hive(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."""
# TODO: Fix type hint
if isinstance(data_type, int):
# Assuming we're already a winreg value type constant
pass
@ -405,7 +462,7 @@ def reg_get_data_type(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."""
exists = False
hive = reg_get_hive(hive)
@ -423,7 +480,7 @@ def reg_key_exists(hive, key):
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.
NOTE: Set value='' to read the default value.
@ -447,7 +504,7 @@ def reg_read_value(hive, key, value, force_32=False, force_64=False):
return data
def reg_write_settings(settings):
def reg_write_settings(settings) -> None:
"""Set registry values in bulk from a custom data structure.
Data structure should be as follows:
@ -487,8 +544,7 @@ def reg_write_settings(settings):
reg_set_value(hive, key, *value)
def reg_set_value(hive, key, name, data, data_type, option=None):
# pylint: disable=too-many-arguments
def reg_set_value(hive, key, name, data, data_type, option=None) -> None:
"""Set value for hive/key."""
access = winreg.KEY_WRITE
data_type = reg_get_data_type(data_type)
@ -520,25 +576,25 @@ def reg_set_value(hive, key, name, data, data_type, option=None):
# Safe Mode Functions
def disable_safemode():
def disable_safemode() -> None:
"""Edit BCD to remove safeboot value."""
cmd = ['bcdedit', '/deletevalue', '{default}', 'safeboot']
run_program(cmd)
def disable_safemode_msi():
def disable_safemode_msi() -> None:
"""Disable MSI access under safemode."""
cmd = ['reg', 'delete', REG_MSISERVER, '/f']
run_program(cmd)
def enable_safemode():
def enable_safemode() -> None:
"""Edit BCD to set safeboot as default."""
cmd = ['bcdedit', '/set', '{default}', 'safeboot', 'network']
run_program(cmd)
def enable_safemode_msi():
def enable_safemode_msi() -> None:
"""Enable MSI access under safemode."""
cmd = ['reg', 'add', REG_MSISERVER, '/f']
run_program(cmd)
@ -551,7 +607,7 @@ def enable_safemode_msi():
# Secure Boot Functions
def is_booted_uefi():
def is_booted_uefi() -> bool:
"""Check if booted UEFI or legacy, returns bool."""
kernel = ctypes.windll.kernel32
firmware_type = ctypes.c_uint()
@ -559,7 +615,7 @@ def is_booted_uefi():
# Get value from kernel32 API (firmware_type is updated by the call)
try:
kernel.GetFirmwareType(ctypes.byref(firmware_type))
except Exception: # pylint: disable=broad-except
except Exception:
# Ignore and set firmware_type back to zero
firmware_type = ctypes.c_uint(0)
@ -567,7 +623,7 @@ def is_booted_uefi():
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.
If raise_exceptions is True then an exception is raised with details.
@ -617,7 +673,7 @@ def is_secure_boot_enabled(raise_exceptions=False, show_alert=False):
# Service Functions
def disable_service(service_name):
def disable_service(service_name) -> None:
"""Set service startup to disabled."""
cmd = ['sc', 'config', service_name, 'start=', 'disabled']
run_program(cmd, check=False)
@ -627,7 +683,7 @@ def 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."""
cmd = ['sc', 'config', service_name, 'start=', start_type]
psutil_type = 'automatic'
@ -642,7 +698,7 @@ def enable_service(service_name, start_type='auto'):
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."""
status = 'unknown'
try:
@ -654,7 +710,7 @@ def get_service_status(service_name):
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."""
start_type = 'unknown'
try:
@ -666,17 +722,17 @@ def get_service_start_type(service_name):
return start_type
def start_service(service_name):
def start_service(service_name) -> None:
"""Stop service."""
cmd = ['net', 'start', service_name]
run_program(cmd, check=False)
# Verify service was started
if not get_service_status(service_name) in ('running', 'start_pending'):
if get_service_status(service_name) not in ('running', 'start_pending'):
raise GenericError(f'Failed to start service {service_name}')
def stop_service(service_name):
def stop_service(service_name) -> None:
"""Stop service."""
cmd = ['net', 'stop', service_name]
run_program(cmd, check=False)
@ -686,5 +742,62 @@ def 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__':
print("This file is not meant to be called directly.")

File diff suppressed because it is too large Load diff

View file

@ -1,5 +1,4 @@
"""WizardKit: Setup - Windows"""
# pylint: disable=too-many-lines
# vim: sts=2 sw=2 ts=2
import configparser
@ -9,21 +8,24 @@ import os
import re
import sys
from typing import Any
from wk.cfg.main import KIT_NAME_FULL
from wk.cfg.setup import (
BROWSER_PATHS,
DISABLED_ENTRIES_WINDOWS_11,
LIBREOFFICE_XCU_DATA,
REG_CHROME_UBLOCK_ORIGIN,
REG_WINDOWS_EXPLORER,
REG_OPEN_SHELL_SETTINGS,
REG_OPEN_SHELL_LOW_POWER_IDLE,
REG_WINDOWS_BSOD_MINIDUMPS,
UBLOCK_ORIGIN_URLS,
)
from wk.exe import kill_procs, run_program, popen_program
from wk.io import case_insensitive_path, get_path_obj
from wk.kit.tools import (
ARCH,
download_tool,
extract_archive,
extract_tool,
find_kit_dir,
@ -35,16 +37,21 @@ from wk.os.win import (
OS_VERSION,
activate_with_bios,
check_4k_alignment,
get_installed_antivirus,
get_installed_ram,
get_os_activation,
get_os_name,
get_raw_disks,
get_service_status,
get_volume_usage,
is_activated,
is_secure_boot_enabled,
list_installed_antivirus,
reg_set_value,
reg_write_settings,
stop_service,
winget_check,
winget_import,
winget_upgrade,
)
from wk.repairs.win import (
WIDTH,
@ -60,22 +67,10 @@ from wk.repairs.win import (
from wk.std import (
GenericError,
GenericWarning,
Menu,
TryAndPrint,
abort,
ask,
clear_screen,
color_string,
pause,
print_error,
print_info,
print_standard,
print_warning,
set_title,
show_data,
sleep,
strip_colors,
)
from wk.ui import cli as ui
from wk.ui import ansi
# STATIC VARIABLES
@ -91,7 +86,7 @@ KNOWN_ENCODINGS = (
'utf-32-le',
)
IN_CONEMU = 'ConEmuPID' in os.environ
MENU_PRESETS = Menu()
MENU_PRESETS = ui.Menu()
PROGRAMFILES_32 = os.environ.get(
'PROGRAMFILES(X86)', os.environ.get(
'PROGRAMFILES', r'C:\Program Files (x86)',
@ -103,7 +98,7 @@ PROGRAMFILES_64 = os.environ.get(
),
)
SYSTEMDRIVE = os.environ.get('SYSTEMDRIVE', 'C:')
TRY_PRINT = TryAndPrint()
TRY_PRINT = ui.TryAndPrint()
TRY_PRINT.width = WIDTH
TRY_PRINT.verbose = True
for error in ('CalledProcessError', 'FileNotFoundError'):
@ -111,10 +106,10 @@ for error in ('CalledProcessError', 'FileNotFoundError'):
# Auto Setup
def build_menus(base_menus, title, presets):
def build_menus(base_menus, title, presets) -> dict[str, ui.Menu]:
"""Build menus, returns dict."""
menus = {}
menus['Main'] = Menu(title=f'{title}\n{color_string("Main Menu", "GREEN")}')
menus['Main'] = ui.Menu(title=f'{title}\n{ansi.color_string("Main Menu", "GREEN")}')
# Main Menu
for entry in base_menus['Actions']:
@ -124,7 +119,7 @@ def build_menus(base_menus, title, presets):
# Run groups
for group, entries in base_menus['Groups'].items():
menus[group] = Menu(title=f'{title}\n{color_string(group, "GREEN")}')
menus[group] = ui.Menu(title=f'{title}\n{ansi.color_string(group, "GREEN")}')
for entry in entries:
menus[group].add_option(entry.name, entry.details)
menus[group].add_action('All')
@ -153,7 +148,7 @@ def build_menus(base_menus, title, presets):
)
# Update presets Menu
MENU_PRESETS.title = f'{title}\n{color_string("Load Preset", "GREEN")}'
MENU_PRESETS.title = f'{title}\n{ansi.color_string("Load Preset", "GREEN")}'
MENU_PRESETS.add_option('Default')
for name in presets:
MENU_PRESETS.add_option(name)
@ -166,33 +161,33 @@ def build_menus(base_menus, title, presets):
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."""
color = None
os_name = get_os_name(check=False)
print_standard(f'Operating System: {os_name}')
ui.print_standard(f'Operating System: {os_name}')
# Check support status and set color
try:
get_os_name()
except GenericWarning:
# Outdated version
print_warning('OS version is outdated, updating is recommended.')
if not ask('Continue anyway?'):
abort()
ui.print_warning('OS version is outdated, updating is recommended.')
if not ui.ask('Continue anyway?'):
ui.abort()
color = 'YELLOW'
except GenericError:
# Unsupported version
print_error('OS version is unsupported, updating is recommended.')
if not ask('Continue anyway? (NOT RECOMMENDED)'):
abort()
ui.print_error('OS version is unsupported, updating is recommended.')
if not ui.ask('Continue anyway? (NOT RECOMMENDED)'):
ui.abort()
color = 'RED'
# Done
return f'{title} ({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)."""
if not enable_menu_exit:
MENU_PRESETS.actions['Main Menu'].update({'Disabled':True, 'Hidden':True})
@ -215,25 +210,31 @@ def load_preset(menus, presets, title, enable_menu_exit=True):
menu.options[name]['Selected'] = value
# Ask selection question(s)
clear_screen()
print_standard(f'{title}')
ui.clear_screen()
ui.print_standard(f'{title}')
print('')
if selection[0] == 'Default' and ask('Install LibreOffice?'):
if selection[0] == 'Default' and ui.ask('Install LibreOffice?'):
menus['Install Software'].options['LibreOffice']['Selected'] = True
# Re-enable Main Menu action if disabled
MENU_PRESETS.actions['Main Menu'].update({'Disabled':False, 'Hidden':False})
# Disable entries incompatible with Windows 11
if OS_VERSION == 11:
for group_name, entry_name in DISABLED_ENTRIES_WINDOWS_11.items():
menus[group_name].options[entry_name]['Disabled'] = True
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."""
update_log_path(dest_name='Auto Setup', timestamp=True)
title = f'{KIT_NAME_FULL}: Auto Setup'
clear_screen()
set_title(title)
print_info(title)
ui.clear_screen()
ui.set_title(title)
ui.print_info(title)
print('')
print_standard('Initializing...')
ui.print_standard('Initializing...')
# Check OS and update title for menus
title = check_os_and_set_menu_title(title)
@ -248,10 +249,10 @@ def run_auto_setup(base_menus, presets):
show_main_menu(base_menus, menus, presets, title)
# Start setup
clear_screen()
print_standard(title)
ui.clear_screen()
ui.print_standard(title)
print('')
print_info('Running setup')
ui.print_info('Running setup')
# Run setup
for group, menu in menus.items():
@ -260,29 +261,29 @@ def run_auto_setup(base_menus, presets):
try:
run_group(group, menu)
except KeyboardInterrupt:
abort()
ui.abort()
# Done
print_info('Done')
pause('Press Enter to exit...')
ui.print_info('Done')
ui.pause('Press Enter to exit...')
def run_group(group, menu):
def run_group(group, menu) -> None:
"""Run entries in group if appropriate."""
print_info(f' {group}')
ui.print_info(f' {group}')
for name, details in menu.options.items():
name_str = strip_colors(name)
name_str = ansi.strip_colors(name)
# Not selected
if not details.get('Selected', False):
show_data(f'{name_str}...', 'Skipped', 'YELLOW', width=WIDTH)
ui.show_data(f'{name_str}...', 'Skipped', 'YELLOW', width=WIDTH)
continue
# Selected
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."""
while True:
update_main_menu(menus)
@ -297,7 +298,7 @@ def show_main_menu(base_menus, menus, presets, title):
raise SystemExit
def show_sub_menu(menu):
def show_sub_menu(menu) -> None:
"""Show sub-menu and handle sub-menu actions."""
while True:
selection = menu.advanced_select()
@ -313,7 +314,7 @@ def show_sub_menu(menu):
menu.options[name]['Selected'] = value
def update_main_menu(menus):
def update_main_menu(menus) -> None:
"""Update main menu based on current selections."""
index = 1
skip = 'Reboot'
@ -332,37 +333,37 @@ def update_main_menu(menus):
# Auto Repairs: Wrapper Functions
def auto_backup_registry():
def auto_backup_registry() -> None:
"""Backup registry."""
TRY_PRINT.run('Backup Registry...', backup_registry)
def auto_backup_browser_profiles():
def auto_backup_browser_profiles() -> None:
"""Backup browser profiles."""
backup_all_browser_profiles(use_try_print=True)
def auto_backup_power_plans():
def auto_backup_power_plans() -> None:
"""Backup 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."""
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."""
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."""
TRY_PRINT.run('Enable BSoD mini dumps...', enable_bsod_minidumps)
def auto_enable_regback():
def auto_enable_regback() -> None:
"""Enable RegBack."""
TRY_PRINT.run(
'Enable RegBack...', reg_set_value, 'HKLM',
@ -371,7 +372,7 @@ def auto_enable_regback():
)
def auto_system_restore_enable():
def auto_system_restore_enable() -> None:
"""Enable System Restore."""
cmd = [
'powershell', '-Command', 'Enable-ComputerRestore',
@ -380,28 +381,28 @@ def auto_system_restore_enable():
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."""
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."""
TRY_PRINT.run('Create System Restore...', create_system_restore_point)
def auto_windows_updates_enable():
def auto_windows_updates_enable() -> None:
"""Enable Windows Updates."""
TRY_PRINT.run('Enable Windows Updates...', enable_windows_updates)
# Auto Setup: Wrapper Functions
def auto_activate_windows():
def auto_activate_windows() -> None:
"""Attempt to activate Windows using BIOS key."""
TRY_PRINT.run('Windows Activation...', activate_with_bios)
def auto_config_browsers():
def auto_config_browsers() -> None:
"""Configure Browsers."""
prompt = ' Press Enter to continue...'
TRY_PRINT.run('Chrome Notifications...', disable_chrome_notifications)
@ -412,33 +413,38 @@ def auto_config_browsers():
'Set default browser...', set_default_browser, msg_good='STARTED',
)
print(prompt, end='', flush=True)
pause('')
ui.pause(' ')
# Move cursor to beginning of the previous line and clear prompt
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."""
TRY_PRINT.run('Windows Explorer...', config_explorer)
def auto_config_open_shell():
def auto_config_open_shell() -> None:
"""Configure 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."""
TRY_PRINT.run('AIDA64 Report...', export_aida64_report)
def auto_install_firefox():
def auto_install_firefox() -> None:
"""Install Firefox."""
TRY_PRINT.run('Firefox...', install_firefox)
def auto_install_libreoffice():
def auto_install_libreoffice() -> None:
"""Install LibreOffice.
NOTE: It is assumed that auto_install_vcredists() will be run
@ -447,100 +453,120 @@ def auto_install_libreoffice():
TRY_PRINT.run('LibreOffice...', install_libreoffice, vcredist=False)
def auto_install_open_shell():
def auto_install_open_shell() -> None:
"""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."""
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."""
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."""
TRY_PRINT.run('Device Manager...', open_device_manager)
def auto_open_hwinfo_sensors():
def auto_open_hwinfo_sensors() -> None:
"""Open HWiNFO Sensors."""
TRY_PRINT.run('HWiNFO Sensors...', open_hwinfo_sensors)
def auto_open_windows_activation():
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."""
TRY_PRINT.run('Snappy Driver Installer...', open_snappy_driver_installer_origin)
def auto_open_windows_activation() -> None:
"""Open Windows Activation."""
if not is_activated():
TRY_PRINT.run('Windows Activation...', open_windows_activation)
def auto_open_windows_updates():
def auto_open_windows_updates() -> None:
"""Open Windows Updates."""
TRY_PRINT.run('Windows Updates...', open_windows_updates)
def auto_open_xmplay():
def auto_open_xmplay() -> None:
"""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."""
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."""
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."""
TRY_PRINT.run('Installed RAM...', get_installed_ram,
as_list=True, raise_exceptions=True,
)
def auto_show_os_activation():
def auto_show_os_activation() -> None:
"""Display OS activation status."""
TRY_PRINT.run('Activation...', get_os_activation, as_list=True)
def auto_show_os_name():
def auto_show_os_name() -> None:
"""Display OS Name."""
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."""
TRY_PRINT.run(
'Secure Boot...', check_secure_boot_status, msg_good='Enabled',
)
def auto_show_storage_status():
def auto_show_storage_status() -> None:
"""Display 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."""
TRY_PRINT.run(r'Windows\Temp fix...', fix_windows_temp)
# Configure Functions
def config_explorer():
def config_explorer() -> None:
"""Configure Windows Explorer and restart the process."""
reg_write_settings(REG_WINDOWS_EXPLORER)
kill_procs('explorer.exe', force=True)
popen_program(['explorer.exe'])
def config_open_shell():
def config_open_shell() -> None:
"""Configure Open Shell."""
has_low_power_idle = False
@ -560,7 +586,7 @@ def config_open_shell():
reg_write_settings(REG_OPEN_SHELL_LOW_POWER_IDLE)
def disable_chrome_notifications():
def disable_chrome_notifications() -> None:
"""Disable notifications in Google Chrome."""
defaults_key = 'default_content_setting_values'
profiles = []
@ -602,13 +628,19 @@ def disable_chrome_notifications():
pref_file.write_text(json.dumps(pref_data, separators=(',', ':')))
def enable_bsod_minidumps():
"""Enable saving minidumps during BSoDs."""
cmd = ['wmic', 'RECOVEROS', 'set', 'DebugInfoType', '=', '3']
def disable_password_expiration() -> None:
"""Disable password expiration for all users."""
script_path = find_kit_dir('Scripts').joinpath('disable_password_expiration.ps1')
cmd = ['PowerShell', '-ExecutionPolicy', 'Bypass', '-File', script_path]
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."""
base_paths = [
PROGRAMFILES_64, PROGRAMFILES_32, os.environ.get('LOCALAPPDATA'),
@ -635,10 +667,10 @@ def enable_ublock_origin():
# Open detected browsers
for cmd in cmds:
popen_program(cmd)
popen_program(cmd, pipe=True)
def fix_windows_temp():
def fix_windows_temp() -> None:
"""Restore default permissions for Windows\\Temp."""
permissions = (
'Users:(CI)(X,WD,AD)',
@ -650,7 +682,7 @@ def fix_windows_temp():
# Install Functions
def install_firefox():
def install_firefox() -> None:
"""Install Firefox.
As far as I can tell if you use the EXE installers then it will use
@ -755,12 +787,12 @@ def install_libreoffice(
run_program(cmd)
def install_open_shell():
def install_open_shell() -> None:
"""Install Open Shell (just the Start Menu)."""
skin_zip = get_tool_path('OpenShell', 'Fluent-Metro', suffix='zip')
# Bail early
if OS_VERSION != 10:
if OS_VERSION < 10:
raise GenericWarning('Unsupported OS')
# Install OpenShell
@ -785,49 +817,7 @@ def install_open_shell():
run_program(cmd)
def install_software_bundle():
"""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
print_standard(msg)
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, 2019):
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():
def uninstall_firefox() -> None:
"""Uninstall all copies of Firefox."""
json_file = format_log_path(log_name='Installed Programs', timestamp=True)
json_file = json_file.with_name(f'{json_file.stem}.json')
@ -848,13 +838,14 @@ def uninstall_firefox():
# Misc Functions
def check_secure_boot_status():
def check_secure_boot_status() -> None:
"""Check Secure Boot status."""
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."""
# TODO: Refactor to remove dependancy on Any
default_profile = None
encoding = None
parser = None
@ -891,24 +882,24 @@ def get_firefox_default_profile(profiles_ini):
return (default_profile, encoding)
def get_storage_status():
def get_storage_status() -> list[str]:
"""Get storage status for fixed disks, returns list."""
report = get_volume_usage(use_colors=True)
for disk in get_raw_disks():
report.append(color_string(f'Uninitialized Disk: {disk}', 'RED'))
report.append(ansi.color_string(f'Uninitialized Disk: {disk}', 'RED'))
# Done
return report
def set_default_browser():
def set_default_browser() -> None:
"""Open Windows Settings to the default apps section."""
cmd = ['start', '', 'ms-settings:defaultapps']
popen_program(cmd, shell=True)
# Tool Functions
def export_aida64_report():
def export_aida64_report() -> None:
"""Export AIDA64 report."""
report_path = format_log_path(
log_name='AIDA64 System Report',
@ -929,12 +920,12 @@ def export_aida64_report():
raise GenericError('Error(s) encountered exporting report.')
def open_device_manager():
def open_device_manager() -> None:
"""Open Device Manager."""
popen_program(['mmc', 'devmgmt.msc'])
def open_hwinfo_sensors():
def open_hwinfo_sensors() -> None:
"""Open HWiNFO sensors."""
hwinfo_path = get_tool_path('HWiNFO', 'HWiNFO')
base_config = hwinfo_path.with_name('general.ini')
@ -950,17 +941,33 @@ def open_hwinfo_sensors():
run_tool('HWiNFO', 'HWiNFO', popen=True)
def open_windows_activation():
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."""
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)
def open_windows_activation() -> None:
"""Open Windows Activation."""
popen_program(['slui'])
def open_windows_updates():
def open_windows_updates() -> None:
"""Open Windows Updates."""
popen_program(['control', '/name', 'Microsoft.WindowsUpdate'])
def open_xmplay():
def open_xmplay() -> None:
"""Open XMPlay."""
sleep(2)
run_tool('XMPlay', 'XMPlay', 'music.7z', cwd=True, popen=True)

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,6 @@
"""WizardKit: ui module init"""
from . import ansi
from . import cli
from . import tmux
from . import tui

70
scripts/wk/ui/ansi.py Normal file
View file

@ -0,0 +1,70 @@
"""WizardKit: ANSI control/escape functions"""
# vim: sts=2 sw=2 ts=2
import itertools
import logging
from typing import Iterable
# STATIC VARIABLES
LOG = logging.getLogger(__name__)
COLORS = {
'CLEAR': '\033[0m',
'RED': '\033[31m',
'RED_BLINK': '\033[31;5m',
'ORANGE': '\033[31;1m',
'ORANGE_RED': '\033[1;31;41m',
'GREEN': '\033[32m',
'YELLOW': '\033[33m',
'YELLOW_BLINK': '\033[33;5m',
'BLUE': '\033[34m',
'PURPLE': '\033[35m',
'CYAN': '\033[36m',
}
# Functions
def clear_screen() -> None:
"""Clear screen using ANSI escape."""
print('\033c', end='', flush=True)
def color_string(
strings: Iterable[str] | str,
colors: Iterable[str | None] | str,
sep=' ',
) -> str:
"""Build colored string using ANSI escapes, returns str."""
data = {'strings': strings, 'colors': colors}
msg = []
# Convert input to tuples of strings
for k, v in data.items():
if isinstance(v, str):
# Avoid splitting string into a list of characters
data[k] = (v,)
try:
iter(v)
except TypeError:
# Assuming single element passed, convert to string
data[k] = (str(v),)
# Build new string with color escapes added
for string, color in itertools.zip_longest(data['strings'], data['colors']):
color_code = COLORS.get(str(color), COLORS['CLEAR'])
msg.append(f'{color_code}{string}{COLORS["CLEAR"]}')
# Done
return sep.join(msg)
def strip_colors(string: str) -> str:
"""Strip known ANSI color escapes from string, returns str."""
LOG.debug('string: %s', string)
for color in COLORS.values():
string = string.replace(color, '')
return string
if __name__ == '__main__':
print("This file is not meant to be called directly.")

924
scripts/wk/ui/cli.py Normal file
View file

@ -0,0 +1,924 @@
"""WizardKit: CLI functions"""
# vim: sts=2 sw=2 ts=2
import logging
import os
import platform
import re
import subprocess
import sys
import traceback
from typing import Any, Callable, Iterable
from prompt_toolkit import prompt
from prompt_toolkit.document import Document
from prompt_toolkit.validation import Validator, ValidationError
try:
from functools import cache
except ImportError:
# Assuming Python is < 3.9
from functools import lru_cache as cache
from wk.cfg.main import (
ENABLED_UPLOAD_DATA,
INDENT,
SUPPORT_MESSAGE,
WIDTH,
)
from wk.std import (sleep, GenericWarning)
from wk.ui.ansi import clear_screen, color_string, strip_colors
# STATIC VARIABLES
LOG = logging.getLogger(__name__)
PLATFORM = platform.system()
# Classes
class InputChoiceValidator(Validator):
"""Validate that input is one of the provided choices."""
def __init__(self, choices: Iterable[str], allow_empty: bool = False):
self.allow_empty: bool = allow_empty
self.choices: list[str] = [str(c).upper() for c in choices]
super().__init__()
def validate(self, document: Document) -> None:
text = document.text
if not (text or self.allow_empty):
raise ValidationError(
message='This input is required!',
cursor_position=len(text),
)
if text and text.upper() not in self.choices:
raise ValidationError(
message='Invalid selection',
cursor_position=len(text),
)
class InputNotEmptyValidator(Validator):
"""Validate that input is not empty."""
def validate(self, document: Document) -> None:
text = document.text
if not text:
raise ValidationError(
message='This input is required!',
cursor_position=len(text),
)
class InputTicketIDValidator(Validator):
"""Validate that input resembles a ticket ID."""
def __init__(self, allow_empty: bool = False):
self.allow_empty: bool = allow_empty
super().__init__()
def validate(self, document: Document) -> None:
text = document.text
if not (text or self.allow_empty):
raise ValidationError(
message='This input is required!',
cursor_position=len(text),
)
if text and not re.match(r'^\d', text):
raise ValidationError(
message='Ticket ID should start with a number!',
cursor_position=len(text),
)
class InputYesNoValidator(Validator):
"""Validate that input is a yes or no."""
def __init__(self, allow_empty: bool = False):
self.allow_empty: bool = allow_empty
super().__init__()
def validate(self, document: Document) -> None:
text = document.text
if not (text or self.allow_empty):
raise ValidationError(
message='This input is required!',
cursor_position=len(text),
)
if text and not re.match(r'^(y(es|up|)|n(o|ope|))$', text, re.IGNORECASE):
raise ValidationError(
message='Please answer "yes" or "no"',
cursor_position=len(text),
)
class Menu():
"""Object for tracking menu specific data and methods.
ASSUMPTIONS:
1. All entry names are unique.
2. All action entry names start with different letters.
"""
def __init__(self, title: str = '[Untitled Menu]'):
self.actions: dict[str, dict[Any, Any]] = {}
self.options: dict[str, dict[Any, Any]] = {}
self.sets: dict[str, dict[Any, Any]] = {}
self.toggles: dict[str, dict[Any, Any]] = {}
self.disabled_str: str = 'Disabled'
self.separator: str = ''
self.title: str = title
def _generate_menu_text(self) -> str:
"""Generate menu text, returns str."""
separator_string = self._get_separator_string()
menu_lines = [self.title, separator_string] if self.title else []
# Sets & toggles
for section in (self.sets, self.toggles):
for details in section.values():
if details.get('Hidden', False):
continue
if details.get('Separator', False):
menu_lines.append(separator_string)
menu_lines.append(details['Display Name'])
if self.sets or self.toggles:
menu_lines.append(separator_string)
# Options
for details in self.options.values():
if details.get('Hidden', False):
continue
if details.get('Separator', False):
menu_lines.append(separator_string)
menu_lines.append(details['Display Name'])
if self.options:
menu_lines.append(separator_string)
# Actions
for details in self.actions.values():
if details.get('Hidden', False):
continue
if details.get('Separator', False):
menu_lines.append(separator_string)
menu_lines.append(details['Display Name'])
# Show menu
menu_lines.append('')
menu_lines = [str(line) for line in menu_lines]
return '\n'.join(menu_lines)
def _get_display_name(
self, name, details,
index=None, no_checkboxes=True, setting_item=False) -> str:
"""Format display name based on details and args, returns str."""
disabled = details.get('Disabled', False)
if setting_item and not details['Selected']:
# Display item in YELLOW
disabled = True
checkmark = '*'
if 'CONEMUPID' in os.environ or 'DISPLAY' in os.environ or PLATFORM == 'Darwin':
checkmark = ''
display_name = f'{index if index else name[:1].upper()}: '
if not (index and index >= 10):
display_name = f' {display_name}'
if setting_item and 'Value' in details:
name = f'{name} = {details["Value"]}'
# Add enabled status if necessary
if not no_checkboxes:
display_name += f'[{checkmark if details["Selected"] else " "}] '
# Add name
if disabled:
display_name += color_string(f'{name} ({self.disabled_str})', 'YELLOW')
else:
display_name += name
# Done
return display_name
def _get_separator_string(self) -> str:
"""Format separator length based on name lengths, returns str."""
separator_length = 0
# Check title line(s)
if self.title:
for line in self.title.split('\n'):
separator_length = max(separator_length, len(strip_colors(line)))
# Loop over all item names
for section in (self.actions, self.options, self.sets, self.toggles):
for details in section.values():
if details.get('Hidden', False):
# Skip hidden lines
continue
line = strip_colors(details['Display Name'])
separator_length = max(separator_length, len(line))
separator_length += 1
# Done
return self.separator * separator_length
def _get_valid_answers(self) -> list[str]:
"""Get valid answers based on menu items, returns list."""
valid_answers = []
# Numbered items
index = 0
for section in (self.sets, self.toggles, self.options):
for details in section.values():
if details.get('Hidden', False):
# Don't increment index or add to valid_answers
continue
index += 1
if not details.get('Disabled', False):
valid_answers.append(str(index))
# Action items
for name, details in self.actions.items():
if not details.get('Disabled', False):
valid_answers.append(name[:1].upper())
# Done
return valid_answers
def _resolve_selection(self, selection: str) -> tuple[str, dict[Any, Any]]:
"""Get menu item based on user selection, returns tuple."""
offset = 1
resolved_selection = tuple()
if selection.isnumeric():
# Enumerate over numbered entries
entries = [
*self.sets.items(),
*self.toggles.items(),
*self.options.items(),
]
for _i, details in enumerate(entries):
if details[1].get('Hidden', False):
offset -= 1
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)
break
else:
# Just check actions
for action, details in self.actions.items():
if action.lower().startswith(selection.lower()):
resolved_selection = (action, details)
break
# Done
return resolved_selection
def _update(self, single_selection: bool = True, settings_mode: bool = False) -> None:
"""Update menu items in preparation for printing to screen."""
index = 0
# Fix selection status for sets
for set_details in self.sets.values():
set_selected = True
set_targets = set_details['Targets']
for option, option_details in self.options.items():
if option in set_targets and not option_details['Selected']:
set_selected = False
elif option not in set_targets and option_details['Selected']:
set_selected = False
set_details['Selected'] = set_selected
# Numbered sections
for section in (self.sets, self.toggles, self.options):
for name, details in section.items():
if details.get('Hidden', False):
# Skip hidden lines and don't increment index
continue
index += 1
details['Display Name'] = self._get_display_name(
name,
details,
index=index,
no_checkboxes=single_selection,
setting_item=settings_mode,
)
# Actions
for name, details in self.actions.items():
details['Display Name'] = self._get_display_name(
name,
details,
no_checkboxes=True,
)
def _update_entry_selection_status(
self, entry: str, toggle: bool = True, status: bool = False) -> None:
"""Update entry selection status either directly or by toggling."""
if entry in self.sets:
# Update targets not the set itself
new_status = not self.sets[entry]['Selected'] if toggle else status
targets = self.sets[entry]['Targets']
self._update_set_selection_status(targets, new_status)
for section in (self.toggles, self.options, self.actions):
if entry in section:
if toggle:
section[entry]['Selected'] = not section[entry]['Selected']
else:
section[entry]['Selected'] = status
def _update_set_selection_status(self, targets: Iterable[str], status: bool) -> None:
"""Select or deselect options based on targets and status."""
for option, details in self.options.items():
# If (new) status is True and this option is a target then select
# Otherwise deselect
details['Selected'] = status and option in targets
def _user_select(self, prompt_msg: str) -> str:
"""Show menu and select an entry, returns str."""
menu_text = self._generate_menu_text()
valid_answers = self._get_valid_answers()
# Menu loop
while True:
clear_screen()
print(menu_text)
sleep(0.01)
answer = input_text(prompt_msg).strip()
if answer.upper() in valid_answers:
break
# Done
return answer
def add_action(self, name: str, details: dict[Any, Any] | None = None) -> None:
"""Add action to menu."""
details = details if details else {}
details['Selected'] = details.get('Selected', False)
self.actions[name] = details
def add_option(self, name: str, details: dict[Any, Any] | None = None) -> None:
"""Add option to menu."""
details = details if details else {}
details['Selected'] = details.get('Selected', False)
self.options[name] = details
def add_set(self, name: str, details: dict[Any, Any] | None = None) -> None:
"""Add set to menu."""
details = details if details else {}
details['Selected'] = details.get('Selected', False)
# Safety check
if 'Targets' not in details:
raise KeyError('Menu set has no targets')
# Add set
self.sets[name] = details
def add_toggle(self, name: str, details: dict[Any, Any] | None = None) -> None:
"""Add toggle to menu."""
details = details if details else {}
details['Selected'] = details.get('Selected', False)
self.toggles[name] = details
def advanced_select(
self,
prompt_msg: str = 'Please make a selection: ',
) -> tuple[str, dict[Any, Any]]:
"""Display menu and make multiple selections, returns tuple.
NOTE: Menu is displayed until an action entry is selected.
"""
while True:
self._update(single_selection=False)
user_selection = self._user_select(prompt_msg)
selected_entry = self._resolve_selection(user_selection)
if user_selection.isnumeric():
# Update selection(s)
self._update_entry_selection_status(selected_entry[0])
else:
# Action selected
break
# Done
return selected_entry
def settings_select(
self,
prompt_msg: str = 'Please make a selection: ',
) -> tuple[str, dict[Any, Any]]:
"""Display menu and make multiple selections, returns tuple.
NOTE: Menu is displayed until an action entry is selected.
"""
choice_kwargs = {
'prompt_msg': 'Toggle or change value?',
'choices': ['T', 'C'],
}
while True:
self._update(single_selection=True, settings_mode=True)
user_selection = self._user_select(prompt_msg)
selected_entry = self._resolve_selection(user_selection)
if user_selection.isnumeric():
if 'Value' in selected_entry[-1] and choice(**choice_kwargs) == 'C':
# Change
selected_entry[-1]['Value'] = input_text('Enter new value: ')
else:
# Toggle
self._update_entry_selection_status(selected_entry[0])
else:
# Action selected
break
# Done
return selected_entry
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."""
if update:
self._update()
user_selection = self._user_select(prompt_msg)
return self._resolve_selection(user_selection)
def update(self) -> None:
"""Update menu with default settings."""
self._update()
class TryAndPrint():
"""Object used to standardize running functions and returning the result.
The errors and warning attributes are used to allow fine-tuned results
based on exception names.
"""
def __init__(self, msg_bad: str = 'FAILED', msg_good: str = 'SUCCESS'):
self.catch_all : bool = True
self.indent: int = INDENT
self.list_errors: list[str] = ['GenericError']
self.list_warnings: list[str] = ['GenericWarning']
self.msg_bad: str = msg_bad
self.msg_good: str = msg_good
self.verbose : bool = False
self.width: int = WIDTH
def _format_exception_message(self, _exception: Exception) -> str:
"""Format using the exception's args or name, returns str."""
LOG.debug(
'Formatting exception: %s, %s',
_exception.__class__.__name__,
_exception,
)
message = ''
# Format message string from _exception
try:
if isinstance(_exception, subprocess.CalledProcessError):
message = _exception.stderr
if not isinstance(message, str):
message = message.decode('utf-8')
message = message.strip()
elif isinstance(_exception, ZeroDivisionError):
# Skip and just use exception name below
pass
else:
message = str(_exception)
except Exception:
# Just use the exception name instead
pass
# Prepend exception name
if _exception.__class__.__name__ not in ('GenericError', 'GenericWarning'):
try:
message = f'{_exception.__class__.__name__}: {message}'
except Exception:
message = f'UNKNOWN ERROR: {message}'
# Fix multi-line messages
if '\n' in message:
try:
lines = [
f'{" "*(self.indent+self.width)}{line.strip()}'
for line in message.splitlines() if line.strip()
]
lines[0] = lines[0].strip()
message = '\n'.join(lines)
except Exception:
pass
# Done
return message
def _format_function_output(
self,
output: list | subprocess.CompletedProcess,
msg_good: str,
) -> str:
"""Format function output for use in try_and_print(), returns str."""
LOG.debug('Formatting output: %s', output)
if not output:
raise GenericWarning('No output')
# Ensure we're working with a list
if isinstance(output, subprocess.CompletedProcess):
stdout = output.stdout
if not isinstance(stdout, str):
stdout = stdout.decode('utf8')
output = stdout.strip().splitlines()
if not output:
# Going to treat these as successes (for now)
LOG.warning('Program output was empty, assuming good result.')
return color_string(msg_good, 'GREEN')
else:
try:
output = list(output)
except TypeError:
output = [output]
# Safety check
if not output:
# Going to ignore empty function output for now
LOG.error('Output is empty')
return 'UNKNOWN'
# Build result_msg
result_msg = f'{output.pop(0)}'
if output:
output = [f'{" "*(self.indent+self.width)}{line}' for line in output]
result_msg += '\n' + '\n'.join(output)
# Done
return result_msg
def _log_result(self, message: str, result_msg: str) -> None:
"""Log result text without color formatting."""
log_text = f'{" "*self.indent}{message:<{self.width}}{result_msg}'
for line in log_text.splitlines():
line = strip_colors(line)
LOG.info(line)
def add_error(self, exception_name: str) -> None:
"""Add exception name to error list."""
if exception_name not in self.list_errors:
self.list_errors.append(exception_name)
def add_warning(self, exception_name: str) -> None:
"""Add exception name to warning list."""
if exception_name not in self.list_warnings:
self.list_warnings.append(exception_name)
def run(
self,
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.
If catch_all is True then (nearly) all exceptions will be caught.
Otherwise if an exception occurs that wasn't specified it will be
re-raised.
If the function returns data it will be used instead of msg_good,
msg_bad, or exception text.
The output should be a list or a subprocess.CompletedProcess object.
If msg_good is passed it will override self.msg_good.
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.
If catch_all and/or verbose are passed it will override
self.catch_all and/or self.verbose for this call.
args and kwargs are passed to the function.
"""
LOG.debug('function: %s.%s', function.__module__, function.__name__)
LOG.debug('args: %s', args)
LOG.debug('kwargs: %s', kwargs)
LOG.debug(
'catch_all: %s, msg_good: %s, verbose: %s',
catch_all,
msg_good,
verbose,
)
f_exception = None
catch_all = catch_all if catch_all is not None else self.catch_all
msg_good = msg_good if msg_good is not None else self.msg_good
output = None
result_msg = 'UNKNOWN'
verbose = verbose if verbose is not None else self.verbose
# Build exception tuples
e_exceptions: tuple = tuple(get_exception(e) for e in self.list_errors)
w_exceptions: tuple = tuple(get_exception(e) for e in self.list_warnings)
# Run function and catch exceptions
print(f'{" "*self.indent}{message:<{self.width}}', end='', flush=True)
LOG.debug('Running function: %s.%s', function.__module__, function.__name__)
try:
output = function(*args, **kwargs)
except w_exceptions as _exception:
# Warnings
result_msg = self._format_exception_message(_exception)
print_warning(result_msg, log=False)
f_exception = _exception
except e_exceptions as _exception:
# Exceptions
result_msg = self._format_exception_message(_exception)
print_error(result_msg, log=False)
f_exception = _exception
except Exception as _exception:
# Unexpected exceptions
if verbose:
result_msg = self._format_exception_message(_exception)
else:
result_msg = self.msg_bad
print_error(result_msg, log=False)
f_exception = _exception
if not catch_all:
# Re-raise error as necessary
raise
else:
# Success
if output:
result_msg = self._format_function_output(output, msg_good)
print(result_msg)
else:
result_msg = msg_good
print_success(result_msg, log=False)
# Done
self._log_result(message, result_msg)
return {
'Exception': f_exception,
'Failed': bool(f_exception),
'Message': result_msg,
'Output': output,
}
# Functions
def abort(
prompt_msg: str = 'Aborted.',
show_prompt_msg: bool = True,
return_code: int = 1,
) -> None:
"""Abort script."""
print_warning(prompt_msg)
if show_prompt_msg:
sleep(0.5)
pause(prompt_msg='Press Enter to exit... ')
sys.exit(return_code)
def ask(prompt_msg: str) -> bool:
"""Prompt the user with a Y/N question, returns bool."""
validator = InputYesNoValidator()
# Show prompt
response = input_text(f'{prompt_msg} [Y/N]: ', validator=validator)
if response.upper().startswith('Y'):
LOG.info('%s Yes', prompt_msg)
return True
if response.upper().startswith('N'):
LOG.info('%s No', prompt_msg)
return False
# This shouldn't ever be reached
raise ValueError(f'Invalid answer given: {response}')
def beep(repeat: int = 1) -> None:
"""Play system bell with optional repeat."""
while repeat >= 1:
# Print bell char without a newline
print('\a', end='', flush=True)
sleep(0.5)
repeat -= 1
def choice(prompt_msg: str, choices: Iterable[str]) -> str:
"""Choose an option from a provided list, returns str.
Choices provided will be converted to uppercase and returned as such.
Similar to the commands choice (Windows) and select (Linux).
"""
LOG.debug('prompt_msg: %s, choices: %s', prompt_msg, choices)
choices = [str(c).upper()[:1] for c in choices]
prompt_msg = f'{prompt_msg} [{"/".join(choices)}]'
# Show prompt
response = input_text(prompt_msg, validator=InputChoiceValidator(choices))
# Done
LOG.info('%s %s', prompt_msg, response)
return response.upper()
def fix_prompt(message: str) -> str:
"""Fix prompt, returns str."""
if not message:
message = 'Input text: '
message = str(message)
if message[-1:] != ' ':
message += ' '
return message
@cache
def get_exception(name: str) -> Exception:
"""Get exception by name, returns exception object.
[Doctest]
>>> t = TryAndPrint()
>>> t._get_exception('AttributeError')
<class 'AttributeError'>
>>> t._get_exception('CalledProcessError')
<class 'subprocess.CalledProcessError'>
>>> t._get_exception('GenericError')
<class 'wk.std.GenericError'>
"""
LOG.debug('Getting exception: %s', name)
obj = getattr(sys.modules[__name__], name, None)
if obj:
return obj
# Try builtin classes
obj = getattr(sys.modules['builtins'], name, None)
if obj:
return obj
# Try all modules
for _mod in sys.modules.values():
obj = getattr(_mod, name, None)
if obj:
break
# Check if not found
if not obj:
raise AttributeError(f'Failed to find exception: {name}')
# Done
return obj
def get_ticket_id() -> str:
"""Get ticket ID, returns str."""
prompt_msg = 'Please enter ticket ID:'
validator = InputTicketIDValidator()
# Show prompt
ticket_id = input_text(prompt_msg, validator=validator)
# Done
return ticket_id
def input_text(
prompt_msg: str = 'Enter text: ',
allow_empty: bool = False,
validator: Validator | None = None,
) -> str:
"""Get input from user, returns str."""
prompt_msg = fix_prompt(prompt_msg)
# Accept empty responses?
if not (allow_empty or validator):
validator = InputNotEmptyValidator()
# Show prompt
result = None
while result is None:
try:
result = prompt(prompt_msg, validator=validator)
except KeyboardInterrupt:
# Ignore CTRL+c
pass
# Done
return result
def major_exception() -> None:
"""Display traceback, optionally upload detailes, and exit."""
LOG.critical('Major exception encountered', exc_info=True)
print_error('Major exception', log=False)
print_warning(SUPPORT_MESSAGE)
if ENABLED_UPLOAD_DATA:
print_warning('Also, please run upload-logs to help debugging!')
print(traceback.format_exc())
# Done
pause('Press Enter to exit... ')
raise SystemExit(1)
def pause(prompt_msg: str = 'Press Enter to continue... ') -> None:
"""Simple pause implementation."""
input_text(prompt_msg, allow_empty=True)
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."""
LOG.debug(
'strings: %s, colors: %s, sep: %s, kwargs: %s',
strings, colors, sep, kwargs,
)
msg = color_string(strings, colors, sep=sep)
print_options = {
'end': kwargs.get('end', '\n'),
'file': kwargs.get('file', sys.stdout),
'flush': kwargs.get('flush', False),
}
print(msg, **print_options)
if log:
LOG.info(strip_colors(msg))
def print_error(msg: str, log: bool = True, **kwargs) -> None:
"""Prints message in RED and log as ERROR."""
if 'file' not in kwargs:
# Only set if not specified
kwargs['file'] = sys.stderr
print_colored(msg, 'RED', **kwargs)
if log:
LOG.error(msg)
def print_info(msg: str, log: bool = True, **kwargs) -> None:
"""Prints message in BLUE and log as INFO."""
print_colored(msg, 'BLUE', **kwargs)
if log:
LOG.info(msg)
def print_report(report: list[str], indent=None, log: bool = True) -> None:
"""Print report to screen and optionally to log."""
for line in report:
if indent:
line = f'{" "*indent}{line}'
print(line)
if log:
LOG.info(strip_colors(line))
def print_standard(msg: str, log: bool = True, **kwargs) -> None:
"""Prints message and log as INFO."""
print(msg, **kwargs)
if log:
LOG.info(msg)
def print_success(msg: str, log: bool = True, **kwargs) -> None:
"""Prints message in GREEN and log as INFO."""
print_colored(msg, 'GREEN', **kwargs)
if log:
LOG.info(msg)
def print_warning(msg: str, log: bool = True, **kwargs) -> None:
"""Prints message in YELLOW and log as WARNING."""
if 'file' not in kwargs:
# Only set if not specified
kwargs['file'] = sys.stderr
print_colored(msg, 'YELLOW', **kwargs)
if log:
LOG.warning(msg)
def set_title(title: str) -> None:
"""Set window title."""
LOG.debug('title: %s', title)
if os.name == 'nt':
os.system(f'title {title}')
else:
print_error('Setting the title is only supported under Windows.')
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."""
indent = INDENT if indent is None else indent
width = WIDTH if width is None else width
print_colored(
(f'{" "*indent}{message:<{width}}', data),
(None, color if color else None),
log=True,
sep='',
)
if __name__ == '__main__':
print("This file is not meant to be called directly.")

View file

@ -4,6 +4,8 @@
import logging
import pathlib
from typing import Any
from wk.exe import run_program
from wk.std import PLATFORM
@ -13,7 +15,7 @@ LOG = logging.getLogger(__name__)
# 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."""
cmd = ['tmux', 'capture-pane', '-p']
if pane_id:
@ -24,49 +26,122 @@ def capture_pane(pane_id=None):
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."""
cmd = ['tmux', 'send-keys', '-R']
commands = [
['tmux', 'send-keys', '-R'],
['tmux', 'clear-history'],
]
if pane_id:
cmd.extend(['-t', pane_id])
commands = [[*cmd, '-t', pane_id] for cmd in commands]
# Clear pane
run_program(cmd, check=False)
for cmd in commands:
run_program(cmd, check=False)
def fix_layout(panes, layout, forced=False):
"""Fix pane sizes based on layout."""
if not (forced or layout_needs_fixed(panes, layout)):
def fix_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 = []
# Bail early
if not (forced or layout_needs_fixed(layout)):
# Layout should be fine
return
# Update panes
for name, data in layout.items():
# Skip missing panes
if name not in panes:
# Clear current pane if needed
if clear_on_resize:
clear_pane()
# Remove closed panes
for data in layout.values():
data['Panes'] = [pane for pane in data['Panes'] if poll_pane(pane)]
# Calculate constraints
avail_horizontal, avail_vertical = get_window_size()
avail_vertical -= layout['Current'].get('height', 0)
for group in ('Title', 'Info'):
if not layout[group]['Panes']:
continue
avail_vertical -= layout[group].get('height', 0) + 1
num_workers = len(layout['Workers']['Panes'])
avail_vertical -= num_workers * (layout['Workers'].get('height', 0) + 1)
avail_horizontal -= layout['Progress']['width'] + 1
# Resize pane(s)
pane_list = panes[name]
if isinstance(pane_list, str):
pane_list = [pane_list]
for pane_id in pane_list:
if name == 'Current':
pane_id = None
try:
resize_pane(pane_id, **data)
except RuntimeError:
# Assuming pane was closed just before resizing
pass
# 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:
try:
resize_pane(**kwargs)
except RuntimeError:
# Assuming pane was closed just before resizing
pass
def get_pane_size(pane_id=None):
def get_pane_size(pane_id: str | None = None) -> tuple[int, int]:
"""Get current or target pane size, returns tuple."""
cmd = ['tmux', 'display', '-p']
cmd = ['tmux', 'display-message', '-p']
if pane_id:
cmd.extend(['-t', pane_id])
cmd.append('#{pane_width} #{pane_height}')
# Get resolution
proc = run_program(cmd, check=False)
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)
height = int(height)
# Done
return (width, height)
def get_window_size() -> tuple[int, int]:
"""Get current window size, returns tuple."""
cmd = ['tmux', 'display-message', '-p', '#{window_width} #{window_height}']
# Get resolution
proc = run_program(cmd, check=False)
width, height = proc.stdout.strip().split()
@ -77,7 +152,7 @@ def get_pane_size(pane_id=None):
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."""
cmd = ['tmux', 'kill-pane', '-a']
if pane_id:
@ -87,7 +162,7 @@ def kill_all_panes(pane_id=None):
run_program(cmd, check=False)
def kill_pane(*pane_ids):
def kill_pane(*pane_ids: str) -> None:
"""Kill pane(s) by id."""
cmd = ['tmux', 'kill-pane', '-t']
@ -96,42 +171,28 @@ def kill_pane(*pane_ids):
run_program(cmd+[pane_id], check=False)
def layout_needs_fixed(panes, layout):
def layout_needs_fixed(layout: dict[str, dict[str, Any]]) -> bool:
"""Check if layout needs fixed, returns bool."""
needs_fixed = False
# Check panes
for name, data in layout.items():
# Skip unpredictably sized panes
if not data.get('Check', False):
continue
# Skip missing panes
if name not in panes:
continue
# Check pane size(s)
pane_list = panes[name]
if isinstance(pane_list, str):
pane_list = [pane_list]
for pane_id in pane_list:
try:
width, height = get_pane_size(pane_id)
except ValueError:
# Pane may have disappeared during this loop
continue
if data.get('width', False) and data['width'] != width:
needs_fixed = True
if data.get('height', False) and data['height'] != height:
needs_fixed = True
for data in layout.values():
if 'height' in data:
needs_fixed = needs_fixed or any(
get_pane_size(pane)[1] != data['height'] for pane in data['Panes']
)
if 'width' in data:
needs_fixed = needs_fixed or any(
get_pane_size(pane)[0] != data['width'] for pane in data['Panes']
)
# Done
return needs_fixed
def poll_pane(pane_id):
def poll_pane(pane_id: str) -> bool:
"""Check if pane exists, returns bool."""
cmd = ['tmux', 'list-panes', '-F', '#D']
cmd = ['tmux', 'list-panes', '-F', '#{pane_id}']
# Get list of panes
proc = run_program(cmd, check=False)
@ -142,7 +203,12 @@ def poll_pane(pane_id):
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.
This will prep for running a basic command, displaying text on screen,
@ -192,7 +258,7 @@ def prep_action(
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."""
path = pathlib.Path(path).resolve()
try:
@ -202,8 +268,11 @@ def prep_file(path):
pass
def resize_pane(pane_id=None, width=None, height=None, **kwargs):
# pylint: disable=unused-argument
def resize_pane(
pane_id: str | None = None,
width: int | None = None,
height: int | None = None,
) -> None:
"""Resize current or target pane.
NOTE: kwargs is only here to make calling this function easier
@ -228,12 +297,24 @@ def resize_pane(pane_id=None, width=None, height=None, **kwargs):
run_program(cmd, check=False)
def respawn_pane(pane_id: str, **action) -> None:
"""Respawn pane with action."""
cmd = ['tmux', 'respawn-pane', '-k', '-t', pane_id]
cmd.extend(prep_action(**action))
# Respawn
run_program(cmd, check=False)
def split_window(
lines=None, percent=None,
behind=False, vertical=False,
target_id=None, **action):
lines: int | None = None,
percent: int | None = None,
behind: bool = False,
vertical: bool = False,
target_id: str | None = None,
**action) -> 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
if not (lines or percent):
@ -254,7 +335,7 @@ def split_window(
if lines:
cmd.extend(['-l', str(lines)])
elif percent:
cmd.extend(['-p', str(percent)])
cmd.extend(['-l', f'{percent}%'])
# New pane action
cmd.extend(prep_action(**action))
@ -264,16 +345,7 @@ def split_window(
return proc.stdout.strip()
def respawn_pane(pane_id, **action):
"""Respawn pane with action."""
cmd = ['tmux', 'respawn-pane', '-k', '-t', pane_id]
cmd.extend(prep_action(**action))
# Respawn
run_program(cmd, check=False)
def zoom_pane(pane_id=None):
def zoom_pane(pane_id: str | None = None) -> None:
"""Toggle zoom status for current or target pane."""
cmd = ['tmux', 'resize-pane', '-Z']
if pane_id:

382
scripts/wk/ui/tui.py Normal file
View file

@ -0,0 +1,382 @@
"""WizardKit: TUI functions"""
# vim: sts=2 sw=2 ts=2
import atexit
import logging
import time
from copy import deepcopy
from os import environ
from typing import Any
from wk.exe import start_thread
from wk.std import sleep
from wk.ui import ansi, tmux
# STATIC VARIABLES
LOG = logging.getLogger(__name__)
TMUX_SIDE_WIDTH = 21
TMUX_TITLE_HEIGHT = 2
TMUX_LAYOUT = { # NOTE: This needs to be in order from top to bottom
'Title': {'Panes': [], 'height': TMUX_TITLE_HEIGHT},
'Info': {'Panes': []},
'Current': {'Panes': [environ.get('TMUX_PANE', None)]},
'Workers': {'Panes': []},
'Started': {'Panes': [], 'height': TMUX_TITLE_HEIGHT},
'Progress': {'Panes': [], 'width': TMUX_SIDE_WIDTH},
}
# Classes
class TUI():
"""Object for tracking TUI elements."""
def __init__(self, title_text: str | None = None):
self.clear_on_resize = False
self.layout: dict[str, dict[str, Any]] = deepcopy(TMUX_LAYOUT)
self.side_width: int = TMUX_SIDE_WIDTH
self.title_text: str = title_text if title_text else 'Title Text'
self.title_text_line2: str = ''
self.title_colors: list[str] = ['BLUE', '']
# Init tmux and start a background process to maintain layout
self.init_tmux()
start_thread(self.fix_layout_loop)
# Close all panes at exit
atexit.register(tmux.kill_all_panes)
def add_info_pane(
self,
lines: int | None = None,
percent: int = 0,
update_layout: bool = True,
**tmux_args,
) -> None:
"""Add info pane."""
if not (lines or percent):
# Bail early
raise RuntimeError('Neither lines nor percent specified.')
# Calculate lines if needed
if not lines:
lines = int(tmux.get_pane_size()[1] * (percent/100))
# Set tmux split args
tmux_args.update({
'behind': True,
'lines': lines,
'target_id': None,
'vertical': True,
})
if self.layout['Info']['Panes']:
tmux_args.update({
'behind': False,
'percent': 50,
'target_id': self.layout['Info']['Panes'][-1],
'vertical': False,
})
tmux_args.pop('lines')
# Update layout
if update_layout:
self.layout['Info']['height'] = lines
# Add pane
self.layout['Info']['Panes'].append(tmux.split_window(**tmux_args))
def add_title_pane(
self,
line1: str,
line2: str | None = None,
colors: list[str] | None = None,
) -> None:
"""Add pane to title row."""
lines = [line1, line2]
colors = colors if colors else self.title_colors.copy()
if not line2:
lines.pop()
colors.pop()
tmux_args = {
'behind': True,
'lines': TMUX_TITLE_HEIGHT,
'target_id': None,
'text': ansi.color_string(lines, colors, sep='\n'),
'vertical': True,
}
if self.layout['Title']['Panes']:
tmux_args.update({
'behind': False,
'percent': 50,
'target_id': self.layout['Title']['Panes'][-1],
'vertical': False,
})
tmux_args.pop('lines')
# Add pane
self.layout['Title']['Panes'].append(tmux.split_window(**tmux_args))
def add_worker_pane(
self,
lines: int | None = None,
percent: int = 0,
update_layout: bool = True,
**tmux_args,
) -> None:
"""Add worker pane."""
height = lines
# Bail early
if not (lines or percent):
raise RuntimeError('Neither lines nor percent specified.')
# Calculate height if needed
if not height:
height = int(tmux.get_pane_size()[1] * (percent/100))
# Set tmux split args
tmux_args.update({
'behind': False,
'lines': lines,
'percent': percent if percent else None,
'target_id': None,
'vertical': True,
})
# Update layout
if update_layout:
self.layout['Workers']['height'] = height
# Add pane (ensure panes are sorted top to bottom)
self.layout['Workers']['Panes'].insert(0, tmux.split_window(**tmux_args))
def clear_current_pane(self) -> None:
"""Clear screen and history for current pane."""
tmux.clear_pane()
def clear_current_pane_height(self) -> None:
"""Clear current pane height and update layout."""
self.layout['Current'].pop('height', None)
def fix_layout(self, forced: bool = True) -> None:
"""Fix tmux layout based on self.layout."""
try:
tmux.fix_layout(self.layout, clear_on_resize=self.clear_on_resize, forced=forced)
except RuntimeError:
# Assuming self.panes changed while running
pass
def fix_layout_loop(self) -> None:
"""Fix layout on a loop.
NOTE: This should be called as a thread.
"""
while True:
self.fix_layout(forced=False)
sleep(1)
def init_tmux(self) -> None:
"""Initialize tmux layout."""
tmux.kill_all_panes()
self.layout.clear()
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
self.layout['Title']['Panes'].append(tmux.split_window(
behind=True,
lines=2,
vertical=True,
text=ansi.color_string(
[self.title_text, self.title_text_line2],
self.title_colors,
sep = '\n',
),
))
# Done
sleep(0.2)
def remove_all_info_panes(self) -> None:
"""Remove all info panes and update layout."""
self.layout['Info'].pop('height', None)
panes = self.layout['Info']['Panes'].copy()
self.layout['Info']['Panes'].clear()
tmux.kill_pane(*panes)
def remove_all_worker_panes(self) -> None:
"""Remove all worker panes and update layout."""
self.layout['Workers'].pop('height', None)
panes = self.layout['Workers']['Panes'].copy()
self.layout['Workers']['Panes'].clear()
tmux.kill_pane(*panes)
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."""
self.layout['Current']['height'] = height
tmux.resize_pane(height=height)
def set_progress_file(self, progress_file: str) -> None:
"""Set the file to use for the progresse pane."""
tmux.respawn_pane(
pane_id=self.layout['Progress']['Panes'][0],
watch_file=progress_file,
)
def set_title(
self,
line1: str,
line2: str | None = None,
colors: list[str] | None = None,
) -> None:
"""Set title text."""
self.title_text = line1
self.title_text_line2 = line2 if line2 else ''
if colors:
self.title_colors = colors
# Update pane (if present)
if self.layout['Title']['Panes']:
tmux.respawn_pane(
pane_id=self.layout['Title']['Panes'][0],
text=ansi.color_string(
[self.title_text, self.title_text_line2],
self.title_colors,
sep = '\n',
),
)
def update_clock(self) -> None:
"""Update 'Started' pane following clock sync."""
tmux.respawn_pane(
pane_id=self.layout['Started']['Panes'][0],
text=ansi.color_string(
['Started', time.strftime("%Y-%m-%d %H:%M %Z")],
['BLUE', None],
sep='\n',
),
)
# Functions
def fix_layout(layout, forced: bool = False) -> None:
"""Fix pane sizes based on layout."""
resize_kwargs = []
# Bail early
if not (forced or layout_needs_fixed(layout)):
# Layout should be fine
return
# Remove closed panes
for data in layout.values():
data['Panes'] = [pane for pane in data['Panes'] if tmux.poll_pane(pane)]
# Update main panes
for section, data in layout.items():
if section == 'Workers':
# Skip for now
continue
if 'height' in data:
for pane_id in data['Panes']:
resize_kwargs.append({'pane_id': pane_id, 'height': data['height']})
if 'width' in data:
for pane_id in data['Panes']:
resize_kwargs.append({'pane_id': pane_id, 'width': data['width']})
for kwargs in resize_kwargs:
try:
tmux.resize_pane(**kwargs)
except RuntimeError:
# Assuming pane was closed just before resizing
pass
# Update "group" panes widths
for group in ('Title', 'Info'):
num_panes = len(layout[group]['Panes'])
if num_panes <= 1:
continue
width = int( (tmux.get_pane_size()[0] - (1 - num_panes)) / num_panes )
for pane_id in layout[group]['Panes']:
tmux.resize_pane(pane_id, width=width)
if group == 'Title':
# (re)fix Started pane
tmux.resize_pane(layout['Started']['Panes'][0], width=TMUX_SIDE_WIDTH)
# Bail early
if not (layout['Workers']['Panes'] and layout['Workers']['height']):
return
# Update worker heights
worker_height = layout['Workers']['height']
workers = layout['Workers']['Panes'].copy()
num_workers = len(workers)
avail_height = sum(tmux.get_pane_size(pane)[1] for pane in workers)
avail_height += tmux.get_pane_size()[1] # Current pane
# Check if window is too small
if avail_height < (worker_height*num_workers) + 3:
# Just leave things as-is
return
# Resize current pane
tmux.resize_pane(height=avail_height-(worker_height*num_workers))
# Resize bottom pane
tmux.resize_pane(workers.pop(0), height=worker_height)
# Resize the rest of the panes by adjusting the ones above them
while len(workers) > 1:
next_height = sum(tmux.get_pane_size(pane)[1] for pane in workers[:2])
next_height -= worker_height
tmux.resize_pane(workers[1], height=next_height)
workers.pop(0)
def layout_needs_fixed(layout) -> bool:
"""Check if layout needs fixed, returns bool."""
needs_fixed = False
# Check panes
for data in layout.values():
if 'height' in data:
needs_fixed = needs_fixed or any(
tmux.get_pane_size(pane)[1] != data['height'] for pane in data['Panes']
)
if 'width' in data:
needs_fixed = needs_fixed or any(
tmux.get_pane_size(pane)[0] != data['width'] for pane in data['Panes']
)
# Done
return needs_fixed
if __name__ == '__main__':
print("This file is not meant to be called directly.")

68
scripts/wk_debug.py Executable file
View file

@ -0,0 +1,68 @@
#!/usr/bin/env python3
"""WizardKit: Debug Launcher"""
# vim: sts=2 sw=2 ts=2
import pathlib
import pickle
import wk
# STATIC VARIABLES
OPTIONS = {
'Cloning / Imaging': 'ddrescue-TUI',
'Hardware Diagnostics': 'Hardware-Diagnostics',
}
def get_debug_prefix() -> str:
"""Ask what we're debugging, returns log dir prefix."""
menu = wk.ui.cli.Menu(title=f'{wk.cfg.main.KIT_NAME_FULL}: Debugging Menu\n')
for name, prefix in OPTIONS.items():
menu.add_option(name, {'Prefix': prefix})
selection = menu.simple_select()
return selection[1]['Prefix']
def get_debug_path() -> pathlib.Path:
"""Get debug path."""
log_dir = pathlib.Path('~/Logs').expanduser().resolve()
debug_paths = []
prefix = get_debug_prefix()
# Build list of options
for item in log_dir.iterdir():
if item.is_dir() and item.name.startswith(prefix):
debug_paths.append(item.joinpath('debug'))
debug_paths = [item for item in debug_paths if item.exists()]
debug_paths.sort()
# Safety check
if not debug_paths:
wk.ui.cli.abort('No logs found, aborting.')
# Use latest option
if wk.ui.cli.ask('Use latest session?'):
return debug_paths[-1]
# Select from list
menu = wk.ui.cli.Menu(title=f'{wk.cfg.main.KIT_NAME_FULL}: Debugging Menu\n')
for item in debug_paths:
menu.add_option(item.parent.name, {'Path': item})
selection = menu.simple_select()
# Done
return selection[1]['Path']
if __name__ == '__main__':
# Leaving this at the global level
try:
debug_path = get_debug_path()
except SystemExit:
pass
else:
state = None
# Load pickle
with open(debug_path.joinpath('state.pickle'), 'rb') as f:
state = pickle.load(f)

View file

@ -35,9 +35,9 @@ fi
function ask() {
while :; do
read -p "$1 [Y/N] " -r answer
if echo "$answer" | egrep -iq '^(y|yes|sure)$'; then
if echo "$answer" | grep -Eiq '^(y|yes|sure)$'; then
return 0
elif echo "$answer" | egrep -iq '^(n|no|nope)$'; then
elif echo "$answer" | grep -Eiq '^(n|no|nope)$'; then
return 1
fi
done
@ -67,7 +67,7 @@ function fix_kit_permissions() {
function load_settings() {
dos2unix "$ROOT_DIR/scripts/wk/cfg/main.py"
while read line; do
if echo "$line" | egrep -q "^\w+='"; then
if echo "$line" | grep -Eq "^\w+='"; then
line="$(echo "$line" | sed -r 's/[\r\n]+//')"
eval "$line"
fi
@ -76,15 +76,25 @@ function load_settings() {
function copy_live_env() {
echo "Copying Archlinux files..."
rsync -aI "$ROOT_DIR/setup/linux/profile_base/" "$PROFILE_DIR/"
# Add items
if [[ "${1:-}" != "--minimal" ]]; then
rsync -aI "$ROOT_DIR/setup/linux/profile_gui/" "$PROFILE_DIR/"
fi
rsync -aI "$ROOT_DIR/setup/linux/profile/" "$PROFILE_DIR/"
mkdir -p "$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
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"
@ -117,49 +127,21 @@ function update_live_env() {
username="tech"
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
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/SUPPORT_URL/$KIT_NAME_SHORT/" "$PROFILE_DIR/profiledef.sh"
# Boot config (legacy)
mkdir -p "$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"
mkdir -p "$TEMP_DIR" 2>/dev/null
curl -Lo "$TEMP_DIR/wimboot.zip" "http://git.ipxe.org/releases/wimboot/wimboot-latest.zip"
7z e -aoa "$TEMP_DIR/wimboot.zip" -o"$PROFILE_DIR/syslinux/wimboot" 'wimboot*/LICENSE.txt' 'wimboot*/README.txt' 'wimboot*/wimboot'
# Boot config (UEFI)
cp "/usr/share/refind/refind_x64.efi" "$PROFILE_DIR/EFI/boot/bootx64.efi"
cp "$ROOT_DIR/images/rEFInd.png" "$PROFILE_DIR/EFI/boot/rEFInd.png"
rsync -aI "/usr/share/refind/drivers_x64/" "$PROFILE_DIR/EFI/boot/drivers_x64/"
rsync -aI "/usr/share/refind/icons/" "$PROFILE_DIR/EFI/boot/icons/" --exclude "/usr/share/refind/icons/svg"
sed -i "s/%ARCHISO_LABEL%/${label}/" "$PROFILE_DIR/EFI/boot/refind.conf"
# Memtest86
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/memtestx64.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
echo "$hostname" > "$PROFILE_DIR/airootfs/etc/hostname"
echo "127.0.1.1 $hostname.localdomain $hostname" >> "$PROFILE_DIR/airootfs/etc/hosts"
# Live packages
cp "$ROOT_DIR/setup/linux/packages/base" "$PROFILE_DIR/packages.x86_64"
if [[ "${1:-}" != "--minimal" ]]; then
cat "$ROOT_DIR/setup/linux/packages/gui" >> "$PROFILE_DIR/packages.x86_64"
fi
echo "[custom]" >> "$PROFILE_DIR/pacman.conf"
echo "SigLevel = Optional TrustAll" >> "$PROFILE_DIR/pacman.conf"
echo "Server = file://$REPO_DIR" >> "$PROFILE_DIR/pacman.conf"
@ -172,30 +154,25 @@ function update_live_env() {
# 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
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"
curl -o "$SKEL_DIR/.oh-my-zsh/themes/lean.zsh-theme" https://raw.githubusercontent.com/miekg/lean/master/prompt_lean_setup
if [[ "${1:-}" != "--minimal" ]]; then
# Openbox theme
git clone --depth=1 https://github.com/addy-dclxvi/Openbox-Theme-Collections.git "$TEMP_DIR/ob-themes"
mkdir -p "$PROFILE_DIR/airootfs/usr/share/themes"
cp -a "$TEMP_DIR/ob-themes/Triste-Orange" "$PROFILE_DIR/airootfs/usr/share/themes/"
# Openbox theme
git clone --depth=1 https://github.com/addy-dclxvi/Openbox-Theme-Collections.git "$TEMP_DIR/ob-themes"
mkdir -p "$PROFILE_DIR/airootfs/usr/share/themes"
cp -a "$TEMP_DIR/ob-themes/Triste-Orange" "$PROFILE_DIR/airootfs/usr/share/themes/"
# Rofi
## Probably don't need the exact commit but it'll be fine
mkdir -p "$PROFILE_DIR/airootfs/usr/share/fonts/"
curl -Lo \
"$PROFILE_DIR/airootfs/usr/share/fonts/Fantasque-Sans-Mono-Nerd-Font.ttf" \
"https://github.com/adi1090x/rofi/raw/9c4093c665326bb08d6affc7e16d18d8f25c4452/fonts/Fantasque-Sans-Mono-Nerd-Font.ttf"
curl -Lo \
"$PROFILE_DIR/airootfs/usr/share/fonts/Feather.ttf" \
"https://github.com/adi1090x/rofi/raw/9c4093c665326bb08d6affc7e16d18d8f25c4452/fonts/Feather.ttf"
fi
# Rofi
## Probably don't need the exact commit but it'll be fine
mkdir -p "$PROFILE_DIR/airootfs/usr/share/fonts/"
curl -Lo \
"$PROFILE_DIR/airootfs/usr/share/fonts/Fantasque-Sans-Mono-Nerd-Font.ttf" \
"https://github.com/adi1090x/rofi/raw/9c4093c665326bb08d6affc7e16d18d8f25c4452/fonts/Fantasque-Sans-Mono-Nerd-Font.ttf"
curl -Lo \
"$PROFILE_DIR/airootfs/usr/share/fonts/Feather.ttf" \
"https://github.com/adi1090x/rofi/raw/9c4093c665326bb08d6affc7e16d18d8f25c4452/fonts/Feather.ttf"
# SSH
mkdir -p "$SKEL_DIR/.ssh"
@ -214,15 +191,13 @@ function update_live_env() {
# Timezone
ln -sf "/usr/share/zoneinfo/$LINUX_TIME_ZONE" "$PROFILE_DIR/airootfs/etc/localtime"
if [[ "${1:-}" != "--minimal" ]]; then
# VNC password
mkdir "$SKEL_DIR/.vnc"
echo "$TECH_PASSWORD" | vncpasswd -f > "$SKEL_DIR/.vnc/passwd"
# VNC password
mkdir "$SKEL_DIR/.vnc"
echo "$TECH_PASSWORD" | vncpasswd -f > "$SKEL_DIR/.vnc/passwd"
# Wallpaper
mkdir -p "$PROFILE_DIR/airootfs/usr/share/wallpaper"
cp "$ROOT_DIR/images/Linux.png" "$PROFILE_DIR/airootfs/usr/share/wallpaper/burned.in"
fi
# Wallpaper
mkdir -p "$PROFILE_DIR/airootfs/usr/share/wallpaper"
cp "$ROOT_DIR/images/Linux.png" "$PROFILE_DIR/airootfs/usr/share/wallpaper/burned.in"
# udevil
mkdir -p "$PROFILE_DIR/airootfs/media"
@ -300,7 +275,9 @@ function install_deps() {
echo "Installing dependencies..."
packages=
while read -r line; do
packages="$packages $line"
if ! echo "$line" | grep -Fq "#"; then
packages="$packages $line"
fi
done < "$ROOT_DIR/setup/linux/packages/dependencies"
run_elevated pacman -Syu --needed --noconfirm $packages
}
@ -316,21 +293,12 @@ function build_linux() {
update_repo
fi
# Build requested version(s)
for version in "$@"; do
if [[ "$version" == "Full" ]]; then
copy_live_env
update_live_env
elif [[ "$version" == "Minimal" ]]; then
copy_live_env --minimal
update_live_env --minimal
fi
# Rerun script as root to start Archiso build process
run_elevated "$(realpath "$0")" --build-iso
# Cleanup
mv -nv "$PROFILE_DIR" "${PROFILE_DIR}.${version}"
perl-rename -v "s/(${KIT_NAME_SHORT}-Linux)-(${DATE}.*)/\1-${version}-\2/" "$OUT_DIR"/*
done
# Build live environment
copy_live_env
update_live_env
# Rerun script as root to start Archiso build process
run_elevated "$(realpath "$0")" --build-iso
}
function build_iso() {
@ -358,18 +326,6 @@ function build_iso() {
-v "$PROFILE_DIR" \
| tee -a "$LOG_DIR/$DATETIME.log"
# Build better ISO
rm -r "${ISO_DIR:-safety}/EFI"
rm -r "${ISO_DIR:-safety}/loader"
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
echo "Removing temp files..."
rm "$TEMP_DIR/Linux" -Rf | tee -a "$LOG_DIR/$DATETIME.log"
@ -383,13 +339,8 @@ function build_iso() {
# Check input
case ${1:-} in
-a|--build-all)
build_linux Full Minimal
echo Done
;;
-b|--build-full)
build_linux Full
-b|--build)
build_linux
echo Done
;;
@ -403,18 +354,6 @@ case ${1:-} in
echo Done
;;
-m|--build-minimal)
build_linux Minimal
echo Done
;;
-n|--prep-minimal-env)
load_settings --edit
copy_live_env --minimal
update_live_env --minimal
echo Done
;;
-o|--build-iso)
load_settings
build_iso
@ -437,18 +376,15 @@ case ${1:-} in
echo "Usage: $(basename "$0") [OPTIONS]"
echo ""
echo "Options:"
echo " -a --build-all Perform all tasks to build all isos"
echo " -b --build-full Perform all tasks to build the full iso"
echo " -m --build-minimal Perform all tasks to build the minimal iso"
echo " -h --help Show usage"
echo " -b --build Perform all tasks to build the iso"
echo " -h --help Show usage"
echo ""
echo "Advanced options:"
echo " -f --fix-perms Fix folder permissions"
echo " -i --install-deps Install build dependencies"
echo " -n --prep-minimal-env Prep live & airootfs folders (minimal packages)"
echo " -o --build-iso Build ISO (using current setup)"
echo " -p --prep-live-env Prep live & airootfs folders"
echo " -u --update-repo Update custom pacman repo"
echo " -f --fix-perms Fix folder permissions"
echo " -i --install-deps Install build dependencies"
echo " -o --build-iso Build ISO (using current setup)"
echo " -p --prep-live-env Prep live & airootfs folders"
echo " -u --update-repo Update custom pacman repo"
;;
esac

View file

@ -6,10 +6,6 @@
setlocal EnableDelayedExpansion
title WizardKit: Build Tool
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 :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)
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
set "script=%~dp0\.bin\Scripts\build_pe.ps1"
set "script=%~dp0\pe\build_pe.ps1"
powershell -executionpolicy bypass -noprofile -file %script% || goto ErrorUnknown
goto Exit

View file

@ -1,11 +1,3 @@
# WizardKit: Linux #
Files used to create the Linux build(s).
## Profiles ##
profile_base is used for both full and minimal Linux builds.
profile_gui is only used for full Linux builds.
NOTE: The Minimal Linux build is currently deprecated and may be removed from a future release.

View file

@ -4,11 +4,11 @@ hardinfo-gtk3
hfsprogs
iwgtk
memtest86-efi
mprime
openbox-patched
mprime-bin
opensuperclone-git
pipes.sh
smartmontools-svn
testdisk-wip
ttf-font-awesome-4
udevil
wd719x-firmware
wimboot-bin

View file

@ -2,95 +2,188 @@ aic94xx-firmware
alsa-utils
amd-ucode
antiword
arandr
arc-gtk-theme
arch-install-scripts
base
bc
bind
bluez
bluez-utils
bolt
btrfs-progs
cbatticon
chntpw
cmatrix
colordiff
conky
cpio
cryptsetup
curl
ddrescue
ddrescueview-bin
device-mapper
diffutils
dkms
dmidecode
dmraid
dos2unix
dosfstools
dunst
e2fsprogs
edk2-shell
efibootmgr
evince
exfatprogs
f2fs-tools
fatresize
feh
ffmpeg
firefox
foot-terminfo
gnome-keyring
gnu-netcat
gparted
gpicview
gptfdisk
grub
gsmartcontrol
hardinfo-gtk3
hexedit
hfsprogs
htop
inetutils
intel-ucode
iwd
iwgtk
jfsutils
kitty-terminfo
ldns
leafpad
less
lha
libewf
libinput
libldm
libusb-compat
libxft
linux
linux-firmware
linux-firmware-marvell
lm_sensors
lsscsi
lvm2
lzip
man-db
man-pages
mdadm
mediainfo
memtest86+
memtest86-efi
mesa-demos
mesa-utils
mkinitcpio
mkinitcpio-archiso
mprime
mkinitcpio-nfs-utils
mkvtoolnix-cli
mprime-bin
mpv
mtools
nano
nbd
ncdu
ndisc6
nfs-utils
nmap
noto-fonts
noto-fonts-cjk
ntfs-3g
numlockx
nvme-cli
open-iscsi
openbox
openssh
opensuperclone-git
otf-font-awesome-4
p7zip
papirus-icon-theme
parted
perl
picom
pipes.sh
reiserfsprogs
pv
python
python-docopt
python-prompt_toolkit
python-psutil
python-pytz
python-requests
qemu-guest-agent
refind
reiserfsprogs
rfkill
rng-tools
rofi
rsync
rxvt-unicode
rxvt-unicode-terminfo
sdparm
smartmontools-svn
sof-firmware
speedtest-cli
spice-vdagent
squashfs-tools
st
sudo
sysbench
sysfsutils
syslinux
systemd-resolvconf
systemd-sysvcompat
terminus-font
testdisk-wip
testdisk
texinfo
thunar
tigervnc
tint2
tk
tmux
tpm2-tools
tpm2-tss
tree
ttf-font-awesome-4
ttf-hack
ttf-inconsolata
udevil
udisks2
ufw
unarj
unrar
unzip
usb_modeswitch
usbmuxd
usbutils
util-linux
veracrypt
vim
virtualbox-guest-utils
volumeicon
wd719x-firmware
wezterm-terminfo
which
wimboot-bin
wimlib
wmctrl
xarchiver
xf86-input-libinput
xf86-video-amdgpu
xf86-video-fbdev
xf86-video-nouveau
xf86-video-qxl
xf86-video-vesa
xfsprogs
xorg-server
xorg-xdpyinfo
xorg-xev
xorg-xinit
xorg-xinput
xz
zip
zsh

View file

@ -1,6 +1,6 @@
pkgbase = st
pkgdesc = A simple virtual terminal emulator for X.
pkgver = 0.8.5
pkgver = 0.9
pkgrel = 1
url = https://st.suckless.org
arch = i686
@ -9,10 +9,10 @@ pkgbase = st
arch = aarch64
license = MIT
depends = libxft
source = https://dl.suckless.org/st/st-0.8.5.tar.gz
source = https://dl.suckless.org/st/st-0.9.tar.gz
source = terminfo.patch
source = README.terminfo.rst
sha256sums = ea6832203ed02ff74182bcb8adaa9ec454c8f989e79232cb859665e2f544ab37
sha256sums = f36359799734eae785becb374063f0be833cf22f88b4f169cd251b99324e08e7
sha256sums = f9deea445a5c6203a0e8e699f3c3b55e27275f17fb408562c4dd5d649edeea23
sha256sums = 0ebcbba881832adf9c98ce9fe7667c851d3cc3345077cb8ebe32702698665be2

View file

@ -1,10 +1,11 @@
# Maintainer: Jose Riha <jose1711 gmail com>
# Maintainer: Sebastian J. Bronner <waschtl@sbronner.com>
# Maintainer: Kevin Stolp <kevinstolp@gmail.com>
# Contributor: Patrick Jackson <PatrickSJackson gmail com>
# Contributor: Christoph Vigano <mail@cvigano.de>
pkgname=st
pkgver=0.8.5
pkgver=0.9
pkgrel=1
pkgdesc='A simple virtual terminal emulator for X.'
arch=('i686' 'x86_64' 'armv7h' 'aarch64')
@ -23,7 +24,7 @@ source=(https://dl.suckless.org/$pkgname/$pkgname-$pkgver.tar.gz
https://st.suckless.org/patches/scrollback/st-scrollback-mouse-altscreen-20220127-2c5edf2.diff
st-scrollback-mouse-increment-0.8.2-fixed.diff
)
sha256sums=('ea6832203ed02ff74182bcb8adaa9ec454c8f989e79232cb859665e2f544ab37'
sha256sums=('f36359799734eae785becb374063f0be833cf22f88b4f169cd251b99324e08e7'
'f9deea445a5c6203a0e8e699f3c3b55e27275f17fb408562c4dd5d649edeea23'
'0ebcbba881832adf9c98ce9fe7667c851d3cc3345077cb8ebe32702698665be2'
'42e4803ce2a67835f7e533a707a8a28e3804a26ced163145108970b9aee5fb81'

View file

@ -1,482 +0,0 @@
/* See LICENSE file for copyright and license details. */
/*
* appearance
*
* font: see http://freedesktop.org/software/fontconfig/fontconfig-user.html
*/
static char *font = "Liberation Mono:pixelsize=12:antialias=true:autohint=true";
static int borderpx = 2;
/*
* What program is execed by st depends of these precedence rules:
* 1: program passed with -e
* 2: scroll and/or utmp
* 3: SHELL environment variable
* 4: value of shell in /etc/passwd
* 5: value of shell in config.h
*/
static char *shell = "/bin/sh";
char *utmp = NULL;
/* scroll program: to enable use a string like "scroll" */
char *scroll = NULL;
char *stty_args = "stty raw pass8 nl -echo -iexten -cstopb 38400";
/* identification sequence returned in DA and DECID */
char *vtiden = "\033[?6c";
/* Kerning / character bounding-box multipliers */
static float cwscale = 1.0;
static float chscale = 1.0;
/*
* word delimiter string
*
* More advanced example: L" `'\"()[]{}"
*/
wchar_t *worddelimiters = L" ";
/* selection timeouts (in milliseconds) */
static unsigned int doubleclicktimeout = 300;
static unsigned int tripleclicktimeout = 600;
/* alt screens */
int allowaltscreen = 1;
/* allow certain non-interactive (insecure) window operations such as:
setting the clipboard text */
int allowwindowops = 0;
/*
* draw latency range in ms - from new content/keypress/etc until drawing.
* within this range, st draws when content stops arriving (idle). mostly it's
* near minlatency, but it waits longer for slow updates to avoid partial draw.
* low minlatency will tear/flicker more, as it can "detect" idle too early.
*/
static double minlatency = 8;
static double maxlatency = 33;
/*
* blinking timeout (set to 0 to disable blinking) for the terminal blinking
* attribute.
*/
static unsigned int blinktimeout = 800;
/*
* thickness of underline and bar cursors
*/
static unsigned int cursorthickness = 2;
/*
* bell volume. It must be a value between -100 and 100. Use 0 for disabling
* it
*/
static int bellvolume = 0;
/* default TERM value */
char *termname = "st-256color";
/*
* spaces per tab
*
* When you are changing this value, don't forget to adapt the »it« value in
* the st.info and appropriately install the st.info in the environment where
* you use this st version.
*
* it#$tabspaces,
*
* Secondly make sure your kernel is not expanding tabs. When running `stty
* -a` »tab0« should appear. You can tell the terminal to not expand tabs by
* running following command:
*
* stty tabs
*/
unsigned int tabspaces = 8;
/* bg opacity */
float alpha = 0.8;
/* Terminal colors (16 first used in escape sequence) */
static const char *colorname[] = {
/* 8 normal colors */
"#000000",
"#ff0000",
"#33ff00",
"#ffd000",
"#0066ff",
"#cc00ff",
"#00ffff",
"#d0d0d0",
/* 8 bright colors */
"#808080",
"#ff9900",
"#404040",
"#606060",
"#c0c0c0",
"#e0e0e0",
"#3300ff",
"#ffffff",
[255] = 0,
/* more colors can be added after 255 to use with DefaultXX */
"#cccccc",
"#555555",
"gray90", /* default foreground colour */
"black", /* default background colour */
};
/*
* Default colors (colorname index)
* foreground, background, cursor, reverse cursor
*/
unsigned int defaultfg = 258;
unsigned int defaultbg = 259;
unsigned int defaultcs = 256;
static unsigned int defaultrcs = 257;
/*
* Default shape of cursor
* 2: Block ("")
* 4: Underline ("_")
* 6: Bar ("|")
* 7: Snowman ("")
*/
static unsigned int cursorshape = 2;
/*
* Default columns and rows numbers
*/
static unsigned int cols = 80;
static unsigned int rows = 24;
/*
* Default colour and shape of the mouse cursor
*/
static unsigned int mouseshape = XC_xterm;
static unsigned int mousefg = 7;
static unsigned int mousebg = 0;
/*
* Color used to display font attributes when fontconfig selected a font which
* doesn't match the ones requested.
*/
static unsigned int defaultattr = 11;
/*
* Force mouse select/shortcuts while mask is active (when MODE_MOUSE is set).
* Note that if you want to use ShiftMask with selmasks, set this to an other
* modifier, set to 0 to not use it.
*/
static uint forcemousemod = ShiftMask;
/*
* Internal mouse shortcuts.
* Beware that overloading Button1 will disable the selection.
*/
const unsigned int mousescrollincrement = 4;
static MouseShortcut mshortcuts[] = {
/* mask button function argument release */
{ XK_ANY_MOD, Button4, kscrollup, {.i = mousescrollincrement}, 0, /* !alt */ -1 },
{ XK_ANY_MOD, Button5, kscrolldown, {.i = mousescrollincrement}, 0, /* !alt */ -1 },
{ XK_ANY_MOD, Button2, selpaste, {.i = 0}, 1 },
{ ShiftMask, Button4, ttysend, {.s = "\033[5;2~"} },
{ XK_ANY_MOD, Button4, ttysend, {.s = "\031"} },
{ ShiftMask, Button5, ttysend, {.s = "\033[6;2~"} },
{ XK_ANY_MOD, Button5, ttysend, {.s = "\005"} },
};
/* Internal keyboard shortcuts. */
#define MODKEY Mod1Mask
#define TERMMOD (ControlMask|ShiftMask)
static Shortcut shortcuts[] = {
/* mask keysym function argument */
{ XK_ANY_MOD, XK_Break, sendbreak, {.i = 0} },
{ ControlMask, XK_Print, toggleprinter, {.i = 0} },
{ ShiftMask, XK_Print, printscreen, {.i = 0} },
{ XK_ANY_MOD, XK_Print, printsel, {.i = 0} },
{ TERMMOD, XK_Prior, zoom, {.f = +1} },
{ TERMMOD, XK_Next, zoom, {.f = -1} },
{ TERMMOD, XK_Home, zoomreset, {.f = 0} },
{ TERMMOD, XK_C, clipcopy, {.i = 0} },
{ TERMMOD, XK_V, clippaste, {.i = 0} },
{ TERMMOD, XK_Y, selpaste, {.i = 0} },
{ ShiftMask, XK_Insert, selpaste, {.i = 0} },
{ TERMMOD, XK_Num_Lock, numlock, {.i = 0} },
{ ShiftMask, XK_Page_Up, kscrollup, {.i = -1} },
{ ShiftMask, XK_Page_Down, kscrolldown, {.i = -1} },
};
/*
* Special keys (change & recompile st.info accordingly)
*
* Mask value:
* * Use XK_ANY_MOD to match the key no matter modifiers state
* * Use XK_NO_MOD to match the key alone (no modifiers)
* appkey value:
* * 0: no value
* * > 0: keypad application mode enabled
* * = 2: term.numlock = 1
* * < 0: keypad application mode disabled
* appcursor value:
* * 0: no value
* * > 0: cursor application mode enabled
* * < 0: cursor application mode disabled
*
* Be careful with the order of the definitions because st searches in
* this table sequentially, so any XK_ANY_MOD must be in the last
* position for a key.
*/
/*
* If you want keys other than the X11 function keys (0xFD00 - 0xFFFF)
* to be mapped below, add them to this array.
*/
static KeySym mappedkeys[] = { -1 };
/*
* State bits to ignore when matching key or button events. By default,
* numlock (Mod2Mask) and keyboard layout (XK_SWITCH_MOD) are ignored.
*/
static uint ignoremod = Mod2Mask|XK_SWITCH_MOD;
/*
* This is the huge key array which defines all compatibility to the Linux
* world. Please decide about changes wisely.
*/
static Key key[] = {
/* keysym mask string appkey appcursor */
{ XK_KP_Home, ShiftMask, "\033[2J", 0, -1},
{ XK_KP_Home, ShiftMask, "\033[1;2H", 0, +1},
{ XK_KP_Home, XK_ANY_MOD, "\033[H", 0, -1},
{ XK_KP_Home, XK_ANY_MOD, "\033[1~", 0, +1},
{ XK_KP_Up, XK_ANY_MOD, "\033Ox", +1, 0},
{ XK_KP_Up, XK_ANY_MOD, "\033[A", 0, -1},
{ XK_KP_Up, XK_ANY_MOD, "\033OA", 0, +1},
{ XK_KP_Down, XK_ANY_MOD, "\033Or", +1, 0},
{ XK_KP_Down, XK_ANY_MOD, "\033[B", 0, -1},
{ XK_KP_Down, XK_ANY_MOD, "\033OB", 0, +1},
{ XK_KP_Left, XK_ANY_MOD, "\033Ot", +1, 0},
{ XK_KP_Left, XK_ANY_MOD, "\033[D", 0, -1},
{ XK_KP_Left, XK_ANY_MOD, "\033OD", 0, +1},
{ XK_KP_Right, XK_ANY_MOD, "\033Ov", +1, 0},
{ XK_KP_Right, XK_ANY_MOD, "\033[C", 0, -1},
{ XK_KP_Right, XK_ANY_MOD, "\033OC", 0, +1},
{ XK_KP_Prior, ShiftMask, "\033[5;2~", 0, 0},
{ XK_KP_Prior, XK_ANY_MOD, "\033[5~", 0, 0},
{ XK_KP_Begin, XK_ANY_MOD, "\033[E", 0, 0},
{ XK_KP_End, ControlMask, "\033[J", -1, 0},
{ XK_KP_End, ControlMask, "\033[1;5F", +1, 0},
{ XK_KP_End, ShiftMask, "\033[K", -1, 0},
{ XK_KP_End, ShiftMask, "\033[1;2F", +1, 0},
{ XK_KP_End, XK_ANY_MOD, "\033[4~", 0, 0},
{ XK_KP_Next, ShiftMask, "\033[6;2~", 0, 0},
{ XK_KP_Next, XK_ANY_MOD, "\033[6~", 0, 0},
{ XK_KP_Insert, ShiftMask, "\033[2;2~", +1, 0},
{ XK_KP_Insert, ShiftMask, "\033[4l", -1, 0},
{ XK_KP_Insert, ControlMask, "\033[L", -1, 0},
{ XK_KP_Insert, ControlMask, "\033[2;5~", +1, 0},
{ XK_KP_Insert, XK_ANY_MOD, "\033[4h", -1, 0},
{ XK_KP_Insert, XK_ANY_MOD, "\033[2~", +1, 0},
{ XK_KP_Delete, ControlMask, "\033[M", -1, 0},
{ XK_KP_Delete, ControlMask, "\033[3;5~", +1, 0},
{ XK_KP_Delete, ShiftMask, "\033[2K", -1, 0},
{ XK_KP_Delete, ShiftMask, "\033[3;2~", +1, 0},
{ XK_KP_Delete, XK_ANY_MOD, "\033[3~", -1, 0},
{ XK_KP_Delete, XK_ANY_MOD, "\033[3~", +1, 0},
{ XK_KP_Multiply, XK_ANY_MOD, "\033Oj", +2, 0},
{ XK_KP_Add, XK_ANY_MOD, "\033Ok", +2, 0},
{ XK_KP_Enter, XK_ANY_MOD, "\033OM", +2, 0},
{ XK_KP_Enter, XK_ANY_MOD, "\r", -1, 0},
{ XK_KP_Subtract, XK_ANY_MOD, "\033Om", +2, 0},
{ XK_KP_Decimal, XK_ANY_MOD, "\033On", +2, 0},
{ XK_KP_Divide, XK_ANY_MOD, "\033Oo", +2, 0},
{ XK_KP_0, XK_ANY_MOD, "\033Op", +2, 0},
{ XK_KP_1, XK_ANY_MOD, "\033Oq", +2, 0},
{ XK_KP_2, XK_ANY_MOD, "\033Or", +2, 0},
{ XK_KP_3, XK_ANY_MOD, "\033Os", +2, 0},
{ XK_KP_4, XK_ANY_MOD, "\033Ot", +2, 0},
{ XK_KP_5, XK_ANY_MOD, "\033Ou", +2, 0},
{ XK_KP_6, XK_ANY_MOD, "\033Ov", +2, 0},
{ XK_KP_7, XK_ANY_MOD, "\033Ow", +2, 0},
{ XK_KP_8, XK_ANY_MOD, "\033Ox", +2, 0},
{ XK_KP_9, XK_ANY_MOD, "\033Oy", +2, 0},
{ XK_Up, ShiftMask, "\033[1;2A", 0, 0},
{ XK_Up, Mod1Mask, "\033[1;3A", 0, 0},
{ XK_Up, ShiftMask|Mod1Mask,"\033[1;4A", 0, 0},
{ XK_Up, ControlMask, "\033[1;5A", 0, 0},
{ XK_Up, ShiftMask|ControlMask,"\033[1;6A", 0, 0},
{ XK_Up, ControlMask|Mod1Mask,"\033[1;7A", 0, 0},
{ XK_Up,ShiftMask|ControlMask|Mod1Mask,"\033[1;8A", 0, 0},
{ XK_Up, XK_ANY_MOD, "\033[A", 0, -1},
{ XK_Up, XK_ANY_MOD, "\033OA", 0, +1},
{ XK_Down, ShiftMask, "\033[1;2B", 0, 0},
{ XK_Down, Mod1Mask, "\033[1;3B", 0, 0},
{ XK_Down, ShiftMask|Mod1Mask,"\033[1;4B", 0, 0},
{ XK_Down, ControlMask, "\033[1;5B", 0, 0},
{ XK_Down, ShiftMask|ControlMask,"\033[1;6B", 0, 0},
{ XK_Down, ControlMask|Mod1Mask,"\033[1;7B", 0, 0},
{ XK_Down,ShiftMask|ControlMask|Mod1Mask,"\033[1;8B",0, 0},
{ XK_Down, XK_ANY_MOD, "\033[B", 0, -1},
{ XK_Down, XK_ANY_MOD, "\033OB", 0, +1},
{ XK_Left, ShiftMask, "\033[1;2D", 0, 0},
{ XK_Left, Mod1Mask, "\033[1;3D", 0, 0},
{ XK_Left, ShiftMask|Mod1Mask,"\033[1;4D", 0, 0},
{ XK_Left, ControlMask, "\033[1;5D", 0, 0},
{ XK_Left, ShiftMask|ControlMask,"\033[1;6D", 0, 0},
{ XK_Left, ControlMask|Mod1Mask,"\033[1;7D", 0, 0},
{ XK_Left,ShiftMask|ControlMask|Mod1Mask,"\033[1;8D",0, 0},
{ XK_Left, XK_ANY_MOD, "\033[D", 0, -1},
{ XK_Left, XK_ANY_MOD, "\033OD", 0, +1},
{ XK_Right, ShiftMask, "\033[1;2C", 0, 0},
{ XK_Right, Mod1Mask, "\033[1;3C", 0, 0},
{ XK_Right, ShiftMask|Mod1Mask,"\033[1;4C", 0, 0},
{ XK_Right, ControlMask, "\033[1;5C", 0, 0},
{ XK_Right, ShiftMask|ControlMask,"\033[1;6C", 0, 0},
{ XK_Right, ControlMask|Mod1Mask,"\033[1;7C", 0, 0},
{ XK_Right,ShiftMask|ControlMask|Mod1Mask,"\033[1;8C",0, 0},
{ XK_Right, XK_ANY_MOD, "\033[C", 0, -1},
{ XK_Right, XK_ANY_MOD, "\033OC", 0, +1},
{ XK_ISO_Left_Tab, ShiftMask, "\033[Z", 0, 0},
{ XK_Return, Mod1Mask, "\033\r", 0, 0},
{ XK_Return, XK_ANY_MOD, "\r", 0, 0},
{ XK_Insert, ShiftMask, "\033[4l", -1, 0},
{ XK_Insert, ShiftMask, "\033[2;2~", +1, 0},
{ XK_Insert, ControlMask, "\033[L", -1, 0},
{ XK_Insert, ControlMask, "\033[2;5~", +1, 0},
{ XK_Insert, XK_ANY_MOD, "\033[4h", -1, 0},
{ XK_Insert, XK_ANY_MOD, "\033[2~", +1, 0},
{ XK_Delete, ControlMask, "\033[M", -1, 0},
{ XK_Delete, ControlMask, "\033[3;5~", +1, 0},
{ XK_Delete, ShiftMask, "\033[2K", -1, 0},
{ XK_Delete, ShiftMask, "\033[3;2~", +1, 0},
{ XK_Delete, XK_ANY_MOD, "\033[3~", -1, 0},
{ XK_Delete, XK_ANY_MOD, "\033[3~", +1, 0},
{ XK_BackSpace, XK_NO_MOD, "\177", 0, 0},
{ XK_BackSpace, Mod1Mask, "\033\177", 0, 0},
{ XK_Home, ShiftMask, "\033[2J", 0, -1},
{ XK_Home, ShiftMask, "\033[1;2H", 0, +1},
{ XK_Home, XK_ANY_MOD, "\033[H", 0, -1},
{ XK_Home, XK_ANY_MOD, "\033[1~", 0, +1},
{ XK_End, ControlMask, "\033[J", -1, 0},
{ XK_End, ControlMask, "\033[1;5F", +1, 0},
{ XK_End, ShiftMask, "\033[K", -1, 0},
{ XK_End, ShiftMask, "\033[1;2F", +1, 0},
{ XK_End, XK_ANY_MOD, "\033[4~", 0, 0},
{ XK_Prior, ControlMask, "\033[5;5~", 0, 0},
{ XK_Prior, ShiftMask, "\033[5;2~", 0, 0},
{ XK_Prior, XK_ANY_MOD, "\033[5~", 0, 0},
{ XK_Next, ControlMask, "\033[6;5~", 0, 0},
{ XK_Next, ShiftMask, "\033[6;2~", 0, 0},
{ XK_Next, XK_ANY_MOD, "\033[6~", 0, 0},
{ XK_F1, XK_NO_MOD, "\033OP" , 0, 0},
{ XK_F1, /* F13 */ ShiftMask, "\033[1;2P", 0, 0},
{ XK_F1, /* F25 */ ControlMask, "\033[1;5P", 0, 0},
{ XK_F1, /* F37 */ Mod4Mask, "\033[1;6P", 0, 0},
{ XK_F1, /* F49 */ Mod1Mask, "\033[1;3P", 0, 0},
{ XK_F1, /* F61 */ Mod3Mask, "\033[1;4P", 0, 0},
{ XK_F2, XK_NO_MOD, "\033OQ" , 0, 0},
{ XK_F2, /* F14 */ ShiftMask, "\033[1;2Q", 0, 0},
{ XK_F2, /* F26 */ ControlMask, "\033[1;5Q", 0, 0},
{ XK_F2, /* F38 */ Mod4Mask, "\033[1;6Q", 0, 0},
{ XK_F2, /* F50 */ Mod1Mask, "\033[1;3Q", 0, 0},
{ XK_F2, /* F62 */ Mod3Mask, "\033[1;4Q", 0, 0},
{ XK_F3, XK_NO_MOD, "\033OR" , 0, 0},
{ XK_F3, /* F15 */ ShiftMask, "\033[1;2R", 0, 0},
{ XK_F3, /* F27 */ ControlMask, "\033[1;5R", 0, 0},
{ XK_F3, /* F39 */ Mod4Mask, "\033[1;6R", 0, 0},
{ XK_F3, /* F51 */ Mod1Mask, "\033[1;3R", 0, 0},
{ XK_F3, /* F63 */ Mod3Mask, "\033[1;4R", 0, 0},
{ XK_F4, XK_NO_MOD, "\033OS" , 0, 0},
{ XK_F4, /* F16 */ ShiftMask, "\033[1;2S", 0, 0},
{ XK_F4, /* F28 */ ControlMask, "\033[1;5S", 0, 0},
{ XK_F4, /* F40 */ Mod4Mask, "\033[1;6S", 0, 0},
{ XK_F4, /* F52 */ Mod1Mask, "\033[1;3S", 0, 0},
{ XK_F5, XK_NO_MOD, "\033[15~", 0, 0},
{ XK_F5, /* F17 */ ShiftMask, "\033[15;2~", 0, 0},
{ XK_F5, /* F29 */ ControlMask, "\033[15;5~", 0, 0},
{ XK_F5, /* F41 */ Mod4Mask, "\033[15;6~", 0, 0},
{ XK_F5, /* F53 */ Mod1Mask, "\033[15;3~", 0, 0},
{ XK_F6, XK_NO_MOD, "\033[17~", 0, 0},
{ XK_F6, /* F18 */ ShiftMask, "\033[17;2~", 0, 0},
{ XK_F6, /* F30 */ ControlMask, "\033[17;5~", 0, 0},
{ XK_F6, /* F42 */ Mod4Mask, "\033[17;6~", 0, 0},
{ XK_F6, /* F54 */ Mod1Mask, "\033[17;3~", 0, 0},
{ XK_F7, XK_NO_MOD, "\033[18~", 0, 0},
{ XK_F7, /* F19 */ ShiftMask, "\033[18;2~", 0, 0},
{ XK_F7, /* F31 */ ControlMask, "\033[18;5~", 0, 0},
{ XK_F7, /* F43 */ Mod4Mask, "\033[18;6~", 0, 0},
{ XK_F7, /* F55 */ Mod1Mask, "\033[18;3~", 0, 0},
{ XK_F8, XK_NO_MOD, "\033[19~", 0, 0},
{ XK_F8, /* F20 */ ShiftMask, "\033[19;2~", 0, 0},
{ XK_F8, /* F32 */ ControlMask, "\033[19;5~", 0, 0},
{ XK_F8, /* F44 */ Mod4Mask, "\033[19;6~", 0, 0},
{ XK_F8, /* F56 */ Mod1Mask, "\033[19;3~", 0, 0},
{ XK_F9, XK_NO_MOD, "\033[20~", 0, 0},
{ XK_F9, /* F21 */ ShiftMask, "\033[20;2~", 0, 0},
{ XK_F9, /* F33 */ ControlMask, "\033[20;5~", 0, 0},
{ XK_F9, /* F45 */ Mod4Mask, "\033[20;6~", 0, 0},
{ XK_F9, /* F57 */ Mod1Mask, "\033[20;3~", 0, 0},
{ XK_F10, XK_NO_MOD, "\033[21~", 0, 0},
{ XK_F10, /* F22 */ ShiftMask, "\033[21;2~", 0, 0},
{ XK_F10, /* F34 */ ControlMask, "\033[21;5~", 0, 0},
{ XK_F10, /* F46 */ Mod4Mask, "\033[21;6~", 0, 0},
{ XK_F10, /* F58 */ Mod1Mask, "\033[21;3~", 0, 0},
{ XK_F11, XK_NO_MOD, "\033[23~", 0, 0},
{ XK_F11, /* F23 */ ShiftMask, "\033[23;2~", 0, 0},
{ XK_F11, /* F35 */ ControlMask, "\033[23;5~", 0, 0},
{ XK_F11, /* F47 */ Mod4Mask, "\033[23;6~", 0, 0},
{ XK_F11, /* F59 */ Mod1Mask, "\033[23;3~", 0, 0},
{ XK_F12, XK_NO_MOD, "\033[24~", 0, 0},
{ XK_F12, /* F24 */ ShiftMask, "\033[24;2~", 0, 0},
{ XK_F12, /* F36 */ ControlMask, "\033[24;5~", 0, 0},
{ XK_F12, /* F48 */ Mod4Mask, "\033[24;6~", 0, 0},
{ XK_F12, /* F60 */ Mod1Mask, "\033[24;3~", 0, 0},
{ XK_F13, XK_NO_MOD, "\033[1;2P", 0, 0},
{ XK_F14, XK_NO_MOD, "\033[1;2Q", 0, 0},
{ XK_F15, XK_NO_MOD, "\033[1;2R", 0, 0},
{ XK_F16, XK_NO_MOD, "\033[1;2S", 0, 0},
{ XK_F17, XK_NO_MOD, "\033[15;2~", 0, 0},
{ XK_F18, XK_NO_MOD, "\033[17;2~", 0, 0},
{ XK_F19, XK_NO_MOD, "\033[18;2~", 0, 0},
{ XK_F20, XK_NO_MOD, "\033[19;2~", 0, 0},
{ XK_F21, XK_NO_MOD, "\033[20;2~", 0, 0},
{ XK_F22, XK_NO_MOD, "\033[21;2~", 0, 0},
{ XK_F23, XK_NO_MOD, "\033[23;2~", 0, 0},
{ XK_F24, XK_NO_MOD, "\033[24;2~", 0, 0},
{ XK_F25, XK_NO_MOD, "\033[1;5P", 0, 0},
{ XK_F26, XK_NO_MOD, "\033[1;5Q", 0, 0},
{ XK_F27, XK_NO_MOD, "\033[1;5R", 0, 0},
{ XK_F28, XK_NO_MOD, "\033[1;5S", 0, 0},
{ XK_F29, XK_NO_MOD, "\033[15;5~", 0, 0},
{ XK_F30, XK_NO_MOD, "\033[17;5~", 0, 0},
{ XK_F31, XK_NO_MOD, "\033[18;5~", 0, 0},
{ XK_F32, XK_NO_MOD, "\033[19;5~", 0, 0},
{ XK_F33, XK_NO_MOD, "\033[20;5~", 0, 0},
{ XK_F34, XK_NO_MOD, "\033[21;5~", 0, 0},
{ XK_F35, XK_NO_MOD, "\033[23;5~", 0, 0},
};
/*
* Selection types' masks.
* Use the same masks as usual.
* Button1Mask is always unset, to make masks match between ButtonPress.
* ButtonRelease and MotionNotify.
* If no match is found, regular selection is used.
*/
static uint selmasks[] = {
[SEL_RECTANGULAR] = Mod1Mask,
};
/*
* Printable characters in ASCII, used to estimate the advance width
* of single wide characters.
*/
static char ascii_printable[] =
" !\"#$%&'()*+,-./0123456789:;<=>?"
"@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_"
"`abcdefghijklmnopqrstuvwxyz{|}~";

View file

@ -1,43 +0,0 @@
--- a/config.def.h 2021-12-20 21:35:22.214525009 -0700
+++ b/config.def.h 2021-12-20 21:38:26.933659040 -0700
@@ -99,24 +99,24 @@
/* Terminal colors (16 first used in escape sequence) */
static const char *colorname[] = {
/* 8 normal colors */
- "black",
- "red3",
- "green3",
- "yellow3",
- "blue2",
- "magenta3",
- "cyan3",
- "gray90",
+ "#000000",
+ "#ff0000",
+ "#33ff00",
+ "#ffd000",
+ "#0066ff",
+ "#cc00ff",
+ "#00ffff",
+ "#d0d0d0",
/* 8 bright colors */
- "gray50",
- "red",
- "green",
- "yellow",
- "#5c5cff",
- "magenta",
- "cyan",
- "white",
+ "#808080",
+ "#ff9900",
+ "#404040",
+ "#606060",
+ "#c0c0c0",
+ "#e0e0e0",
+ "#3300ff",
+ "#ffffff",
[255] = 0,

View file

@ -1,39 +1,35 @@
# WizardKit: Package dependencies
archiso
attr
base-devel
boost
curl
dos2unix
erofs-utils
git
gtk-doc
gtk3
hwloc
imlib2
iwd
json-glib
lhasa
libbsd
libewf
librsvg
libsm
libxcursor
libxft
libxinerama
libxml2
libxrandr
ntfs-3g
openssh
memtest86+
memtest86+-efi
p7zip
pango
perl-rename
reiserfsprogs
pv
refind
rsync
startup-notification
subversion
syslinux
tigervnc
unzip
wpa_supplicant
# hardinfo-gtk3 / opensuperclone-git
cmake
# iwgtk
gtk4
meson
qrencode
scdoc
# smartmontools-svn
subversion
# udevil
gettext
intltool
# wd719x-firmware
lha

View file

@ -1,56 +0,0 @@
arandr
arc-gtk-theme
cbatticon
compton
conky
ddrescueview-bin
dunst
evince
feh
ffmpeg
firefox
gnome-keyring
gparted
gpicview
gsmartcontrol
hardinfo-gtk3
iwgtk
leafpad
libinput
libxft
mesa-demos
mkvtoolnix-cli
mpv
noto-fonts
noto-fonts-cjk
numlockx
openbox-patched
otf-font-awesome-4
papirus-icon-theme
qemu-guest-agent
rofi
rxvt-unicode
spice-vdagent
st
thunar
tigervnc
tint2
tk
ttf-font-awesome-4
ttf-hack
ttf-inconsolata
veracrypt
virtualbox-guest-utils
volumeicon
wmctrl
xarchiver
xf86-input-libinput
xf86-video-amdgpu
xf86-video-fbdev
xf86-video-nouveau
xf86-video-vesa
xorg-server
xorg-xdpyinfo
xorg-xev
xorg-xinit
xorg-xinput

View file

@ -0,0 +1 @@
LANG=C.UTF-8

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"

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