add thread for map, add queue for loggin system

This commit is contained in:
VALLONGOL 2025-05-29 21:20:01 +02:00
parent 574a8bf7e0
commit 5842e8c37c
5 changed files with 1197 additions and 1416 deletions

View File

@ -716,34 +716,47 @@ class AppController:
) )
def on_application_exit(self): def on_application_exit(self):
module_logger.info( module_logger.info("Controller: Application exit requested. Cleaning up resources.")
"Controller: Application exit requested. Cleaning up resources."
) # MODIFIED: Added shutdown for MapCanvasManager's worker thread.
is_adapter_considered_running = ( # WHY: To ensure the map rendering thread is properly terminated before the application exits.
self.live_adapter_thread and self.live_adapter_thread.is_alive() # WHERE: At the beginning of the application exit process.
) or self.is_live_monitoring_active # HOW: Checking for the existence of main_window, map_manager_instance,
if is_adapter_considered_running: # and the shutdown_worker method, then calling it within a try-except block.
module_logger.debug( if self.main_window and \
"Controller: Live monitoring/adapter active during app exit, stopping it." hasattr(self.main_window, "map_manager_instance") and \
) self.main_window.map_manager_instance is not None:
self.stop_live_monitoring(from_error=False) map_manager = self.main_window.map_manager_instance
if hasattr(map_manager, "shutdown_worker") and callable(map_manager.shutdown_worker):
module_logger.debug("Controller: Requesting MapCanvasManager to shutdown its worker.")
try:
map_manager.shutdown_worker()
module_logger.info("Controller: MapCanvasManager worker shutdown requested.")
except Exception as e_map_shutdown:
module_logger.error(
f"Controller: Error during MapCanvasManager worker shutdown: {e_map_shutdown}",
exc_info=True
)
else:
module_logger.debug("Controller: MapCanvasManager instance found, but no 'shutdown_worker' method.")
else: else:
module_logger.debug( module_logger.debug("Controller: No MapCanvasManager instance found or main window not set; skipping map worker shutdown.")
"Controller: Live monitoring/adapter not active or already stopped during app exit."
) is_adapter_considered_running = (self.live_adapter_thread and self.live_adapter_thread.is_alive()) or self.is_live_monitoring_active
if is_adapter_considered_running:
module_logger.debug("Controller: Live monitoring/adapter active during app exit, stopping it.")
self.stop_live_monitoring(from_error=False) # This will join the adapter thread
else:
module_logger.debug("Controller: Live monitoring/adapter not active or already stopped during app exit.")
if self.data_storage: if self.data_storage:
module_logger.debug( module_logger.debug("Controller: Closing DataStorage connection during app exit.")
"Controller: Closing DataStorage connection during app exit."
)
try: try:
self.data_storage.close_connection() self.data_storage.close_connection()
except Exception as e_db_close: except Exception as e_db_close:
module_logger.error( module_logger.error(f"Error closing DataStorage: {e_db_close}", exc_info=True)
f"Error closing DataStorage: {e_db_close}", exc_info=True
)
finally: finally:
self.data_storage = None self.data_storage = None # Ensure it's cleared
module_logger.info("Controller: Cleanup on application exit finished.") module_logger.info("Controller: Cleanup on application exit finished.")
def start_history_monitoring(self): def start_history_monitoring(self):

View File

@ -14,7 +14,7 @@ import logging # Import per i livelli di logging (es. logging.ERROR)
# Relative imports # Relative imports
from ..data import config as app_config # Standardized alias from ..data import config as app_config # Standardized alias
from ..utils.logger import get_logger, setup_logging, shutdown_gui_logging from ..utils.logger import get_logger, setup_logging, shutdown_logging_system
from ..data.common_models import CanonicalFlightState from ..data.common_models import CanonicalFlightState
# Importa le costanti di stato GUI dal modulo utils centralizzato # Importa le costanti di stato GUI dal modulo utils centralizzato
@ -824,13 +824,14 @@ class MainWindow:
exc_info=True, exc_info=True,
) )
module_logger.info("Shutting down GUI logging.") module_logger.info("Shutting down logging system.") # Messaggio aggiornato
try: try:
shutdown_gui_logging() # Assicurati che questa funzione sia robusta # MODIFIED: Changed shutdown_gui_logging() to shutdown_logging_system()
except Exception as e: # Logga eventuali errori, ma non fermare la chiusura # WHY: The function name was changed in the logger.py module.
module_logger.error( # HOW: Updated the function call.
f"Error during shutdown_gui_logging: {e}", exc_info=True shutdown_logging_system() # Assicurati che questa funzione sia robusta
) except Exception as e:
module_logger.error(f"Error during shutdown_logging_system: {e}", exc_info=True)
if hasattr(self, "root") and self.root.winfo_exists(): if hasattr(self, "root") and self.root.winfo_exists():
try: try:

File diff suppressed because it is too large Load Diff

View File

@ -51,14 +51,16 @@ DEFAULT_MAX_ZOOM_FALLBACK: int = 19
# Default color for placeholder tile images when a tile cannot be loaded. # Default color for placeholder tile images when a tile cannot be loaded.
DEFAULT_PLACEHOLDER_COLOR_RGB: Tuple[int, int, int] = (220, 220, 220) # Light grey DEFAULT_PLACEHOLDER_COLOR_RGB: Tuple[int, int, int] = (220, 220, 220) # Light grey
# MODIFIED: Added constant for Tkinter canvas background color for placeholders.
# WHY: Needed by MapCanvasManager to set the canvas background when displaying placeholder text.
# HOW: Added the new constant definition.
DEFAULT_PLACEHOLDER_COLOR_RGB_TK: str = "gray85" # Tkinter color string for canvas background
# --- Map Information Panel Formatting --- # --- Map Information Panel Formatting ---
# Number of decimal places to display for coordinates in the info panel. # Number of decimal places to display for coordinates in the info panel.
COORDINATE_DECIMAL_PLACES: int = 5 COORDINATE_DECIMAL_PLACES: int = 5
# MODIFIED: Add constant for decimal places for map size in km.
# WHY: Centralize formatting constant for map info panel.
# HOW: Added the constant.
MAP_SIZE_KM_DECIMAL_PLACES: int = 1 # Number of decimal places for map size in km. MAP_SIZE_KM_DECIMAL_PLACES: int = 1 # Number of decimal places for map size in km.

View File

@ -1,295 +1,235 @@
# FlightMonitor/utils/logger.py # FlightMonitor/utils/logger.py
import logging import logging
import logging.handlers # For RotatingFileHandler
import tkinter as tk import tkinter as tk
from tkinter.scrolledtext import ScrolledText from tkinter.scrolledtext import ScrolledText
from queue import Queue, Empty as QueueEmpty from queue import Queue, Empty as QueueEmpty # Renamed for clarity
from typing import Optional, Dict, Any from typing import Optional, Dict, Any
# --- Module-level globals for the new 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 # Forward declaration
_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 # Store the main formatter
# Interval for polling the global log queue by the GUI thread processor
GLOBAL_LOG_QUEUE_POLL_INTERVAL_MS = 50
class TkinterTextHandler(logging.Handler): class TkinterTextHandler(logging.Handler):
""" """
A logging handler that directs log messages to a Tkinter Text widget A logging handler that directs log messages to a Tkinter Text widget.
in a thread-safe manner using an internal queue and root.after(). This handler itself is now called by _process_global_log_queue,
which runs in the GUI thread. Its internal queue is for batching updates
to the Text widget if needed, or could be simplified later.
""" """
def __init__( def __init__(
self, self,
text_widget: tk.Text, text_widget: tk.Text, # ScrolledText is a tk.Text
root_tk_instance: tk.Tk,
level_colors: Dict[int, str], level_colors: Dict[int, str],
queue_poll_interval_ms: int, # root_tk_instance and queue_poll_interval_ms for its own internal queue might be redundant
# if _process_global_log_queue handles batching, but kept for now.
# For now, this handler will still use its own internal queue and after loop for safety.
root_tk_instance_for_widget_update: tk.Tk,
internal_poll_interval_ms: int = 100
): ):
super().__init__() super().__init__()
self.text_widget = text_widget self.text_widget = text_widget
self.root_tk_instance = root_tk_instance
self.log_queue = Queue()
self.level_colors = level_colors self.level_colors = level_colors
self.queue_poll_interval_ms = queue_poll_interval_ms self._is_active = True # Internal active state for this specific handler
self._after_id_log_processor: Optional[str] = None self._widget_update_queue = Queue()
self._is_active = True self._root_for_widget_update = root_tk_instance_for_widget_update
self._internal_poll_interval_ms = internal_poll_interval_ms
self._internal_after_id: Optional[str] = None
if not ( if not (
self.text_widget self.text_widget
and hasattr(self.text_widget, "winfo_exists") and hasattr(self.text_widget, "winfo_exists")
and self.text_widget.winfo_exists() and self.text_widget.winfo_exists()
): ):
print( print("ERROR: TkinterTextHandler initialized with an invalid or non-existent text_widget.", flush=True)
"Warning: TkinterTextHandler initialized with an invalid or non-existent text_widget.",
flush=True,
)
self._is_active = False self._is_active = False
return return
if not ( if not (
self.root_tk_instance self._root_for_widget_update
and hasattr(self.root_tk_instance, "winfo_exists") and hasattr(self._root_for_widget_update, "winfo_exists")
and self.root_tk_instance.winfo_exists() and self._root_for_widget_update.winfo_exists()
): ):
print( print("ERROR: TkinterTextHandler initialized with an invalid root for widget update.", flush=True)
"Warning: TkinterTextHandler initialized with an invalid or non-existent root_tk_instance.",
flush=True,
)
self._is_active = False self._is_active = False
return return
if self._is_active: if self._is_active:
for level, color_value in self.level_colors.items(): self._configure_tags()
level_name = logging.getLevelName(level) self._process_widget_update_queue() # Start its own update loop
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.",
flush=True,
)
pass
if self._is_active: def _configure_tags(self):
self._process_log_queue() if not self._is_active or not self.text_widget.winfo_exists(): return
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} in TkinterTextHandler.", flush=True)
def emit(self, record: logging.LogRecord): def emit(self, record: logging.LogRecord):
if not self._is_active: # This emit is called by _process_global_log_queue (already in GUI thread)
if not self._is_active or not self.text_widget.winfo_exists():
# This case should ideally not happen if setup is correct
print(f"DEBUG: TkinterTextHandler.emit called but inactive or widget gone. Record: {record.getMessage()}", flush=True)
return return
if not (
hasattr(self.text_widget, "winfo_exists")
and self.text_widget.winfo_exists()
and hasattr(self.root_tk_instance, "winfo_exists")
and self.root_tk_instance.winfo_exists()
):
print(
"Warning: TkinterTextHandler.emit: Widget or root instance does not exist. Deactivating handler.",
flush=True,
)
self._is_active = False
return
try: try:
msg = self.format(record) msg = self.format(record) # Format the record using the formatter set on this handler
level_name = record.levelname level_name = record.levelname
self.log_queue.put_nowait((level_name, msg)) # Put in its own queue for batched widget updates
self._widget_update_queue.put_nowait((level_name, msg))
except Exception as e: except Exception as e:
print(f"Error in TkinterTextHandler.emit before queueing: {e}", flush=True) print(f"Error in TkinterTextHandler.emit (to internal queue): {e}", flush=True)
def _process_log_queue(self): def _process_widget_update_queue(self):
if not self._is_active: if not self._is_active or \
if ( not self.text_widget.winfo_exists() or \
self._after_id_log_processor not self._root_for_widget_update.winfo_exists():
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:
pass
except Exception as e:
print(
f"Error cancelling after_id in _process_log_queue (handler inactive): {e}",
flush=True,
)
self._after_id_log_processor = None
return
if not (
hasattr(self.text_widget, "winfo_exists")
and self.text_widget.winfo_exists()
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.",
flush=True,
)
if self._after_id_log_processor:
try:
self.root_tk_instance.after_cancel(self._after_id_log_processor)
except tk.TclError:
pass
except Exception as e:
print(
f"Error cancelling after_id in _process_log_queue (widgets gone): {e}",
flush=True,
)
self._after_id_log_processor = None
self._is_active = False self._is_active = False
if self._internal_after_id and self._root_for_widget_update.winfo_exists():
try: self._root_for_widget_update.after_cancel(self._internal_after_id)
except Exception: pass
self._internal_after_id = None
# print("DEBUG: TkinterTextHandler._process_widget_update_queue stopping (inactive/widget gone).", flush=True)
return return
had_messages = False
try: try:
while self._is_active: while not self._widget_update_queue.empty():
try: had_messages = True
level_name, msg = self.log_queue.get(block=False, timeout=0.01) level_name, msg = self._widget_update_queue.get_nowait()
except QueueEmpty: if self.text_widget.winfo_exists():
break
except Exception as e_get:
print(
f"Error getting from log queue: {e_get}. Stopping processing for this cycle.",
flush=True,
)
break
if not self._is_active or not (
self.text_widget.winfo_exists()
and self.root_tk_instance.winfo_exists()
):
print(
"Debug: TkinterTextHandler._process_log_queue: Widgets gone during queue processing loop. Stopping.",
flush=True,
)
self._is_active = False
break
try:
self.text_widget.configure(state=tk.NORMAL) self.text_widget.configure(state=tk.NORMAL)
if self.text_widget.winfo_exists(): self.text_widget.insert(tk.END, msg + "\n", (level_name,))
self.text_widget.insert(tk.END, msg + "\n", (level_name,)) self.text_widget.configure(state=tk.DISABLED)
self.text_widget.see(tk.END) else: # Widget destroyed during processing
if self.text_widget.winfo_exists(): self._is_active = False; break
self.text_widget.configure(state=tk.DISABLED) self._widget_update_queue.task_done()
self.log_queue.task_done() if had_messages and self.text_widget.winfo_exists():
self.text_widget.see(tk.END)
except tk.TclError as e_tcl_inner: except tk.TclError as e_tcl:
print( print(f"TkinterTextHandler TclError during widget update: {e_tcl}. Handler stopping.", flush=True)
f"TkinterTextHandler TclError during widget update: {e_tcl_inner}. Attempting to stop handler.",
flush=True,
)
self._is_active = False
break
except Exception as e_inner:
print(
f"Unexpected error updating text widget: {e_inner}. Attempting to stop handler.",
flush=True,
)
self._is_active = False
break
except tk.TclError as e_tcl_outer:
print(
f"TkinterTextHandler TclError in _process_log_queue (outer): {e_tcl_outer}",
flush=True,
)
if self._after_id_log_processor:
try:
self.root_tk_instance.after_cancel(self._after_id_log_processor)
except tk.TclError:
pass
except Exception as e_cancel:
print(
f"Error cancelling after_id in _process_log_queue (TclError outer): {e_cancel}",
flush=True,
)
self._after_id_log_processor = None
self._is_active = False self._is_active = False
return except Exception as e_update:
print(f"Unexpected error updating TkinterTextHandler widget: {e_update}. Handler stopping.", flush=True)
except Exception as e_outer:
print(
f"Unexpected error in TkinterTextHandler._process_log_queue (outer): {e_outer}",
flush=True,
)
self._is_active = False self._is_active = False
return
if ( if self._is_active and self._root_for_widget_update.winfo_exists():
self._is_active self._internal_after_id = self._root_for_widget_update.after(
and hasattr(self.root_tk_instance, "winfo_exists") self._internal_poll_interval_ms, self._process_widget_update_queue
and self.root_tk_instance.winfo_exists() )
): else: # Ensure it's cleaned up if we are stopping
try: if self._internal_after_id and self._root_for_widget_update.winfo_exists():
self._after_id_log_processor = self.root_tk_instance.after( try: self._root_for_widget_update.after_cancel(self._internal_after_id)
self.queue_poll_interval_ms, self._process_log_queue except Exception: pass
) self._internal_after_id = None
except tk.TclError:
print(
"Debug: TkinterTextHandler._process_log_queue: Root destroyed. Cannot reschedule.",
flush=True,
)
self._after_id_log_processor = None
self._is_active = False
except Exception as e_reschedule:
print(
f"Error rescheduling _process_log_queue: {e_reschedule}", flush=True
)
self._after_id_log_processor = None
self._is_active = False
elif self._after_id_log_processor:
if (
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:
pass
except Exception as e_cancel_final:
print(
f"Error cancelling after_id in final _process_log_queue check: {e_cancel_final}",
flush=True,
)
self._after_id_log_processor = None
def close(self): def close(self):
""" print("INFO: TkinterTextHandler close called.", flush=True)
Cleans up resources, like stopping the log queue processor.
Called when the handler is removed or logging system shuts down.
"""
self._is_active = False self._is_active = False
if self._internal_after_id and hasattr(self, '_root_for_widget_update') and \
if self._after_id_log_processor: self._root_for_widget_update and self._root_for_widget_update.winfo_exists():
if (
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).",
flush=True,
)
pass
except Exception as e_cancel_close:
print(
f"Error cancelling after_id in close: {e_cancel_close}",
flush=True,
)
pass
self._after_id_log_processor = None
while not self.log_queue.empty():
try: try:
self.log_queue.get_nowait() self._root_for_widget_update.after_cancel(self._internal_after_id)
pass except Exception: pass
except QueueEmpty: self._internal_after_id = None
break # Drain its internal queue
except Exception as e_q_drain: while not self._widget_update_queue.empty():
print(f"Error draining log queue during close: {e_q_drain}", flush=True) try: self._widget_update_queue.get_nowait(); self._widget_update_queue.task_done()
break except Exception: break
super().close() super().close()
_tkinter_handler_instance: Optional[TkinterTextHandler] = None 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):
try:
# We can choose to make a copy of the record if there's any concern
# about it being modified before processing, though typically not an issue.
# For now, put the original record.
self.handler_queue.put_nowait(record)
except queue.Full:
print(f"CRITICAL: Global log queue is full! Log record for '{record.name}' might be lost.", flush=True)
except Exception as e:
print(f"CRITICAL: Error putting log record into global queue: {e}", flush=True)
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
# print(f"DEBUG: _process_global_log_queue called. Active: {_logging_system_active}", flush=True) # Can be very verbose
if not _logging_system_active:
if _log_processor_after_id and _tk_root_instance_for_processing and _tk_root_instance_for_processing.winfo_exists():
try: _tk_root_instance_for_processing.after_cancel(_log_processor_after_id)
except Exception: pass
_log_processor_after_id = None
return
if not (_tk_root_instance_for_processing and _tk_root_instance_for_processing.winfo_exists()):
# print("DEBUG: Global log queue processor: Tk root instance gone. Stopping.", flush=True)
_logging_system_active = False # Stop if root is gone
_log_processor_after_id = None
return
processed_count = 0
try:
while _global_log_queue and not _global_log_queue.empty():
if not _logging_system_active: break # Check before processing each item
try:
record = _global_log_queue.get_nowait()
processed_count +=1
# Dispatch to actual handlers
if _actual_console_handler:
try: _actual_console_handler.handle(record)
except Exception as e_con: print(f"Error in console_handler.handle: {e_con}", flush=True)
if _actual_file_handler:
try: _actual_file_handler.handle(record)
except Exception as e_file: print(f"Error in file_handler.handle: {e_file}", flush=True)
if _actual_tkinter_handler:
try: _actual_tkinter_handler.handle(record)
except Exception as e_tk: print(f"Error in tkinter_handler.handle: {e_tk}", flush=True)
_global_log_queue.task_done()
except QueueEmpty:
break # Should not happen due to outer loop condition, but defensive
except Exception as e_proc_item:
print(f"Error processing a log item from global queue: {e_proc_item}", flush=True)
# Potentially re-queue or log to a failsafe if critical
except Exception as e_outer_loop:
print(f"Critical error in _process_global_log_queue outer loop: {e_outer_loop}", flush=True)
if _logging_system_active and _tk_root_instance_for_processing.winfo_exists():
_log_processor_after_id = _tk_root_instance_for_processing.after(
GLOBAL_LOG_QUEUE_POLL_INTERVAL_MS, _process_global_log_queue
)
# else: print(f"DEBUG: Not rescheduling _process_global_log_queue. Active: {_logging_system_active}", flush=True)
def setup_logging( def setup_logging(
@ -297,195 +237,157 @@ def setup_logging(
root_tk_instance: Optional[tk.Tk] = None, root_tk_instance: Optional[tk.Tk] = None,
logging_config_dict: Optional[Dict[str, Any]] = None, logging_config_dict: Optional[Dict[str, Any]] = None,
): ):
""" global _global_log_queue, _actual_console_handler, _actual_file_handler
Sets up application-wide logging based on the provided configuration dictionary. global _actual_tkinter_handler, _logging_system_active, _tk_root_instance_for_processing
If logging_config_dict is None, uses basic default settings (console only). global _log_processor_after_id, _base_formatter
"""
global _tkinter_handler_instance print("INFO: Configuring centralized queued logging system...", flush=True)
if _logging_system_active:
print("INFO: Logging system already active. Shutting down existing to reconfigure.", flush=True)
shutdown_logging_system() # Ensure clean state before re-setup
if logging_config_dict is None: if logging_config_dict is None:
print( print("Warning: No logging_config_dict. Using basic console-only defaults for queued logging.", flush=True)
"Warning: No logging_config_dict provided to setup_logging. Using basic default console logging.",
flush=True,
)
logging_config_dict = { logging_config_dict = {
"default_root_level": logging.INFO, "default_root_level": logging.INFO, "specific_levels": {},
"specific_levels": {}, "format": "%(asctime)s [%(levelname)-8s] %(name)-25s : %(message)s",
"format": "%(asctime)s - %(name)s - %(levelname)s - %(message)s",
"date_format": "%Y-%m-%d %H:%M:%S", "date_format": "%Y-%m-%d %H:%M:%S",
"colors": {logging.INFO: "black"}, "colors": {logging.INFO: "black", logging.DEBUG: "blue", logging.WARNING:"orange", logging.ERROR:"red", logging.CRITICAL:"purple"},
"queue_poll_interval_ms": 100, "queue_poll_interval_ms": 100, # For TkinterTextHandler's internal queue
"enable_console": True, "enable_console": True, "enable_file": False,
"enable_file": False,
} }
# --- Extract Config ---
root_log_level = logging_config_dict.get("default_root_level", logging.INFO) root_log_level = logging_config_dict.get("default_root_level", logging.INFO)
specific_levels = logging_config_dict.get("specific_levels", {}) specific_levels = logging_config_dict.get("specific_levels", {})
log_format_str = logging_config_dict.get( log_format_str = logging_config_dict.get("format", "%(asctime)s [%(levelname)-8s] %(name)-25s : %(message)s")
"format", "%(asctime)s - %(name)s - %(levelname)s - %(message)s"
)
log_date_format_str = logging_config_dict.get("date_format", "%Y-%m-%d %H:%M:%S") log_date_format_str = logging_config_dict.get("date_format", "%Y-%m-%d %H:%M:%S")
level_colors = logging_config_dict.get("colors", {}) level_colors = logging_config_dict.get("colors", {})
queue_poll_interval_ms = logging_config_dict.get("queue_poll_interval_ms", 100) tkinter_handler_poll_ms = logging_config_dict.get("queue_poll_interval_ms", 100) # For TkinterTextHandler's internal queue
enable_console = logging_config_dict.get("enable_console", True) enable_console = logging_config_dict.get("enable_console", True)
# MODIFIED: De-commented the line to extract 'enable_file' from the config dictionary.
# WHY: The variable 'enable_file' was used later in an 'if' statement but was not defined because this line was commented out.
# HOW: Removed the '#' at the beginning of the line.
enable_file = logging_config_dict.get("enable_file", False) enable_file = logging_config_dict.get("enable_file", False)
formatter = logging.Formatter(log_format_str, datefmt=log_date_format_str) _base_formatter = logging.Formatter(log_format_str, datefmt=log_date_format_str)
_global_log_queue = Queue() # Initialize the global queue for LogRecords
_tk_root_instance_for_processing = root_tk_instance # Store for the after() loop
# --- Configure Root Logger ---
root_logger = logging.getLogger() root_logger = logging.getLogger()
# Remove ALL existing handlers from the root logger to ensure clean state
handlers_to_remove = []
for handler in root_logger.handlers[:]: for handler in root_logger.handlers[:]:
if isinstance(handler, (logging.StreamHandler, logging.FileHandler)):
handlers_to_remove.append(handler)
elif handler is _tkinter_handler_instance:
pass
else:
module_logger_internal = logging.getLogger(__name__)
module_logger_internal.warning(
f"Removing unknown logger handler during setup: {handler}"
)
handlers_to_remove.append(handler)
for handler in handlers_to_remove:
try: try:
handler.close() handler.close() # Close handler first
root_logger.removeHandler(handler) root_logger.removeHandler(handler)
module_logger_internal = logging.getLogger(__name__) print(f"DEBUG: Removed and closed old handler from root: {handler}", flush=True)
module_logger_internal.debug(f"Removed and closed old handler: {handler}") except Exception as e_rem:
except Exception as e: print(f"Error removing/closing old handler {handler}: {e_rem}", flush=True)
module_logger_internal = logging.getLogger(__name__) root_logger.handlers = [] # Ensure it's empty
module_logger_internal.error(
f"Error removing/closing old handler {handler}: {e}", exc_info=False
)
if _tkinter_handler_instance:
if _tkinter_handler_instance in root_logger.handlers:
try:
root_logger.removeHandler(_tkinter_handler_instance)
module_logger_internal = logging.getLogger(__name__)
module_logger_internal.debug("Removed old TkinterTextHandler instance.")
except Exception as e:
module_logger_internal = logging.getLogger(__name__)
module_logger_internal.error(
f"Error removing old TkinterTextHandler instance: {e}",
exc_info=False,
)
try:
_tkinter_handler_instance.close()
module_logger_internal = logging.getLogger(__name__)
module_logger_internal.debug("Closed old TkinterTextHandler instance.")
except Exception as e:
module_logger_internal = logging.getLogger(__name__)
module_logger_internal.error(
f"Error closing old TkinterTextHandler instance: {e}", exc_info=False
)
finally:
_tkinter_handler_instance = None
root_logger.setLevel(root_log_level) root_logger.setLevel(root_log_level)
module_logger_internal = logging.getLogger(__name__) print(f"INFO: Root logger level set to {logging.getLevelName(root_logger.level)}.", flush=True)
module_logger_internal.debug(
f"Root logger level set to {logging.getLevelName(root_logger.level)}"
)
# --- Configure Specific Logger Levels ---
for logger_name, level in specific_levels.items(): for logger_name, level in specific_levels.items():
named_logger = logging.getLogger(logger_name) named_logger = logging.getLogger(logger_name)
named_logger.setLevel(level) named_logger.setLevel(level)
module_logger_internal.debug( # Ensure propagation is on if they are not root, or if they should also go to root's QueuePuttingHandler
f"Logger '{logger_name}' level set to {logging.getLevelName(named_logger.level)}" named_logger.propagate = True
) print(f"INFO: Logger '{logger_name}' level set to {logging.getLevelName(named_logger.level)}.", flush=True)
# --- Create Actual Handlers (but don't add to root logger yet) ---
if enable_console: if enable_console:
try: try:
console_handler = logging.StreamHandler() _actual_console_handler = logging.StreamHandler() # Defaults to stderr
console_handler.setFormatter(formatter) _actual_console_handler.setFormatter(_base_formatter)
root_logger.addHandler(console_handler) _actual_console_handler.setLevel(logging.DEBUG) # Let console handler decide based on record level
module_logger_internal.debug("Console logging enabled and handler added.") print("INFO: Actual ConsoleHandler created.", flush=True)
except Exception as e: except Exception as e:
module_logger_internal.error( print(f"ERROR: Failed to create actual ConsoleHandler: {e}", flush=True)
f"Error adding console handler: {e}", exc_info=False _actual_console_handler = None
)
# MODIFIED: Added the block to configure the file handler if enable_file is True.
# WHY: This logic was intended to be here.
# HOW: Moved the block from the original code and ensured it's under the 'if enable_file:' condition.
if enable_file: if enable_file:
try: try:
from logging.handlers import RotatingFileHandler file_path = logging_config_dict.get("file_path", "flight_monitor_app.log")
file_max_bytes = logging_config_dict.get("file_max_bytes", 5 * 1024 * 1024) # 5MB
file_path = logging_config_dict.get("file_path", "app.log") file_backup_count = logging_config_dict.get("file_backup_count", 3)
file_max_bytes = logging_config_dict.get("file_max_bytes", 10 * 1024 * 1024) _actual_file_handler = logging.handlers.RotatingFileHandler(
file_backup_count = logging_config_dict.get("file_backup_count", 5) file_path, maxBytes=file_max_bytes, backupCount=file_backup_count, encoding='utf-8'
file_handler = RotatingFileHandler(
file_path, maxBytes=file_max_bytes, backupCount=file_backup_count
)
file_handler.setFormatter(formatter)
root_logger.addHandler(file_handler)
module_logger_internal.debug(
f"File logging enabled to '{file_path}' and handler added."
) )
_actual_file_handler.setFormatter(_base_formatter)
_actual_file_handler.setLevel(logging.DEBUG) # Let file handler decide
print(f"INFO: Actual RotatingFileHandler created for '{file_path}'.", flush=True)
except Exception as e: except Exception as e:
module_logger_internal.error( print(f"ERROR: Failed to create actual FileHandler for '{file_path}': {e}", flush=True)
f"Error adding file handler to '{file_path}': {e}", exc_info=True _actual_file_handler = None
)
if gui_log_widget and root_tk_instance: if gui_log_widget and root_tk_instance:
is_widget_valid = ( is_widget_valid = (
isinstance(gui_log_widget, (tk.Text, ScrolledText)) isinstance(gui_log_widget, (tk.Text, ScrolledText)) and
and hasattr(gui_log_widget, "winfo_exists") hasattr(gui_log_widget, "winfo_exists") and gui_log_widget.winfo_exists()
and gui_log_widget.winfo_exists()
) )
is_root_valid = ( if is_widget_valid:
hasattr(root_tk_instance, "winfo_exists")
and root_tk_instance.winfo_exists()
)
if not is_widget_valid:
print(
f"ERROR: GUI log widget is not a valid tk.Text/ScrolledText or does not exist: {type(gui_log_widget)}",
flush=True,
)
elif not is_root_valid:
print(
"WARNING: Root Tk instance provided to setup_logging does not exist.",
flush=True,
)
else:
try: try:
_tkinter_handler_instance = TkinterTextHandler( _actual_tkinter_handler = TkinterTextHandler(
text_widget=gui_log_widget, text_widget=gui_log_widget,
root_tk_instance=root_tk_instance,
level_colors=level_colors, level_colors=level_colors,
queue_poll_interval_ms=queue_poll_interval_ms, root_tk_instance_for_widget_update=root_tk_instance, # Pass root for its own after loop
) internal_poll_interval_ms=tkinter_handler_poll_ms
_tkinter_handler_instance.setFormatter(formatter)
root_logger.addHandler(_tkinter_handler_instance)
module_logger_internal = logging.getLogger(__name__)
module_logger_internal.info(
"GUI logging handler (thread-safe) initialized and attached."
) )
_actual_tkinter_handler.setFormatter(_base_formatter)
_actual_tkinter_handler.setLevel(logging.DEBUG) # Let Tkinter handler decide
print("INFO: Actual TkinterTextHandler created.", flush=True)
except Exception as e: except Exception as e:
_tkinter_handler_instance = None print(f"ERROR: Failed to create actual TkinterTextHandler: {e}", flush=True)
print( _actual_tkinter_handler = None
f"ERROR: Failed to initialize TkinterTextHandler: {e}", flush=True else:
) print("ERROR: GUI log widget invalid or non-existent, cannot create TkinterTextHandler.", flush=True)
elif gui_log_widget and not root_tk_instance: elif gui_log_widget and not root_tk_instance:
print( print("WARNING: GUI log widget provided, but root_tk_instance missing for TkinterTextHandler.", flush=True)
"WARNING: GUI log widget provided, but root Tk instance is missing. Cannot initialize GUI logger.",
flush=True,
) # --- Add ONLY the QueuePuttingHandler to the root logger ---
elif not gui_log_widget and root_tk_instance: if _global_log_queue is not None:
print( queue_putter = QueuePuttingHandler(handler_queue=_global_log_queue)
"DEBUG: Root Tk instance provided, but no GUI log widget. GUI logger not initialized.", queue_putter.setLevel(logging.DEBUG) # Capture all messages from root level downwards
flush=True, root_logger.addHandler(queue_putter)
print(f"INFO: QueuePuttingHandler added to root logger. All logs will go to global queue.", flush=True)
else:
print("CRITICAL ERROR: Global log queue not initialized. Logging will not function correctly.", flush=True)
# Fallback to basic console if queue setup failed badly
if not _actual_console_handler: # If even this failed
_bf = logging.Formatter("%(asctime)s - %(name)s - %(levelname)s - %(message)s")
_ch = logging.StreamHandler()
_ch.setFormatter(_bf)
root_logger.addHandler(_ch)
print("CRITICAL: Added basic emergency console logger.", flush=True)
# --- Start the global log queue processor (if root_tk_instance is available) ---
_logging_system_active = True
if _tk_root_instance_for_processing and _tk_root_instance_for_processing.winfo_exists():
if _log_processor_after_id: # Cancel previous if any (shouldn't happen if shutdown was called)
try: _tk_root_instance_for_processing.after_cancel(_log_processor_after_id)
except Exception: pass
_log_processor_after_id = _tk_root_instance_for_processing.after(
GLOBAL_LOG_QUEUE_POLL_INTERVAL_MS, _process_global_log_queue
) )
print(f"INFO: Global log queue processor scheduled with Tkinter.after (ID: {_log_processor_after_id}).", flush=True)
elif root_tk_instance is None and (gui_log_widget is not None or enable_file or enable_console):
# This case means we want logging but have no Tkinter root to schedule the queue processor.
# This is problematic for GUI logging. For console/file, logs will just queue up.
# A dedicated thread could process the queue if no Tkinter available, but adds complexity.
print("WARNING: No Tkinter root instance provided to setup_logging. "
"Logs will be queued but not processed by GUI/File/Console handlers unless _process_global_log_queue is run manually or by another mechanism.", flush=True)
elif not (enable_console or enable_file or gui_log_widget):
print("INFO: No log output handlers (console, file, gui) are enabled. Logs will be queued but not displayed.", flush=True)
print("INFO: Centralized queued logging system setup complete.", flush=True)
# Test log
test_logger = logging.getLogger("FlightMonitor.LoggerTest")
test_logger.info("Logging system initialized. This is a test message from setup_logging.")
def get_logger(name: str) -> logging.Logger: def get_logger(name: str) -> logging.Logger:
@ -496,30 +398,84 @@ def get_logger(name: str) -> logging.Logger:
return logging.getLogger(name) return logging.getLogger(name)
def shutdown_gui_logging(): def shutdown_logging_system():
""" """
Closes and removes the TkinterTextHandler instance from the root logger. Shuts down the centralized logging system, processing remaining logs
This should be called before the Tkinter root window is destroyed. and closing actual handlers.
""" """
global _tkinter_handler_instance global _logging_system_active, _log_processor_after_id
root_logger = logging.getLogger() global _actual_console_handler, _actual_file_handler, _actual_tkinter_handler
if _tkinter_handler_instance: global _global_log_queue, _tk_root_instance_for_processing
if _tkinter_handler_instance in root_logger.handlers:
print(
f"INFO: Closing and removing GUI logging handler ({_tkinter_handler_instance.name}).",
flush=True,
)
try:
root_logger.removeHandler(_tkinter_handler_instance)
except Exception as e:
print(f"Error removing GUI handler from logger: {e}", flush=True)
print("INFO: Initiating shutdown of centralized logging system...", flush=True)
_logging_system_active = False # Signal processor to stop
if _log_processor_after_id and _tk_root_instance_for_processing and _tk_root_instance_for_processing.winfo_exists():
try: try:
_tkinter_handler_instance.close() _tk_root_instance_for_processing.after_cancel(_log_processor_after_id)
print("INFO: GUI logging handler has been shut down.", flush=True) print("INFO: Cancelled global log queue processor task.", flush=True)
except Exception as e: except Exception as e_cancel:
print(f"Error closing GUI logging handler: {e}", flush=True) print(f"Warning: Error cancelling log processor task: {e_cancel}", flush=True)
finally: _log_processor_after_id = None
_tkinter_handler_instance = None
else: # Attempt to process any remaining logs in the global queue
print("DEBUG: No active GUI logging handler to shut down.", flush=True) print("INFO: Processing any remaining logs in the global queue before full shutdown...", flush=True)
if _global_log_queue:
final_processed_count = 0
while not _global_log_queue.empty():
try:
record = _global_log_queue.get_nowait()
final_processed_count += 1
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:
break
except Exception as e_final_proc:
print(f"Error during final log processing: {e_final_proc}", flush=True)
break # Stop if error during final processing
if final_processed_count > 0:
print(f"INFO: Processed {final_processed_count} remaining log messages.", flush=True)
else:
print("INFO: No remaining log messages in global queue to process.", flush=True)
# Close actual handlers
if _actual_tkinter_handler:
try:
print("INFO: Closing actual TkinterTextHandler.", flush=True)
_actual_tkinter_handler.close()
except Exception as e: print(f"Error closing TkinterTextHandler: {e}", flush=True)
_actual_tkinter_handler = None
if _actual_console_handler:
try:
print("INFO: Closing actual ConsoleHandler.", flush=True)
_actual_console_handler.close()
except Exception as e: print(f"Error closing ConsoleHandler: {e}", flush=True)
_actual_console_handler = None
if _actual_file_handler:
try:
print("INFO: Closing actual FileHandler.", flush=True)
_actual_file_handler.close()
except Exception as e: print(f"Error closing FileHandler: {e}", flush=True)
_actual_file_handler = None
# Clear the root logger's handlers (should only be the QueuePuttingHandler)
root_logger = logging.getLogger()
for handler in root_logger.handlers[:]:
try:
if isinstance(handler, QueuePuttingHandler): # Specifically target our handler
handler.close()
root_logger.removeHandler(handler)
print(f"INFO: Removed and closed QueuePuttingHandler: {handler}", flush=True)
except Exception as e_rem_qph:
print(f"Error removing QueuePuttingHandler: {e_rem_qph}", flush=True)
_global_log_queue = None # Allow it to be garbage collected
_tk_root_instance_for_processing = None # Clear reference
_base_formatter = None
print("INFO: Centralized logging system shutdown complete.", flush=True)