Files
OpenCore-Legacy-Patcher/resources/logging_handler.py
2023-05-24 12:24:09 -06:00

237 lines
7.6 KiB
Python

import wx
import os
import sys
import time
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.constants: constants.Constants = global_constants
self.log_filename: str = f"OpenCore-Patcher-{self.constants.patcher_version}.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 # 1 MB
self.file_size_redline: int = 1024 * 1024 - 1024 * 100 # 900 KB, when to start cleaning log file
self._initialize_logging_path()
self._clean_log_file()
self._attempt_initialize_logging_configuration()
self._start_logging()
self._implement_custom_traceback_handler()
self._fix_file_permission()
self._clean_prior_version_logs()
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
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 _clean_prior_version_logs(self) -> None:
"""
Clean logs from old Patcher versions
If file is more than a week old, assume it's unused and delete it
"""
time_threshold = time.time() - 60 * 60 * 24 * 7
for file in self.log_filepath.parent.glob("OpenCore-Patcher*"):
if not file.is_file():
continue
if not file.name.endswith(".log"):
continue
if file.name == self.log_filename:
continue
if file.stat().st_mtime < time_threshold:
try:
file.unlink()
except Exception as e:
print(f"Failed to clean prior version 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:
logging.error(f"Failed to fix log file permissions")
if result.stdout:
logging.error("STDOUT:")
logging.error(result.stdout.decode("utf-8"))
if result.stderr:
logging.error("STDERR:")
logging.error(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)-32s] [%(lineno)-4d]: %(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 _start_logging(self):
"""
Start logging, used as easily identifiable start point in logs
"""
str_msg = f"# OpenCore Legacy Patcher ({self.constants.patcher_version}) #"
str_len = len(str_msg)
logging.info('#' * str_len)
logging.info(str_msg)
logging.info('#' * str_len)
logging.info(f"Log file set to: {self.log_filepath}")
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))
if self.constants.cli_mode is True:
return
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