SXXXXXXX_RepoSync/reposync/utility/logger.py
2025-07-11 08:27:29 +02:00

176 lines
6.2 KiB
Python

# RepoSync/utility/logger.py
"""
Configures and provides access to the application-wide logging system.
This module sets up logging to the console, a file, and a thread-safe
Tkinter widget. It is designed to be configured once at application startup.
"""
import logging
import tkinter as tk
from logging.handlers import RotatingFileHandler
from queue import Queue, Empty as QueueEmpty
from tkinter.scrolledtext import ScrolledText
from typing import Optional, Dict, Any
_tkinter_handler_instance: Optional["TkinterTextHandler"] = None
class TkinterTextHandler(logging.Handler):
"""
A logging handler that directs records to a Tkinter Text widget.
It uses a thread-safe queue to avoid blocking the GUI and updates the
widget periodically via the Tkinter event loop (`root.after`).
"""
def __init__(
self,
text_widget: tk.Text,
root_tk_instance: tk.Tk,
level_colors: Dict[int, str],
queue_poll_interval_ms: int,
):
super().__init__()
self.text_widget = text_widget
self.root_tk_instance = root_tk_instance
self.log_queue = Queue()
self.level_colors = level_colors
self.queue_poll_interval_ms = queue_poll_interval_ms
self._after_id: Optional[str] = None
self._is_active = True
if not (self.text_widget and self.text_widget.winfo_exists()):
print("Warning: TkinterTextHandler initialized with invalid text_widget.")
self._is_active = False
return
for level, color in self.level_colors.items():
tag_name = logging.getLevelName(level)
try:
self.text_widget.tag_config(tag_name, foreground=color)
except tk.TclError as e:
print(f"Warning: Could not configure tag '{tag_name}': {e}")
self._process_log_queue()
def emit(self, record: logging.LogRecord):
if self._is_active:
self.log_queue.put_nowait((record.levelname, self.format(record)))
def _process_log_queue(self):
if not self._is_active:
return
try:
while True:
level_name, msg = self.log_queue.get_nowait()
if self.text_widget.winfo_exists():
self.text_widget.configure(state=tk.NORMAL)
self.text_widget.insert(tk.END, msg + "\n", (level_name,))
self.text_widget.see(tk.END)
self.text_widget.configure(state=tk.DISABLED)
else:
self.close()
return
self.log_queue.task_done()
except QueueEmpty:
pass
except Exception as e:
print(f"Error updating log widget: {e}")
self.close()
return
self._after_id = self.root_tk_instance.after(
self.queue_poll_interval_ms, self._process_log_queue
)
def close(self):
self._is_active = False
if self._after_id:
try:
if self.root_tk_instance.winfo_exists():
self.root_tk_instance.after_cancel(self._after_id)
except tk.TclError:
pass
self._after_id = None
super().close()
def setup_logging(
gui_log_widget: Optional[tk.Text] = None,
root_tk_instance: Optional[tk.Tk] = None,
config: Optional[Dict[str, Any]] = None,
):
"""
Sets up application-wide logging based on a configuration dictionary.
Handles partial configurations by merging them with a set of defaults.
"""
default_config = {
"level": logging.INFO,
"format": "%(asctime)s [%(levelname)-8s] (%(name)s) %(message)s",
"date_format": "%H:%M:%S", "enable_console": True, "enable_file": False,
"file_path": "RepoSync.log", "file_max_bytes": 5 * 1024 * 1024,
"file_backup_count": 3,
"colors": {
logging.DEBUG: "gray", logging.INFO: "black", logging.WARNING: "orange",
logging.ERROR: "red", logging.CRITICAL: "purple",
},
"queue_poll_interval_ms": 100,
}
if config:
default_config.update(config)
final_config = default_config
formatter = logging.Formatter(
final_config["format"], datefmt=final_config["date_format"]
)
root_logger = logging.getLogger()
root_logger.setLevel(final_config["level"])
# Clear any previous handlers to prevent duplication
for handler in root_logger.handlers[:]:
handler.close()
root_logger.removeHandler(handler)
if final_config["enable_console"]:
console_handler = logging.StreamHandler()
console_handler.setFormatter(formatter)
root_logger.addHandler(console_handler)
if final_config["enable_file"]:
file_handler = RotatingFileHandler(
final_config["file_path"], maxBytes=final_config["file_max_bytes"],
backupCount=final_config["file_backup_count"], encoding="utf-8",
)
file_handler.setFormatter(formatter)
root_logger.addHandler(file_handler)
if gui_log_widget and root_tk_instance:
if gui_log_widget.winfo_exists() and root_tk_instance.winfo_exists():
gui_handler = TkinterTextHandler(
text_widget=gui_log_widget, root_tk_instance=root_tk_instance,
level_colors=final_config["colors"],
queue_poll_interval_ms=final_config["queue_poll_interval_ms"],
)
gui_handler.setFormatter(formatter)
root_logger.addHandler(gui_handler)
else:
print("Warning: Tkinter root or widget does not exist. GUI logger not created.")
# This is the definitive fix for the shutdown race condition.
# By replacing the official shutdown function with a no-op, we prevent
# the logging module's internal locks from being destroyed while thread
# finalizers might still need them. The OS will handle file cleanup.
logging.shutdown = lambda: None
def shutdown_logging():
"""
This function is now effectively obsolete for the main application shutdown
but is kept for potential other uses. The real shutdown is prevented by
the monkey patch in setup_logging.
"""
pass