import logging import sys import threading from pathlib import Path class InitializeLoggingSupport: """ Initialize logging framework for program Primary responsibilities: - Determine where to store log file - Clean log file if it's near the max file size - Initialize logging framework configuration - Implement custom traceback handler - Implement error handling for file write Usage: >>> from resources.logging_handler import InitializeLoggingSupport >>> InitializeLoggingSupport() """ def __init__(self) -> None: self.log_filename: str = "OpenCore-Patcher.log" self.log_filepath: Path = None self.original_excepthook: sys = sys.excepthook self.original_thread_excepthook: threading = threading.excepthook self.max_file_size: int = 1024 * 1024 * 10 # 10 MB self.file_size_redline: int = 1024 * 1024 * 9 # 9 MB, when to start cleaning log file self._initialize_logging_path() self._clean_log_file() self._attempt_initialize_logging_configuration() self._implement_custom_traceback_handler() def __del__(self): self._restore_original_excepthook() def _initialize_logging_path(self): """ Initialize logging framework storage path """ self.log_filepath = Path(f"~/Library/Logs/{self.log_filename}").expanduser() if not self.log_filepath.parent.exists(): # Likely in an installer environment, store in /Users/Shared self.log_filepath = Path("/Users/Shared") / self.log_filename print("- Initializing logging framework...") print(f" - Log file: {self.log_filepath}") def _clean_log_file(self): """ Determine if log file should be cleaned We check if we're near the max file size, and if so, we clean the log file """ if not self.log_filepath.exists(): return if self.log_filepath.stat().st_size < self.file_size_redline: return # Check if backup log file exists backup_log_filepath = self.log_filepath.with_suffix(".old.log") try: if backup_log_filepath.exists(): backup_log_filepath.unlink() # Rename current log file to backup log file self.log_filepath.rename(backup_log_filepath) except Exception as e: print(f"- Failed to clean log file: {e}") def _initialize_logging_configuration(self, log_to_file: bool = True): """ Initialize logging framework configuration StreamHandler's format is used to mimic the default behavior of print() While FileHandler's format is for more in-depth logging """ logging.basicConfig( level=logging.NOTSET, format="%(asctime)s - %(filename)s (%(lineno)d): %(message)s", handlers=[ logging.StreamHandler(stream = sys.stdout), logging.FileHandler(self.log_filepath) if log_to_file is True else logging.NullHandler() ], ) logging.getLogger().setLevel(logging.INFO) logging.getLogger().handlers[0].setFormatter(logging.Formatter("%(message)s")) logging.getLogger().handlers[1].maxBytes = self.max_file_size def _attempt_initialize_logging_configuration(self): """ Attempt to initialize logging framework configuration If we fail to initialize the logging framework, we will disable logging to file """ try: self._initialize_logging_configuration() except Exception as e: print(f"- Failed to initialize logging framework: {e}") print("- Retrying without logging to file...") self._initialize_logging_configuration(log_to_file=False) def _implement_custom_traceback_handler(self): """ Reroute traceback to logging module """ def custom_excepthook(type, value, tb): """ Reroute traceback in main thread to logging module """ logging.error("Uncaught exception in main thread", exc_info=(type, value, tb)) def custom_thread_excepthook(args): """ Reroute traceback in spawned thread to logging module """ logging.error("Uncaught exception in spawned thread", exc_info=(args)) sys.excepthook = custom_excepthook threading.excepthook = custom_thread_excepthook def _restore_original_excepthook(self): """ Restore original traceback handlers """ sys.excepthook = self.original_excepthook threading.excepthook = self.original_thread_excepthook