311 lines
8.8 KiB
Python
311 lines
8.8 KiB
Python
"""WizardKit: System object and functions"""
|
|
# vim: sts=2 sw=2 ts=2
|
|
|
|
import logging
|
|
import os
|
|
import plistlib
|
|
import re
|
|
|
|
from dataclasses import dataclass, field
|
|
from typing import Any
|
|
|
|
from wk.cfg.hw import KNOWN_RAM_VENDOR_IDS
|
|
from wk.exe import get_json_from_command, run_program
|
|
from wk.hw.test import Test
|
|
from wk.std import PLATFORM, bytes_to_string, string_to_bytes
|
|
from wk.ui import ansi
|
|
|
|
|
|
# STATIC VARIABLES
|
|
LOG = logging.getLogger(__name__)
|
|
EVERYMAC_URL = 'https://everymac.com/ultimate-mac-lookup/?search_keywords='
|
|
|
|
|
|
@dataclass(slots=True)
|
|
class System:
|
|
"""Object for tracking system specific hardware data."""
|
|
cpu_description: str = field(init=False)
|
|
ram_dimms: list[str] = field(init=False, default_factory=list)
|
|
ram_total: str = field(init=False, default='Unknown')
|
|
raw_details: dict[Any, Any] = field(init=False, default_factory=dict)
|
|
tests: list[Test] = field(init=False, default_factory=list)
|
|
|
|
def __post_init__(self) -> None:
|
|
self.get_cpu_details()
|
|
self.set_cpu_description()
|
|
self.get_ram_details()
|
|
|
|
def generate_cpu_ram_report(self) -> list[str]:
|
|
"""Generate CPU & RAM report, returns list."""
|
|
report = []
|
|
report.append(ansi.color_string('Device', 'BLUE'))
|
|
report.append(f' {self.cpu_description}')
|
|
|
|
# Include RAM details
|
|
report.append(ansi.color_string('RAM', 'BLUE'))
|
|
report.append(f' {self.ram_total} ({", ".join(self.ram_dimms)})')
|
|
|
|
# Tests
|
|
for test in self.tests:
|
|
report.extend(test.report)
|
|
|
|
return report
|
|
|
|
def generate_full_report(self) -> list[str]:
|
|
"""Generate full report, returns list."""
|
|
report = ['[System]']
|
|
report.extend([f'... {line}' for line in self.get_system_info()])
|
|
report.append('\n[Motherboard]')
|
|
report.extend([f'... {line}' for line in self.get_mobo_info()])
|
|
report.append('\n[BIOS]')
|
|
report.extend([f'... {line}' for line in self.get_bios_info()])
|
|
report.append('\n[CPU]')
|
|
report.append(f'... {self.cpu_description}')
|
|
report.append('\n[RAM]')
|
|
report.append(f'... {self.ram_total} ({", ".join(self.ram_dimms)})')
|
|
report.append('\n[GPU]')
|
|
report.extend([f'... {line}' for line in self.get_gpu_info()])
|
|
return report
|
|
|
|
def get_bios_info(self) -> list[str]:
|
|
"""Get BIOS details, returns list."""
|
|
report = []
|
|
|
|
# Bail early
|
|
if PLATFORM != 'Linux':
|
|
# Only Linux is supported ATM
|
|
return report
|
|
|
|
# Get details
|
|
report.append(f'Version: {get_dmi_info_linux("bios_version")}')
|
|
report.append(f'Released: {get_dmi_info_linux("bios_date")}')
|
|
|
|
# Done
|
|
return report
|
|
|
|
def get_cpu_details(self) -> None:
|
|
"""Get CPU details using OS specific methods."""
|
|
cmd = ['lscpu', '--json']
|
|
|
|
# Bail early
|
|
if PLATFORM != 'Linux':
|
|
# Only Linux is supported ATM
|
|
return
|
|
|
|
# Parse details
|
|
json_data = get_json_from_command(cmd)
|
|
for line in json_data.get('lscpu', [{}]):
|
|
_field = line.get('field', '').replace(':', '')
|
|
_data = line.get('data', '')
|
|
if not (_field or _data):
|
|
# Skip
|
|
continue
|
|
self.raw_details[_field] = _data
|
|
|
|
def get_gpu_info(self) -> list[str]:
|
|
"""Get GPU details, returns list."""
|
|
report = []
|
|
|
|
# Bail early
|
|
if PLATFORM != 'Linux':
|
|
# Only Linux is supported ATM
|
|
return report
|
|
|
|
# Get PCI details
|
|
proc = run_program(['lspci'])
|
|
for line in proc.stdout.splitlines():
|
|
if 'VGA' not in line:
|
|
continue
|
|
line = re.sub('^.*:', '', line)
|
|
line = re.sub('Integrated Graphics Controller.*', 'iGPU', line)
|
|
line = line.replace('Advanced Micro Devices, Inc.', 'AMD')
|
|
line = line.replace('Intel Corporation', 'Intel')
|
|
line = line.replace('Generation Core Processor Family', 'Gen')
|
|
report.append(f'{line.strip()}')
|
|
|
|
# Get GLX info
|
|
if 'DISPLAY' in os.environ or 'WAYLAND_DISPLAY' in os.environ:
|
|
proc = run_program(['glxinfo'])
|
|
for line in proc.stdout.splitlines():
|
|
if 'OpenGL renderer' in line:
|
|
line = re.sub('^.*:', '', line)
|
|
report.append(line.strip())
|
|
break
|
|
|
|
# Done
|
|
return report
|
|
|
|
def get_mobo_info(self) -> list[str]:
|
|
"""Get motherboard details, returns list."""
|
|
report = []
|
|
|
|
# Bail early
|
|
if PLATFORM != 'Linux':
|
|
# Only Linux is supported ATM
|
|
return report
|
|
|
|
# Get details
|
|
report.append(f'Vendor: {get_dmi_info_linux("board_vendor")}')
|
|
report.append(f'Name: {get_dmi_info_linux("board_name")}')
|
|
report.append(f'Version: {get_dmi_info_linux("board_version")}')
|
|
report.append(f'Serial: {get_dmi_info_linux("board_serial")}')
|
|
|
|
# Done
|
|
return report
|
|
|
|
def get_system_info(self) -> list[str]:
|
|
"""Get system details, returns list."""
|
|
report = []
|
|
|
|
# Bail early
|
|
if PLATFORM != 'Linux':
|
|
# Only Linux is supported ATM
|
|
return report
|
|
|
|
# Get details
|
|
vendor = get_dmi_info_linux("sys_vendor")
|
|
serial = get_dmi_info_linux("product_serial")
|
|
report.append(f'Vendor: {vendor}')
|
|
report.append(f'Name: {get_dmi_info_linux("product_name")}')
|
|
report.append(f'Serial: {serial}')
|
|
if 'apple' in vendor.lower():
|
|
report.append(f'{EVERYMAC_URL}{serial}')
|
|
|
|
# Done
|
|
return report
|
|
|
|
def get_ram_details(self) -> None:
|
|
"""Get RAM details using OS specific methods."""
|
|
if PLATFORM == 'Darwin':
|
|
dimm_list = get_ram_list_macos()
|
|
elif PLATFORM == 'Linux':
|
|
dimm_list = get_ram_list_linux()
|
|
|
|
details = {'Total': 0}
|
|
for dimm_details in dimm_list:
|
|
size, manufacturer = dimm_details
|
|
if size <= 0:
|
|
# Skip empty DIMMs
|
|
continue
|
|
description = f'{bytes_to_string(size)} {manufacturer}'
|
|
details['Total'] += size
|
|
if description in details:
|
|
details[description] += 1
|
|
else:
|
|
details[description] = 1
|
|
|
|
# Save details
|
|
self.ram_total = bytes_to_string(details.pop('Total', 0))
|
|
self.ram_dimms = [
|
|
f'{count}x {desc}' for desc, count in sorted(details.items())
|
|
]
|
|
|
|
def set_cpu_description(self) -> None:
|
|
"""Set CPU description."""
|
|
self.cpu_description = self.raw_details.get('Model name', 'Unknown CPU')
|
|
|
|
# macOS
|
|
if PLATFORM == 'Darwin':
|
|
cmd = 'sysctl -n machdep.cpu.brand_string'.split()
|
|
proc = run_program(cmd, check=False)
|
|
self.cpu_description = re.sub(r'\s+', ' ', proc.stdout.strip())
|
|
|
|
|
|
def get_dmi_info_linux(value) -> str:
|
|
"""Get DMI info, returns str."""
|
|
dmi_path = '/sys/devices/virtual/dmi/id'
|
|
cmd = ['sudo', 'cat', f'{dmi_path}/{value}']
|
|
proc = run_program(cmd, check=False)
|
|
if proc.returncode:
|
|
return '[???]'
|
|
return proc.stdout.strip()
|
|
|
|
|
|
def get_ram_list_linux() -> list[list]:
|
|
"""Get RAM list using dmidecode."""
|
|
cmd = ['sudo', 'dmidecode', '--type', 'memory']
|
|
dimm_list = []
|
|
manufacturer = 'Unknown'
|
|
part_number = 'Unknown'
|
|
size = 0
|
|
|
|
# Get DMI data
|
|
proc = run_program(cmd)
|
|
dmi_data = proc.stdout.splitlines()
|
|
|
|
# Parse data
|
|
for line in dmi_data:
|
|
line = line.strip()
|
|
if line == 'Memory Device':
|
|
# Add to list
|
|
if size and (manufacturer or part_number):
|
|
dimm_list.append([size, manufacturer, part_number])
|
|
# Reset vars
|
|
manufacturer = 'Unknown'
|
|
part_number = 'Unknown'
|
|
size = 0
|
|
elif line.startswith('Size:'):
|
|
size = line.replace('Size: ', '')
|
|
try:
|
|
size = string_to_bytes(size, assume_binary=True)
|
|
except ValueError:
|
|
# Assuming empty module
|
|
size = 0
|
|
elif line.startswith('Manufacturer:'):
|
|
manufacturer = line.replace('Manufacturer: ', '')
|
|
elif line.startswith('Part Number: '):
|
|
part_number = line.replace('Part Number: ', '')
|
|
|
|
# Add last DIMM
|
|
if size and (manufacturer or part_number):
|
|
dimm_list.append([size, manufacturer, part_number])
|
|
|
|
# Cleanup list
|
|
dimm_list = [
|
|
[dimm[0], dimm[1] if dimm[1] != 'Unknown' else dimm[2]]
|
|
for dimm in dimm_list
|
|
]
|
|
|
|
# Save details
|
|
return dimm_list
|
|
|
|
|
|
def get_ram_list_macos() -> list[list]:
|
|
"""Get RAM list using system_profiler."""
|
|
dimm_list = []
|
|
|
|
# Get and parse plist data
|
|
cmd = [
|
|
'system_profiler',
|
|
'-xml',
|
|
'SPMemoryDataType',
|
|
]
|
|
proc = run_program(cmd, check=False, encoding=None, errors=None)
|
|
try:
|
|
plist_data = plistlib.loads(proc.stdout)
|
|
except (TypeError, ValueError):
|
|
# Ignore and return an empty list
|
|
return dimm_list
|
|
|
|
# Check DIMM data
|
|
dimm_details = plist_data[0].get('_items', [{}])[0].get('_items', [])
|
|
for dimm in dimm_details:
|
|
manufacturer = dimm.get('dimm_manufacturer', None)
|
|
manufacturer = KNOWN_RAM_VENDOR_IDS.get(
|
|
manufacturer,
|
|
f'Unknown ({manufacturer})')
|
|
size = dimm.get('dimm_size', '0 GB')
|
|
try:
|
|
size = string_to_bytes(size, assume_binary=True)
|
|
except ValueError:
|
|
# Empty DIMM?
|
|
LOG.error('Invalid DIMM size: %s', size)
|
|
continue
|
|
dimm_list.append([size, manufacturer])
|
|
|
|
# Save details
|
|
return dimm_list
|
|
|
|
|
|
if __name__ == '__main__':
|
|
print("This file is not meant to be called directly.")
|