diff --git a/.bin/Scripts/borrowed/sensors-README.md b/.bin/Scripts/borrowed/sensors-README.md deleted file mode 100644 index 11858382..00000000 --- a/.bin/Scripts/borrowed/sensors-README.md +++ /dev/null @@ -1,35 +0,0 @@ -sensors.py -========== -python bindings using ctypes for libsensors3 of the [lm-sensors project](https://github.com/groeck/lm-sensors). The code was written against libsensors 3.3.4. - -For documentation of the low level API see [sensors.h](https://github.com/groeck/lm-sensors/blob/master/lib/sensors.h). For an example of the high level API see [example.py](example.py). - -For a GUI application that displays the sensor readings and is based on this library, take a look at [sensors-unity](https://launchpad.net/sensors-unity). - -Features --------- -* Full access to low level libsensors3 API -* High level iterator API -* unicode handling -* Python2 and Python3 compatible - -Licensing ---------- -LGPLv2 (same as libsensors3) - -Usage Notes ------------ -As Python does not support call by reference for primitive types some of the libsensors API had to be adapted: - -```python -# nr is changed by refrence in the C API -chip_name, nr = sensors.get_detected_chips(None, nr) - -# returns the value. throws on error -val = sensors.get_value(chip, subfeature_nr) -``` - -Missing Features (pull requests are welcome): -* `sensors_subfeature_type` enum -* `sensors_get_subfeature` -* Error handlers diff --git a/.bin/Scripts/borrowed/sensors.py b/.bin/Scripts/borrowed/sensors.py deleted file mode 100644 index 39b00a4f..00000000 --- a/.bin/Scripts/borrowed/sensors.py +++ /dev/null @@ -1,236 +0,0 @@ -""" -@package sensors.py -Python Bindings for libsensors3 - -use the documentation of libsensors for the low level API. -see example.py for high level API usage. - -@author: Pavel Rojtberg (http://www.rojtberg.net) -@see: https://github.com/paroj/sensors.py -@copyright: LGPLv2 (same as libsensors) -""" - -from ctypes import * -import ctypes.util - -_libc = cdll.LoadLibrary(ctypes.util.find_library("c")) -# see https://github.com/paroj/sensors.py/issues/1 -_libc.free.argtypes = [c_void_p] - -_hdl = cdll.LoadLibrary(ctypes.util.find_library("sensors")) - -version = c_char_p.in_dll(_hdl, "libsensors_version").value.decode("ascii") - -class bus_id(Structure): - _fields_ = [("type", c_short), - ("nr", c_short)] - -class chip_name(Structure): - _fields_ = [("prefix", c_char_p), - ("bus", bus_id), - ("addr", c_int), - ("path", c_char_p)] - -class feature(Structure): - _fields_ = [("name", c_char_p), - ("number", c_int), - ("type", c_int)] - - # sensors_feature_type - IN = 0x00 - FAN = 0x01 - TEMP = 0x02 - POWER = 0x03 - ENERGY = 0x04 - CURR = 0x05 - HUMIDITY = 0x06 - MAX_MAIN = 0x7 - VID = 0x10 - INTRUSION = 0x11 - MAX_OTHER = 0x12 - BEEP_ENABLE = 0x18 - -class subfeature(Structure): - _fields_ = [("name", c_char_p), - ("number", c_int), - ("type", c_int), - ("mapping", c_int), - ("flags", c_uint)] - -_hdl.sensors_get_detected_chips.restype = POINTER(chip_name) -_hdl.sensors_get_features.restype = POINTER(feature) -_hdl.sensors_get_all_subfeatures.restype = POINTER(subfeature) -_hdl.sensors_get_label.restype = c_void_p # return pointer instead of str so we can free it -_hdl.sensors_get_adapter_name.restype = c_char_p # docs do not say whether to free this or not -_hdl.sensors_strerror.restype = c_char_p - -### RAW API ### -MODE_R = 1 -MODE_W = 2 -COMPUTE_MAPPING = 4 - -def init(cfg_file = None): - file = _libc.fopen(cfg_file.encode("utf-8"), "r") if cfg_file is not None else None - - if _hdl.sensors_init(file) != 0: - raise Exception("sensors_init failed") - - if file is not None: - _libc.fclose(file) - -def cleanup(): - _hdl.sensors_cleanup() - -def parse_chip_name(orig_name): - ret = chip_name() - err= _hdl.sensors_parse_chip_name(orig_name.encode("utf-8"), byref(ret)) - - if err < 0: - raise Exception(strerror(err)) - - return ret - -def strerror(errnum): - return _hdl.sensors_strerror(errnum).decode("utf-8") - -def free_chip_name(chip): - _hdl.sensors_free_chip_name(byref(chip)) - -def get_detected_chips(match, nr): - """ - @return: (chip, next nr to query) - """ - _nr = c_int(nr) - - if match is not None: - match = byref(match) - - chip = _hdl.sensors_get_detected_chips(match, byref(_nr)) - chip = chip.contents if bool(chip) else None - return chip, _nr.value - -def chip_snprintf_name(chip, buffer_size=200): - """ - @param buffer_size defaults to the size used in the sensors utility - """ - ret = create_string_buffer(buffer_size) - err = _hdl.sensors_snprintf_chip_name(ret, buffer_size, byref(chip)) - - if err < 0: - raise Exception(strerror(err)) - - return ret.value.decode("utf-8") - -def do_chip_sets(chip): - """ - @attention this function was not tested - """ - err = _hdl.sensors_do_chip_sets(byref(chip)) - if err < 0: - raise Exception(strerror(err)) - -def get_adapter_name(bus): - return _hdl.sensors_get_adapter_name(byref(bus)).decode("utf-8") - -def get_features(chip, nr): - """ - @return: (feature, next nr to query) - """ - _nr = c_int(nr) - feature = _hdl.sensors_get_features(byref(chip), byref(_nr)) - feature = feature.contents if bool(feature) else None - return feature, _nr.value - -def get_label(chip, feature): - ptr = _hdl.sensors_get_label(byref(chip), byref(feature)) - val = cast(ptr, c_char_p).value.decode("utf-8") - _libc.free(ptr) - return val - -def get_all_subfeatures(chip, feature, nr): - """ - @return: (subfeature, next nr to query) - """ - _nr = c_int(nr) - subfeature = _hdl.sensors_get_all_subfeatures(byref(chip), byref(feature), byref(_nr)) - subfeature = subfeature.contents if bool(subfeature) else None - return subfeature, _nr.value - -def get_value(chip, subfeature_nr): - val = c_double() - err = _hdl.sensors_get_value(byref(chip), subfeature_nr, byref(val)) - if err < 0: - raise Exception(strerror(err)) - return val.value - -def set_value(chip, subfeature_nr, value): - """ - @attention this function was not tested - """ - val = c_double(value) - err = _hdl.sensors_set_value(byref(chip), subfeature_nr, byref(val)) - if err < 0: - raise Exception(strerror(err)) - -### Convenience API ### -class ChipIterator: - def __init__(self, match = None): - self.match = parse_chip_name(match) if match is not None else None - self.nr = 0 - - def __iter__(self): - return self - - def __next__(self): - chip, self.nr = get_detected_chips(self.match, self.nr) - - if chip is None: - raise StopIteration - - return chip - - def __del__(self): - if self.match is not None: - free_chip_name(self.match) - - def next(self): # python2 compability - return self.__next__() - -class FeatureIterator: - def __init__(self, chip): - self.chip = chip - self.nr = 0 - - def __iter__(self): - return self - - def __next__(self): - feature, self.nr = get_features(self.chip, self.nr) - - if feature is None: - raise StopIteration - - return feature - - def next(self): # python2 compability - return self.__next__() - -class SubFeatureIterator: - def __init__(self, chip, feature): - self.chip = chip - self.feature = feature - self.nr = 0 - - def __iter__(self): - return self - - def __next__(self): - subfeature, self.nr = get_all_subfeatures(self.chip, self.feature, self.nr) - - if subfeature is None: - raise StopIteration - - return subfeature - - def next(self): # python2 compability - return self.__next__() diff --git a/.bin/Scripts/build-ufd b/.bin/Scripts/build-ufd index c606892b..1253472c 100755 --- a/.bin/Scripts/build-ufd +++ b/.bin/Scripts/build-ufd @@ -533,6 +533,9 @@ echo -e "${GREEN}Building Kit${CLEAR}" touch "${LOG_FILE}" tmux split-window -dl 10 tail -f "${LOG_FILE}" +# Zero beginning of device +dd bs=4M count=16 if=/dev/zero of="${DEST_DEV}" >> "${LOG_FILE}" 2>&1 + # Format echo "Formatting drive..." if [[ "${USE_MBR}" == "True" ]]; then diff --git a/.bin/Scripts/ddrescue-tui-smart-display b/.bin/Scripts/ddrescue-tui-smart-display deleted file mode 100755 index 285229d6..00000000 --- a/.bin/Scripts/ddrescue-tui-smart-display +++ /dev/null @@ -1,39 +0,0 @@ -#!/bin/python3 -# -## Wizard Kit: SMART attributes display for ddrescue TUI - -import os -import sys -import time - -# Init -os.chdir(os.path.dirname(os.path.realpath(__file__))) -sys.path.append(os.getcwd()) -from functions.hw_diags import * -#init_global_vars() - -if __name__ == '__main__': - try: - # Prep - clear_screen() - dev_path = sys.argv[1] - devs = scan_disks(True, dev_path) - - # Warn if SMART unavailable - if dev_path not in devs: - print_error('SMART data not available') - exit_script() - - # Initial screen - dev = devs[dev_path] - clear_screen() - show_disk_details(dev, only_attributes=True) - - # Done - exit_script() - except SystemExit: - pass - except: - major_exception() - -# vim: sts=4 sw=4 ts=4 diff --git a/.bin/Scripts/functions/common.py b/.bin/Scripts/functions/common.py index ee222937..21c0b62d 100644 --- a/.bin/Scripts/functions/common.py +++ b/.bin/Scripts/functions/common.py @@ -25,12 +25,14 @@ global_vars = {} # STATIC VARIABLES COLORS = { - 'CLEAR': '\033[0m', - 'RED': '\033[31m', - 'GREEN': '\033[32m', + 'CLEAR': '\033[0m', + 'RED': '\033[31m', + 'ORANGE': '\033[31;1m', + 'GREEN': '\033[32m', 'YELLOW': '\033[33m', - 'BLUE': '\033[34m' -} + 'BLUE': '\033[34m', + 'CYAN': '\033[36m', + } try: HKU = winreg.HKEY_USERS HKCR = winreg.HKEY_CLASSES_ROOT @@ -305,20 +307,20 @@ def major_exception(): except GenericAbort: # User declined upload print_warning('Upload: Aborted') - sleep(30) + sleep(10) except GenericError: # No log file or uploading disabled - sleep(30) + sleep(10) except: print_error('Upload: NS') - sleep(30) + sleep(10) else: print_success('Upload: CS') pause('Press Enter to exit...') exit_script(1) def menu_select(title='~ Untitled Menu ~', - prompt='Please make a selection', secret_exit=False, + prompt='Please make a selection', secret_actions=[], secret_exit=False, main_entries=[], action_entries=[], disabled_label='DISABLED', spacer=''): """Display options in a menu and return selected option as a str.""" @@ -334,8 +336,10 @@ def menu_select(title='~ Untitled Menu ~', menu_splash = '{}\n{}\n'.format(title, spacer) width = len(str(len(main_entries))) valid_answers = [] - if (secret_exit): + if secret_exit: valid_answers.append('Q') + if secret_actions: + valid_answers.extend(secret_actions) # Add main entries for i in range(len(main_entries)): @@ -367,7 +371,6 @@ def menu_select(title='~ Untitled Menu ~', letter = entry['Letter'].upper(), width = len(str(len(action_entries))), name = entry['Name']) - menu_splash += '\n' answer = '' @@ -403,19 +406,24 @@ def ping(addr='google.com'): def popen_program(cmd, pipe=False, minimized=False, shell=False, **kwargs): """Run program and return a subprocess.Popen object.""" - startupinfo=None + cmd_kwargs = {'args': cmd, 'shell': shell} + if minimized: startupinfo = subprocess.STARTUPINFO() startupinfo.dwFlags = subprocess.STARTF_USESHOWWINDOW startupinfo.wShowWindow = 6 + cmd_kwargs['startupinfo'] = startupinfo if pipe: - popen_obj = subprocess.Popen(cmd, shell=shell, startupinfo=startupinfo, - stdout=subprocess.PIPE, stderr=subprocess.PIPE) - else: - popen_obj = subprocess.Popen(cmd, shell=shell, startupinfo=startupinfo) + cmd_kwargs.update({ + 'stdout': subprocess.PIPE, + 'stderr': subprocess.PIPE, + }) - return popen_obj + if 'cwd' in kwargs: + cmd_kwargs['cwd'] = kwargs['cwd'] + + return subprocess.Popen(**cmd_kwargs) def print_error(*args, **kwargs): """Prints message to screen in RED.""" @@ -454,7 +462,7 @@ def print_log(message='', end='\n', timestamp=True): line = line, end = end)) -def run_program(cmd, args=[], check=True, pipe=True, shell=False): +def run_program(cmd, args=[], check=True, pipe=True, shell=False, **kwargs): """Run program and return a subprocess.CompletedProcess object.""" if args: # Deprecated so let's raise an exception to find & fix all occurances @@ -464,13 +472,18 @@ def run_program(cmd, args=[], check=True, pipe=True, shell=False): if shell: cmd = ' '.join(cmd) - if pipe: - process_return = subprocess.run(cmd, check=check, shell=shell, - stdout=subprocess.PIPE, stderr=subprocess.PIPE) - else: - process_return = subprocess.run(cmd, check=check, shell=shell) + cmd_kwargs = {'args': cmd, 'check': check, 'shell': shell} - return process_return + if pipe: + cmd_kwargs.update({ + 'stdout': subprocess.PIPE, + 'stderr': subprocess.PIPE, + }) + + if 'cwd' in kwargs: + cmd_kwargs['cwd'] = kwargs['cwd'] + + return subprocess.run(**cmd_kwargs) def set_title(title='~Some Title~'): """Set title. @@ -513,6 +526,12 @@ def stay_awake(): print_error('ERROR: No caffeine available.') print_warning('Please set the power setting to High Performance.') +def strip_colors(s): + """Remove all ASCII color escapes from string, returns str.""" + for c in COLORS.values(): + s = s.replace(c, '') + return s + def get_exception(s): """Get exception by name, returns Exception object.""" try: @@ -636,9 +655,10 @@ def wait_for_process(name, poll_rate=3): sleep(1) # global_vars functions -def init_global_vars(): +def init_global_vars(silent=False): """Sets global variables based on system info.""" - print_info('Initializing') + if not silent: + print_info('Initializing') if psutil.WINDOWS: os.system('title Wizard Kit') if psutil.LINUX: @@ -656,10 +676,14 @@ def init_global_vars(): ['Clearing collisions...', clean_env_vars], ] try: - for f in init_functions: - try_and_print( - message=f[0], function=f[1], - cs='Done', ns='Error', catch_all=False) + if silent: + for f in init_functions: + f[1]() + else: + for f in init_functions: + try_and_print( + message=f[0], function=f[1], + cs='Done', ns='Error', catch_all=False) except: major_exception() diff --git a/.bin/Scripts/functions/ddrescue.py b/.bin/Scripts/functions/ddrescue.py index 7f1fc7aa..6e515655 100644 --- a/.bin/Scripts/functions/ddrescue.py +++ b/.bin/Scripts/functions/ddrescue.py @@ -8,8 +8,11 @@ import signal import stat import time +from collections import OrderedDict from functions.common import * from functions.data import * +from functions.hw_diags import * +from functions.tmux import * from operator import itemgetter # STATIC VARIABLES @@ -30,6 +33,11 @@ DDRESCUE_SETTINGS = { } RECOMMENDED_FSTYPES = ['ext3', 'ext4', 'xfs'] SIDE_PANE_WIDTH = 21 +TMUX_LAYOUT = OrderedDict({ + 'Source': {'y': 2, 'Check': True}, + 'Started': {'x': SIDE_PANE_WIDTH, 'Check': True}, + 'Progress': {'x': SIDE_PANE_WIDTH, 'Check': True}, +}) USAGE = """ {script_name} clone [source [destination]] {script_name} image [source [destination]] (e.g. {script_name} clone /dev/sda /dev/sdb) @@ -276,6 +284,7 @@ class RecoveryState(): self.current_pass_str = '0: Initializing' self.settings = DDRESCUE_SETTINGS.copy() self.finished = False + self.panes = {} self.progress_out = '{}/progress.out'.format(global_vars['LogDir']) self.rescued = 0 self.resumed = False @@ -283,6 +292,7 @@ class RecoveryState(): self.total_size = 0 if mode not in ('clone', 'image'): raise GenericError('Unsupported mode') + self.get_smart_source() def add_block_pair(self, source, dest): """Run safety checks and append new BlockPair to internal list.""" @@ -341,6 +351,14 @@ class RecoveryState(): min_percent = min(min_percent, bp.rescued_percent) return min_percent + def get_smart_source(self): + """Get source for SMART dispay.""" + disk_path = self.source.path + if self.source.parent: + disk_path = self.source.parent + + self.smart_source = DiskObj(disk_path) + def retry_all_passes(self): """Mark all passes as pending for all block-pairs.""" self.finished = False @@ -351,7 +369,34 @@ class RecoveryState(): self.set_pass_num() def self_checks(self): - """Run self-checks for each BlockPair and update state values.""" + """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 = {} + + # Avoid saving map to non-persistent filesystem + try: + result = run_program(cmd) + json_data = json.loads(result.stdout.decode()) + except Exception: + print_error('ERROR: Failed to verify map path') + raise GenericAbort() + fstype = json_data.get( + 'filesystems', [{}])[0].get( + 'fstype', 'unknown') + if fstype not in map_allowed_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())) + 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() @@ -398,46 +443,22 @@ class RecoveryState(): # Functions def build_outer_panes(state): """Build top and side panes.""" - clear_screen() - result = run_program(['tput', 'cols']) - width = int( - (int(result.stdout.decode().strip()) - SIDE_PANE_WIDTH) / 2) - 2 - - # Top panes - source_str = state.source.name - if len(source_str) > width: - source_str = '{}...'.format(source_str[:width-3]) - 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:]) - source_pane = tmux_splitw( - '-bdvl', '2', - '-PF', '#D', - 'echo-and-hold "{BLUE}Source{CLEAR}\n{text}"'.format( - text=source_str, - **COLORS)) - tmux_splitw( - '-t', source_pane, - '-dhl', '{}'.format(SIDE_PANE_WIDTH), - 'echo-and-hold "{BLUE}Started{CLEAR}\n{text}"'.format( - text=time.strftime("%Y-%m-%d %H:%M %Z"), - **COLORS)) - tmux_splitw( - '-t', source_pane, - '-dhp', '50', - 'echo-and-hold "{BLUE}Destination{CLEAR}\n{text}"'.format( - text=dest_str, + 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) - tmux_splitw( - '-dhl', str(SIDE_PANE_WIDTH), - 'watch', '--color', '--no-title', '--interval', '1', - 'cat', state.progress_out) + state.panes['Progress'] = tmux_split_window( + lines=SIDE_PANE_WIDTH, watch=state.progress_out) def create_path_obj(path): @@ -464,6 +485,94 @@ def double_confirm_clone(): 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.""" try: @@ -660,7 +769,9 @@ 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) menu_main(state) # Done @@ -842,7 +953,8 @@ def read_map_file(map_path): def run_ddrescue(state, pass_settings): """Run ddrescue pass.""" - return_code = None + return_code = -1 + aborted = False if state.finished: clear_screen() @@ -850,29 +962,21 @@ def run_ddrescue(state, pass_settings): pause('Press Enter to return to main menu...') return - # Set heights - # NOTE: 12/33 is based on min heights for SMART/ddrescue panes (12+22+1sep) - result = run_program(['tput', 'lines']) - height = int(result.stdout.decode().strip()) - height_smart = int(height * (8 / 33)) - height_journal = int(height * (4 / 33)) - height_ddrescue = height - height_smart - height_journal - - # Show SMART status - smart_dev = state.source_path - if state.source.parent: - smart_dev = state.source.parent - smart_pane = tmux_splitw( - '-bdvl', str(height_smart), - '-PF', '#D', - 'watch', '--color', '--no-title', '--interval', '300', - 'ddrescue-tui-smart-display', smart_dev) + # 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...') + state.panes['SMART'] = tmux_split_window( + behind=True, lines=12, vertical=True, watch=state.smart_out) # Show systemd journal output - journal_pane = tmux_splitw( - '-dvl', str(height_journal), - '-PF', '#D', - 'journalctl', '-f') + state.panes['Journal'] = tmux_split_window( + lines=4, vertical=True, + command=['sudo', 'journalctl', '-f']) + + # Fix layout + fix_tmux_panes(state, forced=True) # Run pass for each block-pair for bp in state.block_pairs: @@ -901,21 +1005,40 @@ def run_ddrescue(state, pass_settings): clear_screen() print_info('Current dev: {}'.format(bp.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: + report = state.smart_source.generate_attribute_report( + timestamp=True) + for line in report: + f.write('{}\n'.format(line)) + i += 1 + + # Update progress bp.update_progress(state.current_pass) update_sidepane(state) + + # Fix panes + fix_tmux_panes(state) + + # Check if ddrescue has finished try: - ddrescue_proc.wait(timeout=10) + ddrescue_proc.wait(timeout=1) sleep(2) bp.update_progress(state.current_pass) update_sidepane(state) break except subprocess.TimeoutExpired: - # Catch to update bp/sidepane + # Catch to update smart/bp/sidepane pass + except KeyboardInterrupt: # Catch user abort - pass + aborted = True + ddrescue_proc.wait(timeout=10) # Update progress/sidepane again bp.update_progress(state.current_pass) @@ -923,12 +1046,19 @@ def run_ddrescue(state, pass_settings): # Was ddrescue aborted? return_code = ddrescue_proc.poll() - if return_code is None or return_code is 130: - clear_screen() + if aborted: + print_standard(' ') + print_standard(' ') + print_error('DDRESCUE PROCESS HALTED') + print_standard(' ') print_warning('Aborted') break elif return_code: - # i.e. not None and not 0 + # i.e. True when non-zero + print_standard(' ') + print_standard(' ') + print_error('DDRESCUE PROCESS HALTED') + print_standard(' ') print_error('Error(s) encountered, see message above.') break else: @@ -940,8 +1070,9 @@ def run_ddrescue(state, pass_settings): if str(return_code) != '0': # Pause on errors pause('Press Enter to return to main menu... ') - run_program(['tmux', 'kill-pane', '-t', smart_pane]) - run_program(['tmux', 'kill-pane', '-t', journal_pane]) + + # Cleanup + tmux_kill_pane(state.panes['SMART'], state.panes['Journal']) def select_parts(source_device): @@ -1192,13 +1323,6 @@ def show_usage(script_name): pause() -def tmux_splitw(*args): - """Run tmux split-window command and return output as str.""" - cmd = ['tmux', 'split-window', *args] - result = run_program(cmd) - return result.stdout.decode().strip() - - def update_sidepane(state): """Update progress file for side pane.""" output = [] diff --git a/.bin/Scripts/functions/hw_diags.py b/.bin/Scripts/functions/hw_diags.py index 39789149..9f10f997 100644 --- a/.bin/Scripts/functions/hw_diags.py +++ b/.bin/Scripts/functions/hw_diags.py @@ -1,1670 +1,1675 @@ # Wizard Kit: Functions - HW Diagnostics -import base64 -import Gnuplot import json -import math -import mysql.connector as mariadb -import requests +import re import time -from functions.data import * -from numpy import * - -# Database connection -ost_db = { - 'Connection': None, - 'Cursor': None, - 'Errors': False, - 'Tunnel': None, - } +from collections import OrderedDict +from functions.sensors import * +from functions.tmux import * # STATIC VARIABLES ATTRIBUTES = { - 'NVMe': { - 'critical_warning': {'Error': 1}, - 'media_errors': {'Error': 1}, - 'power_on_hours': {'Warning': 12000, 'Error': 18000, 'Ignore': True}, - 'unsafe_shutdowns': {'Warning': 1}, - }, - 'SMART': { - 5: {'Hex': '05', 'Error': 1}, - 9: {'Hex': '09', 'Warning': 12000, 'Error': 18000, 'Ignore': True}, - 10: {'Hex': '0A', 'Error': 1}, - 184: {'Hex': 'B8', 'Error': 1}, - 187: {'Hex': 'BB', 'Error': 1}, - 188: {'Hex': 'BC', 'Error': 1}, - 196: {'Hex': 'C4', 'Error': 1}, - 197: {'Hex': 'C5', 'Error': 1}, - 198: {'Hex': 'C6', 'Error': 1}, - 199: {'Hex': 'C7', 'Error': 1, 'Ignore': True}, - 201: {'Hex': 'C9', 'Error': 1}, - }, - } + 'NVMe': { + 'critical_warning': {'Error': 1, 'Critical': True}, + 'media_errors': {'Error': 1, 'Critical': True}, + 'power_on_hours': {'Warning': 12000, 'Error': 26298, 'Ignore': True}, + 'unsafe_shutdowns': {'Warning': 1}, + }, + 'SMART': { + 5: {'Hex': '05', 'Error': 1, 'Critical': True}, + 9: {'Hex': '09', 'Warning': 12000, 'Error': 26298, 'Ignore': True}, + 10: {'Hex': '0A', 'Error': 1}, + 184: {'Hex': 'B8', 'Error': 1}, + 187: {'Hex': 'BB', 'Error': 1}, + 188: {'Hex': 'BC', 'Error': 1}, + 196: {'Hex': 'C4', 'Error': 1}, + 197: {'Hex': 'C5', 'Error': 1, 'Critical': True}, + 198: {'Hex': 'C6', 'Error': 1, 'Critical': True}, + 199: {'Hex': 'C7', 'Error': 1, 'Ignore': True}, + 201: {'Hex': 'C9', 'Error': 1}, + }, + } +HW_OVERRIDES_FORCED = HW_OVERRIDES_FORCED and not HW_OVERRIDES_LIMITED IO_VARS = { - 'Block Size': 512*1024, - 'Chunk Size': 32*1024**2, - 'Minimum Dev Size': 8*1024**3, - 'Minimum Test Size': 10*1024**3, - 'Alt Test Size Factor': 0.01, - 'Progress Refresh Rate': 5, - 'Scale 8': [2**(0.56*(x+1))+(16*(x+1)) for x in range(8)], - 'Scale 16': [2**(0.56*(x+1))+(16*(x+1)) for x in range(16)], - 'Scale 32': [2**(0.56*(x+1)/2)+(16*(x+1)/2) for x in range(32)], - 'Threshold Graph Fail': 65*1024**2, - 'Threshold Graph Warn': 135*1024**2, - 'Threshold Graph Great': 750*1024**2, - 'Threshold HDD Min': 50*1024**2, - 'Threshold HDD High Avg': 75*1024**2, - 'Threshold HDD Low Avg': 65*1024**2, - 'Threshold SSD Min': 90*1024**2, - 'Threshold SSD High Avg': 135*1024**2, - 'Threshold SSD Low Avg': 100*1024**2, - 'Graph Horizontal': ('▁', '▂', '▃', '▄', '▅', '▆', '▇', '█'), - 'Graph Horizontal Width': 40, - 'Graph Vertical': ( - '▏', '▎', '▍', '▌', - '▋', '▊', '▉', '█', - '█▏', '█▎', '█▍', '█▌', - '█▋', '█▊', '█▉', '██', - '██▏', '██▎', '██▍', '██▌', - '██▋', '██▊', '██▉', '███', - '███▏', '███▎', '███▍', '███▌', - '███▋', '███▊', '███▉', '████'), - } -OST_STAFF_ID = '23' -OST_STAFF_NAME = 'Wizard Kit' -OST_SQL_SET_HOLD = "UPDATE `{db_name}`.`ost_ticket` SET `hold` = '{hold_type}' WHERE `ost_ticket`.`ticket_id` = {ticket_id};" -OST_SQL_SET_FLAG = "UPDATE `{db_name}`.`ost_ticket` SET `{flag}` = '{value}' WHERE `ost_ticket`.`ticket_id` = {ticket_id};" -OST_SQL_POST_REPLY = ("INSERT INTO `{db_name}`.`ost_ticket_response` (ticket_id, staff_id, staff_name, response, created) " - "VALUES ('{ticket_id}', '{staff_id}', '{staff_name}', '{response}', '{created}');") -OST_DRIVE_FLAG = 'zHDTune' -OST_DRIVE_PASSED = 1 -OST_DRIVE_FAILED = 2 -OST_NEEDS_ATTENTION = 4 -TESTS = { - 'Prime95': { - 'Enabled': False, - 'Status': 'Pending', - }, - 'NVMe/SMART': { - 'Enabled': False, - 'Quick': False, - 'Short Test': {}, - 'Status': {}, - }, - 'badblocks': { - 'Enabled': False, - 'Results': {}, - 'Status': {}, - }, - 'iobenchmark': { - 'Data': {}, - 'Enabled': False, - 'Results': {}, - 'Status': {}, - }, - } + 'Block Size': 512*1024, + 'Chunk Size': 32*1024**2, + 'Minimum Test Size': 10*1024**3, + 'Alt Test Size Factor': 0.01, + 'Progress Refresh Rate': 5, + 'Scale 8': [2**(0.56*(x+1))+(16*(x+1)) for x in range(8)], + 'Scale 16': [2**(0.56*(x+1))+(16*(x+1)) for x in range(16)], + 'Scale 32': [2**(0.56*(x+1)/2)+(16*(x+1)/2) for x in range(32)], + 'Threshold Graph Fail': 65*1024**2, + 'Threshold Graph Warn': 135*1024**2, + 'Threshold Graph Great': 750*1024**2, + 'Threshold HDD Min': 50*1024**2, + 'Threshold HDD High Avg': 75*1024**2, + 'Threshold HDD Low Avg': 65*1024**2, + 'Threshold SSD Min': 90*1024**2, + 'Threshold SSD High Avg': 135*1024**2, + 'Threshold SSD Low Avg': 100*1024**2, + 'Graph Horizontal': ('▁', '▂', '▃', '▄', '▅', '▆', '▇', '█'), + 'Graph Horizontal Width': 40, + 'Graph Vertical': ( + '▏', '▎', '▍', '▌', + '▋', '▊', '▉', '█', + '█▏', '█▎', '█▍', '█▌', + '█▋', '█▊', '█▉', '██', + '██▏', '██▎', '██▍', '██▌', + '██▋', '██▊', '██▉', '███', + '███▏', '███▎', '███▍', '███▌', + '███▋', '███▊', '███▉', '████'), + } +KEY_NVME = 'nvme_smart_health_information_log' +KEY_SMART = 'ata_smart_attributes' +QUICK_LABEL = '{YELLOW}(Quick){CLEAR}'.format(**COLORS) +SIDE_PANE_WIDTH = 20 +TESTS_CPU = ['Prime95'] +TESTS_DISK = [ + 'I/O Benchmark', + 'NVMe / SMART', + 'badblocks', + ] +TOP_PANE_TEXT = '{GREEN}Hardware Diagnostics{CLEAR}'.format(**COLORS) +TMUX_LAYOUT = OrderedDict({ + 'Top': {'y': 2, 'Check': True}, + 'Started': {'x': SIDE_PANE_WIDTH, 'Check': True}, + 'Progress': {'x': SIDE_PANE_WIDTH, 'Check': True}, +}) -def connect_to_db(): - """Connect to osTicket database via SSH tunnel.""" - cmd = [ - 'ssh', '-N', '-p', SSH_PORT, '-L3306:127.0.0.1:3306', - '{user}@{host}'.format(user=SSH_USER, host=DB_HOST), - ] +# Error Classe +class DeviceTooSmallError(Exception): + pass - # Establish SSH tunnel unless one already exists - if not ost_db['Tunnel'] or ost_db['Tunnel'].poll() is not None: - ost_db['Tunnel'] = popen_program(cmd) +# Classes +class CpuObj(): + """Object for tracking CPU specific data.""" + def __init__(self): + self.lscpu = {} + self.tests = OrderedDict() + self.get_details() + self.name = self.lscpu.get('Model name', 'Unknown CPU') - # Establish SQL connection (try a few times in case SSH is slow) - for x in range(5): - sleep(2) - try: - ost_db['Connection'] = mariadb.connect( - user=DB_USER, password=DB_PASS, database=DB_NAME) - ost_db['Cursor'] = ost_db['Connection'].cursor() - except: - # Just try again - pass - else: - break - -def disconnect_from_db(reset_errors=False): - """Disconnect from SQL DB.""" - for c in ['Cursor', 'Connection']: - try: - ost_db[c].close() - except: - # Ignore - pass - ost_db[c] = None - if reset_errors: - ost_db['Errors'] = False - -def export_png_graph(name, dev): - """Exports PNG graph using gnuplot, returns file path as str.""" - max_rate = max(TESTS['iobenchmark']['Data'][name]['Read Rates']) - max_rate /= (1024**2) - max_rate = max(800, max_rate) - out_path = '{}/iobenchmark-{}.png'.format(global_vars['LogDir'], name) - plot_data = '{}/iobenchmark-{}-raw.log'.format(global_vars['LogDir'], name) - - # Adjust Y-axis range to either 1000 or roughly max rate + 200 - ## Round up to the nearest 100 and then add 200 - y_range = (math.ceil(max_rate/100)*100) + 200 - - # Run gnuplot commands - g = Gnuplot.Gnuplot() - g('reset') - g('set output "{}"'.format(out_path)) - g('set terminal png large size 660,300 truecolor font "Noto Sans,11"') - g('set title "I/O Benchmark"') - g('set yrange [0:{}]'.format(y_range)) - g('set style data lines') - g('plot "{data}" title "{size} ({tran}) {model} {serial}"'.format( - data=plot_data, - size=dev['lsblk'].get('size', '???b'), - tran=dev['lsblk'].get('tran', '???'), - model=dev['lsblk'].get('model', 'Unknown Model'), - serial=dev['lsblk'].get('serial', 'Unknown Serial'), - )) - - # Cleanup - g.close() - del(g) - - return out_path - -def generate_horizontal_graph(rates, oneline=False): - """Generate two-line horizontal graph from rates, returns str.""" - line_1 = '' - line_2 = '' - line_3 = '' - line_4 = '' - for r in rates: - step = get_graph_step(r, scale=32) - if oneline: - step = get_graph_step(r, scale=8) - - # Set color - r_color = COLORS['CLEAR'] - if r < IO_VARS['Threshold Graph Fail']: - r_color = COLORS['RED'] - elif r < IO_VARS['Threshold Graph Warn']: - r_color = COLORS['YELLOW'] - elif r > IO_VARS['Threshold Graph Great']: - r_color = COLORS['GREEN'] - - # Build graph - full_block = '{}{}'.format(r_color, IO_VARS['Graph Horizontal'][-1]) - if step >= 24: - line_1 += '{}{}'.format(r_color, IO_VARS['Graph Horizontal'][step-24]) - line_2 += full_block - line_3 += full_block - line_4 += full_block - elif step >= 16: - line_1 += ' ' - line_2 += '{}{}'.format(r_color, IO_VARS['Graph Horizontal'][step-16]) - line_3 += full_block - line_4 += full_block - elif step >= 8: - line_1 += ' ' - line_2 += ' ' - line_3 += '{}{}'.format(r_color, IO_VARS['Graph Horizontal'][step-8]) - line_4 += full_block - else: - line_1 += ' ' - line_2 += ' ' - line_3 += ' ' - line_4 += '{}{}'.format(r_color, IO_VARS['Graph Horizontal'][step]) - line_1 += COLORS['CLEAR'] - line_2 += COLORS['CLEAR'] - line_3 += COLORS['CLEAR'] - line_4 += COLORS['CLEAR'] - if oneline: - return line_4 - else: - return '\n'.join([line_1, line_2, line_3, line_4]) - -def get_graph_step(rate, scale=16): - """Get graph step based on rate and scale, returns int.""" - m_rate = rate / (1024**2) - step = 0 - scale_name = 'Scale {}'.format(scale) - for x in range(scale-1, -1, -1): - # Iterate over scale backwards - if m_rate >= IO_VARS[scale_name][x]: - step = x - break - return step - -def get_osticket_number(): - """Get ticket number and confirm with name from osTicket DB.""" - ticket_number = None - if not ost_db['Cursor']: - # No DB access, return None to disable integration - return None - while ticket_number is None: - print_standard(' ') - _input = input('Enter ticket number (or leave blank to disable): ') - if re.match(r'^\s*$', _input): - if ask('Disable osTicket integration for this run?'): - return None - else: - continue - if not re.match(r'^([0-9]+)$', _input): - continue - _name = osticket_get_ticket_name(_input) - if _name: - print_standard('You have selected ticket #{} {}'.format( - _input, _name)) - if ask('Is this correct?'): - ticket_number = _input - return ticket_number - -def get_read_rate(s): - """Get read rate in bytes/s from dd progress output.""" - real_rate = None - if re.search(r'[KMGT]B/s', s): - human_rate = re.sub(r'^.*\s+(\d+\.?\d*)\s+(.B)/s\s*$', r'\1 \2', s) - real_rate = convert_to_bytes(human_rate) - return real_rate - -def get_smart_details(dev): - """Get SMART data for dev if possible, returns dict.""" - cmd = 'sudo smartctl --all --json {}{}'.format( - '' if '/dev/' in dev else '/dev/', - dev).split() - result = run_program(cmd, check=False) + def get_details(self): + """Get CPU details from lscpu.""" + cmd = ['lscpu', '--json'] try: - return json.loads(result.stdout.decode()) + result = run_program(cmd, check=False) + json_data = json.loads(result.stdout.decode()) except Exception: - # Let other sections deal with the missing data - return {} - -def get_smart_value(smart_data, smart_id): - """Get SMART value from table, returns int or None.""" - value = None - table = smart_data.get('ata_smart_attributes', {}).get('table', []) - for row in table: - if str(row.get('id', '?')) == str(smart_id): - value = row.get('raw', {}).get('value', None) - return value - -def get_status_color(s): - """Get color based on status, returns str.""" - color = COLORS['CLEAR'] - if s in ['Denied', 'ERROR', 'NS', 'OVERRIDE']: - color = COLORS['RED'] - elif s in ['Aborted', 'Unknown', 'Working', 'Skipped']: - color = COLORS['YELLOW'] - elif s in ['CS']: - color = COLORS['GREEN'] - return color - -def menu_diags(*args): - """Main HW-Diagnostic menu.""" - diag_modes = [ - {'Name': 'All tests', - 'Tests': ['Prime95', 'NVMe/SMART', 'badblocks', 'iobenchmark']}, - {'Name': 'Prime95', - 'Tests': ['Prime95']}, - {'Name': 'All drive tests', - 'Tests': ['NVMe/SMART', 'badblocks', 'iobenchmark']}, - {'Name': 'NVMe/SMART', - 'Tests': ['NVMe/SMART']}, - {'Name': 'badblocks', - 'Tests': ['badblocks']}, - {'Name': 'I/O Benchmark', - 'Tests': ['iobenchmark']}, - {'Name': 'Quick drive test', - 'Tests': ['Quick', 'NVMe/SMART']}, - ] - actions = [ - {'Letter': 'A', 'Name': 'Audio test'}, - {'Letter': 'K', 'Name': 'Keyboard test'}, - {'Letter': 'N', 'Name': 'Network test'}, - {'Letter': 'M', 'Name': 'Screen Saver - Matrix', 'CRLF': True}, - {'Letter': 'P', 'Name': 'Screen Saver - Pipes'}, - {'Letter': 'Q', 'Name': 'Quit', 'CRLF': True}, - ] - - # CLI-mode actions - if 'DISPLAY' not in global_vars['Env']: - actions.extend([ - {'Letter': 'R', 'Name': 'Reboot', 'CRLF': True}, - {'Letter': 'S', 'Name': 'Shutdown'}, - ]) - - # Quick disk check - if 'quick' in args: - run_tests(['Quick', 'NVMe/SMART']) - exit_script() - - # Show menu - while True: - selection = menu_select( - title = 'Hardware Diagnostics: Menu', - main_entries = diag_modes, - action_entries = actions, - spacer = '──────────────────────────') - if selection.isnumeric(): - ticket_number = None - if diag_modes[int(selection)-1]['Name'] != 'Quick drive test': - clear_screen() - print_standard(' ') - result = try_and_print( - message='Connecting to osTicket database...', - function=connect_to_db, - width=40) - if not result['CS']: - print_warning('osTicket integration disabled for this run.') - pause() - ticket_number = get_osticket_number() - disconnect_from_db() - # Save log for non-quick tests - global_vars['Date-Time'] = time.strftime("%Y-%m-%d_%H%M_%z") - global_vars['LogDir'] = '{}/Logs/{}_{}'.format( - global_vars['Env']['HOME'], - ticket_number, - global_vars['Date-Time']) - os.makedirs(global_vars['LogDir'], exist_ok=True) - global_vars['LogFile'] = '{}/Hardware Diagnostics.log'.format( - global_vars['LogDir']) - run_tests(diag_modes[int(selection)-1]['Tests'], ticket_number) - elif selection == 'A': - run_program(['hw-diags-audio'], check=False, pipe=False) - pause('Press Enter to return to main menu... ') - elif selection == 'K': - run_program(['xev', '-event', 'keyboard'], check=False, pipe=False) - elif selection == 'N': - run_program(['hw-diags-network'], check=False, pipe=False) - pause('Press Enter to return to main menu... ') - elif selection == 'M': - run_program(['cmatrix', '-abs'], check=False, pipe=False) - elif selection == 'P': - run_program( - 'pipes -t 0 -t 1 -t 2 -t 3 -p 5 -R -r 4000'.split(), - check=False, pipe=False) - elif selection == 'R': - run_program(['systemctl', 'reboot']) - elif selection == 'S': - run_program(['systemctl', 'poweroff']) - elif selection == 'Q': - break - - # Done - disconnect_from_db(reset_errors=True) - -def osticket_get_ticket_name(ticket_id): - """Lookup ticket and return name as str.""" - ticket_name = 'Unknown' - if not ticket_id: - raise GenericError - if not ost_db['Cursor']: - # Skip section - return - - # Set command - sql_cmd = "SELECT name FROM `ost_ticket` WHERE `ticket_id` = {}".format( - ticket_id) - - # Run command - try: - ost_db['Cursor'].execute(sql_cmd) - for name in ost_db['Cursor']: - ticket_name = name[0] - return ticket_name - except: - ost_db['Errors'] = True - -def osticket_needs_attention(ticket_id): - """[DISABLED] Marks the ticket as "NEEDS ATTENTION" in osTicket.""" - return # This function has been DISABLED due to a repurposing of that flag - if not ticket_id: - raise GenericError - - # Connect to DB - connect_to_db() - if not ost_db['Cursor']: - # Skip section - return - - # Set command - sql_cmd = OST_SQL_SET_HOLD.format( - db_name=DB_NAME, - hold_type=OST_NEEDS_ATTENTION, - ticket_id=ticket_id) - - # Run command - try: - ost_db['Cursor'].execute(sql_cmd) - except: - ost_db['Errors'] = True - disconnect_from_db() - -def osticket_post_reply(ticket_id, response): - """Post a reply to a ticket in osTicket.""" - if not ticket_id: - raise GenericError - - # Connect to DB - connect_to_db() - if not ost_db['Cursor']: - # Skip section - return - - # Set command - sql_cmd = OST_SQL_POST_REPLY.format( - db_name=DB_NAME, - ticket_id=ticket_id, - staff_id=OST_STAFF_ID, - staff_name=OST_STAFF_NAME, - response=response, - created=time.strftime("%Y-%m-%d %H:%M:%S")) - - # Run command - try: - ost_db['Cursor'].execute(sql_cmd) - except: - ost_db['Errors'] = True - disconnect_from_db() - -def osticket_set_drive_result(ticket_id, passed): - """Marks the pass/fail box for the drive(s) in osTicket.""" - if not ticket_id: - raise GenericError - - # Connect to DB - connect_to_db() - if not ost_db['Cursor']: - # Skip section - return - - # Set command - sql_cmd = OST_SQL_SET_FLAG.format( - db_name=DB_NAME, - flag=OST_DRIVE_FLAG, - value=OST_DRIVE_PASSED if passed else OST_DRIVE_FAILED, - ticket_id=ticket_id) - - # Run command - try: - ost_db['Cursor'].execute(sql_cmd) - except: - ost_db['Errors'] = True - disconnect_from_db() - -def pad_with_dots(s, left_pad=True): - """Replace ' ' padding with '..' for osTicket posts.""" - s = str(s).replace(' ', '..') - if '.' in s: - if left_pad: - s = '.' + s - else: - s = s + '.' - return s - -def post_drive_results(ticket_number): - """Post drive test results to osTicket.""" - tested = False - - # Check if test(s) were run - for t in ['NVMe/SMART', 'badblocks', 'iobenchmark']: - tested |= TESTS[t]['Enabled'] - if not tested or TESTS['NVMe/SMART']['Quick']: - # No tests were run so no post necessary - return - - # Build reports for all tested devices - for name, dev in sorted(TESTS['NVMe/SMART']['Devices'].items()): - dev_failed = False - dev_passed = True - dev_unknown = False - report = [] - - # Check all test results for dev - for t in ['NVMe/SMART', 'badblocks', 'iobenchmark']: - if not TESTS[t]['Enabled']: - continue - status = TESTS[t]['Status'].get(name, 'Unknown') - dev_failed |= status == 'NS' - dev_passed &= status == 'CS' - dev_unknown |= status in ('Working', 'Unknown') - - # Start drive report - if dev_failed: - report.append('Drive hardware diagnostics tests: FAILED') - elif dev_unknown: - report.append('Drive hardware diagnostics tests: UNKNOWN') - elif dev_passed: - report.append('Drive hardware diagnostics tests: Passed') - else: - report.append('Drive hardware diagnostics tests: INCOMPLETE') - report.append('') - - # Drive description - report.append('{size} ({tran}) {model} {serial}'.format( - size=dev['lsblk'].get('size', '???b'), - tran=dev['lsblk'].get('tran', '???'), - model=dev['lsblk'].get('model', 'Unknown Model'), - serial=dev['lsblk'].get('serial', 'Unknown Serial'), - )) - report.append('') - - # Warnings (if any) - if dev.get('NVMe Disk', False): - if dev['Quick Health OK']: - report.append('WARNING: NVMe support is still experimental') - else: - report.append('ERROR: NVMe disk is reporting critical warnings') - report.append('') - elif not dev['SMART Support']: - report.append('ERROR: Unable to retrieve SMART data') - report.append('') - elif not dev['SMART Pass']: - report.append('ERROR: SMART overall-health assessment result: FAILED') - report.append('') - - # NVMe/SMART Attributes - if dev.get('NVMe Disk', False): - report.append('NVMe Attributes ({}):'.format( - TESTS['NVMe/SMART']['Status'][name])) - for attrib in sorted(ATTRIBUTES['NVMe'].keys()): - if attrib in dev['nvme-cli']: - report.append('{attrib:30}{value}'.format( - attrib=attrib, - value=dev['nvme-cli'][attrib], - )) - report[-1] = report[-1].strip().replace(' ', '.') - report[-1] = report[-1].replace('_', ' ') - elif dev['smartctl'].get('ata_smart_attributes', None): - report.append('SMART Attributes ({}):'.format( - TESTS['NVMe/SMART']['Status'][name])) - s_table = dev['smartctl'].get('ata_smart_attributes', {}).get( - 'table', {}) - s_table = {a.get('id', 'Unknown'): a for a in s_table} - for attrib in sorted(ATTRIBUTES['SMART'].keys()): - if attrib in s_table: - # Pad attributewith dots for osTicket - hex_str = str(hex(int(attrib))).upper()[2:] - hex_str = pad_with_dots('{:>2}'.format(hex_str)) - dec_str = pad_with_dots('{:>3}'.format(attrib)) - val_str = '{:<20}'.format(s_table[attrib]['raw']['string']) - val_str = pad_with_dots(val_str, left_pad=False) - report.append('{:>2}/{:>3}: {} ({})'.format( - hex_str, - dec_str, - val_str, - s_table[attrib]['name'], - )) - report[-1] = report[-1].replace('_', ' ') - report.append('') - - # SMART Short test result - if TESTS['NVMe/SMART']['Short Test'].get(name, False): - report.append('SMART short test result: {}'.format( - TESTS['NVMe/SMART']['Short Test'][name])) - report.append('') - - # badblocks - bb_status = TESTS['badblocks']['Status'].get(name, None) - if TESTS['badblocks']['Enabled'] and bb_status not in ['Denied', 'Skipped', 'Aborted']: - report.append('badblocks ({}):'.format( - TESTS['badblocks']['Status'][name])) - bb_result = TESTS['badblocks']['Results'].get( - name, - 'ERROR: Failed to read log.') - for line in bb_result.splitlines(): - line = line.strip() - if not line: - continue - if re.search('Pass completed', line, re.IGNORECASE): - line = re.sub( - r'Pass completed,?\s+', - r'', - line, - re.IGNORECASE) - report.append(line) - report.append('') - - # I/O Benchmark - io_status = TESTS['iobenchmark']['Status'].get(name, None) - if TESTS['iobenchmark']['Enabled'] and io_status not in ['Denied', 'ERROR', 'Skipped', 'Aborted']: - one_line_graph = generate_horizontal_graph( - rates=TESTS['iobenchmark']['Data'][name]['Merged Rates'], - oneline=True) - for c in COLORS.values(): - one_line_graph = one_line_graph.replace(c, '') - report.append('I/O Benchmark ({}):'.format( - TESTS['iobenchmark']['Status'][name])) - report.append(one_line_graph) - report.append('{}'.format( - TESTS['iobenchmark']['Data'][name]['Avg/Min/Max'])) - - # Export PNG - try: - png_path = export_png_graph(name, dev) - except: - png_path = None - - # imgur - try: - url = upload_to_imgur(png_path) - report.append('Imgur: {}'.format(url)) - except: - # Oh well - pass - - # Nextcloud - try: - url = upload_to_nextcloud(png_path, ticket_number, name) - report.append('Nextcloud: {}'.format(url)) - except: - # Oh well - pass - - # Used space - report.append('') - report.append('Volumes:') - core_storage = dev_passed and not dev_failed and not dev_unknown - volume_report = mount_volumes( - all_devices=False, - device_path='/dev/{}'.format(name), - core_storage=core_storage) - for vol_path, vol_data in sorted(volume_report.items()): - vol_report = [ - vol_path, - '{q}{label}{q}'.format( - label=vol_data.get('label', ''), - q='"' if vol_data.get('label', '') else ''), - '{}'.format( - vol_data.get('size', 'UNKNOWN').upper()), - '{}'.format( - vol_data.get('size_used', 'UNKNOWN').upper()), - '{}'.format( - vol_data.get('size_avail', 'UNKNOWN').upper()), - ] - if vol_report[2][-1:] != 'N': - vol_report[2] = '{} {}B'.format( - vol_report[2][:-1], - vol_report[2][-1:]) - vol_report = [v.strip().replace(' ', '_') for v in vol_report] - for i in range(5): - pad = 8 - if i < 2: - pad += 4 * (2 - i) - vol_report[i] = pad_with_dots( - left_pad=False, - s='{s:<{p}}'.format( - s=vol_report[i], - p=pad)) - vol_report[-1] = re.sub(r'\.*$', '', vol_report[-1]) - vol_report = [v.replace('_', ' ') for v in vol_report] - line = '{}..{}..Total..{}..(Used..{}..Free..{})'.format( - *vol_report) - report.append(line) - - # Post reply for drive - osticket_post_reply( - ticket_id=ticket_number, - response='\n'.join(report)) - - # Mark ticket HDD/SSD pass/fail checkbox (as needed) - if dev_failed: - osticket_set_drive_result( - ticket_id=ticket_number, - passed=False) - elif dev_unknown: - pass - elif dev_passed: - osticket_set_drive_result( - ticket_id=ticket_number, - passed=True) - - # Mark ticket as NEEDS ATTENTION - osticket_needs_attention(ticket_id=ticket_number) - -def run_badblocks(ticket_number): - """Run a read-only test for all detected disks.""" - aborted = False - clear_screen() - print_log('\nStart badblocks test(s)\n') - progress_file = '{}/badblocks_progress.out'.format(global_vars['LogDir']) - update_progress() - - # Set Window layout and start test - run_program('tmux split-window -dhl 15 watch -c -n1 -t cat {}'.format( - TESTS['Progress Out']).split()) - - # Show disk details - for name, dev in sorted(TESTS['badblocks']['Devices'].items()): - show_disk_details(dev) - print_standard(' ') - update_progress() - - # Run - print_standard('Running badblock test(s):') - for name, dev in sorted(TESTS['badblocks']['Devices'].items()): - cur_status = TESTS['badblocks']['Status'][name] - nvme_smart_status = TESTS['NVMe/SMART']['Status'].get(name, None) - if cur_status == 'Denied': - # Skip denied disks - continue - if nvme_smart_status == 'NS': - TESTS['badblocks']['Status'][name] = 'Skipped' - else: - # Not testing SMART, SMART CS, or SMART OVERRIDE - TESTS['badblocks']['Status'][name] = 'Working' - update_progress() - print_standard(' /dev/{:11} '.format(name+'...'), end='', flush=True) - run_program('tmux split-window -dl 5 {} {} {}'.format( - 'hw-diags-badblocks', - '/dev/{}'.format(name), - progress_file).split()) - wait_for_process('badblocks') - print_standard('Done', timestamp=False) - - # Check results - if os.path.exists(progress_file): - with open(progress_file, 'r') as f: - text = f.read() - TESTS['badblocks']['Results'][name] = text - r = re.search(r'Pass completed.*0/0/0 errors', text) - if r: - TESTS['badblocks']['Status'][name] = 'CS' - else: - TESTS['badblocks']['Status'][name] = 'NS' - - # Move temp file - shutil.move(progress_file, '{}/badblocks-{}.log'.format( - global_vars['LogDir'], name)) - else: - TESTS['badblocks']['Status'][name] = 'NS' - update_progress() - - # Done - run_program('tmux kill-pane -a'.split(), check=False) - pass - -def run_iobenchmark(ticket_number): - """Run a read-only test for all detected disks.""" - aborted = False - clear_screen() - print_log('\nStart I/O Benchmark test(s)\n') - progress_file = '{}/iobenchmark_progress.out'.format(global_vars['LogDir']) - update_progress() - - # Set Window layout and start test - run_program('tmux split-window -dhl 15 watch -c -n1 -t cat {}'.format( - TESTS['Progress Out']).split()) - - # Show disk details - for name, dev in sorted(TESTS['iobenchmark']['Devices'].items()): - show_disk_details(dev) - print_standard(' ') - update_progress() - - # Run - print_standard('Running benchmark test(s):') - for name, dev in sorted(TESTS['iobenchmark']['Devices'].items()): - cur_status = TESTS['iobenchmark']['Status'][name] - nvme_smart_status = TESTS['NVMe/SMART']['Status'].get(name, None) - bb_status = TESTS['badblocks']['Status'].get(name, None) - if cur_status == 'Denied': - # Skip denied disks - continue - if nvme_smart_status == 'NS': - TESTS['iobenchmark']['Status'][name] = 'Skipped' - elif bb_status in ['NS', 'Skipped']: - TESTS['iobenchmark']['Status'][name] = 'Skipped' - else: - # (SMART tests not run or CS/OVERRIDE) - # AND (BADBLOCKS tests not run or CS) - TESTS['iobenchmark']['Status'][name] = 'Working' - update_progress() - print_standard(' /dev/{:11} '.format(name+'...'), end='', flush=True) - - # Get dev size - cmd = 'sudo lsblk -bdno size /dev/{}'.format(name) - try: - result = run_program(cmd.split()) - dev_size = result.stdout.decode().strip() - dev_size = int(dev_size) - except: - # Failed to get dev size, requires manual testing instead - TESTS['iobenchmark']['Status'][name] = 'ERROR' - continue - if dev_size < IO_VARS['Minimum Dev Size']: - TESTS['iobenchmark']['Status'][name] = 'ERROR' - continue - - # Calculate dd values - ## test_size is the area to be read in bytes - ## If the dev is < 10Gb then it's the whole dev - ## Otherwise it's the larger of 10Gb or 1% of the dev - ## - ## test_chunks is the number of groups of "Chunk Size" in test_size - ## This number is reduced to a multiple of the graph width in - ## order to allow for the data to be condensed cleanly - ## - ## skip_blocks is the number of "Block Size" groups not tested - ## skip_count is the number of blocks to skip per test_chunk - ## skip_extra is how often to add an additional skip block - ## This is needed to ensure an even testing across the dev - ## This is calculated by using the fractional amount left off - ## of the skip_count variable - test_size = min(IO_VARS['Minimum Test Size'], dev_size) - test_size = max( - test_size, dev_size*IO_VARS['Alt Test Size Factor']) - test_chunks = int(test_size // IO_VARS['Chunk Size']) - test_chunks -= test_chunks % IO_VARS['Graph Horizontal Width'] - test_size = test_chunks * IO_VARS['Chunk Size'] - skip_blocks = int((dev_size - test_size) // IO_VARS['Block Size']) - skip_count = int((skip_blocks / test_chunks) // 1) - skip_extra = 0 - try: - skip_extra = 1 + int(1 / ((skip_blocks / test_chunks) % 1)) - except ZeroDivisionError: - # skip_extra == 0 is fine - pass - - # Open dd progress pane after initializing file - with open(progress_file, 'w') as f: - f.write('') - sleep(1) - cmd = 'tmux split-window -dp 75 -PF #D tail -f {}'.format( - progress_file) - result = run_program(cmd.split()) - bottom_pane = result.stdout.decode().strip() - - # Run dd read tests - offset = 0 - TESTS['iobenchmark']['Data'][name] = { - 'Graph': [], - 'Read Rates': []} - for i in range(test_chunks): - i += 1 - s = skip_count - c = int(IO_VARS['Chunk Size'] / IO_VARS['Block Size']) - if skip_extra and i % skip_extra == 0: - s += 1 - cmd = 'sudo dd bs={b} skip={s} count={c} if=/dev/{n} of={o} iflag=direct'.format( - b=IO_VARS['Block Size'], - s=offset+s, - c=c, - n=name, - o='/dev/null') - result = run_program(cmd.split()) - result_str = result.stderr.decode().replace('\n', '') - cur_rate = get_read_rate(result_str) - TESTS['iobenchmark']['Data'][name]['Read Rates'].append( - cur_rate) - TESTS['iobenchmark']['Data'][name]['Graph'].append( - '{percent:0.1f} {rate}'.format( - percent=i/test_chunks*100, - rate=int(cur_rate/(1024**2)))) - if i % IO_VARS['Progress Refresh Rate'] == 0: - # Update vertical graph - update_io_progress( - percent=i/test_chunks*100, - rate=cur_rate, - progress_file=progress_file) - # Update offset - offset += s + c - print_standard('Done', timestamp=False) - - # Close bottom pane - run_program(['tmux', 'kill-pane', '-t', bottom_pane]) - - # Build report - avg_min_max = 'Average read speed: {:3.1f} MB/s (Min: {:3.1f}, Max: {:3.1f})'.format( - sum(TESTS['iobenchmark']['Data'][name]['Read Rates'])/len( - TESTS['iobenchmark']['Data'][name]['Read Rates'])/(1024**2), - min(TESTS['iobenchmark']['Data'][name]['Read Rates'])/(1024**2), - max(TESTS['iobenchmark']['Data'][name]['Read Rates'])/(1024**2)) - TESTS['iobenchmark']['Data'][name]['Avg/Min/Max'] = avg_min_max - TESTS['iobenchmark']['Data'][name]['Merged Rates'] = [] - pos = 0 - width = int(test_chunks / IO_VARS['Graph Horizontal Width']) - for i in range(IO_VARS['Graph Horizontal Width']): - # Append average rate for WIDTH number of rates to new array - TESTS['iobenchmark']['Data'][name]['Merged Rates'].append(sum( - TESTS['iobenchmark']['Data'][name]['Read Rates'][pos:pos+width])/width) - pos += width - report = generate_horizontal_graph( - TESTS['iobenchmark']['Data'][name]['Merged Rates']) - report += '\n{}'.format(avg_min_max) - TESTS['iobenchmark']['Results'][name] = report - - # Set CS/NS - min_read = min(TESTS['iobenchmark']['Data'][name]['Read Rates']) - avg_read = sum( - TESTS['iobenchmark']['Data'][name]['Read Rates'])/len( - TESTS['iobenchmark']['Data'][name]['Read Rates']) - dev_rotational = dev['lsblk'].get('rota', None) - if dev_rotational == "0": - # Use SSD scale - thresh_min = IO_VARS['Threshold SSD Min'] - thresh_high_avg = IO_VARS['Threshold SSD High Avg'] - thresh_low_avg = IO_VARS['Threshold SSD Low Avg'] - else: - # Use HDD scale - thresh_min = IO_VARS['Threshold HDD Min'] - thresh_high_avg = IO_VARS['Threshold HDD High Avg'] - thresh_low_avg = IO_VARS['Threshold HDD Low Avg'] - if min_read <= thresh_min and avg_read <= thresh_high_avg: - TESTS['iobenchmark']['Status'][name] = 'NS' - elif avg_read <= thresh_low_avg: - TESTS['iobenchmark']['Status'][name] = 'NS' - else: - TESTS['iobenchmark']['Status'][name] = 'CS' - - # Save logs - dest_filename = '{}/iobenchmark-{}.log'.format(global_vars['LogDir'], name) - shutil.move(progress_file, dest_filename) - with open(dest_filename.replace('.', '-raw.'), 'a') as f: - f.write('\n'.join(TESTS['iobenchmark']['Data'][name]['Graph'])) - update_progress() - - # Done - run_program('tmux kill-pane -a'.split(), check=False) - pass - -def run_mprime(ticket_number): - """Run Prime95 for MPRIME_LIMIT minutes while showing the temps.""" - aborted = False - print_log('\nStart Prime95 test') - TESTS['Prime95']['Status'] = 'Working' - update_progress() - - # Set Window layout and start test - run_program('tmux split-window -dl 10 -c {wd} {cmd} {wd}'.format( - wd=global_vars['TmpDir'], cmd='hw-diags-prime95').split()) - run_program('tmux split-window -dhl 15 watch -c -n1 -t cat {}'.format( - TESTS['Progress Out']).split()) - run_program('tmux split-window -bd watch -c -n1 -t hw-sensors'.split()) - run_program('tmux resize-pane -y 3'.split()) - - # Start test - run_program(['apple-fans', 'max']) - try: - for i in range(int(MPRIME_LIMIT)): - clear_screen() - min_left = int(MPRIME_LIMIT) - i - print_standard('Running Prime95 ({} minute{} left)'.format( - min_left, - 's' if min_left != 1 else '')) - print_warning('If running too hot, press CTRL+c to abort the test') - sleep(60) - except KeyboardInterrupt: - # Catch CTRL+C - aborted = True - TESTS['Prime95']['Status'] = 'Aborted' - print_warning('\nAborted.') - update_progress() - - # Save "final" temps - run_program( - cmd = 'hw-sensors >> "{}/Final Temps.out"'.format( - global_vars['LogDir']).split(), - check = False, - pipe = False, - shell = True) - run_program( - cmd = 'hw-sensors --nocolor >> "{}/Final Temps.log"'.format( - global_vars['LogDir']).split(), - check = False, - pipe = False, - shell = True) - - # Stop test - run_program('killall -s INT mprime'.split(), check=False) - run_program(['apple-fans', 'auto']) - - # Move logs to Ticket folder - for item in os.scandir(global_vars['TmpDir']): - try: - shutil.move(item.path, global_vars['LogDir']) - except Exception: - print_error('ERROR: Failed to move "{}" to "{}"'.format( - item.path, - global_vars['LogDir'])) - - # Check logs - TESTS['Prime95']['NS'] = False - TESTS['Prime95']['CS'] = False - log = '{}/results.txt'.format(global_vars['LogDir']) - if os.path.exists(log): - with open(log, 'r') as f: - text = f.read() - TESTS['Prime95']['results.txt'] = text - r = re.search(r'(error|fail)', text) - TESTS['Prime95']['NS'] = bool(r) - log = '{}/prime.log'.format(global_vars['LogDir']) - if os.path.exists(log): - with open(log, 'r') as f: - text = f.read() - TESTS['Prime95']['prime.log'] = text - r = re.search(r'completed.*0 errors, 0 warnings', text) - TESTS['Prime95']['CS'] = bool(r) - - # Update status - if not aborted: - if TESTS['Prime95']['NS']: - TESTS['Prime95']['Status'] = 'NS' - elif TESTS['Prime95']['CS']: - TESTS['Prime95']['Status'] = 'CS' - else: - TESTS['Prime95']['Status'] = 'Unknown' - update_progress() - - # Build osTicket report - if ticket_number: - report = ['Prime95 ({}):'.format(TESTS['Prime95']['Status'])] - log_path = '{}/prime.log'.format(global_vars['LogDir']) - try: - with open(log_path, 'r') as f: - for line in f.readlines(): - line = line.strip() - r = re.search('(completed \d+ tests.*)', line, re.IGNORECASE) - if r: - report.append(r.group(1)) - except: - report.append('ERROR: Failed to read log.') - report.append('') - report.append('Final temps:') - log_path = '{}/Final Temps.log'.format(global_vars['LogDir']) - try: - with open(log_path, 'r') as f: - for line in f.readlines(): - line = line.strip() - if not line: - # Stop after CPU temp(s) - break - report.append(line) - except: - report.append('ERROR: Failed to read log.') - - # Upload osTicket report - osticket_post_reply( - ticket_id=ticket_number, - response='\n'.join(report)) - - if aborted: - if TESTS['NVMe/SMART']['Enabled'] or TESTS['badblocks']['Enabled']: - if not ask('Proceed to next test?'): - for name in TESTS['NVMe/SMART']['Devices'].keys(): - for t in ['NVMe/SMART', 'badblocks', 'iobenchmark']: - cur_status = TESTS[t]['Status'][name] - if cur_status not in ['CS', 'Denied', 'NS']: - TESTS[t]['Status'][name] = 'Aborted' - run_program('tmux kill-pane -a'.split()) - raise GenericError - - # Done - run_program('tmux kill-pane -a'.split()) - -def run_nvme_smart(ticket_number): - """Run the built-in NVMe or SMART test for all detected disks.""" - aborted = False - clear_screen() - print_log('\nStart NVMe/SMART test(s)\n') - progress_file = '{}/selftest_progress.out'.format(global_vars['LogDir']) - update_progress() - - # Set Window layout and start test - run_program('tmux split-window -dl 3 watch -c -n1 -t cat {}'.format( - progress_file).split()) - run_program('tmux split-window -dhl 15 watch -c -n1 -t cat {}'.format( - TESTS['Progress Out']).split()) - - # Show disk details - for name, dev in sorted(TESTS['NVMe/SMART']['Devices'].items()): - show_disk_details(dev) - print_standard(' ') - update_progress() - - # Run - for name, dev in sorted(TESTS['NVMe/SMART']['Devices'].items()): - TESTS['NVMe/SMART']['Short Test'][name] = None - cur_status = TESTS['NVMe/SMART']['Status'][name] - if cur_status == 'OVERRIDE': - # Skipping test per user request - continue - if TESTS['NVMe/SMART']['Quick'] or dev.get('NVMe Disk', False): - # Skip SMART self-tests for quick checks and NVMe disks - if dev['Quick Health OK']: - TESTS['NVMe/SMART']['Status'][name] = 'CS' - else: - TESTS['NVMe/SMART']['Status'][name] = 'NS' - elif not dev['Quick Health OK']: - # SMART overall == Failed or attributes bad, avoid self-test - TESTS['NVMe/SMART']['Status'][name] = 'NS' - else: - # Start SMART short self-test - test_length = dev['smartctl'].get( - 'ata_smart_data', {}).get( - 'self_test', {}).get( - 'polling_minutes', {}).get( - 'short', 5) - test_length = int(test_length) + 5 - TESTS['NVMe/SMART']['Status'][name] = 'Working' - update_progress() - print_standard('Running SMART short self-test(s):') - print_standard( - ' /dev/{:8}({} minutes)... '.format(name, test_length), - end='', flush=True) - run_program( - 'sudo smartctl -t short /dev/{}'.format(name).split(), - check=False) - - # Wait and show progress (in 10 second increments) - for iteration in range(int(test_length*60/10)): - # Update SMART data - dev['smartctl'] = get_smart_details(name) - - # Check if test is complete - if iteration >= 6: - done = dev['smartctl'].get( - 'ata_smart_data', {}).get( - 'self_test', {}).get( - 'status', {}).get( - 'passed', False) - if done: - break - - # Update progress_file - with open(progress_file, 'w') as f: - f.write('SMART self-test status:\n {}'.format( - dev['smartctl'].get( - 'ata_smart_data', {}).get( - 'self_test', {}).get( - 'status', {}).get( - 'string', 'unknown'))) - sleep(10) - os.remove(progress_file) - - # Check result - test_passed = dev['smartctl'].get( - 'ata_smart_data', {}).get( - 'self_test', {}).get( - 'status', {}).get( - 'passed', False) - if test_passed: - TESTS['NVMe/SMART']['Status'][name] = 'CS' - TESTS['NVMe/SMART']['Short Test'][name] = 'CS' - else: - TESTS['NVMe/SMART']['Status'][name] = 'NS' - TESTS['NVMe/SMART']['Short Test'][name] = 'NS' - update_progress() - print_standard('Done', timestamp=False) - - # Done - run_program('tmux kill-pane -a'.split(), check=False) - -def run_tests(tests, ticket_number=None): - """Run selected hardware test(s).""" - clear_screen() - print_standard('Starting Hardware Diagnostics') - if ticket_number: - print_standard(' For osTicket #{}'.format(ticket_number)) - print_standard(' ') - print_standard('Running tests: {}'.format(', '.join(tests))) - # Enable selected tests - for t in ['Prime95', 'NVMe/SMART', 'badblocks', 'iobenchmark']: - TESTS[t]['Enabled'] = t in tests - TESTS['NVMe/SMART']['Quick'] = 'Quick' in tests - - # Initialize - if TESTS['NVMe/SMART']['Enabled'] or TESTS['badblocks']['Enabled'] or TESTS['iobenchmark']['Enabled']: - print_standard(' ') - scan_disks() - update_progress() - - # Run - mprime_aborted = False - if TESTS['Prime95']['Enabled']: - try: - run_mprime(ticket_number) - except GenericError: - mprime_aborted = True - if not mprime_aborted: - if TESTS['NVMe/SMART']['Enabled']: - run_nvme_smart(ticket_number) - if TESTS['badblocks']['Enabled']: - run_badblocks(ticket_number) - if TESTS['iobenchmark']['Enabled']: - run_iobenchmark(ticket_number) - - # Show results - if ticket_number: - post_drive_results(ticket_number) - show_results() - - # Open log - if not TESTS['NVMe/SMART']['Quick'] and ENABLED_OPEN_LOGS: - try: - popen_program(['nohup', 'leafpad', global_vars['LogFile']], pipe=True) - except Exception: - print_error('ERROR: Failed to open log: {}'.format( - global_vars['LogFile'])) - pause('Press Enter to exit...') - -def scan_disks(full_paths=False, only_path=None): - """Scan for disks eligible for hardware testing.""" - - # Get eligible disk list - cmd = ['lsblk', '-J', '-O'] - if full_paths: - cmd.append('-p') - if only_path: - cmd.append(only_path) + # Ignore and leave self.lscpu empty + return + for line in json_data.get('lscpu', []): + _field = line.get('field', None).replace(':', '') + _data = line.get('data', None) + if not _field and not _data: + # Skip + print_warning(_field, _data) + pause() + continue + self.lscpu[_field] = _data + + def generate_cpu_report(self): + """Generate CPU report with data from all tests.""" + report = [] + report.append('{BLUE}Device{CLEAR}'.format(**COLORS)) + report.append(' {}'.format(self.name)) + + # Tests + for test in self.tests.values(): + report.extend(test.report) + + return report + +class DiskObj(): + """Object for tracking disk specific data.""" + def __init__(self, disk_path): + self.disk_ok = True + self.labels = [] + self.lsblk = {} + self.name = re.sub(r'^.*/(.*)', r'\1', disk_path) + self.nvme_attributes = {} + self.path = disk_path + self.smart_attributes = {} + self.smart_timeout = False + self.smart_self_test = {} + self.smartctl = {} + self.tests = OrderedDict() + self.get_details() + self.get_smart_details() + self.description = '{size} ({tran}) {model} {serial}'.format( + **self.lsblk) + + def calc_io_dd_values(self): + """Calcualte I/O benchmark dd values.""" + # Get real disk size + cmd = ['lsblk', + '--bytes', '--nodeps', '--noheadings', + '--output', 'size', self.path] result = run_program(cmd) - json_data = json.loads(result.stdout.decode()) - devs = {} - for d in json_data.get('blockdevices', []): - if d['type'] == 'disk': - if d['hotplug'] == '0': - devs[d['name']] = {'lsblk': d} - TESTS['NVMe/SMART']['Status'][d['name']] = 'Pending' - TESTS['badblocks']['Status'][d['name']] = 'Pending' - TESTS['iobenchmark']['Status'][d['name']] = 'Pending' - else: - # Skip WizardKit devices - skip_dev=False - wk_label_regex = r'{}_(LINUX|UFD)'.format(KIT_NAME_SHORT) - for c in d.get('children', []): - r = re.search( - wk_label_regex, c.get('label', ''), re.IGNORECASE) - skip_dev = bool(r) - if not skip_dev: - devs[d['name']] = {'lsblk': d} - TESTS['NVMe/SMART']['Status'][d['name']] = 'Pending' - TESTS['badblocks']['Status'][d['name']] = 'Pending' - TESTS['iobenchmark']['Status'][d['name']] = 'Pending' + self.size_bytes = int(result.stdout.decode().strip()) - for dev, data in devs.items(): - # Get SMART attributes - run_program( - cmd = 'sudo smartctl -s on {}{}'.format( - '' if full_paths else '/dev/', - dev).split(), - check = False) - data['smartctl'] = get_smart_details(dev) + # dd calculations + ## The minimum dev size is 'Graph Horizontal Width' * 'Chunk Size' + ## (e.g. 1.25 GB for a width of 40 and a chunk size of 32MB) + ## If the device is smaller than the minimum dd_chunks would be set + ## to zero which would cause a divide by zero error. + ## If the device is below the minimum size an Exception will be raised + ## + ## dd_size is the area to be read in bytes + ## If the dev is < 10Gb then it's the whole dev + ## Otherwise it's the larger of 10Gb or 1% of the dev + ## + ## dd_chunks is the number of groups of "Chunk Size" in self.dd_size + ## This number is reduced to a multiple of the graph width in + ## order to allow for the data to be condensed cleanly + ## + ## dd_chunk_blocks is the chunk size in number of blocks + ## (e.g. 64 if block size is 512KB and chunk size is 32MB + ## + ## dd_skip_blocks is the number of "Block Size" groups not tested + ## dd_skip_count is the number of blocks to skip per self.dd_chunk + ## dd_skip_extra is how often to add an additional skip block + ## This is needed to ensure an even testing across the dev + ## This is calculated by using the fractional amount left off + ## of the dd_skip_count variable + self.dd_size = min(IO_VARS['Minimum Test Size'], self.size_bytes) + self.dd_size = max( + self.dd_size, + self.size_bytes * IO_VARS['Alt Test Size Factor']) + self.dd_chunks = int(self.dd_size // IO_VARS['Chunk Size']) + self.dd_chunks -= self.dd_chunks % IO_VARS['Graph Horizontal Width'] + if self.dd_chunks < IO_VARS['Graph Horizontal Width']: + raise DeviceTooSmallError + self.dd_chunk_blocks = int(IO_VARS['Chunk Size'] / IO_VARS['Block Size']) + self.dd_size = self.dd_chunks * IO_VARS['Chunk Size'] + self.dd_skip_blocks = int( + (self.size_bytes - self.dd_size) // IO_VARS['Block Size']) + self.dd_skip_count = int((self.dd_skip_blocks / self.dd_chunks) // 1) + self.dd_skip_extra = 0 + try: + self.dd_skip_extra = 1 + int( + 1 / ((self.dd_skip_blocks / self.dd_chunks) % 1)) + except ZeroDivisionError: + # self.dd_skip_extra == 0 is fine + pass - # Get NVMe attributes - if data['lsblk']['tran'] == 'nvme': - cmd = 'sudo nvme smart-log /dev/{} -o json'.format(dev).split() - cmd = 'sudo nvme smart-log {}{} -o json'.format( - '' if full_paths else '/dev/', - dev).split() - result = run_program(cmd, check=False) - try: - data['nvme-cli'] = json.loads(result.stdout.decode()) - except Exception: - # Let other sections deal with the missing data - data['nvme-cli'] = {} - data['NVMe Disk'] = True + def check_attributes(self, silent=False): + """Check NVMe / SMART attributes for errors.""" + override_disabled = False + if self.nvme_attributes: + attr_type = 'NVMe' + items = self.nvme_attributes.items() + elif self.smart_attributes: + attr_type = 'SMART' + items = self.smart_attributes.items() + for k, v in items: + if k in ATTRIBUTES[attr_type]: + if 'Error' not in ATTRIBUTES[attr_type][k]: + # Only worried about error thresholds + continue + if ATTRIBUTES[attr_type][k].get('Ignore', False): + # Attribute is non-failing, skip + continue + if v['raw'] >= ATTRIBUTES[attr_type][k]['Error']: + self.disk_ok = False - # Set "Quick Health OK" value - ## NOTE: If False then require override for badblocks test - wanted_smart_list = [ - 'ata_smart_attributes', - 'ata_smart_data', - 'smart_status', - ] - if data.get('NVMe Disk', False): - crit_warn = data['nvme-cli'].get('critical_warning', 1) - if crit_warn == 0: - dev_name = data['lsblk']['name'] - data['Quick Health OK'] = True - TESTS['NVMe/SMART']['Status'][dev_name] = 'CS' - else: - data['Quick Health OK'] = False - data['Quick Health OK'] = True if crit_warn == 0 else False - elif set(wanted_smart_list).issubset(data['smartctl'].keys()): - data['SMART Pass'] = data['smartctl'].get('smart_status', {}).get( - 'passed', False) - data['Quick Health OK'] = data['SMART Pass'] - data['SMART Support'] = True - else: - data['Quick Health OK'] = False - data['SMART Support'] = False + # Disable override if necessary + override_disabled |= ATTRIBUTES[attr_type][k].get( + 'Critical', False) - # Ask for manual overrides if necessary - if TESTS['badblocks']['Enabled'] or TESTS['iobenchmark']['Enabled']: - show_disk_details(data) - needs_override = False - if not data['Quick Health OK']: - needs_override = True - print_warning( - "WARNING: Health can't be confirmed for: /dev/{}".format(dev)) - if get_smart_value(data['smartctl'], '199'): - # SMART attribute present and it's value is non-zero - needs_override = True - print_warning( - 'WARNING: SMART 199/C7 error detected on /dev/{}'.format(dev)) - print_standard(' (Have you tried swapping the drive cable?)') - if needs_override: - dev_name = data['lsblk']['name'] - print_standard(' ') - if ask('Run tests on this device anyway?'): - TESTS['NVMe/SMART']['Status'][dev_name] = 'OVERRIDE' - else: - TESTS['NVMe/SMART']['Status'][dev_name] = 'Skipped' - TESTS['badblocks']['Status'][dev_name] = 'Denied' - TESTS['iobenchmark']['Status'][dev_name] = 'Denied' - print_standard(' ') # In case there's more than one "OVERRIDE" disk + # SMART overall assessment + ## NOTE: Only fail drives if the overall value exists and reports failed + if not self.smartctl.get('smart_status', {}).get('passed', True): + self.disk_ok = False + override_disabled = True - TESTS['NVMe/SMART']['Devices'] = devs - TESTS['badblocks']['Devices'] = devs - TESTS['iobenchmark']['Devices'] = devs - return devs - -def show_disk_details(dev, only_attributes=False): - """Display disk details.""" - dev_name = dev['lsblk']['name'] - if not only_attributes: - # Device description - print_info('Device: {}{}'.format( - '' if '/dev/' in dev['lsblk']['name'] else '/dev/', - dev['lsblk']['name'])) - print_standard(' {:>4} ({}) {} {}'.format( - str(dev['lsblk'].get('size', '???b')).strip(), - str(dev['lsblk'].get('tran', '???')).strip().upper().replace( - 'NVME', 'NVMe'), - str(dev['lsblk'].get('model', 'Unknown Model')).strip(), - str(dev['lsblk'].get('serial', 'Unknown Serial')).strip(), - )) - - # Warnings - if dev.get('NVMe Disk', False): - if dev['Quick Health OK']: - print_warning('WARNING: NVMe support is still experimental') - else: - print_error('ERROR: NVMe disk is reporting critical warnings') - elif not dev['SMART Support']: - print_error('ERROR: Unable to retrieve SMART data') - elif not dev['SMART Pass']: - print_error('ERROR: SMART overall-health assessment result: FAILED') - - # Attributes - if dev.get('NVMe Disk', False): - if only_attributes: - print_info('SMART Attributes:', end='') - print_warning(' Updated: {}'.format( - time.strftime('%Y-%m-%d %H:%M %Z'))) - else: - print_info('Attributes:') - for attrib, threshold in sorted(ATTRIBUTES['NVMe'].items()): - if attrib in dev['nvme-cli']: - print_standard( - ' {:37}'.format(attrib.replace('_', ' ').title()), - end='', flush=True) - raw_num = dev['nvme-cli'][attrib] - raw_str = str(raw_num) - if (threshold.get('Error', False) and - raw_num >= threshold.get('Error', -1)): - print_error(raw_str, timestamp=False) - if not threshold.get('Ignore', False): - dev['Quick Health OK'] = False - TESTS['NVMe/SMART']['Status'][dev_name] = 'NS' - elif (threshold.get('Warning', False) and - raw_num >= threshold.get('Warning', -1)): - print_warning(raw_str, timestamp=False) - else: - print_success(raw_str, timestamp=False) - elif dev['smartctl'].get('ata_smart_attributes', None): - # SMART attributes - if only_attributes: - print_info('SMART Attributes:', end='') - print_warning(' Updated: {}'.format( - time.strftime('%Y-%m-%d %H:%M %Z'))) - else: - print_info('Attributes:') - s_table = dev['smartctl'].get('ata_smart_attributes', {}).get( - 'table', {}) - s_table = {a.get('id', 'Unknown'): a for a in s_table} - for attrib, threshold in sorted(ATTRIBUTES['SMART'].items()): - if attrib in s_table: - print_standard( - ' {:>3} {:32}'.format( - attrib, - s_table[attrib]['name']).replace('_', ' ').title(), - end='', flush=True) - raw_str = s_table[attrib]['raw']['string'] - raw_num = re.sub(r'^(\d+).*$', r'\1', raw_str) - try: - raw_num = float(raw_num) - except ValueError: - # Not sure about this one, print raw_str without color? - print_standard(raw_str, timestamp=False) - continue - if (threshold.get('Error', False) and - raw_num >= threshold.get('Error', -1)): - print_error(raw_str, timestamp=False) - if not threshold.get('Ignore', False): - dev['Quick Health OK'] = False - TESTS['NVMe/SMART']['Status'][dev_name] = 'NS' - elif (threshold.get('Warning', False) and - raw_num >= threshold.get('Warning', -1)): - print_warning(raw_str, timestamp=False) - else: - print_success(raw_str, timestamp=False) - -def show_results(): - """Show results for selected test(s).""" - clear_screen() - print_log('\n───────────────────────────') - print_standard('Hardware Diagnostic Results') - update_progress() - - # Set Window layout and show progress - run_program('tmux split-window -dhl 15 watch -c -n1 -t cat {}'.format( - TESTS['Progress Out']).split()) - - # Prime95 - if TESTS['Prime95']['Enabled']: - print_success('\nPrime95:') - for log, regex in [ - ['results.txt', r'(error|fail)'], - ['prime.log', r'completed.*0 errors, 0 warnings']]: - if log in TESTS['Prime95']: - print_info('Log: {}'.format(log)) - lines = [line.strip() for line - in TESTS['Prime95'][log].splitlines() - if re.search(regex, line, re.IGNORECASE)] - for line in lines[-4:]: - line = re.sub(r'^.*Worker #\d.*Torture Test (.*)', r'\1', - line, re.IGNORECASE) - if TESTS['Prime95'].get('NS', False): - print_error(' {}'.format(line)) - else: - print_standard(' {}'.format(line)) - print_info('Final temps') - print_log(' See Final Temps.log') - with open('{}/Final Temps.out'.format(global_vars['LogDir']), 'r') as f: - for line in f.readlines(): - if re.search(r'^\s*$', line.strip()): - # Stop after coretemps (which should be first) - break - print(' {}'.format(line.strip())) - print_standard(' ') - - # NVMe/SMART / badblocks / iobenchmark - if TESTS['NVMe/SMART']['Enabled'] or TESTS['badblocks']['Enabled'] or TESTS['iobenchmark']['Enabled']: - print_success('Disks:') - for name, dev in sorted(TESTS['NVMe/SMART']['Devices'].items()): - show_disk_details(dev) - bb_status = TESTS['badblocks']['Status'].get(name, None) - if (TESTS['badblocks']['Enabled'] - and bb_status not in ['Denied', 'OVERRIDE', 'Skipped']): - print_info('badblocks:') - result = TESTS['badblocks']['Results'].get(name, '') - for line in result.splitlines(): - if re.search(r'Pass completed', line, re.IGNORECASE): - line = re.sub( - r'Pass completed,?\s+', r'', - line.strip(), re.IGNORECASE) - if TESTS['badblocks']['Status'][name] == 'CS': - print_standard(' {}'.format(line)) - else: - print_error(' {}'.format(line)) - io_status = TESTS['iobenchmark']['Status'].get(name, None) - if (TESTS['iobenchmark']['Enabled'] - and io_status not in ['Denied', 'OVERRIDE', 'Skipped']): - print_info('Benchmark:') - result = TESTS['iobenchmark']['Results'].get(name, '') - for line in result.split('\n'): - print_standard(' {}'.format(line)) + # Print errors + if not silent: + if self.disk_ok: + # 199/C7 warning + if self.smart_attributes.get(199, {}).get('raw', 0) > 0: + print_warning('199/C7 error detected') + print_standard(' (Have you tried swapping the disk cable?)') + else: + # Override? + show_report(self.generate_attribute_report(description=True)) + print_warning(' {} error(s) detected.'.format(attr_type)) + if override_disabled: + print_standard('Tests disabled for this device') + pause() + elif not (len(self.tests) == 3 and HW_OVERRIDES_LIMITED): + if HW_OVERRIDES_FORCED or ask('Run tests on this device anyway?'): + self.disk_ok = True + if 'NVMe / SMART' in self.tests: + self.disable_test('NVMe / SMART', 'OVERRIDE') + if not self.nvme_attributes and self.smart_attributes: + # Re-enable for SMART short-tests + self.tests['NVMe / SMART'].disabled = False print_standard(' ') - # osTicket - if ost_db['Errors']: - print_warning('WARNING: Failed to post result(s) to osTicket') - print_standard(' ') + def disable_test(self, name, status): + """Disable test by name and update status.""" + if name in self.tests: + self.tests[name].update_status(status) + self.tests[name].disabled = True + + def generate_attribute_report( + self, description=False, short_test=False, timestamp=False): + """Generate NVMe / SMART report, returns list.""" + report = [] + if description: + report.append('{BLUE}Device ({name}){CLEAR}'.format( + name=self.name, **COLORS)) + report.append(' {}'.format(self.description)) + + # Warnings + if self.nvme_attributes: + attr_type = 'NVMe' + report.append( + ' {YELLOW}NVMe disk support is still experimental{CLEAR}'.format( + **COLORS)) + elif self.smart_attributes: + attr_type = 'SMART' + else: + # No attribute data available, return short report + report.append( + ' {YELLOW}No NVMe or SMART data available{CLEAR}'.format( + **COLORS)) + return report + if not self.smartctl.get('smart_status', {}).get('passed', True): + report.append( + ' {RED}SMART overall self-assessment: Failed{CLEAR}'.format( + **COLORS)) + + # Attributes + report.append('{BLUE}{a} Attributes{YELLOW}{u:>23} {t}{CLEAR}'.format( + a=attr_type, + u='Updated:' if timestamp else '', + t=time.strftime('%Y-%m-%d %H:%M %Z') if timestamp else '', + **COLORS)) + if self.nvme_attributes: + attr_type = 'NVMe' + items = self.nvme_attributes.items() + elif self.smart_attributes: + attr_type = 'SMART' + items = self.smart_attributes.items() + for k, v in items: + if k in ATTRIBUTES[attr_type]: + _note = '' + _color = COLORS['GREEN'] + + # Attribute ID & Name + if attr_type == 'NVMe': + _line = ' {:38}'.format(k.replace('_', ' ').title()) + else: + _line = ' {i:>3} / {h}: {n:28}'.format( + i=k, + h=ATTRIBUTES[attr_type][k]['Hex'], + n=v['name'][:28]) + + # Set color + for _t, _c in [['Warning', 'YELLOW'], ['Error', 'RED']]: + if _t in ATTRIBUTES[attr_type][k]: + if v['raw'] >= ATTRIBUTES[attr_type][k][_t]: + _color = COLORS[_c] + + # 199/C7 warning + if str(k) == '199' and v['raw'] > 0: + _note = '(bad cable?)' + + # Attribute value + _line += '{c}{v} {YELLOW}{n}{CLEAR}'.format( + c=_color, + v=v['raw_str'], + n=_note, + **COLORS) + + # Add line to report + report.append(_line) + + # SMART short-test + if short_test: + report.append('{BLUE}SMART Short self-test{CLEAR}'.format(**COLORS)) + report.append(' {}'.format( + self.smart_self_test['status'].get( + 'string', 'UNKNOWN').capitalize())) + if self.smart_timeout: + report.append(' {YELLOW}Timed out{CLEAR}'.format(**COLORS)) # Done + return report + + def generate_disk_report(self): + """Generate disk report with data from all tests.""" + report = [] + report.append('{BLUE}Device ({name}){CLEAR}'.format( + name=self.name, **COLORS)) + report.append(' {}'.format(self.description)) + + # Attributes + if 'NVMe / SMART' not in self.tests: + report.extend(self.generate_attribute_report()) + + # Tests + for test in self.tests.values(): + report.extend(test.report) + + return report + + def get_details(self): + """Get data from lsblk.""" + cmd = ['lsblk', '--json', '--output-all', '--paths', self.path] + try: + result = run_program(cmd, check=False) + json_data = json.loads(result.stdout.decode()) + self.lsblk = json_data['blockdevices'][0] + except Exception: + # Leave self.lsblk empty + pass + + # Set necessary details + self.lsblk['model'] = self.lsblk.get('model', 'Unknown Model') + self.lsblk['name'] = self.lsblk.get('name', self.path) + self.lsblk['rota'] = self.lsblk.get('rota', True) + self.lsblk['serial'] = self.lsblk.get('serial', 'Unknown Serial') + self.lsblk['size'] = self.lsblk.get('size', '???b') + self.lsblk['tran'] = self.lsblk.get('tran', '???') + + # Ensure certain attributes are strings + for attr in ['model', 'name', 'rota', 'serial', 'size', 'tran']: + if not isinstance(self.lsblk[attr], str): + self.lsblk[attr] = str(self.lsblk[attr]) + self.lsblk['tran'] = self.lsblk['tran'].upper().replace('NVME', 'NVMe') + + # Build list of labels + for disk in [self.lsblk, *self.lsblk.get('children', [])]: + self.labels.append(disk.get('label', '')) + self.labels.append(disk.get('partlabel', '')) + self.labels = [str(label) for label in self.labels if label] + + def get_smart_details(self): + """Get data from smartctl.""" + cmd = ['sudo', 'smartctl', '--all', '--json', self.path] + try: + result = run_program(cmd, check=False) + self.smartctl = json.loads(result.stdout.decode()) + except Exception: + # Leave self.smartctl empty + pass + + # Check for attributes + if KEY_NVME in self.smartctl: + self.nvme_attributes.update(self.smartctl[KEY_NVME]) + elif KEY_SMART in self.smartctl: + for a in self.smartctl[KEY_SMART].get('table', {}): + try: + _id = int(a.get('id', -1)) + except ValueError: + # Ignoring invalid attribute + continue + _name = str(a.get('name', 'UNKNOWN')).replace('_', ' ').title() + _raw = int(a.get('raw', {}).get('value', -1)) + _raw_str = a.get('raw', {}).get('string', 'UNKNOWN') + + # Fix power-on time + _r = re.match(r'^(\d+)[Hh].*', _raw_str) + if _id == 9 and _r: + _raw = int(_r.group(1)) + + # Add to dict + self.smart_attributes[_id] = { + 'name': _name, 'raw': _raw, 'raw_str': _raw_str} + + # Self-test data + self.smart_self_test = {} + for k in ['polling_minutes', 'status']: + self.smart_self_test[k] = self.smartctl.get( + 'ata_smart_data', {}).get( + 'self_test', {}).get( + k, {}) + + def safety_check(self, silent=False): + """Run safety checks and disable tests if necessary.""" + if self.nvme_attributes or self.smart_attributes: + self.check_attributes(silent) + + # Check if a self-test is currently running + if 'remaining_percent' in self.smart_self_test['status']: + _msg = 'SMART self-test in progress, all tests disabled' + + # Ask to abort + if not silent: + print_warning('WARNING: {}'.format(_msg)) + print_standard(' ') + if ask('Abort HW Diagnostics?'): + exit_script() + + # Add warning to report + if 'NVMe / SMART' in self.tests: + self.tests['NVMe / SMART'].report = self.generate_attribute_report() + self.tests['NVMe / SMART'].report.append( + '{YELLOW}WARNING: {msg}{CLEAR}'.format(msg=_msg, **COLORS)) + + # Disable all tests for this disk + for t in self.tests.keys(): + self.disable_test(t, 'Denied') + else: + # No NVMe/SMART details + self.disable_test('NVMe / SMART', 'N/A') + if silent: + self.disk_ok = HW_OVERRIDES_FORCED + else: + print_info('Device ({})'.format(self.name)) + print_standard(' {}'.format(self.description)) + print_warning(' No NVMe or SMART data available') + self.disk_ok = HW_OVERRIDES_FORCED or ask( + 'Run tests on this device anyway?') + print_standard(' ') + + if not self.disk_ok: + if 'NVMe / SMART' in self.tests: + # NOTE: This will not overwrite the existing status if set + self.disable_test('NVMe / SMART', 'NS') + if not self.tests['NVMe / SMART'].report: + self.tests['NVMe / SMART'].report = self.generate_attribute_report() + for t in ['badblocks', 'I/O Benchmark']: + self.disable_test(t, 'Denied') + +class State(): + """Object to track device objects and overall state.""" + def __init__(self): + self.cpu = None + self.disks = [] + self.panes = {} + self.quick_mode = False + self.tests = OrderedDict({ + 'Prime95': { + 'Enabled': False, + 'Function': run_mprime_test, + 'Objects': [], + }, + 'NVMe / SMART': { + 'Enabled': False, + 'Function': run_nvme_smart_tests, + 'Objects': [], + }, + 'badblocks': { + 'Enabled': False, + 'Function': run_badblocks_test, + 'Objects': [], + }, + 'I/O Benchmark': { + 'Enabled': False, + 'Function': run_io_benchmark, + 'Objects': [], + }, + }) + + def init(self): + """Remove test objects, set log, and add devices.""" + self.disks = [] + for k, v in self.tests.items(): + v['Objects'] = [] + + # Update LogDir + if not self.quick_mode: + 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']) + self.progress_out = '{}/progress.out'.format(global_vars['LogDir']) + + # Add CPU + self.cpu = CpuObj() + + # Add block devices + cmd = ['lsblk', '--json', '--nodeps', '--paths'] + result = run_program(cmd, check=False) + json_data = json.loads(result.stdout.decode()) + for disk in json_data['blockdevices']: + skip_disk = False + disk_obj = DiskObj(disk['name']) + + # Skip loopback devices, optical devices, etc + if disk_obj.lsblk['type'] != 'disk': + skip_disk = True + + # Skip WK disks + wk_label_regex = r'{}_(LINUX|UFD)'.format(KIT_NAME_SHORT) + for label in disk_obj.labels: + if re.search(wk_label_regex, label, re.IGNORECASE): + skip_disk = True + + # Add disk + if not skip_disk: + self.disks.append(disk_obj) + +class TestObj(): + """Object to track test data.""" + def __init__(self, dev, label=None, info_label=False): + self.aborted = False + self.dev = dev + self.label = label + self.info_label = info_label + self.disabled = False + self.failed = False + self.passed = False + self.report = [] + self.started = False + self.status = '' + self.update_status() + + def update_status(self, new_status=None): + """Update status strings.""" + if self.disabled or re.search(r'ERROR|OVERRIDE', self.status): + return + if new_status: + self.status = build_status_string( + self.label, new_status, self.info_label) + elif not self.status: + self.status = build_status_string( + self.label, 'Pending', self.info_label) + elif self.started and 'Pending' in self.status: + self.status = build_status_string( + self.label, 'Working', self.info_label) + +# 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'] + if status in ['Denied', 'ERROR', 'NS', 'OVERRIDE', 'TimedOut']: + status_color = COLORS['RED'] + elif status in ['Aborted', 'N/A', 'Skipped', 'Unknown', 'Working']: + status_color = COLORS['YELLOW'] + elif status in ['CS']: + status_color = COLORS['GREEN'] + + return '{l_c}{l}{CLEAR}{s_c}{s:>{s_w}}{CLEAR}'.format( + l_c=COLORS['BLUE'] if info_label else '', + l=label, + s_c=status_color, + s=status, + s_w=SIDE_PANE_WIDTH-len(label), + **COLORS) + +def fix_tmux_panes(state, tmux_layout): + """Fix pane sizes if the window 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: + return + + # 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) + +def generate_horizontal_graph(rates, oneline=False): + """Generate horizontal graph from rates, returns list.""" + graph = ['', '', '', ''] + for r in rates: + step = get_graph_step(r, scale=32) + if oneline: + step = get_graph_step(r, scale=8) + + # Set color + r_color = COLORS['CLEAR'] + if r < IO_VARS['Threshold Graph Fail']: + r_color = COLORS['RED'] + elif r < IO_VARS['Threshold Graph Warn']: + r_color = COLORS['YELLOW'] + elif r > IO_VARS['Threshold Graph Great']: + r_color = COLORS['GREEN'] + + # Build graph + full_block = '{}{}'.format(r_color, IO_VARS['Graph Horizontal'][-1]) + if step >= 24: + graph[0] += '{}{}'.format(r_color, IO_VARS['Graph Horizontal'][step-24]) + graph[1] += full_block + graph[2] += full_block + graph[3] += full_block + elif step >= 16: + graph[0] += ' ' + graph[1] += '{}{}'.format(r_color, IO_VARS['Graph Horizontal'][step-16]) + graph[2] += full_block + graph[3] += full_block + elif step >= 8: + graph[0] += ' ' + graph[1] += ' ' + graph[2] += '{}{}'.format(r_color, IO_VARS['Graph Horizontal'][step-8]) + graph[3] += full_block + else: + graph[0] += ' ' + graph[1] += ' ' + graph[2] += ' ' + graph[3] += '{}{}'.format(r_color, IO_VARS['Graph Horizontal'][step]) + graph = [line+COLORS['CLEAR'] for line in graph] + if oneline: + return graph[:-1] + else: + return graph + +def get_graph_step(rate, scale=16): + """Get graph step based on rate and scale, returns int.""" + m_rate = rate / (1024**2) + step = 0 + scale_name = 'Scale {}'.format(scale) + for x in range(scale-1, -1, -1): + # Iterate over scale backwards + if m_rate >= IO_VARS[scale_name][x]: + step = x + break + return step + +def get_read_rate(s): + """Get read rate in bytes/s from dd progress output.""" + real_rate = None + if re.search(r'[KMGT]B/s', s): + human_rate = re.sub(r'^.*\s+(\d+\.?\d*)\s+(.B)/s\s*$', r'\1 \2', s) + real_rate = convert_to_bytes(human_rate) + return real_rate + +def get_status_color(s): + """Get color based on status, returns str.""" + color = COLORS['CLEAR'] + if s in ['Denied', 'ERROR', 'NS', 'OVERRIDE']: + color = COLORS['RED'] + elif s in ['Aborted', 'N/A', 'Unknown', 'Working', 'Skipped']: + color = COLORS['YELLOW'] + elif s in ['CS']: + color = COLORS['GREEN'] + return color + +def menu_diags(state, args): + """Main menu to select and run HW tests.""" + args = [a.lower() for a in args] + title = '{}\nMain Menu'.format(TOP_PANE_TEXT) + # NOTE: Changing the order of main_options will break everything + main_options = [ + {'Base Name': 'Full Diagnostic', 'Enabled': False}, + {'Base Name': 'Disk Diagnostic', 'Enabled': False}, + {'Base Name': 'Disk Diagnostic (Quick)', 'Enabled': False}, + {'Base Name': 'Prime95', 'Enabled': False, 'CRLF': True}, + {'Base Name': 'NVMe / SMART', 'Enabled': False}, + {'Base Name': 'badblocks', 'Enabled': False}, + {'Base Name': 'I/O Benchmark', 'Enabled': False}, + ] + actions = [ + {'Letter': 'A', 'Name': 'Audio Test'}, + {'Letter': 'K', 'Name': 'Keyboard Test'}, + {'Letter': 'N', 'Name': 'Network Test'}, + {'Letter': 'S', 'Name': 'Start', 'CRLF': True}, + {'Letter': 'Q', 'Name': 'Quit'}, + ] + secret_actions = ['M', 'T'] + + # Set initial selections + update_main_options(state, '1', main_options) + + # CLI mode check + if '--cli' in args or 'DISPLAY' not in global_vars['Env']: + actions.append({'Letter': 'R', 'Name': 'Reboot'}) + actions.append({'Letter': 'P', 'Name': 'Power Off'}) + + # Skip menu if running quick check + if '--quick' in args: + update_main_options(state, '3', main_options) + state.quick_mode = True + run_hw_tests(state) + return True + + while True: + # Set quick mode as necessary + if main_options[2]['Enabled'] and main_options[4]['Enabled']: + # Check if only Disk Diags (Quick) and NVMe/SMART are enabled + # 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'] + else: + state.quick_mode = False + + # Deselect presets + slice_end = 3 + if state.quick_mode: + slice_end = 2 + for opt in main_options[:slice_end]: + opt['Enabled'] = False + + # Verify preset selections + num_tests_selected = 0 + for opt in main_options[3:]: + if opt['Enabled']: + num_tests_selected += 1 + if num_tests_selected == 4: + # Full + main_options[0]['Enabled'] = True + elif num_tests_selected == 3 and not main_options[3]['Enabled']: + # Disk + main_options[1]['Enabled'] = True + + # Update checkboxes + for opt in main_options: + _nvme_smart = opt['Base Name'] == 'NVMe / SMART' + opt['Name'] = '{} {} {}'.format( + '[✓]' if opt['Enabled'] else '[ ]', + opt['Base Name'], + QUICK_LABEL if state.quick_mode and _nvme_smart else '') + + # Show menu + selection = menu_select( + title=title, + main_entries=main_options, + action_entries=actions, + secret_actions=secret_actions, + spacer='───────────────────────────────') + + if selection.isnumeric(): + update_main_options(state, selection, main_options) + elif selection == 'A': + run_audio_test() + elif selection == 'K': + run_keyboard_test() + elif selection == 'N': + run_network_test() + elif selection == 'M': + secret_screensaver('matrix') + elif selection == 'T': + # Tubes is close to pipes right? + secret_screensaver('pipes') + elif selection == 'R': + run_program(['/usr/local/bin/wk-power-command', 'reboot']) + elif selection == 'P': + run_program(['/usr/local/bin/wk-power-command', 'poweroff']) + elif selection == 'Q': + break + elif selection == 'S': + run_hw_tests(state) + +def run_audio_test(): + """Run audio test.""" + clear_screen() + run_program(['hw-diags-audio'], check=False, pipe=False) + pause('Press Enter to return to main menu... ') + +def run_badblocks_test(state, test): + """Run a read-only surface scan with badblocks.""" + # Bail early + if test.disabled: + return + + # Prep + print_log('Starting badblocks test for {}'.format(test.dev.path)) + test.started = True + test.update_status() + update_progress_pane(state) + + # Update tmux layout + tmux_update_pane( + state.panes['Top'], + text='{}\nbadblocks: {}'.format( + TOP_PANE_TEXT, test.dev.description)) + test.tmux_layout = TMUX_LAYOUT.copy() + test.tmux_layout.update({ + 'badblocks': {'y': 5, 'Check': True}, + }) + + # Create monitor pane + test.badblocks_out = '{}/badblocks_{}.out'.format( + global_vars['LogDir'], test.dev.name) + state.panes['badblocks'] = tmux_split_window( + lines=5, vertical=True, watch=test.badblocks_out, watch_cmd='tail') + + # Show disk details + clear_screen() + show_report(test.dev.generate_attribute_report()) + print_standard(' ') + + # Start badblocks + print_standard('Running badblocks test...') + try: + test.badblocks_proc = popen_program( + ['sudo', 'hw-diags-badblocks', test.dev.path, test.badblocks_out], + pipe=True) + while True: + try: + test.badblocks_proc.wait(timeout=1) + except subprocess.TimeoutExpired: + fix_tmux_panes(state, test.tmux_layout) + else: + # badblocks finished, exit loop + break + + except KeyboardInterrupt: + test.aborted = True + + # Check result and build report + test.report.append('{BLUE}badblocks{CLEAR}'.format(**COLORS)) + try: + test.badblocks_out = test.badblocks_proc.stdout.read().decode() + except Exception as err: + test.badblocks_out = 'Error: {}'.format(err) + for line in test.badblocks_out.splitlines(): + line = line.strip() + if not line or re.search(r'^Checking', line, re.IGNORECASE): + # Skip empty and progress lines + continue + if re.search(r'^Pass completed.*0.*0/0/0', line, re.IGNORECASE): + test.report.append(' {}'.format(line)) + if not test.aborted: + test.passed = True + else: + test.report.append(' {YELLOW}{line}{CLEAR}'.format( + line=line, **COLORS)) + test.failed = True + if test.aborted: + test.report.append(' {YELLOW}Aborted{CLEAR}'.format(**COLORS)) + test.update_status('Aborted') + raise GenericAbort('Aborted') + + # Disable other drive tests if necessary + if not test.passed: + test.dev.disable_test('I/O Benchmark', 'Denied') + + # Update status + if test.failed: + test.update_status('NS') + elif test.passed: + test.update_status('CS') + else: + test.update_status('Unknown') + + # Done + update_progress_pane(state) + + # Cleanup + tmux_kill_pane(state.panes['badblocks']) + +def run_hw_tests(state): + """Run enabled hardware tests.""" + print_standard('Scanning devices...') + state.init() + + # Build Panes + update_progress_pane(state) + build_outer_panes(state) + + # Show selected tests and create TestObj()s + print_info('Selected Tests:') + for k, v in state.tests.items(): + print_standard(' {:<15} {}{}{} {}'.format( + k, + COLORS['GREEN'] if v['Enabled'] else COLORS['RED'], + 'Enabled' if v['Enabled'] else 'Disabled', + COLORS['CLEAR'], + QUICK_LABEL if state.quick_mode and 'NVMe' in k else '')) + if v['Enabled']: + # Create TestObj and track under both CpuObj/DiskObj and State + if k in TESTS_CPU: + test_obj = TestObj( + dev=state.cpu, label='Prime95', info_label=True) + state.cpu.tests[k] = test_obj + v['Objects'].append(test_obj) + elif k in TESTS_DISK: + for disk in state.disks: + test_obj = TestObj(dev=disk, label=disk.name) + disk.tests[k] = test_obj + v['Objects'].append(test_obj) + print_standard('') + + # Run disk safety checks (if necessary) + _disk_tests_enabled = False + for k in TESTS_DISK: + _disk_tests_enabled |= state.tests[k]['Enabled'] + if _disk_tests_enabled: + for disk in state.disks: + disk.safety_check(silent=state.quick_mode) + + # Run tests + ## Because state.tests is an OrderedDict and the disks were added + ## in order, the tests will be run in order. + try: + for k, v in state.tests.items(): + if v['Enabled']: + f = v['Function'] + for test_obj in v['Objects']: + f(state, test_obj) + except GenericAbort: + # Cleanup + tmux_kill_pane(*state.panes.values()) + + # Rebuild panes + update_progress_pane(state) + build_outer_panes(state) + + # Mark unfinished tests as aborted + for k, v in state.tests.items(): + if v['Enabled']: + for test_obj in v['Objects']: + if re.search(r'(Pending|Working)', test_obj.status): + test_obj.update_status('Aborted') + + # Update side pane + update_progress_pane(state) + + # Done + show_results(state) + if state.quick_mode: + pause('Press Enter to exit...') + else: pause('Press Enter to return to main menu... ') - run_program('tmux kill-pane -a'.split()) + + # Cleanup + tmux_kill_pane(*state.panes.values()) + +def run_io_benchmark(state, test): + """Run a read-only I/O benchmark using dd.""" + # Bail early + if test.disabled: + return + + # Prep + print_log('Starting I/O benchmark test for {}'.format(test.dev.path)) + test.started = True + test.update_status() + update_progress_pane(state) + + # Update tmux layout + tmux_update_pane( + state.panes['Top'], + text='{}\nI/O Benchmark: {}'.format( + TOP_PANE_TEXT, test.dev.description)) + test.tmux_layout = TMUX_LAYOUT.copy() + test.tmux_layout.update({ + 'io_benchmark': {'y': 1000, 'Check': False}, + 'Current': {'y': 15, 'Check': True}, + }) + + # Create monitor pane + test.io_benchmark_out = '{}/io_benchmark_{}.out'.format( + global_vars['LogDir'], test.dev.name) + state.panes['io_benchmark'] = tmux_split_window( + percent=75, vertical=True, + watch=test.io_benchmark_out, watch_cmd='tail') + tmux_resize_pane(y=15) + + # Show disk details + clear_screen() + show_report(test.dev.generate_attribute_report()) + print_standard(' ') + + # Start I/O Benchmark + print_standard('Running I/O benchmark test...') + try: + test.merged_rates = [] + test.read_rates = [] + test.vertical_graph = [] + test.dev.calc_io_dd_values() + + # Run dd read tests + offset = 0 + for i in range(test.dev.dd_chunks): + # Build cmd + i += 1 + skip = test.dev.dd_skip_count + if test.dev.dd_skip_extra and i % test.dev.dd_skip_extra == 0: + skip += 1 + cmd = [ + 'sudo', 'dd', + 'bs={}'.format(IO_VARS['Block Size']), + 'skip={}'.format(offset+skip), + 'count={}'.format(test.dev.dd_chunk_blocks), + 'iflag=direct', + 'if={}'.format(test.dev.path), + 'of=/dev/null'] + + # Run cmd and get read rate + result = run_program(cmd) + result_str = result.stderr.decode().replace('\n', '') + cur_rate = get_read_rate(result_str) + + # Add rate to lists + test.read_rates.append(cur_rate) + test.vertical_graph.append( + '{percent:0.1f} {rate}'.format( + percent=(i/test.dev.dd_chunks)*100, + rate=int(cur_rate/(1024**2)))) + + # Show progress + if i % IO_VARS['Progress Refresh Rate'] == 0: + update_io_progress( + percent=(i/test.dev.dd_chunks)*100, + rate=cur_rate, + progress_file=test.io_benchmark_out) + + # Update offset + offset += test.dev.dd_chunk_blocks + skip + + # Fix panes + fix_tmux_panes(state, test.tmux_layout) + + except DeviceTooSmallError: + # Device too small, skipping test + test.update_status('N/A') + except KeyboardInterrupt: + test.aborted = True + except (subprocess.CalledProcessError, TypeError, ValueError): + # Something went wrong, results unknown + test.update_status('ERROR') + + # Check result and build report + test.report.append('{BLUE}I/O Benchmark{CLEAR}'.format(**COLORS)) + if test.aborted: + test.report.append(' {YELLOW}Aborted{CLEAR}'.format(**COLORS)) + raise GenericAbort('Aborted') + elif not test.read_rates: + if 'ERROR' in test.status: + test.report.append(' {RED}Unknown error{CLEAR}'.format(**COLORS)) + elif 'N/A' in test.status: + # Device too small + test.report.append(' {YELLOW}Disk too small to test{CLEAR}'.format( + **COLORS)) + else: + # Merge rates for horizontal graph + offset = 0 + width = int(test.dev.dd_chunks / IO_VARS['Graph Horizontal Width']) + for i in range(IO_VARS['Graph Horizontal Width']): + test.merged_rates.append( + sum(test.read_rates[offset:offset+width])/width) + offset += width + + # Add horizontal graph to report + for line in generate_horizontal_graph(test.merged_rates): + if not re.match(r'^\s+$', strip_colors(line)): + test.report.append(line) + + # Add read speeds to report + avg_read = sum(test.read_rates) / len(test.read_rates) + min_read = min(test.read_rates) + max_read = max(test.read_rates) + avg_min_max = 'Read speeds avg: {:3.1f}'.format(avg_read/(1024**2)) + avg_min_max += ' min: {:3.1f}'.format(min_read/(1024**2)) + avg_min_max += ' max: {:3.1f}'.format(max_read/(1024**2)) + test.report.append(avg_min_max) + + # Compare read speeds to thresholds + if test.dev.lsblk['rota']: + # Use HDD scale + thresh_min = IO_VARS['Threshold HDD Min'] + thresh_high_avg = IO_VARS['Threshold HDD High Avg'] + thresh_low_avg = IO_VARS['Threshold HDD Low Avg'] + else: + # Use SSD scale + thresh_min = IO_VARS['Threshold SSD Min'] + thresh_high_avg = IO_VARS['Threshold SSD High Avg'] + thresh_low_avg = IO_VARS['Threshold SSD Low Avg'] + if min_read <= thresh_min and avg_read <= thresh_high_avg: + test.failed = True + elif avg_read <= thresh_low_avg: + test.failed = True + else: + test.passed = True + + # Update status + if test.failed: + test.update_status('NS') + elif test.passed: + test.update_status('CS') + elif not 'N/A' in test.status: + test.update_status('Unknown') + + # Save log + with open(test.io_benchmark_out.replace('.', '-raw.'), 'a') as f: + f.write('\n'.join(test.vertical_graph)) + + # Done + update_progress_pane(state) + + # Cleanup + tmux_kill_pane(state.panes['io_benchmark']) + +def run_keyboard_test(): + """Run keyboard test.""" + clear_screen() + run_program(['xev', '-event', 'keyboard'], check=False, pipe=False) + +def run_mprime_test(state, test): + """Test CPU with Prime95 and track temps.""" + # Bail early + if test.disabled: + return + + # Prep + print_log('Starting Prime95 test') + test.started = True + test.update_status() + update_progress_pane(state) + test.sensor_data = get_sensor_data() + + # Update tmux layout + tmux_update_pane( + state.panes['Top'], + text='{}\nPrime95: {}'.format(TOP_PANE_TEXT, test.dev.name)) + test.tmux_layout = TMUX_LAYOUT.copy() + test.tmux_layout.update({ + 'Temps': {'y': 1000, 'Check': False}, + 'mprime': {'y': 11, 'Check': False}, + 'Current': {'y': 3, 'Check': True}, + }) + + # Start live sensor monitor + test.sensors_out = '{}/sensors.out'.format(global_vars['TmpDir']) + with open(test.sensors_out, 'w') as f: + f.write(' ') + f.flush() + sleep(0.5) + test.monitor_proc = popen_program( + ['hw-sensors-monitor', test.sensors_out], + pipe=True) + + # Create monitor and worker panes + state.panes['mprime'] = tmux_split_window( + lines=10, vertical=True, text=' ') + state.panes['Temps'] = tmux_split_window( + behind=True, percent=80, vertical=True, watch=test.sensors_out) + tmux_resize_pane(global_vars['Env']['TMUX_PANE'], y=3) + + # Get idle temps + clear_screen() + try_and_print( + message='Getting idle temps...', indent=0, + function=save_average_temp, cs='Done', + sensor_data=test.sensor_data, temp_label='Idle', + seconds=5) + + # Stress CPU + print_log('Starting Prime95') + test.abort_msg = 'If running too hot, press CTRL+c to abort the test' + run_program(['apple-fans', 'max']) + tmux_update_pane( + state.panes['mprime'], + command=['hw-diags-prime95', global_vars['TmpDir']], + working_dir=global_vars['TmpDir']) + time_limit = int(MPRIME_LIMIT) * 60 + try: + for i in range(time_limit): + clear_screen() + sec_left = (time_limit - i) % 60 + min_left = int( (time_limit - i) / 60) + _status_str = 'Running Prime95 (' + if min_left > 0: + _status_str += '{} minute{}, '.format( + min_left, + 's' if min_left != 1 else '') + _status_str += '{} second{} left)'.format( + sec_left, + 's' if sec_left != 1 else '') + # Not using print wrappers to avoid flooding the log + print(_status_str) + print('{YELLOW}{msg}{CLEAR}'.format(msg=test.abort_msg, **COLORS)) + update_sensor_data(test.sensor_data) + + # Fix panes + fix_tmux_panes(state, test.tmux_layout) + + # Wait + sleep(1) + except KeyboardInterrupt: + # Catch CTRL+C + test.aborted = True + test.update_status('Aborted') + print_warning('\nAborted.') + update_progress_pane(state) + + # Restart live monitor + test.monitor_proc = popen_program( + ['hw-sensors-monitor', test.sensors_out], + pipe=True) + + # Stop Prime95 (twice for good measure) + run_program(['killall', '-s', 'INT', 'mprime'], check=False) + sleep(1) + tmux_kill_pane(state.panes['mprime']) + + # Get cooldown temp + run_program(['apple-fans', 'auto']) + clear_screen() + try_and_print( + message='Letting CPU cooldown for bit...', indent=0, + function=sleep, cs='Done', seconds=10) + try_and_print( + message='Getting cooldown temps...', indent=0, + function=save_average_temp, cs='Done', + sensor_data=test.sensor_data, temp_label='Cooldown', + seconds=5) + + # Move logs to Ticket folder + for item in os.scandir(global_vars['TmpDir']): + try: + shutil.move(item.path, global_vars['LogDir']) + except Exception: + print_error('ERROR: Failed to move "{}" to "{}"'.format( + item.path, + global_vars['LogDir'])) + + # Check results and build report + test.report.append('{BLUE}Prime95{CLEAR}'.format(**COLORS)) + test.logs = {} + for log in ['results.txt', 'prime.log']: + lines = [] + log_path = '{}/{}'.format(global_vars['LogDir'], log) + + # Read and save log + try: + with open(log_path, 'r') as f: + lines = f.read().splitlines() + test.logs[log] = lines + except FileNotFoundError: + # Ignore since files may be missing for slower CPUs + pass + + # results.txt (NS check) + if log == 'results.txt': + for line in lines: + line = line.strip() + if re.search(r'(error|fail)', line, re.IGNORECASE): + test.failed = True + test.update_status('NS') + test.report.append(' {YELLOW}{line}{CLEAR}'.format(line=line, **COLORS)) + + # prime.log (CS check) + if log == 'prime.log': + _tmp = {'Pass': {}, 'Warn': {}} + for line in lines: + line = line.strip() + _r = re.search( + r'(completed.*(\d+) errors, (\d+) warnings)', + line, + re.IGNORECASE) + if _r: + if int(_r.group(2)) + int(_r.group(3)) > 0: + # Encountered errors and/or warnings + _tmp['Warn'][_r.group(1)] = None + else: + # No errors + _tmp['Pass'][_r.group(1)] = None + if len(_tmp['Warn']) > 0: + # NS + test.failed = True + test.passed = False + test.update_status('NS') + elif len(_tmp['Pass']) > 0 and not test.aborted: + test.passed = True + test.update_status('CS') + for line in sorted(_tmp['Pass'].keys()): + test.report.append(' {}'.format(line)) + for line in sorted(_tmp['Warn'].keys()): + test.report.append(' {YELLOW}{line}{CLEAR}'.format(line=line, **COLORS)) + + # Unknown result + if not (test.aborted or test.failed or test.passed): + test.report.append(' {YELLOW}Unknown result{CLEAR}'.format(**COLORS)) + test.update_status('Unknown') + + # 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.report.append(' {}'.format(line)) + + # Done + update_progress_pane(state) + + # Cleanup + tmux_kill_pane(state.panes['mprime'], state.panes['Temps']) + test.monitor_proc.kill() + +def run_network_test(): + """Run network test.""" + clear_screen() + run_program(['hw-diags-network'], check=False, pipe=False) + pause('Press Enter to return to main menu... ') + +def run_nvme_smart_tests(state, test): + """Run NVMe or SMART test for test.dev.""" + # Bail early + if test.disabled: + return + + # Prep + print_log('Starting NVMe/SMART test for {}'.format(test.dev.path)) + _include_short_test = False + test.started = True + test.update_status() + update_progress_pane(state) + + # Update tmux layout + tmux_update_pane( + state.panes['Top'], + text='{}\nDisk Health: {}'.format( + TOP_PANE_TEXT, test.dev.description)) + test.tmux_layout = TMUX_LAYOUT.copy() + test.tmux_layout.update({ + 'smart': {'y': 3, 'Check': True}, + }) + + # NVMe + if test.dev.nvme_attributes: + # NOTE: Pass/Fail is just the attribute check + if test.dev.disk_ok: + test.passed = True + test.update_status('CS') + else: + # NOTE: Other test(s) should've been disabled by DiskObj.safety_check() + test.failed = True + test.update_status('NS') + + # SMART + elif test.dev.smart_attributes: + # NOTE: Pass/Fail based on both attributes and SMART short self-test + if not (test.dev.disk_ok or 'OVERRIDE' in test.status): + test.failed = True + test.update_status('NS') + elif state.quick_mode: + if test.dev.disk_ok: + test.passed = True + test.update_status('CS') + else: + test.failed = True + test.update_status('NS') + else: + # Prep + test.timeout = test.dev.smart_self_test['polling_minutes'].get( + 'short', 5) + test.timeout = int(test.timeout) + 5 + _include_short_test = True + _self_test_started = False + _self_test_finished = False + + # Create monitor pane + test.smart_out = '{}/smart_{}.out'.format( + global_vars['LogDir'], test.dev.name) + with open(test.smart_out, 'w') as f: + f.write('SMART self-test status:\n Starting...') + state.panes['smart'] = tmux_split_window( + lines=3, vertical=True, watch=test.smart_out) + + # Show attributes + clear_screen() + print_info('Device ({})'.format(test.dev.name)) + print_standard(' {}'.format(test.dev.description)) + show_report(test.dev.generate_attribute_report()) + print_standard(' ') + + # Start short test + print_standard('Running self-test...') + cmd = ['sudo', 'smartctl', '--test=short', test.dev.path] + run_program(cmd, check=False) + + # Monitor progress + try: + for i in range(int(test.timeout*60)): + sleep(1) + + # Fix panes + fix_tmux_panes(state, test.tmux_layout) + + # Only update SMART progress every 5 seconds + if i % 5 != 0: + continue + + # Update SMART data + test.dev.get_smart_details() + + if _self_test_started: + # Update progress file + with open(test.smart_out, 'w') as f: + f.write('SMART self-test status:\n {}'.format( + test.dev.smart_self_test['status'].get( + 'string', 'UNKNOWN').capitalize())) + + # Check if test has finished + if 'remaining_percent' not in test.dev.smart_self_test['status']: + _self_test_finished = True + break + + else: + # Check if test has started + if 'remaining_percent' in test.dev.smart_self_test['status']: + _self_test_started = True + + except KeyboardInterrupt: + test.aborted = True + test.report = test.dev.generate_attribute_report() + test.report.append('{BLUE}SMART Short self-test{CLEAR}'.format( + **COLORS)) + test.report.append(' {YELLOW}Aborted{CLEAR}'.format(**COLORS)) + test.update_status('Aborted') + raise GenericAbort('Aborted') + + # Check if timed out + if _self_test_finished: + if test.dev.smart_self_test['status'].get('passed', False): + if 'OVERRIDE' not in test.status: + test.passed = True + test.update_status('CS') + else: + test.failed = True + test.update_status('NS') + else: + test.dev.smart_timeout = True + test.update_status('TimedOut') + + # Disable other drive tests if necessary + if not test.passed: + for t in ['badblocks', 'I/O Benchmark']: + test.dev.disable_test(t, 'Denied') + + # Cleanup + tmux_kill_pane(state.panes['smart']) + + # Save report + test.report = test.dev.generate_attribute_report( + short_test=_include_short_test) + + # Done + update_progress_pane(state) + +def secret_screensaver(screensaver=None): + """Show screensaver.""" + if screensaver == 'matrix': + cmd = 'cmatrix -abs'.split() + elif screensaver == 'pipes': + cmd = 'pipes -t 0 -t 1 -t 2 -t 3 -p 5 -R -r 4000'.split() + else: + raise Exception('Invalid screensaver') + run_program(cmd, check=False, pipe=False) + +def show_report(report): + """Show report on screen and save to log w/out color.""" + for line in report: + print(line) + print_log(strip_colors(line)) + +def show_results(state): + """Show results for all tests.""" + clear_screen() + tmux_update_pane( + state.panes['Top'], + text='{}\nResults'.format(TOP_PANE_TEXT)) + + # CPU tests + _enabled = False + for k in TESTS_CPU: + _enabled |= state.tests[k]['Enabled'] + if _enabled: + print_success('CPU:'.format(k)) + show_report(state.cpu.generate_cpu_report()) + print_standard(' ') + + # Disk tests + _enabled = False + for k in TESTS_DISK: + _enabled |= state.tests[k]['Enabled'] + if _enabled: + print_success('Disk{}:'.format( + '' if len(state.disks) == 1 else 's')) + for disk in state.disks: + show_report(disk.generate_disk_report()) + print_standard(' ') + +def update_main_options(state, selection, main_options): + """Update menu and state based on selection.""" + index = int(selection) - 1 + main_options[index]['Enabled'] = not main_options[index]['Enabled'] + + # Handle presets + if index == 0: + # Full + if main_options[index]['Enabled']: + for opt in main_options[1:3]: + opt['Enabled'] = False + for opt in main_options[3:]: + opt['Enabled'] = True + else: + for opt in main_options[3:]: + opt['Enabled'] = False + elif index == 1: + # Disk + if main_options[index]['Enabled']: + main_options[0]['Enabled'] = False + for opt in main_options[2:4]: + opt['Enabled'] = False + for opt in main_options[4:]: + opt['Enabled'] = True + else: + for opt in main_options[4:]: + opt['Enabled'] = False + elif index == 2: + # Disk (Quick) + if main_options[index]['Enabled']: + for opt in main_options[:2] + main_options[3:]: + opt['Enabled'] = False + main_options[4]['Enabled'] = True + else: + main_options[4]['Enabled'] = False + + # Update state + for opt in main_options[3:]: + state.tests[opt['Base Name']]['Enabled'] = opt['Enabled'] + + # Done + return main_options def update_io_progress(percent, rate, progress_file): - """Update I/O progress file.""" - bar_color = COLORS['CLEAR'] - rate_color = COLORS['CLEAR'] - step = get_graph_step(rate, scale=32) - if rate < IO_VARS['Threshold Graph Fail']: - bar_color = COLORS['RED'] - rate_color = COLORS['YELLOW'] - elif rate < IO_VARS['Threshold Graph Warn']: - bar_color = COLORS['YELLOW'] - rate_color = COLORS['YELLOW'] - elif rate > IO_VARS['Threshold Graph Great']: - bar_color = COLORS['GREEN'] - rate_color = COLORS['GREEN'] - line = ' {p:5.1f}% {b_color}{b:<4} {r_color}{r:6.1f} Mb/s{c}\n'.format( - p=percent, - b_color=bar_color, - b=IO_VARS['Graph Vertical'][step], - r_color=rate_color, - r=rate/(1024**2), - c=COLORS['CLEAR']) - with open(progress_file, 'a') as f: - f.write(line) + """Update I/O progress file.""" + bar_color = COLORS['CLEAR'] + rate_color = COLORS['CLEAR'] + step = get_graph_step(rate, scale=32) + if rate < IO_VARS['Threshold Graph Fail']: + bar_color = COLORS['RED'] + rate_color = COLORS['YELLOW'] + elif rate < IO_VARS['Threshold Graph Warn']: + bar_color = COLORS['YELLOW'] + rate_color = COLORS['YELLOW'] + elif rate > IO_VARS['Threshold Graph Great']: + bar_color = COLORS['GREEN'] + rate_color = COLORS['GREEN'] + line = ' {p:5.1f}% {b_color}{b:<4} {r_color}{r:6.1f} Mb/s{c}\n'.format( + p=percent, + b_color=bar_color, + b=IO_VARS['Graph Vertical'][step], + r_color=rate_color, + r=rate/(1024**2), + c=COLORS['CLEAR']) + with open(progress_file, 'a') as f: + f.write(line) -def update_progress(): - """Update progress file.""" - if 'Progress Out' not in TESTS: - TESTS['Progress Out'] = '{}/progress.out'.format(global_vars['LogDir']) - output = [] - output.append('{BLUE}HW Diagnostics{CLEAR}'.format(**COLORS)) - output.append('───────────────') - if TESTS['Prime95']['Enabled']: - output.append(' ') - output.append('{BLUE}Prime95{s_color}{status:>8}{CLEAR}'.format( - s_color = get_status_color(TESTS['Prime95']['Status']), - status = TESTS['Prime95']['Status'], - **COLORS)) - if TESTS['NVMe/SMART']['Enabled']: - output.append(' ') - output.append('{BLUE}NVMe / SMART{CLEAR}'.format(**COLORS)) - if TESTS['NVMe/SMART']['Quick']: - output.append('{YELLOW} (Quick Check){CLEAR}'.format(**COLORS)) - for dev, status in sorted(TESTS['NVMe/SMART']['Status'].items()): - output.append('{dev}{s_color}{status:>{pad}}{CLEAR}'.format( - dev = dev, - pad = 15-len(dev), - s_color = get_status_color(status), - status = status, - **COLORS)) - if TESTS['badblocks']['Enabled']: - output.append(' ') - output.append('{BLUE}badblocks{CLEAR}'.format(**COLORS)) - for dev, status in sorted(TESTS['badblocks']['Status'].items()): - output.append('{dev}{s_color}{status:>{pad}}{CLEAR}'.format( - dev = dev, - pad = 15-len(dev), - s_color = get_status_color(status), - status = status, - **COLORS)) - if TESTS['iobenchmark']['Enabled']: - output.append(' ') - output.append('{BLUE}I/O Benchmark{CLEAR}'.format(**COLORS)) - for dev, status in sorted(TESTS['iobenchmark']['Status'].items()): - output.append('{dev}{s_color}{status:>{pad}}{CLEAR}'.format( - dev = dev, - pad = 15-len(dev), - s_color = get_status_color(status), - status = status, - **COLORS)) +def update_progress_pane(state): + """Update progress file for side pane.""" + output = [] + for k, v in state.tests.items(): + # Skip disabled sections + if not v['Enabled']: + continue - # Add line-endings - output = ['{}\n'.format(line) for line in output] + # Add section name + if k != 'Prime95': + output.append('{BLUE}{name}{CLEAR}'.format(name=k, **COLORS)) + if 'SMART' in k and state.quick_mode: + output[-1] += ' {}'.format(QUICK_LABEL) - with open(TESTS['Progress Out'], 'w') as f: - f.writelines(output) + # Add status from test object(s) + for test in v['Objects']: + output.append(test.status) -def upload_to_imgur(image_path): - """Upload image to Imgur and return image url as str.""" - image_data = None - image_link = None + # Add spacer before next section + output.append(' ') - # Bail early - if not image_path: - raise GenericError + # Add line-endings + output = ['{}\n'.format(line) for line in output] - # Read image file and convert to base64 then convert to str - with open(image_path, 'rb') as f: - image_data = base64.b64encode(f.read()).decode() - - # POST image - url = "https://api.imgur.com/3/image" - boundary = '----WebKitFormBoundary7MA4YWxkTrZu0gW' - payload = ('--{boundary}\r\nContent-Disposition: form-data; ' - 'name="image"\r\n\r\n{data}\r\n--{boundary}--') - payload = payload.format(boundary=boundary, data=image_data) - headers = { - 'content-type': 'multipart/form-data; boundary={}'.format(boundary), - 'Authorization': 'Client-ID {}'.format(IMGUR_CLIENT_ID), - } - response = requests.request("POST", url, data=payload, headers=headers) - - # Return image link - if response.ok: - json_data = json.loads(response.text) - image_link = json_data['data']['link'] - return image_link - -def upload_to_nextcloud(image_path, ticket_number, dev_name): - """Upload image to Nextcloud server and return folder url as str.""" - image_data = None - - # Bail early - if not image_path: - raise GenericError - - # Read image file and convert to base64 - with open(image_path, 'rb') as f: - image_data = f.read() - - # PUT image - url = '{base_url}/{ticket_number}_iobenchmark_{dev_name}_{date}.png'.format( - base_url=BENCHMARK_SERVER['Url'], - ticket_number=ticket_number, - dev_name=dev_name, - date=global_vars.get('Date-Time', 'Unknown Date-Time')) - requests.put( - url, - data=image_data, - headers = {'X-Requested-With': 'XMLHttpRequest'}, - auth = (BENCHMARK_SERVER['User'], BENCHMARK_SERVER['Pass'])) - - # Return folder link - return BENCHMARK_SERVER['Short Url'] + with open(state.progress_out, 'w') as f: + f.writelines(output) if __name__ == '__main__': - print("This file is not meant to be called directly.") + print("This file is not meant to be called directly.") -# vim: sts=4 sw=4 ts=4 +# vim: sts=2 sw=2 ts=2 diff --git a/.bin/Scripts/functions/sensors.py b/.bin/Scripts/functions/sensors.py new file mode 100644 index 00000000..99e7999a --- /dev/null +++ b/.bin/Scripts/functions/sensors.py @@ -0,0 +1,198 @@ +# Wizard Kit: Functions - Sensors + +import json +import re + +from functions.tmux import * + +# STATIC VARIABLES +TEMP_LIMITS = { + 'GREEN': 60, + 'YELLOW': 70, + 'ORANGE': 80, + 'RED': 90, + } + +# REGEX +REGEX_COLORS = re.compile(r'\033\[\d+;?1?m') + +def clear_temps(sensor_data): + """Clear saved temps but keep structure, returns dict.""" + for _section, _adapters in sensor_data.items(): + for _adapter, _sources in _adapters.items(): + for _source, _data in _sources.items(): + _data['Temps'] = [] + +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 + +def generate_sensor_report( + sensor_data, *temp_labels, + colors=True, core_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: + continue + for _adapter, _sources in sorted(_adapters.items()): + # Adapter + report.append(fix_sensor_str(_adapter)) + for _source, _data in sorted(_sources.items()): + # Source + _line = '{:18} '.format(fix_sensor_str(_source)) + # Temps (skip label for Current) + for _label in temp_labels: + _line += '{}{}{} '.format( + _label.lower() if _label != 'Current' else '', + ': ' if _label != 'Current' else '', + get_temp_str(_data.get(_label, '???'), colors=colors)) + report.append(_line) + if not core_only: + report.append(' ') + + # Handle empty reports (i.e. no sensors detected) + if not report: + report = [ + '{}WARNING: No sensors found{}'.format( + COLORS['YELLOW'] if colors else '', + COLORS['CLEAR'] if colors else ''), + ' ', + 'Please monitor temps manually'] + + # Done + return report + +def get_colored_temp_str(temp): + """Get colored string based on temp, returns str.""" + try: + temp = float(temp) + except ValueError: + return '{YELLOW}{temp}{CLEAR}'.format(temp=temp, **COLORS) + if temp > TEMP_LIMITS['RED']: + color = COLORS['RED'] + elif temp > TEMP_LIMITS['ORANGE']: + color = COLORS['ORANGE'] + elif temp > TEMP_LIMITS['YELLOW']: + color = COLORS['YELLOW'] + elif temp > TEMP_LIMITS['GREEN']: + color = COLORS['GREEN'] + elif temp > 0: + color = COLORS['BLUE'] + else: + color = COLORS['CLEAR'] + return '{color}{prefix}{temp:2.0f}°C{CLEAR}'.format( + color = color, + prefix = '-' if temp < 0 else '', + temp = temp, + **COLORS) + +def get_raw_sensor_data(): + """Read sensor data and return dict.""" + cmd = ['sensors', '-j'] + result = run_program(cmd) + return json.loads(result.stdout.decode()) + +def get_sensor_data(): + """Parse raw sensor data and return new dict.""" + json_data = get_raw_sensor_data() + sensor_data = {'CoreTemps': {}, 'Other': {}} + for _adapter, _sources in json_data.items(): + if 'coretemp' in _adapter: + _section = 'CoreTemps' + else: + _section = 'Other' + sensor_data[_section][_adapter] = {} + _sources.pop('Adapter', None) + + # Find current temp and add to dict + ## current temp is labeled xxxx_input + for _source, _labels in _sources.items(): + for _label, _temp in _labels.items(): + if 'input' in _label: + sensor_data[_section][_adapter][_source] = { + 'Current': _temp, + 'Label': _label, + 'Max': _temp, + 'Temps': [_temp], + } + + # Remove empty sections + for k, v in sensor_data.items(): + v = {k2: v2 for k2, v2 in v.items() if v2} + + # Done + return sensor_data + +def get_temp_str(temp, colors=True): + """Get temp string, returns str.""" + if colors: + return get_colored_temp_str(temp) + try: + temp = float(temp) + except ValueError: + return '{}'.format(temp) + else: + return '{}{:2.0f}°C'.format( + '-' if temp < 0 else '', + temp) + +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: + report = generate_sensor_report(sensor_data, 'Current', 'Max') + f.write('\n'.join(report)) + sleep(1) + if monitor_pane and not tmux_poll_pane(monitor_pane): + break + +def save_average_temp(sensor_data, temp_label, seconds=10): + """Calculate average temps and record under temp_label, returns dict.""" + clear_temps(sensor_data) + + # Get temps + for i in range(seconds): + update_sensor_data(sensor_data) + sleep(1) + + # Calculate averages + for _section, _adapters in sensor_data.items(): + for _adapter, _sources in _adapters.items(): + for _source, _data in _sources.items(): + _data[temp_label] = sum(_data['Temps']) / len(_data['Temps']) + +def update_sensor_data(sensor_data): + """Read sensors and update existing sensor_data, returns dict.""" + json_data = get_raw_sensor_data() + for _section, _adapters in sensor_data.items(): + for _adapter, _sources in _adapters.items(): + for _source, _data in _sources.items(): + _label = _data['Label'] + _temp = json_data[_adapter][_source][_label] + _data['Current'] = _temp + _data['Max'] = max(_temp, _data['Max']) + _data['Temps'].append(_temp) + +def join_columns(column1, column2, width=55): + return '{:<{}}{}'.format( + column1, + 55+len(column1)-len(REGEX_COLORS.sub('', column1)), + column2) + +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/tmux.py b/.bin/Scripts/functions/tmux.py new file mode 100644 index 00000000..84046d0b --- /dev/null +++ b/.bin/Scripts/functions/tmux.py @@ -0,0 +1,148 @@ +# Wizard Kit: Functions - tmux + +from functions.common import * + +def create_file(filepath): + """Create file if it doesn't exist.""" + if not os.path.exists(filepath): + with open(filepath, 'w') as f: + f.write('') + +def tmux_get_pane_size(pane_id=None): + """Get target, or current, pane size, returns tuple.""" + x = -1 + y = -1 + cmd = ['tmux', 'display', '-p'] + if pane_id: + cmd.extend(['-t', pane_id]) + cmd.append('#{pane_width} #{pane_height}') + + # Run cmd and set x & y + result = run_program(cmd, check=False) + try: + x, y = result.stdout.decode().strip().split() + x = int(x) + y = int(y) + except Exception: + # Ignore and return unrealistic values + pass + + return (x, y) + +def tmux_kill_all_panes(pane_id=None): + """Kill all tmux panes except the active pane or pane_id if specified.""" + cmd = ['tmux', 'kill-pane', '-a'] + if pane_id: + cmd.extend(['-t', pane_id]) + run_program(cmd, check=False) + +def tmux_kill_pane(*panes): + """Kill tmux pane by id.""" + cmd = ['tmux', 'kill-pane', '-t'] + for pane_id in panes: + run_program(cmd+[pane_id], check=False) + +def tmux_poll_pane(pane_id): + """Check if pane exists, returns bool.""" + cmd = ['tmux', 'list-panes', '-F', '#D'] + result = run_program(cmd, check=False) + panes = result.stdout.decode().splitlines() + return pane_id in panes + +def tmux_resize_pane(pane_id=None, x=None, y=None, **kwargs): + """Resize pane to specific hieght or width.""" + if not x and not y: + raise Exception('Neither height nor width specified.') + + cmd = ['tmux', 'resize-pane'] + if pane_id: + # NOTE: If pane_id not specified then the current pane will be resized + cmd.extend(['-t', pane_id]) + if x: + cmd.extend(['-x', str(x)]) + elif y: + cmd.extend(['-y', str(y)]) + + run_program(cmd, check=False) + +def tmux_split_window( + lines=None, percent=None, + behind=False, vertical=False, + follow=False, target_pane=None, + command=None, working_dir=None, + text=None, watch=None, watch_cmd='cat'): + """Run tmux split-window command and return pane_id as str.""" + # Bail early + if not lines and not percent: + raise Exception('Neither lines nor percent specified.') + if not command and not text and not watch: + raise Exception('No command, text, or watch file specified.') + + # Build cmd + cmd = ['tmux', 'split-window', '-PF', '#D'] + if behind: + cmd.append('-b') + if vertical: + cmd.append('-v') + else: + cmd.append('-h') + if not follow: + cmd.append('-d') + if lines is not None: + cmd.extend(['-l', str(lines)]) + elif percent is not None: + cmd.extend(['-p', str(percent)]) + if target_pane: + cmd.extend(['-t', str(target_pane)]) + + if working_dir: + cmd.extend(['-c', working_dir]) + if command: + cmd.extend(command) + elif text: + cmd.extend(['echo-and-hold "{}"'.format(text)]) + elif watch: + create_file(watch) + if watch_cmd == 'cat': + cmd.extend([ + 'watch', '--color', '--no-title', + '--interval', '1', + 'cat', watch]) + elif watch_cmd == 'tail': + cmd.extend(['tail', '-f', watch]) + + # Run and return pane_id + result = run_program(cmd) + return result.stdout.decode().strip() + +def tmux_update_pane( + pane_id, command=None, working_dir=None, + text=None, watch=None, watch_cmd='cat'): + """Respawn with either a new command or new text.""" + # Bail early + if not command and not text and not watch: + raise Exception('No command, text, or watch file specified.') + + cmd = ['tmux', 'respawn-pane', '-k', '-t', pane_id] + if working_dir: + cmd.extend(['-c', working_dir]) + if command: + cmd.extend(command) + elif text: + cmd.extend(['echo-and-hold "{}"'.format(text)]) + elif watch: + create_file(watch) + if watch_cmd == 'cat': + cmd.extend([ + 'watch', '--color', '--no-title', + '--interval', '1', + 'cat', watch]) + elif watch_cmd == 'tail': + cmd.extend(['tail', '-f', watch]) + + run_program(cmd) + +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 f7c1739c..5b6f4f76 100755 --- a/.bin/Scripts/hw-diags-menu +++ b/.bin/Scripts/hw-diags-menu @@ -9,22 +9,29 @@ import sys os.chdir(os.path.dirname(os.path.realpath(__file__))) sys.path.append(os.getcwd()) from functions.hw_diags import * +from functions.tmux import * init_global_vars() if __name__ == '__main__': - try: - # Prep - clear_screen() + # Show menu + try: + state = State() + menu_diags(state, sys.argv) + except KeyboardInterrupt: + print_standard(' ') + print_warning('Aborted') + print_standard(' ') + sleep(1) + pause('Press Enter to exit...') + except SystemExit: + # Normal exit + pass + except: + tmux_kill_all_panes() + major_exception() - # Show menu - menu_diags(*sys.argv) - - # Done - #print_standard('\nDone.') - #pause("Press Enter to exit...") - exit_script() - except SystemExit: - pass - except: - major_exception() + # Done + tmux_kill_all_panes() + exit_script() +# vim: sts=2 sw=2 ts=2 diff --git a/.bin/Scripts/hw-diags-prime95 b/.bin/Scripts/hw-diags-prime95 index 30c6994d..4927da76 100755 --- a/.bin/Scripts/hw-diags-prime95 +++ b/.bin/Scripts/hw-diags-prime95 @@ -14,5 +14,6 @@ if [ ! -d "$1" ]; then fi # Run Prime95 -mprime -t | grep -iv --line-buffered 'stress.txt' | tee -a "$1/prime.log" +cd "$1" +mprime -t | grep -iv --line-buffered 'stress.txt' | tee -a "prime.log" diff --git a/.bin/Scripts/hw-sensors b/.bin/Scripts/hw-sensors index f251c589..39ca7147 100755 --- a/.bin/Scripts/hw-sensors +++ b/.bin/Scripts/hw-sensors @@ -1,168 +1,10 @@ -#!/bin/python3 +#!/bin/bash # ## Wizard Kit: Sensor monitoring tool -import itertools -import os -import shutil -import sys +WINDOW_NAME="Hardware Sensors" +MONITOR="hw-sensors-monitor" -# Init -os.chdir(os.path.dirname(os.path.realpath(__file__))) -sys.path.append(os.getcwd()) -from functions.common import * -from borrowed import sensors - -# STATIC VARIABLES -COLORS = { - 'CLEAR': '\033[0m', - 'RED': '\033[31m', - 'GREEN': '\033[32m', - 'YELLOW': '\033[33m', - 'ORANGE': '\033[31;1m', - 'BLUE': '\033[34m' - } -TEMP_LIMITS = { - 'GREEN': 60, - 'YELLOW': 70, - 'ORANGE': 80, - 'RED': 90, - } - -# REGEX -REGEX_COLORS = re.compile(r'\033\[\d+;?1?m') - -def color_temp(temp): - try: - temp = float(temp) - except ValueError: - return '{YELLOW}{temp}{CLEAR}'.format(temp=temp, **COLORS) - if temp > TEMP_LIMITS['RED']: - color = COLORS['RED'] - elif temp > TEMP_LIMITS['ORANGE']: - color = COLORS['ORANGE'] - elif temp > TEMP_LIMITS['YELLOW']: - color = COLORS['YELLOW'] - elif temp > TEMP_LIMITS['GREEN']: - color = COLORS['GREEN'] - elif temp > 0: - color = COLORS['BLUE'] - else: - color = COLORS['CLEAR'] - return '{color}{prefix}{temp:2.0f}°C{CLEAR}'.format( - color = color, - prefix = '-' if temp < 0 else '', - temp = temp, - **COLORS) - -def get_feature_string(chip, feature): - sfs = list(sensors.SubFeatureIterator(chip, feature)) # get a list of all subfeatures - label = sensors.get_label(chip, feature) - skipname = len(feature.name)+1 # skip common prefix - data = {} - - if feature.type != sensors.feature.TEMP: - # Skip non-temperature sensors - return None - - for sf in sfs: - name = sf.name[skipname:].decode("utf-8").strip() - try: - val = sensors.get_value(chip, sf.number) - except Exception: - # Ignore upstream sensor bugs and lie instead - val = -123456789 - if 'alarm' in name: - # Skip - continue - if '--nocolor' in sys.argv: - try: - temp = float(val) - except ValueError: - data[name] = ' {}°C'.format(val) - else: - data[name] = '{}{:2.0f}°C'.format( - '-' if temp < 0 else '', - temp) - else: - data[name] = color_temp(val) - - main_temp = data.pop('input', None) - if main_temp: - list_data = [] - for item in ['max', 'crit']: - if item in data: - list_data.append('{}: {}'.format(item, data.pop(item))) - list_data.extend( - ['{}: {}'.format(k, v) for k, v in sorted(data.items())]) - data_str = '{:18} {} {}'.format( - label, main_temp, ', '.join(list_data)) - else: - list_data.extend(sorted(data.items())) - list_data = ['{}: {}'.format(item[0], item[1]) for item in list_data] - data_str = '{:18} {}'.format(label, ', '.join(list_data)) - return data_str - -def join_columns(column1, column2, width=55): - return '{:<{}}{}'.format( - column1, - 55+len(column1)-len(REGEX_COLORS.sub('', column1)), - column2) - -if __name__ == '__main__': - try: - # Prep - sensors.init() - - # Get sensor data - chip_temps = {} - for chip in sensors.ChipIterator(): - chip_name = '{} ({})'.format( - sensors.chip_snprintf_name(chip), - sensors.get_adapter_name(chip.bus)) - chip_feats = [get_feature_string(chip, feature) - for feature in sensors.FeatureIterator(chip)] - # Strip empty/None items - chip_feats = [f for f in chip_feats if f] - - if chip_feats: - chip_temps[chip_name] = [chip_name, *chip_feats, ''] - - # Sort chips - sensor_temps = [] - for chip in [k for k in sorted(chip_temps.keys()) if 'coretemp' in k]: - sensor_temps.extend(chip_temps[chip]) - for chip in sorted(chip_temps.keys()): - if 'coretemp' not in chip: - sensor_temps.extend(chip_temps[chip]) - - # Wrap columns as needed - screen_size = shutil.get_terminal_size() - rows = screen_size.lines - 1 - if len(sensor_temps) > rows and screen_size.columns > 55*2: - sensor_temps = list(itertools.zip_longest( - sensor_temps[:rows], sensor_temps[rows:], fillvalue='')) - sensor_temps = [join_columns(a, b) for a, b in sensor_temps] - - # Print data - if sensor_temps: - for line in sensor_temps: - print_standard(line) - else: - if '--nocolor' in sys.argv: - print_standard('WARNING: No sensors found') - print_standard('\nPlease monitor temps manually') - else: - print_warning('WARNING: No sensors found') - print_standard('\nPlease monitor temps manually') - - # Done - sensors.cleanup() - exit_script() - except SystemExit: - sensors.cleanup() - pass - except: - sensors.cleanup() - major_exception() +# Start session +tmux new-session -n "$WINDOW_NAME" "$MONITOR" diff --git a/.bin/Scripts/hw-sensors-monitor b/.bin/Scripts/hw-sensors-monitor new file mode 100755 index 00000000..42757748 --- /dev/null +++ b/.bin/Scripts/hw-sensors-monitor @@ -0,0 +1,37 @@ +#!/bin/python3 +# +## Wizard Kit: Sensor monitoring tool + +import os +import sys + +# Init +os.chdir(os.path.dirname(os.path.realpath(__file__))) +sys.path.append(os.getcwd()) +from functions.sensors import * +from functions.tmux import * +init_global_vars() + +if __name__ == '__main__': + background = False + try: + if len(sys.argv) > 1 and os.path.exists(sys.argv[1]): + background = True + monitor_file = sys.argv[1] + monitor_pane = None + else: + result = run_program(['mktemp']) + monitor_file = result.stdout.decode().strip() + if not background: + monitor_pane = tmux_split_window( + percent=1, vertical=True, watch=monitor_file) + cmd = ['tmux', 'resize-pane', '-Z', '-t', monitor_pane] + run_program(cmd, check=False) + monitor_sensors(monitor_pane, monitor_file) + exit_script() + except SystemExit: + pass + except: + major_exception() + +# vim: sts=2 sw=2 ts=2 diff --git a/.bin/Scripts/settings/main.py b/.bin/Scripts/settings/main.py index b59dce45..e6aff08f 100644 --- a/.bin/Scripts/settings/main.py +++ b/.bin/Scripts/settings/main.py @@ -4,6 +4,8 @@ ENABLED_OPEN_LOGS = False ENABLED_TICKET_NUMBERS = False ENABLED_UPLOAD_DATA = True +HW_OVERRIDES_FORCED = False +HW_OVERRIDES_LIMITED = True # If True this disables HW_OVERRIDE_FORCED # STATIC VARIABLES (also used by BASH and BATCH files) ## NOTE: There are no spaces around the = for easier parsing in BASH and BATCH @@ -22,7 +24,7 @@ SSH_USER='sql_tunnel' # imgur IMGUR_CLIENT_ID='3d1ee1d38707b85' # Live Linux -MPRIME_LIMIT='7' # of minutes to run Prime95 during hw-diags +MPRIME_LIMIT='7' # of minutes to run Prime95 during hw-diags ROOT_PASSWORD='1201 loves computers!' TECH_PASSWORD='Sorted1201' # Root Certificate Authority diff --git a/.bin/Scripts/system_checklist.py b/.bin/Scripts/system_checklist.py index aa4e68b7..3cbc2224 100644 --- a/.bin/Scripts/system_checklist.py +++ b/.bin/Scripts/system_checklist.py @@ -139,3 +139,5 @@ if __name__ == '__main__': pass except: major_exception() + +# vim: sts=4 sw=4 ts=4 diff --git a/.bin/Scripts/user_checklist.py b/.bin/Scripts/user_checklist.py index 29accf82..56ae7241 100644 --- a/.bin/Scripts/user_checklist.py +++ b/.bin/Scripts/user_checklist.py @@ -34,8 +34,6 @@ if __name__ == '__main__': 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?') answer_config_classicshell = True answer_config_explorer_user = True @@ -95,3 +93,5 @@ if __name__ == '__main__': pass except: major_exception() + +# vim: sts=4 sw=4 ts=4 diff --git a/.linux_items/include/EFI/boot/refind.conf b/.linux_items/include/EFI/boot/refind.conf index 5ca39701..ffe5c747 100644 --- a/.linux_items/include/EFI/boot/refind.conf +++ b/.linux_items/include/EFI/boot/refind.conf @@ -32,42 +32,6 @@ menuentry "Linux" { submenuentry "Linux (CLI)" { add_options "loglevel=4 nomodeset nox" } - submenuentry "Linux (Mac CLI)" { - add_options "loglevel=5 nomodeset nox" - } - submenuentry "Linux (MacBook9,1)" { - add_options "loglevel=5 intremap=nosid noacpi nomodeset" - } - submenuentry "Linux (MacBookAir5,2)" { - add_options "loglevel=5 intremap=off" - } - submenuentry "Linux (MacBookAir6,x)" { - add_options "loglevel=5 libata.force=1:noncq" - } - submenuentry "Linux (MacBookPro7,1)" { - add_options "loglevel=5 acpi_osi=! acpi_osi="Darwin" intremap=off nomodeset" - } - submenuentry "Linux (MacBookPro10,x)" { - add_options "loglevel=5 noapic" - } - submenuentry "Linux (MacBookPro11,x)" { - add_options "loglevel=5 acpi_osi=" - } - submenuentry "Linux (Mac Generic Fix 1)" { - add_options "loglevel=5 acpi=force irqpoll noapic" - } - submenuentry "Linux (Mac Generic Fix 2)" { - add_options "loglevel=5 acpi=off" - } - submenuentry "Linux (Mac Generic Fix 3)" { - add_options "loglevel=5 acpi_osi=! acpi_osi="Darwin"" - } - submenuentry "Linux (Mac Generic Fix 4)" { - add_options "loglevel=5 add_efi_memmap" - } - submenuentry "Linux (DEBUG)" { - add_options "loglevel=7 nomodeset nox" - } } #UFD#menuentry "WindowsPE" { #UFD# ostype windows diff --git a/.linux_items/include/airootfs/etc/skel/.config/i3/config b/.linux_items/include/airootfs/etc/skel/.config/i3/config index ca1a5439..102b50f5 100644 --- a/.linux_items/include/airootfs/etc/skel/.config/i3/config +++ b/.linux_items/include/airootfs/etc/skel/.config/i3/config @@ -72,7 +72,7 @@ bindsym $mod+d exec "urxvt -title 'Hardware Diagnostics' -e hw-diags" bindsym $mod+f exec "thunar ~" bindsym $mod+i exec "hardinfo" bindsym $mod+m exec "urxvt -title 'Mount All Volumes' -e mount-all-volumes gui" -bindsym $mod+s exec "urxvt -title 'Hardware Diagnostics' -e hw-diags quick" +bindsym $mod+s exec "urxvt -title 'Hardware Diagnostics' -e hw-diags --quick" bindsym $mod+t exec "urxvt -e zsh -c 'tmux new-session -A -t general; zsh'" bindsym $mod+v exec "urxvt -title 'Hardware Sensors' -e watch -c -n1 -t hw-sensors" bindsym $mod+w exec "firefox" diff --git a/.linux_items/include/airootfs/etc/skel/.config/openbox/rc.xml b/.linux_items/include/airootfs/etc/skel/.config/openbox/rc.xml index 90c7b0e0..e563ec04 100644 --- a/.linux_items/include/airootfs/etc/skel/.config/openbox/rc.xml +++ b/.linux_items/include/airootfs/etc/skel/.config/openbox/rc.xml @@ -324,7 +324,7 @@ - urxvt -title "Hardware Diagnostics" -e hw-diags quick + urxvt -title "Hardware Diagnostics" -e hw-diags --quick diff --git a/.linux_items/include/airootfs/etc/skel/.update_x b/.linux_items/include/airootfs/etc/skel/.update_x index 4763a175..650d3162 100755 --- a/.linux_items/include/airootfs/etc/skel/.update_x +++ b/.linux_items/include/airootfs/etc/skel/.update_x @@ -92,3 +92,6 @@ else cbatticon --hide-notification & fi +# Prevent Xorg from being killed by .zlogin +touch "/tmp/x_ok" + diff --git a/.linux_items/include/airootfs/etc/skel/.zlogin b/.linux_items/include/airootfs/etc/skel/.zlogin index ecdc1bbf..775ac320 100644 --- a/.linux_items/include/airootfs/etc/skel/.zlogin +++ b/.linux_items/include/airootfs/etc/skel/.zlogin @@ -17,8 +17,19 @@ if [ "$(fgconsole 2>/dev/null)" -eq "1" ]; then # Start X or HW-diags if ! fgrep -q "nox" /proc/cmdline; then + # Kill Xorg after 30 seconds if it doesn't fully initialize + (sleep 30s; if ! [[ -f "/tmp/x_ok" ]]; then pkill '(Xorg|startx)'; fi) & + + # Try starting X startx >/dev/null + + # Run Hw-Diags CLI if necessary + if ! [[ -f "/tmp/x_ok" ]]; then + echo "There was an issue starting Xorg, starting CLI interface..." + sleep 2s + hw-diags --cli + fi else - hw-diags cli + hw-diags --cli fi fi diff --git a/.linux_items/include/syslinux/wk_sys_linux.cfg b/.linux_items/include/syslinux/wk_sys_linux.cfg index b65047ad..9c229249 100644 --- a/.linux_items/include/syslinux/wk_sys_linux.cfg +++ b/.linux_items/include/syslinux/wk_sys_linux.cfg @@ -12,6 +12,19 @@ LABEL wk_sys_linux_extras TEXT HELP Show extra Linux options ENDTEXT -MENU LABEL Linux (Extras) -KERNEL vesamenu.c32 -APPEND boot/syslinux/wk_sys_linux_extras.cfg +MENU LABEL Linux (i3) +LINUX boot/x86_64/vmlinuz +INITRD boot/intel_ucode.img,boot/amd_ucode.img,boot/x86_64/archiso.img +APPEND archisobasedir=%INSTALL_DIR% archisolabel=%ARCHISO_LABEL% copytoram loglevel=3 i3 +SYSAPPEND 3 + +LABEL wk_linux_cli +TEXT HELP +A live Linux environment (CLI) + * HW diagnostics, file-based backups, data recovery, etc +ENDTEXT +MENU LABEL Linux (CLI) +LINUX boot/x86_64/vmlinuz +INITRD boot/intel_ucode.img,boot/amd_ucode.img,boot/x86_64/archiso.img +APPEND archisobasedir=%INSTALL_DIR% archisolabel=%ARCHISO_LABEL% copytoram nox nomodeset +SYSAPPEND 3 diff --git a/.linux_items/include/syslinux/wk_sys_linux_extras.cfg b/.linux_items/include/syslinux/wk_sys_linux_extras.cfg deleted file mode 100644 index 979be787..00000000 --- a/.linux_items/include/syslinux/wk_sys_linux_extras.cfg +++ /dev/null @@ -1,198 +0,0 @@ -INCLUDE boot/syslinux/wk_head.cfg - -LABEL wk_linux -TEXT HELP -A live Linux environment - * HW diagnostics, file-based backups, data recovery, etc -ENDTEXT -MENU LABEL Linux -LINUX boot/x86_64/vmlinuz -INITRD boot/intel_ucode.img,boot/amd_ucode.img,boot/x86_64/archiso.img -APPEND archisobasedir=%INSTALL_DIR% archisolabel=%ARCHISO_LABEL% copytoram loglevel=3 - -LABEL wk_linux_i3 -TEXT HELP -A live Linux environment (i3) - * HW diagnostics, file-based backups, data recovery, etc -ENDTEXT -MENU LABEL Linux (i3) -LINUX boot/x86_64/vmlinuz -INITRD boot/intel_ucode.img,boot/amd_ucode.img,boot/x86_64/archiso.img -APPEND archisobasedir=%INSTALL_DIR% archisolabel=%ARCHISO_LABEL% copytoram loglevel=3 i3 -SYSAPPEND 3 - -LABEL wk_linux_cli -TEXT HELP -A live Linux environment (CLI) - * HW diagnostics, file-based backups, data recovery, etc -ENDTEXT -MENU LABEL Linux (CLI) -LINUX boot/x86_64/vmlinuz -INITRD boot/intel_ucode.img,boot/amd_ucode.img,boot/x86_64/archiso.img -APPEND archisobasedir=%INSTALL_DIR% archisolabel=%ARCHISO_LABEL% copytoram loglevel=4 nomodeset nox -SYSAPPEND 3 - -LABEL wk_linux_mac_generic -TEXT HELP -A live Linux environment - * HW diagnostics, file-based backups, data recovery, etc -ENDTEXT -MENU LABEL Linux (Mac) -LINUX boot/x86_64/vmlinuz -INITRD boot/intel_ucode.img,boot/amd_ucode.img,boot/x86_64/archiso.img -APPEND archisobasedir=%INSTALL_DIR% archisolabel=%ARCHISO_LABEL% copytoram loglevel=5 reboot=pci -SYSAPPEND 3 - -LABEL wk_linux_mac_cli -TEXT HELP -A live Linux environment - * HW diagnostics, file-based backups, data recovery, etc -ENDTEXT -MENU LABEL Linux (Mac CLI) -LINUX boot/x86_64/vmlinuz -INITRD boot/intel_ucode.img,boot/amd_ucode.img,boot/x86_64/archiso.img -APPEND archisobasedir=%INSTALL_DIR% archisolabel=%ARCHISO_LABEL% copytoram loglevel=5 nomodeset nox reboot=pci -SYSAPPEND 3 - -MENU SEPARATOR - -LABEL wk_linux_macbook52 -TEXT HELP -A live Linux environment - * WARNING System will be limited to one CPU/thread - * HW diagnostics, file-based backups, data recovery, etc -ENDTEXT -MENU LABEL Linux (MacBook5,2) -LINUX boot/x86_64/vmlinuz -INITRD boot/intel_ucode.img,boot/amd_ucode.img,boot/x86_64/archiso.img -APPEND archisobasedir=%INSTALL_DIR% archisolabel=%ARCHISO_LABEL% copytoram loglevel=5 acpi=off irqpoll maxcpus=1 noapic reboot=pci -SYSAPPEND 3 - -LABEL wk_linux_macbook91 -TEXT HELP -A live Linux environment - * HW diagnostics, file-based backups, data recovery, etc -ENDTEXT -MENU LABEL Linux (MacBook9,1) -LINUX boot/x86_64/vmlinuz -INITRD boot/intel_ucode.img,boot/amd_ucode.img,boot/x86_64/archiso.img -APPEND archisobasedir=%INSTALL_DIR% archisolabel=%ARCHISO_LABEL% copytoram loglevel=5 intremap=nosid noacpi nomodeset reboot=pci -SYSAPPEND 3 - -MENU SEPARATOR - -LABEL wk_linux_macbookair52 -TEXT HELP -A live Linux environment - * HW diagnostics, file-based backups, data recovery, etc -ENDTEXT -MENU LABEL Linux (MacBookAir5,2) -LINUX boot/x86_64/vmlinuz -INITRD boot/intel_ucode.img,boot/amd_ucode.img,boot/x86_64/archiso.img -APPEND archisobasedir=%INSTALL_DIR% archisolabel=%ARCHISO_LABEL% copytoram loglevel=5 intremap=off reboot=pci -SYSAPPEND 3 - -LABEL wk_linux_macbookair6_ -TEXT HELP -A live Linux environment - * WARNING Drive I/O performance will be impacted - * HW diagnostics, file-based backups, data recovery, etc -ENDTEXT -MENU LABEL Linux (MacBookAir6,x) -LINUX boot/x86_64/vmlinuz -INITRD boot/intel_ucode.img,boot/amd_ucode.img,boot/x86_64/archiso.img -APPEND archisobasedir=%INSTALL_DIR% archisolabel=%ARCHISO_LABEL% copytoram loglevel=5 libata.force=1:noncq reboot=pci -SYSAPPEND 3 - -MENU SEPARATOR - -LABEL wk_linux_macbookpro71 -TEXT HELP -A live Linux environment - * HW diagnostics, file-based backups, data recovery, etc -ENDTEXT -MENU LABEL Linux (MacBookPro7,1) -LINUX boot/x86_64/vmlinuz -INITRD boot/intel_ucode.img,boot/amd_ucode.img,boot/x86_64/archiso.img -APPEND archisobasedir=%INSTALL_DIR% archisolabel=%ARCHISO_LABEL% copytoram loglevel=5 acpi_osi=! acpi_osi="Darwin" intremap=off nomodeset reboot=pci -SYSAPPEND 3 - -LABEL wk_linux_macbookpro10_ -TEXT HELP -A live Linux environment - * HW diagnostics, file-based backups, data recovery, etc -ENDTEXT -MENU LABEL Linux (MacBookPro10,x) -LINUX boot/x86_64/vmlinuz -INITRD boot/intel_ucode.img,boot/amd_ucode.img,boot/x86_64/archiso.img -APPEND archisobasedir=%INSTALL_DIR% archisolabel=%ARCHISO_LABEL% copytoram loglevel=5 noapic reboot=pci -SYSAPPEND 3 - -LABEL wk_linux_macbookpro11_ -TEXT HELP -A live Linux environment - * HW diagnostics, file-based backups, data recovery, etc -ENDTEXT -MENU LABEL Linux (MacBookPro11,x) -LINUX boot/x86_64/vmlinuz -INITRD boot/intel_ucode.img,boot/amd_ucode.img,boot/x86_64/archiso.img -APPEND archisobasedir=%INSTALL_DIR% archisolabel=%ARCHISO_LABEL% copytoram loglevel=5 acpi_osi= reboot=pci -SYSAPPEND 3 - -MENU SEPARATOR - -LABEL wk_linux_mac_misc1 -TEXT HELP -A live Linux environment - * HW diagnostics, file-based backups, data recovery, etc -ENDTEXT -MENU LABEL Linux (Misc Mac Fix 1) -LINUX boot/x86_64/vmlinuz -INITRD boot/intel_ucode.img,boot/amd_ucode.img,boot/x86_64/archiso.img -APPEND archisobasedir=%INSTALL_DIR% archisolabel=%ARCHISO_LABEL% copytoram loglevel=5 acpi=force irqpoll noapic reboot=pci -SYSAPPEND 3 - -LABEL wk_linux_mac_misc2 -TEXT HELP -A live Linux environment - * HW diagnostics, file-based backups, data recovery, etc -ENDTEXT -MENU LABEL Linux (Misc Mac Fix 2) -LINUX boot/x86_64/vmlinuz -INITRD boot/intel_ucode.img,boot/amd_ucode.img,boot/x86_64/archiso.img -APPEND archisobasedir=%INSTALL_DIR% archisolabel=%ARCHISO_LABEL% copytoram loglevel=5 acpi=off reboot=pci -SYSAPPEND 3 - -LABEL wk_linux_mac_misc3 -TEXT HELP -A live Linux environment - * HW diagnostics, file-based backups, data recovery, etc -ENDTEXT -MENU LABEL Linux (Misc Mac Fix 3) -LINUX boot/x86_64/vmlinuz -INITRD boot/intel_ucode.img,boot/amd_ucode.img,boot/x86_64/archiso.img -APPEND archisobasedir=%INSTALL_DIR% archisolabel=%ARCHISO_LABEL% copytoram loglevel=5 acpi_osi=! acpi_osi="Darwin" reboot=pci -SYSAPPEND 3 - -MENU SEPARATOR - -LABEL wk_linux_debug -TEXT HELP -A live Linux environment - * HW diagnostics, file-based backups, data recovery, etc -ENDTEXT -MENU LABEL Linux (DEBUG) -LINUX boot/x86_64/vmlinuz -INITRD boot/intel_ucode.img,boot/amd_ucode.img,boot/x86_64/archiso.img -APPEND archisobasedir=%INSTALL_DIR% archisolabel=%ARCHISO_LABEL% copytoram loglevel=7 nomodeset nox -SYSAPPEND 3 - -MENU SEPARATOR - -LABEL wk_return -TEXT HELP -Show Return to the main menu -ENDTEXT -MENU LABEL Main Menu -KERNEL vesamenu.c32 -APPEND boot/syslinux/wk_sys.cfg diff --git a/Build Linux b/Build Linux index 56233d51..41e5dea5 100755 --- a/Build Linux +++ b/Build Linux @@ -170,13 +170,15 @@ function update_live_env() { # Memtest86 mkdir -p "$LIVE_DIR/EFI/memtest86/Benchmark" mkdir -p "$TEMP_DIR/memtest86" - curl -Lo "$TEMP_DIR/memtest86/memtest86.iso.tar.gz" "https://www.memtest86.com/downloads/memtest86-iso.tar.gz" - tar xvf "$TEMP_DIR/memtest86/memtest86.iso.tar.gz" -C "$TEMP_DIR/memtest86" - 7z x "$TEMP_DIR/memtest86"/*.iso -o"$TEMP_DIR/memtest86" - mv "$TEMP_DIR/memtest86/EFI/BOOT/BLACKLIS.CFG" "$LIVE_DIR/EFI/memtest86/blacklist.cfg" + curl -Lo "$TEMP_DIR/memtest86/memtest86-usb.zip" "https://www.memtest86.com/downloads/memtest86-usb.zip" + 7z e "$TEMP_DIR/memtest86/memtest86-usb.zip" -o"$TEMP_DIR/memtest86" "memtest86-usb.img" + 7z e "$TEMP_DIR/memtest86/memtest86-usb.img" -o"$TEMP_DIR/memtest86" "MemTest86.img" + 7z x "$TEMP_DIR/memtest86/MemTest86.img" -o"$TEMP_DIR/memtest86" + rm "$TEMP_DIR/memtest86/EFI/BOOT/BOOTIA32.EFI" mv "$TEMP_DIR/memtest86/EFI/BOOT/BOOTX64.EFI" "$LIVE_DIR/EFI/memtest86/memtestx64.efi" - mv "$TEMP_DIR/memtest86/EFI/BOOT/MT86.PNG" "$LIVE_DIR/EFI/memtest86/mt86.png" - mv "$TEMP_DIR/memtest86/EFI/BOOT/UNIFONT.BIN" "$LIVE_DIR/EFI/memtest86/unifont.bin" + for f in "$TEMP_DIR/memtest86/EFI/BOOT"/* "help"/* license.rtf; do + mv "$f" "$LIVE_DIR/EFI/memtest86"/ + done # build.sh if ! grep -iq 'wizardkit additions' "$LIVE_DIR/build.sh"; then