diff --git a/scripts/wk/hw/diags.py b/scripts/wk/hw/diags.py index 4a83a5bf..96e05951 100644 --- a/scripts/wk/hw/diags.py +++ b/scripts/wk/hw/diags.py @@ -8,7 +8,6 @@ import pathlib import platform import plistlib import re -import signal import subprocess import time @@ -270,7 +269,7 @@ def build_menu(cli_mode=False, quick_mode=False): # Update default selections for quick mode if necessary if quick_mode: - for name in menu.options.keys(): + for name in menu.options: # Only select quick option(s) menu.options[name]['Selected'] = name in MENU_OPTIONS_QUICK @@ -291,29 +290,79 @@ def build_menu(cli_mode=False, quick_mode=False): return menu +def check_mprime_results(test_obj, working_dir): + """Check mprime log files to determine if test passed.""" + passing_lines = {} + warning_lines = {} + + def _read_file(log_name): + """Read file and split into lines, returns list.""" + lines = [] + try: + with open(f'{working_dir}/{log_name}', 'r') as _f: + lines = _f.readlines() + except FileNotFoundError: + # File may be missing on older systems + lines = [] + + return lines + + # results.txt (check if failed) + for line in _read_file('results.txt'): + line = line.strip() + if re.search(r'(error|fail)', line, re.IGNORECASE): + warning_lines[line] = None + + # print.log (check if passed) + for line in _read_file('prime.log'): + line = line.strip() + match = re.search( + r'(completed.*(\d+) errors, (\d+) warnings)', line, re.IGNORECASE) + if match: + if int(match.group(2)) + int(match.group(3)) > 0: + # Errors and/or warnings encountered + warning_lines[match.group(1).capitalize()] = None + else: + # No errors/warnings + passing_lines[match.group(1).capitalize()] = None + + # Update status + if warning_lines: + test_obj.failed = True + test_obj.set_status('Failed') + elif passing_lines and 'Aborted' not in test_obj.status: + test_obj.passed = True + test_obj.set_status('Passed') + else: + test_obj.set_status('Unknown') + + # Update report + for line in passing_lines: + test_obj.report.append(f' {line}') + for line in warning_lines: + test_obj.report.append(std.color_string(f' {line}', 'YELLOW')) + if not (passing_lines or warning_lines): + test_obj.report.append(std.color_string(' Unknown result', 'YELLOW')) + + def cpu_mprime_test(state, test_objects): - # pylint: disable=too-many-statements - #TODO: Fix above? """CPU & cooling check using Prime95.""" LOG.info('CPU Test (Prime95)') - thermal_abort = False prime_log = pathlib.Path(f'{state.log_dir}/prime.log') - test = test_objects[0] + sensors_out = pathlib.Path(f'{state.log_dir}/sensors.out') + test_obj = test_objects[0] # Bail early - if test.disabled: + if test_obj.disabled: return # Prep - dev = test.dev - test.set_status('Working') - state.update_top_pane(dev.description) + state.update_top_pane(test_obj.dev.description) + test_obj.set_status('Working') # Start sensors monitor sensors = hw_sensors.Sensors() - sensors_out = pathlib.Path(f'{state.log_dir}/sensors.out') - sensors_thread = exe.start_thread( - sensors.monitor_to_file, args=(sensors_out,)) + sensors.start_background_monitor(sensors_out) # Create monitor and worker panes state.panes['Prime95'] = tmux.split_window( @@ -333,44 +382,27 @@ def cpu_mprime_test(state, test_objects): std.print_info('Starting stress test') std.print_warning('If running too hot, press CTRL+c to abort the test') set_apple_fan_speed('max') - proc_mprime = subprocess.Popen( - ['mprime', '-t'], - bufsize=1, - cwd=state.log_dir, - stdout=subprocess.PIPE, - ) - proc_grep = subprocess.Popen( - 'grep --ignore-case --invert-match --line-buffered stress.txt'.split(), - bufsize=1, - stdin=proc_mprime.stdout, - stdout=subprocess.PIPE, - ) - proc_mprime.stdout.close() - save_nsbr = exe.NonBlockingStreamReader(proc_grep.stdout) - save_thread = exe.start_thread( - save_nsbr.save_to_file, - args=(proc_grep, prime_log), - ) + proc_mprime = start_mprime_thread(state.log_dir, prime_log) # Show countdown try: - print_countdown(seconds=cfg.hw.CPU_TEST_MINUTES*60) + #print_countdown(seconds=cfg.hw.CPU_TEST_MINUTES*60) + print_countdown(seconds=7) except KeyboardInterrupt: - test.set_status('Aborted') + test_obj.set_status('Aborted') except hw_sensors.ThermalLimitReachedError: - test.set_status('Failed') - test.failed = True - thermal_abort = True + test_obj.failed = True + test_obj.set_status('Failed') # Stop Prime95 - proc_mprime.send_signal(signal.SIGINT) - std.sleep(1) - proc_mprime.kill() - save_thread.join() - tmux.kill_pane(state.panes.pop('Prime95', None)) + proc_mprime.terminate() + try: + proc_mprime.wait(timeout=5) + except subprocess.TimeoutExpired: + proc_mprime.kill() + set_apple_fan_speed('auto') # Get cooldown temp - set_apple_fan_speed('auto') std.clear_screen() std.print_standard('Letting CPU cooldown...') std.sleep(5) @@ -378,18 +410,21 @@ def cpu_mprime_test(state, test_objects): sensors.save_average_temps(temp_label='Cooldown', seconds=5) # Check results and build report - std.print_report(sensors.generate_report('Current', 'Idle', 'Max','Cooldown')) - - # Stop sensors monitor - sensors_out.with_suffix('.stop').touch() - sensors_thread.join() + test_obj.report.append(std.color_string('Prime95', 'BLUE')) + check_mprime_results(test_obj=test_obj, working_dir=state.log_dir) + test_obj.report.append(std.color_string('Temps', 'BLUE')) + for line in sensors.generate_report( + 'Idle', 'Max', 'Cooldown', only_cpu=True): + test_obj.report.append(f' {line}') # Cleanup + sensors.stop_background_monitor() state.panes.pop('Current', None) + tmux.kill_pane(state.panes.pop('Prime95', None)) tmux.kill_pane(state.panes.pop('Temps', None)) #TODO: p95 - std.pause() + std.print_report(test_obj.report) def disk_attribute_check(state, test_objects): @@ -706,5 +741,31 @@ def set_apple_fan_speed(speed): exe.run_program(cmd, check=False) +def start_mprime_thread(working_dir, log_path): + """Start mprime and save filtered output to log, returns Popen object.""" + proc_mprime = subprocess.Popen( + ['mprime', '-t'], + bufsize=1, + cwd=working_dir, + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, + ) + proc_grep = subprocess.Popen( + 'grep --ignore-case --invert-match --line-buffered stress.txt'.split(), + bufsize=1, + stdin=proc_mprime.stdout, + stdout=subprocess.PIPE, + ) + proc_mprime.stdout.close() + save_nsbr = exe.NonBlockingStreamReader(proc_grep.stdout) + exe.start_thread( + save_nsbr.save_to_file, + args=(proc_grep, log_path), + ) + + # Return objects + return proc_mprime + + if __name__ == '__main__': print("This file is not meant to be called directly.") diff --git a/scripts/wk/hw/sensors.py b/scripts/wk/hw/sensors.py index fce71d32..9956bf1a 100644 --- a/scripts/wk/hw/sensors.py +++ b/scripts/wk/hw/sensors.py @@ -10,7 +10,7 @@ import re from subprocess import CalledProcessError from wk.cfg.hw import CPU_THERMAL_LIMIT, SMC_IDS, TEMP_COLORS -from wk.exe import run_program +from wk.exe import run_program, start_thread from wk.std import color_string, sleep @@ -23,6 +23,7 @@ SMC_REGEX = re.compile( r'\s+(?P.*?)' r'\s*\(bytes (?P.*)\)$' ) +SENSOR_SOURCE_WIDTH = 25 if platform.system() == 'Darwin' else 20 # Error Classes @@ -34,7 +35,9 @@ class ThermalLimitReachedError(RuntimeError): class Sensors(): """Class for holding sensor specific data.""" def __init__(self): + self.background_thread = None self.data = get_sensor_data() + self.out_path = None def clear_temps(self): """Clear saved temps but keep structure""" @@ -55,7 +58,7 @@ class Sensors(): for adapter, sources in sorted(adapters.items()): report.append(fix_sensor_name(adapter)) for source, source_data in sorted(sources.items()): - line = f'{fix_sensor_name(source):25} ' + line = f'{fix_sensor_name(source):{SENSOR_SOURCE_WIDTH}} ' for label in temp_labels: if label != 'Current': line += f' {label.lower()}: ' @@ -78,12 +81,16 @@ class Sensors(): # Done return report - def monitor_to_file(self, out_path): + def monitor_to_file(self, out_path, temp_labels=None): """Write report to path every second until stopped.""" stop_path = pathlib.Path(out_path).resolve().with_suffix('.stop') + if not temp_labels: + temp_labels = ('Current', 'Max') + + # Start loop while True: self.update_sensor_data() - report = self.generate_report('Current', 'Max') + report = self.generate_report(*temp_labels) with open(out_path, 'w') as _f: _f.write('\n'.join(report)) @@ -111,6 +118,26 @@ class Sensors(): temps = source_data['Temps'] source_data[temp_label] = sum(temps) / len(temps) + def start_background_monitor(self, out_path, temp_labels=None): + """Start background thread to save report to file.""" + if self.background_thread: + raise RuntimeError('Background thread already running') + + self.out_path = pathlib.Path(out_path) + self.background_thread = start_thread( + self.monitor_to_file, + args=(out_path, temp_labels), + ) + + def stop_background_monitor(self): + """Stop background thread.""" + self.out_path.with_suffix('.stop').touch() + self.background_thread.join() + + # Reset vars to None + self.background_thread = None + self.out_path = None + def update_sensor_data(self, exit_on_thermal_limit=True): """Update sensor data via OS-specific means.""" if platform.system() == 'Darwin':