import os import sys import logging import threading import traceback import subprocess import applescript from pathlib import Path from resources import constants 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() FOR DEVELOPERS: - Do not invoke logging until after '_attempt_initialize_logging_configuration()' has been invoked """ def __init__(self, global_constants: constants.Constants) -> None: self.log_filename: str = "OpenCore-Patcher.log" self.log_filepath: Path = None self.constants: constants.Constants = global_constants 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() self._fix_file_permission() def _initialize_logging_path(self) -> None: """ 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) -> None: """ 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 _fix_file_permission(self) -> None: """ Fixes file permission for log file If OCLP was invoked as root, file permission will only allow root to write to log file This in turn breaks normal OCLP execution to write to log file """ if os.geteuid() != 0: return result = subprocess.run(["chmod", "777", self.log_filepath], capture_output=True) if result.returncode != 0: print(f"- Failed to fix log file permissions") if result.stderr: print(result.stderr.decode("utf-8")) def _initialize_logging_configuration(self, log_to_file: bool = True) -> None: """ 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 Parameters: log_to_file (bool): Whether to log to file or not """ 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) -> None: """ 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) -> None: """ Reroute traceback to logging module """ def custom_excepthook(type, value, tb) -> None: """ Reroute traceback in main thread to logging module """ logging.error("Uncaught exception in main thread", exc_info=(type, value, tb)) error_msg = f"OpenCore Legacy Patcher encountered the following internal error:\n\n" error_msg += f"{type.__name__}: {value}" if tb: error_msg += f"\n\n{traceback.extract_tb(tb)[-1]}" error_msg += "\n\nPlease report this error on our Discord server." applescript.AppleScript(f'display dialog "{error_msg}" with title "OpenCore Legacy Patcher ({self.constants.patcher_version})" buttons {{"OK"}} default button "OK" with icon caution giving up after 30').run() def custom_thread_excepthook(args) -> None: """ 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) -> None: """ Restore original traceback handlers """ sys.excepthook = self.original_excepthook threading.excepthook = self.original_thread_excepthook