"""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.cfg.python import DATACLASS_DECORATOR_KWARGS from wk.exe import get_json_from_command, run_program from wk.hw.test import Test from wk.std import ( PLATFORM, bytes_to_string, color_string, string_to_bytes, ) # STATIC VARIABLES LOG = logging.getLogger(__name__) @dataclass(**DATACLASS_DECORATOR_KWARGS) 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(color_string('Device', 'BLUE')) report.append(f' {self.cpu_description}') # Include RAM details report.append(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 report.append(f'Vendor: {get_dmi_info_linux("sys_vendor")}') report.append(f'Name: {get_dmi_info_linux("product_name")}') report.append(f'Serial: {get_dmi_info_linux("product_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.")