From 759cd123793a1b4ed12dbe45461d4a0d26e1554d Mon Sep 17 00:00:00 2001 From: 2Shirt <2xShirt@gmail.com> Date: Thu, 15 Apr 2021 19:48:36 -0600 Subject: [PATCH 01/43] Reoder Windows functions --- scripts/wk/os/win.py | 202 ++++++++++++++++++++++--------------------- 1 file changed, 102 insertions(+), 100 deletions(-) diff --git a/scripts/wk/os/win.py b/scripts/wk/os/win.py index eadcab71..8f1cc52c 100644 --- a/scripts/wk/os/win.py +++ b/scripts/wk/os/win.py @@ -56,7 +56,7 @@ REG_MSISERVER = r'HKLM\SYSTEM\CurrentControlSet\Control\SafeBoot\Network\MSIServ SLMGR = pathlib.Path(f'{os.environ.get("SYSTEMROOT")}/System32/slmgr.vbs') -# Functions +# Activation Functions def activate_with_bios(): """Attempt to activate Windows with a key stored in the BIOS.""" # Code borrowed from https://github.com/aeruder/get_win8key @@ -96,36 +96,6 @@ def activate_with_bios(): raise GenericError('Activation Failed') -def disable_safemode(): - """Edit BCD to remove safeboot value.""" - cmd = ['bcdedit', '/deletevalue', '{default}', 'safeboot'] - run_program(cmd) - - -def disable_safemode_msi(): - """Disable MSI access under safemode.""" - cmd = ['reg', 'delete', REG_MSISERVER, '/f'] - run_program(cmd) - - -def enable_safemode(): - """Edit BCD to set safeboot as default.""" - cmd = ['bcdedit', '/set', '{default}', 'safeboot', 'network'] - run_program(cmd) - - -def enable_safemode_msi(): - """Enable MSI access under safemode.""" - cmd = ['reg', 'add', REG_MSISERVER, '/f'] - run_program(cmd) - cmd = [ - 'reg', 'add', REG_MSISERVER, '/ve', - '/t', 'REG_SZ', - '/d', 'Service', '/f', - ] - run_program(cmd) - - def get_activation_string(): """Get activation status, returns str.""" cmd = ['cscript', '//nologo', SLMGR, '/xpr'] @@ -144,75 +114,6 @@ def is_activated(): return act_str and 'permanent' in act_str -def run_chkdsk_offline(): - """Set filesystem 'dirty bit' to force a CHKDSK during startup.""" - cmd = f'fsutil dirty set {os.environ.get("SYSTEMDRIVE")}' - proc = run_program(cmd.split(), check=False) - - # Check result - if proc.returncode > 0: - raise GenericError('Failed to set dirty bit.') - - -def run_chkdsk_online(): - """Run CHKDSK in a split window. - - NOTE: If run on Windows 8+ online repairs are attempted. - """ - cmd = ['CHKDSK', os.environ.get('SYSTEMDRIVE', 'C:')] - if OS_VERSION >= 8: - cmd.extend(['/scan', '/perf']) - log_path = format_log_path(log_name='CHKDSK', tool=True) - err_path = log_path.with_suffix('.err') - - # Run scan - proc = run_program(cmd, check=False) - - # Check result - if proc.returncode == 1: - raise GenericWarning('Repaired (or manually aborted)') - if proc.returncode > 1: - raise GenericError('Issue(s) detected') - - # Save output - os.makedirs(log_path.parent, exist_ok=True) - with open(log_path, 'w') as _f: - _f.write(proc.stdout) - with open(err_path, 'w') as _f: - _f.write(proc.stderr) - - -def run_sfc_scan(): - """Run SFC and save results.""" - cmd = ['sfc', '/scannow'] - log_path = format_log_path(log_name='SFC', tool=True) - err_path = log_path.with_suffix('.err') - - # Run SFC - proc = run_program(cmd, check=False, encoding='utf-16') - - # Fix paths - log_path = non_clobber_path(log_path) - err_path = non_clobber_path(err_path) - - # Save output - os.makedirs(log_path.parent, exist_ok=True) - with open(log_path, 'w') as _f: - _f.write(proc.stdout) - with open(err_path, 'w') as _f: - _f.write(proc.stderr) - - # Check result - if 'did not find any integrity violations' in proc.stdout: - pass - elif 'successfully repaired' in proc.stdout: - raise GenericWarning('Repaired') - elif 'found corrupt files' in proc.stdout: - raise GenericError('Corruption detected') - else: - raise OSError - - # Registry Functions def reg_delete_key(hive, key, recurse=False): # pylint: disable=raise-missing-from @@ -407,5 +308,106 @@ def reg_set_value(hive, key, name, data, data_type, option=None): winreg.SetValue(hive, key, data_type, data) +# Repair Functions +def run_chkdsk_offline(): + """Set filesystem 'dirty bit' to force a CHKDSK during startup.""" + cmd = f'fsutil dirty set {os.environ.get("SYSTEMDRIVE")}' + proc = run_program(cmd.split(), check=False) + + # Check result + if proc.returncode > 0: + raise GenericError('Failed to set dirty bit.') + + +def run_chkdsk_online(): + """Run CHKDSK in a split window. + + NOTE: If run on Windows 8+ online repairs are attempted. + """ + cmd = ['CHKDSK', os.environ.get('SYSTEMDRIVE', 'C:')] + if OS_VERSION >= 8: + cmd.extend(['/scan', '/perf']) + log_path = format_log_path(log_name='CHKDSK', tool=True) + err_path = log_path.with_suffix('.err') + + # Run scan + proc = run_program(cmd, check=False) + + # Check result + if proc.returncode == 1: + raise GenericWarning('Repaired (or manually aborted)') + if proc.returncode > 1: + raise GenericError('Issue(s) detected') + + # Save output + os.makedirs(log_path.parent, exist_ok=True) + with open(log_path, 'w') as _f: + _f.write(proc.stdout) + with open(err_path, 'w') as _f: + _f.write(proc.stderr) + + +def run_sfc_scan(): + """Run SFC and save results.""" + cmd = ['sfc', '/scannow'] + log_path = format_log_path(log_name='SFC', tool=True) + err_path = log_path.with_suffix('.err') + + # Run SFC + proc = run_program(cmd, check=False, encoding='utf-16') + + # Fix paths + log_path = non_clobber_path(log_path) + err_path = non_clobber_path(err_path) + + # Save output + os.makedirs(log_path.parent, exist_ok=True) + with open(log_path, 'w') as _f: + _f.write(proc.stdout) + with open(err_path, 'w') as _f: + _f.write(proc.stderr) + + # Check result + if 'did not find any integrity violations' in proc.stdout: + pass + elif 'successfully repaired' in proc.stdout: + raise GenericWarning('Repaired') + elif 'found corrupt files' in proc.stdout: + raise GenericError('Corruption detected') + else: + raise OSError + + +# Safe Mode Functions +def disable_safemode(): + """Edit BCD to remove safeboot value.""" + cmd = ['bcdedit', '/deletevalue', '{default}', 'safeboot'] + run_program(cmd) + + +def disable_safemode_msi(): + """Disable MSI access under safemode.""" + cmd = ['reg', 'delete', REG_MSISERVER, '/f'] + run_program(cmd) + + +def enable_safemode(): + """Edit BCD to set safeboot as default.""" + cmd = ['bcdedit', '/set', '{default}', 'safeboot', 'network'] + run_program(cmd) + + +def enable_safemode_msi(): + """Enable MSI access under safemode.""" + cmd = ['reg', 'add', REG_MSISERVER, '/f'] + run_program(cmd) + cmd = [ + 'reg', 'add', REG_MSISERVER, '/ve', + '/t', 'REG_SZ', + '/d', 'Service', '/f', + ] + run_program(cmd) + + if __name__ == '__main__': print("This file is not meant to be called directly.") From ed6f188eb2a66c712d94203430aa2352d2ef9fba Mon Sep 17 00:00:00 2001 From: 2Shirt <2xShirt@gmail.com> Date: Thu, 15 Apr 2021 20:04:33 -0600 Subject: [PATCH 02/43] Avoid pylint errors under Linux/macOS --- scripts/wk/os/win.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/scripts/wk/os/win.py b/scripts/wk/os/win.py index 8f1cc52c..44e7efb4 100644 --- a/scripts/wk/os/win.py +++ b/scripts/wk/os/win.py @@ -5,10 +5,15 @@ import logging import os import pathlib import platform -import winreg from contextlib import suppress +try: + import winreg +except ImportError as err: + if platform.system() == 'Windows': + raise err + from wk.borrowed import acpi from wk.exe import run_program from wk.io import non_clobber_path @@ -126,7 +131,7 @@ def reg_delete_key(hive, key, recurse=False): # Delete subkeys first if recurse: - with suppress(WindowsError), winreg.OpenKey(hive, key) as open_key: + with suppress(OSError), winreg.OpenKey(hive, key) as open_key: while True: subkey = fr'{key}\{winreg.EnumKey(open_key, 0)}' reg_delete_key(hive, subkey, recurse=recurse) From 943c1e11b9574f8b0e38aa96048a5cc4ce6547e7 Mon Sep 17 00:00:00 2001 From: 2Shirt <2xShirt@gmail.com> Date: Thu, 15 Apr 2021 21:13:28 -0600 Subject: [PATCH 03/43] Retry CHKDSK on failures Fixes issue #159 --- scripts/wk/os/win.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/scripts/wk/os/win.py b/scripts/wk/os/win.py index 44e7efb4..63100b7c 100644 --- a/scripts/wk/os/win.py +++ b/scripts/wk/os/win.py @@ -334,21 +334,26 @@ def run_chkdsk_online(): cmd.extend(['/scan', '/perf']) log_path = format_log_path(log_name='CHKDSK', tool=True) err_path = log_path.with_suffix('.err') + retried = False # Run scan proc = run_program(cmd, check=False) + if proc.returncode > 1: + # Try again + retried = True + proc = run_program(cmd, check=False) # Check result - if proc.returncode == 1: + if (proc.returncode == 0 and retried) or proc.returncode == 1: raise GenericWarning('Repaired (or manually aborted)') if proc.returncode > 1: raise GenericError('Issue(s) detected') # Save output os.makedirs(log_path.parent, exist_ok=True) - with open(log_path, 'w') as _f: + with open(log_path, 'a') as _f: _f.write(proc.stdout) - with open(err_path, 'w') as _f: + with open(err_path, 'a') as _f: _f.write(proc.stderr) From 47b49077da5142b9a82bc514dc10a11bc8357306 Mon Sep 17 00:00:00 2001 From: 2Shirt <2xShirt@gmail.com> Date: Thu, 15 Apr 2021 21:16:02 -0600 Subject: [PATCH 04/43] Show CHKDSK progress in separate pane under ConEmu --- scripts/wk/exe.py | 7 ++++++- scripts/wk/os/win.py | 34 ++++++++++++++++------------------ 2 files changed, 22 insertions(+), 19 deletions(-) diff --git a/scripts/wk/exe.py b/scripts/wk/exe.py index 87b90935..82e9dff3 100644 --- a/scripts/wk/exe.py +++ b/scripts/wk/exe.py @@ -130,7 +130,7 @@ def get_json_from_command(cmd, check=True, encoding='utf-8', errors='ignore'): return json_data -def get_procs(name, exact=True): +def get_procs(name, exact=True, try_again=True): """Get process object(s) based on name, returns list of proc objects.""" LOG.debug('name: %s, exact: %s', name, exact) processes = [] @@ -141,6 +141,11 @@ def get_procs(name, exact=True): if re.search(regex, proc.name(), re.IGNORECASE): processes.append(proc) + # Try again? + if not processes and try_again: + time.sleep(1) + processes = get_procs(name, exact, try_again=False) + # Done return processes diff --git a/scripts/wk/os/win.py b/scripts/wk/os/win.py index 63100b7c..919a4406 100644 --- a/scripts/wk/os/win.py +++ b/scripts/wk/os/win.py @@ -15,7 +15,7 @@ except ImportError as err: raise err from wk.borrowed import acpi -from wk.exe import run_program +from wk.exe import get_procs, run_program from wk.io import non_clobber_path from wk.log import format_log_path from wk.std import GenericError, GenericWarning, sleep @@ -23,6 +23,7 @@ from wk.std import GenericError, GenericWarning, sleep # STATIC VARIABLES LOG = logging.getLogger(__name__) +CONEMU = 'ConEmuPID' in os.environ KNOWN_DATA_TYPES = { 'BINARY': winreg.REG_BINARY, 'DWORD': winreg.REG_DWORD, @@ -316,8 +317,8 @@ def reg_set_value(hive, key, name, data, data_type, option=None): # Repair Functions def run_chkdsk_offline(): """Set filesystem 'dirty bit' to force a CHKDSK during startup.""" - cmd = f'fsutil dirty set {os.environ.get("SYSTEMDRIVE")}' - proc = run_program(cmd.split(), check=False) + cmd = ['fsutil', 'dirty', 'set', os.environ.get('SYSTEMDRIVE', 'C:')] + proc = run_program(cmd, check=False) # Check result if proc.returncode > 0: @@ -325,37 +326,34 @@ def run_chkdsk_offline(): def run_chkdsk_online(): - """Run CHKDSK in a split window. + """Run CHKDSK. NOTE: If run on Windows 8+ online repairs are attempted. """ cmd = ['CHKDSK', os.environ.get('SYSTEMDRIVE', 'C:')] if OS_VERSION >= 8: cmd.extend(['/scan', '/perf']) - log_path = format_log_path(log_name='CHKDSK', tool=True) - err_path = log_path.with_suffix('.err') + if CONEMU: + cmd.extend(['-new_console:n', '-new_console:s33V']) retried = False # Run scan - proc = run_program(cmd, check=False) - if proc.returncode > 1: + run_program(cmd, check=False) + proc = get_procs('chkdsk.exe')[0] + return_code = proc.wait() + if return_code > 1: # Try again retried = True - proc = run_program(cmd, check=False) + run_program(cmd, check=False) + proc = get_procs('chkdsk.exe')[0] + return_code = proc.wait() # Check result - if (proc.returncode == 0 and retried) or proc.returncode == 1: + if (return_code == 0 and retried) or return_code == 1: raise GenericWarning('Repaired (or manually aborted)') - if proc.returncode > 1: + if return_code > 1: raise GenericError('Issue(s) detected') - # Save output - os.makedirs(log_path.parent, exist_ok=True) - with open(log_path, 'a') as _f: - _f.write(proc.stdout) - with open(err_path, 'a') as _f: - _f.write(proc.stderr) - def run_sfc_scan(): """Run SFC and save results.""" From e088f705ba9324c6c817691831ebbc236b7d4e1a Mon Sep 17 00:00:00 2001 From: 2Shirt <2xShirt@gmail.com> Date: Thu, 15 Apr 2021 23:33:11 -0600 Subject: [PATCH 05/43] Add run_dism() --- scripts/wk/exe.py | 4 ++-- scripts/wk/os/win.py | 37 ++++++++++++++++++++++++++++++++++++- 2 files changed, 38 insertions(+), 3 deletions(-) diff --git a/scripts/wk/exe.py b/scripts/wk/exe.py index 82e9dff3..5edeb041 100644 --- a/scripts/wk/exe.py +++ b/scripts/wk/exe.py @@ -247,10 +247,10 @@ def wait_for_procs(name, exact=True, timeout=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) - results = psutil.wait_procs(target_procs, timeout=timeout) + procs = psutil.wait_procs(target_procs, timeout=timeout) # Raise exception if necessary - if results[1]: # Alive processes + if procs[1]: # Alive processes raise psutil.TimeoutExpired(name=name, seconds=timeout) diff --git a/scripts/wk/os/win.py b/scripts/wk/os/win.py index 919a4406..415679e5 100644 --- a/scripts/wk/os/win.py +++ b/scripts/wk/os/win.py @@ -15,7 +15,7 @@ except ImportError as err: raise err from wk.borrowed import acpi -from wk.exe import get_procs, run_program +from wk.exe import get_procs, run_program, wait_for_procs from wk.io import non_clobber_path from wk.log import format_log_path from wk.std import GenericError, GenericWarning, sleep @@ -355,6 +355,41 @@ def run_chkdsk_online(): raise GenericError('Issue(s) detected') +def run_dism(repair=True): + """Run DISM to either scan or repair component store health.""" + conemu_args = ['-new_console:n', '-new_console:s33V'] if CONEMU else [] + + # Bail early + if OS_VERSION < 8: + raise GenericWarning('Unsupported OS') + + # Run (repair) scan + log_path = format_log_path( + log_name=f'DISM_{"Restore" if repair else "Scan"}Health', tool=True, + ) + cmd = [ + 'DISM', '/Online', '/Cleanup-Image', + '/RestoreHealth' if repair else '/ScanHealth', + f'/LogPath:{log_path}', + *conemu_args, + ] + run_program(cmd, check=False, pipe=False) + wait_for_procs('dism.exe') + + # Run check health + log_path = format_log_path(log_name='DISM_CheckHealth.log', tool=True) + cmd = [ + 'DISM', '/Online', '/Cleanup-Image', + '/CheckHealth', + f'/LogPath:{log_path}', + ] + proc = run_program(cmd, check=False) + + # Check for errors + if 'no component store corruption detected' not in proc.stdout.lower(): + raise GenericError('Issue(s) detected') + + def run_sfc_scan(): """Run SFC and save results.""" cmd = ['sfc', '/scannow'] From 7064472e0bcc0ba6aa1fa192c05b0eed1ca12021 Mon Sep 17 00:00:00 2001 From: 2Shirt <2xShirt@gmail.com> Date: Fri, 16 Apr 2021 03:33:47 -0600 Subject: [PATCH 06/43] Fix SFC scan --- scripts/wk/os/win.py | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/scripts/wk/os/win.py b/scripts/wk/os/win.py index 415679e5..27bab457 100644 --- a/scripts/wk/os/win.py +++ b/scripts/wk/os/win.py @@ -16,7 +16,6 @@ except ImportError as err: from wk.borrowed import acpi from wk.exe import get_procs, run_program, wait_for_procs -from wk.io import non_clobber_path from wk.log import format_log_path from wk.std import GenericError, GenericWarning, sleep @@ -397,17 +396,13 @@ def run_sfc_scan(): err_path = log_path.with_suffix('.err') # Run SFC - proc = run_program(cmd, check=False, encoding='utf-16') - - # Fix paths - log_path = non_clobber_path(log_path) - err_path = non_clobber_path(err_path) + proc = run_program(cmd, check=False, encoding='utf-16le') # Save output os.makedirs(log_path.parent, exist_ok=True) - with open(log_path, 'w') as _f: + with open(log_path, 'a') as _f: _f.write(proc.stdout) - with open(err_path, 'w') as _f: + with open(err_path, 'a') as _f: _f.write(proc.stderr) # Check result From 9351b597c21af3e720ef91707f31b9e63b30d47d Mon Sep 17 00:00:00 2001 From: 2Shirt <2xShirt@gmail.com> Date: Sat, 17 Apr 2021 11:38:06 -0600 Subject: [PATCH 07/43] Avoid potential crash in run_chkdsk_online() --- scripts/wk/os/win.py | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/scripts/wk/os/win.py b/scripts/wk/os/win.py index 27bab457..a4ad5495 100644 --- a/scripts/wk/os/win.py +++ b/scripts/wk/os/win.py @@ -338,16 +338,26 @@ def run_chkdsk_online(): # Run scan run_program(cmd, check=False) - proc = get_procs('chkdsk.exe')[0] - return_code = proc.wait() + try: + proc = get_procs('chkdsk.exe')[0] + return_code = proc.wait() + except IndexError: + # Failed to get CHKDSK process, set return_code to force a retry + return_code = 255 if return_code > 1: # Try again retried = True run_program(cmd, check=False) - proc = get_procs('chkdsk.exe')[0] - return_code = proc.wait() + try: + proc = get_procs('chkdsk.exe')[0] + return_code = proc.wait() + except IndexError: + # Failed to get CHKDSK process + return_code = -1 # Check result + if return_code == -1: + raise GenericError('Failed to find CHKDSK process.') if (return_code == 0 and retried) or return_code == 1: raise GenericWarning('Repaired (or manually aborted)') if return_code > 1: From fd7a8c40665b1c4f6c7f73c6725e04b3f5a6c729 Mon Sep 17 00:00:00 2001 From: 2Shirt <2xShirt@gmail.com> Date: Sat, 17 Apr 2021 12:13:24 -0600 Subject: [PATCH 08/43] Add Windows service functions --- scripts/wk/os/win.py | 71 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 71 insertions(+) diff --git a/scripts/wk/os/win.py b/scripts/wk/os/win.py index a4ad5495..f4a03f30 100644 --- a/scripts/wk/os/win.py +++ b/scripts/wk/os/win.py @@ -7,6 +7,7 @@ import pathlib import platform from contextlib import suppress +import psutil try: import winreg @@ -457,5 +458,75 @@ def enable_safemode_msi(): run_program(cmd) +# Service Functions +def disable_service(service_name): + """Set service startup to disabled.""" + cmd = ['sc', 'config', service_name, 'start=', 'disabled'] + run_program(cmd, check=False) + + # Verify service was disabled + if get_service_start_type(service_name) != 'disabled': + raise GenericError(f'Failed to disable service {service_name}') + + +def enable_service(service_name, start_type='auto'): + """Enable service by setting start type.""" + cmd = ['sc', 'config', service_name, 'start=', start_type] + psutil_type = 'automatic' + if start_type == 'demand': + psutil_type = 'manual' + + # Enable service + run_program(cmd, check=False) + + # Verify service was enabled + if get_service_start_type(service_name) != psutil_type: + raise GenericError(f'Failed to enable service {service_name}') + + +def get_service_status(service_name): + """Get service status using psutil, returns str.""" + status = 'unknown' + try: + service = psutil.win_service_get(service_name) + status = service.status() + except psutil.NoSuchProcess: + status = 'missing?' + + return status + + +def get_service_start_type(service_name): + """Get service startup type using psutil, returns str.""" + start_type = 'unknown' + try: + service = psutil.win_service_get(service_name) + start_type = service.start_type() + except psutil.NoSuchProcess: + start_type = 'missing?' + + return start_type + + +def start_service(service_name): + """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'): + raise GenericError(f'Failed to start service {service_name}') + + +def stop_service(service_name): + """Stop service.""" + cmd = ['net', 'stop', service_name] + run_program(cmd, check=False) + + # Verify service was stopped + if not get_service_status(service_name) == 'stopped': + raise GenericError(f'Failed to stop service {service_name}') + + if __name__ == '__main__': print("This file is not meant to be called directly.") From 65cb8481bc9ec3db79759f7ccfbc47dc67daef44 Mon Sep 17 00:00:00 2001 From: 2Shirt <2xShirt@gmail.com> Date: Sat, 17 Apr 2021 13:26:21 -0600 Subject: [PATCH 09/43] Add wk.repairs section --- scripts/auto_repairs.py | 18 +++ scripts/wk/__init__.py | 1 + scripts/wk/os/win.py | 116 +------------- scripts/wk/repairs/__init__.py | 7 + scripts/wk/repairs/win.py | 277 +++++++++++++++++++++++++++++++++ 5 files changed, 304 insertions(+), 115 deletions(-) create mode 100644 scripts/auto_repairs.py create mode 100644 scripts/wk/repairs/__init__.py create mode 100644 scripts/wk/repairs/win.py diff --git a/scripts/auto_repairs.py b/scripts/auto_repairs.py new file mode 100644 index 00000000..4409e3ab --- /dev/null +++ b/scripts/auto_repairs.py @@ -0,0 +1,18 @@ +"""Wizard Kit: Auto-Repair Tool""" +# 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 + + +if __name__ == '__main__': + try: + wk.repairs.win.run_auto_repair() + except SystemExit: + raise + except: #pylint: disable=bare-except + wk.std.major_exception() diff --git a/scripts/wk/__init__.py b/scripts/wk/__init__.py index b6a11b56..25620f35 100644 --- a/scripts/wk/__init__.py +++ b/scripts/wk/__init__.py @@ -13,6 +13,7 @@ from wk import kit from wk import log from wk import net from wk import os +from wk import repairs from wk import std from wk import sw from wk import tmux diff --git a/scripts/wk/os/win.py b/scripts/wk/os/win.py index f4a03f30..20ee4917 100644 --- a/scripts/wk/os/win.py +++ b/scripts/wk/os/win.py @@ -16,8 +16,7 @@ except ImportError as err: raise err from wk.borrowed import acpi -from wk.exe import get_procs, run_program, wait_for_procs -from wk.log import format_log_path +from wk.exe import run_program from wk.std import GenericError, GenericWarning, sleep @@ -314,119 +313,6 @@ def reg_set_value(hive, key, name, data, data_type, option=None): winreg.SetValue(hive, key, data_type, data) -# Repair Functions -def run_chkdsk_offline(): - """Set filesystem 'dirty bit' to force a CHKDSK during startup.""" - cmd = ['fsutil', 'dirty', 'set', os.environ.get('SYSTEMDRIVE', 'C:')] - proc = run_program(cmd, check=False) - - # Check result - if proc.returncode > 0: - raise GenericError('Failed to set dirty bit.') - - -def run_chkdsk_online(): - """Run CHKDSK. - - NOTE: If run on Windows 8+ online repairs are attempted. - """ - cmd = ['CHKDSK', os.environ.get('SYSTEMDRIVE', 'C:')] - if OS_VERSION >= 8: - cmd.extend(['/scan', '/perf']) - if CONEMU: - cmd.extend(['-new_console:n', '-new_console:s33V']) - retried = False - - # Run scan - run_program(cmd, check=False) - try: - proc = get_procs('chkdsk.exe')[0] - return_code = proc.wait() - except IndexError: - # Failed to get CHKDSK process, set return_code to force a retry - return_code = 255 - if return_code > 1: - # Try again - retried = True - run_program(cmd, check=False) - try: - proc = get_procs('chkdsk.exe')[0] - return_code = proc.wait() - except IndexError: - # Failed to get CHKDSK process - return_code = -1 - - # Check result - if return_code == -1: - raise GenericError('Failed to find CHKDSK process.') - if (return_code == 0 and retried) or return_code == 1: - raise GenericWarning('Repaired (or manually aborted)') - if return_code > 1: - raise GenericError('Issue(s) detected') - - -def run_dism(repair=True): - """Run DISM to either scan or repair component store health.""" - conemu_args = ['-new_console:n', '-new_console:s33V'] if CONEMU else [] - - # Bail early - if OS_VERSION < 8: - raise GenericWarning('Unsupported OS') - - # Run (repair) scan - log_path = format_log_path( - log_name=f'DISM_{"Restore" if repair else "Scan"}Health', tool=True, - ) - cmd = [ - 'DISM', '/Online', '/Cleanup-Image', - '/RestoreHealth' if repair else '/ScanHealth', - f'/LogPath:{log_path}', - *conemu_args, - ] - run_program(cmd, check=False, pipe=False) - wait_for_procs('dism.exe') - - # Run check health - log_path = format_log_path(log_name='DISM_CheckHealth.log', tool=True) - cmd = [ - 'DISM', '/Online', '/Cleanup-Image', - '/CheckHealth', - f'/LogPath:{log_path}', - ] - proc = run_program(cmd, check=False) - - # Check for errors - if 'no component store corruption detected' not in proc.stdout.lower(): - raise GenericError('Issue(s) detected') - - -def run_sfc_scan(): - """Run SFC and save results.""" - cmd = ['sfc', '/scannow'] - log_path = format_log_path(log_name='SFC', tool=True) - err_path = log_path.with_suffix('.err') - - # Run SFC - proc = run_program(cmd, check=False, encoding='utf-16le') - - # Save output - os.makedirs(log_path.parent, exist_ok=True) - with open(log_path, 'a') as _f: - _f.write(proc.stdout) - with open(err_path, 'a') as _f: - _f.write(proc.stderr) - - # Check result - if 'did not find any integrity violations' in proc.stdout: - pass - elif 'successfully repaired' in proc.stdout: - raise GenericWarning('Repaired') - elif 'found corrupt files' in proc.stdout: - raise GenericError('Corruption detected') - else: - raise OSError - - # Safe Mode Functions def disable_safemode(): """Edit BCD to remove safeboot value.""" diff --git a/scripts/wk/repairs/__init__.py b/scripts/wk/repairs/__init__.py new file mode 100644 index 00000000..cbdf27e4 --- /dev/null +++ b/scripts/wk/repairs/__init__.py @@ -0,0 +1,7 @@ +"""WizardKit: repairs module init""" +# vim: sts=2 sw=2 ts=2 + +import platform + +if platform.system() == 'Windows': + from wk.repairs import win diff --git a/scripts/wk/repairs/win.py b/scripts/wk/repairs/win.py new file mode 100644 index 00000000..8abf080a --- /dev/null +++ b/scripts/wk/repairs/win.py @@ -0,0 +1,277 @@ +"""WizardKit: Repairs - Windows""" +# vim: sts=2 sw=2 ts=2 + +import atexit +import logging +import os +import platform +import sys + +from wk.cfg.main import KIT_NAME_FULL +from wk.exe import get_procs, run_program, popen_program, wait_for_procs +from wk.log import format_log_path, update_log_path +from wk.os.win import * # pylint: disable=wildcard-import +from wk.std import ( + GenericError, GenericWarning, TryAndPrint, + abort, + clear_screen, print_info, + pause, sleep, + set_title, + ) + + +# STATIC VARIABLES +LOG = logging.getLogger(__name__) +AUTO_REPAIR_DELAY_IN_SECONDS = 30 +AUTO_REPAIR_KEY = fr'Software\{KIT_NAME_FULL}\AutoRepair' +CONEMU = 'ConEmuPID' in os.environ +OS_VERSION = float(platform.win32_ver()[0]) +TRY_PRINT = TryAndPrint() + + +# AutoRepair Functions +def end_session(): + """End AutoRepair session.""" + print_info('Ending repair session') + reg_delete_value('HKCU', AUTO_REPAIR_KEY, 'SessionStarted') + + # Remove logon task + cmd = [ + 'schtasks', '/delete', '/f', + '/tn', f'{KIT_NAME_FULL}-AutoRepair', + ] + run_program(cmd) + + # Disable Autologon + # TODO: Run Autologon + reg_set_value( + 'HKLM', r'Software\Microsoft\Windows NT\CurrentVersion\Winlogon', + 'AutoAdminLogon', '0', 'SZ', + ) + reg_delete_value( + 'HKLM', r'Software\Microsoft\Windows NT\CurrentVersion\Winlogon', + 'DefaultUserName', + ) + reg_delete_value( + 'HKLM', r'Software\Microsoft\Windows NT\CurrentVersion\Winlogon', + 'DefaultDomainName', + ) + + +def init(): + """Initialize AutoRepair.""" + session_started = False + try: + session_started = reg_read_value('HKCU', AUTO_REPAIR_KEY, 'SessionStarted') + except FileNotFoundError: + pass + + # Start or resume a repair session + if not session_started: + init_run() + init_session() + else: + print_info('Resuming session, press CTRL+c to cancel') + try: + for _x in range(AUTO_REPAIR_DELAY_IN_SECONDS, 0, -1): + print(f' {_x} second{"" if _x==1 else "s"} remaining... \r', end='') + sleep(1) + except KeyboardInterrupt: + abort() + print('') + init_run() + + # Done + return session_started + + +def init_run(): + """Initialize AutoRepair Run.""" + atexit.register(start_explorer) + # TODO: Sync Clock + kill_explorer() + # TODO: RKill + + +def init_session(): + """Initialize AutoRepair session.""" + print_info('Starting repair session') + reg_set_value('HKCU', AUTO_REPAIR_KEY, 'SessionStarted', 1, 'DWORD') + + # Create logon task for AutoRepair + cmd = [ + 'schtasks', '/create', '/f', + '/sc', 'ONLOGON', + '/tn', f'{KIT_NAME_FULL}-AutoRepair', + '/rl', 'HIGHEST', + '/tr', fr'C:\Windows\System32\cmd.exe "/C {sys.executable} {sys.argv[0]}"', + ] + run_program(cmd) + + # One-time tasks + # TODO: Backup Registry + # TODO: Enable and create restore point + # TODO: Run Autologon + # TODO: Disable Windows updates + # TODO: Reset Windows updates + reboot() + + +def run_auto_repair(): + """Run AutoRepair.""" + update_log_path(dest_name='Auto-Repair Tool', timestamp=True) + title = f'{KIT_NAME_FULL}: Auto-Repair Tool' + clear_screen() + set_title(title) + print_info(title) + print('') + + # Show Menu on first run + session_started = init() + if not session_started: + # TODO: Show Menu + pass + + # Run repairs + # TODO: Run repairs + end_session() + + # Done + print_info('Done') + pause('Press Enter to exit...') + + +# OS Built-in Functions +def kill_explorer(): + """Kill all Explorer processes.""" + cmd = ['taskkill', '/im', 'explorer.exe', '/f'] + run_program(cmd, check=False) + + +def reboot(): + """Reboot the system.""" + atexit.unregister(start_explorer) + cmd = ['shutdown', '-r', '-t', '0'] + run_program(cmd, check=False) + raise SystemExit + + +def run_chkdsk_offline(): + """Set filesystem 'dirty bit' to force a CHKDSK during startup.""" + cmd = ['fsutil', 'dirty', 'set', os.environ.get('SYSTEMDRIVE', 'C:')] + proc = run_program(cmd, check=False) + + # Check result + if proc.returncode > 0: + raise GenericError('Failed to set dirty bit.') + + +def run_chkdsk_online(): + """Run CHKDSK. + + NOTE: If run on Windows 8+ online repairs are attempted. + """ + cmd = ['CHKDSK', os.environ.get('SYSTEMDRIVE', 'C:')] + if OS_VERSION >= 8: + cmd.extend(['/scan', '/perf']) + if CONEMU: + cmd.extend(['-new_console:n', '-new_console:s33V']) + retried = False + + # Run scan + run_program(cmd, check=False) + try: + proc = get_procs('chkdsk.exe')[0] + return_code = proc.wait() + except IndexError: + # Failed to get CHKDSK process, set return_code to force a retry + return_code = 255 + if return_code > 1: + # Try again + retried = True + run_program(cmd, check=False) + try: + proc = get_procs('chkdsk.exe')[0] + return_code = proc.wait() + except IndexError: + # Failed to get CHKDSK process + return_code = -1 + + # Check result + if return_code == -1: + raise GenericError('Failed to find CHKDSK process.') + if (return_code == 0 and retried) or return_code == 1: + raise GenericWarning('Repaired (or manually aborted)') + if return_code > 1: + raise GenericError('Issue(s) detected') + + +def run_dism(repair=True): + """Run DISM to either scan or repair component store health.""" + conemu_args = ['-new_console:n', '-new_console:s33V'] if CONEMU else [] + + # Bail early + if OS_VERSION < 8: + raise GenericWarning('Unsupported OS') + + # Run (repair) scan + log_path = format_log_path( + log_name=f'DISM_{"Restore" if repair else "Scan"}Health', tool=True, + ) + cmd = [ + 'DISM', '/Online', '/Cleanup-Image', + '/RestoreHealth' if repair else '/ScanHealth', + f'/LogPath:{log_path}', + *conemu_args, + ] + run_program(cmd, check=False, pipe=False) + wait_for_procs('dism.exe') + + # Run check health + log_path = format_log_path(log_name='DISM_CheckHealth.log', tool=True) + cmd = [ + 'DISM', '/Online', '/Cleanup-Image', + '/CheckHealth', + f'/LogPath:{log_path}', + ] + proc = run_program(cmd, check=False) + + # Check for errors + if 'no component store corruption detected' not in proc.stdout.lower(): + raise GenericError('Issue(s) detected') + + +def run_sfc_scan(): + """Run SFC and save results.""" + cmd = ['sfc', '/scannow'] + log_path = format_log_path(log_name='SFC', tool=True) + err_path = log_path.with_suffix('.err') + + # Run SFC + proc = run_program(cmd, check=False, encoding='utf-16le') + + # Save output + os.makedirs(log_path.parent, exist_ok=True) + with open(log_path, 'a') as _f: + _f.write(proc.stdout) + with open(err_path, 'a') as _f: + _f.write(proc.stderr) + + # Check result + if 'did not find any integrity violations' in proc.stdout: + pass + elif 'successfully repaired' in proc.stdout: + raise GenericWarning('Repaired') + elif 'found corrupt files' in proc.stdout: + raise GenericError('Corruption detected') + else: + raise OSError + + +def start_explorer(): + """Start Explorer.""" + popen_program(['explorer.exe']) + + +if __name__ == '__main__': + print("This file is not meant to be called directly.") From b8335188ce4b09caffd0e0d446c4d5213787fc19 Mon Sep 17 00:00:00 2001 From: 2Shirt <2xShirt@gmail.com> Date: Sat, 17 Apr 2021 17:49:04 -0600 Subject: [PATCH 10/43] Add wk.kit.tools For code related to downloading, finding, and running tools on the kit. --- scripts/wk/kit/__init__.py | 2 + scripts/wk/kit/tools.py | 79 ++++++++++++++++++++++++++++++++++++++ scripts/wk/repairs/win.py | 25 +++++++----- 3 files changed, 96 insertions(+), 10 deletions(-) create mode 100644 scripts/wk/kit/tools.py diff --git a/scripts/wk/kit/__init__.py b/scripts/wk/kit/__init__.py index 0869d9c6..4641fae7 100644 --- a/scripts/wk/kit/__init__.py +++ b/scripts/wk/kit/__init__.py @@ -3,5 +3,7 @@ import platform +from wk.kit import tools + if platform.system() == 'Linux': from wk.kit import ufd diff --git a/scripts/wk/kit/tools.py b/scripts/wk/kit/tools.py new file mode 100644 index 00000000..9c7ce4ba --- /dev/null +++ b/scripts/wk/kit/tools.py @@ -0,0 +1,79 @@ +"""WizardKit: Tool Functions""" +# vim: sts=2 sw=2 ts=2 + +import pathlib +import sys + +from wk.exe import popen_program, run_program + + +# STATIC VARIABLES +ARCH = '64' if sys.maxsize > 2**32 else '32' + + +# "GLOBAL" VARIABLES +CACHED_DIRS = {} + + +# Functions +def find_kit_dir(name=None): + """Find folder in kit, returns pathlib.Path. + + Search is performed in the script's path and then recursively upwards. + If name is given then search for that instead.""" + cur_path = pathlib.Path(__file__).resolve().parent + search = name if name else '.bin' + + # Search + if name in CACHED_DIRS: + return CACHED_DIRS[name] + while not cur_path.match(cur_path.anchor): + if cur_path.joinpath(search).exists(): + break + cur_path = cur_path.parent + + # Check + if cur_path.match(cur_path.anchor): + raise FileNotFoundError(f'Failed to find kit dir, {name=}') + if name: + cur_path = cur_path.joinpath(name) + + # Done + CACHED_DIRS[name] = cur_path + return cur_path + + +def get_tool_path(folder, name): + """Get tool path, returns pathlib.Path""" + bin_dir = find_kit_dir('.bin') + + # "Search" + tool_path = bin_dir.joinpath(f'{folder}/{name}{ARCH}.exe') + if not tool_path.exists(): + tool_path = tool_path.with_stem(name) + + # Missing? + if not tool_path.exists(): + raise FileNotFoundError(f'Failed to find tool, {folder=}, {name=}') + + # Done + return tool_path + + +def run_tool(folder, name, *args, popen=False): + """Run tool from kit.""" + cmd = [get_tool_path(folder, name), *args] + proc = None + + # Run + if popen: + proc = popen_program(cmd) + else: + proc = run_program(cmd, check=False) + + # Done + return proc + + +if __name__ == '__main__': + print("This file is not meant to be called directly.") diff --git a/scripts/wk/repairs/win.py b/scripts/wk/repairs/win.py index 8abf080a..6cba5133 100644 --- a/scripts/wk/repairs/win.py +++ b/scripts/wk/repairs/win.py @@ -7,16 +7,21 @@ import os import platform import sys -from wk.cfg.main import KIT_NAME_FULL -from wk.exe import get_procs, run_program, popen_program, wait_for_procs -from wk.log import format_log_path, update_log_path -from wk.os.win import * # pylint: disable=wildcard-import -from wk.std import ( - GenericError, GenericWarning, TryAndPrint, +from wk.cfg.main import KIT_NAME_FULL +from wk.exe import get_procs, run_program, popen_program, wait_for_procs +from wk.kit.tools import run_tool +from wk.log import format_log_path, update_log_path +from wk.os.win import reg_delete_value, reg_read_value, reg_set_value +from wk.std import ( + GenericError, + GenericWarning, + TryAndPrint, abort, - clear_screen, print_info, - pause, sleep, + clear_screen, + pause, + print_info, set_title, + sleep, ) @@ -43,7 +48,7 @@ def end_session(): run_program(cmd) # Disable Autologon - # TODO: Run Autologon + run_tool('Sysinternals', 'Autologon') reg_set_value( 'HKLM', r'Software\Microsoft\Windows NT\CurrentVersion\Winlogon', 'AutoAdminLogon', '0', 'SZ', @@ -111,7 +116,7 @@ def init_session(): # One-time tasks # TODO: Backup Registry # TODO: Enable and create restore point - # TODO: Run Autologon + run_tool('Sysinternals', 'Autologon') # TODO: Disable Windows updates # TODO: Reset Windows updates reboot() From cb825e37ba042eb5280d456026055d2a81f24b34 Mon Sep 17 00:00:00 2001 From: 2Shirt <2xShirt@gmail.com> Date: Mon, 19 Apr 2021 01:04:18 -0600 Subject: [PATCH 11/43] Add support to run tools from .cbin or online --- scripts/wk/cfg/__init__.py | 1 + scripts/wk/cfg/tools.py | 72 +++++++++++++++++++++++++++++++ scripts/wk/kit/tools.py | 87 +++++++++++++++++++++++++++++++++++--- 3 files changed, 155 insertions(+), 5 deletions(-) create mode 100644 scripts/wk/cfg/tools.py diff --git a/scripts/wk/cfg/__init__.py b/scripts/wk/cfg/__init__.py index 23ca608f..d35b8874 100644 --- a/scripts/wk/cfg/__init__.py +++ b/scripts/wk/cfg/__init__.py @@ -5,4 +5,5 @@ from wk.cfg import hw from wk.cfg import log from wk.cfg import main from wk.cfg import net +from wk.cfg import tools from wk.cfg import ufd diff --git a/scripts/wk/cfg/tools.py b/scripts/wk/cfg/tools.py new file mode 100644 index 00000000..e32d3c2e --- /dev/null +++ b/scripts/wk/cfg/tools.py @@ -0,0 +1,72 @@ +"""WizardKit: Config - Tools""" +# pylint: disable=line-too-long +# vim: sts=2 sw=2 ts=2 + + +# Download frequency in days +DOWNLOAD_FREQUENCY = 7 + + +# Sources +SOURCES = { + 'Adobe Reader DC': 'https://ardownload2.adobe.com/pub/adobe/reader/win/AcrobatDC/2100120145/AcroRdrDC2100120145_en_US.exe', + 'AdwCleaner': 'https://downloads.malwarebytes.com/file/adwcleaner', + 'AIDA64': 'https://download.aida64.com/aida64engineer633.zip', + 'aria2': 'https://github.com/aria2/aria2/releases/download/release-1.35.0/aria2-1.35.0-win-32bit-build1.zip', + 'Autoruns': 'https://download.sysinternals.com/files/Autoruns.zip', + 'BleachBit': 'https://download.bleachbit.org/BleachBit-4.2.0-portable.zip', + 'BlueScreenView32': 'http://www.nirsoft.net/utils/bluescreenview.zip', + 'BlueScreenView64': 'http://www.nirsoft.net/utils/bluescreenview-x64.zip', + 'Caffeine': 'http://www.zhornsoftware.co.uk/caffeine/caffeine.zip', + 'ClassicStartSkin': 'http://www.classicshell.net/forum/download/file.php?id=3001&sid=9a195960d98fd754867dcb63d9315335', + 'Du': 'https://download.sysinternals.com/files/DU.zip', + 'ERUNT': 'http://www.aumha.org/downloads/erunt.zip', + 'ESET AVRemover32': 'https://download.eset.com/com/eset/tools/installers/av_remover/latest/avremover_nt32_enu.exe', + 'ESET AVRemover64': 'https://download.eset.com/com/eset/tools/installers/av_remover/latest/avremover_nt64_enu.exe', + 'ESET NOD32 AV': 'https://download.eset.com/com/eset/apps/home/eav/windows/latest/eav_nt64.exe', + 'ESET Online Scanner': 'https://download.eset.com/com/eset/tools/online_scanner/latest/esetonlinescanner_enu.exe', + 'Everything32': 'https://www.voidtools.com/Everything-1.4.1.1005.x86.en-US.zip', + 'Everything64': 'https://www.voidtools.com/Everything-1.4.1.1005.x64.en-US.zip', + 'FastCopy': 'https://ftp.vector.co.jp/73/10/2323/FastCopy392_installer.exe', + 'FurMark': 'https://geeks3d.com/dl/get/569', + 'Firefox uBO': 'https://addons.mozilla.org/firefox/downloads/file/3740966/ublock_origin-1.34.0-an+fx.xpi', + 'HitmanPro32': 'https://dl.surfright.nl/HitmanPro.exe', + 'HitmanPro64': 'https://dl.surfright.nl/HitmanPro_x64.exe', + 'HWiNFO': 'https://files1.majorgeeks.com/c8a055180587599139f8f454712dcc618cd1740e/systeminfo/hwi_702.zip', + 'Intel SSD Toolbox': r'https://downloadmirror.intel.com/28593/eng/Intel%20SSD%20Toolbox%20-%20v3.5.9.exe', + 'IOBit_Uninstaller': r'https://portableapps.com/redirect/?a=IObitUninstallerPortable&s=s&d=pa&f=IObitUninstallerPortable_7.5.0.7.paf.exe', + 'KVRT': 'https://devbuilds.s.kaspersky-labs.com/devbuilds/KVRT/latest/full/KVRT.exe', + 'LibreOffice': 'https://download.documentfoundation.org/libreoffice/stable/7.1.2/win/x86_64/LibreOffice_7.1.2_Win_x64.msi', + 'Linux Reader': 'https://www.diskinternals.com/download/Linux_Reader.exe', + 'Macs Fan Control': 'https://www.crystalidea.com/downloads/macsfancontrol_setup.exe', + 'NirCmd32': 'https://www.nirsoft.net/utils/nircmd.zip', + 'NirCmd64': 'https://www.nirsoft.net/utils/nircmd-x64.zip', + 'NotepadPlusPlus': 'https://github.com/notepad-plus-plus/notepad-plus-plus/releases/download/v7.9.5/npp.7.9.5.portable.minimalist.7z', + 'Office Deployment Tool': 'https://download.microsoft.com/download/2/7/A/27AF1BE6-DD20-4CB4-B154-EBAB8A7D4A7E/officedeploymenttool_11617-33601.exe', + 'ProduKey32': 'http://www.nirsoft.net/utils/produkey.zip', + 'ProduKey64': 'http://www.nirsoft.net/utils/produkey-x64.zip', + 'PuTTY': 'https://the.earth.li/~sgtatham/putty/latest/w32/putty.zip', + 'RKill': 'https://download.bleepingcomputer.com/grinler/rkill.exe', + 'Samsung Magician': 'https://s3.ap-northeast-2.amazonaws.com/global.semi.static/SAMSUNG_SSD_v5_3_0_181121/CD0C7CC1BE00525FAC4675B9E502899B41D5C3909ECE3AA2FB6B74A766B2A1EA/Samsung_Magician_Installer.zip', + 'SDIO Themes': 'http://snappy-driver-installer.org/downloads/SDIO_Themes.zip', + 'SDIO Torrent': 'http://snappy-driver-installer.org/downloads/SDIO_Update.torrent', + 'ShutUp10': 'https://dl5.oo-software.com/files/ooshutup10/OOSU10.exe', + 'smartmontools': 'https://1278-105252244-gh.circle-artifacts.com/0/builds/smartmontools-win32-setup-7.3-r5216.exe', + 'TDSSKiller': 'https://media.kaspersky.com/utilities/VirusUtilities/EN/tdsskiller.exe', + 'TestDisk': 'https://www.cgsecurity.org/testdisk-7.2-WIP.win.zip', + 'wimlib32': 'https://wimlib.net/downloads/wimlib-1.13.3-windows-i686-bin.zip', + 'wimlib64': 'https://wimlib.net/downloads/wimlib-1.13.3-windows-x86_64-bin.zip', + 'WinAIO Repair': 'http://www.tweaking.com/files/setups/tweaking.com_windows_repair_aio.zip', + 'Winapp2': 'https://github.com/MoscaDotTo/Winapp2/archive/master.zip', + 'WizTree': 'https://wiztreefree.com/files/wiztree_3_39_portable.zip', + '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 WAModern': 'https://support.xmplay.com/files/10/WAModern.zip?v=207099', + 'XMPlay': 'https://support.xmplay.com/files/20/xmplay383.zip?v=298195', + 'XYplorerFree': 'https://www.xyplorer.com/download/xyplorer_free_noinstall.zip', +} + + +if __name__ == '__main__': + print("This file is not meant to be called directly.") diff --git a/scripts/wk/kit/tools.py b/scripts/wk/kit/tools.py index 9c7ce4ba..c1404b69 100644 --- a/scripts/wk/kit/tools.py +++ b/scripts/wk/kit/tools.py @@ -1,14 +1,22 @@ """WizardKit: Tool Functions""" # vim: sts=2 sw=2 ts=2 +from datetime import datetime, timedelta +import logging import pathlib import sys +import requests + +from wk.cfg.main import ARCHIVE_PASSWORD +from wk.cfg.tools import SOURCES, DOWNLOAD_FREQUENCY from wk.exe import popen_program, run_program +from wk.std import GenericError # STATIC VARIABLES ARCH = '64' if sys.maxsize > 2**32 else '32' +LOG = logging.getLogger(__name__) # "GLOBAL" VARIABLES @@ -16,6 +24,41 @@ CACHED_DIRS = {} # Functions +def download_file(out_path, source_url): + """Download a file using requests.""" + out_path = pathlib.Path(out_path).resolve() + out_path.parent.mkdir(parents=True, exist_ok=True) + + # Request download + response = requests.get(source_url, stream=True) + if not response.ok: + LOG.error( + 'Failed to download file (status %s): %s', + response.status_code, out_path.name, + ) + raise GenericError(f'Failed to download file: {out_path.name}') + + # Write to file + with open(out_path, 'wb') as _f: + for chunk in response.iter_content(chunk_size=128): + _f.write(chunk) + + # Done + return out_path + + +def extract_archive(archive, out_path, *args, mode='x', silent=True): + """Extract an archive to out_path.""" + out_path = pathlib.Path(out_path).resolve() + out_path.parent.mkdir(parents=True, exist_ok=True) + cmd = [get_tool_path('7-Zip', '7za'), mode, archive, f'-o{out_path}', *args] + if silent: + cmd.extend(['-bso0', '-bse0', '-bsp0']) + + # Extract + run_program(cmd) + + def find_kit_dir(name=None): """Find folder in kit, returns pathlib.Path. @@ -60,16 +103,50 @@ def get_tool_path(folder, name): return tool_path -def run_tool(folder, name, *args, popen=False): - """Run tool from kit.""" - cmd = [get_tool_path(folder, name), *args] +def run_tool( + folder, name, *run_args, + cbin=False, cwd=False, download=False, popen=False, + **run_kwargs, + ): + """Run tool from the kit or the Internet, returns proc obj. + + proc will be either subprocess.CompletedProcess or subprocess.Popen.""" proc = None + # Extract from .cbin + if cbin: + extract_archive( + find_kit_dir('.cbin').joinpath(folder).with_suffix('.7z'), + find_kit_dir('.bin').joinpath(folder), + '-aos', f'-p{ARCHIVE_PASSWORD}', + ) + + # Download tool + if download: + out_path = find_kit_dir('.bin').joinpath(f'{folder}/{name}.exe') + outdated = False + try: + mod_time = datetime.fromtimestamp(out_path.stat().st_ctime) + outdated = datetime.now() - mod_time > timedelta(days=DOWNLOAD_FREQUENCY) + except FileNotFoundError: + # Ignore and download + pass + if not out_path.exists() or outdated: + LOG.info('Downloading tool: %s', name) + source_url = SOURCES[name] + download_file(out_path, source_url) + else: + LOG.info('Skip downloading tool: %s', name) + # Run + tool_path = get_tool_path(folder, name) + cmd = [tool_path, *run_args] + if cwd: + run_kwargs['cwd'] = tool_path.parent if popen: - proc = popen_program(cmd) + proc = popen_program(cmd, **run_kwargs) else: - proc = run_program(cmd, check=False) + proc = run_program(cmd, check=False, **run_kwargs) # Done return proc From b44fda2ccdf402b7836878e3772ef3fd913274b2 Mon Sep 17 00:00:00 2001 From: 2Shirt <2xShirt@gmail.com> Date: Tue, 20 Apr 2021 21:14:31 -0600 Subject: [PATCH 12/43] Avoid clobbering files when downloading tools. --- scripts/wk/kit/tools.py | 48 ++++++++++++++++++++++++++++++----------- 1 file changed, 35 insertions(+), 13 deletions(-) diff --git a/scripts/wk/kit/tools.py b/scripts/wk/kit/tools.py index c1404b69..12e7d3df 100644 --- a/scripts/wk/kit/tools.py +++ b/scripts/wk/kit/tools.py @@ -24,19 +24,30 @@ CACHED_DIRS = {} # Functions -def download_file(out_path, source_url): - """Download a file using requests.""" +def download_file(out_path, source_url, as_new=False, overwrite=False): + """Download a file using requests, returns pathlib.Path.""" out_path = pathlib.Path(out_path).resolve() + + # Avoid clobbering + if out_path.exists() and not overwrite: + raise FileExistsError(f'Refusing to clobber {out_path}') + + # Create destination directory out_path.parent.mkdir(parents=True, exist_ok=True) + if as_new: + out_path = out_path.with_suffix(f'{out_path.suffix}.new') # Request download response = requests.get(source_url, stream=True) if not response.ok: + name = out_path.name + if as_new: + name = name[:-4] LOG.error( 'Failed to download file (status %s): %s', - response.status_code, out_path.name, + response.status_code, name, ) - raise GenericError(f'Failed to download file: {out_path.name}') + raise GenericError(f'Failed to download file: {name}') # Write to file with open(out_path, 'wb') as _f: @@ -113,6 +124,18 @@ def run_tool( proc will be either subprocess.CompletedProcess or subprocess.Popen.""" proc = None + def _is_outdated(file_path): + """Check if the ctime is older than the threshold, returns bool.""" + outdated = False + try: + ctime = datetime.fromtimestamp(file_path.stat().st_ctime) + outdated = datetime.now() - ctime > timedelta(days=DOWNLOAD_FREQUENCY) + except FileNotFoundError: + LOG.error("This shouldn't happen right?") + + # Done + return outdated + # Extract from .cbin if cbin: extract_archive( @@ -124,17 +147,16 @@ def run_tool( # Download tool if download: out_path = find_kit_dir('.bin').joinpath(f'{folder}/{name}.exe') - outdated = False - try: - mod_time = datetime.fromtimestamp(out_path.stat().st_ctime) - outdated = datetime.now() - mod_time > timedelta(days=DOWNLOAD_FREQUENCY) - except FileNotFoundError: - # Ignore and download - pass - if not out_path.exists() or outdated: + if not out_path.exists() or _is_outdated(out_path): LOG.info('Downloading tool: %s', name) source_url = SOURCES[name] - download_file(out_path, source_url) + try: + new_file = download_file(out_path, source_url, as_new=True) + new_file.replace(out_path) + except GenericError: + # Ignore as long as there's still a version present + if not out_path.exists(): + raise else: LOG.info('Skip downloading tool: %s', name) From ee3203c485bbe9df0534a1c1cd3f5375c7ae4bf2 Mon Sep 17 00:00:00 2001 From: 2Shirt <2xShirt@gmail.com> Date: Tue, 20 Apr 2021 22:06:03 -0600 Subject: [PATCH 13/43] Show message when downloading tools --- scripts/wk/kit/tools.py | 3 +++ scripts/wk/std.py | 11 ++++++++--- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/scripts/wk/kit/tools.py b/scripts/wk/kit/tools.py index 12e7d3df..4f23b9a9 100644 --- a/scripts/wk/kit/tools.py +++ b/scripts/wk/kit/tools.py @@ -27,6 +27,8 @@ CACHED_DIRS = {} def download_file(out_path, source_url, as_new=False, overwrite=False): """Download a file using requests, returns pathlib.Path.""" out_path = pathlib.Path(out_path).resolve() + cursor_left = '\u001B[14D' + print(f'Downloading...{cursor_left}', end='', flush=True) # Avoid clobbering if out_path.exists() and not overwrite: @@ -55,6 +57,7 @@ def download_file(out_path, source_url, as_new=False, overwrite=False): _f.write(chunk) # Done + print(f' {cursor_left}', end='', flush=True) return out_path diff --git a/scripts/wk/std.py b/scripts/wk/std.py index 5dba174c..8da412f1 100644 --- a/scripts/wk/std.py +++ b/scripts/wk/std.py @@ -455,7 +455,7 @@ class TryAndPrint(): # Done return message - def _format_function_output(self, output): + def _format_function_output(self, output, msg_good): """Format function output for use in try_and_print(), returns str.""" LOG.debug('Formatting output: %s', output) @@ -468,6 +468,10 @@ class TryAndPrint(): 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) @@ -560,6 +564,7 @@ class TryAndPrint(): verbose, ) f_exception = None + msg_good = msg_good if msg_good else self.msg_good output = None result_msg = 'UNKNOWN' if catch_all is None: @@ -600,10 +605,10 @@ class TryAndPrint(): else: # Success if output: - result_msg = self._format_function_output(output) + result_msg = self._format_function_output(output, msg_good) print(result_msg) else: - result_msg = msg_good if msg_good else self.msg_good + result_msg = msg_good print_success(result_msg, log=False) # Done From 3c748520e10851d4bf542f039b793e1d2b75e0a5 Mon Sep 17 00:00:00 2001 From: 2Shirt <2xShirt@gmail.com> Date: Tue, 20 Apr 2021 22:23:05 -0600 Subject: [PATCH 14/43] Don't suppress bells in ConEmu --- scripts/wk/std.py | 1 - setup/windows/bin/ConEmu/ConEmu.xml | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/scripts/wk/std.py b/scripts/wk/std.py index 8da412f1..2768fea8 100644 --- a/scripts/wk/std.py +++ b/scripts/wk/std.py @@ -650,7 +650,6 @@ def ask(prompt='Kotaero!'): def beep(repeat=1): """Play system bell with optional repeat.""" - # TODO: Verify Windows functionality while repeat >= 1: # Print bell char without a newline print('\a', end='', flush=True) diff --git a/setup/windows/bin/ConEmu/ConEmu.xml b/setup/windows/bin/ConEmu/ConEmu.xml index dd6ee9b1..93e91bff 100644 --- a/setup/windows/bin/ConEmu/ConEmu.xml +++ b/setup/windows/bin/ConEmu/ConEmu.xml @@ -89,7 +89,7 @@ - + From 125907ed3afd1c5eb44d1088c9e7b4d3a6cffffd Mon Sep 17 00:00:00 2001 From: 2Shirt <2xShirt@gmail.com> Date: Wed, 21 Apr 2021 05:15:12 -0600 Subject: [PATCH 15/43] Add Auto Repairs menus --- scripts/auto_repairs.py | 2 +- scripts/wk/repairs/win.py | 265 +++++++++++++++++++++++++++++--------- scripts/wk/std.py | 85 ++++++------ 3 files changed, 250 insertions(+), 102 deletions(-) diff --git a/scripts/auto_repairs.py b/scripts/auto_repairs.py index 4409e3ab..f4c471fc 100644 --- a/scripts/auto_repairs.py +++ b/scripts/auto_repairs.py @@ -11,7 +11,7 @@ import wk if __name__ == '__main__': try: - wk.repairs.win.run_auto_repair() + wk.repairs.win.run_auto_repairs() except SystemExit: raise except: #pylint: disable=bare-except diff --git a/scripts/wk/repairs/win.py b/scripts/wk/repairs/win.py index 6cba5133..324d05a7 100644 --- a/scripts/wk/repairs/win.py +++ b/scripts/wk/repairs/win.py @@ -7,6 +7,8 @@ import os import platform import sys +from subprocess import CalledProcessError + from wk.cfg.main import KIT_NAME_FULL from wk.exe import get_procs, run_program, popen_program, wait_for_procs from wk.kit.tools import run_tool @@ -15,9 +17,11 @@ from wk.os.win import reg_delete_value, reg_read_value, reg_set_value from wk.std import ( GenericError, GenericWarning, + Menu, TryAndPrint, - abort, + ask, clear_screen, + color_string, pause, print_info, set_title, @@ -28,117 +32,242 @@ from wk.std import ( # STATIC VARIABLES LOG = logging.getLogger(__name__) AUTO_REPAIR_DELAY_IN_SECONDS = 30 -AUTO_REPAIR_KEY = fr'Software\{KIT_NAME_FULL}\AutoRepair' +AUTO_REPAIR_KEY = fr'Software\{KIT_NAME_FULL}\Auto Repairs' CONEMU = 'ConEmuPID' in os.environ +MENUS = { + 'Options': { + 'Backup Settings': ( + ('Enable System Restore', None), + ('Create System Restore', None), + ('Backup Browsers', None), + ('Backup Power Plans', None), + ), + 'Windows Repairs': ( + ('Disable Windows Updates', None), + ('Reset Windows Updates', None), + ('-Reboot-', None), + ('CHKDSK', None), + ('DISM RestoreHealth', None), + ('SFC Scan', None), + ('Fix File Associations', None), + ('Clear Proxy Settings', None), + ('Disable Pending Renames', None), + ('Registry Repairs', None), + ('Repair Safe Mode', None), + ('Reset Windows Policies', None), + ), + 'Malware Cleanup': ( + ('BleachBit', None), + ('HitmanPro', None), + ('KVRT', None), + ('Windows Defender', None), + ('-Reboot-', None), + ), + 'Manual Steps': ( + ('AdwCleaner', None), + ('IO Bit Uninstaller', None), + ('Enable Windows Updates', None), + ), + }, + 'Toggles': ( + 'Kill Explorer', + 'Run RKill at startup', + 'Use Autologon', + ), + 'Actions': ( + 'Options', + 'Start', + 'Quit', + ), + } OS_VERSION = float(platform.win32_ver()[0]) TRY_PRINT = TryAndPrint() -# AutoRepair Functions +# Auto Repairs Functions +def build_menus(title): + """Build menus, returns dict.""" + menus = {} + menus['Main'] = Menu(title=f'{title}\n{color_string("Main Menu", "GREEN")}') + + # Run groups + for group, items in MENUS['Options'].items(): + menus[group] = Menu(title=f'{title}\n{color_string(group, "GREEN")}') + menus[group].disabled_str = color_string('Forced', 'ORANGE') + for item in items: + menus[group].add_option(item[0], {'Display Name': item[0], 'Selected': True}) + if '-Reboot-' in menus[group].options: + menus[group].options['-Reboot-']['Disabled'] = True + menus[group].add_action('Main Menu') + for option in MENUS['Options']: + menus['Main'].add_option(option, {'Display Name': option, 'Selected': True}) + + # Actions + for action in MENUS['Actions']: + menus['Main'].add_action(action, {'Display Name': action, 'Selected': True}) + menus['Main'].actions['Start']['Separator'] = True + + # Options + menus['Options'] = Menu(title=f'{title}\n{color_string("Options", "GREEN")}') + for toggle in MENUS['Toggles']: + menus['Options'].add_toggle(toggle, {'Display Name': toggle, 'Selected': True}) + menus['Options'].add_action('Main Menu') + + # Initialize main menu display names + menus['Main'].update() + + # Done + return menus + + def end_session(): - """End AutoRepair session.""" + """End Auto Repairs session.""" print_info('Ending repair session') - reg_delete_value('HKCU', AUTO_REPAIR_KEY, 'SessionStarted') + auto_admin_logon = '0' + + # Delete Auto Repairs session key + try: + reg_delete_value('HKCU', AUTO_REPAIR_KEY, 'SessionStarted') + except FileNotFoundError: + LOG.error('Ending repair session but session not started.') # Remove logon task cmd = [ 'schtasks', '/delete', '/f', - '/tn', f'{KIT_NAME_FULL}-AutoRepair', + '/tn', f'{KIT_NAME_FULL}-AutoRepairs', ] - run_program(cmd) + try: + run_program(cmd) + except CalledProcessError: + LOG.error("Failed to remove scheduled task or it doesn't exist.") # Disable Autologon - run_tool('Sysinternals', 'Autologon') - reg_set_value( - 'HKLM', r'Software\Microsoft\Windows NT\CurrentVersion\Winlogon', - 'AutoAdminLogon', '0', 'SZ', - ) - reg_delete_value( - 'HKLM', r'Software\Microsoft\Windows NT\CurrentVersion\Winlogon', - 'DefaultUserName', - ) - reg_delete_value( - 'HKLM', r'Software\Microsoft\Windows NT\CurrentVersion\Winlogon', - 'DefaultDomainName', - ) + try: + auto_admin_logon = reg_read_value( + 'HKLM', r'Software\Microsoft\Windows NT\CurrentVersion\Winlogon', + 'AutoAdminLogon', + ) + except FileNotFoundError: + # Ignore and assume it's disabled + return + if auto_admin_logon != '0': + run_tool('Sysinternals', 'Autologon') + reg_set_value( + 'HKLM', r'Software\Microsoft\Windows NT\CurrentVersion\Winlogon', + 'AutoAdminLogon', '0', 'SZ', + ) + reg_delete_value( + 'HKLM', r'Software\Microsoft\Windows NT\CurrentVersion\Winlogon', + 'DefaultUserName', + ) + reg_delete_value( + 'HKLM', r'Software\Microsoft\Windows NT\CurrentVersion\Winlogon', + 'DefaultDomainName', + ) def init(): - """Initialize AutoRepair.""" - session_started = False - try: - session_started = reg_read_value('HKCU', AUTO_REPAIR_KEY, 'SessionStarted') - except FileNotFoundError: - pass + """Initialize Auto Repairs.""" + session_started = is_session_started() # Start or resume a repair session - if not session_started: - init_run() - init_session() - else: + if session_started: print_info('Resuming session, press CTRL+c to cancel') - try: - for _x in range(AUTO_REPAIR_DELAY_IN_SECONDS, 0, -1): - print(f' {_x} second{"" if _x==1 else "s"} remaining... \r', end='') - sleep(1) - except KeyboardInterrupt: - abort() + for _x in range(AUTO_REPAIR_DELAY_IN_SECONDS, 0, -1): + print(f' {_x} second{"" if _x==1 else "s"} remaining... \r', end='') + sleep(1) print('') - init_run() # Done return session_started -def init_run(): - """Initialize AutoRepair Run.""" - atexit.register(start_explorer) +def init_run(options): + """Initialize Auto Repairs Run.""" + if options['Kill Explorer']['Selected']: + atexit.register(start_explorer) + kill_explorer() # TODO: Sync Clock - kill_explorer() # TODO: RKill -def init_session(): - """Initialize AutoRepair session.""" +def init_session(options): + """Initialize Auto Repairs session.""" print_info('Starting repair session') reg_set_value('HKCU', AUTO_REPAIR_KEY, 'SessionStarted', 1, 'DWORD') - # Create logon task for AutoRepair + # Create logon task for Auto Repairs cmd = [ 'schtasks', '/create', '/f', '/sc', 'ONLOGON', - '/tn', f'{KIT_NAME_FULL}-AutoRepair', + '/tn', f'{KIT_NAME_FULL}-AutoRepairs', '/rl', 'HIGHEST', '/tr', fr'C:\Windows\System32\cmd.exe "/C {sys.executable} {sys.argv[0]}"', ] run_program(cmd) # One-time tasks - # TODO: Backup Registry - # TODO: Enable and create restore point - run_tool('Sysinternals', 'Autologon') - # TODO: Disable Windows updates - # TODO: Reset Windows updates + if options['Use Autologon']['Selected']: + run_tool('Sysinternals', 'Autologon') reboot() -def run_auto_repair(): - """Run AutoRepair.""" - update_log_path(dest_name='Auto-Repair Tool', timestamp=True) - title = f'{KIT_NAME_FULL}: Auto-Repair Tool' +def is_session_started(): + """Check if session was started, returns bool.""" + session_started = False + try: + session_started = reg_read_value('HKCU', AUTO_REPAIR_KEY, 'SessionStarted') + except FileNotFoundError: + pass + + # Done + return session_started + + +def run_auto_repairs(): + """Run Auto Repairs.""" + update_log_path(dest_name='Auto Repairs', timestamp=True) + title = f'{KIT_NAME_FULL}: Auto Repairs' clear_screen() set_title(title) print_info(title) print('') - # Show Menu on first run - session_started = init() - if not session_started: - # TODO: Show Menu - pass + # Generate menus + menus = build_menus(title) + + # Init + try: + session_started = init() + except KeyboardInterrupt: + # Assuming session was started and resume countdown was interrupted + session_started = None + + # Show Menu + if session_started is None or not session_started: + while True: + update_main_menu(menus) + selection = menus['Main'].simple_select(update=False) + if selection[0] in MENUS['Options'] or selection[0] == 'Options': + menus[selection[0]].advanced_select() + elif 'Start' in selection: + # TODO: Run repairs + pass + elif 'Quit' in selection: + if ask('End session?'): + end_session() + raise SystemExit + + # Re-check if a repair session was started + if session_started is None: + session_started = is_session_started() # Run repairs # TODO: Run repairs + init_run(menus['Options']) + if not session_started: + init_session(menus['Options']) + atexit.unregister(start_explorer) end_session() # Done @@ -146,6 +275,24 @@ def run_auto_repair(): pause('Press Enter to exit...') +def update_main_menu(menus): + """Update main menu based on current selections.""" + index = 1 + skip = '-Reboot-' + for name in menus['Main'].options: + checkmark = ' ' + selected = [ + _v['Selected'] for _k, _v in menus[name].options.items() if _k != skip + ] + if all(selected): + checkmark = '✓' + elif any(selected): + checkmark = '-' + display_name = f' {index}: [{checkmark}] {name}' + index += 1 + menus['Main'].options[name]['Display Name'] = display_name + + # OS Built-in Functions def kill_explorer(): """Kill all Explorer processes.""" diff --git a/scripts/wk/std.py b/scripts/wk/std.py index 2768fea8..f55cd1b3 100644 --- a/scripts/wk/std.py +++ b/scripts/wk/std.py @@ -228,44 +228,6 @@ class Menu(): # Done return resolved_selection - def _update(self, single_selection=True, settings_mode=False): - """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, toggle=True, status=None): """Update entry selection status either directly or by toggling.""" if entry in self.sets: @@ -340,7 +302,7 @@ class Menu(): NOTE: Menu is displayed until an action entry is selected. """ while True: - self._update(single_selection=False) + self.update(single_selection=False) user_selection = self._user_select(prompt) selected_entry = self._resolve_selection(user_selection) if user_selection.isnumeric(): @@ -364,7 +326,7 @@ class Menu(): } while True: - self._update(single_selection=True, settings_mode=True) + self.update(single_selection=True, settings_mode=True) user_selection = self._user_select(prompt) selected_entry = self._resolve_selection(user_selection) if user_selection.isnumeric(): @@ -381,12 +343,51 @@ class Menu(): # Done return selected_entry - def simple_select(self, prompt='Please make a selection: '): + def simple_select(self, prompt='Please make a selection: ', update=True): """Display menu and make a single selection, returns tuple.""" - self._update() + if update: + self.update() user_selection = self._user_select(prompt) return self._resolve_selection(user_selection) + def update(self, single_selection=True, settings_mode=False): + """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, + ) + class TryAndPrint(): # pylint: disable=too-many-instance-attributes From d200878e0dbea3d60e15c9c04f6b603aa4135c1a Mon Sep 17 00:00:00 2001 From: 2Shirt <2xShirt@gmail.com> Date: Wed, 21 Apr 2021 22:27:49 -0600 Subject: [PATCH 16/43] Revert update() to private method and add wrapper --- scripts/wk/std.py | 84 +++++++++++++++++++++++++---------------------- 1 file changed, 44 insertions(+), 40 deletions(-) diff --git a/scripts/wk/std.py b/scripts/wk/std.py index f55cd1b3..b8f6910a 100644 --- a/scripts/wk/std.py +++ b/scripts/wk/std.py @@ -228,6 +228,44 @@ class Menu(): # Done return resolved_selection + def _update(self, single_selection=True, settings_mode=False): + """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, toggle=True, status=None): """Update entry selection status either directly or by toggling.""" if entry in self.sets: @@ -302,7 +340,7 @@ class Menu(): NOTE: Menu is displayed until an action entry is selected. """ while True: - self.update(single_selection=False) + self._update(single_selection=False) user_selection = self._user_select(prompt) selected_entry = self._resolve_selection(user_selection) if user_selection.isnumeric(): @@ -326,7 +364,7 @@ class Menu(): } while True: - self.update(single_selection=True, settings_mode=True) + self._update(single_selection=True, settings_mode=True) user_selection = self._user_select(prompt) selected_entry = self._resolve_selection(user_selection) if user_selection.isnumeric(): @@ -346,47 +384,13 @@ class Menu(): def simple_select(self, prompt='Please make a selection: ', update=True): """Display menu and make a single selection, returns tuple.""" if update: - self.update() + self._update() user_selection = self._user_select(prompt) return self._resolve_selection(user_selection) - def update(self, single_selection=True, settings_mode=False): - """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(self): + """Update menu with default settings.""" + self._update() class TryAndPrint(): From c2d3752bc595a4f0be7d156538594078ab8d8d01 Mon Sep 17 00:00:00 2001 From: 2Shirt <2xShirt@gmail.com> Date: Wed, 21 Apr 2021 23:57:19 -0600 Subject: [PATCH 17/43] Update TryAndPrint() --- scripts/wk/std.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/scripts/wk/std.py b/scripts/wk/std.py index b8f6910a..c568c499 100644 --- a/scripts/wk/std.py +++ b/scripts/wk/std.py @@ -569,13 +569,11 @@ class TryAndPrint(): verbose, ) f_exception = None + catch_all = catch_all if catch_all else self.catch_all msg_good = msg_good if msg_good else self.msg_good output = None result_msg = 'UNKNOWN' - if catch_all is None: - catch_all = self.catch_all - if verbose is None: - verbose = self.verbose + verbose = verbose if verbose else self.verbose # Build exception tuples e_exceptions = tuple(self._get_exception(e) for e in self.list_errors) @@ -619,8 +617,8 @@ class TryAndPrint(): # Done self._log_result(message, result_msg) return { - 'Failed': bool(f_exception), 'Exception': f_exception, + 'Failed': bool(f_exception), 'Output': output, } From 10b443f0f5331ffc92763f2a2a3f4e75cb77a002 Mon Sep 17 00:00:00 2001 From: 2Shirt <2xShirt@gmail.com> Date: Wed, 21 Apr 2021 23:58:51 -0600 Subject: [PATCH 18/43] Add framework for running auto repair groups --- scripts/auto_repairs.py | 95 ++++++++++++++++- scripts/wk/repairs/win.py | 217 +++++++++++++++++++++++--------------- scripts/wk/std.py | 1 + 3 files changed, 226 insertions(+), 87 deletions(-) diff --git a/scripts/auto_repairs.py b/scripts/auto_repairs.py index f4c471fc..8096e5b8 100644 --- a/scripts/auto_repairs.py +++ b/scripts/auto_repairs.py @@ -3,15 +3,106 @@ import os import sys +import random # TODO: Deleteme +import time os.chdir(os.path.dirname(os.path.realpath(__file__))) sys.path.append(os.getcwd()) -import wk +import wk # pylint: disable=wrong-import-position + + +# Classes +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 = { + 'Selected': True, + **kwargs, + } + if function: + self.details['Function'] = function + + +# TODO: Deleteme +TRY_AND_PRINT = wk.std.TryAndPrint() +def placeholder_function(group, name): + TRY_AND_PRINT.run(f'{name}...', time.sleep, random.randint(1, 3)) + wk.repairs.win.save_settings(group, name, done=True) + +def placeholder_reboot(group, name): + print('"Rebooting" shortly...') + time.sleep(random.randint(1, 3)) + wk.repairs.win.save_settings(group, name, done=True) + raise SystemExit + + +# STATIC VARIABLES +BASE_MENUS = { + 'Options': { + 'Backup Settings': ( + MenuEntry('Enable RegBack', placeholder_function), + MenuEntry('Enable System Restore', placeholder_function), + MenuEntry('Create System Restore', placeholder_function), + MenuEntry('Backup Browsers', placeholder_function), + MenuEntry('Backup Power Plans', placeholder_function), + MenuEntry('Backup Registry', placeholder_function), + ), + 'Windows Repairs': ( + MenuEntry('Disable Windows Updates', placeholder_function), + MenuEntry('Reset Windows Updates', placeholder_function), + MenuEntry('Reboot', placeholder_reboot), + MenuEntry('CHKDSK', placeholder_function), + MenuEntry('DISM RestoreHealth', wk.repairs.win.auto_dism), + MenuEntry('SFC Scan', placeholder_function), + MenuEntry('Fix File Associations', placeholder_function), + MenuEntry('Clear Proxy Settings', placeholder_function), + MenuEntry('Disable Pending Renames', placeholder_function), + MenuEntry('Registry Repairs', placeholder_function), + MenuEntry('Repair Safe Mode', placeholder_function), + MenuEntry('Reset UAC', placeholder_function), + MenuEntry('Reset Windows Policies', placeholder_function), + ), + 'Malware Cleanup': ( + MenuEntry('BleachBit', placeholder_function), + MenuEntry('HitmanPro', placeholder_function), + MenuEntry('KVRT', placeholder_function), + MenuEntry('Windows Defender', placeholder_function), + MenuEntry('Reboot', placeholder_reboot), + ), + 'Manual Steps': ( + MenuEntry('AdwCleaner', placeholder_function), + MenuEntry('IO Bit Uninstaller', placeholder_function), + MenuEntry('Enable Windows Updates', placeholder_function), + ), + }, + 'Toggles': ( + MenuEntry('Kill Explorer'), + MenuEntry('Run RKill at startup'), + MenuEntry('Use Autologon'), + ), + 'Actions': ( + MenuEntry('Options'), + MenuEntry('Start', Separator=True), + MenuEntry('Quit'), + ), + } if __name__ == '__main__': try: - wk.repairs.win.run_auto_repairs() + wk.repairs.win.run_auto_repairs(BASE_MENUS) except SystemExit: raise except: #pylint: disable=bare-except diff --git a/scripts/wk/repairs/win.py b/scripts/wk/repairs/win.py index 324d05a7..6ae06700 100644 --- a/scripts/wk/repairs/win.py +++ b/scripts/wk/repairs/win.py @@ -13,7 +13,14 @@ from wk.cfg.main import KIT_NAME_FULL from wk.exe import get_procs, run_program, popen_program, wait_for_procs from wk.kit.tools import run_tool from wk.log import format_log_path, update_log_path -from wk.os.win import reg_delete_value, reg_read_value, reg_set_value +from wk.os.win import ( + reg_delete_value, + reg_read_value, + reg_set_value, + disable_service, + enable_service, + stop_service, + ) from wk.std import ( GenericError, GenericWarning, @@ -24,8 +31,11 @@ from wk.std import ( color_string, pause, print_info, + print_standard, + print_warning, set_title, sleep, + strip_colors, ) @@ -34,85 +44,39 @@ LOG = logging.getLogger(__name__) AUTO_REPAIR_DELAY_IN_SECONDS = 30 AUTO_REPAIR_KEY = fr'Software\{KIT_NAME_FULL}\Auto Repairs' CONEMU = 'ConEmuPID' in os.environ -MENUS = { - 'Options': { - 'Backup Settings': ( - ('Enable System Restore', None), - ('Create System Restore', None), - ('Backup Browsers', None), - ('Backup Power Plans', None), - ), - 'Windows Repairs': ( - ('Disable Windows Updates', None), - ('Reset Windows Updates', None), - ('-Reboot-', None), - ('CHKDSK', None), - ('DISM RestoreHealth', None), - ('SFC Scan', None), - ('Fix File Associations', None), - ('Clear Proxy Settings', None), - ('Disable Pending Renames', None), - ('Registry Repairs', None), - ('Repair Safe Mode', None), - ('Reset Windows Policies', None), - ), - 'Malware Cleanup': ( - ('BleachBit', None), - ('HitmanPro', None), - ('KVRT', None), - ('Windows Defender', None), - ('-Reboot-', None), - ), - 'Manual Steps': ( - ('AdwCleaner', None), - ('IO Bit Uninstaller', None), - ('Enable Windows Updates', None), - ), - }, - 'Toggles': ( - 'Kill Explorer', - 'Run RKill at startup', - 'Use Autologon', - ), - 'Actions': ( - 'Options', - 'Start', - 'Quit', - ), - } OS_VERSION = float(platform.win32_ver()[0]) TRY_PRINT = TryAndPrint() +TRY_PRINT.verbose = True +#for error in ('subprocess.CalledProcessError', 'FileNotFoundError'): +# TRY_PRINT.add_error(error) -# Auto Repairs Functions -def build_menus(title): +# Auto Repairs +def build_menus(base_menus, title): """Build menus, returns dict.""" menus = {} menus['Main'] = Menu(title=f'{title}\n{color_string("Main Menu", "GREEN")}') - # Run groups - for group, items in MENUS['Options'].items(): - menus[group] = Menu(title=f'{title}\n{color_string(group, "GREEN")}') - menus[group].disabled_str = color_string('Forced', 'ORANGE') - for item in items: - menus[group].add_option(item[0], {'Display Name': item[0], 'Selected': True}) - if '-Reboot-' in menus[group].options: - menus[group].options['-Reboot-']['Disabled'] = True - menus[group].add_action('Main Menu') - for option in MENUS['Options']: - menus['Main'].add_option(option, {'Display Name': option, 'Selected': True}) - - # Actions - for action in MENUS['Actions']: - menus['Main'].add_action(action, {'Display Name': action, 'Selected': True}) - menus['Main'].actions['Start']['Separator'] = True + # Main Menu + for entry in base_menus['Actions']: + menus['Main'].add_action(entry.name, entry.details) + for group in base_menus['Options']: + menus['Main'].add_option(group, {'Selected': True}) # Options menus['Options'] = Menu(title=f'{title}\n{color_string("Options", "GREEN")}') - for toggle in MENUS['Toggles']: - menus['Options'].add_toggle(toggle, {'Display Name': toggle, 'Selected': True}) + for entry in base_menus['Toggles']: + menus['Options'].add_toggle(entry.name, entry.details) menus['Options'].add_action('Main Menu') + # Run groups + for group, entries in base_menus['Options'].items(): + menus[group] = Menu(title=f'{title}\n{color_string(group, "GREEN")}') + menus[group].disabled_str = 'Done' + for entry in entries: + menus[group].add_option(entry.name, entry.details) + menus[group].add_action('Main Menu') + # Initialize main menu display names menus['Main'].update() @@ -166,12 +130,28 @@ def end_session(): ) -def init(): +def get_entry_settings(group, name): + """Get menu entry settings from the registry, returns dict.""" + key_path = fr'{AUTO_REPAIR_KEY}\{group}\{strip_colors(name)}' + settings = {} + for value in ('Done', 'Failed', 'Result'): + try: + settings[value] = reg_read_value('HKCU', key_path, value) + except FileNotFoundError: + # Ignore and use current settings + pass + + # Done + return settings + + +def init(menus): """Initialize Auto Repairs.""" session_started = is_session_started() # Start or resume a repair session if session_started: + load_settings(menus) print_info('Resuming session, press CTRL+c to cancel') for _x in range(AUTO_REPAIR_DELAY_IN_SECONDS, 0, -1): print(f' {_x} second{"" if _x==1 else "s"} remaining... \r', end='') @@ -184,7 +164,7 @@ def init(): def init_run(options): """Initialize Auto Repairs Run.""" - if options['Kill Explorer']['Selected']: + if options.toggles['Kill Explorer']['Selected']: atexit.register(start_explorer) kill_explorer() # TODO: Sync Clock @@ -207,9 +187,9 @@ def init_session(options): run_program(cmd) # One-time tasks - if options['Use Autologon']['Selected']: + if options.toggles['Use Autologon']['Selected']: run_tool('Sysinternals', 'Autologon') - reboot() + # TODO: Re-enable reboot() def is_session_started(): @@ -224,7 +204,19 @@ def is_session_started(): return session_started -def run_auto_repairs(): +def load_settings(menus): + """Load session settings from the registry.""" + for group, menu in menus.items(): + if group == 'Main': + continue + for name in menu.options: + menu.options[name].update(get_entry_settings(group, name)) + if menu.options[name].get('Done', '0') == '1': + menu.options[name]['Selected'] = False + menu.options[name]['Disabled'] = True + + +def run_auto_repairs(base_menus): """Run Auto Repairs.""" update_log_path(dest_name='Auto Repairs', timestamp=True) title = f'{KIT_NAME_FULL}: Auto Repairs' @@ -234,11 +226,12 @@ def run_auto_repairs(): print('') # Generate menus - menus = build_menus(title) + print_standard('Initializing...') + menus = build_menus(base_menus, title) # Init try: - session_started = init() + session_started = init(menus) except KeyboardInterrupt: # Assuming session was started and resume countdown was interrupted session_started = None @@ -248,11 +241,10 @@ def run_auto_repairs(): while True: update_main_menu(menus) selection = menus['Main'].simple_select(update=False) - if selection[0] in MENUS['Options'] or selection[0] == 'Options': + if selection[0] in base_menus['Options'] or selection[0] == 'Options': menus[selection[0]].advanced_select() elif 'Start' in selection: - # TODO: Run repairs - pass + break elif 'Quit' in selection: if ask('End session?'): end_session() @@ -262,23 +254,39 @@ def run_auto_repairs(): if session_started is None: session_started = is_session_started() - # Run repairs - # TODO: Run repairs + # Start or resume repairs init_run(menus['Options']) if not session_started: init_session(menus['Options']) - atexit.unregister(start_explorer) - end_session() + + # Run repairs + clear_screen() + for group, menu in menus.items(): + if group in ('Main', 'Options'): + continue + for name, details in menu.options.items(): + if details.get('Done', None) == '1': + continue + details['Function'](group, name) # Done + end_session() print_info('Done') pause('Press Enter to exit...') +def save_settings(group, name, done=False, failed=False, result='Unknown'): + """Load session settings from the registry.""" + key_path = fr'{AUTO_REPAIR_KEY}\{group}\{strip_colors(name)}' + reg_set_value('HKCU', key_path, 'Done', '1' if done else '0', 'SZ') + reg_set_value('HKCU', key_path, 'Failed', '1' if failed else '0', 'SZ') + reg_set_value('HKCU', key_path, 'Result', result, 'SZ') + + def update_main_menu(menus): """Update main menu based on current selections.""" index = 1 - skip = '-Reboot-' + skip = 'Reboot' for name in menus['Main'].options: checkmark = ' ' selected = [ @@ -293,16 +301,54 @@ def update_main_menu(menus): menus['Main'].options[name]['Display Name'] = display_name +# Auto Repairs: Wrapper Functions +def auto_dism(group, name): + """Auto DISM repairs, returns bool.""" + needs_reboot = False + result = TRY_PRINT.run('DISM (RestoreHealth)...', run_dism) + + # Try again if necessary + if result['Failed']: + TRY_PRINT.run('Enabling Windows Updates...', enable_windows_updates) + result = TRY_PRINT.run('DISM (RestoreHealth)...', run_dism) + TRY_PRINT.run('Disabling Windows Updates...', disable_windows_updates) + needs_reboot = True + + # Save settings + save_settings( + group, name, done=True, + failed=result['Failed'], + result=result['Message'], + ) + + # Done + if needs_reboot: + reboot() + + # OS Built-in Functions +def disable_windows_updates(): + """Disable and stop Windows Updates.""" + stop_service('wuauserv') + disable_service('wuauserv') + + +def enable_windows_updates(): + """Enable Windows Updates.""" + enable_service('wuauserv', 'demand') + + def kill_explorer(): """Kill all Explorer processes.""" cmd = ['taskkill', '/im', 'explorer.exe', '/f'] run_program(cmd, check=False) -def reboot(): +def reboot(timeout=10): """Reboot the system.""" atexit.unregister(start_explorer) + print_warning(f'Rebooting the system in {timeout} seconds...') + sleep(timeout) cmd = ['shutdown', '-r', '-t', '0'] run_program(cmd, check=False) raise SystemExit @@ -327,7 +373,7 @@ def run_chkdsk_online(): if OS_VERSION >= 8: cmd.extend(['/scan', '/perf']) if CONEMU: - cmd.extend(['-new_console:n', '-new_console:s33V']) + cmd.extend(['-new_console:nb', '-new_console:s33V']) retried = False # Run scan @@ -360,7 +406,7 @@ def run_chkdsk_online(): def run_dism(repair=True): """Run DISM to either scan or repair component store health.""" - conemu_args = ['-new_console:n', '-new_console:s33V'] if CONEMU else [] + conemu_args = ['-new_console:nb', '-new_console:s33V'] if CONEMU else [] # Bail early if OS_VERSION < 8: @@ -370,6 +416,7 @@ def run_dism(repair=True): log_path = format_log_path( log_name=f'DISM_{"Restore" if repair else "Scan"}Health', tool=True, ) + log_path.parent.mkdir(parents=True, exist_ok=True) cmd = [ 'DISM', '/Online', '/Cleanup-Image', '/RestoreHealth' if repair else '/ScanHealth', diff --git a/scripts/wk/std.py b/scripts/wk/std.py index c568c499..e601befc 100644 --- a/scripts/wk/std.py +++ b/scripts/wk/std.py @@ -619,6 +619,7 @@ class TryAndPrint(): return { 'Exception': f_exception, 'Failed': bool(f_exception), + 'Message': result_msg, 'Output': output, } From 9a77a5cb9b103198e5d532444583bc17007bcab7 Mon Sep 17 00:00:00 2001 From: 2Shirt <2xShirt@gmail.com> Date: Fri, 23 Apr 2021 23:39:11 -0600 Subject: [PATCH 19/43] Update Auto Repair sections * Expanded saving/loading settings from registry * Keep previous selections by default but allow changes * Print previous session(s) to have the whole session info present * Changed variable names for clarity (why am I so bad at this?) --- scripts/auto_repairs.py | 26 ++--- scripts/wk/repairs/win.py | 193 ++++++++++++++++++++++++++++++-------- scripts/wk/std.py | 8 +- 3 files changed, 174 insertions(+), 53 deletions(-) diff --git a/scripts/auto_repairs.py b/scripts/auto_repairs.py index 8096e5b8..2ddfc344 100644 --- a/scripts/auto_repairs.py +++ b/scripts/auto_repairs.py @@ -15,7 +15,7 @@ import wk # pylint: disable=wrong-import-position class MenuEntry(): # pylint: disable=too-few-public-methods """Simple class to allow cleaner code below.""" - def __init__(self, name, function=None, **kwargs): + def __init__(self, name, function, **kwargs): self.name = name # Color reboot entries @@ -28,29 +28,29 @@ class MenuEntry(): # Set details self.details = { + 'Function': function, 'Selected': True, **kwargs, } - if function: - self.details['Function'] = function # TODO: Deleteme TRY_AND_PRINT = wk.std.TryAndPrint() +TRY_AND_PRINT.width = 50 def placeholder_function(group, name): TRY_AND_PRINT.run(f'{name}...', time.sleep, random.randint(1, 3)) - wk.repairs.win.save_settings(group, name, done=True) + wk.repairs.win.save_settings(group, name, done=True, result='SUCCESS') def placeholder_reboot(group, name): print('"Rebooting" shortly...') time.sleep(random.randint(1, 3)) - wk.repairs.win.save_settings(group, name, done=True) + wk.repairs.win.save_settings(group, name, done=True, result='DONE') raise SystemExit # STATIC VARIABLES BASE_MENUS = { - 'Options': { + 'Groups': { 'Backup Settings': ( MenuEntry('Enable RegBack', placeholder_function), MenuEntry('Enable System Restore', placeholder_function), @@ -87,15 +87,15 @@ BASE_MENUS = { MenuEntry('Enable Windows Updates', placeholder_function), ), }, - 'Toggles': ( - MenuEntry('Kill Explorer'), - MenuEntry('Run RKill at startup'), - MenuEntry('Use Autologon'), + 'Options': ( + MenuEntry('Kill Explorer', placeholder_function), + MenuEntry('Run RKill at startup', placeholder_function), + MenuEntry('Use Autologon', placeholder_function), ), 'Actions': ( - MenuEntry('Options'), - MenuEntry('Start', Separator=True), - MenuEntry('Quit'), + MenuEntry('Options', placeholder_function), + MenuEntry('Start', placeholder_function, Separator=True), + MenuEntry('Quit', placeholder_function), ), } diff --git a/scripts/wk/repairs/win.py b/scripts/wk/repairs/win.py index 6ae06700..97223d64 100644 --- a/scripts/wk/repairs/win.py +++ b/scripts/wk/repairs/win.py @@ -34,6 +34,7 @@ from wk.std import ( print_standard, print_warning, set_title, + show_data, sleep, strip_colors, ) @@ -45,7 +46,9 @@ AUTO_REPAIR_DELAY_IN_SECONDS = 30 AUTO_REPAIR_KEY = fr'Software\{KIT_NAME_FULL}\Auto Repairs' CONEMU = 'ConEmuPID' in os.environ OS_VERSION = float(platform.win32_ver()[0]) +WIDTH = 50 TRY_PRINT = TryAndPrint() +TRY_PRINT.width = WIDTH TRY_PRINT.verbose = True #for error in ('subprocess.CalledProcessError', 'FileNotFoundError'): # TRY_PRINT.add_error(error) @@ -60,22 +63,30 @@ def build_menus(base_menus, title): # Main Menu for entry in base_menus['Actions']: menus['Main'].add_action(entry.name, entry.details) - for group in base_menus['Options']: + for group in base_menus['Groups']: menus['Main'].add_option(group, {'Selected': True}) # Options menus['Options'] = Menu(title=f'{title}\n{color_string("Options", "GREEN")}') - for entry in base_menus['Toggles']: - menus['Options'].add_toggle(entry.name, entry.details) - menus['Options'].add_action('Main Menu') + for entry in base_menus['Options']: + menus['Options'].add_option(entry.name, entry.details) + menus['Options'].add_action('All') + menus['Options'].add_action('None') + menus['Options'].add_action('Main Menu', {'Separator': True}) + menus['Options'].add_action('Quit') # Run groups - for group, entries in base_menus['Options'].items(): + for group, entries in base_menus['Groups'].items(): menus[group] = Menu(title=f'{title}\n{color_string(group, "GREEN")}') - menus[group].disabled_str = 'Done' + menus[group].disabled_str = 'Locked' for entry in entries: menus[group].add_option(entry.name, entry.details) - menus[group].add_action('Main Menu') + menus[group].add_action('All') + menus[group].add_action('None') + menus[group].add_action('Select Skipped Entries', {'Separator': True}) + menus[group].add_action('Unlock All Entries') + menus[group].add_action('Main Menu', {'Separator': True}) + menus[group].add_action('Quit') # Initialize main menu display names menus['Main'].update() @@ -89,11 +100,16 @@ def end_session(): print_info('Ending repair session') auto_admin_logon = '0' - # Delete Auto Repairs session key + # Delete Auto Repairs keys try: reg_delete_value('HKCU', AUTO_REPAIR_KEY, 'SessionStarted') except FileNotFoundError: LOG.error('Ending repair session but session not started.') + try: + cmd = ['reg', 'delete', fr'HKCU\{AUTO_REPAIR_KEY}', '/f'] + run_program(cmd) + except CalledProcessError: + LOG.error('Failed to remote Auto Repairs session settings') # Remove logon task cmd = [ @@ -134,13 +150,17 @@ def get_entry_settings(group, name): """Get menu entry settings from the registry, returns dict.""" key_path = fr'{AUTO_REPAIR_KEY}\{group}\{strip_colors(name)}' settings = {} - for value in ('Done', 'Failed', 'Result'): + for value in ('done', 'failed', 'result', 'selected', 'skipped', 'warning'): try: - settings[value] = reg_read_value('HKCU', key_path, value) + settings[value.title()] = reg_read_value('HKCU', key_path, value) except FileNotFoundError: # Ignore and use current settings pass + # Disable previously run or skipped entries + if settings.get('Done', False) or settings.get('Skipped', False): + settings['Disabled'] = True + # Done return settings @@ -164,7 +184,7 @@ def init(menus): def init_run(options): """Initialize Auto Repairs Run.""" - if options.toggles['Kill Explorer']['Selected']: + if options['Kill Explorer']['Selected']: atexit.register(start_explorer) kill_explorer() # TODO: Sync Clock @@ -187,8 +207,9 @@ def init_session(options): run_program(cmd) # One-time tasks - if options.toggles['Use Autologon']['Selected']: + if options['Use Autologon']['Selected']: run_tool('Sysinternals', 'Autologon') + # TODO: TDSSKiller? # TODO: Re-enable reboot() @@ -211,9 +232,6 @@ def load_settings(menus): continue for name in menu.options: menu.options[name].update(get_entry_settings(group, name)) - if menu.options[name].get('Done', '0') == '1': - menu.options[name]['Selected'] = False - menu.options[name]['Disabled'] = True def run_auto_repairs(base_menus): @@ -238,36 +256,29 @@ def run_auto_repairs(base_menus): # Show Menu if session_started is None or not session_started: - while True: - update_main_menu(menus) - selection = menus['Main'].simple_select(update=False) - if selection[0] in base_menus['Options'] or selection[0] == 'Options': - menus[selection[0]].advanced_select() - elif 'Start' in selection: - break - elif 'Quit' in selection: - if ask('End session?'): - end_session() - raise SystemExit + try: + show_main_menu(base_menus, menus) + except SystemExit: + if ask('End session?'): + end_session() + raise # Re-check if a repair session was started if session_started is None: session_started = is_session_started() # Start or resume repairs - init_run(menus['Options']) + save_selection_settings(menus) + init_run(menus['Options'].options) if not session_started: - init_session(menus['Options']) + init_session(menus['Options'].options) # Run repairs clear_screen() for group, menu in menus.items(): if group in ('Main', 'Options'): continue - for name, details in menu.options.items(): - if details.get('Done', None) == '1': - continue - details['Function'](group, name) + run_group(group, menu) # Done end_session() @@ -275,12 +286,119 @@ def run_auto_repairs(base_menus): pause('Press Enter to exit...') -def save_settings(group, name, done=False, failed=False, result='Unknown'): - """Load session settings from the registry.""" +def run_group(group, menu): + """Run entries in group if appropriate.""" + print_info(f' {group}') + for name, details in menu.options.items(): + name_str = strip_colors(name) + skipped = details.get('Skipped', False) + done = details.get('Done', False) + disabled = details.get('Disabled', False) + selected = details.get('Selected', False) + + # Selection changed + if (skipped or done) and not disabled and selected: + save_settings(group, name, done=False, skipped=False) + details['Function'](group, name) + continue + + # Previously skipped + if skipped: + show_data(f'{name_str}...', 'Skipped', 'YELLOW', width=WIDTH) + continue + + # Previously ran + if done: + color = 'GREEN' + if details.get('Failed', False): + color = 'RED' + elif details.get('Warning', False): + color = 'YELLOW' + show_data( + f'{name_str}...', details.get('Result', 'Unknown'), color, width=WIDTH, + ) + continue + + # Not selected + if not selected: + show_data(f'{name_str}...', 'Skipped', 'YELLOW', width=WIDTH) + save_settings(group, name, skipped=True) + continue + + # Selected + details['Function'](group, name) + + +def save_selection_settings(menus): + """Save selections in the registry.""" + for group, menu in menus.items(): + if group == 'Main': + continue + for name, details in menu.options.items(): + save_settings( + group, name, + disabled=details.get('Disabled', False), + selected=details.get('Selected', False), + ) + + +def save_settings(group, name, **kwargs): + """Save entry settings in the registry.""" key_path = fr'{AUTO_REPAIR_KEY}\{group}\{strip_colors(name)}' - reg_set_value('HKCU', key_path, 'Done', '1' if done else '0', 'SZ') - reg_set_value('HKCU', key_path, 'Failed', '1' if failed else '0', 'SZ') - reg_set_value('HKCU', key_path, 'Result', result, 'SZ') + for value_name, data in kwargs.items(): + if isinstance(data, bool): + data = 1 if data else 0 + if isinstance(data, int): + data_type = 'DWORD' + elif isinstance(data, str): + data_type = 'SZ' + else: + raise TypeError(f'Invalid data: "{data}" ({type(data)})') + reg_set_value('HKCU', key_path, value_name, data, data_type) + + +def show_main_menu(base_menus, menus): + """Show main menu and handle actions.""" + while True: + update_main_menu(menus) + selection = menus['Main'].simple_select(update=False) + if selection[0] in base_menus['Groups'] or selection[0] == 'Options': + show_sub_menu(menus[selection[0]]) + elif 'Start' in selection: + break + elif 'Quit' in selection: + raise SystemExit + + +def show_sub_menu(menu): + """Show sub-menu and handle sub-menu actions.""" + while True: + selection = menu.advanced_select() + if 'Main Menu' in selection: + break + if 'Quit' in selection: + raise SystemExit + + # Modify entries + key = 'Selected' + unlock_all = False + unlock_skipped = False + if 'Select Skipped Entries' in selection: + key = 'Disabled' + unlock_skipped = True + value = False + if 'Unlock All Entries' in selection: + key = 'Disabled' + unlock_all = True + value = False + else: + value = 'All' in selection + for name in menu.options: + if (unlock_all + or (unlock_skipped and not menu.options[name].get('Selected', False)) + or not menu.options[name].get('Disabled', False) + ): + menu.options[name][key] = value def update_main_menu(menus): @@ -318,6 +436,7 @@ def auto_dism(group, name): save_settings( group, name, done=True, failed=result['Failed'], + warning=not result['Failed'] and needs_reboot, result=result['Message'], ) diff --git a/scripts/wk/std.py b/scripts/wk/std.py index e601befc..9f235a1f 100644 --- a/scripts/wk/std.py +++ b/scripts/wk/std.py @@ -985,11 +985,13 @@ def set_title(title): print_error('Setting the title is only supported under Windows.') -def show_data(message, data, color=None): - """Display info using standard WIDTH and INDENT.""" +def show_data(message, data, color=None, indent=None, width=None): + """Display info using default or provided indent and width.""" colors = (None, color if color else None) + indent = INDENT if indent is None else indent + width = WIDTH if width is None else width print_colored( - (f'{" "*INDENT}{message:<{WIDTH}}', data), + (f'{" "*indent}{message:<{width}}', data), colors, log=True, sep='', From 005d4d1ea698c06b2dbaae75226fd81c144fc44f Mon Sep 17 00:00:00 2001 From: 2Shirt <2xShirt@gmail.com> Date: Sat, 24 Apr 2021 16:55:17 -0600 Subject: [PATCH 20/43] Search all modules for a matching exception --- scripts/wk/std.py | 36 +++++++++++++++++++++++++++--------- 1 file changed, 27 insertions(+), 9 deletions(-) diff --git a/scripts/wk/std.py b/scripts/wk/std.py index 9f235a1f..fe31e50c 100644 --- a/scripts/wk/std.py +++ b/scripts/wk/std.py @@ -18,6 +18,7 @@ import time import traceback from collections import OrderedDict +from functools import cache import requests @@ -498,24 +499,41 @@ class TryAndPrint(): # Done return result_msg + @cache def _get_exception(self, name): # pylint: disable=no-self-use """Get exception by name, returns exception object. [Doctest] - >>> self._get_exception('AttributeError') + >>> t = TryAndPrint() + >>> t._get_exception('AttributeError') - >>> self._get_exception('CalledProcessError') + >>> t._get_exception('CalledProcessError') - >>> self._get_exception('GenericError') - + >>> t._get_exception('GenericError') + """ LOG.debug('Getting exception: %s', name) - try: - obj = getattr(sys.modules[__name__], name) - except AttributeError: - # Try builtin classes - obj = getattr(sys.modules['builtins'], 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: + obj = getattr(sys.modules[_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 _log_result(self, message, result_msg): From b2c94113d9ebf63e2da48d0ad08639d2fc97bf60 Mon Sep 17 00:00:00 2001 From: 2Shirt <2xShirt@gmail.com> Date: Sat, 24 Apr 2021 19:03:41 -0600 Subject: [PATCH 21/43] Remove date from DEFAULT_LOG_DIR --- scripts/wk/log.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/scripts/wk/log.py b/scripts/wk/log.py index cf7b373a..2cabd520 100644 --- a/scripts/wk/log.py +++ b/scripts/wk/log.py @@ -17,8 +17,7 @@ if os.name == 'nt': # Example: "C:\WK\1955-11-05\WizardKit" DEFAULT_LOG_DIR = ( f'{os.environ.get("SYSTEMDRIVE", "C:")}/' - f'{cfg.main.KIT_NAME_SHORT}/' - f'{time.strftime("%Y-%m-%d")}' + f'{cfg.main.KIT_NAME_SHORT}/Logs/' ) else: # Example: "/home/tech/Logs" From 1dbad4bafec756b4211fbb2a0db84959926fe178 Mon Sep 17 00:00:00 2001 From: 2Shirt <2xShirt@gmail.com> Date: Sat, 24 Apr 2021 20:18:45 -0600 Subject: [PATCH 22/43] Split result variables to result and message result is for the return variable from TryAndPrint calls. message is the text to display or save to the registry. --- scripts/auto_repairs.py | 6 +++--- scripts/wk/repairs/win.py | 25 ++++++++++++++++++------- 2 files changed, 21 insertions(+), 10 deletions(-) diff --git a/scripts/auto_repairs.py b/scripts/auto_repairs.py index 2ddfc344..00cea939 100644 --- a/scripts/auto_repairs.py +++ b/scripts/auto_repairs.py @@ -38,13 +38,13 @@ class MenuEntry(): TRY_AND_PRINT = wk.std.TryAndPrint() TRY_AND_PRINT.width = 50 def placeholder_function(group, name): - TRY_AND_PRINT.run(f'{name}...', time.sleep, random.randint(1, 3)) - wk.repairs.win.save_settings(group, name, done=True, result='SUCCESS') + result = TRY_AND_PRINT.run(f'{name}...', time.sleep, random.randint(1, 3)) + wk.repairs.win.save_settings(group, name, result=result) def placeholder_reboot(group, name): print('"Rebooting" shortly...') time.sleep(random.randint(1, 3)) - wk.repairs.win.save_settings(group, name, done=True, result='DONE') + wk.repairs.win.save_settings(group, name, done=True, message='DONE') raise SystemExit diff --git a/scripts/wk/repairs/win.py b/scripts/wk/repairs/win.py index 97223d64..150229cd 100644 --- a/scripts/wk/repairs/win.py +++ b/scripts/wk/repairs/win.py @@ -150,7 +150,7 @@ def get_entry_settings(group, name): """Get menu entry settings from the registry, returns dict.""" key_path = fr'{AUTO_REPAIR_KEY}\{group}\{strip_colors(name)}' settings = {} - for value in ('done', 'failed', 'result', 'selected', 'skipped', 'warning'): + for value in ('done', 'failed', 'message', 'selected', 'skipped', 'warning'): try: settings[value.title()] = reg_read_value('HKCU', key_path, value) except FileNotFoundError: @@ -310,12 +310,13 @@ def run_group(group, menu): # Previously ran if done: color = 'GREEN' - if details.get('Failed', False): - color = 'RED' - elif details.get('Warning', False): + if details.get('Warning', False): color = 'YELLOW' + elif details.get('Failed', False): + color = 'RED' show_data( - f'{name_str}...', details.get('Result', 'Unknown'), color, width=WIDTH, + f'{name_str}...', + details.get('Message', 'Unknown'), color, width=WIDTH, ) continue @@ -342,9 +343,19 @@ def save_selection_settings(menus): ) -def save_settings(group, name, **kwargs): +def save_settings(group, name, result=None, **kwargs): """Save entry settings in the registry.""" key_path = fr'{AUTO_REPAIR_KEY}\{group}\{strip_colors(name)}' + + # Get values from TryAndPrint result + if result: + kwargs.update({ + 'done': True, + 'failed': result['Failed'], + 'message': result['Message'], + }) + + # Write values to registry for value_name, data in kwargs.items(): if isinstance(data, bool): data = 1 if data else 0 @@ -437,7 +448,7 @@ def auto_dism(group, name): group, name, done=True, failed=result['Failed'], warning=not result['Failed'] and needs_reboot, - result=result['Message'], + message=result['Message'], ) # Done From a7db972ba5883649964cc2c45291490facdb1742 Mon Sep 17 00:00:00 2001 From: 2Shirt <2xShirt@gmail.com> Date: Sat, 24 Apr 2021 20:23:08 -0600 Subject: [PATCH 23/43] Get repair functions by name instead of full path --- scripts/auto_repairs.py | 16 ++++++++-------- scripts/wk/repairs/win.py | 11 +++++++++++ 2 files changed, 19 insertions(+), 8 deletions(-) diff --git a/scripts/auto_repairs.py b/scripts/auto_repairs.py index 00cea939..1e47cfeb 100644 --- a/scripts/auto_repairs.py +++ b/scripts/auto_repairs.py @@ -15,7 +15,7 @@ import wk # pylint: disable=wrong-import-position class MenuEntry(): # pylint: disable=too-few-public-methods """Simple class to allow cleaner code below.""" - def __init__(self, name, function, **kwargs): + def __init__(self, name, function=None, **kwargs): self.name = name # Color reboot entries @@ -64,7 +64,7 @@ BASE_MENUS = { MenuEntry('Reset Windows Updates', placeholder_function), MenuEntry('Reboot', placeholder_reboot), MenuEntry('CHKDSK', placeholder_function), - MenuEntry('DISM RestoreHealth', wk.repairs.win.auto_dism), + MenuEntry('DISM RestoreHealth', 'auto_dism'), MenuEntry('SFC Scan', placeholder_function), MenuEntry('Fix File Associations', placeholder_function), MenuEntry('Clear Proxy Settings', placeholder_function), @@ -88,14 +88,14 @@ BASE_MENUS = { ), }, 'Options': ( - MenuEntry('Kill Explorer', placeholder_function), - MenuEntry('Run RKill at startup', placeholder_function), - MenuEntry('Use Autologon', placeholder_function), + MenuEntry('Kill Explorer'), + MenuEntry('Run RKill at startup'), + MenuEntry('Use Autologon'), ), 'Actions': ( - MenuEntry('Options', placeholder_function), - MenuEntry('Start', placeholder_function, Separator=True), - MenuEntry('Quit', placeholder_function), + MenuEntry('Options'), + MenuEntry('Start', Separator=True), + MenuEntry('Quit'), ), } diff --git a/scripts/wk/repairs/win.py b/scripts/wk/repairs/win.py index 150229cd..b3f6353d 100644 --- a/scripts/wk/repairs/win.py +++ b/scripts/wk/repairs/win.py @@ -91,6 +91,17 @@ def build_menus(base_menus, title): # Initialize main menu display names menus['Main'].update() + # Fix Function references + for group, menu in menus.items(): + if group not in base_menus['Groups']: + continue + for name in menu.options: + _function = menu.options[name]['Function'] + if isinstance(_function, str): + menu.options[name]['Function'] = getattr( + sys.modules[__name__], _function, + ) + # Done return menus From 078859838af79f67c62855d78e6f5b257127c7d1 Mon Sep 17 00:00:00 2001 From: 2Shirt <2xShirt@gmail.com> Date: Sat, 24 Apr 2021 20:24:39 -0600 Subject: [PATCH 24/43] Add 'Backup Settings' group functions --- scripts/auto_repairs.py | 55 ++++++++--------- scripts/wk/repairs/win.py | 123 ++++++++++++++++++++++++++++++++++++-- 2 files changed, 147 insertions(+), 31 deletions(-) diff --git a/scripts/auto_repairs.py b/scripts/auto_repairs.py index 1e47cfeb..55aff945 100644 --- a/scripts/auto_repairs.py +++ b/scripts/auto_repairs.py @@ -52,39 +52,40 @@ def placeholder_reboot(group, name): BASE_MENUS = { 'Groups': { 'Backup Settings': ( - MenuEntry('Enable RegBack', placeholder_function), - MenuEntry('Enable System Restore', placeholder_function), - MenuEntry('Create System Restore', placeholder_function), - MenuEntry('Backup Browsers', placeholder_function), - MenuEntry('Backup Power Plans', placeholder_function), - MenuEntry('Backup Registry', placeholder_function), + 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('Create System Restore', 'auto_system_restore_create'), + MenuEntry('Backup Browsers', placeholder_function), + MenuEntry('Backup Power Plans', 'auto_backup_power_plans'), + MenuEntry('Backup Registry', 'auto_backup_registry'), ), 'Windows Repairs': ( - MenuEntry('Disable Windows Updates', placeholder_function), - MenuEntry('Reset Windows Updates', placeholder_function), - MenuEntry('Reboot', placeholder_reboot), - MenuEntry('CHKDSK', placeholder_function), - MenuEntry('DISM RestoreHealth', 'auto_dism'), - MenuEntry('SFC Scan', placeholder_function), - MenuEntry('Fix File Associations', placeholder_function), - MenuEntry('Clear Proxy Settings', placeholder_function), - MenuEntry('Disable Pending Renames', placeholder_function), - MenuEntry('Registry Repairs', placeholder_function), - MenuEntry('Repair Safe Mode', placeholder_function), - MenuEntry('Reset UAC', placeholder_function), - MenuEntry('Reset Windows Policies', placeholder_function), + MenuEntry('Disable Windows Updates', placeholder_function), + MenuEntry('Reset Windows Updates', placeholder_function), + MenuEntry('Reboot', placeholder_reboot), + MenuEntry('CHKDSK', placeholder_function), + MenuEntry('DISM RestoreHealth', 'auto_dism'), + MenuEntry('SFC Scan', placeholder_function), + MenuEntry('Fix File Associations', placeholder_function), + MenuEntry('Clear Proxy Settings', placeholder_function), + MenuEntry('Disable Pending Renames', placeholder_function), + MenuEntry('Registry Repairs', placeholder_function), + MenuEntry('Repair Safe Mode', placeholder_function), + MenuEntry('Reset UAC', placeholder_function), + MenuEntry('Reset Windows Policies', placeholder_function), ), 'Malware Cleanup': ( - MenuEntry('BleachBit', placeholder_function), - MenuEntry('HitmanPro', placeholder_function), - MenuEntry('KVRT', placeholder_function), - MenuEntry('Windows Defender', placeholder_function), - MenuEntry('Reboot', placeholder_reboot), + MenuEntry('BleachBit', placeholder_function), + MenuEntry('HitmanPro', placeholder_function), + MenuEntry('KVRT', placeholder_function), + MenuEntry('Windows Defender', placeholder_function), + MenuEntry('Reboot', placeholder_reboot), ), 'Manual Steps': ( - MenuEntry('AdwCleaner', placeholder_function), - MenuEntry('IO Bit Uninstaller', placeholder_function), - MenuEntry('Enable Windows Updates', placeholder_function), + MenuEntry('AdwCleaner', placeholder_function), + MenuEntry('IO Bit Uninstaller', placeholder_function), + MenuEntry('Enable Windows Updates', placeholder_function), ), }, 'Options': ( diff --git a/scripts/wk/repairs/win.py b/scripts/wk/repairs/win.py index b3f6353d..eb080d95 100644 --- a/scripts/wk/repairs/win.py +++ b/scripts/wk/repairs/win.py @@ -5,9 +5,11 @@ import atexit import logging import os import platform +import re import sys +import time -from subprocess import CalledProcessError +from subprocess import CalledProcessError, DEVNULL from wk.cfg.main import KIT_NAME_FULL from wk.exe import get_procs, run_program, popen_program, wait_for_procs @@ -47,11 +49,12 @@ AUTO_REPAIR_KEY = fr'Software\{KIT_NAME_FULL}\Auto Repairs' CONEMU = 'ConEmuPID' in os.environ OS_VERSION = float(platform.win32_ver()[0]) WIDTH = 50 +SYSTEMDRIVE = os.environ.get('SYSTEMDRIVE') TRY_PRINT = TryAndPrint() TRY_PRINT.width = WIDTH TRY_PRINT.verbose = True -#for error in ('subprocess.CalledProcessError', 'FileNotFoundError'): -# TRY_PRINT.add_error(error) +for error in ('CalledProcessError', 'FileNotFoundError'): + TRY_PRINT.add_error(error) # Auto Repairs @@ -442,8 +445,20 @@ def update_main_menu(menus): # Auto Repairs: Wrapper Functions +def auto_backup_power_plans(group, name): + """Backup power plans.""" + result = TRY_PRINT.run('Backup Power Plans...', export_power_plans) + save_settings(group, name, result=result) + + +def auto_backup_registry(group, name): + """Backup registry.""" + result = TRY_PRINT.run('Backup Registry...', backup_registry) + save_settings(group, name, result=result) + + def auto_dism(group, name): - """Auto DISM repairs, returns bool.""" + """Run DISM repairs.""" needs_reboot = False result = TRY_PRINT.run('DISM (RestoreHealth)...', run_dism) @@ -467,7 +482,73 @@ def auto_dism(group, name): reboot() +def auto_enable_regback(group, name): + """Enable RegBack.""" + result = TRY_PRINT.run( + 'Enable RegBack...', reg_set_value, 'HKLM', + r'System\CurrentControlSet\Control\Session Manager\Configuration Manager', + 'EnablePeriodicBackup', 1, 'DWORD', + ) + save_settings(group, name, result=result) + + +def auto_system_restore_create(group, name): + """Create a System Restore point.""" + result = TRY_PRINT.run( + 'Create System Restore...', create_system_restore_point, + ) + save_settings(group, name, result=result) + + +def auto_system_restore_enable(group, name): + """Enable System Restore.""" + cmd = [ + 'powershell', '-Command', 'Enable-ComputerRestore', + '-Drive', SYSTEMDRIVE, + ] + result = TRY_PRINT.run('Enable System Restore...', run_program, cmd=cmd) + save_settings(group, name, result=result) + + +def auto_system_restore_set_size(group, name): + """Set System Restore size.""" + result = TRY_PRINT.run('Set System Restore Size...', set_system_restore_size) + save_settings(group, name, result=result) + + +# Misc Functions +def set_backup_path(name, date=False): + """Set backup path, returns pathlib.Path.""" + backup_path = format_log_path(log_name=f'../Backups/{name}').with_suffix('') + if date: + backup_path = backup_path.joinpath(time.strftime('%Y-%m-%d')) + return backup_path.resolve() + + +# Tool Functions +def backup_registry(): + """Backup Registry.""" + backup_path = set_backup_path('Registry', date=True) + backup_path.parent.mkdir(parents=True, exist_ok=True) + run_tool('Erunt', 'ERUNT', backup_path, 'sysreg', 'curuser', 'otherusers') + + # OS Built-in Functions +def create_system_restore_point(): + """Create System Restore point.""" + cmd = [ + 'powershell', '-Command', 'Checkpoint-Computer', + '-Description', f'{KIT_NAME_FULL}-AutoRepairs', + ] + too_recent = ( + 'WARNING: A new system restore point cannot be created' + 'because one has already been created within the past' + ) + proc = run_program(cmd) + if too_recent in proc.stdout: + raise GenericWarning('Skipped, a restore point was created too recently') + + def disable_windows_updates(): """Disable and stop Windows Updates.""" stop_service('wuauserv') @@ -479,6 +560,31 @@ def enable_windows_updates(): enable_service('wuauserv', 'demand') +def export_power_plans(): + """Export existing power plans.""" + backup_path = set_backup_path('Power Plans', date=True) + backup_path.mkdir(parents=True, exist_ok=True) + cmd = ['powercfg', '/L'] + proc = run_program(cmd) + plans = {} + + # Get plans + for line in proc.stdout.splitlines(): + line = line.strip() + match = re.match(r'^Power Scheme GUID: (.{36})\s+\((.*)\)\s*(\*?)', line) + if match: + name = match.group(2) + if match.group(3): + name += ' (Default)' + plans[name] = match.group(1) + + # Backup plans to disk + for name, guid in plans.items(): + out_path = backup_path.joinpath(f'{name}.pow') + cmd = ['powercfg', '-export', out_path, guid] + run_program(cmd) + + def kill_explorer(): """Kill all Explorer processes.""" cmd = ['taskkill', '/im', 'explorer.exe', '/f'] @@ -608,6 +714,15 @@ def run_sfc_scan(): raise OSError +def set_system_restore_size(size=8): + """Set System Restore size.""" + cmd = [ + 'vssadmin', 'Resize', 'ShadowStorage', + f'/On={SYSTEMDRIVE}', f'/For={SYSTEMDRIVE}', f'/MaxSize={size}%', + ] + run_program(cmd, pipe=False, stderr=DEVNULL, stdout=DEVNULL) + + def start_explorer(): """Start Explorer.""" popen_program(['explorer.exe']) From 973dad32403227fc9c023daf170faf7ff7b817cc Mon Sep 17 00:00:00 2001 From: 2Shirt <2xShirt@gmail.com> Date: Thu, 29 Apr 2021 19:37:09 -0600 Subject: [PATCH 25/43] Add "auto_" windows updates functions --- scripts/auto_repairs.py | 8 ++++---- scripts/wk/io.py | 6 ++++++ scripts/wk/repairs/win.py | 37 ++++++++++++++++++++++++++++++++++++- 3 files changed, 46 insertions(+), 5 deletions(-) diff --git a/scripts/auto_repairs.py b/scripts/auto_repairs.py index 55aff945..aabb9ba6 100644 --- a/scripts/auto_repairs.py +++ b/scripts/auto_repairs.py @@ -56,13 +56,13 @@ BASE_MENUS = { MenuEntry('Enable System Restore', 'auto_system_restore_enable'), MenuEntry('Set System Restore Size', 'auto_system_restore_set_size'), MenuEntry('Create System Restore', 'auto_system_restore_create'), - MenuEntry('Backup Browsers', placeholder_function), + #MenuEntry('Backup Browsers', #TODO), MenuEntry('Backup Power Plans', 'auto_backup_power_plans'), MenuEntry('Backup Registry', 'auto_backup_registry'), ), 'Windows Repairs': ( - MenuEntry('Disable Windows Updates', placeholder_function), - MenuEntry('Reset Windows Updates', placeholder_function), + MenuEntry('Disable Windows Updates', 'auto_windows_updates_disable'), + MenuEntry('Reset Windows Updates', 'auto_windows_updates_reset'), MenuEntry('Reboot', placeholder_reboot), MenuEntry('CHKDSK', placeholder_function), MenuEntry('DISM RestoreHealth', 'auto_dism'), @@ -85,7 +85,7 @@ BASE_MENUS = { 'Manual Steps': ( MenuEntry('AdwCleaner', placeholder_function), MenuEntry('IO Bit Uninstaller', placeholder_function), - MenuEntry('Enable Windows Updates', placeholder_function), + MenuEntry('Enable Windows Updates', 'auto_windows_updates_enable'), ), }, 'Options': ( diff --git a/scripts/wk/io.py b/scripts/wk/io.py index f81d9a26..2cbbef46 100644 --- a/scripts/wk/io.py +++ b/scripts/wk/io.py @@ -192,5 +192,11 @@ def recursive_copy(source, dest, overwrite=False): raise FileExistsError(f'Refusing to delete file: {dest}') +def rename_item(path, new_path): + """Rename item, returns pathlib.Path.""" + path = pathlib.Path(path) + return path.rename(new_path) + + if __name__ == '__main__': print("This file is not meant to be called directly.") diff --git a/scripts/wk/repairs/win.py b/scripts/wk/repairs/win.py index eb080d95..206683ea 100644 --- a/scripts/wk/repairs/win.py +++ b/scripts/wk/repairs/win.py @@ -13,6 +13,7 @@ from subprocess import CalledProcessError, DEVNULL from wk.cfg.main import KIT_NAME_FULL from wk.exe import get_procs, run_program, popen_program, wait_for_procs +from wk.io import delete_folder, rename_item from wk.kit.tools import run_tool from wk.log import format_log_path, update_log_path from wk.os.win import ( @@ -516,6 +517,30 @@ def auto_system_restore_set_size(group, name): save_settings(group, name, result=result) +def auto_windows_updates_disable(group, name): + """Disable Windows Updates.""" + result = TRY_PRINT.run('Disable Windows Updates...', disable_windows_updates) + if result['Failed']: + # Reboot and try again? + reboot() + save_settings(group, name, result=result) + + +def auto_windows_updates_enable(group, name): + """Enable Windows Updates.""" + result = TRY_PRINT.run('Enable Windows Updates...', enable_windows_updates) + save_settings(group, name, result=result) + + +def auto_windows_updates_reset(group, name): + """Reset Windows Updates.""" + result = TRY_PRINT.run('Reset Windows Updates...', reset_windows_updates) + if result['Failed']: + # Reboot and try again? + reboot() + save_settings(group, name, result=result) + + # Misc Functions def set_backup_path(name, date=False): """Set backup path, returns pathlib.Path.""" @@ -551,8 +576,8 @@ def create_system_restore_point(): def disable_windows_updates(): """Disable and stop Windows Updates.""" - stop_service('wuauserv') disable_service('wuauserv') + stop_service('wuauserv') def enable_windows_updates(): @@ -601,6 +626,16 @@ def reboot(timeout=10): raise SystemExit +def reset_windows_updates(): + """Reset Windows Updates.""" + system_root = os.environ.get('SYSTEMROOT', 'C:/Windows') + rename_item( + f'{system_root}/SoftwareDistribution', + f'{system_root}/SoftwareDistribution.old', + ) + delete_folder(f'{system_root}/SoftwareDistribution.old', force=True) + + def run_chkdsk_offline(): """Set filesystem 'dirty bit' to force a CHKDSK during startup.""" cmd = ['fsutil', 'dirty', 'set', os.environ.get('SYSTEMDRIVE', 'C:')] From e17c96d4657a9c23519d1cb4568527d292a997fa Mon Sep 17 00:00:00 2001 From: 2Shirt <2xShirt@gmail.com> Date: Thu, 29 Apr 2021 21:08:50 -0600 Subject: [PATCH 26/43] Add auto_chkdsk() --- scripts/auto_repairs.py | 4 ++-- scripts/wk/repairs/win.py | 27 +++++++++++++++++++++++++++ 2 files changed, 29 insertions(+), 2 deletions(-) diff --git a/scripts/auto_repairs.py b/scripts/auto_repairs.py index aabb9ba6..1cfb8da5 100644 --- a/scripts/auto_repairs.py +++ b/scripts/auto_repairs.py @@ -29,7 +29,7 @@ class MenuEntry(): # Set details self.details = { 'Function': function, - 'Selected': True, + 'Selected': False, **kwargs, } @@ -64,7 +64,7 @@ BASE_MENUS = { MenuEntry('Disable Windows Updates', 'auto_windows_updates_disable'), MenuEntry('Reset Windows Updates', 'auto_windows_updates_reset'), MenuEntry('Reboot', placeholder_reboot), - MenuEntry('CHKDSK', placeholder_function), + MenuEntry('CHKDSK', 'auto_chkdsk'), MenuEntry('DISM RestoreHealth', 'auto_dism'), MenuEntry('SFC Scan', placeholder_function), MenuEntry('Fix File Associations', placeholder_function), diff --git a/scripts/wk/repairs/win.py b/scripts/wk/repairs/win.py index 206683ea..31a661fe 100644 --- a/scripts/wk/repairs/win.py +++ b/scripts/wk/repairs/win.py @@ -369,6 +369,8 @@ def save_settings(group, name, result=None, **kwargs): 'failed': result['Failed'], 'message': result['Message'], }) + if isinstance(result['Exception'], GenericWarning): + kwargs['warning'] = True # Write values to registry for value_name, data in kwargs.items(): @@ -458,6 +460,31 @@ def auto_backup_registry(group, name): save_settings(group, name, result=result) +def auto_chkdsk(group, name): + """Run CHKDSK repairs.""" + needs_reboot = False + system_disk = os.environ.get('SYSTEMDRIVE', 'C:') + result = TRY_PRINT.run(f'CHKDSK ({system_disk})...', run_chkdsk_online) + + # Run offline CHKDSK if required + if result['Failed'] and 'Repaired' not in result['Message']: + needs_reboot = True + result = TRY_PRINT.run( + f'Scheduling offline CHKDSK ({system_disk})...', + run_chkdsk_offline, + ) + if not result['Failed']: + # Successfully set dirty bit to force offline check + # Set result['Failed'] to True because we failed to repair online + result['Failed'] = True + result['Message'] = 'Scheduled offline repairs' + + # Done + save_settings(group, name, result=result) + if needs_reboot: + reboot() + + def auto_dism(group, name): """Run DISM repairs.""" needs_reboot = False From 7f40a52444097248feb3fe869ffacfd1ce5495e0 Mon Sep 17 00:00:00 2001 From: 2Shirt <2xShirt@gmail.com> Date: Fri, 30 Apr 2021 00:06:22 -0600 Subject: [PATCH 27/43] Add remaining Windows Repairs functions --- scripts/auto_repairs.py | 18 +++--- scripts/wk/cfg/tools.py | 1 + scripts/wk/repairs/win.py | 120 ++++++++++++++++++++++++++++++++++++-- 3 files changed, 123 insertions(+), 16 deletions(-) diff --git a/scripts/auto_repairs.py b/scripts/auto_repairs.py index 1cfb8da5..65cce403 100644 --- a/scripts/auto_repairs.py +++ b/scripts/auto_repairs.py @@ -63,24 +63,22 @@ BASE_MENUS = { 'Windows Repairs': ( MenuEntry('Disable Windows Updates', 'auto_windows_updates_disable'), MenuEntry('Reset Windows Updates', 'auto_windows_updates_reset'), - MenuEntry('Reboot', placeholder_reboot), + MenuEntry('Reboot', 'auto_reboot'), MenuEntry('CHKDSK', 'auto_chkdsk'), MenuEntry('DISM RestoreHealth', 'auto_dism'), - MenuEntry('SFC Scan', placeholder_function), - MenuEntry('Fix File Associations', placeholder_function), - MenuEntry('Clear Proxy Settings', placeholder_function), - MenuEntry('Disable Pending Renames', placeholder_function), - MenuEntry('Registry Repairs', placeholder_function), - MenuEntry('Repair Safe Mode', placeholder_function), - MenuEntry('Reset UAC', placeholder_function), - MenuEntry('Reset Windows Policies', placeholder_function), + MenuEntry('SFC Scan', 'auto_sfc'), + MenuEntry('Clear Proxy Settings', 'auto_reset_proxy'), + MenuEntry('Disable Pending Renames', 'auto_disable_pending_renames'), + MenuEntry('Registry Repairs', 'auto_repair_registry'), + MenuEntry('Reset UAC', 'auto_restore_uac_defaults'), + MenuEntry('Reset Windows Policies', 'auto_reset_windows_policies'), ), 'Malware Cleanup': ( MenuEntry('BleachBit', placeholder_function), MenuEntry('HitmanPro', placeholder_function), MenuEntry('KVRT', placeholder_function), MenuEntry('Windows Defender', placeholder_function), - MenuEntry('Reboot', placeholder_reboot), + MenuEntry('Reboot', 'auto_reboot'), ), 'Manual Steps': ( MenuEntry('AdwCleaner', placeholder_function), diff --git a/scripts/wk/cfg/tools.py b/scripts/wk/cfg/tools.py index e32d3c2e..d7eaa837 100644 --- a/scripts/wk/cfg/tools.py +++ b/scripts/wk/cfg/tools.py @@ -46,6 +46,7 @@ SOURCES = { 'ProduKey32': 'http://www.nirsoft.net/utils/produkey.zip', 'ProduKey64': 'http://www.nirsoft.net/utils/produkey-x64.zip', 'PuTTY': 'https://the.earth.li/~sgtatham/putty/latest/w32/putty.zip', + 'RegDelNull': 'https://download.sysinternals.com/files/Regdelnull.zip', 'RKill': 'https://download.bleepingcomputer.com/grinler/rkill.exe', 'Samsung Magician': 'https://s3.ap-northeast-2.amazonaws.com/global.semi.static/SAMSUNG_SSD_v5_3_0_181121/CD0C7CC1BE00525FAC4675B9E502899B41D5C3909ECE3AA2FB6B74A766B2A1EA/Samsung_Magician_Installer.zip', 'SDIO Themes': 'http://snappy-driver-installer.org/downloads/SDIO_Themes.zip', diff --git a/scripts/wk/repairs/win.py b/scripts/wk/repairs/win.py index 31a661fe..14e9b5c9 100644 --- a/scripts/wk/repairs/win.py +++ b/scripts/wk/repairs/win.py @@ -20,6 +20,7 @@ from wk.os.win import ( reg_delete_value, reg_read_value, reg_set_value, + reg_write_settings, disable_service, enable_service, stop_service, @@ -48,9 +49,22 @@ LOG = logging.getLogger(__name__) AUTO_REPAIR_DELAY_IN_SECONDS = 30 AUTO_REPAIR_KEY = fr'Software\{KIT_NAME_FULL}\Auto Repairs' CONEMU = 'ConEmuPID' in os.environ +GPUPDATE_SUCCESS_STRINGS = ( + 'Computer Policy update has completed successfully.', + 'User Policy update has completed successfully.', + ) OS_VERSION = float(platform.win32_ver()[0]) -WIDTH = 50 +REG_UAC_DEFAULT_SETTINGS = { + 'HKLM': { + r'Software\Microsoft\Windows\CurrentVersion\Policies\System': ( + ('ConsentPromptBehaviorAdmin', 5, 'DWORD'), + ('ConsentPromptBehaviorUser', 3, 'DWORD'), + ('EnableLUA', 1, 'DWORD'), + ), + }, + } SYSTEMDRIVE = os.environ.get('SYSTEMDRIVE') +WIDTH = 50 TRY_PRINT = TryAndPrint() TRY_PRINT.width = WIDTH TRY_PRINT.verbose = True @@ -290,6 +304,9 @@ def run_auto_repairs(base_menus): # Run repairs clear_screen() + print_standard(title) + print('') + print_info('Running repairs') for group, menu in menus.items(): if group in ('Main', 'Options'): continue @@ -485,6 +502,14 @@ def auto_chkdsk(group, name): reboot() +def auto_disable_pending_renames(group, name): + """Disable pending renames.""" + result = TRY_PRINT.run( + 'Disabling pending renames...', disable_pending_renames, + ) + save_settings(group, name, result=result) + + def auto_dism(group, name): """Run DISM repairs.""" needs_reboot = False @@ -520,6 +545,47 @@ def auto_enable_regback(group, name): save_settings(group, name, result=result) +def auto_reboot(group, name): + """Reboot the system.""" + save_settings(group, name, done=True, failed=False, message='DONE') + print('') + reboot(30) + + +def auto_repair_registry(group, name): + """Delete registry keys with embedded null characters.""" + result = TRY_PRINT.run( + 'Running Registry repairs...', delete_registry_null_keys, + ) + save_settings(group, name, result=result) + + +def auto_reset_proxy(group, name): + """Reset proxy settings.""" + result = TRY_PRINT.run('Clearing proxy settings...', reset_proxy) + save_settings(group, name, result=result) + + +def auto_reset_windows_policies(group, name): + """Reset Windows policies to defaults.""" + result = TRY_PRINT.run( + 'Resetting Windows policies...', reset_windows_policies, + ) + save_settings(group, name, result=result) + + +def auto_restore_uac_defaults(group, name): + """Restore UAC default settings.""" + result = TRY_PRINT.run('Restoring UAC defaults...', restore_uac_defaults) + save_settings(group, name, result=result) + + +def auto_sfc(group, name): + """Run SFC repairs.""" + result = TRY_PRINT.run('SFC Scan...', run_sfc_scan) + save_settings(group, name, result=result) + + def auto_system_restore_create(group, name): """Create a System Restore point.""" result = TRY_PRINT.run( @@ -585,6 +651,11 @@ def backup_registry(): run_tool('Erunt', 'ERUNT', backup_path, 'sysreg', 'curuser', 'otherusers') +def delete_registry_null_keys(): + """Delete registry keys with embedded null characters.""" + run_tool('RegDelNull', 'RegDelNull', '-s', '-y', cbin=True) + + # OS Built-in Functions def create_system_restore_point(): """Create System Restore point.""" @@ -601,6 +672,14 @@ def create_system_restore_point(): raise GenericWarning('Skipped, a restore point was created too recently') +def disable_pending_renames(): + """Disable pending renames.""" + reg_set_value( + 'HKLM', r'SYSTEM\CurrentControlSet\Control\Session Manager', + 'PendingFileRenameOperations', [], 'MULTI_SZ', + ) + + def disable_windows_updates(): """Disable and stop Windows Updates.""" disable_service('wuauserv') @@ -653,14 +732,43 @@ def reboot(timeout=10): raise SystemExit +def reset_proxy(): + """Reset WinHTTP proxy settings.""" + cmd = ['netsh', 'winhttp', 'reset', 'proxy'] + proc = run_program(cmd, check=False) + + # Check result + if 'Direct access (no proxy server)' not in proc.stdout: + raise GenericError('Failed to reset proxy settings.') + + +def reset_windows_policies(): + """Reset Windows policies to defaults.""" + cmd = ['gpupdate', '/force'] + proc = run_program(cmd, check=False) + + # Check result + if not all(_s in proc.stdout for _s in GPUPDATE_SUCCESS_STRINGS): + raise GenericError('Failed to reset one or more policies.') + + def reset_windows_updates(): """Reset Windows Updates.""" system_root = os.environ.get('SYSTEMROOT', 'C:/Windows') - rename_item( - f'{system_root}/SoftwareDistribution', - f'{system_root}/SoftwareDistribution.old', - ) - delete_folder(f'{system_root}/SoftwareDistribution.old', force=True) + try: + rename_item( + f'{system_root}/SoftwareDistribution', + f'{system_root}/SoftwareDistribution.old', + ) + delete_folder(f'{system_root}/SoftwareDistribution.old', force=True) + except FileNotFoundError: + # Ignore + pass + + +def restore_uac_defaults(): + """Restore UAC default settings.""" + reg_write_settings(REG_UAC_DEFAULT_SETTINGS) def run_chkdsk_offline(): From 447e93ca3a214c1f7adcf414bf0fdc653e72d2aa Mon Sep 17 00:00:00 2001 From: 2Shirt <2xShirt@gmail.com> Date: Fri, 30 Apr 2021 00:19:33 -0600 Subject: [PATCH 28/43] Resume in ConEmu if started in ConEmu --- scripts/wk/repairs/win.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/scripts/wk/repairs/win.py b/scripts/wk/repairs/win.py index 14e9b5c9..110b516d 100644 --- a/scripts/wk/repairs/win.py +++ b/scripts/wk/repairs/win.py @@ -14,7 +14,7 @@ from subprocess import CalledProcessError, DEVNULL from wk.cfg.main import KIT_NAME_FULL from wk.exe import get_procs, run_program, popen_program, wait_for_procs from wk.io import delete_folder, rename_item -from wk.kit.tools import run_tool +from wk.kit.tools import get_tool_path, run_tool from wk.log import format_log_path, update_log_path from wk.os.win import ( reg_delete_value, @@ -233,6 +233,9 @@ def init_session(options): '/rl', 'HIGHEST', '/tr', fr'C:\Windows\System32\cmd.exe "/C {sys.executable} {sys.argv[0]}"', ] + if CONEMU: + exe_path = get_tool_path('ConEmu', 'ConEmu') + cmd[-1] = f'{exe_path} -run {sys.executable} {sys.argv[0]}' run_program(cmd) # One-time tasks From e488b2c89c5af4d3aef4f005f07a26448c5e43bd Mon Sep 17 00:00:00 2001 From: 2Shirt <2xShirt@gmail.com> Date: Fri, 30 Apr 2021 02:08:50 -0600 Subject: [PATCH 29/43] Avoid crash when downloading tools --- scripts/wk/kit/tools.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/scripts/wk/kit/tools.py b/scripts/wk/kit/tools.py index 4f23b9a9..93311098 100644 --- a/scripts/wk/kit/tools.py +++ b/scripts/wk/kit/tools.py @@ -27,6 +27,8 @@ CACHED_DIRS = {} def download_file(out_path, source_url, as_new=False, overwrite=False): """Download a file using requests, returns pathlib.Path.""" out_path = pathlib.Path(out_path).resolve() + if as_new: + out_path = out_path.with_suffix(f'{out_path.suffix}.new') cursor_left = '\u001B[14D' print(f'Downloading...{cursor_left}', end='', flush=True) @@ -36,8 +38,6 @@ def download_file(out_path, source_url, as_new=False, overwrite=False): # Create destination directory out_path.parent.mkdir(parents=True, exist_ok=True) - if as_new: - out_path = out_path.with_suffix(f'{out_path.suffix}.new') # Request download response = requests.get(source_url, stream=True) From 06b0ff81e1cfb2eae05329bedea5015488d38c7f Mon Sep 17 00:00:00 2001 From: 2Shirt <2xShirt@gmail.com> Date: Fri, 30 Apr 2021 02:09:43 -0600 Subject: [PATCH 30/43] Add Auto Repairs init functions --- scripts/auto_repairs.py | 3 +- scripts/wk/kit/tools.py | 4 +- scripts/wk/repairs/win.py | 118 +++++++++++++++++++++++++++++--------- 3 files changed, 96 insertions(+), 29 deletions(-) diff --git a/scripts/auto_repairs.py b/scripts/auto_repairs.py index 65cce403..d9979573 100644 --- a/scripts/auto_repairs.py +++ b/scripts/auto_repairs.py @@ -88,7 +88,8 @@ BASE_MENUS = { }, 'Options': ( MenuEntry('Kill Explorer'), - MenuEntry('Run RKill at startup'), + MenuEntry('Run RKill'), + MenuEntry('Run TDSSKiller (once)'), MenuEntry('Use Autologon'), ), 'Actions': ( diff --git a/scripts/wk/kit/tools.py b/scripts/wk/kit/tools.py index 93311098..cb145de1 100644 --- a/scripts/wk/kit/tools.py +++ b/scripts/wk/kit/tools.py @@ -100,7 +100,7 @@ def find_kit_dir(name=None): return cur_path -def get_tool_path(folder, name): +def get_tool_path(folder, name, check=True): """Get tool path, returns pathlib.Path""" bin_dir = find_kit_dir('.bin') @@ -110,7 +110,7 @@ def get_tool_path(folder, name): tool_path = tool_path.with_stem(name) # Missing? - if not tool_path.exists(): + if check and not tool_path.exists(): raise FileNotFoundError(f'Failed to find tool, {folder=}, {name=}') # Done diff --git a/scripts/wk/repairs/win.py b/scripts/wk/repairs/win.py index 110b516d..b6f17df5 100644 --- a/scripts/wk/repairs/win.py +++ b/scripts/wk/repairs/win.py @@ -48,11 +48,17 @@ from wk.std import ( LOG = logging.getLogger(__name__) AUTO_REPAIR_DELAY_IN_SECONDS = 30 AUTO_REPAIR_KEY = fr'Software\{KIT_NAME_FULL}\Auto Repairs' -CONEMU = 'ConEmuPID' in os.environ +CONEMU_EXE = get_tool_path('ConEmu', 'ConEmu', check=False) GPUPDATE_SUCCESS_STRINGS = ( 'Computer Policy update has completed successfully.', 'User Policy update has completed successfully.', ) +IN_CONEMU = 'ConEmuPID' in os.environ +PROGRAMFILES_32 = os.environ.get( + 'PROGRAMFILES(X86)', os.environ.get( + 'PROGRAMFILES', r'C:\Program Files (x86)', + ), + ) OS_VERSION = float(platform.win32_ver()[0]) REG_UAC_DEFAULT_SETTINGS = { 'HKLM': { @@ -63,6 +69,16 @@ REG_UAC_DEFAULT_SETTINGS = { ), }, } +RKILL_WHITELIST = ( + CONEMU_EXE, + fr'{PROGRAMFILES_32}\TeamViewer\TeamViewer.exe', + fr'{PROGRAMFILES_32}\TeamViewer\TeamViewer_Desktop.exe', + fr'{PROGRAMFILES_32}\TeamViewer\TeamViewer_Note.exe', + fr'{PROGRAMFILES_32}\TeamViewer\TeamViewer_Service.exe', + fr'{PROGRAMFILES_32}\TeamViewer\tv_w32.exe', + fr'{PROGRAMFILES_32}\TeamViewer\tv_x64.exe', + sys.executable, + ) SYSTEMDRIVE = os.environ.get('SYSTEMDRIVE') WIDTH = 50 TRY_PRINT = TryAndPrint() @@ -126,7 +142,6 @@ def build_menus(base_menus, title): def end_session(): """End Auto Repairs session.""" - print_info('Ending repair session') auto_admin_logon = '0' # Delete Auto Repairs keys @@ -215,14 +230,17 @@ def init_run(options): """Initialize Auto Repairs Run.""" if options['Kill Explorer']['Selected']: atexit.register(start_explorer) - kill_explorer() - # TODO: Sync Clock - # TODO: RKill + TRY_PRINT.run('Killing Explorer...', kill_explorer, msg_good='DONE') + TRY_PRINT.run( + 'Syncing Clock...', run_tool, 'Neutron', 'Neutron', + cbin=True, msg_good='DONE', + ) + if options['Run RKill']['Selected']: + TRY_PRINT.run('Running RKill...', run_rkill, msg_good='DONE') def init_session(options): """Initialize Auto Repairs session.""" - print_info('Starting repair session') reg_set_value('HKCU', AUTO_REPAIR_KEY, 'SessionStarted', 1, 'DWORD') # Create logon task for Auto Repairs @@ -233,16 +251,21 @@ def init_session(options): '/rl', 'HIGHEST', '/tr', fr'C:\Windows\System32\cmd.exe "/C {sys.executable} {sys.argv[0]}"', ] - if CONEMU: - exe_path = get_tool_path('ConEmu', 'ConEmu') - cmd[-1] = f'{exe_path} -run {sys.executable} {sys.argv[0]}' + if IN_CONEMU: + cmd[-1] = f'{CONEMU_EXE} -run {sys.executable} {sys.argv[0]}' run_program(cmd) # One-time tasks if options['Use Autologon']['Selected']: - run_tool('Sysinternals', 'Autologon') - # TODO: TDSSKiller? - # TODO: Re-enable reboot() + TRY_PRINT.run( + 'Running Autologon...', run_tool, + 'Autologon', 'Autologon', + cbin=True, msg_good='DONE', + ) + if options['Run TDSSKiller (once)']['Selected']: + TRY_PRINT.run('Running TDSSKiller...', run_tdsskiller, msg_good='DONE') + print('') + reboot(30) def is_session_started(): @@ -300,16 +323,17 @@ def run_auto_repairs(base_menus): session_started = is_session_started() # Start or resume repairs - save_selection_settings(menus) - init_run(menus['Options'].options) - if not session_started: - init_session(menus['Options'].options) - - # Run repairs clear_screen() print_standard(title) print('') + save_selection_settings(menus) + print_info('Initializing...') + init_run(menus['Options'].options) + if not session_started: + init_session(menus['Options'].options) print_info('Running repairs') + + # Run repairs for group, menu in menus.items(): if group in ('Main', 'Options'): continue @@ -640,10 +664,15 @@ def auto_windows_updates_reset(group, name): # Misc Functions def set_backup_path(name, date=False): """Set backup path, returns pathlib.Path.""" - backup_path = format_log_path(log_name=f'../Backups/{name}').with_suffix('') + return get_local_storage_path('Backups', name, date) + + +def get_local_storage_path(folder, name, date=False): + """Get path for local storage, returns pathlib.Path.""" + local_path = format_log_path(log_name=f'../{folder}/{name}').with_suffix('') if date: - backup_path = backup_path.joinpath(time.strftime('%Y-%m-%d')) - return backup_path.resolve() + local_path = local_path.joinpath(time.strftime('%Y-%m-%d')) + return local_path.resolve() # Tool Functions @@ -659,6 +688,40 @@ def delete_registry_null_keys(): run_tool('RegDelNull', 'RegDelNull', '-s', '-y', cbin=True) +def run_rkill(): + """Run RKill scan.""" + log_path = format_log_path(log_name='RKill', timestamp=True, tool=True) + log_path.parent.mkdir(parents=True, exist_ok=True) + whitelist_path = log_path.with_suffix('.wl') + whitelist_path.write_text('\n'.join(map(str, RKILL_WHITELIST))) + cmd_args = ( + '-l', log_path, + '-w', whitelist_path, + '-s', + ) + run_tool('RKill', 'RKill', *cmd_args, download=True) + + +def run_tdsskiller(): + """Run TDSSKiller scan.""" + log_path = format_log_path(log_name='TDSSKiller', timestamp=True, tool=True) + log_path.parent.mkdir(parents=True, exist_ok=True) + quarantine_path = get_local_storage_path( + 'Quarantine', 'TDSSKiller', date=True, + ) + quarantine_path.mkdir(parents=True, exist_ok=True) + cmd_args = ( + '-accepteula', + '-accepteulaksn', + '-l', log_path, + '-qpath', quarantine_path, + '-qsus', + '-dcexact', + '-silent', + ) + run_tool('TDSSKiller', 'TDSSKiller', *cmd_args, download=True) + + # OS Built-in Functions def create_system_restore_point(): """Create System Restore point.""" @@ -792,7 +855,7 @@ def run_chkdsk_online(): cmd = ['CHKDSK', os.environ.get('SYSTEMDRIVE', 'C:')] if OS_VERSION >= 8: cmd.extend(['/scan', '/perf']) - if CONEMU: + if IN_CONEMU: cmd.extend(['-new_console:nb', '-new_console:s33V']) retried = False @@ -826,7 +889,7 @@ def run_chkdsk_online(): def run_dism(repair=True): """Run DISM to either scan or repair component store health.""" - conemu_args = ['-new_console:nb', '-new_console:s33V'] if CONEMU else [] + conemu_args = ['-new_console:nb', '-new_console:s33V'] if IN_CONEMU else [] # Bail early if OS_VERSION < 8: @@ -834,7 +897,8 @@ def run_dism(repair=True): # Run (repair) scan log_path = format_log_path( - log_name=f'DISM_{"Restore" if repair else "Scan"}Health', tool=True, + log_name=f'DISM_{"Restore" if repair else "Scan"}Health', + timestamp=True, tool=True, ) log_path.parent.mkdir(parents=True, exist_ok=True) cmd = [ @@ -847,7 +911,9 @@ def run_dism(repair=True): wait_for_procs('dism.exe') # Run check health - log_path = format_log_path(log_name='DISM_CheckHealth.log', tool=True) + log_path = format_log_path( + log_name='DISM_CheckHealth.log', timestamp=True, tool=True, + ) cmd = [ 'DISM', '/Online', '/Cleanup-Image', '/CheckHealth', @@ -863,7 +929,7 @@ def run_dism(repair=True): def run_sfc_scan(): """Run SFC and save results.""" cmd = ['sfc', '/scannow'] - log_path = format_log_path(log_name='SFC', tool=True) + log_path = format_log_path(log_name='SFC', timestamp=True, tool=True) err_path = log_path.with_suffix('.err') # Run SFC From 77920db5b5f51e323c809050fbbb78ff30b40419 Mon Sep 17 00:00:00 2001 From: 2Shirt <2xShirt@gmail.com> Date: Fri, 30 Apr 2021 02:58:02 -0600 Subject: [PATCH 31/43] Add missing default UAC setting --- scripts/wk/repairs/win.py | 1 + 1 file changed, 1 insertion(+) diff --git a/scripts/wk/repairs/win.py b/scripts/wk/repairs/win.py index b6f17df5..48e66f06 100644 --- a/scripts/wk/repairs/win.py +++ b/scripts/wk/repairs/win.py @@ -66,6 +66,7 @@ REG_UAC_DEFAULT_SETTINGS = { ('ConsentPromptBehaviorAdmin', 5, 'DWORD'), ('ConsentPromptBehaviorUser', 3, 'DWORD'), ('EnableLUA', 1, 'DWORD'), + ('PromptOnSecureDesktop', 1, 'DWORD'), ), }, } From bdbed4622e7430c2e777b0097c506b5174f7ca78 Mon Sep 17 00:00:00 2001 From: 2Shirt <2xShirt@gmail.com> Date: Fri, 30 Apr 2021 02:59:19 -0600 Subject: [PATCH 32/43] Add BleachBit sections --- scripts/auto_repairs.py | 2 +- scripts/wk/repairs/win.py | 64 +++++++++++++++++++++++++++++++++++++-- 2 files changed, 62 insertions(+), 4 deletions(-) diff --git a/scripts/auto_repairs.py b/scripts/auto_repairs.py index d9979573..d1b13be6 100644 --- a/scripts/auto_repairs.py +++ b/scripts/auto_repairs.py @@ -74,7 +74,7 @@ BASE_MENUS = { MenuEntry('Reset Windows Policies', 'auto_reset_windows_policies'), ), 'Malware Cleanup': ( - MenuEntry('BleachBit', placeholder_function), + MenuEntry('BleachBit', 'auto_bleachbit'), MenuEntry('HitmanPro', placeholder_function), MenuEntry('KVRT', placeholder_function), MenuEntry('Windows Defender', placeholder_function), diff --git a/scripts/wk/repairs/win.py b/scripts/wk/repairs/win.py index 48e66f06..b9dac293 100644 --- a/scripts/wk/repairs/win.py +++ b/scripts/wk/repairs/win.py @@ -48,6 +48,42 @@ from wk.std import ( LOG = logging.getLogger(__name__) AUTO_REPAIR_DELAY_IN_SECONDS = 30 AUTO_REPAIR_KEY = fr'Software\{KIT_NAME_FULL}\Auto Repairs' +BLEACH_BIT_CLEANERS = ( + # Applications + 'adobe_reader.cache', + 'adobe_reader.tmp', + 'flash.cache', + 'gimp.tmp', + 'hippo_opensim_viewer.cache', + 'java.cache', + 'miro.cache', + 'openofficeorg.cache', + 'pidgin.cache', + 'secondlife_viewer.Cache', + 'thunderbird.cache', + 'vuze.cache', + 'yahoo_messenger.cache', + # Browsers + 'chromium.cache', + 'chromium.session', + 'firefox.cache', + 'firefox.session_restore', + 'google_chrome.cache', + 'google_chrome.session', + 'google_earth.temporary_files', + 'opera.cache', + 'opera.session', + 'safari.cache', + 'seamonkey.cache', + # System + 'system.clipboard', + 'system.tmp', + 'winapp2_windows.jump_lists', + 'winapp2_windows.ms_search', + 'windows_explorer.run', + 'windows_explorer.search_history', + 'windows_explorer.thumbnails', + ) CONEMU_EXE = get_tool_path('ConEmu', 'ConEmu', check=False) GPUPDATE_SUCCESS_STRINGS = ( 'Computer Policy update has completed successfully.', @@ -505,6 +541,14 @@ def auto_backup_registry(group, name): save_settings(group, name, result=result) +def auto_bleachbit(group, name): + """Run BleachBit to clean files.""" + result = TRY_PRINT.run( + 'BleachBit...', run_bleachbit, BLEACH_BIT_CLEANERS, msg_good='DONE', + ) + save_settings(group, name, result=result) + + def auto_chkdsk(group, name): """Run CHKDSK repairs.""" needs_reboot = False @@ -665,10 +709,10 @@ def auto_windows_updates_reset(group, name): # Misc Functions def set_backup_path(name, date=False): """Set backup path, returns pathlib.Path.""" - return get_local_storage_path('Backups', name, date) + return set_local_storage_path('Backups', name, date) -def get_local_storage_path(folder, name, date=False): +def set_local_storage_path(folder, name, date=False): """Get path for local storage, returns pathlib.Path.""" local_path = format_log_path(log_name=f'../{folder}/{name}').with_suffix('') if date: @@ -689,6 +733,20 @@ def delete_registry_null_keys(): run_tool('RegDelNull', 'RegDelNull', '-s', '-y', cbin=True) +def run_bleachbit(cleaners, preview=True): + """Run BleachBit to either clean or preview files.""" + cmd_args = ( + '--preview' if preview else '--clean', + *cleaners, + ) + log_path = format_log_path(log_name='BleachBit', timestamp=True, tool=True) + proc = run_tool('BleachBit', 'bleachbit_console', *cmd_args, cbin=True) + + # Save logs + log_path.write_text(proc.stdout) + log_path.with_suffix('.err').write_text(proc.stderr) + + def run_rkill(): """Run RKill scan.""" log_path = format_log_path(log_name='RKill', timestamp=True, tool=True) @@ -707,7 +765,7 @@ def run_tdsskiller(): """Run TDSSKiller scan.""" log_path = format_log_path(log_name='TDSSKiller', timestamp=True, tool=True) log_path.parent.mkdir(parents=True, exist_ok=True) - quarantine_path = get_local_storage_path( + quarantine_path = set_local_storage_path( 'Quarantine', 'TDSSKiller', date=True, ) quarantine_path.mkdir(parents=True, exist_ok=True) From f706a48f60ec62e98a97968015f8e6be64a3d1ec Mon Sep 17 00:00:00 2001 From: 2Shirt <2xShirt@gmail.com> Date: Fri, 30 Apr 2021 03:26:45 -0600 Subject: [PATCH 33/43] Add HMP sections --- scripts/auto_repairs.py | 2 +- scripts/wk/cfg/tools.py | 2 +- scripts/wk/repairs/win.py | 22 +++++++++++++++++++++- 3 files changed, 23 insertions(+), 3 deletions(-) diff --git a/scripts/auto_repairs.py b/scripts/auto_repairs.py index d1b13be6..ad229c3a 100644 --- a/scripts/auto_repairs.py +++ b/scripts/auto_repairs.py @@ -75,7 +75,7 @@ BASE_MENUS = { ), 'Malware Cleanup': ( MenuEntry('BleachBit', 'auto_bleachbit'), - MenuEntry('HitmanPro', placeholder_function), + MenuEntry('HitmanPro', 'auto_hitmanpro'), MenuEntry('KVRT', placeholder_function), MenuEntry('Windows Defender', placeholder_function), MenuEntry('Reboot', 'auto_reboot'), diff --git a/scripts/wk/cfg/tools.py b/scripts/wk/cfg/tools.py index d7eaa837..27e56796 100644 --- a/scripts/wk/cfg/tools.py +++ b/scripts/wk/cfg/tools.py @@ -30,7 +30,7 @@ SOURCES = { 'FastCopy': 'https://ftp.vector.co.jp/73/10/2323/FastCopy392_installer.exe', 'FurMark': 'https://geeks3d.com/dl/get/569', 'Firefox uBO': 'https://addons.mozilla.org/firefox/downloads/file/3740966/ublock_origin-1.34.0-an+fx.xpi', - 'HitmanPro32': 'https://dl.surfright.nl/HitmanPro.exe', + 'HitmanPro': 'https://dl.surfright.nl/HitmanPro.exe', 'HitmanPro64': 'https://dl.surfright.nl/HitmanPro_x64.exe', 'HWiNFO': 'https://files1.majorgeeks.com/c8a055180587599139f8f454712dcc618cd1740e/systeminfo/hwi_702.zip', 'Intel SSD Toolbox': r'https://downloadmirror.intel.com/28593/eng/Intel%20SSD%20Toolbox%20-%20v3.5.9.exe', diff --git a/scripts/wk/repairs/win.py b/scripts/wk/repairs/win.py index b9dac293..64422845 100644 --- a/scripts/wk/repairs/win.py +++ b/scripts/wk/repairs/win.py @@ -1,4 +1,5 @@ """WizardKit: Repairs - Windows""" +# pylint: disable=too-many-lines # vim: sts=2 sw=2 ts=2 import atexit @@ -14,7 +15,7 @@ from subprocess import CalledProcessError, DEVNULL from wk.cfg.main import KIT_NAME_FULL from wk.exe import get_procs, run_program, popen_program, wait_for_procs from wk.io import delete_folder, rename_item -from wk.kit.tools import get_tool_path, run_tool +from wk.kit.tools import ARCH, get_tool_path, run_tool from wk.log import format_log_path, update_log_path from wk.os.win import ( reg_delete_value, @@ -617,6 +618,12 @@ def auto_enable_regback(group, name): save_settings(group, name, result=result) +def auto_hitmanpro(group, name): + """Run HitmanPro scan.""" + result = TRY_PRINT.run('HitmanPro...', run_hitmanpro, msg_good='DONE') + save_settings(group, name, result=result) + + def auto_reboot(group, name): """Reboot the system.""" save_settings(group, name, done=True, failed=False, message='DONE') @@ -740,6 +747,7 @@ def run_bleachbit(cleaners, preview=True): *cleaners, ) log_path = format_log_path(log_name='BleachBit', timestamp=True, tool=True) + log_path.parent.mkdir(parents=True, exist_ok=True) proc = run_tool('BleachBit', 'bleachbit_console', *cmd_args, cbin=True) # Save logs @@ -747,6 +755,18 @@ def run_bleachbit(cleaners, preview=True): log_path.with_suffix('.err').write_text(proc.stderr) +def run_hitmanpro(): + """Run HitmanPro scan.""" + log_path = format_log_path(log_name='HitmanPro', timestamp=True, tool=True) + log_path = log_path.with_suffix('.xml') + log_path.parent.mkdir(parents=True, exist_ok=True) + cmd_args = ['/scanonly', f'/log={log_path}'] + run_tool( + 'HitmanPro', f'HitmanPro{"64" if ARCH=="64" else ""}', + *cmd_args, download=True, + ) + + def run_rkill(): """Run RKill scan.""" log_path = format_log_path(log_name='RKill', timestamp=True, tool=True) From cf8b600dd5d0c1c51610850cabe99fe1b10af5cb Mon Sep 17 00:00:00 2001 From: 2Shirt <2xShirt@gmail.com> Date: Sat, 1 May 2021 17:00:48 -0600 Subject: [PATCH 34/43] Drop extraneous SYSTEMDRIVE lookups --- scripts/wk/repairs/win.py | 32 ++++++++++++++++++-------------- 1 file changed, 18 insertions(+), 14 deletions(-) diff --git a/scripts/wk/repairs/win.py b/scripts/wk/repairs/win.py index 64422845..1d217430 100644 --- a/scripts/wk/repairs/win.py +++ b/scripts/wk/repairs/win.py @@ -10,14 +10,19 @@ import re import sys import time -from subprocess import CalledProcessError, DEVNULL +from subprocess import CalledProcessError, DEVNULL -from wk.cfg.main import KIT_NAME_FULL -from wk.exe import get_procs, run_program, popen_program, wait_for_procs -from wk.io import delete_folder, rename_item -from wk.kit.tools import ARCH, get_tool_path, run_tool -from wk.log import format_log_path, update_log_path -from wk.os.win import ( +from wk.cfg.main import KIT_NAME_FULL +from wk.exe import ( + get_procs, + run_program, + popen_program, + wait_for_procs, + ) +from wk.io import delete_folder, rename_item +from wk.kit.tools import ARCH, get_tool_path, run_tool +from wk.log import format_log_path, update_log_path +from wk.os.win import ( reg_delete_value, reg_read_value, reg_set_value, @@ -26,7 +31,7 @@ from wk.os.win import ( enable_service, stop_service, ) -from wk.std import ( +from wk.std import ( GenericError, GenericWarning, Menu, @@ -117,7 +122,7 @@ RKILL_WHITELIST = ( fr'{PROGRAMFILES_32}\TeamViewer\tv_x64.exe', sys.executable, ) -SYSTEMDRIVE = os.environ.get('SYSTEMDRIVE') +SYSTEMDRIVE = os.environ.get('SYSTEMDRIVE', 'C:') WIDTH = 50 TRY_PRINT = TryAndPrint() TRY_PRINT.width = WIDTH @@ -553,14 +558,13 @@ def auto_bleachbit(group, name): def auto_chkdsk(group, name): """Run CHKDSK repairs.""" needs_reboot = False - system_disk = os.environ.get('SYSTEMDRIVE', 'C:') - result = TRY_PRINT.run(f'CHKDSK ({system_disk})...', run_chkdsk_online) + result = TRY_PRINT.run(f'CHKDSK ({SYSTEMDRIVE})...', run_chkdsk_online) # Run offline CHKDSK if required if result['Failed'] and 'Repaired' not in result['Message']: needs_reboot = True result = TRY_PRINT.run( - f'Scheduling offline CHKDSK ({system_disk})...', + f'Scheduling offline CHKDSK ({SYSTEMDRIVE})...', run_chkdsk_offline, ) if not result['Failed']: @@ -918,7 +922,7 @@ def restore_uac_defaults(): def run_chkdsk_offline(): """Set filesystem 'dirty bit' to force a CHKDSK during startup.""" - cmd = ['fsutil', 'dirty', 'set', os.environ.get('SYSTEMDRIVE', 'C:')] + cmd = ['fsutil', 'dirty', 'set', SYSTEMDRIVE] proc = run_program(cmd, check=False) # Check result @@ -931,7 +935,7 @@ def run_chkdsk_online(): NOTE: If run on Windows 8+ online repairs are attempted. """ - cmd = ['CHKDSK', os.environ.get('SYSTEMDRIVE', 'C:')] + cmd = ['CHKDSK', SYSTEMDRIVE] if OS_VERSION >= 8: cmd.extend(['/scan', '/perf']) if IN_CONEMU: From 03000662fe6b683b0d1271107bbc47782464278c Mon Sep 17 00:00:00 2001 From: 2Shirt <2xShirt@gmail.com> Date: Sat, 1 May 2021 17:12:11 -0600 Subject: [PATCH 35/43] Add KVRT sections --- scripts/auto_repairs.py | 2 +- scripts/wk/repairs/win.py | 26 ++++++++++++++++++++++++++ 2 files changed, 27 insertions(+), 1 deletion(-) diff --git a/scripts/auto_repairs.py b/scripts/auto_repairs.py index ad229c3a..d199d918 100644 --- a/scripts/auto_repairs.py +++ b/scripts/auto_repairs.py @@ -76,7 +76,7 @@ BASE_MENUS = { 'Malware Cleanup': ( MenuEntry('BleachBit', 'auto_bleachbit'), MenuEntry('HitmanPro', 'auto_hitmanpro'), - MenuEntry('KVRT', placeholder_function), + MenuEntry('KVRT', 'auto_kvrt'), MenuEntry('Windows Defender', placeholder_function), MenuEntry('Reboot', 'auto_reboot'), ), diff --git a/scripts/wk/repairs/win.py b/scripts/wk/repairs/win.py index 1d217430..d77d19d1 100644 --- a/scripts/wk/repairs/win.py +++ b/scripts/wk/repairs/win.py @@ -628,6 +628,12 @@ def auto_hitmanpro(group, name): save_settings(group, name, result=result) +def auto_kvrt(group, name): + """Run KVRT scan.""" + result = TRY_PRINT.run('KVRT...', run_kvrt, msg_good='DONE') + save_settings(group, name, result=result) + + def auto_reboot(group, name): """Reboot the system.""" save_settings(group, name, done=True, failed=False, message='DONE') @@ -771,6 +777,26 @@ def run_hitmanpro(): ) +def run_kvrt(): + """Run KVRT scan.""" + log_path = format_log_path(log_name='KVRT', timestamp=True, tool=True) + log_path.parent.mkdir(parents=True, exist_ok=True) + quarantine_path = set_local_storage_path( + 'Quarantine', 'KVRT', date=True, + ) + quarantine_path.mkdir(parents=True, exist_ok=True) + cmd_args = ( + '-accepteula', + '-d', quarantine_path, + '-dontencrypt', '-fixednames', + '-processlevel', '1', + '-custom', SYSTEMDRIVE, + '-silent', '-adinsilent', + ) + proc = run_tool('KVRT', 'KVRT', *cmd_args, download=True) + log_path.write_text(proc.stdout) + + def run_rkill(): """Run RKill scan.""" log_path = format_log_path(log_name='RKill', timestamp=True, tool=True) From 04b2c1c9d9839e9a32742a89ac048d06849877fc Mon Sep 17 00:00:00 2001 From: 2Shirt <2xShirt@gmail.com> Date: Sat, 1 May 2021 19:14:32 -0600 Subject: [PATCH 36/43] Add Microsoft Defender sections --- scripts/auto_repairs.py | 2 +- scripts/wk/repairs/win.py | 49 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 50 insertions(+), 1 deletion(-) diff --git a/scripts/auto_repairs.py b/scripts/auto_repairs.py index d199d918..381ea729 100644 --- a/scripts/auto_repairs.py +++ b/scripts/auto_repairs.py @@ -77,7 +77,7 @@ BASE_MENUS = { MenuEntry('BleachBit', 'auto_bleachbit'), MenuEntry('HitmanPro', 'auto_hitmanpro'), MenuEntry('KVRT', 'auto_kvrt'), - MenuEntry('Windows Defender', placeholder_function), + MenuEntry('Windows Defender', 'auto_microsoft_defender'), MenuEntry('Reboot', 'auto_reboot'), ), 'Manual Steps': ( diff --git a/scripts/wk/repairs/win.py b/scripts/wk/repairs/win.py index d77d19d1..b054eb85 100644 --- a/scripts/wk/repairs/win.py +++ b/scripts/wk/repairs/win.py @@ -634,6 +634,14 @@ def auto_kvrt(group, name): save_settings(group, name, result=result) +def auto_microsoft_defender(group, name): + """Run Microsoft Defender scan.""" + result = TRY_PRINT.run( + 'Microsoft Defender...', run_microsoft_defender, msg_good='DONE', + ) + save_settings(group, name, result=result) + + def auto_reboot(group, name): """Reboot the system.""" save_settings(group, name, done=True, failed=False, message='DONE') @@ -797,6 +805,47 @@ def run_kvrt(): log_path.write_text(proc.stdout) +def run_microsoft_defender(full=True): + """Run Microsoft Defender scan.""" + reg_key = r'Software\Microsoft\Windows Defender' + + def _get_defender_path(): + install_path = reg_read_value('HKLM', reg_key, 'InstallLocation') + return fr'{install_path}\MpCmdRun.exe' + + log_path = format_log_path( + log_name='Microsoft Defender', timestamp=True, tool=True, + ) + log_path.parent.mkdir(parents=True, exist_ok=True) + + # Get MS Defender status + ## NOTE: disabled may be set to an int instead of bool + ## This is fine because we're just checking if it's enabled. + disabled = bool(reg_read_value('HKLM', reg_key, 'DisableAntiSpyware')) + disabled = disabled or reg_read_value('HKLM', reg_key, 'DisableAntiVirus') + passive_mode = reg_read_value('HKLM', reg_key, 'PassiveMode') == 2 + if disabled and not passive_mode: + raise GenericError('Defender is disabled.') + + # Update signatures + defender_path = _get_defender_path() + cmd = (defender_path, '-SignatureUpdate') + proc = run_program(cmd, check=False) + sleep(2) + if proc.returncode > 0: + LOG.warning('Failed to update Defender signatures') + + # Update defender path in case it changed after the update + defender_path = _get_defender_path() + + # Run scan + cmd = (defender_path, '-Scan', '-ScanType', '2' if full else '1') + proc = run_program(cmd, check=False) + log_path.write_text(proc.stdout) + if proc.returncode > 0: + raise GenericError('Failed to run scan or clean items.') + + def run_rkill(): """Run RKill scan.""" log_path = format_log_path(log_name='RKill', timestamp=True, tool=True) From 080e440d235681c67d4010ccc6374818c6a46e21 Mon Sep 17 00:00:00 2001 From: 2Shirt <2xShirt@gmail.com> Date: Sat, 1 May 2021 19:28:07 -0600 Subject: [PATCH 37/43] Add AdwCleaner sections --- scripts/auto_repairs.py | 2 +- scripts/wk/repairs/win.py | 17 +++++++++++++++++ 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/scripts/auto_repairs.py b/scripts/auto_repairs.py index 381ea729..d5ce4297 100644 --- a/scripts/auto_repairs.py +++ b/scripts/auto_repairs.py @@ -81,7 +81,7 @@ BASE_MENUS = { MenuEntry('Reboot', 'auto_reboot'), ), 'Manual Steps': ( - MenuEntry('AdwCleaner', placeholder_function), + MenuEntry('AdwCleaner', 'auto_adwcleaner'), MenuEntry('IO Bit Uninstaller', placeholder_function), MenuEntry('Enable Windows Updates', 'auto_windows_updates_enable'), ), diff --git a/scripts/wk/repairs/win.py b/scripts/wk/repairs/win.py index b054eb85..c5a1930b 100644 --- a/scripts/wk/repairs/win.py +++ b/scripts/wk/repairs/win.py @@ -535,6 +535,18 @@ def update_main_menu(menus): # Auto Repairs: Wrapper Functions +def auto_adwcleaner(group, name): + """Run AdwCleaner scan. + + save_settings() is called first since AdwCleaner may kill this script. + """ + save_settings(group, name, done=True, failed=False, message='DONE') + result = TRY_PRINT.run('AdwCleaner...', run_adwcleaner, msg_good='DONE') + + # Update with actual results (assuming this script wasn't killed) + save_settings(group, name, result=result) + + def auto_backup_power_plans(group, name): """Backup power plans.""" result = TRY_PRINT.run('Backup Power Plans...', export_power_plans) @@ -758,6 +770,11 @@ def delete_registry_null_keys(): run_tool('RegDelNull', 'RegDelNull', '-s', '-y', cbin=True) +def run_adwcleaner(): + """Run AdwCleaner.""" + run_tool('AdwCleaner', 'AdwCleaner', download=True) + + def run_bleachbit(cleaners, preview=True): """Run BleachBit to either clean or preview files.""" cmd_args = ( From ff43bc79b8798bd6875aba88bb6482128969828c Mon Sep 17 00:00:00 2001 From: 2Shirt <2xShirt@gmail.com> Date: Sat, 1 May 2021 20:00:46 -0600 Subject: [PATCH 38/43] Add uninstaller sections --- scripts/auto_repairs.py | 2 +- scripts/wk/repairs/win.py | 13 +++++++++++++ 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/scripts/auto_repairs.py b/scripts/auto_repairs.py index d5ce4297..06c4ba92 100644 --- a/scripts/auto_repairs.py +++ b/scripts/auto_repairs.py @@ -82,7 +82,7 @@ BASE_MENUS = { ), 'Manual Steps': ( MenuEntry('AdwCleaner', 'auto_adwcleaner'), - MenuEntry('IO Bit Uninstaller', placeholder_function), + MenuEntry('IO Bit Uninstaller', 'auto_iobit_uninstaller'), MenuEntry('Enable Windows Updates', 'auto_windows_updates_enable'), ), }, diff --git a/scripts/wk/repairs/win.py b/scripts/wk/repairs/win.py index c5a1930b..2945b554 100644 --- a/scripts/wk/repairs/win.py +++ b/scripts/wk/repairs/win.py @@ -640,6 +640,14 @@ def auto_hitmanpro(group, name): save_settings(group, name, result=result) +def auto_iobit_uninstaller(group, name): + """Run IO Bit Uninstaller scan.""" + result = TRY_PRINT.run( + 'IO Bit Uninstaller...', run_iobit_uninstaller, msg_good='DONE', + ) + save_settings(group, name, result=result) + + def auto_kvrt(group, name): """Run KVRT scan.""" result = TRY_PRINT.run('KVRT...', run_kvrt, msg_good='DONE') @@ -802,6 +810,11 @@ def run_hitmanpro(): ) +def run_iobit_uninstaller(): + """Run IO Bit Uninstaller.""" + run_tool('IObitUninstallerPortable', 'IObitUninstallerPortable', cbin=True) + + def run_kvrt(): """Run KVRT scan.""" log_path = format_log_path(log_name='KVRT', timestamp=True, tool=True) From 9b6bfa27600561e442188213b14a5fc765cdfdb4 Mon Sep 17 00:00:00 2001 From: 2Shirt <2xShirt@gmail.com> Date: Sat, 1 May 2021 20:01:01 -0600 Subject: [PATCH 39/43] Make Sync Clock optional --- scripts/auto_repairs.py | 1 + scripts/wk/repairs/win.py | 9 +++++---- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/scripts/auto_repairs.py b/scripts/auto_repairs.py index 06c4ba92..29846f01 100644 --- a/scripts/auto_repairs.py +++ b/scripts/auto_repairs.py @@ -90,6 +90,7 @@ BASE_MENUS = { MenuEntry('Kill Explorer'), MenuEntry('Run RKill'), MenuEntry('Run TDSSKiller (once)'), + MenuEntry('Sync Clock'), MenuEntry('Use Autologon'), ), 'Actions': ( diff --git a/scripts/wk/repairs/win.py b/scripts/wk/repairs/win.py index 2945b554..79424f5b 100644 --- a/scripts/wk/repairs/win.py +++ b/scripts/wk/repairs/win.py @@ -274,10 +274,11 @@ def init_run(options): if options['Kill Explorer']['Selected']: atexit.register(start_explorer) TRY_PRINT.run('Killing Explorer...', kill_explorer, msg_good='DONE') - TRY_PRINT.run( - 'Syncing Clock...', run_tool, 'Neutron', 'Neutron', - cbin=True, msg_good='DONE', - ) + if options['Sync Clock']['Selected']: + TRY_PRINT.run( + 'Syncing Clock...', run_tool, 'Neutron', 'Neutron', + cbin=True, msg_good='DONE', + ) if options['Run RKill']['Selected']: TRY_PRINT.run('Running RKill...', run_rkill, msg_good='DONE') From 6b35d4165d6b8f495495dd842a3aaff70c0bc00d Mon Sep 17 00:00:00 2001 From: 2Shirt <2xShirt@gmail.com> Date: Sat, 1 May 2021 20:04:50 -0600 Subject: [PATCH 40/43] Add download_tool() function --- scripts/wk/kit/tools.py | 54 ++++++++++++++++++++++------------------- 1 file changed, 29 insertions(+), 25 deletions(-) diff --git a/scripts/wk/kit/tools.py b/scripts/wk/kit/tools.py index cb145de1..ca44e61e 100644 --- a/scripts/wk/kit/tools.py +++ b/scripts/wk/kit/tools.py @@ -61,6 +61,34 @@ def download_file(out_path, source_url, as_new=False, overwrite=False): return out_path +def download_tool(folder, name): + """Download tool.""" + out_path = find_kit_dir('.bin').joinpath(f'{folder}/{name}.exe') + up_to_date = False + + # Check if tool is up to date + try: + ctime = datetime.fromtimestamp(out_path.stat().st_ctime) + up_to_date = datetime.now() - ctime < timedelta(days=DOWNLOAD_FREQUENCY) + except FileNotFoundError: + # Ignore - we'll download it below + pass + if out_path.exists() and up_to_date: + LOG.info('Skip downloading up-to-date tool: %s', name) + return + + # Download + LOG.info('Downloading tool: %s', name) + source_url = SOURCES[name] + try: + new_file = download_file(out_path, source_url, as_new=True) + new_file.replace(out_path) + except GenericError: + # Ignore as long as there's still a version present + if not out_path.exists(): + raise + + def extract_archive(archive, out_path, *args, mode='x', silent=True): """Extract an archive to out_path.""" out_path = pathlib.Path(out_path).resolve() @@ -127,18 +155,6 @@ def run_tool( proc will be either subprocess.CompletedProcess or subprocess.Popen.""" proc = None - def _is_outdated(file_path): - """Check if the ctime is older than the threshold, returns bool.""" - outdated = False - try: - ctime = datetime.fromtimestamp(file_path.stat().st_ctime) - outdated = datetime.now() - ctime > timedelta(days=DOWNLOAD_FREQUENCY) - except FileNotFoundError: - LOG.error("This shouldn't happen right?") - - # Done - return outdated - # Extract from .cbin if cbin: extract_archive( @@ -149,19 +165,7 @@ def run_tool( # Download tool if download: - out_path = find_kit_dir('.bin').joinpath(f'{folder}/{name}.exe') - if not out_path.exists() or _is_outdated(out_path): - LOG.info('Downloading tool: %s', name) - source_url = SOURCES[name] - try: - new_file = download_file(out_path, source_url, as_new=True) - new_file.replace(out_path) - except GenericError: - # Ignore as long as there's still a version present - if not out_path.exists(): - raise - else: - LOG.info('Skip downloading tool: %s', name) + download_tool(folder, name) # Run tool_path = get_tool_path(folder, name) From 2d4ae6518830ee49776af134c22a2f071efcecab Mon Sep 17 00:00:00 2001 From: 2Shirt <2xShirt@gmail.com> Date: Sat, 1 May 2021 20:41:24 -0600 Subject: [PATCH 41/43] Run KVRT in new pane under ConEmu --- scripts/wk/repairs/win.py | 20 ++++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/scripts/wk/repairs/win.py b/scripts/wk/repairs/win.py index 79424f5b..165b15dd 100644 --- a/scripts/wk/repairs/win.py +++ b/scripts/wk/repairs/win.py @@ -20,7 +20,7 @@ from wk.exe import ( wait_for_procs, ) from wk.io import delete_folder, rename_item -from wk.kit.tools import ARCH, get_tool_path, run_tool +from wk.kit.tools import ARCH, download_tool, get_tool_path, run_tool from wk.log import format_log_path, update_log_path from wk.os.win import ( reg_delete_value, @@ -826,12 +826,28 @@ def run_kvrt(): quarantine_path.mkdir(parents=True, exist_ok=True) cmd_args = ( '-accepteula', - '-d', quarantine_path, + '-d', str(quarantine_path), '-dontencrypt', '-fixednames', '-processlevel', '1', '-custom', SYSTEMDRIVE, '-silent', '-adinsilent', ) + + # Run in new pane + if IN_CONEMU: + download_tool('KVRT', 'KVRT') + kvrt_path = get_tool_path('KVRT', 'KVRT') + tmp_file = fr'{os.environ.get("TMP")}\run_kvrt.cmd' + with open(tmp_file, 'w') as _f: + _f.write('@echo off\n') + _f.write(f'"{kvrt_path}" {" ".join(cmd_args)}\n') + cmd = ('cmd', '/c', tmp_file, '-new_console:nb', '-new_console:s33V') + run_program(cmd, check=False) + sleep(1) + wait_for_procs('KVRT.exe') + return + + # Run in background proc = run_tool('KVRT', 'KVRT', *cmd_args, download=True) log_path.write_text(proc.stdout) From b1acb6a07606919da770286c03e6cb3a9f049810 Mon Sep 17 00:00:00 2001 From: 2Shirt <2xShirt@gmail.com> Date: Sat, 1 May 2021 20:49:28 -0600 Subject: [PATCH 42/43] Remove placeholder functions --- scripts/auto_repairs.py | 18 +----------------- 1 file changed, 1 insertion(+), 17 deletions(-) diff --git a/scripts/auto_repairs.py b/scripts/auto_repairs.py index 29846f01..7e0fd1f5 100644 --- a/scripts/auto_repairs.py +++ b/scripts/auto_repairs.py @@ -3,8 +3,6 @@ import os import sys -import random # TODO: Deleteme -import time os.chdir(os.path.dirname(os.path.realpath(__file__))) sys.path.append(os.getcwd()) @@ -29,25 +27,11 @@ class MenuEntry(): # Set details self.details = { 'Function': function, - 'Selected': False, + 'Selected': True, **kwargs, } -# TODO: Deleteme -TRY_AND_PRINT = wk.std.TryAndPrint() -TRY_AND_PRINT.width = 50 -def placeholder_function(group, name): - result = TRY_AND_PRINT.run(f'{name}...', time.sleep, random.randint(1, 3)) - wk.repairs.win.save_settings(group, name, result=result) - -def placeholder_reboot(group, name): - print('"Rebooting" shortly...') - time.sleep(random.randint(1, 3)) - wk.repairs.win.save_settings(group, name, done=True, message='DONE') - raise SystemExit - - # STATIC VARIABLES BASE_MENUS = { 'Groups': { From e9db4238ff900ebf5fb8120b8001a880f1d843ac Mon Sep 17 00:00:00 2001 From: 2Shirt <2xShirt@gmail.com> Date: Sat, 1 May 2021 21:17:42 -0600 Subject: [PATCH 43/43] Run Autologon before any scans --- scripts/wk/repairs/win.py | 46 ++++++++++++++++++++++++--------------- 1 file changed, 29 insertions(+), 17 deletions(-) diff --git a/scripts/wk/repairs/win.py b/scripts/wk/repairs/win.py index 165b15dd..c21c4c3f 100644 --- a/scripts/wk/repairs/win.py +++ b/scripts/wk/repairs/win.py @@ -185,8 +185,6 @@ def build_menus(base_menus, title): def end_session(): """End Auto Repairs session.""" - auto_admin_logon = '0' - # Delete Auto Repairs keys try: reg_delete_value('HKCU', AUTO_REPAIR_KEY, 'SessionStarted') @@ -209,15 +207,7 @@ def end_session(): LOG.error("Failed to remove scheduled task or it doesn't exist.") # Disable Autologon - try: - auto_admin_logon = reg_read_value( - 'HKLM', r'Software\Microsoft\Windows NT\CurrentVersion\Winlogon', - 'AutoAdminLogon', - ) - except FileNotFoundError: - # Ignore and assume it's disabled - return - if auto_admin_logon != '0': + if is_autologon_enabled(): run_tool('Sysinternals', 'Autologon') reg_set_value( 'HKLM', r'Software\Microsoft\Windows NT\CurrentVersion\Winlogon', @@ -274,6 +264,12 @@ def init_run(options): if options['Kill Explorer']['Selected']: atexit.register(start_explorer) TRY_PRINT.run('Killing Explorer...', kill_explorer, msg_good='DONE') + if options['Use Autologon']['Selected'] and not is_autologon_enabled(): + TRY_PRINT.run( + 'Running Autologon...', run_tool, + 'Autologon', 'Autologon', + cbin=True, msg_good='DONE', + ) if options['Sync Clock']['Selected']: TRY_PRINT.run( 'Syncing Clock...', run_tool, 'Neutron', 'Neutron', @@ -300,18 +296,30 @@ def init_session(options): run_program(cmd) # One-time tasks - if options['Use Autologon']['Selected']: - TRY_PRINT.run( - 'Running Autologon...', run_tool, - 'Autologon', 'Autologon', - cbin=True, msg_good='DONE', - ) if options['Run TDSSKiller (once)']['Selected']: TRY_PRINT.run('Running TDSSKiller...', run_tdsskiller, msg_good='DONE') print('') reboot(30) +def is_autologon_enabled(): + """Check if Autologon is enabled, returns bool.""" + auto_admin_logon = False + try: + auto_admin_logon = reg_read_value( + 'HKLM', r'Software\Microsoft\Windows NT\CurrentVersion\Winlogon', + 'AutoAdminLogon', + ) + except FileNotFoundError: + # Ignore and assume it's disabled + pass + else: + auto_admin_logon = auto_admin_logon != '0' + + # Done + return auto_admin_logon + + def is_session_started(): """Check if session was started, returns bool.""" session_started = False @@ -373,6 +381,10 @@ def run_auto_repairs(base_menus): save_selection_settings(menus) print_info('Initializing...') init_run(menus['Options'].options) + if not is_autologon_enabled(): + # Either it wasn't selected or a password wasn't entered + menus['Options'].options['Use Autologon']['Selected'] = False + save_selection_settings(menus) if not session_started: init_session(menus['Options'].options) print_info('Running repairs')