# FlightMonitor/utils/logger.py import logging import tkinter as tk from tkinter.scrolledtext import ScrolledText import threading # Non più necessario qui direttamente, ma potrebbe esserlo in altre parti del modulo from queue import Queue, Empty as QueueEmpty from typing import Optional # Costanti di default per il logging DEFAULT_LOG_FORMAT = "%(asctime)s - %(name)s - %(levelname)s - %(message)s" DEFAULT_LOG_DATE_FORMAT = "%Y-%m-%d %H:%M:%S" DEFAULT_LOG_LEVEL = logging.INFO # Colori di default per i livelli di log LOG_LEVEL_COLORS_DEFAULT = { logging.DEBUG: "RoyalBlue1", logging.INFO: "black", logging.WARNING: "dark orange", logging.ERROR: "red2", logging.CRITICAL: "red4" } # Intervallo per il polling della coda di log nel TkinterTextHandler (in millisecondi) LOG_QUEUE_POLL_INTERVAL_MS = 100 class TkinterTextHandler(logging.Handler): """ A logging handler that directs log messages to a Tkinter Text widget in a thread-safe manner using an internal queue and root.after(). """ def __init__(self, text_widget: tk.Text, root_tk_instance: tk.Tk, # Richiesto per root.after() level_colors: dict): super().__init__() self.text_widget = text_widget self.root_tk_instance = root_tk_instance self.log_queue = Queue() # Coda interna per i messaggi di log self.level_colors = level_colors self._after_id_log_processor: Optional[str] = None self._is_active = True # MODIFICA: Flag per controllare l'attività dell'handler if not (self.text_widget and hasattr(self.text_widget, 'winfo_exists') and self.text_widget.winfo_exists()): print("Warning: TkinterTextHandler initialized with an invalid or non-existent text_widget.") self._is_active = False return if not (self.root_tk_instance and hasattr(self.root_tk_instance, 'winfo_exists') and self.root_tk_instance.winfo_exists()): print("Warning: TkinterTextHandler initialized with an invalid or non-existent root_tk_instance.") self._is_active = False return # Configura i tag per i colori 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: print(f"Warning: Could not configure tag for {level_name} during TkinterTextHandler init.") pass # Può succedere se il widget è in uno stato strano, ma _is_active dovrebbe proteggere # Avvia il processore della coda di log if self._is_active: self._process_log_queue() def emit(self, record: logging.LogRecord): """ Formats a log record and puts it into the internal queue. The actual writing to the widget is handled by _process_log_queue in the GUI thread. """ # MODIFICA: Controlla prima il flag _is_active if not self._is_active: return # Non fare nulla se l'handler è stato disattivato # Non tentare di loggare se il widget o la root sono già stati distrutti if not (self.text_widget and hasattr(self.text_widget, 'winfo_exists') and self.text_widget.winfo_exists() and \ self.root_tk_instance and hasattr(self.root_tk_instance, 'winfo_exists') and self.root_tk_instance.winfo_exists()): # Se il widget non c'è più, il processore della coda smetterà. # Potremmo stampare sulla console come fallback se necessario, # ma i messaggi dovrebbero già andare al console handler. self._is_active = False # Disattiva se i widget non esistono più return try: msg = self.format(record) level_name = record.levelname self.log_queue.put_nowait((level_name, msg)) except Exception as e: # In caso di errore nell'accodamento (raro con Queue di Python se non piena e usiamo put_nowait) # o nella formattazione. print(f"Error in TkinterTextHandler.emit before queueing: {e}") # Fallback a stderr per il record originale # Considera se rimuovere questo fallback se causa problemi o è ridondante # logging.StreamHandler().handle(record) # Attenzione: questo potrebbe causare output duplicato def _process_log_queue(self): """ Processes messages from the internal log queue and writes them to the Text widget. This method is run in the GUI thread via root.after(). """ # MODIFICA: Controlla prima il flag _is_active if not self._is_active: if self._after_id_log_processor: # Assicurati di cancellare l'after se esiste try: if self.root_tk_instance and hasattr(self.root_tk_instance, 'winfo_exists') and self.root_tk_instance.winfo_exists(): self.root_tk_instance.after_cancel(self._after_id_log_processor) except tk.TclError: pass self._after_id_log_processor = None return # Controlla se il widget e la root esistono ancora if not (self.text_widget and hasattr(self.text_widget, 'winfo_exists') and self.text_widget.winfo_exists() and \ self.root_tk_instance and hasattr(self.root_tk_instance, 'winfo_exists') and self.root_tk_instance.winfo_exists()): # print("Debug: TkinterTextHandler._process_log_queue: Widget or root destroyed. Stopping.") if self._after_id_log_processor: try: # Non c'è bisogno di controllare winfo_exists sulla root qui, # perché se fallisce il check sopra, potremmo essere in uno stato di distruzione parziale. self.root_tk_instance.after_cancel(self._after_id_log_processor) except tk.TclError: pass # La root potrebbe essere già andata self._after_id_log_processor = None self._is_active = False # Disattiva l'handler return try: while self._is_active: # MODIFICA: Aggiunto controllo _is_active anche qui try: level_name, msg = self.log_queue.get_nowait() except QueueEmpty: break # Coda vuota 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) self.log_queue.task_done() # Segnala che l'elemento è stato processato except tk.TclError as e: # Errore Tcl durante l'aggiornamento del widget (es. widget distrutto tra il check e l'uso) # print(f"TkinterTextHandler TclError in _process_log_queue: {e}") if self._after_id_log_processor: try: self.root_tk_instance.after_cancel(self._after_id_log_processor) except tk.TclError: pass self._after_id_log_processor = None self._is_active = False # Disattiva l'handler return # Non riprogrammare except Exception as e: print(f"Unexpected error in TkinterTextHandler._process_log_queue: {e}") self._is_active = False # Disattiva in caso di errore grave return # Non riprogrammare # Riprogramma l'esecuzione solo se l'handler è ancora attivo if self._is_active: try: self._after_id_log_processor = self.root_tk_instance.after( LOG_QUEUE_POLL_INTERVAL_MS, self._process_log_queue ) except tk.TclError: # La root è stata distrutta, non possiamo riprogrammare # print("Debug: TkinterTextHandler._process_log_queue: Root destroyed. Cannot reschedule.") self._after_id_log_processor = None self._is_active = False # Disattiva def close(self): """ Cleans up resources, like stopping the log queue processor. Called when the handler is removed or logging system shuts down. """ # MODIFICA: Imposta _is_active a False per fermare qualsiasi ulteriore elaborazione o riprogrammazione. self._is_active = False if self._after_id_log_processor: # Tenta di cancellare il callback solo se la root instance esiste ancora. # Questo previene l'errore Tcl se la root è già stata distrutta # quando logging.shutdown() chiama questo metodo. if self.root_tk_instance and hasattr(self.root_tk_instance, 'winfo_exists') and self.root_tk_instance.winfo_exists(): try: self.root_tk_instance.after_cancel(self._after_id_log_processor) except tk.TclError: # print(f"Debug: TclError during after_cancel in TkinterTextHandler.close (root might be gone or invalid).") pass self._after_id_log_processor = None # Svuota la coda per evitare che task_done() venga chiamato su una coda non vuota # se l'applicazione si chiude bruscamente. while not self.log_queue.empty(): try: self.log_queue.get_nowait() self.log_queue.task_done() except QueueEmpty: break except Exception: # In caso di altri problemi con la coda durante lo svuotamento break super().close() _tkinter_handler_instance: Optional[TkinterTextHandler] = None def setup_logging(gui_log_widget: Optional[tk.Text] = None, root_tk_instance: Optional[tk.Tk] = None): # Aggiunto root_tk_instance """ Sets up application-wide logging. """ global _tkinter_handler_instance # MODIFICA: Spostato l'import qui per evitare import ciclici se config usasse il logger from ..data import config as app_config log_level_str = getattr(app_config, 'LOG_LEVEL', 'INFO').upper() log_level = getattr(logging, log_level_str, DEFAULT_LOG_LEVEL) log_format_str = getattr(app_config, 'LOG_FORMAT', DEFAULT_LOG_FORMAT) log_date_format_str = getattr(app_config, 'LOG_DATE_FORMAT', DEFAULT_LOG_DATE_FORMAT) formatter = logging.Formatter(log_format_str, datefmt=log_date_format_str) configured_level_colors = { level: getattr(app_config, f'LOG_COLOR_{logging.getLevelName(level).upper()}', default_color) for level, default_color in LOG_LEVEL_COLORS_DEFAULT.items() } root_logger = logging.getLogger() # Rimuovi tutti gli handler esistenti per una configurazione pulita (opzionale, ma spesso utile) # for handler in root_logger.handlers[:]: # root_logger.removeHandler(handler) # handler.close() root_logger.setLevel(log_level) # Reimposta il livello del root logger # Assicurati che _tkinter_handler_instance sia gestito correttamente if _tkinter_handler_instance: if _tkinter_handler_instance in root_logger.handlers: root_logger.removeHandler(_tkinter_handler_instance) _tkinter_handler_instance.close() _tkinter_handler_instance = None # Configura il console handler se non già presente # Questo controllo è un po' più robusto per evitare handler duplicati alla console has_console_handler = any( isinstance(h, logging.StreamHandler) and not isinstance(h, TkinterTextHandler) for h in root_logger.handlers ) if not has_console_handler: console_handler = logging.StreamHandler() console_handler.setFormatter(formatter) # Imposta un livello per il console handler, potrebbe essere diverso dal root logger # console_handler.setLevel(log_level) root_logger.addHandler(console_handler) if gui_log_widget and root_tk_instance: if not (isinstance(gui_log_widget, tk.Text) or isinstance(gui_log_widget, ScrolledText)): # Usa print o un logger di fallback se il logger principale non è ancora pronto print(f"ERROR: GUI log widget is not a valid tk.Text or ScrolledText instance: {type(gui_log_widget)}") elif not (hasattr(gui_log_widget, 'winfo_exists') and gui_log_widget.winfo_exists()): print("WARNING: GUI log widget provided to setup_logging does not exist (winfo_exists is false).") elif not (hasattr(root_tk_instance, 'winfo_exists') and root_tk_instance.winfo_exists()): print("WARNING: Root Tk instance provided to setup_logging does not exist.") else: _tkinter_handler_instance = TkinterTextHandler( text_widget=gui_log_widget, root_tk_instance=root_tk_instance, level_colors=configured_level_colors ) _tkinter_handler_instance.setFormatter(formatter) # Imposta un livello per il TkinterTextHandler, potrebbe essere diverso # _tkinter_handler_instance.setLevel(log_level) root_logger.addHandler(_tkinter_handler_instance) # Il messaggio di log "GUI logging handler initialized" verrà ora gestito # dal logger stesso, incluso il TkinterTextHandler se _is_active è True. root_logger.info("GUI logging handler (thread-safe) initialized and attached.") elif gui_log_widget and not root_tk_instance: print("WARNING: GUI log widget provided, but root Tk instance is missing. Cannot initialize GUI logger.") elif not gui_log_widget and root_tk_instance: print("DEBUG: Root Tk instance provided, but no GUI log widget. GUI logger not initialized.") def get_logger(name: str) -> logging.Logger: return logging.getLogger(name) # --- MODIFICA: Nuova Funzione --- def shutdown_gui_logging(): """ Closes and removes the TkinterTextHandler instance from the root logger. This should be called before the Tkinter root window is destroyed. """ global _tkinter_handler_instance root_logger = logging.getLogger() if _tkinter_handler_instance: if _tkinter_handler_instance in root_logger.handlers: # Logga un messaggio (alla console, dato che stiamo chiudendo quello GUI) # prima di rimuovere l'handler. # Potremmo usare un logger temporaneo o print. print(f"INFO: Closing and removing GUI logging handler ({_tkinter_handler_instance.name}).") root_logger.removeHandler(_tkinter_handler_instance) _tkinter_handler_instance.close() # Chiama il metodo close dell'handler _tkinter_handler_instance = None print("INFO: GUI logging handler has been shut down.") else: print("DEBUG: No active GUI logging handler to shut down.")