SXXXXXXX_FlightMonitor/flightmonitor/utils/logger.py

303 lines
15 KiB
Python

# 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.")