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):
module_logger.info(
"Controller: Application exit requested. Cleaning up resources."
module_logger.info("Controller: Application exit requested. Cleaning up resources.")
# MODIFIED: Added shutdown for MapCanvasManager's worker thread.
# WHY: To ensure the map rendering thread is properly terminated before the application exits.
# WHERE: At the beginning of the application exit process.
# HOW: Checking for the existence of main_window, map_manager_instance,
# and the shutdown_worker method, then calling it within a try-except block.
if self.main_window and \
hasattr(self.main_window, "map_manager_instance") and \
self.main_window.map_manager_instance is not None:
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
)
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)
else:
module_logger.debug(
"Controller: Live monitoring/adapter not active or already stopped during app exit."
)
module_logger.debug("Controller: MapCanvasManager instance found, but no 'shutdown_worker' method.")
else:
module_logger.debug("Controller: No MapCanvasManager instance found or main window not set; skipping map worker shutdown.")
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:
module_logger.debug(
"Controller: Closing DataStorage connection during app exit."
)
module_logger.debug("Controller: Closing DataStorage connection during app exit.")
try:
self.data_storage.close_connection()
except Exception as e_db_close:
module_logger.error(
f"Error closing DataStorage: {e_db_close}", exc_info=True
)
module_logger.error(f"Error closing DataStorage: {e_db_close}", exc_info=True)
finally:
self.data_storage = None
self.data_storage = None # Ensure it's cleared
module_logger.info("Controller: Cleanup on application exit finished.")
def start_history_monitoring(self):

View File

@ -14,7 +14,7 @@ import logging # Import per i livelli di logging (es. logging.ERROR)
# Relative imports
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
# Importa le costanti di stato GUI dal modulo utils centralizzato
@ -824,13 +824,14 @@ class MainWindow:
exc_info=True,
)
module_logger.info("Shutting down GUI logging.")
module_logger.info("Shutting down logging system.") # Messaggio aggiornato
try:
shutdown_gui_logging() # Assicurati che questa funzione sia robusta
except Exception as e: # Logga eventuali errori, ma non fermare la chiusura
module_logger.error(
f"Error during shutdown_gui_logging: {e}", exc_info=True
)
# MODIFIED: Changed shutdown_gui_logging() to shutdown_logging_system()
# WHY: The function name was changed in the logger.py module.
# HOW: Updated the function call.
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():
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_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 ---
# Number of decimal places to display for coordinates in the info panel.
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.

View File

@ -1,295 +1,235 @@
# FlightMonitor/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 queue import Queue, Empty as QueueEmpty # Renamed for clarity
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):
"""
A logging handler that directs log messages to a Tkinter Text widget
in a thread-safe manner using an internal queue and root.after().
A logging handler that directs log messages to a Tkinter Text widget.
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__(
self,
text_widget: tk.Text,
root_tk_instance: tk.Tk,
text_widget: tk.Text, # ScrolledText is a tk.Text
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__()
self.text_widget = text_widget
self.root_tk_instance = root_tk_instance
self.log_queue = Queue()
self.level_colors = level_colors
self.queue_poll_interval_ms = queue_poll_interval_ms
self._after_id_log_processor: Optional[str] = None
self._is_active = True
self._is_active = True # Internal active state for this specific handler
self._widget_update_queue = Queue()
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 (
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.",
flush=True,
)
print("ERROR: TkinterTextHandler initialized with an invalid or non-existent text_widget.", flush=True)
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()
self._root_for_widget_update
and hasattr(self._root_for_widget_update, "winfo_exists")
and self._root_for_widget_update.winfo_exists()
):
print(
"Warning: TkinterTextHandler initialized with an invalid or non-existent root_tk_instance.",
flush=True,
)
print("ERROR: TkinterTextHandler initialized with an invalid root for widget update.", flush=True)
self._is_active = False
return
if self._is_active:
self._configure_tags()
self._process_widget_update_queue() # Start its own update loop
def _configure_tags(self):
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} during TkinterTextHandler init.",
flush=True,
)
pass
if self._is_active:
self._process_log_queue()
print(f"Warning: Could not configure tag for {level_name} in TkinterTextHandler.", flush=True)
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
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:
msg = self.format(record)
msg = self.format(record) # Format the record using the formatter set on this handler
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:
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):
if not self._is_active:
if (
self._after_id_log_processor
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
def _process_widget_update_queue(self):
if not self._is_active or \
not self.text_widget.winfo_exists() or \
not self._root_for_widget_update.winfo_exists():
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
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
return
try:
while self._is_active:
try:
level_name, msg = self.log_queue.get(block=False, timeout=0.01)
except QueueEmpty:
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
had_messages = False
try:
while not self._widget_update_queue.empty():
had_messages = True
level_name, msg = self._widget_update_queue.get_nowait()
if self.text_widget.winfo_exists():
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.see(tk.END)
if self.text_widget.winfo_exists():
self.text_widget.configure(state=tk.DISABLED)
self.log_queue.task_done()
else: # Widget destroyed during processing
self._is_active = False; break
self._widget_update_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:
print(
f"TkinterTextHandler TclError during widget update: {e_tcl_inner}. Attempting to stop handler.",
flush=True,
)
except tk.TclError as e_tcl:
print(f"TkinterTextHandler TclError during widget update: {e_tcl}. Handler stopping.", 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,
)
except Exception as e_update:
print(f"Unexpected error updating TkinterTextHandler widget: {e_update}. Handler stopping.", 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._is_active and self._root_for_widget_update.winfo_exists():
self._internal_after_id = self._root_for_widget_update.after(
self._internal_poll_interval_ms, self._process_widget_update_queue
)
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
return
except Exception as e_outer:
print(
f"Unexpected error in TkinterTextHandler._process_log_queue (outer): {e_outer}",
flush=True,
)
self._is_active = False
return
if (
self._is_active
and hasattr(self.root_tk_instance, "winfo_exists")
and self.root_tk_instance.winfo_exists()
):
try:
self._after_id_log_processor = self.root_tk_instance.after(
self.queue_poll_interval_ms, self._process_log_queue
)
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
else: # Ensure it's cleaned up if we are stopping
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
def close(self):
"""
Cleans up resources, like stopping the log queue processor.
Called when the handler is removed or logging system shuts down.
"""
print("INFO: TkinterTextHandler close called.", flush=True)
self._is_active = False
if self._after_id_log_processor:
if (
hasattr(self.root_tk_instance, "winfo_exists")
and self.root_tk_instance.winfo_exists()
):
if self._internal_after_id and hasattr(self, '_root_for_widget_update') and \
self._root_for_widget_update and self._root_for_widget_update.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:
self.log_queue.get_nowait()
pass
except QueueEmpty:
break
except Exception as e_q_drain:
print(f"Error draining log queue during close: {e_q_drain}", flush=True)
break
self._root_for_widget_update.after_cancel(self._internal_after_id)
except Exception: pass
self._internal_after_id = None
# Drain its internal queue
while not self._widget_update_queue.empty():
try: self._widget_update_queue.get_nowait(); self._widget_update_queue.task_done()
except Exception: break
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(
@ -297,195 +237,157 @@ def setup_logging(
root_tk_instance: Optional[tk.Tk] = None,
logging_config_dict: Optional[Dict[str, Any]] = None,
):
"""
Sets up application-wide logging based on the provided configuration dictionary.
If logging_config_dict is None, uses basic default settings (console only).
"""
global _tkinter_handler_instance
global _global_log_queue, _actual_console_handler, _actual_file_handler
global _actual_tkinter_handler, _logging_system_active, _tk_root_instance_for_processing
global _log_processor_after_id, _base_formatter
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:
print(
"Warning: No logging_config_dict provided to setup_logging. Using basic default console logging.",
flush=True,
)
print("Warning: No logging_config_dict. Using basic console-only defaults for queued logging.", flush=True)
logging_config_dict = {
"default_root_level": logging.INFO,
"specific_levels": {},
"format": "%(asctime)s - %(name)s - %(levelname)s - %(message)s",
"default_root_level": logging.INFO, "specific_levels": {},
"format": "%(asctime)s [%(levelname)-8s] %(name)-25s : %(message)s",
"date_format": "%Y-%m-%d %H:%M:%S",
"colors": {logging.INFO: "black"},
"queue_poll_interval_ms": 100,
"enable_console": True,
"enable_file": False,
"colors": {logging.INFO: "black", logging.DEBUG: "blue", logging.WARNING:"orange", logging.ERROR:"red", logging.CRITICAL:"purple"},
"queue_poll_interval_ms": 100, # For TkinterTextHandler's internal queue
"enable_console": True, "enable_file": False,
}
# --- Extract Config ---
root_log_level = logging_config_dict.get("default_root_level", logging.INFO)
specific_levels = logging_config_dict.get("specific_levels", {})
log_format_str = logging_config_dict.get(
"format", "%(asctime)s - %(name)s - %(levelname)s - %(message)s"
)
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")
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)
# 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)
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()
handlers_to_remove = []
# Remove ALL existing handlers from the root logger to ensure clean state
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:
handler.close()
handler.close() # Close handler first
root_logger.removeHandler(handler)
module_logger_internal = logging.getLogger(__name__)
module_logger_internal.debug(f"Removed and closed old handler: {handler}")
except Exception as e:
module_logger_internal = logging.getLogger(__name__)
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
print(f"DEBUG: Removed and closed old handler from root: {handler}", flush=True)
except Exception as e_rem:
print(f"Error removing/closing old handler {handler}: {e_rem}", flush=True)
root_logger.handlers = [] # Ensure it's empty
root_logger.setLevel(root_log_level)
module_logger_internal = logging.getLogger(__name__)
module_logger_internal.debug(
f"Root logger level set to {logging.getLevelName(root_logger.level)}"
)
print(f"INFO: Root logger level set to {logging.getLevelName(root_logger.level)}.", flush=True)
# --- Configure Specific Logger Levels ---
for logger_name, level in specific_levels.items():
named_logger = logging.getLogger(logger_name)
named_logger.setLevel(level)
module_logger_internal.debug(
f"Logger '{logger_name}' level set to {logging.getLevelName(named_logger.level)}"
)
# Ensure propagation is on if they are not root, or if they should also go to root's QueuePuttingHandler
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:
try:
console_handler = logging.StreamHandler()
console_handler.setFormatter(formatter)
root_logger.addHandler(console_handler)
module_logger_internal.debug("Console logging enabled and handler added.")
_actual_console_handler = logging.StreamHandler() # Defaults to stderr
_actual_console_handler.setFormatter(_base_formatter)
_actual_console_handler.setLevel(logging.DEBUG) # Let console handler decide based on record level
print("INFO: Actual ConsoleHandler created.", flush=True)
except Exception as e:
module_logger_internal.error(
f"Error adding console handler: {e}", exc_info=False
)
print(f"ERROR: Failed to create actual ConsoleHandler: {e}", flush=True)
_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:
try:
from logging.handlers import RotatingFileHandler
file_path = logging_config_dict.get("file_path", "app.log")
file_max_bytes = logging_config_dict.get("file_max_bytes", 10 * 1024 * 1024)
file_backup_count = logging_config_dict.get("file_backup_count", 5)
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."
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_backup_count = logging_config_dict.get("file_backup_count", 3)
_actual_file_handler = logging.handlers.RotatingFileHandler(
file_path, maxBytes=file_max_bytes, backupCount=file_backup_count, encoding='utf-8'
)
_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:
module_logger_internal.error(
f"Error adding file handler to '{file_path}': {e}", exc_info=True
)
print(f"ERROR: Failed to create actual FileHandler for '{file_path}': {e}", flush=True)
_actual_file_handler = None
if gui_log_widget and root_tk_instance:
is_widget_valid = (
isinstance(gui_log_widget, (tk.Text, ScrolledText))
and hasattr(gui_log_widget, "winfo_exists")
and gui_log_widget.winfo_exists()
isinstance(gui_log_widget, (tk.Text, ScrolledText)) and
hasattr(gui_log_widget, "winfo_exists") and gui_log_widget.winfo_exists()
)
is_root_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:
if is_widget_valid:
try:
_tkinter_handler_instance = TkinterTextHandler(
_actual_tkinter_handler = TkinterTextHandler(
text_widget=gui_log_widget,
root_tk_instance=root_tk_instance,
level_colors=level_colors,
queue_poll_interval_ms=queue_poll_interval_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."
root_tk_instance_for_widget_update=root_tk_instance, # Pass root for its own after loop
internal_poll_interval_ms=tkinter_handler_poll_ms
)
_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:
_tkinter_handler_instance = None
print(
f"ERROR: Failed to initialize TkinterTextHandler: {e}", flush=True
)
print(f"ERROR: Failed to create actual TkinterTextHandler: {e}", flush=True)
_actual_tkinter_handler = None
else:
print("ERROR: GUI log widget invalid or non-existent, cannot create TkinterTextHandler.", flush=True)
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.",
flush=True,
)
elif not gui_log_widget and root_tk_instance:
print(
"DEBUG: Root Tk instance provided, but no GUI log widget. GUI logger not initialized.",
flush=True,
print("WARNING: GUI log widget provided, but root_tk_instance missing for TkinterTextHandler.", flush=True)
# --- Add ONLY the QueuePuttingHandler to the root logger ---
if _global_log_queue is not None:
queue_putter = QueuePuttingHandler(handler_queue=_global_log_queue)
queue_putter.setLevel(logging.DEBUG) # Capture all messages from root level downwards
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:
@ -496,30 +398,84 @@ def get_logger(name: str) -> logging.Logger:
return logging.getLogger(name)
def shutdown_gui_logging():
def shutdown_logging_system():
"""
Closes and removes the TkinterTextHandler instance from the root logger.
This should be called before the Tkinter root window is destroyed.
Shuts down the centralized logging system, processing remaining logs
and closing actual handlers.
"""
global _tkinter_handler_instance
root_logger = logging.getLogger()
if _tkinter_handler_instance:
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)
global _logging_system_active, _log_processor_after_id
global _actual_console_handler, _actual_file_handler, _actual_tkinter_handler
global _global_log_queue, _tk_root_instance_for_processing
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:
_tkinter_handler_instance.close()
print("INFO: GUI logging handler has been shut down.", flush=True)
except Exception as e:
print(f"Error closing GUI logging handler: {e}", flush=True)
finally:
_tkinter_handler_instance = None
_tk_root_instance_for_processing.after_cancel(_log_processor_after_id)
print("INFO: Cancelled global log queue processor task.", flush=True)
except Exception as e_cancel:
print(f"Warning: Error cancelling log processor task: {e_cancel}", flush=True)
_log_processor_after_id = None
# Attempt to process any remaining logs in the global queue
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("DEBUG: No active GUI logging handler to shut down.", flush=True)
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)