diff --git a/.bin/Scripts/add-known-networks b/.bin/Scripts/add-known-networks index 6047c438..bdfc7a1c 100644 --- a/.bin/Scripts/add-known-networks +++ b/.bin/Scripts/add-known-networks @@ -4,6 +4,7 @@ import os import re +import sys import uuid KNOWN_NETWORKS = '/root/known_networks' @@ -34,13 +35,21 @@ method=auto ''' def get_user_name(): - """Get real user name, returns str.""" + """Get user name, returns str.""" user = None + + # Get running user if 'SUDO_USER' in os.environ: user = os.environ.get('SUDO_USER') else: user = os.environ.get('USER') + # Check if user manually specified + for a in sys.argv: + a = a.strip().lower() + if a.startswith('--user='): + user = a.replace('--user=', '') + return user if __name__ == '__main__': @@ -54,7 +63,7 @@ if __name__ == '__main__': for ssid, password in known_networks.items(): out_path = '{}/{}.nmconnection'.format( '/etc/NetworkManager/system-connections', - password, + ssid, ) if not os.path.exists(out_path): with open(out_path, 'w') as f: diff --git a/.bin/Scripts/build-ufd b/.bin/Scripts/build-ufd index bb0813a8..45c3ff35 100755 --- a/.bin/Scripts/build-ufd +++ b/.bin/Scripts/build-ufd @@ -1,6 +1,6 @@ #!/bin/env python3 # -# pylint: disable=no-name-in-module,wildcard-import +# pylint: disable=no-name-in-module,wildcard-import,wrong-import-position # vim: sts=2 sw=2 ts=2 """Wizard Kit: UFD build tool""" @@ -54,16 +54,8 @@ if __name__ == '__main__': confirm_selections(args) # Prep UFD - print_info('Prep UFD') - if args['--update']: - # Remove arch folder - try_and_print( - indent=2, - message='Removing Linux...', - function=remove_arch, - ) - else: - # Format and partition + if not args['--update']: + print_info('Prep UFD') prep_device(ufd_dev, UFD_LABEL, use_mbr=args['--use-mbr']) # Mount UFD @@ -73,8 +65,17 @@ if __name__ == '__main__': function=mount, mount_source=find_first_partition(ufd_dev), mount_point='/mnt/UFD', + read_write=True, ) + # Remove Arch folder + if args['--update']: + try_and_print( + indent=2, + message='Removing Linux...', + function=remove_arch, + ) + # Copy sources print_standard(' ') print_info('Copy Sources') diff --git a/.bin/Scripts/check_disk.py b/.bin/Scripts/check_disk.py index 8919063e..fe18650b 100644 --- a/.bin/Scripts/check_disk.py +++ b/.bin/Scripts/check_disk.py @@ -38,7 +38,7 @@ if __name__ == '__main__': if repair: cs = 'Scheduled' else: - cs = 'CS' + cs = 'No issues' message = 'CHKDSK ({SYSTEMDRIVE})...'.format(**global_vars['Env']) try_and_print(message=message, function=run_chkdsk, cs=cs, other_results=other_results, repair=repair) diff --git a/.bin/Scripts/debug/hw_diags.py b/.bin/Scripts/debug/hw_diags.py index 87a35990..44517fb4 100644 --- a/.bin/Scripts/debug/hw_diags.py +++ b/.bin/Scripts/debug/hw_diags.py @@ -149,11 +149,14 @@ def save_debug_reports(state, global_vars): f.write('{}\n'.format(line)) -def upload_logdir(global_vars): +def upload_logdir(global_vars, reason='Crash'): """Upload compressed LogDir to CRASH_SERVER.""" source = global_vars['LogDir'] source = source[source.rfind('/')+1:] - dest = '{}.txz'.format(source) + dest = 'HW-Diags_{reason}_{Date-Time}.txz'.format( + reason=reason, + **global_vars, + ) data = None # Compress LogDir @@ -166,7 +169,7 @@ def upload_logdir(global_vars): data = f.read() # Upload data - url = '{}/Crash_{}.txz'.format(CRASH_SERVER['Url'], source) + url = '{}/{}'.format(CRASH_SERVER['Url'], dest) r = requests.put( url, data=data, diff --git a/.bin/Scripts/functions/browsers.py b/.bin/Scripts/functions/browsers.py index dcb0ed2f..8a1f4dcb 100644 --- a/.bin/Scripts/functions/browsers.py +++ b/.bin/Scripts/functions/browsers.py @@ -32,8 +32,8 @@ def archive_all_users(): user_path = os.path.join(users_root, user_name) appdata_local = os.path.join(user_path, r'AppData\Local') appdata_roaming = os.path.join(user_path, r'AppData\Roaming') - valid_user &= os.path.exists(appdata_local) - valid_user &= os.path.exists(appdata_roaming) + valid_user = valid_user and os.path.exists(appdata_local) + valid_user = valid_user and os.path.exists(appdata_roaming) if valid_user: user_envs.append({ 'USERNAME': user_name, @@ -325,7 +325,6 @@ def install_adblock(indent=8, width=32, just_firefox=False): if just_firefox and browser_data[browser]['base'] != 'mozilla': continue exe_path = browser_data[browser].get('exe_path', None) - function=run_program if not exe_path: if browser_data[browser]['profiles']: print_standard( @@ -375,7 +374,6 @@ def install_adblock(indent=8, width=32, just_firefox=False): elif browser_data[browser]['base'] == 'ie': urls.append(IE_GALLERY) - function=popen_program # By using check=False we're skipping any return codes so # it should only fail if the program can't be run @@ -384,10 +382,16 @@ def install_adblock(indent=8, width=32, just_firefox=False): # installation status. try_and_print(message='{}...'.format(browser), indent=indent, width=width, - cs='Done', function=function, + cs='Started', function=popen_program, cmd=[exe_path, *urls], check=False) +def is_installed(browser_name): + """Checks if browser is installed based on exe_path, returns bool.""" + browser_name = browser_name.replace(' Chromium', '') + return bool(browser_data.get(browser_name, {}).get('exe_path', False)) + + def list_homepages(indent=8, width=32): """List current homepages for reference.""" browser_list = [k for k, v in sorted(browser_data.items()) if v['exe_path']] @@ -419,6 +423,12 @@ def list_homepages(indent=8, width=32): indent=' '*indent, width=width, name=name, page=page)) +def profile_present(browser_name): + """Checks if a profile was detected for browser, returns bool.""" + browser_name = browser_name.replace(' Chromium', '') + return bool(browser_data.get(browser_name, {}).get('profiles', False)) + + def reset_browsers(indent=8, width=32): """Reset all detected browsers to safe defaults.""" browser_list = [k for k, v in sorted(browser_data.items()) if v['profiles']] @@ -437,14 +447,21 @@ def reset_browsers(indent=8, width=32): other_results=other_results, profile=profile) -def scan_for_browsers(just_firefox=False): +def scan_for_browsers(just_firefox=False, silent=False): """Scan system for any supported browsers.""" for name, details in sorted(SUPPORTED_BROWSERS.items()): if just_firefox and details['base'] != 'mozilla': continue - try_and_print(message='{}...'.format(name), - function=get_browser_details, cs='Detected', - other_results=other_results, name=name) + if silent: + try: + get_browser_details(name) + except Exception: + # Ignore errors in silent mode + pass + else: + try_and_print(message='{}...'.format(name), + function=get_browser_details, cs='Detected', + other_results=other_results, name=name) if __name__ == '__main__': diff --git a/.bin/Scripts/functions/cleanup.py b/.bin/Scripts/functions/cleanup.py index 5ee20be1..744ee048 100644 --- a/.bin/Scripts/functions/cleanup.py +++ b/.bin/Scripts/functions/cleanup.py @@ -1,7 +1,9 @@ -# Wizard Kit: Functions - Cleanup - -from functions.common import * +'''Wizard Kit: Functions - Cleanup''' +# pylint: disable=no-name-in-module,wildcard-import +# vim: sts=2 sw=2 ts=2 +from functions.setup import * +from settings.cleanup import * def cleanup_adwcleaner(): """Move AdwCleaner folders into the ClientDir.""" @@ -75,8 +77,7 @@ def cleanup_desktop(): desktop_path = r'{USERPROFILE}\Desktop'.format(**global_vars['Env']) for entry in os.scandir(desktop_path): - # JRT, RKill, Shortcut cleaner - if re.search(r'^(JRT|RKill|sc-cleaner)', entry.name, re.IGNORECASE): + if DESKTOP_ITEMS.search(entry.name): dest_name = r'{}\{}'.format(dest_folder, entry.name) dest_name = non_clobber_rename(dest_name) shutil.move(entry.path, dest_name) @@ -130,7 +131,14 @@ def delete_registry_value(hive, key, value): winreg.DeleteValue(k, value) +def restore_default_uac(): + """Restores default UAC settings via the registry.""" + if global_vars['OS']['Version'] == '10': + write_registry_settings(UAC_DEFAULTS_WIN10, all_users=True) + else: + # Haven't checked Win8 settings, only applying minimum set + write_registry_settings(UAC_DEFAULTS_WIN7, all_users=True) + + if __name__ == '__main__': print("This file is not meant to be called directly.") - -# vim: sts=2 sw=2 ts=2 diff --git a/.bin/Scripts/functions/common.py b/.bin/Scripts/functions/common.py index 28834660..689cc85f 100644 --- a/.bin/Scripts/functions/common.py +++ b/.bin/Scripts/functions/common.py @@ -64,10 +64,13 @@ class GenericRepair(Exception): class MultipleInstallationsError(Exception): pass -class NotInstalledError(Exception): +class NoProfilesError(Exception): pass -class NoProfilesError(Exception): +class Not4KAlignedError(Exception): + pass + +class NotInstalledError(Exception): pass class OSInstalledLegacyError(Exception): @@ -88,6 +91,12 @@ class SecureBootNotAvailError(Exception): class SecureBootUnknownError(Exception): pass +class WindowsOutdatedError(Exception): + pass + +class WindowsUnsupportedError(Exception): + pass + # General functions def abort(show_prompt=True): @@ -164,18 +173,22 @@ def clear_screen(): def convert_to_bytes(size): """Convert human-readable size str to bytes and return an int.""" size = str(size) - tmp = re.search(r'(\d+\.?\d*)\s+([KMGT]B)', size.upper()) + tmp = re.search(r'(\d+\.?\d*)\s+([PTGMKB])B?', size.upper()) if tmp: size = float(tmp.group(1)) units = tmp.group(2) - if units == 'TB': - size *= 1099511627776 - elif units == 'GB': - size *= 1073741824 - elif units == 'MB': - size *= 1048576 - elif units == 'KB': - size *= 1024 + if units == 'P': + size *= 1024 ** 5 + if units == 'T': + size *= 1024 ** 4 + elif units == 'G': + size *= 1024 ** 3 + elif units == 'M': + size *= 1024 ** 2 + elif units == 'K': + size *= 1024 ** 1 + elif units == 'B': + size *= 1024 ** 0 size = int(size) else: return -1 @@ -294,20 +307,24 @@ def human_readable_size(size, decimals=0): return '{size:>{width}} b'.format(size='???', width=width) # Convert to sensible units - if size >= 1099511627776: - size /= 1099511627776 - units = 'Tb' - elif size >= 1073741824: - size /= 1073741824 - units = 'Gb' - elif size >= 1048576: - size /= 1048576 - units = 'Mb' - elif size >= 1024: - size /= 1024 - units = 'Kb' + if size >= 1024 ** 5: + size /= 1024 ** 5 + units = 'PB' + elif size >= 1024 ** 4: + size /= 1024 ** 4 + units = 'TB' + elif size >= 1024 ** 3: + size /= 1024 ** 3 + units = 'GB' + elif size >= 1024 ** 2: + size /= 1024 ** 2 + units = 'MB' + elif size >= 1024 ** 1: + size /= 1024 ** 1 + units = 'KB' else: - units = ' b' + size /= 1024 ** 0 + units = ' B' # Return return '{size:>{width}.{decimals}f} {units}'.format( @@ -422,6 +439,8 @@ def non_clobber_rename(full_path): def pause(prompt='Press Enter to continue... '): """Simple pause implementation.""" + if prompt[-1] != ' ': + prompt += ' ' input(prompt) diff --git a/.bin/Scripts/functions/data.py b/.bin/Scripts/functions/data.py index fa0bb20c..bf091b37 100644 --- a/.bin/Scripts/functions/data.py +++ b/.bin/Scripts/functions/data.py @@ -151,12 +151,16 @@ def is_valid_wim_file(item): def get_mounted_volumes(): """Get mounted volumes, returns dict.""" cmd = [ - 'findmnt', '-J', '-b', '-i', - '-t', ( + 'findmnt', + '--list', + '--json', + '--bytes', + '--invert', + '--types', ( 'autofs,binfmt_misc,bpf,cgroup,cgroup2,configfs,debugfs,devpts,' 'devtmpfs,hugetlbfs,mqueue,proc,pstore,securityfs,sysfs,tmpfs' ), - '-o', 'SOURCE,TARGET,FSTYPE,LABEL,SIZE,AVAIL,USED'] + '--output', 'SOURCE,TARGET,FSTYPE,LABEL,SIZE,AVAIL,USED'] json_data = get_json_from_command(cmd) mounted_volumes = [] for item in json_data.get('filesystems', []): @@ -195,6 +199,8 @@ def mount_volumes( volumes.update({child['name']: child}) for grandchild in child.get('children', []): volumes.update({grandchild['name']: grandchild}) + for great_grandchild in grandchild.get('children', []): + volumes.update({great_grandchild['name']: great_grandchild}) # Get list of mounted volumes mounted_volumes = get_mounted_volumes() @@ -212,7 +218,7 @@ def mount_volumes( report[vol_path] = vol_data elif 'children' in vol_data: # Skip LVM/RAID partitions (the real volume is mounted separately) - vol_data['show_data']['data'] = vol_data.get('fstype', 'UNKNOWN') + vol_data['show_data']['data'] = vol_data.get('fstype', 'Unknown') if vol_data.get('label', None): vol_data['show_data']['data'] += ' "{}"'.format(vol_data['label']) vol_data['show_data']['info'] = True @@ -237,6 +243,7 @@ def mount_volumes( if vol_data['show_data']['data'] == 'Failed to mount': vol_data['mount_point'] = None else: + fstype = vol_data.get('fstype', 'Unknown FS') size_used = human_readable_size( mounted_volumes[vol_path]['used'], decimals=1, @@ -250,8 +257,9 @@ def mount_volumes( vol_data['mount_point'] = mounted_volumes[vol_path]['target'] vol_data['show_data']['data'] = 'Mounted on {}'.format( mounted_volumes[vol_path]['target']) - vol_data['show_data']['data'] = '{:40} ({} used, {} free)'.format( + vol_data['show_data']['data'] = '{:40} ({}, {} used, {} free)'.format( vol_data['show_data']['data'], + fstype, size_used, size_avail) @@ -285,6 +293,14 @@ def mount_backup_shares(read_write=False): def mount_network_share(server, read_write=False): """Mount a network share defined by server.""" + uid = '1000' + + # Get UID + cmd = ['id', '--user', 'tech'] + result = run_program(cmd, check=False, encoding='utf-8', errors='ignore') + if result.stdout.strip().isnumeric(): + uid = result.stdout.strip() + if read_write: username = server['RW-User'] password = server['RW-Pass'] @@ -300,18 +316,35 @@ def mount_network_share(server, read_write=False): error = r'Failed to mount \\{Name}\{Share} ({IP})'.format(**server) success = 'Mounted {Name}'.format(**server) elif psutil.LINUX: + # Make mountpoint cmd = [ 'sudo', 'mkdir', '-p', '/Backups/{Name}'.format(**server)] run_program(cmd) + + # Set mount options + cmd_options = [ + # Assuming GID matches UID + 'gid={}'.format(uid), + 'uid={}'.format(uid), + ] + cmd_options.append('rw' if read_write else 'ro') + cmd_options.append('username={}'.format(username)) + if password: + cmd_options.append('password={}'.format(password)) + else: + # Skip password check + cmd_options.append('guest') + + # Set mount command cmd = [ 'sudo', 'mount', - '//{IP}/{Share}'.format(**server), + '//{IP}/{Share}'.format(**server).replace('\\', '/'), '/Backups/{Name}'.format(**server), - '-o', '{}username={},password={}'.format( - '' if read_write else 'ro,', - username, - password)] + '-o', ','.join(cmd_options), + ] + + # Set result messages warning = 'Failed to mount /Backups/{Name}, {IP} unreachable.'.format( **server) error = 'Failed to mount /Backups/{Name}'.format(**server) diff --git a/.bin/Scripts/functions/ddrescue.py b/.bin/Scripts/functions/ddrescue.py index 545d08e0..463adf4d 100644 --- a/.bin/Scripts/functions/ddrescue.py +++ b/.bin/Scripts/functions/ddrescue.py @@ -1,25 +1,25 @@ -# Wizard Kit: Functions - ddrescue-tui +# pylint: disable=no-name-in-module,too-many-lines,wildcard-import +# vim: sts=2 sw=2 ts=2 +'''Wizard Kit: Functions - ddrescue-tui''' import datetime import pathlib -import psutil -import pytz import re -import signal import stat import time +from operator import itemgetter -from collections import OrderedDict +import pytz from functions.data import * from functions.hw_diags import * from functions.json import * from functions.tmux import * -from operator import itemgetter from settings.ddrescue import * # Clases class BaseObj(): + # pylint: disable=missing-docstring """Base object used by DevObj, DirObj, and ImageObj.""" def __init__(self, path): self.type = 'base' @@ -44,6 +44,7 @@ class BaseObj(): class BlockPair(): + # pylint: disable=too-many-instance-attributes """Object to track data and methods together for source and dest.""" def __init__(self, mode, source, dest): self.mode = mode @@ -60,9 +61,10 @@ class BlockPair(): if self.mode == 'clone': # Cloning self.dest_path = dest.path - self.map_path = '{pwd}/Clone_{prefix}.map'.format( - pwd=os.path.realpath(global_vars['Env']['PWD']), - prefix=source.prefix) + self.map_path = '{cwd}/Clone_{prefix}.map'.format( + cwd=os.path.realpath(os.getcwd()), + prefix=source.prefix, + ) else: # Imaging self.dest_path = '{path}/{prefix}.dd'.format( @@ -105,19 +107,19 @@ class BlockPair(): def load_map_data(self): """Load data from map file and set progress.""" map_data = read_map_file(self.map_path) - self.rescued_percent = map_data['rescued'] - self.rescued = (self.rescued_percent * self.size) / 100 + self.rescued = map_data.get('rescued', 0) + self.rescued_percent = (self.rescued / self.size) * 100 if map_data['full recovery']: self.pass_done = [True, True, True] self.rescued = self.size self.status = ['Skipped', 'Skipped', 'Skipped'] - elif map_data['non-tried'] > 0: + elif map_data.get('non-tried', 0) > 0: # Initial pass incomplete pass - elif map_data['non-trimmed'] > 0: + elif map_data.get('non-trimmed', 0) > 0: self.pass_done = [True, False, False] self.status = ['Skipped', 'Pending', 'Pending'] - elif map_data['non-scraped'] > 0: + elif map_data.get('non-scraped', 0) > 0: self.pass_done = [True, True, False] self.status = ['Skipped', 'Skipped', 'Pending'] else: @@ -145,14 +147,15 @@ class BlockPair(): """Update progress using map file.""" if os.path.exists(self.map_path): map_data = read_map_file(self.map_path) - self.rescued_percent = map_data.get('rescued', 0) - self.rescued = (self.rescued_percent * self.size) / 100 + self.rescued = map_data.get('rescued', 0) + self.rescued_percent = (self.rescued / self.size) * 100 self.status[pass_num] = get_formatted_status( label='Pass {}'.format(pass_num+1), data=(self.rescued/self.size)*100) class DevObj(BaseObj): + # pylint: disable=too-many-instance-attributes """Block device object.""" def self_check(self): """Verify that self.path points to a block device.""" @@ -186,6 +189,7 @@ class DevObj(BaseObj): self.update_filename_prefix() def update_filename_prefix(self): + # pylint: disable=attribute-defined-outside-init """Set filename prefix based on details.""" self.prefix = '{m_size}_{model}'.format( m_size=self.model_size, @@ -205,6 +209,7 @@ class DevObj(BaseObj): class DirObj(BaseObj): + """Directory object.""" def self_check(self): """Verify that self.path points to a directory.""" if not pathlib.Path(self.path).is_dir(): @@ -222,6 +227,7 @@ class DirObj(BaseObj): class ImageObj(BaseObj): + """Image file object.""" def self_check(self): """Verify that self.path points to a file.""" if not pathlib.Path(self.path).is_file(): @@ -243,10 +249,11 @@ class ImageObj(BaseObj): self.report = get_device_report(self.loop_dev) self.report = self.report.replace( self.loop_dev[self.loop_dev.rfind('/')+1:], '(Img)') - run_program(['losetup', '--detach', self.loop_dev], check=False) + run_program(['sudo', 'losetup', '--detach', self.loop_dev], check=False) class RecoveryState(): + # pylint: disable=too-many-instance-attributes """Object to track BlockPair objects and overall state.""" def __init__(self, mode, source, dest): self.mode = mode.lower() @@ -270,6 +277,7 @@ class RecoveryState(): if mode not in ('clone', 'image'): raise GenericError('Unsupported mode') self.get_smart_source() + self.set_working_dir() def add_block_pair(self, source, dest): """Run safety checks and append new BlockPair to internal list.""" @@ -314,20 +322,134 @@ class RecoveryState(): # Safety checks passed self.block_pairs.append(BlockPair(self.mode, source, dest)) + def build_outer_panes(self): + """Build top and side panes.""" + clear_screen() + + # Top + self.panes['Source'] = tmux_split_window( + behind=True, vertical=True, lines=2, + text='{BLUE}Source{CLEAR}'.format(**COLORS)) + + # Started + self.panes['Started'] = tmux_split_window( + lines=SIDE_PANE_WIDTH, target_pane=self.panes['Source'], + text='{BLUE}Started{CLEAR}\n{s}'.format( + s=time.strftime("%Y-%m-%d %H:%M %Z"), + **COLORS)) + + # Destination + self.panes['Destination'] = tmux_split_window( + percent=50, target_pane=self.panes['Source'], + text='{BLUE}Destination{CLEAR}'.format(**COLORS)) + + # Progress + update_sidepane(self) + self.panes['Progress'] = tmux_split_window( + lines=SIDE_PANE_WIDTH, watch=self.progress_out) + def current_pass_done(self): """Checks if pass is done for all block-pairs, returns bool.""" done = True - for bp in self.block_pairs: - done &= bp.pass_done[self.current_pass] + for b_pair in self.block_pairs: + done = done and b_pair.pass_done[self.current_pass] return done def current_pass_min(self): """Gets minimum pass rescued percentage, returns float.""" min_percent = 100 - for bp in self.block_pairs: - min_percent = min(min_percent, bp.rescued_percent) + for b_pair in self.block_pairs: + min_percent = min(min_percent, b_pair.rescued_percent) return min_percent + def fix_tmux_panes(self, forced=False): + # pylint: disable=too-many-branches,too-many-locals + """Fix pane sizes if the winodw has been resized.""" + needs_fixed = False + + # Check layout + for pane, pane_data in TMUX_LAYOUT.items(): + if not pane_data.get('Check'): + # Not concerned with the size of this pane + continue + # Get target + target = None + if pane != 'Current': + if pane not in self.panes: + # Skip missing panes + continue + else: + target = self.panes[pane] + + # Check pane size + size_x, size_y = tmux_get_pane_size(pane_id=target) + if pane_data.get('x', False) and pane_data['x'] != size_x: + needs_fixed = True + if pane_data.get('y', False) and pane_data['y'] != size_y: + needs_fixed = True + + # Bail? + if not needs_fixed and not forced: + return + + # Remove Destination pane (temporarily) + tmux_kill_pane(self.panes['Destination']) + + # Update layout + for pane, pane_data in TMUX_LAYOUT.items(): + # Get target + target = None + if pane != 'Current': + if pane not in self.panes: + # Skip missing panes + continue + else: + target = self.panes[pane] + + # Resize pane + tmux_resize_pane(pane_id=target, **pane_data) + + # Calc Source/Destination pane sizes + width, height = tmux_get_pane_size() + width = int(width / 2) - 1 + + # Update Source string + source_str = self.source.name + if len(source_str) > width: + source_str = '{}...'.format(source_str[:width-3]) + + # Update Destination string + dest_str = self.dest.name + if len(dest_str) > width: + if self.mode == 'clone': + dest_str = '{}...'.format(dest_str[:width-3]) + else: + dest_str = '...{}'.format(dest_str[-width+3:]) + + # Rebuild Source/Destination panes + tmux_update_pane( + pane_id=self.panes['Source'], + text='{BLUE}Source{CLEAR}\n{s}'.format( + s=source_str, **COLORS)) + self.panes['Destination'] = tmux_split_window( + percent=50, target_pane=self.panes['Source'], + text='{BLUE}Destination{CLEAR}\n{s}'.format( + s=dest_str, **COLORS)) + + if 'SMART' in self.panes: + # Calc SMART/ddrescue/Journal panes sizes + ratio = [12, 22, 4] + width, height = tmux_get_pane_size(pane_id=self.panes['Progress']) + height -= 2 + total = sum(ratio) + p_ratio = [int((x/total) * height) for x in ratio] + p_ratio[1] = height - p_ratio[0] - p_ratio[2] + + # Resize SMART/Journal panes + tmux_resize_pane(self.panes['SMART'], y=ratio[0]) + tmux_resize_pane(y=ratio[1]) + tmux_resize_pane(self.panes['Journal'], y=ratio[2]) + def get_smart_source(self): """Get source for SMART dispay.""" disk_path = self.source.path @@ -339,18 +461,15 @@ class RecoveryState(): def retry_all_passes(self): """Mark all passes as pending for all block-pairs.""" self.finished = False - for bp in self.block_pairs: - bp.pass_done = [False, False, False] - bp.status = ['Pending', 'Pending', 'Pending'] - bp.fix_status_strings() + for b_pair in self.block_pairs: + b_pair.pass_done = [False, False, False] + b_pair.status = ['Pending', 'Pending', 'Pending'] + b_pair.fix_status_strings() self.set_pass_num() def self_checks(self): """Run self-checks and update state values.""" cmd = ['findmnt', '--json', '--target', os.getcwd()] - map_allowed_fstypes = RECOMMENDED_FSTYPES.copy() - map_allowed_fstypes.extend(['cifs', 'ext2', 'vfat']) - map_allowed_fstypes.sort() json_data = get_json_from_command(cmd) # Abort if json_data is empty @@ -361,23 +480,24 @@ class RecoveryState(): # Avoid saving map to non-persistent filesystem fstype = json_data.get( 'filesystems', [{}])[0].get( - 'fstype', 'unknown') - if fstype not in map_allowed_fstypes: + 'fstype', 'unknown') + if fstype not in RECOMMENDED_MAP_FSTYPES: print_error( "Map isn't being saved to a recommended filesystem ({})".format( fstype.upper())) print_info('Recommended types are: {}'.format( - ' / '.join(map_allowed_fstypes).upper())) + ' / '.join(RECOMMENDED_MAP_FSTYPES).upper())) print_standard(' ') if not ask('Proceed anyways? (Strongly discouraged)'): raise GenericAbort() # Run BlockPair self checks and get total size self.total_size = 0 - for bp in self.block_pairs: - bp.self_check() - self.resumed |= bp.resumed - self.total_size += bp.size + for b_pair in self.block_pairs: + b_pair.self_check() + if b_pair.resumed: + self.resumed = True + self.total_size += b_pair.size def set_pass_num(self): """Set current pass based on all block-pair's progress.""" @@ -385,8 +505,8 @@ class RecoveryState(): for pass_num in (2, 1, 0): # Iterate backwards through passes pass_done = True - for bp in self.block_pairs: - pass_done &= bp.pass_done[pass_num] + for b_pair in self.block_pairs: + pass_done = pass_done and b_pair.pass_done[pass_num] if pass_done: # All block-pairs reported being done # Set to next pass, unless we're on the last pass (2) @@ -404,6 +524,34 @@ class RecoveryState(): elif self.current_pass == 2: self.current_pass_str = '3 "Scraping bad areas"' + def set_working_dir(self): + # pylint: disable=no-self-use + """Set working dir to MAP_DIR if possible. + + NOTE: This is to help ensure the map file + is saved to non-volatile storage.""" + map_dir = '{}/{}'.format(MAP_DIR, global_vars['Date-Time']) + + # Mount backup shares + mount_backup_shares(read_write=True) + + # Get MAP_DIR filesystem type + # NOTE: If the backup share fails to mount then this will + # likely be the type of / + cmd = [ + 'findmnt', + '--noheadings', + '--target', MAP_DIR, + '--output', 'FSTYPE', + ] + result = run_program(cmd, check=False, encoding='utf-8', errors='ingnore') + map_dir_type = result.stdout.strip().lower() + + # Change working dir if map_dir_type is acceptable + if map_dir_type in RECOMMENDED_MAP_FSTYPES: + os.makedirs(map_dir, exist_ok=True) + os.chdir(map_dir) + def update_etoc(self): """Search ddrescue output for the current EToC, returns str.""" now = datetime.datetime.now(tz=self.timezone) @@ -413,7 +561,7 @@ class RecoveryState(): # Just set to N/A (NOTE: this overrules the refresh rate below) self.etoc = 'N/A' return - elif 'In Progress' not in self.status: + if 'In Progress' not in self.status: # Don't update when EToC is hidden return if now.second % ETOC_REFRESH_RATE != 0: @@ -427,13 +575,14 @@ class RecoveryState(): # Capture main tmux pane try: text = tmux_capture_pane() - except Exception: + except Exception: # pylint: disable=broad-except # Ignore pass # Search for EToC delta matches = re.findall(r'remaining time:.*$', text, re.MULTILINE) if matches: + # pylint: disable=invalid-name r = REGEX_REMAINING_TIME.search(matches[-1]) if r.group('na'): self.etoc = 'N/A' @@ -450,7 +599,7 @@ class RecoveryState(): minutes=int(minutes), seconds=int(seconds), ) - except Exception: + except Exception: # pylint: disable=broad-except # Ignore and leave as raw string pass @@ -460,15 +609,16 @@ class RecoveryState(): now = datetime.datetime.now(tz=self.timezone) _etoc = now + etoc_delta self.etoc = _etoc.strftime('%Y-%m-%d %H:%M %Z') - except Exception: + except Exception: # pylint: disable=broad-except # Ignore and leave as current string pass def update_progress(self): + # pylint: disable=attribute-defined-outside-init """Update overall progress using block_pairs.""" self.rescued = 0 - for bp in self.block_pairs: - self.rescued += bp.rescued + for b_pair in self.block_pairs: + self.rescued += b_pair.rescued self.rescued_percent = (self.rescued / self.total_size) * 100 self.status_percent = get_formatted_status( label='Recovered:', data=self.rescued_percent) @@ -477,26 +627,6 @@ class RecoveryState(): # Functions -def build_outer_panes(state): - """Build top and side panes.""" - state.panes['Source'] = tmux_split_window( - behind=True, vertical=True, lines=2, - text='{BLUE}Source{CLEAR}'.format(**COLORS)) - state.panes['Started'] = tmux_split_window( - lines=SIDE_PANE_WIDTH, target_pane=state.panes['Source'], - text='{BLUE}Started{CLEAR}\n{s}'.format( - s=time.strftime("%Y-%m-%d %H:%M %Z"), - **COLORS)) - state.panes['Destination'] = tmux_split_window( - percent=50, target_pane=state.panes['Source'], - text='{BLUE}Destination{CLEAR}'.format(**COLORS)) - - # Side pane - update_sidepane(state) - state.panes['Progress'] = tmux_split_window( - lines=SIDE_PANE_WIDTH, watch=state.progress_out) - - def create_path_obj(path): """Create Dev, Dir, or Image obj based on path given.""" obj = None @@ -514,101 +644,16 @@ def create_path_obj(path): def double_confirm_clone(): """Display warning and get 2nd confirmation, returns bool.""" print_standard('\nSAFETY CHECK') - print_warning('All data will be DELETED from the ' - 'destination device and partition(s) listed above.') - print_warning('This is irreversible and will lead ' - 'to {CLEAR}{RED}DATA LOSS.'.format(**COLORS)) + print_warning( + 'All data will be DELETED from the ' + 'destination device and partition(s) listed above.' + ) + print_warning( + 'This is irreversible and will lead to {CLEAR}{RED}DATA LOSS.'.format( + **COLORS)) return ask('Asking again to confirm, is this correct?') -def fix_tmux_panes(state, forced=False): - """Fix pane sizes if the winodw has been resized.""" - needs_fixed = False - - # Check layout - for k, v in TMUX_LAYOUT.items(): - if not v.get('Check'): - # Not concerned with the size of this pane - continue - # Get target - target = None - if k != 'Current': - if k not in state.panes: - # Skip missing panes - continue - else: - target = state.panes[k] - - # Check pane size - x, y = tmux_get_pane_size(pane_id=target) - if v.get('x', False) and v['x'] != x: - needs_fixed = True - if v.get('y', False) and v['y'] != y: - needs_fixed = True - - # Bail? - if not needs_fixed and not forced: - return - - # Remove Destination pane (temporarily) - tmux_kill_pane(state.panes['Destination']) - - # Update layout - for k, v in TMUX_LAYOUT.items(): - # Get target - target = None - if k != 'Current': - if k not in state.panes: - # Skip missing panes - continue - else: - target = state.panes[k] - - # Resize pane - tmux_resize_pane(pane_id=target, **v) - - # Calc Source/Destination pane sizes - width, height = tmux_get_pane_size() - width = int(width / 2) - 1 - - # Update Source string - source_str = state.source.name - if len(source_str) > width: - source_str = '{}...'.format(source_str[:width-3]) - - # Update Destination string - dest_str = state.dest.name - if len(dest_str) > width: - if state.mode == 'clone': - dest_str = '{}...'.format(dest_str[:width-3]) - else: - dest_str = '...{}'.format(dest_str[-width+3:]) - - # Rebuild Source/Destination panes - tmux_update_pane( - pane_id=state.panes['Source'], - text='{BLUE}Source{CLEAR}\n{s}'.format( - s=source_str, **COLORS)) - state.panes['Destination'] = tmux_split_window( - percent=50, target_pane=state.panes['Source'], - text='{BLUE}Destination{CLEAR}\n{s}'.format( - s=dest_str, **COLORS)) - - if 'SMART' in state.panes: - # Calc SMART/ddrescue/Journal panes sizes - ratio = [12, 22, 4] - width, height = tmux_get_pane_size(pane_id=state.panes['Progress']) - height -= 2 - total = sum(ratio) - p_ratio = [int((x/total) * height) for x in ratio] - p_ratio[1] = height - p_ratio[0] - p_ratio[2] - - # Resize SMART/Journal panes - tmux_resize_pane(state.panes['SMART'], y=ratio[0]) - tmux_resize_pane(y=ratio[1]) - tmux_resize_pane(state.panes['Journal'], y=ratio[2]) - - def get_device_details(dev_path): """Get device details via lsblk, returns JSON dict.""" cmd = ['lsblk', '--json', '--output-all', '--paths', dev_path] @@ -677,22 +722,22 @@ def get_dir_report(dir_path): output.append('{BLUE}{label:<{width}}{line}{CLEAR}'.format( label='PATH', width=width, - line=line.replace('\n',''), + line=line.replace('\n', ''), **COLORS)) else: output.append('{path:<{width}}{line}'.format( path=dir_path, width=width, - line=line.replace('\n',''))) + line=line.replace('\n', ''))) # Done return '\n'.join(output) -def get_size_in_bytes(s): +def get_size_in_bytes(size): """Convert size string from lsblk string to bytes, returns int.""" - s = re.sub(r'(\d+\.?\d*)\s*([KMGTB])B?', r'\1 \2B', s, re.IGNORECASE) - return convert_to_bytes(s) + size = re.sub(r'(\d+\.?\d*)\s*([KMGTB])B?', r'\1 \2B', size, re.IGNORECASE) + return convert_to_bytes(size) def get_formatted_status(label, data): @@ -700,13 +745,15 @@ def get_formatted_status(label, data): data_width = SIDE_PANE_WIDTH - len(label) try: data_str = '{data:>{data_width}.2f} %'.format( - data=data, - data_width=data_width-2) + data=data, + data_width=data_width-2, + ) except ValueError: # Assuming non-numeric data data_str = '{data:>{data_width}}'.format( - data=data, - data_width=data_width) + data=data, + data_width=data_width, + ) status = '{label}{s_color}{data_str}{CLEAR}'.format( label=label, s_color=get_status_color(data), @@ -715,19 +762,19 @@ def get_formatted_status(label, data): return status -def get_status_color(s, t_success=99, t_warn=90): +def get_status_color(status, t_success=99, t_warn=90): """Get color based on status, returns str.""" color = COLORS['CLEAR'] p_recovered = -1 try: - p_recovered = float(s) + p_recovered = float(status) except ValueError: # Status is either in lists below or will default to red pass - if s in ('Pending',) or str(s)[-2:] in (' b', 'Kb', 'Mb', 'Gb', 'Tb'): + if status == 'Pending' or str(status)[-2:] in (' b', 'Kb', 'Mb', 'Gb', 'Tb'): color = COLORS['CLEAR'] - elif s in ('Skipped', 'Unknown'): + elif status in ('Skipped', 'Unknown'): color = COLORS['YELLOW'] elif p_recovered >= t_success: color = COLORS['GREEN'] @@ -742,9 +789,9 @@ def is_writable_dir(dir_obj): """Check if we have read-write-execute permissions, returns bool.""" is_ok = True path_st_mode = os.stat(dir_obj.path).st_mode - is_ok == is_ok and path_st_mode & stat.S_IRUSR - is_ok == is_ok and path_st_mode & stat.S_IWUSR - is_ok == is_ok and path_st_mode & stat.S_IXUSR + is_ok = is_ok and path_st_mode & stat.S_IRUSR + is_ok = is_ok and path_st_mode & stat.S_IWUSR + is_ok = is_ok and path_st_mode & stat.S_IXUSR return is_ok @@ -754,6 +801,7 @@ def is_writable_filesystem(dir_obj): def menu_ddrescue(source_path, dest_path, run_mode): + # pylint: disable=too-many-branches """ddrescue menu.""" source = None dest = None @@ -797,9 +845,8 @@ def menu_ddrescue(source_path, dest_path, run_mode): raise GenericAbort() # Main menu - clear_screen() - build_outer_panes(state) - fix_tmux_panes(state, forced=True) + state.build_outer_panes() + state.fix_tmux_panes(forced=True) menu_main(state) # Done @@ -808,6 +855,7 @@ def menu_ddrescue(source_path, dest_path, run_mode): def menu_main(state): + # pylint: disable=too-many-branches,too-many-statements """Main menu is used to set ddrescue settings.""" checkmark = '*' if 'DISPLAY' in global_vars['Env']: @@ -818,16 +866,15 @@ def menu_main(state): # Build menu main_options = [ {'Base Name': 'Auto continue (if recovery % over threshold)', - 'Enabled': True}, + 'Enabled': True}, {'Base Name': 'Retry (mark non-rescued sectors "non-tried")', - 'Enabled': False}, + 'Enabled': False}, {'Base Name': 'Reverse direction', 'Enabled': False}, ] actions = [ {'Name': 'Start', 'Letter': 'S'}, - {'Name': 'Change settings {YELLOW}(experts only){CLEAR}'.format( - **COLORS), - 'Letter': 'C'}, + {'Name': 'Change settings {YELLOW}(experts only){CLEAR}'.format(**COLORS), + 'Letter': 'C'}, {'Name': 'Quit', 'Letter': 'Q', 'CRLF': True}, ] @@ -858,13 +905,13 @@ def menu_main(state): elif selection == 'S': # Set settings for pass pass_settings = [] - for k, v in state.settings.items(): - if not v['Enabled']: + for option, option_data in state.settings.items(): + if not option_data['Enabled']: continue - if 'Value' in v: - pass_settings.append('{}={}'.format(k, v['Value'])) + if 'Value' in option_data: + pass_settings.append('{}={}'.format(option, option_data['Value'])) else: - pass_settings.append(k) + pass_settings.append(option) for opt in main_options: if 'Auto' in opt['Base Name']: auto_run = opt['Enabled'] @@ -887,7 +934,7 @@ def menu_main(state): state.current_pass_min() < AUTO_PASS_1_THRESHOLD): auto_run = False elif (state.current_pass == 1 and - state.current_pass_min() < AUTO_PASS_2_THRESHOLD): + state.current_pass_min() < AUTO_PASS_2_THRESHOLD): auto_run = False else: auto_run = False @@ -916,13 +963,15 @@ def menu_settings(state): # Build menu settings = [] - for k, v in sorted(state.settings.items()): - if not v.get('Hidden', False): - settings.append({'Base Name': k, 'Flag': k}) + for option, option_data in sorted(state.settings.items()): + if not option_data.get('Hidden', False): + settings.append({'Base Name': option, 'Flag': option}) actions = [{'Name': 'Main Menu', 'Letter': 'M'}] # Show menu while True: + # pylint: disable=invalid-name + # TODO: Clean up and/or replace with new menu-select function for s in settings: s['Name'] = '{}{}{}'.format( s['Base Name'], @@ -959,25 +1008,27 @@ def menu_settings(state): def read_map_file(map_path): """Read map file with ddrescuelog and return data as dict.""" - map_data = {'full recovery': False} + cmd = [ + 'ddrescuelog', + '--binary-prefixes', + '--show-status', + map_path, + ] + map_data = {'full recovery': False, 'pass completed': False} try: - result = run_program(['ddrescuelog', '-t', map_path]) + result = run_program(cmd, encoding='utf-8', errors='ignore') except CalledProcessError: # (Grossly) assuming map_data hasn't been saved yet, return empty dict return map_data # Parse output - for line in result.stdout.decode().splitlines(): - m = re.match( - r'^\s*(?P\S+):.*\(\s*(?P\d+\.?\d*)%.*', line.strip()) - if m: - try: - map_data[m.group('key')] = float(m.group('value')) - except ValueError: - raise GenericError('Failed to read map data') - m = re.match(r'.*current status:\s+(?P.*)', line.strip()) - if m: - map_data['pass completed'] = bool(m.group('status') == 'finished') + for line in result.stdout.splitlines(): + line = line.strip() + _r = REGEX_DDRESCUE_LOG.search(line) + if _r: + map_data[_r.group('key')] = convert_to_bytes('{size} {unit}B'.format( + **_r.groupdict())) + map_data['pass completed'] = 'current status: finished' in line # Check if 100% done try: @@ -991,6 +1042,7 @@ def read_map_file(map_path): def run_ddrescue(state, pass_settings): + # pylint: disable=too-many-branches,too-many-statements """Run ddrescue pass.""" return_code = -1 aborted = False @@ -1005,8 +1057,8 @@ def run_ddrescue(state, pass_settings): # Create SMART monitor pane state.smart_out = '{}/smart_{}.out'.format( global_vars['TmpDir'], state.smart_source.name) - with open(state.smart_out, 'w') as f: - f.write('Initializing...') + with open(state.smart_out, 'w') as _f: + _f.write('Initializing...') state.panes['SMART'] = tmux_split_window( behind=True, lines=12, vertical=True, watch=state.smart_out) @@ -1016,19 +1068,19 @@ def run_ddrescue(state, pass_settings): command=['sudo', 'journalctl', '-f']) # Fix layout - fix_tmux_panes(state, forced=True) + state.fix_tmux_panes(forced=True) # Run pass for each block-pair - for bp in state.block_pairs: - if bp.pass_done[state.current_pass]: + for b_pair in state.block_pairs: + if b_pair.pass_done[state.current_pass]: # Skip to next block-pair continue update_sidepane(state) # Set ddrescue cmd cmd = [ - 'ddrescue', *pass_settings, - bp.source_path, bp.dest_path, bp.map_path] + 'sudo', 'ddrescue', *pass_settings, + b_pair.source_path, b_pair.dest_path, b_pair.map_path] if state.mode == 'clone': cmd.append('--force') if state.current_pass == 0: @@ -1043,36 +1095,36 @@ def run_ddrescue(state, pass_settings): # Start ddrescue try: clear_screen() - print_info('Current dev: {}'.format(bp.source_path)) + print_info('Current dev: {}'.format(b_pair.source_path)) ddrescue_proc = popen_program(cmd) i = 0 while True: # Update SMART display (every 30 seconds) if i % 30 == 0: state.smart_source.get_smart_details() - with open(state.smart_out, 'w') as f: + with open(state.smart_out, 'w') as _f: report = state.smart_source.generate_attribute_report( - timestamp=True) + timestamp=True) for line in report: - f.write('{}\n'.format(line)) + _f.write('{}\n'.format(line)) i += 1 # Update progress - bp.update_progress(state.current_pass) + b_pair.update_progress(state.current_pass) update_sidepane(state) # Fix panes - fix_tmux_panes(state) + state.fix_tmux_panes() # Check if ddrescue has finished try: ddrescue_proc.wait(timeout=1) sleep(2) - bp.update_progress(state.current_pass) + b_pair.update_progress(state.current_pass) update_sidepane(state) break except subprocess.TimeoutExpired: - # Catch to update smart/bp/sidepane + # Catch to update smart/b_pair/sidepane pass except KeyboardInterrupt: @@ -1081,7 +1133,7 @@ def run_ddrescue(state, pass_settings): ddrescue_proc.wait(timeout=10) # Update progress/sidepane again - bp.update_progress(state.current_pass) + b_pair.update_progress(state.current_pass) update_sidepane(state) # Was ddrescue aborted? @@ -1103,7 +1155,7 @@ def run_ddrescue(state, pass_settings): break else: # Mark pass finished - bp.finish_pass(state.current_pass) + b_pair.finish_pass(state.current_pass) update_sidepane(state) # Done @@ -1119,6 +1171,8 @@ def run_ddrescue(state, pass_settings): def select_parts(source_device): + # pylint: disable=too-many-branches + # TODO: Clean up and/or replace with new menu-select function """Select partition(s) or whole device, returns list of DevObj()s.""" selected_parts = [] children = source_device.details.get('children', []) @@ -1180,24 +1234,26 @@ def select_parts(source_device): raise GenericAbort() # Build list of selected parts - for d in dev_options: - if d['Selected']: - d['Dev'].model = source_device.model - d['Dev'].model_size = source_device.model_size - d['Dev'].update_filename_prefix() - selected_parts.append(d['Dev']) + for _d in dev_options: + if _d['Selected']: + _d['Dev'].model = source_device.model + _d['Dev'].model_size = source_device.model_size + _d['Dev'].update_filename_prefix() + selected_parts.append(_d['Dev']) return selected_parts def select_path(skip_device=None): + # pylint: disable=too-many-branches,too-many-locals + # TODO: Clean up and/or replace with new menu-select function """Optionally mount local dev and select path, returns DirObj.""" - wd = os.path.realpath(global_vars['Env']['PWD']) + work_dir = os.path.realpath(global_vars['Env']['PWD']) selected_path = None # Build menu path_options = [ - {'Name': 'Current directory: {}'.format(wd), 'Path': wd}, + {'Name': 'Current directory: {}'.format(work_dir), 'Path': work_dir}, {'Name': 'Local device', 'Path': None}, {'Name': 'Enter manually', 'Path': None}] actions = [{'Name': 'Quit', 'Letter': 'Q'}] @@ -1212,9 +1268,9 @@ def select_path(skip_device=None): raise GenericAbort() elif selection.isnumeric(): index = int(selection) - 1 - if path_options[index]['Path'] == wd: + if path_options[index]['Path'] == work_dir: # Current directory - selected_path = DirObj(wd) + selected_path = DirObj(work_dir) elif path_options[index]['Name'] == 'Local device': # Local device @@ -1230,15 +1286,15 @@ def select_path(skip_device=None): # Select volume vol_options = [] - for k, v in sorted(report.items()): - disabled = v['show_data']['data'] == 'Failed to mount' + for _k, _v in sorted(report.items()): + disabled = _v['show_data']['data'] == 'Failed to mount' if disabled: - name = '{name} (Failed to mount)'.format(**v) + name = '{name} (Failed to mount)'.format(**_v) else: - name = '{name} (mounted on "{mount_point}")'.format(**v) + name = '{name} (mounted on "{mount_point}")'.format(**_v) vol_options.append({ 'Name': name, - 'Path': v['mount_point'], + 'Path': _v['mount_point'], 'Disabled': disabled}) selection = menu_select( title='Please select a volume', @@ -1313,15 +1369,17 @@ def select_device(description='device', skip_device=None): action_entries=actions, disabled_label='ALREADY SELECTED') + if selection == 'Q': + raise GenericAbort() + if selection.isnumeric(): return dev_options[int(selection)-1]['Dev'] - elif selection == 'Q': - raise GenericAbort() def setup_loopback_device(source_path): """Setup loopback device for source_path, returns dev_path as str.""" cmd = ( + 'sudo', 'losetup', '--find', '--partscan', @@ -1355,6 +1413,7 @@ def show_selection_details(state): def show_usage(script_name): + """Show usage.""" print_info('Usage:') print_standard(USAGE.format(script_name=script_name)) pause() @@ -1378,14 +1437,14 @@ def update_sidepane(state): output.append('─────────────────────') # Source(s) progress - for bp in state.block_pairs: + for b_pair in state.block_pairs: if state.source.is_image(): output.append('{BLUE}Image File{CLEAR}'.format(**COLORS)) else: output.append('{BLUE}{source}{CLEAR}'.format( - source=bp.source_path, + source=b_pair.source_path, **COLORS)) - output.extend(bp.status) + output.extend(b_pair.status) output.append(' ') # EToC @@ -1404,11 +1463,9 @@ def update_sidepane(state): # Add line-endings output = ['{}\n'.format(line) for line in output] - with open(state.progress_out, 'w') as f: - f.writelines(output) + with open(state.progress_out, 'w') as _f: + _f.writelines(output) if __name__ == '__main__': print("This file is not meant to be called directly.") - -# vim: sts=2 sw=2 ts=2 diff --git a/.bin/Scripts/functions/hw_diags.py b/.bin/Scripts/functions/hw_diags.py index bea5e01e..db5d01ca 100644 --- a/.bin/Scripts/functions/hw_diags.py +++ b/.bin/Scripts/functions/hw_diags.py @@ -36,6 +36,7 @@ class CpuObj(): self.tests = OrderedDict() self.get_details() self.name = self.lscpu.get('Model name', 'Unknown CPU') + self.description = self.name def get_details(self): """Get CPU details from lscpu.""" @@ -57,6 +58,13 @@ class CpuObj(): report.append('{BLUE}Device{CLEAR}'.format(**COLORS)) report.append(' {}'.format(self.name)) + # Include RAM details + ram_details = get_ram_details() + ram_total = human_readable_size(ram_details.pop('Total', 0)).strip() + ram_dimms = ['{}x {}'.format(v, k) for k, v in sorted(ram_details.items())] + report.append('{BLUE}RAM{CLEAR}'.format(**COLORS)) + report.append(' {} ({})'.format(ram_total, ', '.join(ram_dimms))) + # Tests for test in self.tests.values(): report.extend(test.report) @@ -186,8 +194,8 @@ class DiskObj(): disk_ok = False # Disable override if necessary - self.override_disabled |= ATTRIBUTES[attr_type][k].get( - 'Critical', False) + if ATTRIBUTES[attr_type][k].get('Critical', False): + self.override_disabled = True # SMART overall assessment ## NOTE: Only fail drives if the overall value exists and reports failed @@ -220,11 +228,12 @@ class DiskObj(): # Done return test_running - def disable_test(self, name, status): + def disable_test(self, name, status, test_failed=False): """Disable test by name and update status.""" if name in self.tests: self.tests[name].update_status(status) self.tests[name].disabled = True + self.tests[name].failed = test_failed def generate_attribute_report( self, description=False, timestamp=False): @@ -307,6 +316,11 @@ class DiskObj(): attr_type=self.attr_type, **COLORS)) report.extend(sorted(self.nvme_smart_notes.keys())) + # 4K alignment check + if not self.is_4k_aligned(): + report.append('{YELLOW}Warning{CLEAR}'.format(**COLORS)) + report.append(' One or more partitions are not 4K aligned') + # Tests for test in self.tests.values(): report.extend(test.report) @@ -410,6 +424,26 @@ class DiskObj(): 'self_test', {}).get( k, {}) + def is_4k_aligned(self): + """Check if partitions are 4K aligned, returns bool.""" + cmd = [ + 'sudo', + 'sfdisk', + '--json', + self.path, + ] + aligned = True + + # Get partition details + json_data = get_json_from_command(cmd) + + # Check partitions + for part in json_data.get('partitiontable', {}).get('partitions', []): + aligned = aligned and part.get('start', -1) % 4096 == 0 + + # Done + return aligned + def safety_check(self, silent=False): """Run safety checks and disable tests if necessary.""" test_running = False @@ -454,7 +488,6 @@ class DiskObj(): disk_ok = OVERRIDES_FORCED or ask('Run tests on this device anyway?') print_standard(' ') - # Disable tests if necessary (statuses won't be overwritten) if test_running: if not silent: @@ -463,7 +496,7 @@ class DiskObj(): for t in ['badblocks', 'I/O Benchmark']: self.disable_test(t, 'Denied') elif not disk_ok: - self.disable_test('NVMe / SMART', 'NS') + self.disable_test('NVMe / SMART', 'NS', test_failed=True) for t in ['badblocks', 'I/O Benchmark']: self.disable_test(t, 'Denied') @@ -471,6 +504,7 @@ class DiskObj(): class State(): """Object to track device objects and overall state.""" def __init__(self): + self.args = None self.cpu = None self.disks = [] self.panes = {} @@ -498,6 +532,83 @@ class State(): }, }) + def build_outer_panes(self): + """Build top and side panes.""" + clear_screen() + + # Top + self.panes['Top'] = tmux_split_window( + behind=True, lines=2, vertical=True, + text=TOP_PANE_TEXT) + + # Started + self.panes['Started'] = tmux_split_window( + lines=SIDE_PANE_WIDTH, target_pane=self.panes['Top'], + text='{BLUE}Started{CLEAR}\n{s}'.format( + s=time.strftime("%Y-%m-%d %H:%M %Z"), + **COLORS)) + + # Progress + self.panes['Progress'] = tmux_split_window( + lines=SIDE_PANE_WIDTH, + watch=self.progress_out) + + def fix_tmux_panes(self): + """Fix pane sizes if the window has been resized.""" + needs_fixed = False + + # Bail? + if not self.panes: + return + + # Check layout + for k, v in self.tmux_layout.items(): + if not v.get('Check'): + # Not concerned with the size of this pane + continue + # Get target + target = None + if k != 'Current': + if k not in self.panes: + # Skip missing panes + continue + else: + target = self.panes[k] + + # Check pane size + x, y = tmux_get_pane_size(pane_id=target) + if v.get('x', False) and v['x'] != x: + needs_fixed = True + if v.get('y', False) and v['y'] != y: + needs_fixed = True + + # Bail? + if not needs_fixed: + return + + # Update layout + for k, v in self.tmux_layout.items(): + # Get target + target = None + if k != 'Current': + if k not in self.panes: + # Skip missing panes + continue + else: + target = self.panes[k] + + # Resize pane + tmux_resize_pane(pane_id=target, **v) + + def fix_tmux_panes_loop(self): + while True: + try: + self.fix_tmux_panes() + sleep(1) + except RuntimeError: + # Assuming layout definitions changes mid-run, ignoring + pass + def init(self): """Remove test objects, set log, and add devices.""" self.disks = [] @@ -505,14 +616,18 @@ class State(): v['Objects'] = [] # Update LogDir - if not self.quick_mode: + if self.quick_mode: + global_vars['LogDir'] = '{}/Logs/{}'.format( + global_vars['Env']['HOME'], + time.strftime('%Y-%m-%d_%H%M_%z')) + else: global_vars['LogDir'] = '{}/Logs/{}_{}'.format( global_vars['Env']['HOME'], get_ticket_number(), time.strftime('%Y-%m-%d_%H%M_%z')) - os.makedirs(global_vars['LogDir'], exist_ok=True) - global_vars['LogFile'] = '{}/Hardware Diagnostics.log'.format( - global_vars['LogDir']) + os.makedirs(global_vars['LogDir'], exist_ok=True) + global_vars['LogFile'] = '{}/Hardware Diagnostics.log'.format( + global_vars['LogDir']) self.progress_out = '{}/progress.out'.format(global_vars['LogDir']) # Add CPU @@ -541,7 +656,13 @@ class State(): # Start tmux thread self.tmux_layout = TMUX_LAYOUT.copy() - start_thread(fix_tmux_panes_loop, args=[self]) + start_thread(self.fix_tmux_panes_loop) + + def set_top_pane_text(self, text): + """Set top pane text using TOP_PANE_TEXT and provided text.""" + tmux_update_pane( + self.panes['Top'], + text='{}\n{}'.format(TOP_PANE_TEXT, text)) class TestObj(): @@ -576,28 +697,6 @@ class TestObj(): # Functions -def build_outer_panes(state): - """Build top and side panes.""" - clear_screen() - - # Top - state.panes['Top'] = tmux_split_window( - behind=True, lines=2, vertical=True, - text=TOP_PANE_TEXT) - - # Started - state.panes['Started'] = tmux_split_window( - lines=SIDE_PANE_WIDTH, target_pane=state.panes['Top'], - text='{BLUE}Started{CLEAR}\n{s}'.format( - s=time.strftime("%Y-%m-%d %H:%M %Z"), - **COLORS)) - - # Progress - state.panes['Progress'] = tmux_split_window( - lines=SIDE_PANE_WIDTH, - watch=state.progress_out) - - def build_status_string(label, status, info_label=False): """Build status string with appropriate colors.""" status_color = COLORS['CLEAR'] @@ -614,64 +713,6 @@ def build_status_string(label, status, info_label=False): **COLORS) -def fix_tmux_panes_loop(state): - while True: - try: - fix_tmux_panes(state) - sleep(1) - except RuntimeError: - # Assuming layout definitions changes mid-run, ignoring - pass - - -def fix_tmux_panes(state): - """Fix pane sizes if the window has been resized.""" - needs_fixed = False - - # Bail? - if not state.panes: - return - - # Check layout - for k, v in state.tmux_layout.items(): - if not v.get('Check'): - # Not concerned with the size of this pane - continue - # Get target - target = None - if k != 'Current': - if k not in state.panes: - # Skip missing panes - continue - else: - target = state.panes[k] - - # Check pane size - x, y = tmux_get_pane_size(pane_id=target) - if v.get('x', False) and v['x'] != x: - needs_fixed = True - if v.get('y', False) and v['y'] != y: - needs_fixed = True - - # Bail? - if not needs_fixed: - return - - # Update layout - for k, v in state.tmux_layout.items(): - # Get target - target = None - if k != 'Current': - if k not in state.panes: - # Skip missing panes - continue - else: - target = state.panes[k] - - # Resize pane - tmux_resize_pane(pane_id=target, **v) - - def generate_horizontal_graph(rates, oneline=False): """Generate horizontal graph from rates, returns list.""" graph = ['', '', '', ''] @@ -731,6 +772,44 @@ def get_graph_step(rate, scale=16): return step +def get_ram_details(): + """Get RAM details via dmidecode, returns dict.""" + cmd = ['sudo', 'dmidecode', '--type', 'memory'] + manufacturer = 'UNKNOWN' + ram_details = {'Total': 0} + size = 0 + + # Get DMI data + result = run_program(cmd, encoding='utf-8', errors='ignore') + dmi_data = result.stdout.splitlines() + + # Parse data + for line in dmi_data: + line = line.strip() + if line == 'Memory Device': + # Reset vars + manufacturer = 'UNKNOWN' + size = 0 + elif line.startswith('Size:'): + size = convert_to_bytes(line.replace('Size: ', '')) + elif line.startswith('Manufacturer:'): + manufacturer = line.replace('Manufacturer: ', '') + if size > 0: + # Add RAM to list if slot populated + ram_str = '{} {}'.format( + human_readable_size(size).strip(), + manufacturer, + ) + ram_details['Total'] += size + if ram_str in ram_details: + ram_details[ram_str] += 1 + else: + ram_details[ram_str] = 1 + + # Done + return ram_details + + def get_read_rate(s): """Get read rate in bytes/s from dd progress output.""" real_rate = None @@ -743,6 +822,7 @@ def get_read_rate(s): def menu_diags(state, args): """Main menu to select and run HW tests.""" args = [a.lower() for a in args] + state.args = args checkmark = '*' if 'DISPLAY' in global_vars['Env']: checkmark = '✓' @@ -788,7 +868,7 @@ def menu_diags(state, args): # If so, verify no other tests are enabled and set quick_mode state.quick_mode = True for opt in main_options[3:4] + main_options[5:]: - state.quick_mode &= not opt['Enabled'] + state.quick_mode = state.quick_mode and not opt['Enabled'] else: state.quick_mode = False @@ -884,10 +964,7 @@ def run_badblocks_test(state, test): update_progress_pane(state) # Update tmux layout - tmux_update_pane( - state.panes['Top'], - text='{}\n{}'.format( - TOP_PANE_TEXT, dev.description)) + state.set_top_pane_text(dev.description) # Create monitor pane test.badblocks_out = '{}/badblocks_{}.out'.format( @@ -970,10 +1047,11 @@ def run_hw_tests(state): """Run enabled hardware tests.""" print_standard('Scanning devices...') state.init() + tests_enabled = False # Build Panes update_progress_pane(state) - build_outer_panes(state) + state.build_outer_panes() # Show selected tests and create TestObj()s print_info('Selected Tests:') @@ -985,6 +1063,8 @@ def run_hw_tests(state): COLORS['CLEAR'], QUICK_LABEL if state.quick_mode and 'NVMe' in k else '')) if v['Enabled']: + tests_enabled = True + # Create TestObj and track under both CpuObj/DiskObj and State if k in TESTS_CPU: test_obj = TestObj( @@ -998,10 +1078,16 @@ def run_hw_tests(state): v['Objects'].append(test_obj) print_standard('') + # Bail if no tests selected + if not tests_enabled: + tmux_kill_pane(*state.panes.values()) + return + # Run disk safety checks (if necessary) _disk_tests_enabled = False for k in TESTS_DISK: - _disk_tests_enabled |= state.tests[k]['Enabled'] + if state.tests[k]['Enabled']: + _disk_tests_enabled = True if _disk_tests_enabled: for disk in state.disks: try: @@ -1039,7 +1125,7 @@ def run_hw_tests(state): # Rebuild panes update_progress_pane(state) - build_outer_panes(state) + state.build_outer_panes() # Mark unfinished tests as aborted for k, v in state.tests.items(): @@ -1051,8 +1137,22 @@ def run_hw_tests(state): # Update side pane update_progress_pane(state) - # Done + # Show results show_results(state) + + # Upload for review + if ENABLED_UPLOAD_DATA and ask('Upload results for review?'): + try_and_print( + message='Saving debug reports...', + function=save_debug_reports, + state=state, global_vars=global_vars) + try_and_print( + message='Uploading Data...', + function=upload_logdir, + global_vars=global_vars, + reason='Review') + + # Done sleep(1) if state.quick_mode: pause('Press Enter to exit... ') @@ -1079,10 +1179,7 @@ def run_io_benchmark(state, test): update_progress_pane(state) # Update tmux layout - tmux_update_pane( - state.panes['Top'], - text='{}\n{}'.format( - TOP_PANE_TEXT, dev.description)) + state.set_top_pane_text(dev.description) state.tmux_layout['Current'] = {'y': 15, 'Check': True} # Create monitor pane @@ -1241,9 +1338,7 @@ def run_mprime_test(state, test): test.thermal_abort = False # Update tmux layout - tmux_update_pane( - state.panes['Top'], - text='{}\n{}'.format(TOP_PANE_TEXT, dev.name)) + state.set_top_pane_text(dev.name) # Start live sensor monitor test.sensors_out = '{}/sensors.out'.format(global_vars['TmpDir']) @@ -1406,7 +1501,7 @@ def run_mprime_test(state, test): # Add temps to report test.report.append('{BLUE}Temps{CLEAR}'.format(**COLORS)) for line in generate_sensor_report( - test.sensor_data, 'Idle', 'Max', 'Cooldown', core_only=True): + test.sensor_data, 'Idle', 'Max', 'Cooldown', cpu_only=True): test.report.append(' {}'.format(line)) # Add abort message(s) @@ -1456,10 +1551,7 @@ def run_nvme_smart_tests(state, test, update_mode=False): update_progress_pane(state) # Update tmux layout - tmux_update_pane( - state.panes['Top'], - text='{}\n{}'.format( - TOP_PANE_TEXT, dev.description)) + state.set_top_pane_text(dev.description) # SMART short self-test if dev.smart_attributes and not (state.quick_mode or update_mode): @@ -1604,14 +1696,13 @@ def show_report(report, log_report=False): def show_results(state): """Show results for all tests.""" clear_screen() - tmux_update_pane( - state.panes['Top'], - text='{}\nResults'.format(TOP_PANE_TEXT)) + state.set_top_pane_text('Results') # CPU tests _enabled = False for k in TESTS_CPU: - _enabled |= state.tests[k]['Enabled'] + if state.tests[k]['Enabled']: + _enabled = True if _enabled: print_success('CPU:'.format(k)) show_report(state.cpu.generate_cpu_report(), log_report=True) @@ -1620,7 +1711,8 @@ def show_results(state): # Disk tests _enabled = False for k in TESTS_DISK: - _enabled |= state.tests[k]['Enabled'] + if state.tests[k]['Enabled']: + _enabled = True if _enabled: print_success('Disk{}:'.format( '' if len(state.disks) == 1 else 's')) @@ -1634,17 +1726,6 @@ def show_results(state): # Update progress update_progress_pane(state) - # Ask for review - if ENABLED_UPLOAD_DATA and ask('Upload results for review?'): - try_and_print( - message='Saving debug reports...', - function=save_debug_reports, - state=state, global_vars=global_vars) - try_and_print( - message='Uploading Data...', - function=upload_logdir, - global_vars=global_vars) - def update_main_options(state, selection, main_options): """Update menu and state based on selection.""" diff --git a/.bin/Scripts/functions/info.py b/.bin/Scripts/functions/info.py index 84d92663..b1959090 100644 --- a/.bin/Scripts/functions/info.py +++ b/.bin/Scripts/functions/info.py @@ -95,7 +95,7 @@ def get_installed_antivirus(): out = out.stdout.decode().strip() state = out.split('=')[1] state = hex(int(state)) - if str(state)[3:5] != '10': + if str(state)[3:5] not in ['10', '11']: programs.append('[Disabled] {}'.format(prod)) else: programs.append(prod) @@ -446,16 +446,19 @@ def show_os_name(): def show_temp_files_size(): """Show total size of temp files identified by BleachBit.""" - size = None + size_str = None + total = 0 with open(r'{LogDir}\Tools\BleachBit.log'.format(**global_vars), 'r') as f: for line in f.readlines(): - if re.search(r'^disk space to be recovered:', line, re.IGNORECASE): + if re.search(r'^Disk space (to be |)recovered:', line, re.IGNORECASE): size = re.sub(r'.*: ', '', line.strip()) size = re.sub(r'(\w)iB$', r' \1b', size) - if size is None: - print_warning(size, timestamp=False) + total += convert_to_bytes(size) + size_str = human_readable_size(total, decimals=1) + if size_str is None: + print_warning('UNKNOWN', timestamp=False) else: - print_standard(size, timestamp=False) + print_standard(size_str, timestamp=False) def show_user_data_summary(indent=8, width=32): diff --git a/.bin/Scripts/functions/sensors.py b/.bin/Scripts/functions/sensors.py index 993306bd..49a7472c 100644 --- a/.bin/Scripts/functions/sensors.py +++ b/.bin/Scripts/functions/sensors.py @@ -1,4 +1,6 @@ -# Wizard Kit: Functions - Sensors +'''Wizard Kit: Functions - Sensors''' +# pylint: disable=no-name-in-module,wildcard-import +# vim: sts=2 sw=2 ts=2 import json import re @@ -9,7 +11,7 @@ from settings.sensors import * # Error Classes class ThermalLimitReachedError(Exception): - pass + '''Thermal limit reached error.''' def clear_temps(sensor_data): @@ -20,28 +22,30 @@ def clear_temps(sensor_data): _data['Temps'] = [] -def fix_sensor_str(s): +def fix_sensor_str(_s): """Cleanup string and return str.""" - s = re.sub(r'^(\w+)-(\w+)-(\w+)', r'\1 (\2 \3)', s, re.IGNORECASE) - s = s.title() - s = s.replace('Coretemp', 'CoreTemp') - s = s.replace('Acpi', 'ACPI') - s = s.replace('ACPItz', 'ACPI TZ') - s = s.replace('Isa ', 'ISA ') - s = s.replace('Id ', 'ID ') - s = re.sub(r'(\D+)(\d+)', r'\1 \2', s, re.IGNORECASE) - s = s.replace(' ', ' ') - return s + _s = re.sub(r'^(\w+)-(\w+)-(\w+)', r'\1 (\2 \3)', _s, re.IGNORECASE) + _s = _s.title() + _s = _s.replace('Coretemp', 'CPUTemp') + _s = _s.replace('Acpi', 'ACPI') + _s = _s.replace('ACPItz', 'ACPI TZ') + _s = _s.replace('Isa ', 'ISA ') + _s = _s.replace('Pci ', 'PCI ') + _s = _s.replace('Id ', 'ID ') + _s = re.sub(r'(\D+)(\d+)', r'\1 \2', _s, re.IGNORECASE) + _s = re.sub(r'^K (\d+)Temp', r'AMD K\1 Temps', _s, re.IGNORECASE) + _s = re.sub(r'T(ctl|die)', r'CPU (T\1)', _s, re.IGNORECASE) + return _s def generate_sensor_report( sensor_data, *temp_labels, - colors=True, core_only=False): + colors=True, cpu_only=False): """Generate report based on temp_labels, returns list if str.""" report = [] for _section, _adapters in sorted(sensor_data.items()): - # CoreTemps then Other temps - if core_only and 'Core' not in _section: + # CPU temps then Other temps + if cpu_only and 'CPU' not in _section: continue for _adapter, _sources in sorted(_adapters.items()): # Adapter @@ -56,7 +60,7 @@ def generate_sensor_report( ': ' if _label != 'Current' else '', get_temp_str(_data.get(_label, '???'), colors=colors)) report.append(_line) - if not core_only: + if not cpu_only: report.append(' ') # Handle empty reports (i.e. no sensors detected) @@ -91,17 +95,17 @@ def get_colored_temp_str(temp): else: color = COLORS['CLEAR'] return '{color}{prefix}{temp:2.0f}°C{CLEAR}'.format( - color = color, - prefix = '-' if temp < 0 else '', - temp = temp, + color=color, + prefix='-' if temp < 0 else '', + temp=temp, **COLORS) def get_raw_sensor_data(): """Read sensor data and return dict.""" - data = {} + json_data = {} cmd = ['sensors', '-j'] - + # Get raw data try: result = run_program(cmd) @@ -122,8 +126,8 @@ def get_raw_sensor_data(): try: json_data = json.loads('\n'.join(raw_data)) except json.JSONDecodeError: - # Still broken, just set to empty dict - json_data = {} + # Still broken, just return the empty dict + pass # Done return json_data @@ -132,10 +136,10 @@ def get_raw_sensor_data(): def get_sensor_data(): """Parse raw sensor data and return new dict.""" json_data = get_raw_sensor_data() - sensor_data = {'CoreTemps': {}, 'Other': {}} + sensor_data = {'CPUTemps': {}, 'Other': {}} for _adapter, _sources in json_data.items(): - if 'coretemp' in _adapter: - _section = 'CoreTemps' + if is_cpu_adapter(_adapter): + _section = 'CPUTemps' else: _section = 'Other' sensor_data[_section][_adapter] = {} @@ -157,8 +161,8 @@ def get_sensor_data(): } # Remove empty sections - for k, v in sensor_data.items(): - v = {k2: v2 for k2, v2 in v.items() if v2} + for _k, _v in sensor_data.items(): + _v = {_k2: _v2 for _k2, _v2 in _v.items() if _v2} # Done return sensor_data @@ -178,14 +182,20 @@ def get_temp_str(temp, colors=True): temp) +def is_cpu_adapter(adapter): + """Checks if adapter is a known CPU adapter, returns bool.""" + is_cpu = re.search(r'(core|k\d+)temp', adapter, re.IGNORECASE) + return bool(is_cpu) + + def monitor_sensors(monitor_pane, monitor_file): """Continually update sensor data and report to screen.""" sensor_data = get_sensor_data() while True: update_sensor_data(sensor_data) - with open(monitor_file, 'w') as f: + with open(monitor_file, 'w') as _f: report = generate_sensor_report(sensor_data, 'Current', 'Max') - f.write('\n'.join(report)) + _f.write('\n'.join(report)) sleep(1) if monitor_pane and not tmux_poll_pane(monitor_pane): break @@ -196,7 +206,7 @@ def save_average_temp(sensor_data, temp_label, seconds=10): clear_temps(sensor_data) # Get temps - for i in range(seconds): + for _i in range(seconds): # pylint: disable=unused-variable update_sensor_data(sensor_data) sleep(1) @@ -219,24 +229,15 @@ def update_sensor_data(sensor_data, thermal_limit=None): _data['Current'] = _temp _data['Max'] = max(_temp, _data['Max']) _data['Temps'].append(_temp) - except Exception: + except Exception: # pylint: disable=broad-except # Dumb workound for Dell sensors with changing source names pass # Check if thermal limit reached - if thermal_limit and _section == 'CoreTemps': + if thermal_limit and _section == 'CPUTemps': if max(_data['Current'], _data['Max']) >= thermal_limit: - raise ThermalLimitReachedError('CoreTemps reached limit') - - -def join_columns(column1, column2, width=55): - return '{:<{}}{}'.format( - column1, - 55+len(column1)-len(REGEX_COLORS.sub('', column1)), - column2) + raise ThermalLimitReachedError('CPU temps reached limit') if __name__ == '__main__': print("This file is not meant to be called directly.") - -# vim: sts=2 sw=2 ts=2 diff --git a/.bin/Scripts/functions/setup.py b/.bin/Scripts/functions/setup.py index 05a9dca3..f9f864e9 100644 --- a/.bin/Scripts/functions/setup.py +++ b/.bin/Scripts/functions/setup.py @@ -1,7 +1,10 @@ # Wizard Kit: Functions - Setup +from functions.browsers import * +from functions.json import * from functions.update import * from settings.setup import * +from settings.sources import * # Configuration @@ -63,9 +66,13 @@ def config_explorer_system(): write_registry_settings(SETTINGS_EXPLORER_SYSTEM, all_users=True) -def config_explorer_user(): - """Configure Windows Explorer for current user.""" - write_registry_settings(SETTINGS_EXPLORER_USER, all_users=False) +def config_explorer_user(setup_mode='All'): + """Configure Windows Explorer for current user per setup_mode.""" + settings_explorer_user = { + k: v for k, v in SETTINGS_EXPLORER_USER.items() + if setup_mode not in v.get('Invalid modes', []) + } + write_registry_settings(settings_explorer_user, all_users=False) def config_windows_updates(): @@ -75,7 +82,7 @@ def config_windows_updates(): def update_clock(): """Set Timezone and sync clock.""" - run_program(['tzutil' ,'/s', WINDOWS_TIME_ZONE], check=False) + run_program(['tzutil', '/s', WINDOWS_TIME_ZONE], check=False) run_program(['net', 'stop', 'w32ime'], check=False) run_program( ['w32tm', '/config', '/syncfromflags:manual', @@ -107,6 +114,39 @@ def write_registry_settings(settings, all_users=False): # Installations +def find_current_software(): + """Find currently installed software, returns list.""" + ninite_extras_path = r'{BaseDir}\Installers\Extras'.format(**global_vars) + installers = [] + + # Browsers + scan_for_browsers(silent=True) + for browser in ('Google Chrome', 'Mozilla Firefox', 'Opera Chromium'): + if is_installed(browser): + installers.append( + r'{}\Web Browsers\{}.exe'.format(ninite_extras_path, browser)) + + # TODO: Add more sections + + return installers + +def find_missing_software(): + """Find missing software based on dirs/files present, returns list.""" + ninite_extras_path = r'{BaseDir}\Installers\Extras'.format(**global_vars) + installers = [] + + # Browsers + scan_for_browsers(silent=True) + for browser in ('Google Chrome', 'Mozilla Firefox', 'Opera Chromium'): + if profile_present(browser): + installers.append( + r'{}\Web Browsers\{}.exe'.format(ninite_extras_path, browser)) + + # TODO: Add more sections + + return installers + + def install_adobe_reader(): """Install Adobe Reader.""" cmd = [ @@ -159,29 +199,115 @@ def install_firefox_extensions(): run_program(cmd) -def install_ninite_bundle(mse=False, libreoffice=False): +def install_libreoffice( + quickstart=True, register_mso_types=True, + use_mso_formats=False, vcredist=False): + """Install LibreOffice using specified settings.""" + cmd = [ + 'msiexec', '/passive', '/norestart', + '/i', r'{}\Installers\Extras\Office\LibreOffice.msi'.format( + global_vars['BaseDir']), + 'REBOOTYESNO=No', + 'ISCHECKFORPRODUCTUPDATES=0', + 'QUICKSTART={}'.format(1 if quickstart else 0), + 'UI_LANGS=en_US', + 'VC_REDIST={}'.format(1 if vcredist else 0), + ] + if register_mso_types: + cmd.append('REGISTER_ALL_MSO_TYPES=1') + else: + cmd.append('REGISTER_NO_MSO_TYPES=1') + xcu_dir = r'{APPDATA}\LibreOffice\4\user'.format(**global_vars['Env']) + xcu_file = r'{}\registrymodifications.xcu'.format(xcu_dir) + + # Set default save format + if use_mso_formats and not os.path.exists(xcu_file): + os.makedirs(xcu_dir, exist_ok=True) + with open(xcu_file, 'w', encoding='utf-8', newline='\n') as f: + f.write(LIBREOFFICE_XCU_DATA) + + # Install LibreOffice + run_program(cmd, check=True) + +def install_ninite_bundle( + # pylint: disable=too-many-arguments,too-many-branches + base=True, + browsers_only=False, + libreoffice=False, + missing=False, + mse=False, + standard=True, + ): """Run Ninite installer(s), returns list of Popen objects.""" popen_objects = [] - if global_vars['OS']['Version'] in ('8', '8.1', '10'): - # Modern selection - popen_objects.append( - popen_program(r'{BaseDir}\Installers\Extras\Bundles\Modern.exe'.format( - **global_vars))) - else: - # Legacy selection - if mse: - cmd = r'{BaseDir}\Installers\Extras\Security'.format(**global_vars) - cmd += r'\Microsoft Security Essentials.exe' - popen_objects.append(popen_program(cmd)) - popen_objects.append( - popen_program(r'{BaseDir}\Installers\Extras\Bundles\Legacy.exe'.format( - **global_vars))) + if browsers_only: + # This option is deprecated + installer_path = r'{BaseDir}\Installers\Extras\Web Browsers'.format( + **global_vars) + scan_for_browsers(silent=True) + for browser in ('Google Chrome', 'Mozilla Firefox', 'Opera Chromium'): + if is_installed(browser): + cmd = r'{}\{}.exe'.format(installer_path, browser) + popen_objects.append(popen_program(cmd)) + + # Bail + return popen_objects + + # Main selections + main_selections = [] + if base: + main_selections.append('base') + if standard: + if global_vars['OS']['Version'] in ('8', '8.1', '10'): + main_selections.append('standard') + else: + main_selections.append('standard7') + if main_selections: + # Only run if base and/or standard are enabled + cmd = r'{}\Installers\Extras\Bundles\{}.exe'.format( + global_vars['BaseDir'], + '-'.join(main_selections), + ) + popen_objects.append(popen_program([cmd])) + + # Extra selections + extra_selections = {} + for cmd in find_current_software(): + extra_selections[cmd] = True + if missing: + for cmd in find_missing_software(): + extra_selections[cmd] = True + + # Remove overlapping selections + regex = [] + for n_name, n_group in NINITE_REGEX.items(): + if n_name in main_selections: + regex.extend(n_group) + regex = '({})'.format('|'.join(regex)) + extra_selections = { + cmd: True for cmd in extra_selections + if not re.search(regex, cmd, re.IGNORECASE) + } + + # Start extra selections + for cmd in extra_selections: + popen_objects.append(popen_program([cmd])) + + # Microsoft Security Essentials + if mse: + cmd = r'{}\Installers\Extras\Security\{}'.format( + global_vars['BaseDir'], + 'Microsoft Security Essentials.exe', + ) + popen_objects.append(popen_program([cmd])) # LibreOffice if libreoffice: - cmd = r'{BaseDir}\Installers\Extras\Office'.format(**global_vars) - cmd += r'\LibreOffice.exe' - popen_objects.append(popen_program(cmd)) + cmd = r'{}\Installers\Extras\Office\{}'.format( + global_vars['BaseDir'], + 'LibreOffice.exe', + ) + popen_objects.append(popen_program([cmd])) # Done return popen_objects @@ -208,6 +334,10 @@ def open_device_manager(): popen_program(['mmc', 'devmgmt.msc']) +def open_speedtest(): + popen_program(['start', '', 'https://fast.com'], shell=True) + + def open_windows_activation(): popen_program(['slui']) diff --git a/.bin/Scripts/functions/sw_diags.py b/.bin/Scripts/functions/sw_diags.py index 1b965766..1c5b943f 100644 --- a/.bin/Scripts/functions/sw_diags.py +++ b/.bin/Scripts/functions/sw_diags.py @@ -6,6 +6,35 @@ from functions.common import * from settings.sw_diags import * +def check_4k_alignment(show_alert=False): + """Check that all partitions are 4K aligned.""" + aligned = True + cmd = ['WMIC', 'partition', 'get', 'StartingOffset'] + offsets = [] + + # Get offsets + result = run_program(cmd, encoding='utf-8', errors='ignore', check=False) + offsets = result.stdout.splitlines() + + # Check offsets + for off in offsets: + off = off.strip() + if not off.isnumeric(): + # Skip + continue + + try: + aligned = aligned and int(off) % 4096 == 0 + except ValueError: + # Ignore, this check is low priority + pass + + # Show alert + if show_alert: + show_alert_box('One or more partitions are not 4K aligned') + raise Not4KAlignedError + + def check_connection(): """Check if the system is online and optionally abort the script.""" while True: @@ -19,6 +48,37 @@ def check_connection(): abort() +def check_os_support_status(): + """Check if current OS is supported.""" + msg = '' + outdated = False + unsupported = False + + # Check OS version/notes + os_info = global_vars['OS'].copy() + if os_info['Notes'] == 'unsupported': + msg = 'The installed version of Windows is no longer supported' + unsupported = True + elif os_info['Notes'] == 'preview build': + msg = 'Preview builds are not officially supported' + unsupported = True + elif os_info['Version'] == '10' and os_info['Notes'] == 'outdated': + msg = 'The installed version of Windows is outdated' + outdated = True + if 'Preview' not in msg: + msg += '\n\nPlease consider upgrading before continuing setup.' + + # Show alert + if outdated or unsupported: + show_alert_box(msg) + + # Raise exception if necessary + if outdated: + raise WindowsOutdatedError + if unsupported: + raise WindowsUnsupportedError + + def check_secure_boot_status(show_alert=False): """Checks UEFI Secure Boot status via PowerShell.""" boot_mode = get_boot_mode() @@ -81,33 +141,6 @@ def get_boot_mode(): return type_str -def os_is_unsupported(show_alert=False): - """Checks if the current OS is unsupported, returns bool.""" - msg = '' - unsupported = False - - # Check OS version/notes - os_info = global_vars['OS'].copy() - if os_info['Notes'] == 'unsupported': - msg = 'The installed version of Windows is no longer supported' - unsupported = True - elif os_info['Notes'] == 'preview build': - msg = 'Preview builds are not officially supported' - unsupported = True - elif os_info['Version'] == '10' and os_info['Notes'] == 'outdated': - msg = 'The installed version of Windows is outdated' - unsupported = True - if 'Preview' not in msg: - msg += '\n\nPlease consider upgrading before continuing setup.' - - # Show alert - if unsupported and show_alert: - show_alert_box(msg) - - # Done - return unsupported - - def run_autoruns(): """Run AutoRuns in the background with VirusTotal checks enabled.""" extract_item('Autoruns', filter='autoruns*', silent=True) @@ -197,8 +230,10 @@ def run_rkill(): shutil.move(item.path, dest) -def show_alert_box(message, title='Wizard Kit Warning'): +def show_alert_box(message, title=None): """Show Windows alert box with message.""" + if not title: + title = '{} Warning'.format(KIT_NAME_FULL) message_box = ctypes.windll.user32.MessageBoxW message_box(None, message, title, 0x00001030) diff --git a/.bin/Scripts/functions/ufd.py b/.bin/Scripts/functions/ufd.py index 598985d3..32f08201 100644 --- a/.bin/Scripts/functions/ufd.py +++ b/.bin/Scripts/functions/ufd.py @@ -60,16 +60,16 @@ def confirm_selections(args): def copy_source(source, items, overwrite=False): """Copy source items to /mnt/UFD.""" - is_iso = source.name.lower().endswith('.iso') + is_image = source.is_file() # Mount source if necessary - if is_iso: + if is_image: mount(source, '/mnt/Source') # Copy items for i_source, i_dest in items: i_source = '{}{}'.format( - '/mnt/Source' if is_iso else source, + '/mnt/Source' if is_image else source, i_source, ) i_dest = '/mnt/UFD{}'.format(i_dest) @@ -80,7 +80,7 @@ def copy_source(source, items, overwrite=False): pass # Unmount source if necessary - if is_iso: + if is_image: unmount('/mnt/Source') @@ -199,6 +199,8 @@ def is_valid_path(path_obj, path_type): valid_path = path_obj.is_dir() elif path_type == 'KIT': valid_path = path_obj.is_dir() and path_obj.joinpath('.bin').exists() + elif path_type == 'IMG': + valid_path = path_obj.is_file() and path_obj.suffix.lower() == '.img' elif path_type == 'ISO': valid_path = path_obj.is_file() and path_obj.suffix.lower() == '.iso' elif path_type == 'UFD': @@ -207,10 +209,16 @@ def is_valid_path(path_obj, path_type): return valid_path -def mount(mount_source, mount_point): +def mount(mount_source, mount_point, read_write=False): """Mount mount_source on mount_point.""" os.makedirs(mount_point, exist_ok=True) - cmd = ['mount', mount_source, mount_point] + cmd = [ + 'mount', + mount_source, + mount_point, + '-o', + 'rw' if read_write else 'ro', + ] run_program(cmd) diff --git a/.bin/Scripts/functions/update.py b/.bin/Scripts/functions/update.py index 40bc3a27..38f881d5 100755 --- a/.bin/Scripts/functions/update.py +++ b/.bin/Scripts/functions/update.py @@ -615,6 +615,22 @@ def update_adobe_reader_dc(): dest, 'Adobe Reader DC.exe', SOURCE_URLS['Adobe Reader DC']) +def update_libreoffice(): + # Prep + dest = r'{}\Installers\Extras\Office'.format( + global_vars['BaseDir']) + + # Remove existing installer + try: + os.remove(r'{}\LibreOffice.msi'.format(dest)) + except FileNotFoundError: + pass + + # Download + download_generic( + dest, 'LibreOffice.msi', SOURCE_URLS['LibreOffice']) + + def update_macs_fan_control(): # Prep dest = r'{}\Installers'.format( diff --git a/.bin/Scripts/functions/windows_updates.py b/.bin/Scripts/functions/windows_updates.py new file mode 100644 index 00000000..3618fbb2 --- /dev/null +++ b/.bin/Scripts/functions/windows_updates.py @@ -0,0 +1,143 @@ +# Wizard Kit: Functions - Windows updates + +from functions.common import * + + +# Functions +def delete_folder(folder_path): + """Near-useless wrapper for shutil.rmtree.""" + shutil.rmtree(folder_path) + + +def disable_service(service_name): + """Set service startup to disabled.""" + run_program(['sc', 'config', service_name, 'start=', 'disabled']) + + # Verify service was disabled + start_type = get_service_start_type(service_name) + if not start_type.lower().startswith('disabled'): + raise GenericError('Failed to disable service {}'.format(service_name)) + + +def disable_windows_updates(): + """Disable windows updates and clear SoftwareDistribution folder.""" + indent=2 + width=52 + update_folders = [ + r'{WINDIR}\SoftwareDistribution'.format(**global_vars['Env']), + r'{SYSTEMDRIVE}\$WINDOWS.~BT'.format(**global_vars['Env']), + ] + + for service in ('wuauserv', 'bits'): + # Stop service + result = try_and_print( + 'Stopping service {}...'.format(service), + indent=indent, width=width, + function=stop_service, service_name=service) + if not result['CS']: + result = try_and_print( + 'Stopping service {}...'.format(service), + indent=indent, width=width, + function=stop_service, service_name=service) + if not result['CS']: + raise GenericError('Service {} could not be stopped.'.format(service)) + + # Disable service + result = try_and_print( + 'Disabling service {}...'.format(service), + indent=indent, width=width, + function=disable_service, service_name=service) + if not result['CS']: + result = try_and_print( + 'Disabling service {}...'.format(service), + indent=indent, width=width, + function=disable_service, service_name=service) + if not result['CS']: + raise GenericError('Service {} could not be disabled.'.format(service)) + + # Delete update folders + for folder_path in update_folders: + if os.path.exists(folder_path): + result = try_and_print( + 'Deleting folder {}...'.format(folder_path), + indent=indent, width=width, + function=delete_folder, folder_path=folder_path) + if not result['CS']: + raise GenericError('Failed to remove folder {}'.format(folder_path)) + + +def enable_service(service_name, start_type='auto'): + """Enable service by setting start type.""" + run_program(['sc', 'config', service_name, 'start=', start_type]) + + +def enable_windows_updates(silent=False): + """Enable windows updates""" + indent=2 + width=52 + + for service in ('bits', 'wuauserv'): + # Enable service + start_type = 'auto' + if service == 'wuauserv': + start_type = 'demand' + if silent: + try: + enable_service(service, start_type=start_type) + except Exception: + # Try again + enable_service(service, start_type=start_type) + else: + result = try_and_print( + 'Enabling service {}...'.format(service), + indent=indent, width=width, + function=enable_service, service_name=service, start_type=start_type) + if not result['CS']: + result = try_and_print( + 'Enabling service {}...'.format(service), + indent=indent, width=width, + function=enable_service, service_name=service, start_type=start_type) + if not result['CS']: + raise GenericError('Service {} could not be enabled.'.format(service)) + + +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: + # Ignore and return 'Unknown' below + pass + + 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: + # Ignore and return 'Unknown' below + pass + + return start_type + + +def stop_service(service_name): + """Stop service.""" + run_program(['net', 'stop', service_name], check=False) + + # Verify service was stopped + status = get_service_status(service_name) + if not status.lower().startswith('stopped'): + raise GenericError('Failed to stop service {}'.format(service_name)) + + +if __name__ == '__main__': + print("This file is not meant to be called directly.") + +# vim: sts=2 sw=2 ts=2 diff --git a/.bin/Scripts/hw-diags-menu b/.bin/Scripts/hw-diags-menu index 1c241bf9..fc95e04a 100755 --- a/.bin/Scripts/hw-diags-menu +++ b/.bin/Scripts/hw-diags-menu @@ -49,7 +49,7 @@ if __name__ == '__main__': global_vars=global_vars) # Done - sleep(10) + sleep(1) pause('Press Enter to exit...') exit_script(1) diff --git a/.bin/Scripts/install_sw_bundle.py b/.bin/Scripts/install_sw_bundle.py index b536fd64..2979362e 100644 --- a/.bin/Scripts/install_sw_bundle.py +++ b/.bin/Scripts/install_sw_bundle.py @@ -25,7 +25,6 @@ if __name__ == '__main__': 'UnsupportedOSError': 'Unsupported OS', }} answer_extensions = ask('Install Extensions?') - answer_adobe_reader = ask('Install Adobe Reader?') answer_vcr = ask('Install Visual C++ Runtimes?') answer_ninite = ask('Install Ninite Bundle?') if answer_ninite and global_vars['OS']['Version'] in ['7']: @@ -35,9 +34,6 @@ if __name__ == '__main__': answer_mse = False print_info('Installing Programs') - if answer_adobe_reader: - try_and_print(message='Adobe Reader DC...', - function=install_adobe_reader, other_results=other_results) if answer_vcr: install_vcredists() if answer_ninite: diff --git a/.bin/Scripts/mount-backup-shares b/.bin/Scripts/mount-backup-shares index 6a1e88d7..0d8b7fd3 100755 --- a/.bin/Scripts/mount-backup-shares +++ b/.bin/Scripts/mount-backup-shares @@ -16,9 +16,6 @@ if __name__ == '__main__': # Prep clear_screen() - # Connect - connect_to_network() - # Mount if is_connected(): mount_backup_shares(read_write=True) diff --git a/.bin/Scripts/new_system_setup.py b/.bin/Scripts/new_system_setup.py deleted file mode 100644 index 68e508d2..00000000 --- a/.bin/Scripts/new_system_setup.py +++ /dev/null @@ -1,160 +0,0 @@ -# Wizard Kit: New system setup - -import os -import sys - -# Init -sys.path.append(os.path.dirname(os.path.realpath(__file__))) -from functions.activation import * -from functions.browsers import * -from functions.cleanup import * -from functions.info import * -from functions.product_keys import * -from functions.setup import * -from functions.sw_diags import * -init_global_vars() -os.system('title {}: New System Setup'.format(KIT_NAME_FULL)) -set_log_file('New System Setup.log') - -if __name__ == '__main__': - other_results = { - 'Error': { - 'BIOSKeyNotFoundError': 'BIOS key not found', - 'CalledProcessError': 'Unknown Error', - 'FileNotFoundError': 'File not found', - 'GenericError': 'Unknown Error', - 'SecureBootDisabledError': 'Disabled', - }, - 'Warning': { - 'GenericRepair': 'Repaired', - 'NoProfilesError': 'No profiles found', - 'NotInstalledError': 'Not installed', - 'OSInstalledLegacyError': 'OS installed Legacy', - 'SecureBootNotAvailError': 'Not available', - 'SecureBootUnknownError': 'Unknown', - 'UnsupportedOSError': 'Unsupported OS', - }} - try: - stay_awake() - clear_screen() - - # Check installed OS - if os_is_unsupported(show_alert=False): - print_warning('OS version not supported by this script') - if not ask('Continue anyway? (NOT RECOMMENDED)'): - abort() - - # Install Adobe Reader? - answer_adobe_reader = ask('Install Adobe Reader?') - - # Install LibreOffice? - answer_libreoffice = ask('Install LibreOffice?') - - # Install MSE? - if global_vars['OS']['Version'] == '7': - answer_mse = ask('Install MSE?') - else: - answer_mse = False - - # Install software - print_info('Installing Programs') - install_vcredists() - if answer_adobe_reader: - try_and_print(message='Adobe Reader DC...', - function=install_adobe_reader, other_results=other_results) - result = try_and_print( - message='Ninite bundle...', - function=install_ninite_bundle, cs='Started', - mse=answer_mse, libreoffice=answer_libreoffice, - other_results=other_results) - for proc in result['Out']: - # Wait for all processes to finish - proc.wait() - - # Scan for supported browsers - print_info('Scanning for browsers') - scan_for_browsers() - - # Install extensions - print_info('Installing Extensions') - try_and_print(message='Classic Shell skin...', - function=install_classicstart_skin, - other_results=other_results) - try_and_print(message='Google Chrome extensions...', - function=install_chrome_extensions) - try_and_print(message='Mozilla Firefox extensions...', - function=install_firefox_extensions, - other_results=other_results) - - # Configure software - print_info('Configuring programs') - install_adblock() - if global_vars['OS']['Version'] == '10': - try_and_print(message='ClassicStart...', - function=config_classicstart, cs='Done') - try_and_print(message='Explorer (user)...', - function=config_explorer_user, cs='Done') - - # Configure system - print_info('Configuring system') - if global_vars['OS']['Version'] == '10': - try_and_print(message='Explorer (system)...', - function=config_explorer_system, cs='Done') - try_and_print(message='Windows Updates...', - function=config_windows_updates, cs='Done') - try_and_print(message='Updating Clock...', - function=update_clock, cs='Done') - - # Restart Explorer - try_and_print(message='Restarting Explorer...', - function=restart_explorer, cs='Done') - - # Summary - print_info('Summary') - try_and_print(message='Operating System:', - function=show_os_name, ns='Unknown', silent_function=False) - try_and_print(message='Activation:', - function=show_os_activation, ns='Unknown', silent_function=False) - if (not windows_is_activated() - and global_vars['OS']['Version'] in ('8', '8.1', '10')): - try_and_print(message='BIOS Activation:', - function=activate_with_bios, - other_results=other_results) - try_and_print(message='Secure Boot Status:', - function=check_secure_boot_status, other_results=other_results) - try_and_print(message='Installed RAM:', - function=show_installed_ram, ns='Unknown', silent_function=False) - show_free_space() - try_and_print(message='Installed Antivirus:', - function=get_installed_antivirus, ns='Unknown', - other_results=other_results, print_return=True) - - # Play audio, show devices, open Windows updates, and open Activation - try_and_print(message='Opening Device Manager...', - function=open_device_manager, cs='Started') - try_and_print(message='Opening HWiNFO (Sensors)...', - function=run_hwinfo_sensors, cs='Started', other_results=other_results) - try_and_print(message='Opening Windows Updates...', - function=open_windows_updates, cs='Started') - if not windows_is_activated(): - try_and_print(message='Opening Windows Activation...', - function=open_windows_activation, cs='Started') - sleep(3) - try_and_print(message='Running XMPlay...', - function=run_xmplay, cs='Started', other_results=other_results) - try: - check_secure_boot_status(show_alert=True) - except: - # Only trying to open alert message boxes - pass - - # Done - print_standard('\nDone.') - pause('Press Enter to exit...') - exit_script() - except SystemExit as sys_exit: - exit_script(sys_exit.code) - except: - major_exception() - -# vim: sts=2 sw=2 ts=2 diff --git a/.bin/Scripts/settings/cleanup.py b/.bin/Scripts/settings/cleanup.py new file mode 100644 index 00000000..2188fc6b --- /dev/null +++ b/.bin/Scripts/settings/cleanup.py @@ -0,0 +1,37 @@ +'''Wizard Kit: Settings - Cleanup''' +# vim: sts=2 sw=2 ts=2 + +import re + +# Regex +DESKTOP_ITEMS = re.compile( + r'^(JRT|RKill|sc-cleaner)', + re.IGNORECASE, + ) + +# Registry +UAC_DEFAULTS_WIN7 = { + r'Software\Microsoft\Windows\CurrentVersion\Policies\System': { + 'DWORD Items': { + 'ConsentPromptBehaviorAdmin': 5, + 'EnableLUA': 1, + 'PromptOnSecureDesktop': 1, + }, + }, + } +UAC_DEFAULTS_WIN10 = { + r'Software\Microsoft\Windows\CurrentVersion\Policies\System': { + 'DWORD Items': { + 'ConsentPromptBehaviorAdmin': 5, + 'ConsentPromptBehaviorUser': 3, + 'EnableInstallerDetection': 1, + 'EnableLUA': 1, + 'EnableVirtualization': 1, + 'PromptOnSecureDesktop': 1, + }, + }, + } + + +if __name__ == '__main__': + print("This file is not meant to be called directly.") diff --git a/.bin/Scripts/settings/ddrescue.py b/.bin/Scripts/settings/ddrescue.py index 675019ca..ffe6e215 100644 --- a/.bin/Scripts/settings/ddrescue.py +++ b/.bin/Scripts/settings/ddrescue.py @@ -5,7 +5,9 @@ import re from collections import OrderedDict # General +MAP_DIR = '/Backups/ddrescue-tui' RECOMMENDED_FSTYPES = ['ext3', 'ext4', 'xfs'] +RECOMMENDED_MAP_FSTYPES = ['cifs', 'ext2', 'ext3', 'ext4', 'vfat', 'xfs'] USAGE = """ {script_name} clone [source [destination]] {script_name} image [source [destination]] (e.g. {script_name} clone /dev/sda /dev/sdb) @@ -36,6 +38,12 @@ DDRESCUE_SETTINGS = { '-vvvv': {'Enabled': True, 'Hidden': True, }, } ETOC_REFRESH_RATE = 30 # in seconds +REGEX_DDRESCUE_LOG = re.compile( + r'^\s*(?P\S+):\s+' + r'(?P\d+)\s+' + r'(?P[PTGMKB])i?B?', + re.IGNORECASE, + ) REGEX_REMAINING_TIME = re.compile( r'remaining time:' r'\s*((?P\d+)d)?' diff --git a/.bin/Scripts/settings/launchers.py b/.bin/Scripts/settings/launchers.py index 73a70923..7161c98e 100644 --- a/.bin/Scripts/settings/launchers.py +++ b/.bin/Scripts/settings/launchers.py @@ -1,35 +1,20 @@ -# Wizard Kit: Settings - Launchers +'''Wizard Kit: Settings - Launchers''' +# pylint: disable=line-too-long +# vim: sts=2 sw=2 ts=2 LAUNCHERS = { r'(Root)': { - 'Activate Windows': { - 'L_TYPE': 'PyScript', - 'L_PATH': 'Scripts', - 'L_ITEM': 'activate.py', - 'L_ELEV': 'True', - }, - 'New System Setup': { - 'L_TYPE': 'PyScript', - 'L_PATH': 'Scripts', - 'L_ITEM': 'new_system_setup.py', - 'L_ELEV': 'True', - }, - 'System Checklist': { - 'L_TYPE': 'PyScript', - 'L_PATH': 'Scripts', - 'L_ITEM': 'system_checklist.py', - 'L_ELEV': 'True', - }, 'System Diagnostics': { 'L_TYPE': 'PyScript', 'L_PATH': 'Scripts', 'L_ITEM': 'system_diagnostics.py', 'L_ELEV': 'True', }, - 'User Checklist': { + 'System Setup': { 'L_TYPE': 'PyScript', 'L_PATH': 'Scripts', - 'L_ITEM': 'user_checklist.py', + 'L_ITEM': 'system_setup.py', + 'L_ELEV': 'True', }, }, r'Data Recovery': { @@ -55,6 +40,7 @@ LAUNCHERS = { }, }, r'Data Transfers': { + # pylint: disable=bad-continuation 'FastCopy (as ADMIN)': { 'L_TYPE': 'Executable', 'L_PATH': 'FastCopy', @@ -257,7 +243,7 @@ LAUNCHERS = { 'L_TYPE': 'Executable', 'L_PATH': 'erunt', 'L_ITEM': 'ERUNT.EXE', - 'L_ARGS': '%client_dir%\Backups\Registry\%iso_date% sysreg curuser otherusers', + 'L_ARGS': r'%client_dir%\Backups\Registry\%iso_date% sysreg curuser otherusers', 'L_ELEV': 'True', 'Extra Code': [ r'call "%bin%\Scripts\init_client_dir.cmd" /Logs', @@ -287,13 +273,13 @@ LAUNCHERS = { r'Drivers': { 'Intel RST (Current Release)': { 'L_TYPE': 'Executable', - 'L_PATH': '_Drivers\Intel RST', + 'L_PATH': r'_Drivers\Intel RST', 'L_ITEM': 'SetupRST_17.2.exe', 'L_7ZIP': 'SetupRST_17.2.exe', }, 'Intel RST (Previous Releases)': { 'L_TYPE': 'Folder', - 'L_PATH': '_Drivers\Intel RST', + 'L_PATH': r'_Drivers\Intel RST', 'L_ITEM': '.', 'L_NCMD': 'True', }, @@ -309,7 +295,7 @@ LAUNCHERS = { }, 'Snappy Driver Installer Origin': { 'L_TYPE': 'Executable', - 'L_PATH': '_Drivers\SDIO', + 'L_PATH': r'_Drivers\SDIO', 'L_ITEM': 'SDIO.exe', }, }, @@ -435,6 +421,12 @@ LAUNCHERS = { }, }, r'Misc': { + 'Activate Windows': { + 'L_TYPE': 'PyScript', + 'L_PATH': 'Scripts', + 'L_ITEM': 'activate.py', + 'L_ELEV': 'True', + }, 'Cleanup CBS Temp Files': { 'L_TYPE': 'PyScript', 'L_PATH': 'Scripts', @@ -452,6 +444,20 @@ LAUNCHERS = { 'L_PATH': 'ConEmu', 'L_ITEM': 'ConEmu.exe', }, + 'Disable Windows Updates': { + 'L_TYPE': 'PyScript', + 'L_PATH': 'Scripts', + 'L_ITEM': 'windows_updates.py', + 'L_ARGS': '--disable', + 'L_ELEV': 'True', + }, + 'Enable Windows Updates': { + 'L_TYPE': 'PyScript', + 'L_PATH': 'Scripts', + 'L_ITEM': 'windows_updates.py', + 'L_ARGS': '--enable', + 'L_ELEV': 'True', + }, 'Enter SafeMode': { 'L_TYPE': 'PyScript', 'L_PATH': 'Scripts', @@ -491,7 +497,7 @@ LAUNCHERS = { 'L_TYPE': 'Executable', 'L_PATH': 'XMPlay', 'L_ITEM': 'xmplay.exe', - 'L_ARGS': '"%bin%\XMPlay\music.7z"', + 'L_ARGS': r'"%bin%\XMPlay\music.7z"', }, }, r'Repairs': { @@ -551,7 +557,7 @@ LAUNCHERS = { 'L_TYPE': 'Executable', 'L_PATH': 'RKill', 'L_ITEM': 'RKill.exe', - 'L_ARGS': '-s -l %log_dir%\Tools\RKill.log', + 'L_ARGS': r'-s -l %log_dir%\Tools\RKill.log', 'L_ELEV': 'True', 'Extra Code': [ r'call "%bin%\Scripts\init_client_dir.cmd" /Logs', @@ -594,5 +600,3 @@ LAUNCHERS = { if __name__ == '__main__': print("This file is not meant to be called directly.") - -# vim: sts=2 sw=2 ts=2 diff --git a/.bin/Scripts/settings/setup.py b/.bin/Scripts/settings/setup.py index 232e3a0d..ec3219a2 100644 --- a/.bin/Scripts/settings/setup.py +++ b/.bin/Scripts/settings/setup.py @@ -1,13 +1,19 @@ -# Wizard Kit: Settings - Setup +'''Wizard Kit: Settings - Setup''' +# pylint: disable=bad-continuation,line-too-long +# vim: sts=2 sw=2 ts=2 import os -import winreg +try: + import winreg + HKU = winreg.HKEY_USERS + HKCR = winreg.HKEY_CLASSES_ROOT + HKCU = winreg.HKEY_CURRENT_USER + HKLM = winreg.HKEY_LOCAL_MACHINE +except ImportError: + if os.name != 'posix': + raise # General -HKU = winreg.HKEY_USERS -HKCR = winreg.HKEY_CLASSES_ROOT -HKCU = winreg.HKEY_CURRENT_USER -HKLM = winreg.HKEY_LOCAL_MACHINE OTHER_RESULTS = { 'Error': { 'CalledProcessError': 'Unknown Error', @@ -92,6 +98,15 @@ SETTINGS_EXPLORER_SYSTEM = { }, } SETTINGS_EXPLORER_USER = { + # Desktop theme + r'Software\Microsoft\Windows\CurrentVersion\Themes\Personalize': { + 'Invalid modes': ['Cur'], + 'DWORD Items': { + # <= v1809 default + 'AppsUseLightTheme': 1, + 'SystemUsesLightTheme': 0, + }, + }, # Disable features r'Software\Microsoft\Windows\CurrentVersion\ContentDeliveryManager': { 'DWORD Items': { @@ -104,21 +119,41 @@ SETTINGS_EXPLORER_USER = { }, # File Explorer r'Software\Microsoft\Windows\CurrentVersion\Explorer\Advanced': { + 'Invalid modes': ['Cur'], 'DWORD Items': { # Change default Explorer view to "Computer" 'LaunchTo': 1, }, }, + r'SOFTWARE\Microsoft\Windows\CurrentVersion\Explorer\Advanced': { + # Dup path so it Will be applied to all modes + 'DWORD Items': { + # Launch Folder Windows in a Separate Process + 'SeparateProcess': 1, + }, + }, # Hide People bar r'Software\Microsoft\Windows\CurrentVersion\Explorer\Advanced\People': { + 'Invalid modes': ['Cur'], 'DWORD Items': {'PeopleBand': 0}, }, # Hide Search button / box r'Software\Microsoft\Windows\CurrentVersion\Search': { + 'Invalid modes': ['Cur'], 'DWORD Items': {'SearchboxTaskbarMode': 0}, }, } +# LibreOffice +LIBREOFFICE_XCU_DATA = ''' + +Impress MS PowerPoint 2007 XML +Calc MS Excel 2007 XML +MS Word 2007 XML +false + +''' + # Visual C++ Runtimes VCR_REDISTS = [ {'Name': 'Visual C++ 2010 x32...', @@ -157,5 +192,3 @@ SETTINGS_WINDOWS_UPDATES = { if __name__ == '__main__': print("This file is not meant to be called directly.") - -# vim: sts=2 sw=2 ts=2 diff --git a/.bin/Scripts/settings/sources.py b/.bin/Scripts/settings/sources.py index e6cf5c3a..2abf079f 100644 --- a/.bin/Scripts/settings/sources.py +++ b/.bin/Scripts/settings/sources.py @@ -1,4 +1,6 @@ -# Wizard Kit: Settings - Sources +'''Wizard Kit: Settings - Sources''' +# pylint: disable=line-too-long +# vim: sts=2 sw=2 ts=2 tw=0 SOURCE_URLS = { 'Adobe Reader DC': 'http://ardownload.adobe.com/pub/adobe/reader/win/AcrobatDC/1901020098/AcroRdrDC1901020098_en_US.exe', @@ -15,7 +17,7 @@ SOURCE_URLS = { 'ERUNT': 'http://www.aumha.org/downloads/erunt.zip', 'Everything32': 'https://www.voidtools.com/Everything-1.4.1.935.x86.en-US.zip', 'Everything64': 'https://www.voidtools.com/Everything-1.4.1.935.x64.en-US.zip', - 'FastCopy': 'http://ftp.vector.co.jp/71/31/2323/FastCopy363_installer.exe', + 'FastCopy': 'https://fastcopy.jp/archive/FastCopy380_installer.exe', 'Firefox uBO': 'https://addons.mozilla.org/firefox/downloads/file/1709472/ublock_origin-1.18.6-an+fx.xpi', 'HitmanPro32': 'https://dl.surfright.nl/HitmanPro.exe', 'HitmanPro64': 'https://dl.surfright.nl/HitmanPro_x64.exe', @@ -23,6 +25,7 @@ SOURCE_URLS = { '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': 'http://devbuilds.kaspersky-labs.com/devbuilds/KVRT/latest/full/KVRT.exe', + 'LibreOffice': 'https://download.documentfoundation.org/libreoffice/stable/6.2.4/win/x86_64/LibreOffice_6.2.4_Win_x64.msi', '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', @@ -37,8 +40,8 @@ SOURCE_URLS = { 'SDIO Torrent': 'http://snappy-driver-installer.org/downloads/SDIO_Update.torrent', 'TDSSKiller': 'https://media.kaspersky.com/utilities/VirusUtilities/EN/tdsskiller.exe', 'TestDisk': 'https://www.cgsecurity.org/testdisk-7.1-WIP.win.zip', - 'wimlib32': 'https://wimlib.net/downloads/wimlib-1.13.0-windows-i686-bin.zip', - 'wimlib64': 'https://wimlib.net/downloads/wimlib-1.13.0-windows-x86_64-bin.zip', + 'wimlib32': 'https://wimlib.net/downloads/wimlib-1.13.1-windows-i686-bin.zip', + 'wimlib64': 'https://wimlib.net/downloads/wimlib-1.13.1-windows-x86_64-bin.zip', 'Winapp2': 'https://github.com/MoscaDotTo/Winapp2/archive/master.zip', 'WizTree': 'https://antibody-software.com/files/wiztree_3_28_portable.zip', 'XMPlay 7z': 'https://support.xmplay.com/files/16/xmp-7z.zip?v=800962', @@ -66,10 +69,18 @@ VCREDIST_SOURCES = { '64': 'https://aka.ms/vs/15/release/vc_redist.x64.exe', }, } +NINITE_REGEX = { + 'base': ['7-Zip', 'VLC'], + 'standard': ['Google Chrome', 'Mozilla Firefox', 'SumatraPDF'], + 'standard7': ['Google Chrome', 'Mozilla Firefox', 'SumatraPDF'], + } NINITE_SOURCES = { 'Bundles': { - 'Legacy.exe': '.net4.7.2-7zip-chrome-firefox-vlc', - 'Modern.exe': '.net4.7.2-7zip-chrome-classicstart-firefox-vlc', + 'base.exe': '.net4.7.2-7zip-vlc', + 'base-standard.exe': '.net4.7.2-7zip-chrome-classicstart-firefox-sumatrapdf-vlc', + 'base-standard7.exe': '.net4.7.2-7zip-chrome-firefox-sumatrapdf-vlc', + 'standard.exe': 'chrome-classicstart-firefox-sumatrapdf', + 'standard7.exe': 'chrome-firefox-sumatrapdf', }, 'Audio-Video': { 'AIMP.exe': 'aimp', @@ -216,5 +227,3 @@ WINDOWS_UPDATE_SOURCES = { if __name__ == '__main__': print("This file is not meant to be called directly.") - -# vim: sts=2 sw=2 ts=2 tw=0 diff --git a/.bin/Scripts/settings/windows_builds.py b/.bin/Scripts/settings/windows_builds.py index db538bf2..f7481294 100644 --- a/.bin/Scripts/settings/windows_builds.py +++ b/.bin/Scripts/settings/windows_builds.py @@ -1,8 +1,10 @@ -# Wizard Kit: Settings - Windows Builds +'''Wizard Kit: Settings - Windows Builds''' +# pylint: disable=bad-continuation,bad-whitespace +# vim: sts=2 sw=2 ts=2 +## NOTE: Data from here: https://en.wikipedia.org/wiki/Windows_10_version_history WINDOWS_BUILDS = { # Build, Version, Release, Codename, Marketing Name, Notes - '6000': ('Vista', 'RTM', 'Longhorn', None, 'unsupported'), '6000': ('Vista', 'RTM', 'Longhorn', None, 'unsupported'), '6001': ('Vista', 'SP1', 'Longhorn', None, 'unsupported'), '6002': ('Vista', 'SP2', 'Longhorn', None, 'unsupported'), @@ -202,15 +204,22 @@ WINDOWS_BUILDS = { '18356': ('10', None, '19H1', None, 'preview build'), '18358': ('10', None, '19H1', None, 'preview build'), '18361': ('10', None, '19H1', None, 'preview build'), + '18362': ('10', 'v1903', '19H1', 'May 2019 Update', None), '18836': ('10', None, '20H1', None, 'preview build'), '18841': ('10', None, '20H1', None, 'preview build'), '18845': ('10', None, '20H1', None, 'preview build'), '18850': ('10', None, '20H1', None, 'preview build'), '18855': ('10', None, '20H1', None, 'preview build'), + '18860': ('10', None, '20H1', None, 'preview build'), + '18865': ('10', None, '20H1', None, 'preview build'), + '18875': ('10', None, '20H1', None, 'preview build'), + '18885': ('10', None, '20H1', None, 'preview build'), + '18890': ('10', None, '20H1', None, 'preview build'), + '18894': ('10', None, '20H1', None, 'preview build'), + '18895': ('10', None, '20H1', None, 'preview build'), + '18898': ('10', None, '20H1', None, 'preview build'), } if __name__ == '__main__': print("This file is not meant to be called directly.") - -# vim: sts=2 sw=2 ts=2 diff --git a/.bin/Scripts/system_checklist.py b/.bin/Scripts/system_checklist.py deleted file mode 100644 index a5b86e1e..00000000 --- a/.bin/Scripts/system_checklist.py +++ /dev/null @@ -1,133 +0,0 @@ -# Wizard Kit: System Checklist - -import os -import sys - -# Init -sys.path.append(os.path.dirname(os.path.realpath(__file__))) -from functions.activation import * -from functions.cleanup import * -from functions.info import * -from functions.product_keys import * -from functions.setup import * -from functions.sw_diags import * -init_global_vars() -os.system('title {}: System Checklist Tool'.format(KIT_NAME_FULL)) -set_log_file('System Checklist.log') - -if __name__ == '__main__': - try: - stay_awake() - clear_screen() - print_info('{}: System Checklist Tool\n'.format(KIT_NAME_FULL)) - ticket_number = get_ticket_number() - other_results = { - 'Error': { - 'BIOSKeyNotFoundError': 'BIOS key not found', - 'CalledProcessError': 'Unknown Error', - 'FileNotFoundError': 'File not found', - 'GenericError': 'Unknown Error', - 'SecureBootDisabledError': 'Disabled', - }, - 'Warning': { - 'OSInstalledLegacyError': 'OS installed Legacy', - 'SecureBootNotAvailError': 'Not available', - 'SecureBootUnknownError': 'Unknown', - }} - if ENABLED_TICKET_NUMBERS: - print_info('Starting System Checklist for Ticket #{}\n'.format( - ticket_number)) - - # Configure - print_info('Configure') - if global_vars['OS']['Version'] == '10': - try_and_print(message='Explorer...', - function=config_explorer_system, cs='Done') - try_and_print(message='Windows Updates...', - function=config_windows_updates, cs='Done') - try_and_print(message='Updating Clock...', - function=update_clock, cs='Done') - - # Restart Explorer - try_and_print(message='Restarting Explorer...', - function=restart_explorer, cs='Done') - - # Cleanup - print_info('Cleanup') - try_and_print(message='AdwCleaner...', - function=cleanup_adwcleaner, cs='Done', other_results=other_results) - try_and_print(message='Desktop...', - function=cleanup_desktop, cs='Done') - try_and_print(message='{}...'.format(KIT_NAME_FULL), - function=delete_empty_folders, cs='Done', - folder_path=global_vars['ClientDir']) - - # Export system info - print_info('Backup System Information') - try_and_print(message='AIDA64 reports...', - function=run_aida64, cs='Done', other_results=other_results) - try_and_print(message='File listing...', - function=backup_file_list, cs='Done', other_results=other_results) - try_and_print(message='Power plans...', - function=backup_power_plans, cs='Done') - try_and_print(message='Product Keys...', other_results=other_results, - function=run_produkey, cs='Done') - try_and_print(message='Registry...', - function=backup_registry, cs='Done', other_results=other_results) - - # User data - print_info('User Data') - show_user_data_summary() - - # Summary - print_info('Summary') - try_and_print(message='Operating System:', - function=show_os_name, ns='Unknown', silent_function=False) - try_and_print(message='Activation:', - function=show_os_activation, ns='Unknown', silent_function=False) - if (not windows_is_activated() - and global_vars['OS']['Version'] in ('8', '8.1', '10')): - try_and_print(message='BIOS Activation:', - function=activate_with_bios, - other_results=other_results) - try_and_print(message='Secure Boot Status:', - function=check_secure_boot_status, other_results=other_results) - try_and_print(message='Installed RAM:', - function=show_installed_ram, ns='Unknown', silent_function=False) - show_free_space() - try_and_print(message='Installed Antivirus:', - function=get_installed_antivirus, ns='Unknown', - other_results=other_results, print_return=True) - try_and_print(message='Installed Office:', - function=get_installed_office, ns='Unknown', - other_results=other_results, print_return=True) - - # Play audio, show devices, open Windows updates, and open Activation - try_and_print(message='Opening Device Manager...', - function=open_device_manager, cs='Started') - try_and_print(message='Opening HWiNFO (Sensors)...', - function=run_hwinfo_sensors, cs='Started', other_results=other_results) - try_and_print(message='Opening Windows Updates...', - function=open_windows_updates, cs='Started') - if not windows_is_activated(): - try_and_print(message='Opening Windows Activation...', - function=open_windows_activation, cs='Started') - sleep(3) - try_and_print(message='Running XMPlay...', - function=run_xmplay, cs='Started', other_results=other_results) - try: - check_secure_boot_status(show_alert=True) - except: - # Only trying to open alert message boxes - pass - - # Done - print_standard('\nDone.') - pause('Press Enter exit...') - exit_script() - except SystemExit as sys_exit: - exit_script(sys_exit.code) - except: - major_exception() - -# vim: sts=2 sw=2 ts=2 diff --git a/.bin/Scripts/system_setup.py b/.bin/Scripts/system_setup.py new file mode 100644 index 00000000..5d49b26d --- /dev/null +++ b/.bin/Scripts/system_setup.py @@ -0,0 +1,354 @@ +'''Wizard Kit: System Setup''' +# pylint: disable=wildcard-import,wrong-import-position +# vim: sts=2 sw=2 ts=2 + +import os +import sys + +# Init +sys.path.append(os.path.dirname(os.path.realpath(__file__))) +from collections import OrderedDict +from functions.activation import * +from functions.browsers import * +from functions.cleanup import * +from functions.info import * +from functions.product_keys import * +from functions.setup import * +from functions.sw_diags import * +from functions.windows_updates import * +init_global_vars() +os.system('title {}: System Setup'.format(KIT_NAME_FULL)) +set_log_file('System Setup.log') + + +# STATIC VARIABLES +# pylint: disable=bad-whitespace,line-too-long +OTHER_RESULTS = { + 'Error': { + 'BIOSKeyNotFoundError': 'BIOS KEY NOT FOUND', + 'CalledProcessError': 'UNKNOWN ERROR', + 'FileNotFoundError': 'FILE NOT FOUND', + 'GenericError': 'UNKNOWN ERROR', + 'Not4KAlignedError': 'FALSE', + 'SecureBootDisabledError': 'DISABLED', + 'WindowsUnsupportedError': 'UNSUPPORTED', + }, + 'Warning': { + 'GenericRepair': 'REPAIRED', + 'NoProfilesError': 'NO PROFILES FOUND', + 'NotInstalledError': 'NOT INSTALLED', + 'OSInstalledLegacyError': 'OS INSTALLED LEGACY', + 'SecureBootNotAvailError': 'NOT AVAILABLE', + 'SecureBootUnknownError': 'UNKNOWN', + 'UnsupportedOSError': 'UNSUPPORTED OS', + 'WindowsOutdatedError': 'OUTDATED', + }, + } +SETUP_ACTIONS = OrderedDict({ + # Install software + 'Installing Programs': {'Info': True}, + 'VCR': {'New': True, 'Fab': True, 'Cur': True, 'HW': False, 'Function': install_vcredists, 'Just run': True,}, + 'LibreOffice': {'New': True, 'Fab': True, 'Cur': True, 'HW': False, 'Function': install_libreoffice, + 'If answer': 'LibreOffice', 'KWArgs': {'quickstart': False, 'register_mso_types': True, 'use_mso_formats': True, 'vcredist': False}, + }, + 'Ninite bundle': {'New': True, 'Fab': True, 'Cur': True, 'HW': False, 'Function': install_ninite_bundle, 'KWArgs': {'cs': 'STARTED'},}, + + # Browsers + 'Scanning for browsers': {'Info': True}, + 'Scan': {'New': True, 'Fab': True, 'Cur': True, 'HW': False, 'Function': scan_for_browsers, 'Just run': True, 'KWArgs': {'skip_ie': True},}, + 'Backing up browsers': {'Info': True}, + 'Backup browsers': {'New': False, 'Fab': True, 'Cur': True, 'HW': False, 'Function': backup_browsers, 'Just run': True,}, + + # Install extensions + 'Installing Extensions': {'Info': True}, + 'Classic Shell skin': {'New': True, 'Fab': True, 'Cur': False, 'HW': False, 'Function': install_classicstart_skin, 'Win10 only': True,}, + 'Chrome extensions': {'New': True, 'Fab': True, 'Cur': True, 'HW': False, 'Function': install_chrome_extensions,}, + 'Firefox extensions': {'New': True, 'Fab': True, 'Cur': True, 'HW': False, 'Function': install_firefox_extensions,}, + + # Configure software' + 'Configuring Programs': {'Info': True}, + 'Browser add-ons': {'New': True, 'Fab': True, 'Cur': True, 'HW': False, 'Function': install_adblock, 'Just run': True, + 'Pause': 'Please enable uBlock Origin for all browsers', + }, + 'Classic Start': {'New': True, 'Fab': True, 'Cur': False, 'HW': False, 'Function': config_classicstart, 'Win10 only': True,}, + 'Config Windows Updates': {'New': True, 'Fab': True, 'Cur': True, 'HW': True, 'Function': config_windows_updates, 'Win10 only': True,}, + 'Enable Windows Updates': {'New': True, 'Fab': True, 'Cur': True, 'HW': True, 'Function': enable_windows_updates, 'KWArgs': {'silent': True},}, + 'Explorer (system)': {'New': True, 'Fab': True, 'Cur': True, 'HW': False, 'Function': config_explorer_system, 'Win10 only': True,}, + 'Explorer (user)': {'New': True, 'Fab': True, 'Cur': True, 'HW': False, 'Function': config_explorer_user, 'Win10 only': True,}, + 'Restart Explorer': {'New': True, 'Fab': True, 'Cur': True, 'HW': False, 'Function': restart_explorer,}, + 'Update Clock': {'New': True, 'Fab': True, 'Cur': True, 'HW': False, 'Function': update_clock,}, + + # Cleanup + 'Cleaning up': {'Info': True}, + 'AdwCleaner': {'New': True, 'Fab': True, 'Cur': True, 'HW': False, 'Function': cleanup_adwcleaner,}, + 'Desktop': {'New': True, 'Fab': True, 'Cur': True, 'HW': False, 'Function': cleanup_desktop,}, + 'KIT_NAME_FULL': {'New': True, 'Fab': True, 'Cur': True, 'HW': True, 'Function': delete_empty_folders,}, + + # System Info + 'Exporting system info': {'Info': True}, + 'AIDA64 Report': {'New': True, 'Fab': True, 'Cur': True, 'HW': False, 'Function': run_aida64,}, + 'File listing': {'New': True, 'Fab': True, 'Cur': True, 'HW': False, 'Function': backup_file_list,}, + 'Power plans': {'New': True, 'Fab': True, 'Cur': True, 'HW': True, 'Function': backup_power_plans,}, + 'Product Keys': {'New': True, 'Fab': True, 'Cur': True, 'HW': True, 'Function': run_produkey,}, + 'Registry': {'New': True, 'Fab': True, 'Cur': True, 'HW': True, 'Function': backup_registry,}, + + # Show Summary + 'Summary': {'Info': True}, + 'Operating System': {'New': True, 'Fab': True, 'Cur': True, 'HW': True, 'Function': show_os_name, 'KWArgs': {'ns': 'UNKNOWN', 'silent_function': False},}, + 'Activation': {'New': True, 'Fab': True, 'Cur': True, 'HW': True, 'Function': show_os_activation, 'KWArgs': {'ns': 'UNKNOWN', 'silent_function': False},}, + 'BIOS Activation': {'New': True, 'Fab': True, 'Cur': True, 'HW': False, 'Function': activate_with_bios, 'If not activated': True,}, + 'Secure Boot': {'New': True, 'Fab': True, 'Cur': True, 'HW': True, 'Function': check_secure_boot_status, 'KWArgs': {'show_alert': False},}, + 'Installed RAM': {'New': True, 'Fab': True, 'Cur': True, 'HW': True, 'Function': show_installed_ram, 'KWArgs': {'ns': 'UNKNOWN', 'silent_function': False},}, + 'Temp size': {'New': False, 'Fab': False, 'Cur': True, 'HW': False, 'Function': show_temp_files_size, 'KWArgs': {'ns': 'UNKNOWN', 'silent_function': False},}, + 'Show free space': {'New': True, 'Fab': True, 'Cur': True, 'HW': True, 'Function': show_free_space, 'Just run': True,}, + 'Installed AV': {'New': True, 'Fab': True, 'Cur': True, 'HW': True, 'Function': get_installed_antivirus, 'KWArgs': {'ns': 'UNKNOWN', 'print_return': True},}, + 'Installed Office': {'New': True, 'Fab': True, 'Cur': True, 'HW': False, 'Function': get_installed_office, 'KWArgs': {'ns': 'UNKNOWN', 'print_return': True},}, + 'Partitions 4K aligned': {'New': True, 'Fab': True, 'Cur': True, 'HW': True, 'Function': check_4k_alignment, 'KWArgs': {'cs': 'TRUE', 'ns': 'FALSE'},}, + + # Open things + 'Opening Programs': {'Info': True}, + 'Device Manager': {'New': True, 'Fab': True, 'Cur': True, 'HW': True, 'Function': open_device_manager, 'KWArgs': {'cs': 'STARTED'},}, + 'HWiNFO sensors': {'New': True, 'Fab': True, 'Cur': True, 'HW': True, 'Function': run_hwinfo_sensors, 'KWArgs': {'cs': 'STARTED'},}, + 'Speed test': {'New': True, 'Fab': True, 'Cur': True, 'HW': True, 'Function': open_speedtest, 'KWArgs': {'cs': 'STARTED'},}, + 'Windows Updates': {'New': True, 'Fab': True, 'Cur': True, 'HW': False, 'Function': open_windows_updates, 'KWArgs': {'cs': 'STARTED'},}, + 'Windows Activation': {'New': True, 'Fab': True, 'Cur': True, 'HW': False, 'Function': open_windows_activation, 'If not activated': True, 'KWArgs': {'cs': 'STARTED'},}, + 'Sleep': {'New': True, 'Fab': True, 'Cur': True, 'HW': True, 'Function': sleep, 'Just run': True, 'KWArgs': {'seconds': 3},}, + 'XMPlay': {'New': True, 'Fab': True, 'Cur': True, 'HW': True, 'Function': run_xmplay, 'KWArgs': {'cs': 'STARTED'},}, + }) +SETUP_ACTION_KEYS = ( + 'Function', + 'If not activated', + 'Info', + 'Just run', + 'KWArgs', + 'Pause', + ) +SETUP_QUESTIONS = { + # AV + 'MSE': {'New': None, 'Fab': None, 'Cur': None, 'HW': False, 'Ninite': True}, + + # LibreOffice + 'LibreOffice': {'New': None, 'Fab': None, 'Cur': None, 'HW': False, 'Ninite': True}, + + # Ninite + 'Base': {'New': True, 'Fab': True, 'Cur': True, 'HW': False, 'Ninite': True}, + 'Missing': {'New': False, 'Fab': True, 'Cur': False, 'HW': False, 'Ninite': True}, + 'Standard': {'New': True, 'Fab': True, 'Cur': False, 'HW': False, 'Ninite': True}, + } +# pylint: enable=bad-whitespace,line-too-long + + +# Functions +def check_os_and_abort(): + """Check OS and prompt to abort if not supported.""" + result = try_and_print( + message='OS support status...', + function=check_os_support_status, + cs='GOOD', + ) + if not result['CS'] and 'Unsupported' in result['Error']: + print_warning('OS version not supported by this script') + if not ask('Continue anyway? (NOT RECOMMENDED)'): + abort() + + +def get_actions(setup_mode, answers): + """Get actions to perform based on setup_mode, returns OrderedDict.""" + actions = OrderedDict({}) + for _key, _val in SETUP_ACTIONS.items(): + _action = {} + _if_answer = _val.get('If answer', False) + _win10_only = _val.get('Win10 only', False) + + # Set enabled status + _enabled = _val.get(setup_mode, False) + if _if_answer: + _enabled = _enabled and answers[_if_answer] + if _win10_only: + _enabled = _enabled and global_vars['OS']['Version'] == '10' + _action['Enabled'] = _enabled + + # Set other keys + for _sub_key in SETUP_ACTION_KEYS: + _action[_sub_key] = _val.get(_sub_key, None) + + # Fix KWArgs + if _action.get('KWArgs', {}) is None: + _action['KWArgs'] = {} + + # Handle "special" actions + if _key == 'KIT_NAME_FULL': + # Cleanup WK folders + _key = KIT_NAME_FULL + _action['KWArgs'] = {'folder_path': global_vars['ClientDir']} + elif _key == 'Ninite bundle': + # Add install_ninite_bundle() kwargs + _action['KWArgs'].update({ + kw.lower(): kv for kw, kv in answers.items() + if SETUP_QUESTIONS.get(kw, {}).get('Ninite', False) + }) + elif _key == 'Explorer (user)': + # Explorer settings (user) + _action['KWArgs'] = {'setup_mode': setup_mode} + + # Add to dict + actions[_key] = _action + + return actions + + +def get_answers(setup_mode): + """Get setup answers based on setup_mode and user input, returns dict.""" + answers = {k: v.get(setup_mode, False) for k, v in SETUP_QUESTIONS.items()} + + # Answer setup questions as needed + if answers['MSE'] is None and global_vars['OS']['Version'] == '7': + answers.update(get_av_selection()) + + if answers['LibreOffice'] is None: + answers['LibreOffice'] = ask('Install LibreOffice?') + + return answers + + +def get_av_selection(): + """Get AV selection.""" + av_answers = { + 'MSE': False, + } + av_options = [ + { + 'Name': 'Microsoft Security Essentials', + 'Disabled': global_vars['OS']['Version'] not in ['7'], + }, + ] + actions = [ + {'Name': 'None', 'Letter': 'N'}, + {'Name': 'Quit', 'Letter': 'Q'}, + ] + + # Show menu + selection = menu_select( + 'Please select an option to install', + main_entries=av_options, + action_entries=actions) + if selection.isnumeric(): + index = int(selection) - 1 + if 'Microsoft' in av_options[index]['Name']: + av_answers['MSE'] = True + elif selection == 'Q': + abort() + + return av_answers + + +def get_mode(): + """Get mode via menu_select, returns str.""" + setup_mode = None + mode_options = [ + {'Name': 'New', 'Display Name': 'New / Clean install (no data)'}, + {'Name': 'Data', 'Display Name': 'Clean install with data migration'}, + {'Name': 'Cur', 'Display Name': 'Original OS (post-repair or overinstall)'}, + {'Name': 'HW', 'Display Name': 'Hardware service (i.e. no software work)'}, + ] + actions = [ + {'Name': 'Quit', 'Letter': 'Q'}, + ] + + # Get selection + selection = menu_select( + 'Please select a setup mode', + main_entries=mode_options, + action_entries=actions) + if selection.isnumeric(): + index = int(selection) - 1 + setup_mode = mode_options[index]['Name'] + elif selection == 'Q': + abort() + + return setup_mode + + +def main(): + """Main function.""" + stay_awake() + clear_screen() + + # Check installed OS + check_os_and_abort() + + # Get setup mode + setup_mode = get_mode() + + # Get answers to setup questions + answers = get_answers(setup_mode) + + # Get actions to perform + actions = get_actions(setup_mode, answers) + + # Perform actions + for action, values in actions.items(): + kwargs = values.get('KWArgs', {}) + + # Print info lines + if values.get('Info', False): + print_info(action) + continue + + # Print disabled actions + if not values.get('Enabled', False): + show_data( + message='{}...'.format(action), + data='DISABLED', + warning=True, + ) + continue + + # Check Windows activation if requested + if values.get('If not activated', False) and windows_is_activated(): + # Skip + continue + + # Run function + if values.get('Just run', False): + values['Function'](**kwargs) + else: + result = try_and_print( + message='{}...'.format(action), + function=values['Function'], + other_results=OTHER_RESULTS, + **kwargs) + + # Wait for Ninite proc(s) + if action == 'Ninite bundle': + print_standard('Waiting for installations to finish...') + try: + for proc in result['Out']: + proc.wait() + except KeyboardInterrupt: + pass + + # Pause + if values.get('Pause', False): + print_standard(values['Pause']) + pause() + + # Show alert box for SecureBoot issues + try: + check_secure_boot_status(show_alert=True) + except Exception: # pylint: disable=broad-except + # Ignoring exceptions since we just want to show the popup + pass + + # Done + pause('Press Enter to exit... ') + + +if __name__ == '__main__': + try: + main() + exit_script() + except SystemExit as sys_exit: + exit_script(sys_exit.code) + except: # pylint: disable=bare-except + major_exception() diff --git a/.bin/Scripts/update_kit.py b/.bin/Scripts/update_kit.py index 133fe744..77f527aa 100644 --- a/.bin/Scripts/update_kit.py +++ b/.bin/Scripts/update_kit.py @@ -57,6 +57,7 @@ if __name__ == '__main__': # Installers print_info(' Installers') try_and_print(message='Adobe Reader DC...', function=update_adobe_reader_dc, other_results=other_results, width=40) + try_and_print(message='LibreOffice...', function=update_libreoffice, other_results=other_results, width=40) try_and_print(message='Macs Fan Control...', function=update_macs_fan_control, other_results=other_results, width=40) try_and_print(message='MS Office...', function=update_office, other_results=other_results, width=40) try_and_print(message='Visual C++ Runtimes...', function=update_vcredists, other_results=other_results, width=40) diff --git a/.bin/Scripts/user_checklist.py b/.bin/Scripts/user_checklist.py deleted file mode 100644 index 0abd88f3..00000000 --- a/.bin/Scripts/user_checklist.py +++ /dev/null @@ -1,90 +0,0 @@ -# Wizard Kit: User Checklist - -import os -import sys - -# Init -sys.path.append(os.path.dirname(os.path.realpath(__file__))) -from functions.browsers import * -from functions.cleanup import * -from functions.setup import * -init_global_vars() -os.system('title {}: User Checklist Tool'.format(KIT_NAME_FULL)) -set_log_file('User Checklist ({USERNAME}).log'.format(**global_vars['Env'])) - -if __name__ == '__main__': - try: - stay_awake() - clear_screen() - print_info('{}: User Checklist\n'.format(KIT_NAME_FULL)) - other_results = { - 'Warning': { - 'NotInstalledError': 'Not installed', - 'NoProfilesError': 'No profiles found', - }} - answer_config_browsers = ask('Install adblock?') - if answer_config_browsers: - answer_reset_browsers = ask( - 'Reset browsers to safe defaults first?') - if global_vars['OS']['Version'] == '10': - answer_config_classicshell = ask('Configure ClassicShell?') - answer_config_explorer_user = ask('Configure Explorer?') - - # Cleanup - print_info('Cleanup') - try_and_print(message='Desktop...', - function=cleanup_desktop, cs='Done') - - # Scan for supported browsers - print_info('Scanning for browsers') - scan_for_browsers() - - # Homepages - print_info('Current homepages') - list_homepages() - - # Backup - print_info('Backing up browsers') - backup_browsers() - - # Reset - if answer_config_browsers and answer_reset_browsers: - print_info('Resetting browsers') - reset_browsers() - - # Configure - print_info('Configuring programs') - if answer_config_browsers: - install_adblock() - if global_vars['OS']['Version'] == '10': - if answer_config_classicshell: - try_and_print(message='ClassicStart...', - function=config_classicstart, cs='Done') - if answer_config_explorer_user: - try_and_print(message='Explorer...', - function=config_explorer_user, cs='Done') - if (not answer_config_browsers - and not answer_config_classicshell - and not answer_config_explorer_user): - print_warning(' Skipped') - else: - if not answer_config_browsers: - print_warning(' Skipped') - - # Restart Explorer - try_and_print(message='Restarting Explorer...', - function=restart_explorer, cs='Done') - - # Run speedtest - popen_program(['start', '', 'https://fast.com'], shell=True) - - # Done - print_standard('\nDone.') - pause('Press Enter to exit...') - exit_script() - except SystemExit as sys_exit: - exit_script(sys_exit.code) - except: - major_exception() - -# vim: sts=2 sw=2 ts=2 diff --git a/.bin/Scripts/windows_updates.py b/.bin/Scripts/windows_updates.py new file mode 100644 index 00000000..e29f8c48 --- /dev/null +++ b/.bin/Scripts/windows_updates.py @@ -0,0 +1,46 @@ +# Wizard Kit: Windows updates + +import os +import sys + +# Init +sys.path.append(os.path.dirname(os.path.realpath(__file__))) +from functions.windows_updates import * +init_global_vars() +os.system('title {}: Windows Updates Tool'.format(KIT_NAME_FULL)) +set_log_file('Windows Updates Tool.log') + +if __name__ == '__main__': + try: + clear_screen() + print_info('{}: Windows Updates Tool\n'.format(KIT_NAME_FULL)) + + # Check args + if '--disable' in sys.argv: + disable_windows_updates() + elif '--enable' in sys.argv: + enable_windows_updates() + else: + print_error('Bad mode.') + abort() + + # Done + exit_script() + except GenericError as err: + # Failed to complete request, show error(s) and prompt tech + print_standard(' ') + for line in str(err).splitlines(): + print_warning(line) + print_standard(' ') + print_error('Error(s) encountered, see above.') + print_standard(' ') + if '--disable' in sys.argv: + print_standard('Please reboot and try again.') + pause('Press Enter to exit... ') + exit_script(1) + except SystemExit as sys_exit: + exit_script(sys_exit.code) + except: + major_exception() + +# vim: sts=2 sw=2 ts=2 diff --git a/.linux_items/include/airootfs/etc/skel/.aliases b/.linux_items/include/airootfs/etc/skel/.aliases index d6486258..b0068be3 100644 --- a/.linux_items/include/airootfs/etc/skel/.aliases +++ b/.linux_items/include/airootfs/etc/skel/.aliases @@ -34,5 +34,5 @@ alias srsz='sudo rsync -avhzPS --stats --exclude-from="$HOME/.rsync_exclusions"' alias testdisk='sudo testdisk' alias umount='sudo umount' alias unmount='sudo umount' -alias wkclone='sudo ddrescue-tui clone' -alias wkimage='sudo ddrescue-tui image' +alias wkclone='ddrescue-tui clone' +alias wkimage='ddrescue-tui image' diff --git a/Build Linux b/Build Linux index 980c2e77..f9acd897 100755 --- a/Build Linux +++ b/Build Linux @@ -283,8 +283,8 @@ function update_live_env() { fi # WiFi - cp "$ROOT_DIR/.linux_items/known_networks" "/root/known_networks" - echo "add-known-networks" >> "$LIVE_DIR/airootfs/root/customize_airootfs.sh" + cp "$ROOT_DIR/.linux_items/known_networks" "$LIVE_DIR/airootfs/root/known_networks" + echo "add-known-networks --user=$username" >> "$LIVE_DIR/airootfs/root/customize_airootfs.sh" } function update_repo() {