From 46a6dda0ff071de96c432713cde9e6d01312deaa Mon Sep 17 00:00:00 2001 From: 2Shirt <2xShirt@gmail.com> Date: Wed, 13 Nov 2019 17:47:52 -0700 Subject: [PATCH] Prime95 workflow mostly done --- scripts/wk/cfg/hw.py | 3 +- scripts/wk/hw/diags.py | 131 +++++++++++++++++++++++++++++++++++++-- scripts/wk/hw/sensors.py | 14 ++++- scripts/wk/tmux.py | 22 ++++--- 4 files changed, 150 insertions(+), 20 deletions(-) diff --git a/scripts/wk/cfg/hw.py b/scripts/wk/cfg/hw.py index 791ff48b..fa303cba 100644 --- a/scripts/wk/cfg/hw.py +++ b/scripts/wk/cfg/hw.py @@ -15,6 +15,7 @@ ATTRIBUTE_COLORS = ( ('Maximum', 'PURPLE'), ) CPU_FAILURE_TEMP = 90 +CPU_TEST_MINUTES = 7 CPU_THERMAL_LIMIT = 99 KEY_NVME = 'nvme_smart_health_information_log' KEY_SMART = 'ata_smart_attributes' @@ -118,8 +119,8 @@ TMUX_LAYOUT = OrderedDict({ 'Started': {'width': TMUX_SIDE_WIDTH, 'Check': True}, 'Progress': {'width': TMUX_SIDE_WIDTH, 'Check': True}, # Testing panes - 'Prime95': {'height': 11, 'Check': False}, 'Temps': {'height': 1000, 'Check': False}, + 'Prime95': {'height': 11, 'Check': False}, 'SMART': {'height': 3, 'Check': True}, 'badblocks': {'height': 5, 'Check': True}, 'I/O Benchmark': {'height': 1000, 'Check': False}, diff --git a/scripts/wk/hw/diags.py b/scripts/wk/hw/diags.py index 82d0c689..068a6242 100644 --- a/scripts/wk/hw/diags.py +++ b/scripts/wk/hw/diags.py @@ -16,6 +16,7 @@ from docopt import docopt from wk import cfg, exe, log, net, std, tmux from wk.hw import obj as hw_obj +from wk.hw import sensors as hw_sensors # atexit functions @@ -78,6 +79,7 @@ class State(): def __init__(self): self.cpu = None self.disks = [] + self.layout = cfg.hw.TMUX_LAYOUT.copy() self.log_dir = None self.panes = {} self.tests = OrderedDict({ @@ -111,11 +113,13 @@ class State(): # Init tmux and start a background process to maintain layout self.init_tmux() - if hasattr(signal, 'SIGWINCH'): - # Use signal handling - signal.signal(signal.SIGWINCH, self.fix_tmux_layout) - else: - exe.start_thread(self.fix_tmux_layout_loop) + #TODO: Fix SIGWINCH? + #if hasattr(signal, 'SIGWINCH'): + # # Use signal handling + # signal.signal(signal.SIGWINCH, self.fix_tmux_layout) + #else: + # exe.start_thread(self.fix_tmux_layout_loop) + exe.start_thread(self.fix_tmux_layout_loop) def fix_tmux_layout(self, forced=True, signum=None, frame=None): # pylint: disable=unused-argument @@ -125,7 +129,7 @@ class State(): signum and frame must be valid aguments. """ try: - tmux.fix_layout(self.panes, cfg.hw.TMUX_LAYOUT, forced=forced) + tmux.fix_layout(self.panes, self.layout, forced=forced) except RuntimeError: # Assuming self.panes changed while running pass @@ -143,6 +147,8 @@ class State(): """Initialize diagnostic pass.""" # Reset objects self.disks.clear() + self.layout.clear() + self.layout.update(cfg.hw.TMUX_LAYOUT) for test_data in self.tests.values(): test_data['Objects'].clear() @@ -287,6 +293,81 @@ def build_menu(cli_mode=False, quick_mode=False): def cpu_mprime_test(state, test_objects): """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] + + # Bail early + if test.disabled: + return + + # Prep + dev = test.dev + test.set_status('Working') + state.update_top_pane(dev.description) + + # 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,)) + + # Create monitor and worker panes + state.panes['Prime95'] = tmux.split_window( + lines=10, vertical=True, watch_file=prime_log) + state.panes['Temps'] = tmux.split_window( + behind=True, percent=80, vertical=True, watch_file=sensors_out) + tmux.resize_pane(height=3) + state.panes['Current'] = '' + state.layout['Current'] = {'height': 3, 'Check': True} + + # Get idle temps + std.clear_screen() + std.print_standard('Saving idle temps...') + sensors.save_average_temps(temp_label='Idle', seconds=5) + + # Stress CPU + 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') + #RUN: mprime -t | grep -iv --line-buffered 'stress.txt' | tee -a "prime.log" + try: + print_countdown(seconds=cfg.hw.CPU_TEST_MINUTES*60) + except KeyboardInterrupt: + test.set_status('Aborted') + except hw_sensors.ThermalLimitReachedError: + test.set_status('Failed') + test.failed = True + thermal_abort = True + + # Stop Prime95 + #TODO kill p95 + tmux.kill_pane(state.panes.pop('Prime95', None)) + + # Get cooldown temp + set_apple_fan_speed('auto') + std.clear_screen() + std.print_standard('Letting CPU cooldown...') + std.sleep(5) + std.print_standard('Saving cooldown temps...') + sensors.save_average_temps(temp_label='Cooldown', seconds=5) + + # Check results and build report + #TODO + + # Stop sensors monitor + sensors_out.with_suffix('.stop').touch() + sensors_thread.join() + + # Cleanup + state.panes.pop('Current', None) + tmux.kill_pane(state.panes.pop('Temps', None)) + + + + + + #TODO: p95 std.print_warning('TODO: p95') std.pause() @@ -503,6 +584,26 @@ def network_test(): std.pause('Press Enter to return to main menu...') +def print_countdown(seconds): + """Print countdown to screen.""" + time_limit = seconds + for i in range(seconds): + sec_left = (seconds - i) % 60 + min_left = int((seconds - i) / 60) + + out_str = '\r' + if min_left: + out_str += f'{min_left} minute{"s" if min_left != 1 else ""}, ' + out_str += f'{sec_left} second{"s" if sec_left != 1 else ""}' + out_str += ' remaining' + + print(f'{out_str:<40}', end='', flush=True) + std.sleep(1) + + # Done + print('') + + def run_diags(state, menu, quick_mode=False): """Run selected diagnostics.""" aborted = False @@ -569,5 +670,23 @@ def screensaver(name): tmux.zoom_pane() +def set_apple_fan_speed(speed): + """Set Apple fan speed.""" + cmd = None + + # Check + if speed not in ('auto', 'max'): + raise RuntimeError(f'Invalid speed {speed}') + + # Set cmd + if platform.system() == 'Linux': + cmd = ['apple-fans', speed] + #TODO: Add method for use under macOS + + # Run cmd + if cmd: + exe.run_program(cmd, check=False) + + 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 7307818b..fce71d32 100644 --- a/scripts/wk/hw/sensors.py +++ b/scripts/wk/hw/sensors.py @@ -3,6 +3,7 @@ import json import logging +import pathlib import platform import re @@ -77,14 +78,21 @@ class Sensors(): # Done return report - def monitor_to_file(self, path): + def monitor_to_file(self, out_path): """Write report to path every second until stopped.""" + stop_path = pathlib.Path(out_path).resolve().with_suffix('.stop') while True: self.update_sensor_data() report = self.generate_report('Current', 'Max') - with open(path, 'w') as _f: + with open(out_path, 'w') as _f: _f.write('\n'.join(report)) - sleep(1) + + # Check if we should stop + if stop_path.exists(): + break + + # Sleep before next loop + sleep(0.5) def save_average_temps(self, temp_label, seconds=10): # pylint: disable=unused-variable diff --git a/scripts/wk/tmux.py b/scripts/wk/tmux.py index 10b3b5a1..8639db6f 100644 --- a/scripts/wk/tmux.py +++ b/scripts/wk/tmux.py @@ -41,6 +41,8 @@ def fix_layout(panes, layout, forced=False): if isinstance(pane_list, str): pane_list = [pane_list] for pane_id in pane_list: + if name == 'Current': + pane_id = None try: resize_pane(pane_id, **data) except RuntimeError: @@ -98,13 +100,16 @@ def layout_needs_fixed(panes, layout): if name not in panes: continue - # Check pane size - pane_id = panes[name] - width, height = get_pane_size(pane_id) - if data.get('width', False) and data['width'] != width: - needs_fixed = True - if data.get('height', False) and data['height'] != height: - needs_fixed = True + # Check pane size(s) + pane_list = panes[name] + if isinstance(pane_list, str): + pane_list = [pane_list] + for pane_id in pane_list: + width, height = get_pane_size(pane_id) + if data.get('width', False) and data['width'] != width: + needs_fixed = True + if data.get('height', False) and data['height'] != height: + needs_fixed = True # Done return needs_fixed @@ -193,9 +198,6 @@ def resize_pane(pane_id=None, width=None, height=None, **kwargs): cmd = ['tmux', 'resize-pane'] # Safety checks - if not poll_pane(pane_id): - LOG.debug('tmux pane %s not found', pane_id) - raise RuntimeError(f'tmux pane {pane_id} not found') if not (width or height): LOG.error('Neither width nor height specified') raise RuntimeError('Neither width nor height specified')