Merge remote-tracking branch 'upstream/dev' into dev

This commit is contained in:
2Shirt 2021-06-23 23:36:03 -06:00
commit 29dc4694b4
Signed by: 2Shirt
GPG key ID: 152FAC923B0E132C
8 changed files with 161 additions and 32 deletions

View file

@ -32,8 +32,12 @@ class NonBlockingStreamReader():
def populate_queue(stream, queue): def populate_queue(stream, queue):
"""Collect lines from stream and put them in queue.""" """Collect lines from stream and put them in queue."""
while True: while not stream.closed:
try:
line = stream.read(1) line = stream.read(1)
except ValueError:
# Assuming the stream was closed
line = None
if line: if line:
queue.put(line) queue.put(line)
@ -42,6 +46,10 @@ class NonBlockingStreamReader():
args=(self.stream, self.queue), args=(self.stream, self.queue),
) )
def stop(self):
"""Stop reading from input stream."""
self.stream.close()
def read(self, timeout=None): def read(self, timeout=None):
"""Read from queue if possible, returns item from queue.""" """Read from queue if possible, returns item from queue."""
try: try:
@ -51,6 +59,7 @@ class NonBlockingStreamReader():
def save_to_file(self, proc, out_path): def save_to_file(self, proc, out_path):
"""Continuously save output to file while proc is running.""" """Continuously save output to file while proc is running."""
LOG.debug('Saving process %s output to %s', proc, out_path)
while proc.poll() is None: while proc.poll() is None:
out = b'' out = b''
out_bytes = b'' out_bytes = b''
@ -61,6 +70,9 @@ class NonBlockingStreamReader():
with open(out_path, 'a') as _f: with open(out_path, 'a') as _f:
_f.write(out_bytes.decode('utf-8', errors='ignore')) _f.write(out_bytes.decode('utf-8', errors='ignore'))
# Close stream to prevent 100% CPU usage
self.stream.close()
# Functions # Functions
def build_cmd_kwargs(cmd, minimized=False, pipe=True, shell=False, **kwargs): def build_cmd_kwargs(cmd, minimized=False, pipe=True, shell=False, **kwargs):
@ -70,10 +82,9 @@ def build_cmd_kwargs(cmd, minimized=False, pipe=True, shell=False, **kwargs):
NOTE: If no encoding specified then UTF-8 will be used. NOTE: If no encoding specified then UTF-8 will be used.
""" """
LOG.debug( LOG.debug(
'cmd: %s, minimized: %s, pipe: %s, shell: %s', 'cmd: %s, minimized: %s, pipe: %s, shell: %s, kwargs: %s',
cmd, minimized, pipe, shell, cmd, minimized, pipe, shell, kwargs,
) )
LOG.debug('kwargs: %s', kwargs)
cmd_kwargs = { cmd_kwargs = {
'args': cmd, 'args': cmd,
'shell': shell, 'shell': shell,
@ -118,6 +129,7 @@ def get_json_from_command(cmd, check=True, encoding='utf-8', errors='ignore'):
If the data can't be decoded then either an exception is raised If the data can't be decoded then either an exception is raised
or an empty dict is returned depending on errors. or an empty dict is returned depending on errors.
""" """
LOG.debug('Loading JSON data from cmd: %s', cmd)
json_data = {} json_data = {}
try: try:
@ -187,9 +199,15 @@ def popen_program(cmd, minimized=False, pipe=False, shell=False, **kwargs):
pipe=pipe, pipe=pipe,
shell=shell, shell=shell,
**kwargs) **kwargs)
try:
proc = subprocess.Popen(**cmd_kwargs)
except FileNotFoundError:
LOG.error('Command not found: %s', cmd)
raise
LOG.debug('proc: %s', proc)
# Ready to run program # Done
return subprocess.Popen(**cmd_kwargs) return proc
def run_program(cmd, check=True, pipe=True, shell=False, **kwargs): def run_program(cmd, check=True, pipe=True, shell=False, **kwargs):
@ -206,7 +224,11 @@ def run_program(cmd, check=True, pipe=True, shell=False, **kwargs):
pipe=pipe, pipe=pipe,
shell=shell, shell=shell,
**kwargs) **kwargs)
try:
proc = subprocess.run(**cmd_kwargs) proc = subprocess.run(**cmd_kwargs)
except FileNotFoundError:
LOG.error('Command not found: %s', cmd)
raise
LOG.debug('proc: %s', proc) LOG.debug('proc: %s', proc)
# Done # Done
@ -215,6 +237,10 @@ def run_program(cmd, check=True, pipe=True, shell=False, **kwargs):
def start_thread(function, args=None, daemon=True): def start_thread(function, args=None, daemon=True):
"""Run function as thread in background, returns Thread object.""" """Run function as thread in background, returns Thread object."""
LOG.debug(
'Starting background thread for function: %s, args: %s, daemon: %s',
function, args, daemon,
)
args = args if args else [] args = args if args else []
thread = Thread(target=function, args=args, daemon=daemon) thread = Thread(target=function, args=args, daemon=daemon)
thread.start() thread.start()

View file

@ -132,7 +132,7 @@ class State():
self.tests = OrderedDict({ self.tests = OrderedDict({
'CPU & Cooling': { 'CPU & Cooling': {
'Enabled': False, 'Enabled': False,
'Function': cpu_mprime_test, 'Function': cpu_stress_tests,
'Objects': [], 'Objects': [],
}, },
'Disk Attributes': { 'Disk Attributes': {
@ -331,7 +331,7 @@ class State():
if not details['Selected']: if not details['Selected']:
continue continue
if 'CPU' in name: if 'CPU' in name:
# Create two Test objects which will both be used by cpu_mprime_test # Create two Test objects which will both be used by cpu_stress_tests
# NOTE: Prime95 should be added first # NOTE: Prime95 should be added first
test_mprime_obj = hw_obj.Test(dev=self.cpu, label='Prime95') test_mprime_obj = hw_obj.Test(dev=self.cpu, label='Prime95')
test_cooling_obj = hw_obj.Test(dev=self.cpu, label='Cooling') test_cooling_obj = hw_obj.Test(dev=self.cpu, label='Cooling')
@ -597,9 +597,12 @@ def calc_io_dd_values(dev_size):
} }
def check_cooling_results(test_obj, sensors): def check_cooling_results(test_obj, sensors, run_sysbench=False):
"""Check cooling results and update test_obj.""" """Check cooling results and update test_obj."""
max_temp = sensors.cpu_max_temp() max_temp = sensors.cpu_max_temp()
temp_labels = ['Idle', 'Max', 'Cooldown']
if run_sysbench:
temp_labels.append('Sysbench')
# Check temps # Check temps
if not max_temp: if not max_temp:
@ -612,8 +615,7 @@ def check_cooling_results(test_obj, sensors):
test_obj.set_status('Passed') test_obj.set_status('Passed')
# Add temps to report # Add temps to report
for line in sensors.generate_report( for line in sensors.generate_report(*temp_labels, only_cpu=True):
'Idle', 'Max', 'Cooldown', only_cpu=True):
test_obj.report.append(f' {line}') test_obj.report.append(f' {line}')
@ -767,12 +769,13 @@ def check_self_test_results(test_obj, aborted=False):
test_obj.set_status('Failed') test_obj.set_status('Failed')
def cpu_mprime_test(state, test_objects): def cpu_stress_tests(state, test_objects):
# pylint: disable=too-many-statements # pylint: disable=too-many-statements
"""CPU & cooling check using Prime95.""" """CPU & cooling check using Prime95 and Sysbench."""
LOG.info('CPU Test (Prime95)') LOG.info('CPU Test (Prime95)')
aborted = False aborted = False
prime_log = pathlib.Path(f'{state.log_dir}/prime.log') prime_log = pathlib.Path(f'{state.log_dir}/prime.log')
run_sysbench = False
sensors_out = pathlib.Path(f'{state.log_dir}/sensors.out') sensors_out = pathlib.Path(f'{state.log_dir}/sensors.out')
test_mprime_obj, test_cooling_obj = test_objects test_mprime_obj, test_cooling_obj = test_objects
@ -795,7 +798,7 @@ def cpu_mprime_test(state, test_objects):
# Create monitor and worker panes # Create monitor and worker panes
state.update_progress_pane() state.update_progress_pane()
state.panes['Prime95'] = tmux.split_window( state.panes['Prime95'] = tmux.split_window(
lines=10, vertical=True, watch_file=prime_log) lines=10, vertical=True, watch_file=prime_log, watch_cmd='tail')
if PLATFORM == 'Darwin': if PLATFORM == 'Darwin':
state.panes['Temps'] = tmux.split_window( state.panes['Temps'] = tmux.split_window(
behind=True, percent=80, vertical=True, cmd='./hw-sensors') behind=True, percent=80, vertical=True, cmd='./hw-sensors')
@ -811,7 +814,7 @@ def cpu_mprime_test(state, test_objects):
sensors.save_average_temps(temp_label='Idle', seconds=5) sensors.save_average_temps(temp_label='Idle', seconds=5)
# Stress CPU # Stress CPU
std.print_info('Starting stress test') std.print_info('Running stress test')
set_apple_fan_speed('max') set_apple_fan_speed('max')
proc_mprime = start_mprime(state.log_dir, prime_log) proc_mprime = start_mprime(state.log_dir, prime_log)
@ -842,9 +845,43 @@ def cpu_mprime_test(state, test_objects):
test_mprime_obj.report.append(std.color_string('Prime95', 'BLUE')) test_mprime_obj.report.append(std.color_string('Prime95', 'BLUE'))
check_mprime_results(test_obj=test_mprime_obj, working_dir=state.log_dir) check_mprime_results(test_obj=test_mprime_obj, working_dir=state.log_dir)
# Run Sysbench test if necessary
run_sysbench = (
not aborted and sensors.cpu_max_temp() >= cfg.hw.CPU_FAILURE_TEMP
)
if run_sysbench:
LOG.info('CPU Test (Sysbench)')
std.print_standard('Letting CPU cooldown more...')
std.sleep(30)
std.clear_screen()
std.print_info('Running alternate stress test')
print('')
proc_sysbench, filehandle_sysbench = start_sysbench(
sensors,
sensors_out,
log_path=prime_log.with_name('sysbench.log'),
pane=state.panes['Prime95'],
)
try:
print_countdown(proc=proc_sysbench, seconds=cfg.hw.CPU_TEST_MINUTES*60)
except AttributeError:
# Assuming the sysbench process wasn't found and proc was set to None
LOG.error('Failed to find sysbench process', exc_info=True)
except KeyboardInterrupt:
aborted = True
stop_sysbench(proc_sysbench, filehandle_sysbench)
# Update progress
# NOTE: CPU critical temp check isn't really necessary
# Hard to imagine it wasn't hit during Prime95 but was in sysbench
if sensors.cpu_reached_critical_temp() or aborted:
test_cooling_obj.set_status('Aborted')
test_mprime_obj.set_status('Aborted')
state.update_progress_pane()
# Check Cooling results # Check Cooling results
test_cooling_obj.report.append(std.color_string('Temps', 'BLUE')) test_cooling_obj.report.append(std.color_string('Temps', 'BLUE'))
check_cooling_results(test_obj=test_cooling_obj, sensors=sensors) check_cooling_results(test_cooling_obj, sensors, run_sysbench)
# Post results to osTicket # Post results to osTicket
if not state.ost.disabled: if not state.ost.disabled:
@ -1608,7 +1645,8 @@ def print_countdown(proc, seconds):
except subprocess.TimeoutExpired: except subprocess.TimeoutExpired:
# proc still going, continue # proc still going, continue
pass pass
if proc.poll() is not None: if ((hasattr(proc, 'poll') and proc.poll() is not None)
or (hasattr(proc, 'is_running') and not proc.is_running())):
# proc exited, stop countdown # proc exited, stop countdown
break break
@ -1800,6 +1838,35 @@ def start_mprime(working_dir, log_path):
return proc_mprime return proc_mprime
def start_sysbench(sensors, sensors_out, log_path, pane):
"""Start sysbench, returns tuple with Popen object and file handle."""
set_apple_fan_speed('max')
sysbench_cmd = [
'sysbench',
f'--threads={exe.psutil.cpu_count()}',
'--cpu-max-prime=1000000000',
'cpu',
'run',
]
# Restart background monitor for Sysbench
sensors.stop_background_monitor()
sensors.start_background_monitor(
sensors_out,
alt_max='Sysbench',
thermal_action=('killall', 'sysbench', '-INT'),
)
# Update bottom pane
tmux.respawn_pane(pane, watch_file=log_path, watch_cmd='tail')
# Start sysbench
filehandle_sysbench = open(log_path, 'a')
proc_sysbench = exe.popen_program(sysbench_cmd, stdout=filehandle_sysbench)
# Done
return (proc_sysbench, filehandle_sysbench)
def stop_mprime(proc_mprime): def stop_mprime(proc_mprime):
"""Stop mprime gracefully, then forcefully as needed.""" """Stop mprime gracefully, then forcefully as needed."""
proc_mprime.terminate() proc_mprime.terminate()
@ -1810,6 +1877,18 @@ def stop_mprime(proc_mprime):
set_apple_fan_speed('auto') set_apple_fan_speed('auto')
def stop_sysbench(proc_sysbench, filehandle_sysbench):
"""Stop sysbench."""
proc_sysbench.terminate()
try:
proc_sysbench.wait(timeout=5)
except subprocess.TimeoutExpired:
proc_sysbench.kill()
filehandle_sysbench.flush()
filehandle_sysbench.close()
set_apple_fan_speed('auto')
def sync_clock(): def sync_clock():
"""Sync clock under macOS using sntp.""" """Sync clock under macOS using sntp."""
cmd = ['sudo', 'sntp', '-Ss', 'us.pool.ntp.org'] cmd = ['sudo', 'sntp', '-Ss', 'us.pool.ntp.org']

View file

@ -10,6 +10,7 @@ from subprocess import CalledProcessError
from wk.cfg.hw import CPU_CRITICAL_TEMP, SMC_IDS, TEMP_COLORS from wk.cfg.hw import CPU_CRITICAL_TEMP, SMC_IDS, TEMP_COLORS
from wk.exe import run_program, start_thread from wk.exe import run_program, start_thread
from wk.io import non_clobber_path
from wk.std import PLATFORM, color_string, sleep from wk.std import PLATFORM, color_string, sleep
@ -115,20 +116,27 @@ class Sensors():
return report return report
def monitor_to_file( def monitor_to_file(
self, out_path, self, out_path, alt_max=None,
exit_on_thermal_limit=True, temp_labels=None, thermal_action=None): exit_on_thermal_limit=True, temp_labels=None, thermal_action=None):
# pylint: disable=too-many-arguments
"""Write report to path every second until stopped. """Write report to path every second until stopped.
thermal_action is a cmd to run if ThermalLimitReachedError is caught. thermal_action is a cmd to run if ThermalLimitReachedError is caught.
""" """
stop_path = pathlib.Path(out_path).resolve().with_suffix('.stop') stop_path = pathlib.Path(out_path).resolve().with_suffix('.stop')
if stop_path.exists():
# Rename existing file to allow thread to start as expected
# Yes this is excessive but safe
stop_path.rename(non_clobber_path(stop_path))
if not temp_labels: if not temp_labels:
temp_labels = ('Current', 'Max') temp_labels = ['Current', 'Max']
if alt_max:
temp_labels.append(alt_max)
# Start loop # Start loop
while True: while True:
try: try:
self.update_sensor_data(exit_on_thermal_limit) self.update_sensor_data(alt_max, exit_on_thermal_limit)
except ThermalLimitReachedError: except ThermalLimitReachedError:
if thermal_action: if thermal_action:
run_program(thermal_action, check=False) run_program(thermal_action, check=False)
@ -169,8 +177,9 @@ class Sensors():
source_data[temp_label] = 0 source_data[temp_label] = 0
def start_background_monitor( def start_background_monitor(
self, out_path, self, out_path, alt_max=None,
exit_on_thermal_limit=True, temp_labels=None, thermal_action=None): exit_on_thermal_limit=True, temp_labels=None, thermal_action=None):
# pylint: disable=too-many-arguments
"""Start background thread to save report to file. """Start background thread to save report to file.
thermal_action is a cmd to run if ThermalLimitReachedError is caught. thermal_action is a cmd to run if ThermalLimitReachedError is caught.
@ -181,7 +190,9 @@ class Sensors():
self.out_path = pathlib.Path(out_path) self.out_path = pathlib.Path(out_path)
self.background_thread = start_thread( self.background_thread = start_thread(
self.monitor_to_file, self.monitor_to_file,
args=(out_path, exit_on_thermal_limit, temp_labels, thermal_action), args=(
out_path, alt_max, exit_on_thermal_limit, temp_labels, thermal_action,
),
) )
def stop_background_monitor(self): def stop_background_monitor(self):
@ -193,14 +204,14 @@ class Sensors():
self.background_thread = None self.background_thread = None
self.out_path = None self.out_path = None
def update_sensor_data(self, exit_on_thermal_limit=True): def update_sensor_data(self, alt_max=None, exit_on_thermal_limit=True):
"""Update sensor data via OS-specific means.""" """Update sensor data via OS-specific means."""
if PLATFORM == 'Darwin': if PLATFORM == 'Darwin':
self.update_sensor_data_macos(exit_on_thermal_limit) self.update_sensor_data_macos(alt_max, exit_on_thermal_limit)
elif PLATFORM == 'Linux': elif PLATFORM == 'Linux':
self.update_sensor_data_linux(exit_on_thermal_limit) self.update_sensor_data_linux(alt_max, exit_on_thermal_limit)
def update_sensor_data_linux(self, exit_on_thermal_limit=True): def update_sensor_data_linux(self, alt_max, exit_on_thermal_limit=True):
"""Update sensor data via lm_sensors.""" """Update sensor data via lm_sensors."""
lm_sensor_data = get_sensor_data_lm() lm_sensor_data = get_sensor_data_lm()
for section, adapters in self.data.items(): for section, adapters in self.data.items():
@ -212,6 +223,8 @@ class Sensors():
source_data['Current'] = temp source_data['Current'] = temp
source_data['Max'] = max(temp, source_data['Max']) source_data['Max'] = max(temp, source_data['Max'])
source_data['Temps'].append(temp) source_data['Temps'].append(temp)
if alt_max:
source_data[alt_max] = max(temp, source_data.get(alt_max, 0))
except KeyError: except KeyError:
# Dumb workaround for Dell sensors with changing source names # Dumb workaround for Dell sensors with changing source names
pass pass
@ -221,7 +234,7 @@ class Sensors():
if source_data['Current'] >= CPU_CRITICAL_TEMP: if source_data['Current'] >= CPU_CRITICAL_TEMP:
raise ThermalLimitReachedError('CPU temps reached limit') raise ThermalLimitReachedError('CPU temps reached limit')
def update_sensor_data_macos(self, exit_on_thermal_limit=True): def update_sensor_data_macos(self, alt_max, exit_on_thermal_limit=True):
"""Update sensor data via SMC.""" """Update sensor data via SMC."""
for section, adapters in self.data.items(): for section, adapters in self.data.items():
for sources in adapters.values(): for sources in adapters.values():
@ -239,6 +252,8 @@ class Sensors():
source_data['Current'] = temp source_data['Current'] = temp
source_data['Max'] = max(temp, source_data['Max']) source_data['Max'] = max(temp, source_data['Max'])
source_data['Temps'].append(temp) source_data['Temps'].append(temp)
if alt_max:
source_data[alt_max] = max(temp, source_data.get(alt_max, 0))
# Raise exception if thermal limit reached # Raise exception if thermal limit reached
if exit_on_thermal_limit and section == 'CPUTemps': if exit_on_thermal_limit and section == 'CPUTemps':

View file

@ -68,6 +68,7 @@ smartmontools-svn
smbclient smbclient
speedtest-cli speedtest-cli
sudo sudo
sysbench
sysfsutils sysfsutils
syslinux syslinux
systemd-sysvcompat systemd-sysvcompat

View file

@ -1,4 +1,4 @@
#!/bin/bash #!/usr/bin/env bash
# #
set -o errexit set -o errexit
@ -103,3 +103,11 @@ git clone https://github.com/yuyichao/gnuplot-py gnuplot-py
cd gnuplot-py cd gnuplot-py
git checkout 2c2218dc67 git checkout 2c2218dc67
python3 setup.py install python3 setup.py install
# Sysbench
git clone https://github.com/akopytov/sysbench sysbench
cd sysbench
./autogen.sh LDFLAGS=-L/usr/local/opt/openssl/lib --without-mysql
./configure LDFLAGS=-L/usr/local/opt/openssl/lib --without-mysql
make MACOSX_DEPLOYMENT_TARGET="${OS_VERSION:0:5}" -j
sudo mv -nv sysbench/src/sysbench /usr/local/bin/

View file

@ -1,4 +1,4 @@
#!/bin/bash #!/usr/bin/env bash
# #
## Init macOS env ## Init macOS env

View file

@ -1,4 +1,4 @@
#!/bin/bash #!/usr/bin/env bash
# #
## Update BaseImage for use as WK ## Update BaseImage for use as WK