"""WizardKit: Debug Functions""" # vim: sts=2 sw=2 ts=2 import inspect import logging import lzma import os import pathlib import pickle import platform import re import socket import sys import time from typing import Any import requests from wk.cfg.net import CRASH_SERVER from wk.log import get_root_logger_path # Classes class Debug(): """Object used when dumping debug data.""" def method(self) -> None: """Dummy method used to identify functions vs data.""" # STATIC VARIABLES LOG = logging.getLogger(__name__) DEBUG_CLASS = Debug() METHOD_TYPE = type(DEBUG_CLASS.method) # Functions def generate_debug_report() -> str: """Generate debug report, returns str.""" platform_function_list = ( 'architecture', 'machine', 'platform', 'python_version', ) report = [] # Logging data try: log_path = get_root_logger_path() except RuntimeError: # Assuming logging wasn't started pass else: report.append('------ Start Log -------') report.append('') with open(log_path, 'r', encoding='utf-8') as log_file: report.extend(log_file.read().splitlines()) report.append('') report.append('------- End Log --------') # System report.append('--- Start debug info ---') report.append('') report.append('[System]') report.append(f' {"FQDN":<24} {socket.getfqdn()}') for func in platform_function_list: func_name = func.replace('_', ' ').capitalize() func_result = getattr(platform, func)() report.append(f' {func_name:<24} {func_result}') report.append(f' {"Python sys.argv":<24} {sys.argv}') report.append('') # Environment report.append('[Environment Variables]') for key, value in sorted(os.environ.items()): report.append(f' {key:<24} {value}') report.append('') # Done report.append('---- End debug info ----') return '\n'.join(report) def generate_object_report(obj: Any, indent: int = 0) -> list[str]: """Generate debug report for obj, returns list.""" report = [] attr_list = [] # Get attribute list if hasattr(obj, '__slots__'): attr_list = list(obj.__slots__) else: attr_list = [name for name in dir(obj) if not name.startswith('_')] # Dump object data for name in attr_list: attr = getattr(obj, name) # Skip methods if isinstance(attr, METHOD_TYPE): continue # Add attribute to report (expanded if necessary) if isinstance(attr, dict): report.append(f'{name}:') for key, value in attr.items(): report.append(f'{" "*(indent+1)}{key}: {str(value)}') else: report.append(f'{" "*indent}{name}: {str(attr)}') # Done return report def save_pickles( obj_dict: dict[Any, Any], out_path: pathlib.Path | str | None = None, ) -> None: """Save dict of objects using pickle.""" LOG.info('Saving pickles') # Set path if not out_path: out_path = get_root_logger_path() out_path = out_path.parent.joinpath('../debug').resolve() # Save pickles try: for name, obj in obj_dict.copy().items(): if name.startswith('__') or inspect.ismodule(obj): continue with open(f'{out_path}/{name}.pickle', 'wb') as _f: pickle.dump(obj, _f, protocol=pickle.HIGHEST_PROTOCOL) except Exception: LOG.error('Failed to save all the pickles', exc_info=True) def upload_debug_report( report: str, compress: bool = True, reason: str = 'DEBUG', ) -> None: """Upload debug report to CRASH_SERVER as specified in wk.cfg.main.""" LOG.info('Uploading debug report to %s', CRASH_SERVER.get('Name', '?')) headers = CRASH_SERVER.get('Headers', {'X-Requested-With': 'XMLHttpRequest'}) if compress: headers['Content-Type'] = 'application/octet-stream' # Check if the required server details are available if not all(CRASH_SERVER.get(key, False) for key in ('Name', 'Url', 'User')): msg = 'Server details missing, aborting upload.' print(msg) raise RuntimeError(msg) # Set filename (based on the logging config if possible) filename = 'Unknown' try: log_path = get_root_logger_path() except RuntimeError: # Assuming logging wasn't started pass else: # Strip everything but the prefix filename = re.sub(r'^(.*)_(\d{4}-\d{2}-\d{2}.*)', r'\1', log_path.name) filename = f'{filename}_{reason}_{time.strftime("%Y-%m-%d_%H%M%S%z")}.log' LOG.debug('filename: %s', filename) # Compress report if compress: filename += '.xz' xz_report = lzma.compress(report.encode('utf8')) # Upload report url = f'{CRASH_SERVER["Url"]}/{filename}' response = requests.put( url, auth=(CRASH_SERVER['User'], CRASH_SERVER.get('Pass', '')), data=xz_report if compress else report, headers=headers, timeout=60, ) # Check response if not response.ok: raise RuntimeError('Failed to upload report') if __name__ == '__main__': print("This file is not meant to be called directly.")