diff --git a/scripts/auto_repairs.py b/scripts/auto_repairs.py new file mode 100644 index 00000000..7e0fd1f5 --- /dev/null +++ b/scripts/auto_repairs.py @@ -0,0 +1,94 @@ +"""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 # 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 = { + 'Function': function, + 'Selected': True, + **kwargs, + } + + +# STATIC VARIABLES +BASE_MENUS = { + 'Groups': { + 'Backup Settings': ( + 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', #TODO), + MenuEntry('Backup Power Plans', 'auto_backup_power_plans'), + MenuEntry('Backup Registry', 'auto_backup_registry'), + ), + 'Windows Repairs': ( + MenuEntry('Disable Windows Updates', 'auto_windows_updates_disable'), + MenuEntry('Reset Windows Updates', 'auto_windows_updates_reset'), + MenuEntry('Reboot', 'auto_reboot'), + MenuEntry('CHKDSK', 'auto_chkdsk'), + MenuEntry('DISM RestoreHealth', 'auto_dism'), + 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', 'auto_bleachbit'), + MenuEntry('HitmanPro', 'auto_hitmanpro'), + MenuEntry('KVRT', 'auto_kvrt'), + MenuEntry('Windows Defender', 'auto_microsoft_defender'), + MenuEntry('Reboot', 'auto_reboot'), + ), + 'Manual Steps': ( + MenuEntry('AdwCleaner', 'auto_adwcleaner'), + MenuEntry('IO Bit Uninstaller', 'auto_iobit_uninstaller'), + MenuEntry('Enable Windows Updates', 'auto_windows_updates_enable'), + ), + }, + 'Options': ( + MenuEntry('Kill Explorer'), + MenuEntry('Run RKill'), + MenuEntry('Run TDSSKiller (once)'), + MenuEntry('Sync Clock'), + MenuEntry('Use Autologon'), + ), + 'Actions': ( + MenuEntry('Options'), + MenuEntry('Start', Separator=True), + MenuEntry('Quit'), + ), + } + + +if __name__ == '__main__': + try: + wk.repairs.win.run_auto_repairs(BASE_MENUS) + 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/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..27e56796 --- /dev/null +++ b/scripts/wk/cfg/tools.py @@ -0,0 +1,73 @@ +"""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', + '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', + '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', + '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', + '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/exe.py b/scripts/wk/exe.py index 87b90935..5edeb041 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 @@ -242,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/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/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..ca44e61e --- /dev/null +++ b/scripts/wk/kit/tools.py @@ -0,0 +1,185 @@ +"""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 +CACHED_DIRS = {} + + +# Functions +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) + + # 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) + + # 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, name, + ) + raise GenericError(f'Failed to download file: {name}') + + # Write to file + with open(out_path, 'wb') as _f: + for chunk in response.iter_content(chunk_size=128): + _f.write(chunk) + + # Done + print(f' {cursor_left}', end='', flush=True) + 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() + 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. + + 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, check=True): + """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 check and not tool_path.exists(): + raise FileNotFoundError(f'Failed to find tool, {folder=}, {name=}') + + # Done + return tool_path + + +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: + download_tool(folder, 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, **run_kwargs) + else: + proc = run_program(cmd, check=False, **run_kwargs) + + # Done + return proc + + +if __name__ == '__main__': + print("This file is not meant to be called directly.") 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" diff --git a/scripts/wk/os/win.py b/scripts/wk/os/win.py index eadcab71..20ee4917 100644 --- a/scripts/wk/os/win.py +++ b/scripts/wk/os/win.py @@ -5,19 +5,24 @@ import logging import os import pathlib import platform -import winreg from contextlib import suppress +import psutil + +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 -from wk.log import format_log_path 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, @@ -56,7 +61,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 +101,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 +119,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 @@ -225,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) @@ -407,5 +313,106 @@ def reg_set_value(hive, key, name, data, data_type, option=None): winreg.SetValue(hive, key, data_type, data) +# 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) + + +# 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.") 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..c21c4c3f --- /dev/null +++ b/scripts/wk/repairs/win.py @@ -0,0 +1,1188 @@ +"""WizardKit: Repairs - Windows""" +# pylint: disable=too-many-lines +# vim: sts=2 sw=2 ts=2 + +import atexit +import logging +import os +import platform +import re +import sys +import time + +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, 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, + reg_read_value, + reg_set_value, + reg_write_settings, + disable_service, + enable_service, + stop_service, + ) +from wk.std import ( + GenericError, + GenericWarning, + Menu, + TryAndPrint, + ask, + clear_screen, + color_string, + pause, + print_info, + print_standard, + print_warning, + set_title, + show_data, + sleep, + strip_colors, + ) + + +# STATIC VARIABLES +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.', + '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': { + r'Software\Microsoft\Windows\CurrentVersion\Policies\System': ( + ('ConsentPromptBehaviorAdmin', 5, 'DWORD'), + ('ConsentPromptBehaviorUser', 3, 'DWORD'), + ('EnableLUA', 1, 'DWORD'), + ('PromptOnSecureDesktop', 1, 'DWORD'), + ), + }, + } +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', 'C:') +WIDTH = 50 +TRY_PRINT = TryAndPrint() +TRY_PRINT.width = WIDTH +TRY_PRINT.verbose = True +for error in ('CalledProcessError', 'FileNotFoundError'): + TRY_PRINT.add_error(error) + + +# 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")}') + + # Main Menu + for entry in base_menus['Actions']: + menus['Main'].add_action(entry.name, entry.details) + 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['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['Groups'].items(): + menus[group] = Menu(title=f'{title}\n{color_string(group, "GREEN")}') + menus[group].disabled_str = 'Locked' + for entry in entries: + menus[group].add_option(entry.name, entry.details) + 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() + + # 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 + + +def end_session(): + """End Auto Repairs session.""" + # 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 = [ + 'schtasks', '/delete', '/f', + '/tn', f'{KIT_NAME_FULL}-AutoRepairs', + ] + try: + run_program(cmd) + except CalledProcessError: + LOG.error("Failed to remove scheduled task or it doesn't exist.") + + # Disable Autologon + if is_autologon_enabled(): + 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 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', 'message', 'selected', 'skipped', 'warning'): + try: + 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 + + +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='') + sleep(1) + print('') + + # Done + return session_started + + +def init_run(options): + """Initialize Auto Repairs Run.""" + 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', + 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.""" + reg_set_value('HKCU', AUTO_REPAIR_KEY, 'SessionStarted', 1, 'DWORD') + + # Create logon task for Auto Repairs + cmd = [ + 'schtasks', '/create', '/f', + '/sc', 'ONLOGON', + '/tn', f'{KIT_NAME_FULL}-AutoRepairs', + '/rl', 'HIGHEST', + '/tr', fr'C:\Windows\System32\cmd.exe "/C {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['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 + try: + session_started = reg_read_value('HKCU', AUTO_REPAIR_KEY, 'SessionStarted') + except FileNotFoundError: + pass + + # Done + return session_started + + +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)) + + +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' + clear_screen() + set_title(title) + print_info(title) + print('') + + # Generate menus + print_standard('Initializing...') + menus = build_menus(base_menus, title) + + # Init + try: + session_started = init(menus) + 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: + 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 + clear_screen() + print_standard(title) + print('') + 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') + + # Run repairs + for group, menu in menus.items(): + if group in ('Main', 'Options'): + continue + run_group(group, menu) + + # Done + end_session() + print_info('Done') + pause('Press Enter to exit...') + + +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('Warning', False): + color = 'YELLOW' + elif details.get('Failed', False): + color = 'RED' + show_data( + f'{name_str}...', + details.get('Message', '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, 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'], + }) + if isinstance(result['Exception'], GenericWarning): + kwargs['warning'] = True + + # Write values to registry + 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): + """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 + + +# 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) + 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_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 + 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 ({SYSTEMDRIVE})...', + 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_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 + 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'], + warning=not result['Failed'] and needs_reboot, + message=result['Message'], + ) + + # Done + if needs_reboot: + 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_hitmanpro(group, name): + """Run HitmanPro scan.""" + result = TRY_PRINT.run('HitmanPro...', run_hitmanpro, msg_good='DONE') + 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') + 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') + 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( + '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) + + +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.""" + return set_local_storage_path('Backups', name, date) + + +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: + local_path = local_path.joinpath(time.strftime('%Y-%m-%d')) + return local_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') + + +def delete_registry_null_keys(): + """Delete registry keys with embedded null characters.""" + 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 = ( + '--preview' if preview else '--clean', + *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 + log_path.write_text(proc.stdout) + 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_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) + 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', 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) + + +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) + 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 = set_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.""" + 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_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') + stop_service('wuauserv') + + +def enable_windows_updates(): + """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'] + run_program(cmd, check=False) + + +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 + + +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') + 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(): + """Set filesystem 'dirty bit' to force a CHKDSK during startup.""" + cmd = ['fsutil', 'dirty', 'set', SYSTEMDRIVE] + 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', SYSTEMDRIVE] + if OS_VERSION >= 8: + cmd.extend(['/scan', '/perf']) + if IN_CONEMU: + cmd.extend(['-new_console:nb', '-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:nb', '-new_console:s33V'] if IN_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', + timestamp=True, tool=True, + ) + log_path.parent.mkdir(parents=True, exist_ok=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', timestamp=True, 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', timestamp=True, 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 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']) + + +if __name__ == '__main__': + print("This file is not meant to be called directly.") diff --git a/scripts/wk/std.py b/scripts/wk/std.py index 5dba174c..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 @@ -381,12 +382,17 @@ 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): + """Update menu with default settings.""" + self._update() + class TryAndPrint(): # pylint: disable=too-many-instance-attributes @@ -455,7 +461,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 +474,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) @@ -489,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): @@ -560,12 +587,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) @@ -600,17 +626,18 @@ 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 self._log_result(message, result_msg) return { - 'Failed': bool(f_exception), 'Exception': f_exception, + 'Failed': bool(f_exception), + 'Message': result_msg, 'Output': output, } @@ -645,7 +672,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) @@ -977,11 +1003,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='', 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 @@ - +