303 lines
15 KiB
Python
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.") |