Split hardware diagnostics into multiple files
This commit is contained in:
parent
fc2bb07d11
commit
99dd7661d4
9 changed files with 697 additions and 516 deletions
|
|
@ -1,8 +1,15 @@
|
||||||
"""WizardKit: hw module init"""
|
"""WizardKit: hw module init"""
|
||||||
|
|
||||||
|
from . import audio
|
||||||
|
from . import benchmark
|
||||||
|
from . import cpu
|
||||||
from . import ddrescue
|
from . import ddrescue
|
||||||
from . import diags
|
from . import diags
|
||||||
from . import disk
|
from . import disk
|
||||||
|
from . import keyboard
|
||||||
|
from . import network
|
||||||
|
from . import screensavers
|
||||||
from . import sensors
|
from . import sensors
|
||||||
|
from . import surface_scan
|
||||||
from . import system
|
from . import system
|
||||||
from . import test
|
from . import test
|
||||||
|
|
|
||||||
37
scripts/wk/hw/audio.py
Normal file
37
scripts/wk/hw/audio.py
Normal file
|
|
@ -0,0 +1,37 @@
|
||||||
|
"""WizardKit: Audio test functions"""
|
||||||
|
# vim: sts=2 sw=2 ts=2
|
||||||
|
|
||||||
|
import logging
|
||||||
|
|
||||||
|
from wk.exe import run_program
|
||||||
|
from wk.std import PLATFORM
|
||||||
|
|
||||||
|
|
||||||
|
# STATIC VARIABLES
|
||||||
|
LOG = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
# Functions
|
||||||
|
def audio_test():
|
||||||
|
"""Run an OS-specific audio test."""
|
||||||
|
if PLATFORM == 'Linux':
|
||||||
|
audio_test_linux()
|
||||||
|
|
||||||
|
|
||||||
|
def audio_test_linux():
|
||||||
|
"""Run an audio test using amixer and speaker-test."""
|
||||||
|
LOG.info('Audio Test')
|
||||||
|
|
||||||
|
# Set volume
|
||||||
|
for source in ('Master', 'PCM'):
|
||||||
|
cmd = f'amixer -q set "{source}" 80% unmute'.split()
|
||||||
|
run_program(cmd, check=False)
|
||||||
|
|
||||||
|
# Run audio tests
|
||||||
|
for mode in ('pink', 'wav'):
|
||||||
|
cmd = f'speaker-test -c 2 -l 1 -t {mode}'.split()
|
||||||
|
run_program(cmd, check=False, pipe=False)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
print("This file is not meant to be called directly.")
|
||||||
212
scripts/wk/hw/benchmark.py
Normal file
212
scripts/wk/hw/benchmark.py
Normal file
|
|
@ -0,0 +1,212 @@
|
||||||
|
"""WizardKit: Benchmark test functions"""
|
||||||
|
# vim: sts=2 sw=2 ts=2
|
||||||
|
|
||||||
|
import logging
|
||||||
|
|
||||||
|
from subprocess import PIPE, STDOUT
|
||||||
|
|
||||||
|
from wk import graph
|
||||||
|
from wk.cfg.hw import (
|
||||||
|
IO_ALT_TEST_SIZE_FACTOR,
|
||||||
|
IO_BLOCK_SIZE,
|
||||||
|
IO_CHUNK_SIZE,
|
||||||
|
IO_GRAPH_WIDTH,
|
||||||
|
IO_MINIMUM_TEST_SIZE,
|
||||||
|
IO_RATE_REGEX,
|
||||||
|
THRESH_HDD_AVG_HIGH,
|
||||||
|
THRESH_HDD_AVG_LOW,
|
||||||
|
THRESH_HDD_MIN,
|
||||||
|
THRESH_SSD_AVG_HIGH,
|
||||||
|
THRESH_SSD_AVG_LOW,
|
||||||
|
THRESH_SSD_MIN,
|
||||||
|
)
|
||||||
|
from wk.exe import run_program
|
||||||
|
from wk.std import (
|
||||||
|
PLATFORM,
|
||||||
|
strip_colors,
|
||||||
|
color_string,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# STATIC VARIABLES
|
||||||
|
LOG = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
# Error Classes
|
||||||
|
class DeviceTooSmallError(RuntimeError):
|
||||||
|
"""Raised when a device is too small to test."""
|
||||||
|
|
||||||
|
|
||||||
|
# Functions
|
||||||
|
def calc_io_dd_values(dev_size):
|
||||||
|
"""Calculate I/O benchmark dd values, returns dict.
|
||||||
|
|
||||||
|
Calculations:
|
||||||
|
The minimum dev size is IO_GRAPH_WIDTH * IO_CHUNK_SIZE
|
||||||
|
(e.g. 1.25 GB for a width of 40 and a chunk size of 32MB)
|
||||||
|
|
||||||
|
read_total is the area to be read in bytes
|
||||||
|
If the dev is < IO_MINIMUM_TEST_SIZE then it's the whole dev
|
||||||
|
Else it's the larger of IO_MINIMUM_TEST_SIZE or the alt test size
|
||||||
|
(determined by dev * IO_ALT_TEST_SIZE_FACTOR)
|
||||||
|
|
||||||
|
read_chunks is the number of groups of IO_CHUNK_SIZE in test_obj.dev
|
||||||
|
This number is reduced to a multiple of IO_GRAPH_WIDTH in order
|
||||||
|
to allow for the data to be condensed cleanly
|
||||||
|
|
||||||
|
read_blocks is the chunk size in number of blocks
|
||||||
|
(e.g. 64 if block size is 512KB and chunk size is 32MB
|
||||||
|
|
||||||
|
skip_total is the number of IO_BLOCK_SIZE groups not tested
|
||||||
|
skip_blocks is the number of blocks to skip per IO_CHUNK_SIZE
|
||||||
|
skip_extra_rate 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_blocks variable
|
||||||
|
"""
|
||||||
|
read_total = min(IO_MINIMUM_TEST_SIZE, dev_size)
|
||||||
|
read_total = max(read_total, dev_size*IO_ALT_TEST_SIZE_FACTOR)
|
||||||
|
read_chunks = int(read_total // IO_CHUNK_SIZE)
|
||||||
|
read_chunks -= read_chunks % IO_GRAPH_WIDTH
|
||||||
|
if read_chunks < IO_GRAPH_WIDTH:
|
||||||
|
raise DeviceTooSmallError
|
||||||
|
read_blocks = int(IO_CHUNK_SIZE / IO_BLOCK_SIZE)
|
||||||
|
read_total = read_chunks * IO_CHUNK_SIZE
|
||||||
|
skip_total = int((dev_size - read_total) // IO_BLOCK_SIZE)
|
||||||
|
skip_blocks = int((skip_total / read_chunks) // 1)
|
||||||
|
skip_extra_rate = 0
|
||||||
|
try:
|
||||||
|
skip_extra_rate = 1 + int(1 / ((skip_total / read_chunks) % 1))
|
||||||
|
except ZeroDivisionError:
|
||||||
|
# skip_extra_rate == 0 is fine
|
||||||
|
pass
|
||||||
|
|
||||||
|
# Done
|
||||||
|
return {
|
||||||
|
'Read Chunks': read_chunks,
|
||||||
|
'Read Blocks': read_blocks,
|
||||||
|
'Skip Blocks': skip_blocks,
|
||||||
|
'Skip Extra': skip_extra_rate,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def check_io_results(test_obj, rate_list, graph_width):
|
||||||
|
"""Generate colored report using rate_list, returns list of str."""
|
||||||
|
avg_read = sum(rate_list) / len(rate_list)
|
||||||
|
min_read = min(rate_list)
|
||||||
|
max_read = max(rate_list)
|
||||||
|
if test_obj.dev.ssd:
|
||||||
|
thresh_min = THRESH_SSD_MIN
|
||||||
|
thresh_avg_high = THRESH_SSD_AVG_HIGH
|
||||||
|
thresh_avg_low = THRESH_SSD_AVG_LOW
|
||||||
|
else:
|
||||||
|
thresh_min = THRESH_HDD_MIN
|
||||||
|
thresh_avg_high = THRESH_HDD_AVG_HIGH
|
||||||
|
thresh_avg_low = THRESH_HDD_AVG_LOW
|
||||||
|
|
||||||
|
# Add horizontal graph to report
|
||||||
|
for line in graph.generate_horizontal_graph(rate_list, graph_width):
|
||||||
|
if not strip_colors(line).strip():
|
||||||
|
# Skip empty lines
|
||||||
|
continue
|
||||||
|
test_obj.report.append(line)
|
||||||
|
|
||||||
|
# Add read rates to report
|
||||||
|
test_obj.report.append(
|
||||||
|
f'Read speeds avg: {avg_read/(1000**2):3.1f}'
|
||||||
|
f' min: {min_read/(1000**2):3.1f}'
|
||||||
|
f' max: {max_read/(1000**2):3.1f}'
|
||||||
|
)
|
||||||
|
|
||||||
|
# Compare against thresholds
|
||||||
|
if min_read <= thresh_min and avg_read <= thresh_avg_high:
|
||||||
|
test_obj.failed = True
|
||||||
|
elif avg_read <= thresh_avg_low:
|
||||||
|
test_obj.failed = True
|
||||||
|
else:
|
||||||
|
test_obj.passed = True
|
||||||
|
|
||||||
|
# Set status
|
||||||
|
if test_obj.failed:
|
||||||
|
test_obj.set_status('Failed')
|
||||||
|
elif test_obj.passed:
|
||||||
|
test_obj.set_status('Passed')
|
||||||
|
else:
|
||||||
|
test_obj.set_status('Unknown')
|
||||||
|
|
||||||
|
|
||||||
|
def run_io_test(test_obj, log_path):
|
||||||
|
"""Run I/O benchmark and handle exceptions."""
|
||||||
|
dev_path = test_obj.dev.path
|
||||||
|
if PLATFORM == 'Darwin':
|
||||||
|
# Use "RAW" disks under macOS
|
||||||
|
dev_path = dev_path.with_name(f'r{dev_path.name}')
|
||||||
|
LOG.info('Using %s for better performance', dev_path)
|
||||||
|
offset = 0
|
||||||
|
read_rates = []
|
||||||
|
test_obj.report.append(color_string('I/O Benchmark', 'BLUE'))
|
||||||
|
|
||||||
|
# Get dd values or bail
|
||||||
|
try:
|
||||||
|
dd_values = calc_io_dd_values(test_obj.dev.size)
|
||||||
|
except DeviceTooSmallError:
|
||||||
|
test_obj.set_status('N/A')
|
||||||
|
test_obj.report.append(
|
||||||
|
color_string('Disk too small to test', 'YELLOW'),
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
# Run dd read tests
|
||||||
|
for _i in range(dd_values['Read Chunks']):
|
||||||
|
_i += 1
|
||||||
|
|
||||||
|
# Build cmd
|
||||||
|
skip = dd_values['Skip Blocks']
|
||||||
|
if dd_values['Skip Extra'] and _i % dd_values['Skip Extra'] == 0:
|
||||||
|
skip += 1
|
||||||
|
cmd = [
|
||||||
|
'sudo', 'dd',
|
||||||
|
f'bs={IO_BLOCK_SIZE}',
|
||||||
|
f'skip={offset+skip}',
|
||||||
|
f'count={dd_values["Read Blocks"]}',
|
||||||
|
f'if={dev_path}',
|
||||||
|
'of=/dev/null',
|
||||||
|
]
|
||||||
|
if PLATFORM == 'Linux':
|
||||||
|
cmd.append('iflag=direct')
|
||||||
|
|
||||||
|
# Run and get read rate
|
||||||
|
try:
|
||||||
|
proc = run_program(
|
||||||
|
cmd,
|
||||||
|
pipe=False,
|
||||||
|
stdout=PIPE,
|
||||||
|
stderr=STDOUT,
|
||||||
|
)
|
||||||
|
except PermissionError as err:
|
||||||
|
# Since we're using sudo we can't kill dd
|
||||||
|
# Assuming this happened during a CTRL+c
|
||||||
|
raise KeyboardInterrupt from err
|
||||||
|
match = IO_RATE_REGEX.search(proc.stdout)
|
||||||
|
if match:
|
||||||
|
read_rates.append(
|
||||||
|
int(match.group('bytes')) / float(match.group('seconds')),
|
||||||
|
)
|
||||||
|
match.group(1)
|
||||||
|
|
||||||
|
# Show progress
|
||||||
|
with open(log_path, 'a', encoding='utf-8') as _f:
|
||||||
|
if _i % 5 == 0:
|
||||||
|
percent = (_i / dd_values['Read Chunks']) * 100
|
||||||
|
_f.write(f' {graph.vertical_graph_line(percent, read_rates[-1])}\n')
|
||||||
|
|
||||||
|
# Update offset
|
||||||
|
offset += dd_values['Read Blocks'] + skip
|
||||||
|
|
||||||
|
# Check results
|
||||||
|
check_io_results(test_obj, read_rates, IO_GRAPH_WIDTH)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
print("This file is not meant to be called directly.")
|
||||||
205
scripts/wk/hw/cpu.py
Normal file
205
scripts/wk/hw/cpu.py
Normal file
|
|
@ -0,0 +1,205 @@
|
||||||
|
"""WizardKit: CPU test functions"""
|
||||||
|
# vim: sts=2 sw=2 ts=2
|
||||||
|
|
||||||
|
import logging
|
||||||
|
import re
|
||||||
|
import subprocess
|
||||||
|
|
||||||
|
from wk import exe
|
||||||
|
from wk.cfg.hw import CPU_FAILURE_TEMP
|
||||||
|
from wk.os.mac import set_fans as macos_set_fans
|
||||||
|
from wk.std import (
|
||||||
|
PLATFORM,
|
||||||
|
color_string,
|
||||||
|
print_error,
|
||||||
|
print_warning,
|
||||||
|
)
|
||||||
|
from wk.tmux import respawn_pane as tmux_respawn_pane
|
||||||
|
|
||||||
|
|
||||||
|
# STATIC VARIABLES
|
||||||
|
LOG = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
# Functions
|
||||||
|
def check_cooling_results(test_obj, sensors, run_sysbench=False):
|
||||||
|
"""Check cooling results and update test_obj."""
|
||||||
|
max_temp = sensors.cpu_max_temp()
|
||||||
|
temp_labels = ['Idle', 'Max', 'Cooldown']
|
||||||
|
if run_sysbench:
|
||||||
|
temp_labels.append('Sysbench')
|
||||||
|
|
||||||
|
# Check temps
|
||||||
|
if not max_temp:
|
||||||
|
test_obj.set_status('Unknown')
|
||||||
|
elif max_temp >= CPU_FAILURE_TEMP:
|
||||||
|
test_obj.failed = True
|
||||||
|
test_obj.set_status('Failed')
|
||||||
|
elif 'Aborted' not in test_obj.status:
|
||||||
|
test_obj.passed = True
|
||||||
|
test_obj.set_status('Passed')
|
||||||
|
|
||||||
|
# Add temps to report
|
||||||
|
for line in sensors.generate_report(*temp_labels, only_cpu=True):
|
||||||
|
test_obj.report.append(f' {line}')
|
||||||
|
|
||||||
|
|
||||||
|
def check_mprime_results(test_obj, working_dir):
|
||||||
|
"""Check mprime log files and update test_obj."""
|
||||||
|
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', encoding='utf-8') 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(color_string(f' {line}', 'YELLOW'))
|
||||||
|
if not (passing_lines or warning_lines):
|
||||||
|
test_obj.report.append(color_string(' Unknown result', 'YELLOW'))
|
||||||
|
|
||||||
|
|
||||||
|
def start_mprime(working_dir, log_path):
|
||||||
|
"""Start mprime and save filtered output to log, returns Popen object."""
|
||||||
|
set_apple_fan_speed('max')
|
||||||
|
proc_mprime = subprocess.Popen( # pylint: disable=consider-using-with
|
||||||
|
['mprime', '-t'],
|
||||||
|
cwd=working_dir,
|
||||||
|
stdout=subprocess.PIPE,
|
||||||
|
stderr=subprocess.STDOUT,
|
||||||
|
)
|
||||||
|
proc_grep = subprocess.Popen( # pylint: disable=consider-using-with
|
||||||
|
'grep --ignore-case --invert-match --line-buffered stress.txt'.split(),
|
||||||
|
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
|
||||||
|
|
||||||
|
|
||||||
|
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( # pylint: disable=consider-using-with
|
||||||
|
log_path, 'a', encoding='utf-8',
|
||||||
|
)
|
||||||
|
proc_sysbench = exe.popen_program(sysbench_cmd, stdout=filehandle_sysbench)
|
||||||
|
|
||||||
|
# Done
|
||||||
|
return (proc_sysbench, filehandle_sysbench)
|
||||||
|
|
||||||
|
|
||||||
|
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 == 'Darwin':
|
||||||
|
try:
|
||||||
|
macos_set_fans(speed)
|
||||||
|
except (RuntimeError, ValueError, subprocess.CalledProcessError) as err:
|
||||||
|
LOG.error('Failed to set fans to %s', speed)
|
||||||
|
LOG.error('Error: %s', err)
|
||||||
|
print_error(f'Failed to set fans to {speed}')
|
||||||
|
for line in str(err).splitlines():
|
||||||
|
print_warning(f' {line.strip()}')
|
||||||
|
elif PLATFORM == 'Linux':
|
||||||
|
cmd = ['apple-fans', speed]
|
||||||
|
exe.run_program(cmd, check=False)
|
||||||
|
|
||||||
|
|
||||||
|
def stop_mprime(proc_mprime):
|
||||||
|
"""Stop mprime gracefully, then forcefully as needed."""
|
||||||
|
proc_mprime.terminate()
|
||||||
|
try:
|
||||||
|
proc_mprime.wait(timeout=5)
|
||||||
|
except subprocess.TimeoutExpired:
|
||||||
|
proc_mprime.kill()
|
||||||
|
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')
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
print("This file is not meant to be called directly.")
|
||||||
|
|
@ -1,32 +1,27 @@
|
||||||
"""WizardKit: Hardware diagnostics"""
|
"""WizardKit: Hardware diagnostics"""
|
||||||
# pylint: disable=too-many-lines
|
|
||||||
# vim: sts=2 sw=2 ts=2
|
# vim: sts=2 sw=2 ts=2
|
||||||
|
|
||||||
import atexit
|
import atexit
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
import pathlib
|
import pathlib
|
||||||
import re
|
|
||||||
import subprocess
|
import subprocess
|
||||||
import time
|
import time
|
||||||
|
|
||||||
from docopt import docopt
|
from docopt import docopt
|
||||||
|
|
||||||
from wk import cfg, debug, exe, graph, log, net, std, tmux
|
from wk import cfg, debug, exe, log, std, tmux
|
||||||
from wk import os as wk_os
|
from wk.cfg.hw import STATUS_COLORS
|
||||||
from wk.cfg.hw import (
|
from wk.hw import benchmark as hw_benchmark
|
||||||
BADBLOCKS_REGEX,
|
from wk.hw import cpu as hw_cpu
|
||||||
IO_GRAPH_WIDTH,
|
|
||||||
IO_ALT_TEST_SIZE_FACTOR,
|
|
||||||
IO_BLOCK_SIZE,
|
|
||||||
IO_CHUNK_SIZE,
|
|
||||||
IO_MINIMUM_TEST_SIZE,
|
|
||||||
IO_RATE_REGEX,
|
|
||||||
STATUS_COLORS,
|
|
||||||
)
|
|
||||||
from wk.hw import disk as hw_disk
|
from wk.hw import disk as hw_disk
|
||||||
from wk.hw import sensors as hw_sensors
|
from wk.hw import sensors as hw_sensors
|
||||||
|
from wk.hw import surface_scan as hw_surface_scan
|
||||||
from wk.hw import system as hw_system
|
from wk.hw import system as hw_system
|
||||||
|
from wk.hw.audio import audio_test
|
||||||
|
from wk.hw.keyboard import keyboard_test
|
||||||
|
from wk.hw.network import network_test
|
||||||
|
from wk.hw.screensavers import screensaver
|
||||||
from wk.hw.test import Test, TestGroup
|
from wk.hw.test import Test, TestGroup
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -79,11 +74,6 @@ MENU_TOGGLES = (
|
||||||
)
|
)
|
||||||
PLATFORM = std.PLATFORM
|
PLATFORM = std.PLATFORM
|
||||||
|
|
||||||
# Error Classes
|
|
||||||
class DeviceTooSmallError(RuntimeError):
|
|
||||||
"""Raised when a device is too small to test."""
|
|
||||||
|
|
||||||
|
|
||||||
# Classes
|
# Classes
|
||||||
class State():
|
class State():
|
||||||
"""Object for tracking hardware diagnostic data."""
|
"""Object for tracking hardware diagnostic data."""
|
||||||
|
|
@ -407,27 +397,6 @@ class State():
|
||||||
|
|
||||||
|
|
||||||
# Functions
|
# Functions
|
||||||
def audio_test():
|
|
||||||
"""Run an OS-specific audio test."""
|
|
||||||
if PLATFORM == 'Linux':
|
|
||||||
audio_test_linux()
|
|
||||||
|
|
||||||
|
|
||||||
def audio_test_linux():
|
|
||||||
"""Run an audio test using amixer and speaker-test."""
|
|
||||||
LOG.info('Audio Test')
|
|
||||||
|
|
||||||
# Set volume
|
|
||||||
for source in ('Master', 'PCM'):
|
|
||||||
cmd = f'amixer -q set "{source}" 80% unmute'.split()
|
|
||||||
exe.run_program(cmd, check=False)
|
|
||||||
|
|
||||||
# Run audio tests
|
|
||||||
for mode in ('pink', 'wav'):
|
|
||||||
cmd = f'speaker-test -c 2 -l 1 -t {mode}'.split()
|
|
||||||
exe.run_program(cmd, check=False, pipe=False)
|
|
||||||
|
|
||||||
|
|
||||||
def build_menu(cli_mode=False, quick_mode=False):
|
def build_menu(cli_mode=False, quick_mode=False):
|
||||||
# pylint: disable=too-many-branches
|
# pylint: disable=too-many-branches
|
||||||
"""Build main menu, returns wk.std.Menu."""
|
"""Build main menu, returns wk.std.Menu."""
|
||||||
|
|
@ -480,180 +449,6 @@ def build_menu(cli_mode=False, quick_mode=False):
|
||||||
return menu
|
return menu
|
||||||
|
|
||||||
|
|
||||||
def calc_io_dd_values(dev_size):
|
|
||||||
"""Calculate I/O benchmark dd values, returns dict.
|
|
||||||
|
|
||||||
Calculations:
|
|
||||||
The minimum dev size is IO_GRAPH_WIDTH * IO_CHUNK_SIZE
|
|
||||||
(e.g. 1.25 GB for a width of 40 and a chunk size of 32MB)
|
|
||||||
|
|
||||||
read_total is the area to be read in bytes
|
|
||||||
If the dev is < IO_MINIMUM_TEST_SIZE then it's the whole dev
|
|
||||||
Else it's the larger of IO_MINIMUM_TEST_SIZE or the alt test size
|
|
||||||
(determined by dev * IO_ALT_TEST_SIZE_FACTOR)
|
|
||||||
|
|
||||||
read_chunks is the number of groups of IO_CHUNK_SIZE in test_obj.dev
|
|
||||||
This number is reduced to a multiple of IO_GRAPH_WIDTH in order
|
|
||||||
to allow for the data to be condensed cleanly
|
|
||||||
|
|
||||||
read_blocks is the chunk size in number of blocks
|
|
||||||
(e.g. 64 if block size is 512KB and chunk size is 32MB
|
|
||||||
|
|
||||||
skip_total is the number of IO_BLOCK_SIZE groups not tested
|
|
||||||
skip_blocks is the number of blocks to skip per IO_CHUNK_SIZE
|
|
||||||
skip_extra_rate 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_blocks variable
|
|
||||||
"""
|
|
||||||
read_total = min(IO_MINIMUM_TEST_SIZE, dev_size)
|
|
||||||
read_total = max(read_total, dev_size*IO_ALT_TEST_SIZE_FACTOR)
|
|
||||||
read_chunks = int(read_total // IO_CHUNK_SIZE)
|
|
||||||
read_chunks -= read_chunks % IO_GRAPH_WIDTH
|
|
||||||
if read_chunks < IO_GRAPH_WIDTH:
|
|
||||||
raise DeviceTooSmallError
|
|
||||||
read_blocks = int(IO_CHUNK_SIZE / IO_BLOCK_SIZE)
|
|
||||||
read_total = read_chunks * IO_CHUNK_SIZE
|
|
||||||
skip_total = int((dev_size - read_total) // IO_BLOCK_SIZE)
|
|
||||||
skip_blocks = int((skip_total / read_chunks) // 1)
|
|
||||||
skip_extra_rate = 0
|
|
||||||
try:
|
|
||||||
skip_extra_rate = 1 + int(1 / ((skip_total / read_chunks) % 1))
|
|
||||||
except ZeroDivisionError:
|
|
||||||
# skip_extra_rate == 0 is fine
|
|
||||||
pass
|
|
||||||
|
|
||||||
# Done
|
|
||||||
return {
|
|
||||||
'Read Chunks': read_chunks,
|
|
||||||
'Read Blocks': read_blocks,
|
|
||||||
'Skip Blocks': skip_blocks,
|
|
||||||
'Skip Extra': skip_extra_rate,
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
def check_cooling_results(test_obj, sensors, run_sysbench=False):
|
|
||||||
"""Check cooling results and update test_obj."""
|
|
||||||
max_temp = sensors.cpu_max_temp()
|
|
||||||
temp_labels = ['Idle', 'Max', 'Cooldown']
|
|
||||||
if run_sysbench:
|
|
||||||
temp_labels.append('Sysbench')
|
|
||||||
|
|
||||||
# Check temps
|
|
||||||
if not max_temp:
|
|
||||||
test_obj.set_status('Unknown')
|
|
||||||
elif max_temp >= cfg.hw.CPU_FAILURE_TEMP:
|
|
||||||
test_obj.failed = True
|
|
||||||
test_obj.set_status('Failed')
|
|
||||||
elif 'Aborted' not in test_obj.status:
|
|
||||||
test_obj.passed = True
|
|
||||||
test_obj.set_status('Passed')
|
|
||||||
|
|
||||||
# Add temps to report
|
|
||||||
for line in sensors.generate_report(*temp_labels, only_cpu=True):
|
|
||||||
test_obj.report.append(f' {line}')
|
|
||||||
|
|
||||||
|
|
||||||
def check_io_benchmark_results(test_obj, rate_list, graph_width):
|
|
||||||
"""Generate colored report using rate_list, returns list of str."""
|
|
||||||
avg_read = sum(rate_list) / len(rate_list)
|
|
||||||
min_read = min(rate_list)
|
|
||||||
max_read = max(rate_list)
|
|
||||||
if test_obj.dev.ssd:
|
|
||||||
thresh_min = cfg.hw.THRESH_SSD_MIN
|
|
||||||
thresh_avg_high = cfg.hw.THRESH_SSD_AVG_HIGH
|
|
||||||
thresh_avg_low = cfg.hw.THRESH_SSD_AVG_LOW
|
|
||||||
else:
|
|
||||||
thresh_min = cfg.hw.THRESH_HDD_MIN
|
|
||||||
thresh_avg_high = cfg.hw.THRESH_HDD_AVG_HIGH
|
|
||||||
thresh_avg_low = cfg.hw.THRESH_HDD_AVG_LOW
|
|
||||||
|
|
||||||
# Add horizontal graph to report
|
|
||||||
for line in graph.generate_horizontal_graph(rate_list, graph_width):
|
|
||||||
if not std.strip_colors(line).strip():
|
|
||||||
# Skip empty lines
|
|
||||||
continue
|
|
||||||
test_obj.report.append(line)
|
|
||||||
|
|
||||||
# Add read rates to report
|
|
||||||
test_obj.report.append(
|
|
||||||
f'Read speeds avg: {avg_read/(1000**2):3.1f}'
|
|
||||||
f' min: {min_read/(1000**2):3.1f}'
|
|
||||||
f' max: {max_read/(1000**2):3.1f}'
|
|
||||||
)
|
|
||||||
|
|
||||||
# Compare against thresholds
|
|
||||||
if min_read <= thresh_min and avg_read <= thresh_avg_high:
|
|
||||||
test_obj.failed = True
|
|
||||||
elif avg_read <= thresh_avg_low:
|
|
||||||
test_obj.failed = True
|
|
||||||
else:
|
|
||||||
test_obj.passed = True
|
|
||||||
|
|
||||||
# Set status
|
|
||||||
if test_obj.failed:
|
|
||||||
test_obj.set_status('Failed')
|
|
||||||
elif test_obj.passed:
|
|
||||||
test_obj.set_status('Passed')
|
|
||||||
else:
|
|
||||||
test_obj.set_status('Unknown')
|
|
||||||
|
|
||||||
|
|
||||||
def check_mprime_results(test_obj, working_dir):
|
|
||||||
"""Check mprime log files and update test_obj."""
|
|
||||||
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', encoding='utf-8') 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 check_self_test_results(test_obj, aborted=False):
|
def check_self_test_results(test_obj, aborted=False):
|
||||||
"""Check SMART self-test results."""
|
"""Check SMART self-test results."""
|
||||||
test_obj.report.append(std.color_string('Self-Test', 'BLUE'))
|
test_obj.report.append(std.color_string('Self-Test', 'BLUE'))
|
||||||
|
|
@ -726,8 +521,8 @@ def cpu_stress_tests(state, test_objects):
|
||||||
|
|
||||||
# Stress CPU
|
# Stress CPU
|
||||||
std.print_info('Running stress test')
|
std.print_info('Running stress test')
|
||||||
set_apple_fan_speed('max')
|
hw_cpu.set_apple_fan_speed('max')
|
||||||
proc_mprime = start_mprime(state.log_dir, prime_log)
|
proc_mprime = hw_cpu.start_mprime(state.log_dir, prime_log)
|
||||||
|
|
||||||
# Show countdown
|
# Show countdown
|
||||||
print('')
|
print('')
|
||||||
|
|
@ -737,7 +532,7 @@ def cpu_stress_tests(state, test_objects):
|
||||||
aborted = True
|
aborted = True
|
||||||
|
|
||||||
# Stop Prime95
|
# Stop Prime95
|
||||||
stop_mprime(proc_mprime)
|
hw_cpu.stop_mprime(proc_mprime)
|
||||||
|
|
||||||
# Update progress if necessary
|
# Update progress if necessary
|
||||||
if sensors.cpu_reached_critical_temp() or aborted:
|
if sensors.cpu_reached_critical_temp() or aborted:
|
||||||
|
|
@ -754,7 +549,9 @@ def cpu_stress_tests(state, test_objects):
|
||||||
|
|
||||||
# Check Prime95 results
|
# Check Prime95 results
|
||||||
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)
|
hw_cpu.check_mprime_results(
|
||||||
|
test_obj=test_mprime_obj, working_dir=state.log_dir,
|
||||||
|
)
|
||||||
|
|
||||||
# Run Sysbench test if necessary
|
# Run Sysbench test if necessary
|
||||||
run_sysbench = (
|
run_sysbench = (
|
||||||
|
|
@ -767,7 +564,7 @@ def cpu_stress_tests(state, test_objects):
|
||||||
std.clear_screen()
|
std.clear_screen()
|
||||||
std.print_info('Running alternate stress test')
|
std.print_info('Running alternate stress test')
|
||||||
print('')
|
print('')
|
||||||
proc_sysbench, filehandle_sysbench = start_sysbench(
|
proc_sysbench, filehandle_sysbench = hw_cpu.start_sysbench(
|
||||||
sensors,
|
sensors,
|
||||||
sensors_out,
|
sensors_out,
|
||||||
log_path=prime_log.with_name('sysbench.log'),
|
log_path=prime_log.with_name('sysbench.log'),
|
||||||
|
|
@ -780,7 +577,7 @@ def cpu_stress_tests(state, test_objects):
|
||||||
LOG.error('Failed to find sysbench process', exc_info=True)
|
LOG.error('Failed to find sysbench process', exc_info=True)
|
||||||
except KeyboardInterrupt:
|
except KeyboardInterrupt:
|
||||||
aborted = True
|
aborted = True
|
||||||
stop_sysbench(proc_sysbench, filehandle_sysbench)
|
hw_cpu.stop_sysbench(proc_sysbench, filehandle_sysbench)
|
||||||
|
|
||||||
# Update progress
|
# Update progress
|
||||||
# NOTE: CPU critical temp check isn't really necessary
|
# NOTE: CPU critical temp check isn't really necessary
|
||||||
|
|
@ -792,7 +589,7 @@ def cpu_stress_tests(state, test_objects):
|
||||||
|
|
||||||
# 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_cooling_obj, sensors, run_sysbench)
|
hw_cpu.check_cooling_results(test_cooling_obj, sensors, run_sysbench)
|
||||||
|
|
||||||
# Cleanup
|
# Cleanup
|
||||||
state.update_progress_pane()
|
state.update_progress_pane()
|
||||||
|
|
@ -832,77 +629,6 @@ def disk_io_benchmark(state, test_objects, skip_usb=True):
|
||||||
LOG.info('Disk I/O Benchmark (dd)')
|
LOG.info('Disk I/O Benchmark (dd)')
|
||||||
aborted = False
|
aborted = False
|
||||||
|
|
||||||
def _run_io_benchmark(test_obj, log_path):
|
|
||||||
"""Run I/O benchmark and handle exceptions."""
|
|
||||||
dev_path = test_obj.dev.path
|
|
||||||
if PLATFORM == 'Darwin':
|
|
||||||
# Use "RAW" disks under macOS
|
|
||||||
dev_path = dev_path.with_name(f'r{dev_path.name}')
|
|
||||||
LOG.info('Using %s for better performance', dev_path)
|
|
||||||
offset = 0
|
|
||||||
read_rates = []
|
|
||||||
test_obj.report.append(std.color_string('I/O Benchmark', 'BLUE'))
|
|
||||||
|
|
||||||
# Get dd values or bail
|
|
||||||
try:
|
|
||||||
dd_values = calc_io_dd_values(test_obj.dev.size)
|
|
||||||
except DeviceTooSmallError:
|
|
||||||
test_obj.set_status('N/A')
|
|
||||||
test_obj.report.append(
|
|
||||||
std.color_string('Disk too small to test', 'YELLOW'),
|
|
||||||
)
|
|
||||||
return
|
|
||||||
|
|
||||||
# Run dd read tests
|
|
||||||
for _i in range(dd_values['Read Chunks']):
|
|
||||||
_i += 1
|
|
||||||
|
|
||||||
# Build cmd
|
|
||||||
skip = dd_values['Skip Blocks']
|
|
||||||
if dd_values['Skip Extra'] and _i % dd_values['Skip Extra'] == 0:
|
|
||||||
skip += 1
|
|
||||||
cmd = [
|
|
||||||
'sudo', 'dd',
|
|
||||||
f'bs={IO_BLOCK_SIZE}',
|
|
||||||
f'skip={offset+skip}',
|
|
||||||
f'count={dd_values["Read Blocks"]}',
|
|
||||||
f'if={dev_path}',
|
|
||||||
'of=/dev/null',
|
|
||||||
]
|
|
||||||
if PLATFORM == 'Linux':
|
|
||||||
cmd.append('iflag=direct')
|
|
||||||
|
|
||||||
# Run and get read rate
|
|
||||||
try:
|
|
||||||
proc = exe.run_program(
|
|
||||||
cmd,
|
|
||||||
pipe=False,
|
|
||||||
stdout=subprocess.PIPE,
|
|
||||||
stderr=subprocess.STDOUT,
|
|
||||||
)
|
|
||||||
except PermissionError as err:
|
|
||||||
# Since we're using sudo we can't kill dd
|
|
||||||
# Assuming this happened during a CTRL+c
|
|
||||||
raise KeyboardInterrupt from err
|
|
||||||
match = IO_RATE_REGEX.search(proc.stdout)
|
|
||||||
if match:
|
|
||||||
read_rates.append(
|
|
||||||
int(match.group('bytes')) / float(match.group('seconds')),
|
|
||||||
)
|
|
||||||
match.group(1)
|
|
||||||
|
|
||||||
# Show progress
|
|
||||||
with open(log_path, 'a', encoding='utf-8') as _f:
|
|
||||||
if _i % 5 == 0:
|
|
||||||
percent = (_i / dd_values['Read Chunks']) * 100
|
|
||||||
_f.write(f' {graph.vertical_graph_line(percent, read_rates[-1])}\n')
|
|
||||||
|
|
||||||
# Update offset
|
|
||||||
offset += dd_values['Read Blocks'] + skip
|
|
||||||
|
|
||||||
# Check results
|
|
||||||
check_io_benchmark_results(test_obj, read_rates, IO_GRAPH_WIDTH)
|
|
||||||
|
|
||||||
# Run benchmarks
|
# Run benchmarks
|
||||||
state.update_top_pane(
|
state.update_top_pane(
|
||||||
f'Disk I/O Benchmark{"s" if len(test_objects) > 1 else ""}',
|
f'Disk I/O Benchmark{"s" if len(test_objects) > 1 else ""}',
|
||||||
|
|
@ -935,7 +661,7 @@ def disk_io_benchmark(state, test_objects, skip_usb=True):
|
||||||
)
|
)
|
||||||
state.update_progress_pane()
|
state.update_progress_pane()
|
||||||
try:
|
try:
|
||||||
_run_io_benchmark(test, test_log)
|
hw_benchmark.run_io_test(test, test_log)
|
||||||
except KeyboardInterrupt:
|
except KeyboardInterrupt:
|
||||||
aborted = True
|
aborted = True
|
||||||
except (subprocess.CalledProcessError, TypeError, ValueError) as err:
|
except (subprocess.CalledProcessError, TypeError, ValueError) as err:
|
||||||
|
|
@ -1052,65 +778,6 @@ def disk_surface_scan(state, test_objects):
|
||||||
threads = []
|
threads = []
|
||||||
state.panes['badblocks'] = []
|
state.panes['badblocks'] = []
|
||||||
|
|
||||||
def _run_surface_scan(test_obj, log_path):
|
|
||||||
"""Run surface scan and handle exceptions."""
|
|
||||||
block_size = '1024'
|
|
||||||
dev = test_obj.dev
|
|
||||||
dev_path = test_obj.dev.path
|
|
||||||
if PLATFORM == 'Darwin':
|
|
||||||
# Use "RAW" disks under macOS
|
|
||||||
dev_path = dev_path.with_name(f'r{dev_path.name}')
|
|
||||||
LOG.info('Using %s for better performance', dev_path)
|
|
||||||
test_obj.report.append(std.color_string('badblocks', 'BLUE'))
|
|
||||||
test_obj.set_status('Working')
|
|
||||||
|
|
||||||
# Increase block size if necessary
|
|
||||||
if (dev.phy_sec == 4096
|
|
||||||
or dev.size >= cfg.hw.BADBLOCKS_LARGE_DISK):
|
|
||||||
block_size = '4096'
|
|
||||||
|
|
||||||
# Start scan
|
|
||||||
cmd = ['sudo', 'badblocks', '-sv', '-b', block_size, '-e', '1', dev_path]
|
|
||||||
with open(log_path, 'a', encoding='utf-8') as _f:
|
|
||||||
size_str = std.bytes_to_string(dev.size, use_binary=False)
|
|
||||||
_f.write(
|
|
||||||
std.color_string(
|
|
||||||
['[', dev.path.name, ' ', size_str, ']\n'],
|
|
||||||
[None, 'BLUE', None, 'CYAN', None],
|
|
||||||
sep='',
|
|
||||||
),
|
|
||||||
)
|
|
||||||
_f.flush()
|
|
||||||
exe.run_program(
|
|
||||||
cmd,
|
|
||||||
check=False,
|
|
||||||
pipe=False,
|
|
||||||
stderr=subprocess.STDOUT,
|
|
||||||
stdout=_f,
|
|
||||||
)
|
|
||||||
|
|
||||||
# Check results
|
|
||||||
with open(log_path, 'r', encoding='utf-8') as _f:
|
|
||||||
for line in _f.readlines():
|
|
||||||
line = std.strip_colors(line.strip())
|
|
||||||
if not line or line.startswith('Checking') or line.startswith('['):
|
|
||||||
# Skip
|
|
||||||
continue
|
|
||||||
match = BADBLOCKS_REGEX.search(line)
|
|
||||||
if match:
|
|
||||||
if all(s == '0' for s in match.groups()):
|
|
||||||
test_obj.passed = True
|
|
||||||
test_obj.report.append(f' {line}')
|
|
||||||
test_obj.set_status('Passed')
|
|
||||||
else:
|
|
||||||
test_obj.failed = True
|
|
||||||
test_obj.report.append(f' {std.color_string(line, "YELLOW")}')
|
|
||||||
test_obj.set_status('Failed')
|
|
||||||
else:
|
|
||||||
test_obj.report.append(f' {std.color_string(line, "YELLOW")}')
|
|
||||||
if not (test_obj.passed or test_obj.failed):
|
|
||||||
test_obj.set_status('Unknown')
|
|
||||||
|
|
||||||
# Update panes
|
# Update panes
|
||||||
state.update_top_pane(
|
state.update_top_pane(
|
||||||
f'Disk Surface Scan{"s" if len(test_objects) > 1 else ""}',
|
f'Disk Surface Scan{"s" if len(test_objects) > 1 else ""}',
|
||||||
|
|
@ -1141,7 +808,9 @@ def disk_surface_scan(state, test_objects):
|
||||||
|
|
||||||
# Start thread
|
# Start thread
|
||||||
test_log = f'{state.log_dir}/{test.dev.path.name}_badblocks.log'
|
test_log = f'{state.log_dir}/{test.dev.path.name}_badblocks.log'
|
||||||
threads.append(exe.start_thread(_run_surface_scan, args=(test, test_log)))
|
threads.append(exe.start_thread(
|
||||||
|
hw_surface_scan.run_scan, args=(test, test_log),
|
||||||
|
))
|
||||||
|
|
||||||
# Show progress
|
# Show progress
|
||||||
if threads[-1].is_alive():
|
if threads[-1].is_alive():
|
||||||
|
|
@ -1182,13 +851,6 @@ def disk_surface_scan(state, test_objects):
|
||||||
raise std.GenericAbort('Aborted')
|
raise std.GenericAbort('Aborted')
|
||||||
|
|
||||||
|
|
||||||
def keyboard_test():
|
|
||||||
"""Test keyboard using xev."""
|
|
||||||
LOG.info('Keyboard Test (xev)')
|
|
||||||
cmd = ['xev', '-event', 'keyboard']
|
|
||||||
exe.run_program(cmd, check=False, pipe=False)
|
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
# pylint: disable=too-many-branches
|
# pylint: disable=too-many-branches
|
||||||
"""Main function for hardware diagnostics."""
|
"""Main function for hardware diagnostics."""
|
||||||
|
|
@ -1262,39 +924,6 @@ def main():
|
||||||
state.update_top_pane('Main Menu')
|
state.update_top_pane('Main Menu')
|
||||||
|
|
||||||
|
|
||||||
def network_test():
|
|
||||||
"""Run network tests."""
|
|
||||||
LOG.info('Network Test')
|
|
||||||
try_and_print = std.TryAndPrint()
|
|
||||||
result = try_and_print.run(
|
|
||||||
message='Network connection...',
|
|
||||||
function=net.connected_to_private_network,
|
|
||||||
msg_good='OK',
|
|
||||||
raise_on_error=True,
|
|
||||||
)
|
|
||||||
|
|
||||||
# Bail if not connected
|
|
||||||
if result['Failed']:
|
|
||||||
std.print_warning('Please connect to a network and try again')
|
|
||||||
std.pause('Press Enter to return to main menu...')
|
|
||||||
return
|
|
||||||
|
|
||||||
# Show IP address(es)
|
|
||||||
net.show_valid_addresses()
|
|
||||||
|
|
||||||
# Ping tests
|
|
||||||
try_and_print.run(
|
|
||||||
'Internet connection...', net.ping, msg_good='OK', addr='8.8.8.8')
|
|
||||||
try_and_print.run(
|
|
||||||
'DNS resolution...', net.ping, msg_good='OK', addr='google.com')
|
|
||||||
|
|
||||||
# Speedtest
|
|
||||||
try_and_print.run('Speedtest...', net.speedtest)
|
|
||||||
|
|
||||||
# Done
|
|
||||||
std.pause('Press Enter to return to main menu...')
|
|
||||||
|
|
||||||
|
|
||||||
def print_countdown(proc, seconds):
|
def print_countdown(proc, seconds):
|
||||||
"""Print countdown to screen while proc is alive."""
|
"""Print countdown to screen while proc is alive."""
|
||||||
for i in range(seconds):
|
for i in range(seconds):
|
||||||
|
|
@ -1376,51 +1005,6 @@ def run_diags(state, menu, quick_mode=False):
|
||||||
std.pause('Press Enter to return to main menu...')
|
std.pause('Press Enter to return to main menu...')
|
||||||
|
|
||||||
|
|
||||||
def screensaver(name):
|
|
||||||
"""Show screensaver"""
|
|
||||||
LOG.info('Screensaver (%s)', name)
|
|
||||||
if name == 'matrix':
|
|
||||||
cmd = ['cmatrix', '-abs']
|
|
||||||
elif name == 'pipes':
|
|
||||||
cmd = [
|
|
||||||
'pipes.sh',
|
|
||||||
'-t', '0',
|
|
||||||
'-t', '1',
|
|
||||||
'-t', '2',
|
|
||||||
'-t', '3',
|
|
||||||
'-t', '5',
|
|
||||||
'-R', '-r', '4000',
|
|
||||||
]
|
|
||||||
|
|
||||||
# Switch pane to fullscreen and start screensaver
|
|
||||||
tmux.zoom_pane()
|
|
||||||
exe.run_program(cmd, check=False, pipe=False, stderr=subprocess.PIPE)
|
|
||||||
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 == 'Darwin':
|
|
||||||
try:
|
|
||||||
wk_os.mac.set_fans(speed)
|
|
||||||
except (RuntimeError, ValueError, subprocess.CalledProcessError) as err:
|
|
||||||
LOG.error('Failed to set fans to %s', speed)
|
|
||||||
LOG.error('Error: %s', err)
|
|
||||||
std.print_error(f'Failed to set fans to {speed}')
|
|
||||||
for line in str(err).splitlines():
|
|
||||||
std.print_warning(f' {line.strip()}')
|
|
||||||
elif PLATFORM == 'Linux':
|
|
||||||
cmd = ['apple-fans', speed]
|
|
||||||
exe.run_program(cmd, check=False)
|
|
||||||
|
|
||||||
|
|
||||||
def show_results(state):
|
def show_results(state):
|
||||||
"""Show test results by device."""
|
"""Show test results by device."""
|
||||||
std.sleep(0.5)
|
std.sleep(0.5)
|
||||||
|
|
@ -1450,84 +1034,6 @@ def show_results(state):
|
||||||
std.print_standard(' ')
|
std.print_standard(' ')
|
||||||
|
|
||||||
|
|
||||||
def start_mprime(working_dir, log_path):
|
|
||||||
"""Start mprime and save filtered output to log, returns Popen object."""
|
|
||||||
set_apple_fan_speed('max')
|
|
||||||
proc_mprime = subprocess.Popen( # pylint: disable=consider-using-with
|
|
||||||
['mprime', '-t'],
|
|
||||||
cwd=working_dir,
|
|
||||||
stdout=subprocess.PIPE,
|
|
||||||
stderr=subprocess.STDOUT,
|
|
||||||
)
|
|
||||||
proc_grep = subprocess.Popen( # pylint: disable=consider-using-with
|
|
||||||
'grep --ignore-case --invert-match --line-buffered stress.txt'.split(),
|
|
||||||
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
|
|
||||||
|
|
||||||
|
|
||||||
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( # pylint: disable=consider-using-with
|
|
||||||
log_path, 'a', encoding='utf-8',
|
|
||||||
)
|
|
||||||
proc_sysbench = exe.popen_program(sysbench_cmd, stdout=filehandle_sysbench)
|
|
||||||
|
|
||||||
# Done
|
|
||||||
return (proc_sysbench, filehandle_sysbench)
|
|
||||||
|
|
||||||
def stop_mprime(proc_mprime):
|
|
||||||
"""Stop mprime gracefully, then forcefully as needed."""
|
|
||||||
proc_mprime.terminate()
|
|
||||||
try:
|
|
||||||
proc_mprime.wait(timeout=5)
|
|
||||||
except subprocess.TimeoutExpired:
|
|
||||||
proc_mprime.kill()
|
|
||||||
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']
|
||||||
|
|
|
||||||
31
scripts/wk/hw/keyboard.py
Normal file
31
scripts/wk/hw/keyboard.py
Normal file
|
|
@ -0,0 +1,31 @@
|
||||||
|
"""WizardKit: Keyboard test functions"""
|
||||||
|
# vim: sts=2 sw=2 ts=2
|
||||||
|
|
||||||
|
import logging
|
||||||
|
|
||||||
|
from wk.exe import run_program
|
||||||
|
from wk.std import PLATFORM, print_warning
|
||||||
|
|
||||||
|
|
||||||
|
# STATIC VARIABLES
|
||||||
|
LOG = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
# Functions
|
||||||
|
def keyboard_test():
|
||||||
|
"""Test keyboard using OS specific functions."""
|
||||||
|
if PLATFORM == 'Linux':
|
||||||
|
run_xev()
|
||||||
|
else:
|
||||||
|
print_warning(f'Not supported under this OS: {PLATFORM}')
|
||||||
|
|
||||||
|
|
||||||
|
def run_xev():
|
||||||
|
"""Test keyboard using xev."""
|
||||||
|
LOG.info('Keyboard Test (xev)')
|
||||||
|
cmd = ['xev', '-event', 'keyboard']
|
||||||
|
run_program(cmd, check=False, pipe=False)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
print("This file is not meant to be called directly.")
|
||||||
58
scripts/wk/hw/network.py
Normal file
58
scripts/wk/hw/network.py
Normal file
|
|
@ -0,0 +1,58 @@
|
||||||
|
"""WizardKit: Network test functions"""
|
||||||
|
# vim: sts=2 sw=2 ts=2
|
||||||
|
|
||||||
|
import logging
|
||||||
|
|
||||||
|
from wk.net import (
|
||||||
|
connected_to_private_network,
|
||||||
|
ping,
|
||||||
|
show_valid_addresses,
|
||||||
|
speedtest,
|
||||||
|
)
|
||||||
|
from wk.std import (
|
||||||
|
TryAndPrint,
|
||||||
|
pause,
|
||||||
|
print_warning,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# STATIC VARIABLES
|
||||||
|
LOG = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
# Functions
|
||||||
|
def network_test():
|
||||||
|
"""Run network tests."""
|
||||||
|
LOG.info('Network Test')
|
||||||
|
try_and_print = TryAndPrint()
|
||||||
|
result = try_and_print.run(
|
||||||
|
message='Network connection...',
|
||||||
|
function=connected_to_private_network,
|
||||||
|
msg_good='OK',
|
||||||
|
raise_on_error=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Bail if not connected
|
||||||
|
if result['Failed']:
|
||||||
|
print_warning('Please connect to a network and try again')
|
||||||
|
pause('Press Enter to return to main menu...')
|
||||||
|
return
|
||||||
|
|
||||||
|
# Show IP address(es)
|
||||||
|
show_valid_addresses()
|
||||||
|
|
||||||
|
# Ping tests
|
||||||
|
try_and_print.run(
|
||||||
|
'Internet connection...', ping, msg_good='OK', addr='8.8.8.8')
|
||||||
|
try_and_print.run(
|
||||||
|
'DNS resolution...', ping, msg_good='OK', addr='google.com')
|
||||||
|
|
||||||
|
# Speedtest
|
||||||
|
try_and_print.run('Speedtest...', speedtest)
|
||||||
|
|
||||||
|
# Done
|
||||||
|
pause('Press Enter to return to main menu...')
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
print("This file is not meant to be called directly.")
|
||||||
40
scripts/wk/hw/screensavers.py
Normal file
40
scripts/wk/hw/screensavers.py
Normal file
|
|
@ -0,0 +1,40 @@
|
||||||
|
"""WizardKit: Screensaver functions"""
|
||||||
|
# vim: sts=2 sw=2 ts=2
|
||||||
|
|
||||||
|
import logging
|
||||||
|
|
||||||
|
from subprocess import PIPE
|
||||||
|
|
||||||
|
from wk.exe import run_program
|
||||||
|
from wk.tmux import zoom_pane as tmux_zoom_pane
|
||||||
|
|
||||||
|
|
||||||
|
# STATIC VARIABLES
|
||||||
|
LOG = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
# Functions
|
||||||
|
def screensaver(name):
|
||||||
|
"""Show screensaver"""
|
||||||
|
LOG.info('Screensaver (%s)', name)
|
||||||
|
if name == 'matrix':
|
||||||
|
cmd = ['cmatrix', '-abs']
|
||||||
|
elif name == 'pipes':
|
||||||
|
cmd = [
|
||||||
|
'pipes.sh',
|
||||||
|
'-t', '0',
|
||||||
|
'-t', '1',
|
||||||
|
'-t', '2',
|
||||||
|
'-t', '3',
|
||||||
|
'-t', '5',
|
||||||
|
'-R', '-r', '4000',
|
||||||
|
]
|
||||||
|
|
||||||
|
# Switch pane to fullscreen and start screensaver
|
||||||
|
tmux_zoom_pane()
|
||||||
|
run_program(cmd, check=False, pipe=False, stderr=PIPE)
|
||||||
|
tmux_zoom_pane()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
print("This file is not meant to be called directly.")
|
||||||
85
scripts/wk/hw/surface_scan.py
Normal file
85
scripts/wk/hw/surface_scan.py
Normal file
|
|
@ -0,0 +1,85 @@
|
||||||
|
"""WizardKit: Surface scan test functions"""
|
||||||
|
# vim: sts=2 sw=2 ts=2
|
||||||
|
|
||||||
|
import logging
|
||||||
|
|
||||||
|
from subprocess import STDOUT
|
||||||
|
|
||||||
|
from wk.cfg.hw import BADBLOCKS_LARGE_DISK, BADBLOCKS_REGEX
|
||||||
|
from wk.exe import run_program
|
||||||
|
from wk.std import (
|
||||||
|
PLATFORM,
|
||||||
|
bytes_to_string,
|
||||||
|
color_string,
|
||||||
|
strip_colors,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# STATIC VARIABLES
|
||||||
|
LOG = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
# Functions
|
||||||
|
def run_scan(test_obj, log_path):
|
||||||
|
"""Run surface scan and handle exceptions."""
|
||||||
|
block_size = '1024'
|
||||||
|
dev = test_obj.dev
|
||||||
|
dev_path = test_obj.dev.path
|
||||||
|
if PLATFORM == 'Darwin':
|
||||||
|
# Use "RAW" disks under macOS
|
||||||
|
dev_path = dev_path.with_name(f'r{dev_path.name}')
|
||||||
|
LOG.info('Using %s for better performance', dev_path)
|
||||||
|
test_obj.report.append(color_string('badblocks', 'BLUE'))
|
||||||
|
test_obj.set_status('Working')
|
||||||
|
|
||||||
|
# Increase block size if necessary
|
||||||
|
if (dev.phy_sec == 4096
|
||||||
|
or dev.size >= BADBLOCKS_LARGE_DISK):
|
||||||
|
block_size = '4096'
|
||||||
|
|
||||||
|
# Start scan
|
||||||
|
cmd = ['sudo', 'badblocks', '-sv', '-b', block_size, '-e', '1', dev_path]
|
||||||
|
with open(log_path, 'a', encoding='utf-8') as _f:
|
||||||
|
size_str = bytes_to_string(dev.size, use_binary=False)
|
||||||
|
_f.write(
|
||||||
|
color_string(
|
||||||
|
['[', dev.path.name, ' ', size_str, ']\n'],
|
||||||
|
[None, 'BLUE', None, 'CYAN', None],
|
||||||
|
sep='',
|
||||||
|
),
|
||||||
|
)
|
||||||
|
_f.flush()
|
||||||
|
run_program(
|
||||||
|
cmd,
|
||||||
|
check=False,
|
||||||
|
pipe=False,
|
||||||
|
stderr=STDOUT,
|
||||||
|
stdout=_f,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Check results
|
||||||
|
with open(log_path, 'r', encoding='utf-8') as _f:
|
||||||
|
for line in _f.readlines():
|
||||||
|
line = strip_colors(line.strip())
|
||||||
|
if not line or line.startswith('Checking') or line.startswith('['):
|
||||||
|
# Skip
|
||||||
|
continue
|
||||||
|
match = BADBLOCKS_REGEX.search(line)
|
||||||
|
if match:
|
||||||
|
if all(s == '0' for s in match.groups()):
|
||||||
|
test_obj.passed = True
|
||||||
|
test_obj.report.append(f' {line}')
|
||||||
|
test_obj.set_status('Passed')
|
||||||
|
else:
|
||||||
|
test_obj.failed = True
|
||||||
|
test_obj.report.append(f' {color_string(line, "YELLOW")}')
|
||||||
|
test_obj.set_status('Failed')
|
||||||
|
else:
|
||||||
|
test_obj.report.append(f' {color_string(line, "YELLOW")}')
|
||||||
|
if not (test_obj.passed or test_obj.failed):
|
||||||
|
test_obj.set_status('Unknown')
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
print("This file is not meant to be called directly.")
|
||||||
Loading…
Reference in a new issue