fix problem with stop monitoring and close application

This commit is contained in:
VALLONGOL 2025-05-16 09:34:08 +02:00
parent 23232b6039
commit bdea729783
5 changed files with 582 additions and 247 deletions

View File

@ -23,6 +23,7 @@ from typing import List, Optional, Dict, Any # For type hints
module_logger = get_logger(__name__) # flightmonitor.controller.app_controller module_logger = get_logger(__name__) # flightmonitor.controller.app_controller
GUI_QUEUE_CHECK_INTERVAL_MS = 150 # Check queue a bit less frequently GUI_QUEUE_CHECK_INTERVAL_MS = 150 # Check queue a bit less frequently
ADAPTER_JOIN_TIMEOUT_SECONDS = 2.0 # MODIFICA: Timeout per l'attesa del thread adapter
# Define GUI status levels that MainWindow.update_semaphore_and_status expects # Define GUI status levels that MainWindow.update_semaphore_and_status expects
GUI_STATUS_OK = "OK" GUI_STATUS_OK = "OK"
@ -78,7 +79,7 @@ class AppController:
return return
try: try:
while not self.flight_data_queue.empty(): while not self.flight_data_queue.empty(): # Process all available messages
message: AdapterMessage = self.flight_data_queue.get_nowait() message: AdapterMessage = self.flight_data_queue.get_nowait()
self.flight_data_queue.task_done() # Important for queue management self.flight_data_queue.task_done() # Important for queue management
@ -107,10 +108,13 @@ class AppController:
self.main_window.display_flights_on_canvas(flight_states_payload, self._active_bounding_box) self.main_window.display_flights_on_canvas(flight_states_payload, self._active_bounding_box)
gui_message = f"Live data: {len(flight_states_payload)} aircraft tracked." if flight_states_payload else "Live data: No aircraft in area." gui_message = f"Live data: {len(flight_states_payload)} aircraft tracked." if flight_states_payload else "Live data: No aircraft in area."
self.main_window.update_semaphore_and_status(GUI_STATUS_OK, gui_message) # Solo aggiorna se ci sono dati o se è cambiata la situazione (es. da dati a nessun dato)
if self.main_window and hasattr(self.main_window, 'update_semaphore_and_status'):
self.main_window.update_semaphore_and_status(GUI_STATUS_OK, gui_message)
else: else:
module_logger.warning("Received flight_data message with None payload.") module_logger.warning("Received flight_data message with None payload.")
self.main_window.update_semaphore_and_status(GUI_STATUS_WARNING, "Received empty data payload.") if self.main_window and hasattr(self.main_window, 'update_semaphore_and_status'):
self.main_window.update_semaphore_and_status(GUI_STATUS_WARNING, "Received empty data payload.")
elif message_type == MSG_TYPE_ADAPTER_STATUS: elif message_type == MSG_TYPE_ADAPTER_STATUS:
status_code = message.get("status_code") status_code = message.get("status_code")
@ -139,8 +143,8 @@ class AppController:
elif status_code == STATUS_PERMANENT_FAILURE: elif status_code == STATUS_PERMANENT_FAILURE:
gui_status_level = GUI_STATUS_ERROR gui_status_level = GUI_STATUS_ERROR
gui_message = message.get("message", "Too many API errors. Live updates stopped.") gui_message = message.get("message", "Too many API errors. Live updates stopped.")
elif status_code == STATUS_STOPPED: elif status_code == STATUS_STOPPED: # Gestito da stop_live_monitoring o on_application_exit
gui_status_level = GUI_STATUS_OK # Or UNKNOWN if we prefer neutral for stopped state gui_status_level = GUI_STATUS_OK
gui_message = message.get("message", "Live data adapter stopped.") gui_message = message.get("message", "Live data adapter stopped.")
if self.main_window and self.main_window.root.winfo_exists(): if self.main_window and self.main_window.root.winfo_exists():
@ -148,13 +152,12 @@ class AppController:
if status_code == STATUS_PERMANENT_FAILURE: if status_code == STATUS_PERMANENT_FAILURE:
module_logger.critical("Permanent failure from adapter. Stopping live monitoring via controller.") module_logger.critical("Permanent failure from adapter. Stopping live monitoring via controller.")
self.stop_live_monitoring(from_error=True) # Resets GUI via _reset_gui_to_stopped_state self.stop_live_monitoring(from_error=True)
else: else:
module_logger.warning(f"Unknown message type from adapter: '{message_type}'") module_logger.warning(f"Unknown message type from adapter: '{message_type}'")
if self.main_window and self.main_window.root.winfo_exists(): if self.main_window and self.main_window.root.winfo_exists():
self.main_window.update_semaphore_and_status(GUI_STATUS_WARNING, f"Unknown adapter message: {message_type}") self.main_window.update_semaphore_and_status(GUI_STATUS_WARNING, f"Unknown adapter message: {message_type}")
except QueueEmpty: except QueueEmpty:
pass # Normal if queue is empty pass # Normal if queue is empty
except Exception as e: except Exception as e:
@ -162,7 +165,9 @@ class AppController:
if self.main_window and self.main_window.root.winfo_exists(): if self.main_window and self.main_window.root.winfo_exists():
self.main_window.update_semaphore_and_status(GUI_STATUS_ERROR, "Critical error processing data. See logs.") self.main_window.update_semaphore_and_status(GUI_STATUS_ERROR, "Critical error processing data. See logs.")
finally: finally:
if self.is_live_monitoring_active and self.main_window and self.main_window.root and self.main_window.root.winfo_exists(): # Riprogramma solo se il monitoraggio è attivo E il controller non è in fase di shutdown dell'adapter
if self.is_live_monitoring_active and \
self.main_window and self.main_window.root and self.main_window.root.winfo_exists():
try: try:
self._gui_after_id = self.main_window.root.after( self._gui_after_id = self.main_window.root.after(
GUI_QUEUE_CHECK_INTERVAL_MS, self._process_flight_data_queue GUI_QUEUE_CHECK_INTERVAL_MS, self._process_flight_data_queue
@ -175,7 +180,7 @@ class AppController:
def start_live_monitoring(self, bounding_box: Dict[str, float]): def start_live_monitoring(self, bounding_box: Dict[str, float]):
if not self.main_window: if not self.main_window:
module_logger.error("Controller: Main window not set. Cannot start live monitoring.") module_logger.error("Controller: Main window not set. Cannot start live monitoring.")
return # Should not happen if set_main_window was called return
if not self.data_storage: if not self.data_storage:
err_msg = "DataStorage not initialized. Live monitoring cannot start." err_msg = "DataStorage not initialized. Live monitoring cannot start."
@ -188,47 +193,43 @@ class AppController:
if self.is_live_monitoring_active: if self.is_live_monitoring_active:
module_logger.warning("Controller: Live monitoring already active. Start request ignored.") module_logger.warning("Controller: Live monitoring already active. Start request ignored.")
# self.main_window.update_semaphore_and_status(GUI_STATUS_WARNING, "Monitoring is already running.")
return return
module_logger.info(f"Controller: Starting live monitoring for bbox: {bounding_box}") module_logger.info(f"Controller: Starting live monitoring for bbox: {bounding_box}")
self._active_bounding_box = bounding_box self._active_bounding_box = bounding_box
# MainWindow's _start_monitoring has already set a "WARNING" state like "Attempting to start..."
# The adapter will soon send a STATUS_STARTING or STATUS_FETCHING message.
if self.flight_data_queue is None: if self.flight_data_queue is None:
self.flight_data_queue = Queue() self.flight_data_queue = Queue()
while not self.flight_data_queue.empty(): # Clear old messages while not self.flight_data_queue.empty():
try: self.flight_data_queue.get_nowait() try: self.flight_data_queue.get_nowait()
except QueueEmpty: break except QueueEmpty: break
else: self.flight_data_queue.task_done() # Assicurati che i task_done siano chiamati
# --- MODIFICA: Gestione più robusta del vecchio thread ---
if self.live_adapter_thread and self.live_adapter_thread.is_alive(): if self.live_adapter_thread and self.live_adapter_thread.is_alive():
module_logger.warning("Controller: Old LiveAdapter thread still alive. Stopping it first.") module_logger.warning("Controller: Old LiveAdapter thread still alive. Attempting to stop and join it first.")
self.live_adapter_thread.stop() self.live_adapter_thread.stop()
self.live_adapter_thread.join(timeout=1.0) # Wait a bit for it to stop self.live_adapter_thread.join(timeout=ADAPTER_JOIN_TIMEOUT_SECONDS)
if self.live_adapter_thread.is_alive(): if self.live_adapter_thread.is_alive():
module_logger.error("Controller: Old LiveAdapter thread did not stop in time! May cause issues.") module_logger.error("Controller: Old LiveAdapter thread did not stop in time! May cause issues. Discarding reference.")
self.live_adapter_thread = None # Try to discard it anyway self.live_adapter_thread = None # Resetta comunque
self.live_adapter_thread = OpenSkyLiveAdapter( self.live_adapter_thread = OpenSkyLiveAdapter(
output_queue=self.flight_data_queue, output_queue=self.flight_data_queue,
bounding_box=self._active_bounding_box, bounding_box=self._active_bounding_box,
# Other params use defaults from config via adapter's __init__
) )
self.live_adapter_thread.start() # This will trigger adapter to send initial status messages self.is_live_monitoring_active = True # Imposta PRIMA di avviare il thread e il polling
self.is_live_monitoring_active = True self.live_adapter_thread.start()
# The GUI status will be updated by messages processed in _process_flight_data_queue
if self._gui_after_id: # Cancel any pre-existing queue polling if self._gui_after_id:
if self.main_window.root.winfo_exists(): if self.main_window.root.winfo_exists():
try: self.main_window.root.after_cancel(self._gui_after_id) try: self.main_window.root.after_cancel(self._gui_after_id)
except tk.TclError: pass except tk.TclError: pass
self._gui_after_id = None self._gui_after_id = None
if self.main_window.root.winfo_exists(): # Schedule new queue polling if self.main_window.root.winfo_exists():
self._gui_after_id = self.main_window.root.after( self._gui_after_id = self.main_window.root.after(
10, # Check very soon for the first status message from adapter 10,
self._process_flight_data_queue self._process_flight_data_queue
) )
module_logger.info("Controller: Live monitoring adapter thread started and queue polling scheduled.") module_logger.info("Controller: Live monitoring adapter thread started and queue polling scheduled.")
@ -237,49 +238,100 @@ class AppController:
def stop_live_monitoring(self, from_error: bool = False): def stop_live_monitoring(self, from_error: bool = False):
module_logger.info(f"Controller: Attempting to stop live monitoring. (Triggered by error: {from_error})") module_logger.info(f"Controller: Attempting to stop live monitoring. (Triggered by error: {from_error})")
if self._gui_after_id: # Stop GUI polling the queue # Salva il riferimento al thread corrente che stiamo per fermare
adapter_thread_to_stop = self.live_adapter_thread
# 1. Impedisci ulteriori scheduling e processamento come "live"
self.is_live_monitoring_active = False # Prima cosa!
if self._gui_after_id:
if self.main_window and self.main_window.root and self.main_window.root.winfo_exists(): if self.main_window and self.main_window.root and self.main_window.root.winfo_exists():
try: self.main_window.root.after_cancel(self._gui_after_id) try: self.main_window.root.after_cancel(self._gui_after_id)
except tk.TclError: pass except Exception: pass # Ignora errori qui, stiamo chiudendo
self._gui_after_id = None self._gui_after_id = None
module_logger.debug("Controller: Cancelled GUI queue check callback.") module_logger.debug("Controller: Cancelled GUI queue check callback.")
adapter_to_stop = self.live_adapter_thread # 2. Se il thread adapter esiste ed è vivo, segnalagli di fermarsi.
if adapter_to_stop and adapter_to_stop.is_alive(): if adapter_thread_to_stop and adapter_thread_to_stop.is_alive():
module_logger.debug(f"Controller: Signalling LiveAdapter thread ({adapter_to_stop.name}) to stop.") module_logger.debug(f"Controller: Signaling LiveAdapter thread ({adapter_thread_to_stop.name}) to stop.")
adapter_to_stop.stop() # Adapter will send STATUS_STOPPING then STATUS_STOPPED adapter_thread_to_stop.stop() # Questo setta _stop_event nell'adapter
# 3. Attendi che il thread adapter termini (join).
# Il thread adapter dovrebbe uscire dal suo loop e terminare run().
module_logger.debug(f"Controller: Waiting for LiveAdapter thread ({adapter_thread_to_stop.name}) to join...")
adapter_thread_to_stop.join(timeout=ADAPTER_JOIN_TIMEOUT_SECONDS + 1.0) # Aumenta un po' il timeout per sicurezza
if adapter_thread_to_stop.is_alive():
module_logger.error(f"Controller: LiveAdapter thread ({adapter_thread_to_stop.name}) did NOT join in time after stop signal! This is a problem.")
# Potrebbe essere necessario un cleanup più aggressivo o segnalare un errore critico.
else:
module_logger.info(f"Controller: LiveAdapter thread ({adapter_thread_to_stop.name}) joined successfully.")
else: else:
module_logger.debug("Controller: No active LiveAdapter thread to signal stop, or already stopped.") module_logger.debug("Controller: No active LiveAdapter thread to stop or already stopped.")
self.live_adapter_thread = None # Clear reference # 4. Rimuovi il riferimento al thread ora (dovrebbe essere terminato o timeout scaduto).
self.is_live_monitoring_active = False # Crucial to stop _process_flight_data_queue rescheduling if self.live_adapter_thread == adapter_thread_to_stop: # Controlla se è ancora lo stesso thread
# self._active_bounding_box = None # Optionally clear this self.live_adapter_thread = None
# The final GUI status (semaphore and text) will be set by: # 5. Svuota la coda da messaggi residui (inclusi STATUS_STOPPED o STATUS_PERMANENT_FAILURE).
# 1. The STATUS_STOPPED message from the adapter if it stops cleanly. # Questo svuotamento avviene DOPO il join, quindi il thread adapter non sta più scrivendo.
# 2. The STATUS_PERMANENT_FAILURE message if that was the cause. # È importante processare qui eventuali messaggi di stato finali dall'adapter.
# 3. MainWindow's _reset_gui_to_stopped_state if called directly by user stop button. final_adapter_status_processed = False
# If `from_error` is true, it means a PERMANENT_FAILURE likely occurred and already if self.flight_data_queue:
# triggered the GUI reset. module_logger.debug("Controller: Processing any final messages from adapter queue post-join...")
if not from_error and self.main_window and self.main_window.root.winfo_exists(): while not self.flight_data_queue.empty():
# This provides an immediate feedback if stop was clean from controller side, try:
# but might be overwritten by adapter's final STATUS_STOPPED. message = self.flight_data_queue.get_nowait()
# Let MainWindow._reset_gui_to_stopped_state handle this when user clicks stop. self.flight_data_queue.task_done()
# If called programmatically (not from error), a "stopping" status is okay.
# self.main_window.update_semaphore_and_status(GUI_STATUS_OK, "Live monitoring stopping...")
pass
module_logger.info("Controller: Live monitoring process requested to stop. Adapter signalled.") # Processa specificamente i messaggi di stato finali
msg_type = message.get("type")
status_code = message.get("status_code")
if msg_type == MSG_TYPE_ADAPTER_STATUS:
module_logger.info(f"Controller: Processing final adapter status from queue: {status_code} - {message.get('message')}")
# Aggiorna la GUI con questo stato finale
if self.main_window and self.main_window.root.winfo_exists():
gui_status_level = GUI_STATUS_OK
if status_code == STATUS_PERMANENT_FAILURE: gui_status_level = GUI_STATUS_ERROR
self.main_window.update_semaphore_and_status(gui_status_level, message.get('message', 'Adapter stopped.'))
final_adapter_status_processed = True
# else:
# module_logger.debug(f"Controller: Discarding other message type '{msg_type}' from queue after stop.")
except QueueEmpty:
break
except Exception as e:
module_logger.error(f"Controller: Error processing/discarding message from queue: {e}")
break
module_logger.debug("Controller: Finished processing/discarding final adapter queue messages.")
# 6. Aggiorna la GUI allo stato fermato, se non già fatto da un messaggio finale dell'adapter.
if not from_error and not final_adapter_status_processed:
if self.main_window and self.main_window.root.winfo_exists():
if hasattr(self.main_window, '_reset_gui_to_stopped_state'):
self.main_window._reset_gui_to_stopped_state("Monitoring stopped.")
else:
self.main_window.update_semaphore_and_status(GUI_STATUS_OK, "Monitoring stopped.")
elif from_error and not final_adapter_status_processed: # Fermato per errore, ma nessun messaggio di errore processato dalla coda
if self.main_window and self.main_window.root.winfo_exists():
self.main_window.update_semaphore_and_status(GUI_STATUS_ERROR, "Monitoring stopped due to an error.")
module_logger.info("Controller: Live monitoring shutdown sequence fully completed.")
def on_application_exit(self): 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.")
if self.is_live_monitoring_active or (self.live_adapter_thread and self.live_adapter_thread.is_alive()): # Controlla specificamente se il thread esiste ED è vivo
# is_live_monitoring_active potrebbe essere già False, ma il thread potrebbe essere in fase di join
is_adapter_running = self.live_adapter_thread and self.live_adapter_thread.is_alive()
if self.is_live_monitoring_active or is_adapter_running:
module_logger.debug("Controller: Live monitoring/adapter active during app exit, stopping it.") module_logger.debug("Controller: Live monitoring/adapter active during app exit, stopping it.")
self.stop_live_monitoring(from_error=False) # Treat as a normal, non-error stop # Chiamare stop_live_monitoring qui si occuperà del join e della pulizia della coda
self.stop_live_monitoring(from_error=False)
else: else:
module_logger.debug("Controller: Live monitoring/adapter was not active during app exit.") module_logger.debug("Controller: Live monitoring/adapter was not active or already stopped during app exit.")
if self.data_storage: if self.data_storage:
module_logger.debug("Controller: Closing DataStorage connection during app exit.") module_logger.debug("Controller: Closing DataStorage connection during app exit.")

View File

@ -1,5 +1,7 @@
# FlightMonitor/data/config.py # FlightMonitor/data/config.py
from typing import Optional
""" """
Global configurations for the FlightMonitor application. Global configurations for the FlightMonitor application.
@ -20,6 +22,13 @@ OPENSKY_API_URL: str = "https://opensky-network.org/api/states/all"
# before considering the request as timed out. # before considering the request as timed out.
DEFAULT_API_TIMEOUT_SECONDS: int = 15 DEFAULT_API_TIMEOUT_SECONDS: int = 15
# --- Flag per Mock API ---
# Set to True to use mock data instead of making real API calls to OpenSky.
# This is useful for development to avoid consuming API credits and for offline testing.
USE_MOCK_OPENSKY_API: bool = True # Imposta a False per chiamate reali
MOCK_API_FLIGHT_COUNT: int = 5 # Numero di aerei finti da generare se USE_MOCK_OPENSKY_API è True
MOCK_API_ERROR_SIMULATION: Optional[str] = None # Es: "RATE_LIMITED", "HTTP_ERROR", None per successo
# --- GUI Configuration --- # --- GUI Configuration ---

View File

@ -10,6 +10,7 @@ import time
import threading import threading
from queue import Queue, Full as QueueFull # Import QueueFull for specific exception handling from queue import Queue, Full as QueueFull # Import QueueFull for specific exception handling
from typing import List, Optional, Dict, Any, Union from typing import List, Optional, Dict, Any, Union
import random # Aggiungi per il mock
# Relative imports # Relative imports
from . import config as app_config from . import config as app_config
@ -78,6 +79,33 @@ class OpenSkyLiveAdapter(threading.Thread):
f"Base Interval: {self.base_polling_interval}s, API Timeout: {self.api_timeout}s" f"Base Interval: {self.base_polling_interval}s, API Timeout: {self.api_timeout}s"
) )
def _generate_mock_flight_state(self, icao_suffix: int) -> CanonicalFlightState:
"""Helper to generate a single mock CanonicalFlightState."""
now = time.time()
lat_center = (self.bounding_box["lat_min"] + self.bounding_box["lat_max"]) / 2
lon_center = (self.bounding_box["lon_min"] + self.bounding_box["lon_max"]) / 2
lat_span = self.bounding_box["lat_max"] - self.bounding_box["lat_min"]
lon_span = self.bounding_box["lon_max"] - self.bounding_box["lon_min"]
return CanonicalFlightState(
icao24=f"mock{icao_suffix:02x}",
callsign=f"MOCK{icao_suffix:02X}",
origin_country="Mockland",
timestamp=now,
last_contact_timestamp=now,
latitude=round(lat_center + random.uniform(-lat_span / 2.1, lat_span / 2.1), 4),
longitude=round(lon_center + random.uniform(-lon_span / 2.1, lon_span / 2.1), 4),
baro_altitude_m=random.uniform(1000, 12000),
on_ground=random.choice([True, False]),
velocity_mps=random.uniform(50, 250),
true_track_deg=random.uniform(0, 360),
vertical_rate_mps=random.uniform(-10, 10),
squawk=str(random.randint(1000, 7777)),
spi=random.choice([True, False]),
position_source="MOCK_GENERATOR",
raw_data_provider=f"{PROVIDER_NAME}-Mock"
)
def _send_status_to_queue(self, status_code: str, message: str, details: Optional[Dict] = None): def _send_status_to_queue(self, status_code: str, message: str, details: Optional[Dict] = None):
"""Helper to put a status message into the output queue.""" """Helper to put a status message into the output queue."""
try: try:
@ -97,9 +125,13 @@ class OpenSkyLiveAdapter(threading.Thread):
def stop(self): def stop(self):
"""Signals the thread to stop its execution loop and sends a STOPPING status.""" """Signals the thread to stop its execution loop and sends a STOPPING status."""
# Non inviare STATUS_STOPPING qui, perché potrebbe non essere processato
# se il controller è già in attesa o se la coda è bloccata.
# Invieremo STATUS_STOPPED alla fine di run(), se possibile.
# L'importante è settare l'evento.
module_logger.info(f"Stop signal received for {self.name}. Signaling stop event.") module_logger.info(f"Stop signal received for {self.name}. Signaling stop event.")
self._send_status_to_queue(STATUS_STOPPING, "Stop signal received, attempting to terminate.")
self._stop_event.set() self._stop_event.set()
# Rimuoviamo _send_status_to_queue(STATUS_STOPPING, ...) da qui
def _parse_state_vector(self, raw_sv: list) -> Optional[CanonicalFlightState]: def _parse_state_vector(self, raw_sv: list) -> Optional[CanonicalFlightState]:
@ -149,22 +181,58 @@ class OpenSkyLiveAdapter(threading.Thread):
def _perform_api_request(self) -> Dict[str, Any]: def _perform_api_request(self) -> Dict[str, Any]:
""" """
Performs API request and returns a structured result. Performs API request or generates mock data based on config.
Result keys: 'data' (List[CanonicalFlightState]) on success, Returns a structured result.
'error_type' (str, e.g. STATUS_RATE_LIMITED) on failure,
plus other error details.
""" """
# --- MODIFICA INIZIO: Logica Mock API ---
if app_config.USE_MOCK_OPENSKY_API:
module_logger.info(f"{self.name}: Using MOCK API data as per configuration.")
self._send_status_to_queue(STATUS_FETCHING, "Generating mock flight data...")
time.sleep(0.5) # Simula una piccola latenza di rete
if app_config.MOCK_API_ERROR_SIMULATION == "RATE_LIMITED":
self._consecutive_api_errors += 1
self._in_backoff_mode = True
delay = self._calculate_next_backoff_delay("60") # Simula Retry-After 60s
err_msg = f"MOCK: Rate limited. Errors: {self._consecutive_api_errors}. Retrying in {delay:.1f}s."
module_logger.warning(f"{self.name}: {err_msg}")
return {"error_type": STATUS_RATE_LIMITED, "delay": delay, "message": err_msg, "consecutive_errors": self._consecutive_api_errors}
if app_config.MOCK_API_ERROR_SIMULATION == "HTTP_ERROR":
self._consecutive_api_errors += 1
self._in_backoff_mode = True
delay = self._calculate_next_backoff_delay()
err_msg = f"MOCK: HTTP error 500. Errors: {self._consecutive_api_errors}. Retrying in {delay:.1f}s."
module_logger.error(f"{self.name}: {err_msg}", exc_info=False)
return {"error_type": STATUS_API_ERROR_TEMPORARY, "status_code": 500, "message": err_msg, "delay": delay, "consecutive_errors": self._consecutive_api_errors}
# Se non è un errore simulato, genera dati mock
mock_states: List[CanonicalFlightState] = []
for i in range(app_config.MOCK_API_FLIGHT_COUNT):
mock_states.append(self._generate_mock_flight_state(i + 1))
module_logger.info(f"{self.name}: Generated {len(mock_states)} mock flight states.")
self._reset_error_state() # Mock success, reset error state
return {"data": mock_states}
# --- MODIFICA FINE: Logica Mock API ---
# Codice originale per le chiamate API reali (come prima)
if not self.bounding_box: if not self.bounding_box:
return {"error_type": STATUS_API_ERROR_TEMPORARY, "message": "Bounding box not set."} return {"error_type": STATUS_API_ERROR_TEMPORARY, "message": "Bounding box not set."}
params = { params = {
"lamin": self.bounding_box["lat_min"], "lomin": self.bounding_box["lon_min"], "lamin": self.bounding_box["lat_min"], "lomin": self.bounding_box["lon_min"],
"lamax": self.bounding_box["lat_max"], "lomax": self.bounding_box["lon_max"], "lamax": self.bounding_box["lat_max"], "lomax": self.bounding_box["lon_max"],
} }
self._send_status_to_queue(STATUS_FETCHING, f"Requesting data for bbox: {self.bounding_box}") self._send_status_to_queue(STATUS_FETCHING, f"Requesting REAL data for bbox: {self.bounding_box}")
response: Optional[requests.Response] = None # Define response here for wider scope response: Optional[requests.Response] = None
try: try:
response = requests.get(app_config.OPENSKY_API_URL, params=params, timeout=self.api_timeout) response = requests.get(app_config.OPENSKY_API_URL, params=params, timeout=self.api_timeout)
# ... (resto della logica API reale come prima, assicurati che NON ci sia 'return "test"') ...
# Assicurati che tutti i percorsi di ritorno qui restituiscano un dizionario corretto.
# Ad esempio, il 'return {"data": canonical_states}' è corretto.
# Anche i ritorni per errori come STATUS_RATE_LIMITED sono dizionari corretti.
module_logger.debug(f"{self.name}: API Response Status: {response.status_code} {response.reason}") module_logger.debug(f"{self.name}: API Response Status: {response.status_code} {response.reason}")
if response.status_code == 429: # Rate limit if response.status_code == 429: # Rate limit
@ -175,9 +243,9 @@ class OpenSkyLiveAdapter(threading.Thread):
module_logger.warning(f"{self.name}: {err_msg}") module_logger.warning(f"{self.name}: {err_msg}")
return {"error_type": STATUS_RATE_LIMITED, "delay": delay, "message": err_msg, "consecutive_errors": self._consecutive_api_errors} return {"error_type": STATUS_RATE_LIMITED, "delay": delay, "message": err_msg, "consecutive_errors": self._consecutive_api_errors}
response.raise_for_status() # Other HTTP errors response.raise_for_status()
self._reset_error_state() # Success, reset error counters self._reset_error_state()
response_data = response.json() response_data = response.json()
raw_states_list = response_data.get("states") raw_states_list = response_data.get("states")
canonical_states: List[CanonicalFlightState] = [] canonical_states: List[CanonicalFlightState] = []
@ -189,9 +257,9 @@ class OpenSkyLiveAdapter(threading.Thread):
module_logger.info(f"{self.name}: Fetched and parsed {len(canonical_states)} flight states.") module_logger.info(f"{self.name}: Fetched and parsed {len(canonical_states)} flight states.")
else: else:
module_logger.info(f"{self.name}: API returned no flight states ('states' is null or empty).") module_logger.info(f"{self.name}: API returned no flight states ('states' is null or empty).")
return {"data": canonical_states} # Success return {"data": canonical_states}
except requests.exceptions.HTTPError as http_err: # Other 4xx/5xx except requests.exceptions.HTTPError as http_err:
self._consecutive_api_errors += 1; self._in_backoff_mode = True self._consecutive_api_errors += 1; self._in_backoff_mode = True
delay = self._calculate_next_backoff_delay() delay = self._calculate_next_backoff_delay()
status_code = http_err.response.status_code if http_err.response else 'N/A' status_code = http_err.response.status_code if http_err.response else 'N/A'
@ -208,9 +276,9 @@ class OpenSkyLiveAdapter(threading.Thread):
self._consecutive_api_errors += 1; self._in_backoff_mode = True self._consecutive_api_errors += 1; self._in_backoff_mode = True
delay = self._calculate_next_backoff_delay() delay = self._calculate_next_backoff_delay()
err_msg = f"Request/JSON error: {e}. Errors: {self._consecutive_api_errors}. Retrying in {delay:.1f}s." err_msg = f"Request/JSON error: {e}. Errors: {self._consecutive_api_errors}. Retrying in {delay:.1f}s."
module_logger.error(f"{self.name}: {err_msg}", exc_info=True) # exc_info for these unexpected ones module_logger.error(f"{self.name}: {err_msg}", exc_info=True)
return {"error_type": STATUS_API_ERROR_TEMPORARY, "status_code": "REQUEST_JSON_ERROR", "message": err_msg, "delay": delay, "consecutive_errors": self._consecutive_api_errors} return {"error_type": STATUS_API_ERROR_TEMPORARY, "status_code": "REQUEST_JSON_ERROR", "message": err_msg, "delay": delay, "consecutive_errors": self._consecutive_api_errors}
except Exception as e: # Catch-all except Exception as e:
self._consecutive_api_errors += 1; self._in_backoff_mode = True self._consecutive_api_errors += 1; self._in_backoff_mode = True
delay = self._calculate_next_backoff_delay() delay = self._calculate_next_backoff_delay()
err_msg = f"Unexpected critical error: {e}. Errors: {self._consecutive_api_errors}. Retrying in {delay:.1f}s." err_msg = f"Unexpected critical error: {e}. Errors: {self._consecutive_api_errors}. Retrying in {delay:.1f}s."
@ -239,65 +307,107 @@ class OpenSkyLiveAdapter(threading.Thread):
self._in_backoff_mode = False self._in_backoff_mode = False
def run(self): def run(self):
module_logger.info(f"{self.name} thread started. Base polling interval: {self.base_polling_interval:.1f}s.") initial_settle_delay_seconds = 0.2
self._send_status_to_queue(STATUS_STARTING, "Adapter thread started, preparing initial fetch.") try:
if self._stop_event.wait(timeout=initial_settle_delay_seconds):
# module_logger.info(f"{self.name} thread received stop signal during initial settle delay. Terminating early.")
print(f"DEBUG_ADAPTER ({self.name}): Thread received stop signal during initial settle delay. Terminating early.", flush=True)
return
except Exception as e:
# module_logger.error(f"{self.name} unexpected error during initial settle delay: {e}", exc_info=True)
print(f"DEBUG_ADAPTER ({self.name}): ERROR - Unexpected error during initial settle delay: {e}", flush=True)
return
# module_logger.info(f"{self.name} thread started (after {initial_settle_delay_seconds}s delay). Base polling interval: {self.base_polling_interval:.1f}s.")
print(f"DEBUG_ADAPTER ({self.name}): Thread started (after {initial_settle_delay_seconds}s delay). Base polling interval: {self.base_polling_interval:.1f}s.", flush=True)
if not self._stop_event.is_set():
self._send_status_to_queue(STATUS_STARTING, "Adapter thread started, preparing initial fetch.")
else:
# module_logger.info(f"{self.name}: Stop event was set before sending initial STATUS_STARTING.")
print(f"DEBUG_ADAPTER ({self.name}): Stop event was set before sending initial STATUS_STARTING.", flush=True)
# --- Loop principale dell'adapter ---
while not self._stop_event.is_set(): while not self._stop_event.is_set():
if self._consecutive_api_errors >= MAX_CONSECUTIVE_ERRORS_THRESHOLD: if self._consecutive_api_errors >= MAX_CONSECUTIVE_ERRORS_THRESHOLD:
perm_fail_msg = f"Reached max ({self._consecutive_api_errors}) consecutive API errors. Stopping live updates." perm_fail_msg = f"Reached max ({self._consecutive_api_errors}) consecutive API errors. Stopping live updates."
module_logger.critical(f"{self.name}: {perm_fail_msg}") # module_logger.critical(f"{self.name}: {perm_fail_msg}")
self._send_status_to_queue(STATUS_PERMANENT_FAILURE, perm_fail_msg) print(f"DEBUG_ADAPTER ({self.name}): CRITICAL - {perm_fail_msg}", flush=True)
self.stop() # This will trigger sending STATUS_STOPPING then STATUS_STOPPED if not self._stop_event.is_set():
self._send_status_to_queue(STATUS_PERMANENT_FAILURE, perm_fail_msg)
break break
# Perform the API request and get a structured result api_result = self._perform_api_request()
api_result = self._perform_api_request() # This now sends its own STATUS_FETCHING
# Process the result and send appropriate message to the queue if self._stop_event.is_set():
if "data" in api_result: # Success print(f"DEBUG_ADAPTER ({self.name}): Stop event detected after API request. Exiting loop.", flush=True)
break
if "data" in api_result:
flight_data_payload: List[CanonicalFlightState] = api_result["data"] flight_data_payload: List[CanonicalFlightState] = api_result["data"]
try: try:
self.output_queue.put_nowait({ if not self._stop_event.is_set():
"type": MSG_TYPE_FLIGHT_DATA, self.output_queue.put_nowait({
"payload": flight_data_payload "type": MSG_TYPE_FLIGHT_DATA,
}) "payload": flight_data_payload
module_logger.debug(f"{self.name}: Sent {len(flight_data_payload)} flight states to queue.") })
except QueueFull: print(f"DEBUG_ADAPTER ({self.name}): Sent {len(flight_data_payload)} flight states to queue.", flush=True)
module_logger.warning(f"{self.name}: Output queue full. Discarding {len(flight_data_payload)} flight states.") # else:
# print(f"DEBUG_ADAPTER ({self.name}: Stop event set, not sending {len(flight_data_payload)} flight states to queue.", flush=True)
except QueueFull: # Dovrebbe essere importato 'from queue import Full as QueueFull'
module_logger.warning(f"{self.name}: Output queue full. Discarding {len(flight_data_payload)} flight states.") # Logger OK qui
except Exception as e: except Exception as e:
module_logger.error(f"{self.name}: Error putting flight data into queue: {e}", exc_info=True) module_logger.error(f"{self.name}: Error putting flight data into queue: {e}", exc_info=True) # Logger OK qui
elif "error_type" in api_result: # An error occurred elif "error_type" in api_result:
# Error details already logged by _perform_api_request error_details_for_controller = api_result.copy()
# Send a status update to the controller
error_details_for_controller = api_result.copy() # Make a copy to add type
error_details_for_controller["type"] = MSG_TYPE_ADAPTER_STATUS error_details_for_controller["type"] = MSG_TYPE_ADAPTER_STATUS
error_details_for_controller["status_code"] = api_result["error_type"] # Standardize key error_details_for_controller["status_code"] = api_result["error_type"]
# Message is already in api_result['message']
try: try:
self.output_queue.put_nowait(error_details_for_controller) if not self._stop_event.is_set():
except QueueFull: self.output_queue.put_nowait(error_details_for_controller)
module_logger.warning(f"{self.name}: Output queue full. Discarding error status: {api_result['error_type']}") # else:
# print(f"DEBUG_ADAPTER ({self.name}: Stop event set, not sending error status to queue: {api_result['error_type']}", flush=True)
except QueueFull: # Dovrebbe essere importato 'from queue import Full as QueueFull'
module_logger.warning(f"{self.name}: Output queue full. Discarding error status: {api_result['error_type']}") # Logger OK qui
except Exception as e: except Exception as e:
module_logger.error(f"{self.name}: Error putting error status into queue: {e}", exc_info=True) module_logger.error(f"{self.name}: Error putting error status into queue: {e}", exc_info=True) # Logger OK qui
else: else:
module_logger.error(f"{self.name}: Unknown result structure from _perform_api_request: {api_result}") # Gestito dalla logica mock o un errore reale
print(f"DEBUG_ADAPTER ({self.name}): Unknown result structure from _perform_api_request: {api_result}", flush=True)
if self._stop_event.is_set():
print(f"DEBUG_ADAPTER ({self.name}): Stop event detected before waiting. Exiting loop.", flush=True)
break
# Determine wait time for the next cycle
time_to_wait_seconds: float time_to_wait_seconds: float
if self._in_backoff_mode: if self._in_backoff_mode:
time_to_wait_seconds = self._current_backoff_delay time_to_wait_seconds = self._current_backoff_delay
module_logger.debug(f"{self.name}: In backoff, next attempt in {time_to_wait_seconds:.1f}s.") print(f"DEBUG_ADAPTER ({self.name}): In backoff, next attempt in {time_to_wait_seconds:.1f}s.", flush=True)
else: else:
time_to_wait_seconds = self.base_polling_interval time_to_wait_seconds = self.base_polling_interval
module_logger.debug(f"{self.name}: Next fetch cycle in {time_to_wait_seconds:.1f}s.") print(f"DEBUG_ADAPTER ({self.name}): Next fetch cycle in {time_to_wait_seconds:.1f}s.", flush=True)
# Wait, checking for stop event periodically
# Use _stop_event.wait(timeout) for a cleaner interruptible sleep
if self._stop_event.wait(timeout=time_to_wait_seconds): if self._stop_event.wait(timeout=time_to_wait_seconds):
module_logger.debug(f"{self.name}: Stop event received during wait period.") print(f"DEBUG_ADAPTER ({self.name}): Stop event received during wait period. Exiting loop.", flush=True)
break # Exit while loop if stop event is set break
# --- Fine Loop principale dell'adapter ---
self._send_status_to_queue(STATUS_STOPPED, "Adapter thread terminated.") # --- INIZIO PARTE SUPER SEMPLIFICATA PER DEBUG ---
module_logger.info(f"{self.name} thread event loop finished.") print(f"DEBUG_ADAPTER_FINAL ({self.name}): REACHED END OF WHILE LOOP. About to attempt final put.", flush=True)
try:
# Tentativo di inviare un messaggio molto semplice alla coda
print(f"DEBUG_ADAPTER_FINAL ({self.name}): Attempting very simple put to output_queue.", flush=True)
self.output_queue.put({"type": "ADAPTER_TERMINATING_DEBUG", "message": "Adapter run method ending"}, timeout=0.5)
print(f"DEBUG_ADAPTER_FINAL ({self.name}): Simple put to output_queue SUCCEEDED or TIMED OUT without blocking.", flush=True)
except Exception as e: # Cattura qualsiasi eccezione dal put, inclusa QueueFull se timeout scade su coda piena con maxsize
print(f"DEBUG_ADAPTER_FINAL ({self.name}): EXCEPTION during simple put: {type(e).__name__} - {e}", flush=True)
print(f"DEBUG_ADAPTER_FINAL ({self.name}): RUN METHOD IS TERMINATING NOW. THIS IS THE ABSOLUTE LAST PRINT.", flush=True)
# NESSUN'ALTRA OPERAZIONE QUI
# Il thread termina implicitamente qui quando il metodo run() esce.
# --- FINE PARTE SUPER SEMPLIFICATA PER DEBUG ---
# Il thread termina implicitamente qui quando il metodo run() esce.

View File

@ -13,7 +13,7 @@ from typing import List, Dict, Optional, Tuple, Any
# Relative imports # Relative imports
from ..data import config from ..data import config
from ..utils.logger import get_logger from ..utils.logger import get_logger, shutdown_gui_logging # MODIFICA: Importa shutdown_gui_logging
from ..data.common_models import CanonicalFlightState from ..data.common_models import CanonicalFlightState
module_logger = get_logger(__name__) # flightmonitor.gui.main_window module_logger = get_logger(__name__) # flightmonitor.gui.main_window
@ -195,8 +195,7 @@ class MainWindow:
# --- Status Bar (with Semaphore - inside paned_bottom_panel) --- # --- Status Bar (with Semaphore - inside paned_bottom_panel) ---
self.status_bar_frame = ttk.Frame(self.paned_bottom_panel, padding=(5, 3)) self.status_bar_frame = ttk.Frame(self.paned_bottom_panel, padding=(5, 3))
# Pack at the top of the bottom panel self.status_bar_frame.pack(side=tk.TOP, fill=tk.X, pady=(0,5))
self.status_bar_frame.pack(side=tk.TOP, fill=tk.X, pady=(0,5)) # pady bottom to separate from log
self.semaphore_canvas = tk.Canvas( self.semaphore_canvas = tk.Canvas(
self.status_bar_frame, self.status_bar_frame,
@ -217,12 +216,8 @@ class MainWindow:
self.status_label.pack(side=tk.LEFT, fill=tk.X, expand=True, padx=(0,2)) self.status_label.pack(side=tk.LEFT, fill=tk.X, expand=True, padx=(0,2))
# --- Log Area (ScrolledText Widget - inside paned_bottom_panel) --- # --- Log Area (ScrolledText Widget - inside paned_bottom_panel) ---
# This frame now directly contains the log widget self.log_frame = ttk.Frame(self.paned_bottom_panel, padding=(5,0,5,5))
# self.log_frame = ttk.LabelFrame(self.paned_bottom_panel, text="Application Log", padding=5) # No longer a LabelFrame self.log_frame.pack(side=tk.TOP, fill=tk.BOTH, expand=True, padx=0, pady=0)
self.log_frame = ttk.Frame(self.paned_bottom_panel, padding=(5,0,5,5)) # Just a frame, pady bottom
# Pack log_frame below status_bar_frame
self.log_frame.pack(side=tk.TOP, fill=tk.BOTH, expand=True, padx=0, pady=0) # Fills remaining space in bottom panel
log_font_family = "Consolas" if "Consolas" in tkFont.families() else "Courier New" log_font_family = "Consolas" if "Consolas" in tkFont.families() else "Courier New"
self.log_text_widget = ScrolledText( self.log_text_widget = ScrolledText(
@ -230,17 +225,18 @@ class MainWindow:
font=(log_font_family, 9), font=(log_font_family, 9),
relief=tk.SUNKEN, borderwidth=1 relief=tk.SUNKEN, borderwidth=1
) )
self.log_text_widget.pack(fill=tk.BOTH, expand=True, padx=0, pady=0) # No internal padding, handled by log_frame padding self.log_text_widget.pack(fill=tk.BOTH, expand=True, padx=0, pady=0)
from ..utils.logger import setup_logging as setup_app_logging from ..utils.logger import setup_logging as setup_app_logging
setup_app_logging(gui_log_widget=self.log_text_widget) # MODIFICA CHIAVE QUI: Passa self.root a setup_app_logging
setup_app_logging(gui_log_widget=self.log_text_widget, root_tk_instance=self.root)
# Finalize initialization # Finalize initialization
self.root.protocol("WM_DELETE_WINDOW", self._on_closing) self.root.protocol("WM_DELETE_WINDOW", self._on_closing)
# Initial calls after all widgets are packed # Initial calls after all widgets are packed
self._on_mode_change() # This will call _update_map_placeholder based on default mode (Live) self._on_mode_change()
self.update_semaphore_and_status("OK", "System Initialized. Ready.") # Initial status display self.update_semaphore_and_status("OK", "System Initialized. Ready.")
module_logger.info("MainWindow fully initialized and displayed.") module_logger.info("MainWindow fully initialized and displayed.")
@ -271,9 +267,29 @@ class MainWindow:
module_logger.info("User confirmed quit.") module_logger.info("User confirmed quit.")
if self.controller and hasattr(self.controller, 'on_application_exit'): if self.controller and hasattr(self.controller, 'on_application_exit'):
self.controller.on_application_exit() self.controller.on_application_exit()
# --- MODIFICA INIZIO: Gestione Corretta del Logging GUI ---
# 1. Logga il messaggio "Application window destroyed." PRIMA di distruggere la root.
# Oppure, se si preferisce, si potrebbe anche loggare ad un livello più alto (es. controller)
# o assicurarsi che questo messaggio vada solo al console logger.
# Per ora, lo anticipiamo.
app_destroyed_msg = "Application window will be destroyed."
module_logger.info(app_destroyed_msg) # Va al logger GUI (se ancora attivo) e console
# 2. Chiudi esplicitamente l'handler del logger GUI PRIMA di distruggere la root.
# Questo fermerà i tentativi di scrivere sul widget Text e di usare root.after().
if hasattr(self, 'root') and self.root.winfo_exists(): # Ulteriore controllo
shutdown_gui_logging() # Funzione da aggiungere a utils/logger.py
# --- MODIFICA FINE ---
if hasattr(self, 'root') and self.root.winfo_exists(): if hasattr(self, 'root') and self.root.winfo_exists():
self.root.destroy() self.root.destroy()
module_logger.info("Application window destroyed.") # Il log "Application window destroyed." originale è stato spostato/modificato.
# Ora che la finestra è distrutta, non dovremmo più tentare di loggare tramite la GUI.
# Qualsiasi log successivo andrà solo alla console (se configurata).
# Per coerenza, potremmo usare un logger standard qui se necessario,
# ma il messaggio chiave è già stato loggato.
print(f"{__name__}: Application window has been destroyed (post-destroy print).") # Usa print per output console
else: else:
module_logger.info("User cancelled quit.") module_logger.info("User cancelled quit.")

View File

@ -2,154 +2,302 @@
import logging import logging
import tkinter as tk import tkinter as tk
from tkinter.scrolledtext import ScrolledText from tkinter.scrolledtext import ScrolledText
import threading # Per ottenere il main_thread 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
# Importazioni relative esplicite # Costanti di default per il logging
from ..data import config as app_config
# ... (costanti come prima) ...
DEFAULT_LOG_FORMAT = "%(asctime)s - %(name)s - %(levelname)s - %(message)s" DEFAULT_LOG_FORMAT = "%(asctime)s - %(name)s - %(levelname)s - %(message)s"
DEFAULT_LOG_DATE_FORMAT = "%Y-%m-%d %H:%M:%S" DEFAULT_LOG_DATE_FORMAT = "%Y-%m-%d %H:%M:%S"
DEFAULT_LOG_LEVEL = logging.INFO DEFAULT_LOG_LEVEL = logging.INFO
LOG_LEVEL_COLORS = { # Colori di default per i livelli di log
logging.DEBUG: getattr(app_config, 'LOG_COLOR_DEBUG', 'gray'), LOG_LEVEL_COLORS_DEFAULT = {
logging.INFO: getattr(app_config, 'LOG_COLOR_INFO', 'black'), logging.DEBUG: "RoyalBlue1",
logging.WARNING: getattr(app_config, 'LOG_COLOR_WARNING', 'orange'), logging.INFO: "black",
logging.ERROR: getattr(app_config, 'LOG_COLOR_ERROR', 'red'), logging.WARNING: "dark orange",
logging.CRITICAL: getattr(app_config, 'LOG_COLOR_CRITICAL', 'red4') 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): class TkinterTextHandler(logging.Handler):
def __init__(self, text_widget: tk.Text): """
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__() super().__init__()
self.text_widget = text_widget self.text_widget = text_widget
# Salva un riferimento al thread principale (GUI) self.root_tk_instance = root_tk_instance
self.gui_thread = threading.current_thread() # O threading.main_thread() se si è sicuri che venga chiamato da lì self.log_queue = Queue() # Coda interna per i messaggi di log
# threading.main_thread() è generalmente più sicuro per questo scopo. 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
# Verifica se text_widget è valido prima di configurare i tag if not (self.text_widget and hasattr(self.text_widget, 'winfo_exists') and self.text_widget.winfo_exists()):
if self.text_widget and self.text_widget.winfo_exists(): print("Warning: TkinterTextHandler initialized with an invalid or non-existent text_widget.")
for level, color in LOG_LEVEL_COLORS.items(): self._is_active = False
if color: return
try:
self.text_widget.tag_config(logging.getLevelName(level), foreground=color)
except tk.TclError:
# Potrebbe accadere se il widget è in uno stato strano durante l'init
# anche se winfo_exists() dovrebbe coprirlo.
print(f"Warning: Could not config tag for {logging.getLevelName(level)} during TkinterTextHandler init.")
pass
else:
# Non fare nulla o logga un avviso se il widget non è valido all'inizio
# (anche se questo non dovrebbe accadere se setup_logging è chiamato correttamente)
print("Warning: TkinterTextHandler initialized with an invalid text_widget.")
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): def emit(self, record: logging.LogRecord):
msg = self.format(record) """
level_name = record.levelname 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
# Controlla se siamo nel thread della GUI e se il widget esiste ancora ed è valido # Non tentare di loggare se il widget o la root sono già stati distrutti
# E, cosa più importante, se il mainloop è ancora "attivo" o se l'interprete è in shutdown if not (self.text_widget and hasattr(self.text_widget, 'winfo_exists') and self.text_widget.winfo_exists() and \
# Un modo semplice per verificarlo è vedere se il widget può essere acceduto senza errori Tcl. self.root_tk_instance and hasattr(self.root_tk_instance, 'winfo_exists') and self.root_tk_instance.winfo_exists()):
# O, meglio, usare threading.main_thread().is_alive() è troppo generico. # Se il widget non c'è più, il processore della coda smetterà.
# L'errore "main thread is not in main loop" è specifico di Tkinter. # Potremmo stampare sulla console come fallback se necessario,
# La cosa migliore è provare e catturare l'eccezione TclError. # ma i messaggi dovrebbero già andare al console handler.
self._is_active = False # Disattiva se i widget non esistono più
if not (self.text_widget and self.text_widget.winfo_exists()): return
# Se il widget non esiste più, non tentare di loggare su di esso.
# Potremmo voler loggare sulla console come fallback.
# print(f"Fallback to console (widget destroyed): {msg}")
return # Non fare nulla se il widget non esiste
try: try:
# Questo blocco è la parte critica. Se fallisce, il mainloop potrebbe non essere attivo. msg = self.format(record)
self.text_widget.configure(state=tk.NORMAL) level_name = record.levelname
self.text_widget.insert(tk.END, msg + "\n", (level_name,)) self.log_queue.put_nowait((level_name, msg))
self.text_widget.see(tk.END)
self.text_widget.configure(state=tk.DISABLED)
self.text_widget.update_idletasks() # Questo potrebbe essere problematico se non nel mainloop
except tk.TclError as e:
# Questo errore ("main thread is not in main loop" o simili)
# indica che non possiamo più interagire con Tkinter.
# Logga sulla console come fallback.
# print(f"TkinterTextHandler TclError (GUI likely closing): {e}")
# print(f"Original log message (fallback to console): {msg}")
# Non stampare nulla qui per evitare confusione con il log della console già esistente
pass # Silenzia l'errore, il messaggio è già andato alla console
except RuntimeError as e:
# Ad esempio "Too early to create image" o altri errori di runtime di Tkinter
# durante la chiusura.
# print(f"TkinterTextHandler RuntimeError (GUI likely closing): {e}")
# print(f"Original log message (fallback to console): {msg}")
pass # Silenzia l'errore
except Exception as e: except Exception as e:
# Altri errori imprevisti # In caso di errore nell'accodamento (raro con Queue di Python se non piena e usiamo put_nowait)
print(f"Unexpected error in TkinterTextHandler.emit: {e}") # o nella formattazione.
print(f"Original log message (fallback to console): {msg}") 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
# ... (setup_logging e get_logger come prima) ...
_tkinter_handler = None
def setup_logging(gui_log_widget: tk.Text = None): def _process_log_queue(self):
global _tkinter_handler """
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_str = getattr(app_config, 'LOG_LEVEL', 'INFO').upper()
log_level = getattr(logging, log_level_str, DEFAULT_LOG_LEVEL) log_level = getattr(logging, log_level_str, DEFAULT_LOG_LEVEL)
log_format = getattr(app_config, 'LOG_FORMAT', DEFAULT_LOG_FORMAT) log_format_str = getattr(app_config, 'LOG_FORMAT', DEFAULT_LOG_FORMAT)
date_format = getattr(app_config, 'LOG_DATE_FORMAT', DEFAULT_LOG_DATE_FORMAT) log_date_format_str = getattr(app_config, 'LOG_DATE_FORMAT', DEFAULT_LOG_DATE_FORMAT)
formatter = logging.Formatter(log_format, datefmt=date_format) formatter = logging.Formatter(log_format_str, datefmt=log_date_format_str)
# Usa il logger radice del nostro pacchetto se vogliamo isolarlo. configured_level_colors = {
# Per ora, continuiamo con il root logger globale. level: getattr(app_config, f'LOG_COLOR_{logging.getLevelName(level).upper()}', default_color)
# logger_name_to_configure = 'flightmonitor' # o '' per il root globale for level, default_color in LOG_LEVEL_COLORS_DEFAULT.items()
# current_logger = logging.getLogger(logger_name_to_configure) }
current_logger = logging.getLogger() # Root logger
current_logger.setLevel(log_level) 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
# Rimuovi solo gli handler che potremmo aver aggiunto noi # Assicurati che _tkinter_handler_instance sia gestito correttamente
# per evitare di rimuovere handler di altre librerie (se si usa il root logger globale) if _tkinter_handler_instance:
# È più sicuro se TkinterTextHandler è l'unico handler che potremmo aggiungere più volte if _tkinter_handler_instance in root_logger.handlers:
# o se gestiamo esplicitamente quali handler rimuovere. root_logger.removeHandler(_tkinter_handler_instance)
if _tkinter_handler and _tkinter_handler in current_logger.handlers: _tkinter_handler_instance.close()
current_logger.removeHandler(_tkinter_handler) _tkinter_handler_instance = None
_tkinter_handler = None # Resetta così ne creiamo uno nuovo
# Console Handler (aggiungilo solo se non ne esiste già uno simile)
has_console_handler = any(isinstance(h, logging.StreamHandler) and \
not isinstance(h, TkinterTextHandler) for h in current_logger.handlers)
# 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: if not has_console_handler:
console_handler = logging.StreamHandler() console_handler = logging.StreamHandler()
console_handler.setFormatter(formatter) console_handler.setFormatter(formatter)
console_handler.setLevel(log_level) # Imposta un livello per il console handler, potrebbe essere diverso dal root logger
current_logger.addHandler(console_handler) # console_handler.setLevel(log_level)
# current_logger.info("Console logging handler added.") # Logga solo se aggiunto root_logger.addHandler(console_handler)
# else:
# current_logger.debug("Console handler already exists.")
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.")
if gui_log_widget:
_tkinter_handler = TkinterTextHandler(gui_log_widget)
_tkinter_handler.setFormatter(formatter)
_tkinter_handler.setLevel(log_level)
current_logger.addHandler(_tkinter_handler)
if current_logger.isEnabledFor(logging.INFO): # Logga solo se il livello lo permette
current_logger.info("GUI logging handler initialized/updated.")
# else:
# if current_logger.isEnabledFor(logging.INFO):
# current_logger.info("Console logging active (no GUI widget provided for logging).")
def get_logger(name: str) -> logging.Logger: def get_logger(name: str) -> logging.Logger:
# Se current_logger in setup_logging fosse 'flightmonitor', allora qui:
# if not name.startswith(logger_name_to_configure + '.'):
# qualified_name = logger_name_to_configure + '.' + name.lstrip('.')
# else:
# qualified_name = name
# return logging.getLogger(qualified_name)
return logging.getLogger(name) return logging.getLogger(name)
# Il blocco if __name__ == "__main__" per il test di logger.py è meglio rimuoverlo o # --- MODIFICA: Nuova Funzione ---
# commentarlo pesantemente, dato che dipende da una struttura di pacchetto per def shutdown_gui_logging():
# l'import di app_config e ora ha una logica più complessa. """
# Testare attraverso l'esecuzione dell'applicazione principale è più affidabile. 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.")