# 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