# scenario_simulator/utils/logger.py import logging import logging.handlers # For RotatingFileHandler import tkinter as tk from tkinter.scrolledtext import ScrolledText from queue import Queue, Empty as QueueEmpty from typing import Optional, Dict, Any # --- Module-level globals for the centralized logging queue system --- _global_log_queue: Optional[Queue[logging.LogRecord]] = None _actual_console_handler: Optional[logging.StreamHandler] = None _actual_file_handler: Optional[logging.handlers.RotatingFileHandler] = None _actual_tkinter_handler: Optional["TkinterTextHandler"] = None _log_processor_after_id: Optional[str] = None _logging_system_active: bool = False _tk_root_instance_for_processing: Optional[tk.Tk] = None _base_formatter: Optional[logging.Formatter] = None GLOBAL_LOG_QUEUE_POLL_INTERVAL_MS = 100 class TkinterTextHandler(logging.Handler): """ A logging handler that directs log messages to a Tkinter Text widget. This handler is called directly from the GUI thread's processing loop. """ def __init__(self, text_widget: tk.Text, level_colors: Dict[int, str]): super().__init__() self.text_widget = text_widget self.level_colors = level_colors self._configure_tags() def _configure_tags(self): for level, color_value in self.level_colors.items(): level_name = logging.getLevelName(level) if color_value: try: self.text_widget.tag_config(level_name, foreground=color_value) except tk.TclError: pass # Widget might not be ready def emit(self, record: logging.LogRecord): try: if not self.text_widget.winfo_exists(): return msg = self.format(record) level_name = record.levelname self.text_widget.configure(state=tk.NORMAL) self.text_widget.insert(tk.END, msg + "\n", (level_name,)) self.text_widget.configure(state=tk.DISABLED) self.text_widget.see(tk.END) except Exception as e: print(f"Error in TkinterTextHandler.emit: {e}", flush=True) class QueuePuttingHandler(logging.Handler): """ A simple handler that puts any received LogRecord into a global queue. """ def __init__(self, handler_queue: Queue[logging.LogRecord]): super().__init__() self.handler_queue = handler_queue def emit(self, record: logging.LogRecord): self.handler_queue.put_nowait(record) def _process_global_log_queue(): """ GUI Thread: Periodically processes LogRecords from the _global_log_queue and dispatches them to the actual configured handlers. """ global _logging_system_active, _log_processor_after_id if ( not _logging_system_active or not _tk_root_instance_for_processing or not _tk_root_instance_for_processing.winfo_exists() ): return try: while _global_log_queue and not _global_log_queue.empty(): record = _global_log_queue.get_nowait() if _actual_console_handler: _actual_console_handler.handle(record) if _actual_file_handler: _actual_file_handler.handle(record) if _actual_tkinter_handler: _actual_tkinter_handler.handle(record) _global_log_queue.task_done() except QueueEmpty: pass except Exception as e: print(f"Error in log processing queue: {e}", flush=True) finally: if _logging_system_active: _log_processor_after_id = _tk_root_instance_for_processing.after( GLOBAL_LOG_QUEUE_POLL_INTERVAL_MS, _process_global_log_queue ) def setup_basic_logging( root_tk_instance_for_processor: tk.Tk, logging_config_dict: Optional[Dict[str, Any]] = None, ): global _global_log_queue, _actual_console_handler, _actual_file_handler, _logging_system_active global _tk_root_instance_for_processing, _log_processor_after_id, _base_formatter if _logging_system_active: return if logging_config_dict is None: logging_config_dict = {} log_format_str = logging_config_dict.get( "format", "% (asctime)s [%(levelname)-8s] %(name)-25s : %(message)s" ) log_date_format_str = logging_config_dict.get("date_format", "%Y-%m-%d %H:%M:%S") _base_formatter = logging.Formatter(log_format_str, datefmt=log_date_format_str) _global_log_queue = Queue() _tk_root_instance_for_processing = root_tk_instance_for_processor root_logger = logging.getLogger() for handler in root_logger.handlers[:]: root_logger.removeHandler(handler) root_logger.setLevel(logging_config_dict.get("default_root_level", logging.INFO)) if logging_config_dict.get("enable_console", True): _actual_console_handler = logging.StreamHandler() _actual_console_handler.setFormatter(_base_formatter) _actual_console_handler.setLevel(logging.DEBUG) queue_putter = QueuePuttingHandler(handler_queue=_global_log_queue) queue_putter.setLevel(logging.DEBUG) root_logger.addHandler(queue_putter) _logging_system_active = True _log_processor_after_id = _tk_root_instance_for_processing.after( GLOBAL_LOG_QUEUE_POLL_INTERVAL_MS, _process_global_log_queue ) def add_tkinter_handler(gui_log_widget: tk.Text, logging_config_dict: Dict[str, Any]): global _actual_tkinter_handler, _base_formatter if not _logging_system_active or not _base_formatter: return if _actual_tkinter_handler: _actual_tkinter_handler.close() if ( isinstance(gui_log_widget, (tk.Text, ScrolledText)) and gui_log_widget.winfo_exists() ): level_colors = logging_config_dict.get("colors", {}) _actual_tkinter_handler = TkinterTextHandler( text_widget=gui_log_widget, level_colors=level_colors ) _actual_tkinter_handler.setFormatter(_base_formatter) _actual_tkinter_handler.setLevel(logging.DEBUG) logging.getLogger(__name__).info("Tkinter log handler added successfully.") else: print( "ERROR: GUI log widget invalid, cannot add TkinterTextHandler.", flush=True ) def get_logger(name: str) -> logging.Logger: return logging.getLogger(name) def shutdown_logging_system(): global _logging_system_active, _log_processor_after_id if not _logging_system_active: return _logging_system_active = False if ( _log_processor_after_id and _tk_root_instance_for_processing and _tk_root_instance_for_processing.winfo_exists() ): _tk_root_instance_for_processing.after_cancel(_log_processor_after_id) # Final flush of the queue _process_global_log_queue() logging.shutdown()