diff --git a/flightmonitor/controller/app_controller.py b/flightmonitor/controller/app_controller.py index 2a49ac7..aa17c25 100644 --- a/flightmonitor/controller/app_controller.py +++ b/flightmonitor/controller/app_controller.py @@ -23,6 +23,7 @@ from typing import List, Optional, Dict, Any # For type hints module_logger = get_logger(__name__) # flightmonitor.controller.app_controller 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 GUI_STATUS_OK = "OK" @@ -78,7 +79,7 @@ class AppController: return 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() 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) 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: 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: status_code = message.get("status_code") @@ -139,8 +143,8 @@ class AppController: elif status_code == STATUS_PERMANENT_FAILURE: gui_status_level = GUI_STATUS_ERROR gui_message = message.get("message", "Too many API errors. Live updates stopped.") - elif status_code == STATUS_STOPPED: - gui_status_level = GUI_STATUS_OK # Or UNKNOWN if we prefer neutral for stopped state + elif status_code == STATUS_STOPPED: # Gestito da stop_live_monitoring o on_application_exit + gui_status_level = GUI_STATUS_OK gui_message = message.get("message", "Live data adapter stopped.") if self.main_window and self.main_window.root.winfo_exists(): @@ -148,13 +152,12 @@ class AppController: if status_code == STATUS_PERMANENT_FAILURE: 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: module_logger.warning(f"Unknown message type from adapter: '{message_type}'") 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}") - except QueueEmpty: pass # Normal if queue is empty except Exception as e: @@ -162,7 +165,9 @@ class AppController: 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.") 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: self._gui_after_id = self.main_window.root.after( 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]): if not self.main_window: 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: err_msg = "DataStorage not initialized. Live monitoring cannot start." @@ -188,47 +193,43 @@ class AppController: if self.is_live_monitoring_active: 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 module_logger.info(f"Controller: Starting live monitoring for bbox: {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: 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() 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(): - 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.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(): - module_logger.error("Controller: Old LiveAdapter thread did not stop in time! May cause issues.") - self.live_adapter_thread = None # Try to discard it anyway + module_logger.error("Controller: Old LiveAdapter thread did not stop in time! May cause issues. Discarding reference.") + self.live_adapter_thread = None # Resetta comunque self.live_adapter_thread = OpenSkyLiveAdapter( output_queue=self.flight_data_queue, 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 - # 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 + self.is_live_monitoring_active = True # Imposta PRIMA di avviare il thread e il polling + self.live_adapter_thread.start() + + if self._gui_after_id: if self.main_window.root.winfo_exists(): try: self.main_window.root.after_cancel(self._gui_after_id) except tk.TclError: pass 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( - 10, # Check very soon for the first status message from adapter + 10, self._process_flight_data_queue ) 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): 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(): 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 module_logger.debug("Controller: Cancelled GUI queue check callback.") - adapter_to_stop = self.live_adapter_thread - if adapter_to_stop and adapter_to_stop.is_alive(): - module_logger.debug(f"Controller: Signalling LiveAdapter thread ({adapter_to_stop.name}) to stop.") - adapter_to_stop.stop() # Adapter will send STATUS_STOPPING then STATUS_STOPPED + # 2. Se il thread adapter esiste ed è vivo, segnalagli di fermarsi. + if adapter_thread_to_stop and adapter_thread_to_stop.is_alive(): + module_logger.debug(f"Controller: Signaling LiveAdapter thread ({adapter_thread_to_stop.name}) to stop.") + 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: - 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 - self.is_live_monitoring_active = False # Crucial to stop _process_flight_data_queue rescheduling - # self._active_bounding_box = None # Optionally clear this + # 4. Rimuovi il riferimento al thread ora (dovrebbe essere terminato o timeout scaduto). + if self.live_adapter_thread == adapter_thread_to_stop: # Controlla se è ancora lo stesso thread + self.live_adapter_thread = None - # The final GUI status (semaphore and text) will be set by: - # 1. The STATUS_STOPPED message from the adapter if it stops cleanly. - # 2. The STATUS_PERMANENT_FAILURE message if that was the cause. - # 3. MainWindow's _reset_gui_to_stopped_state if called directly by user stop button. - # If `from_error` is true, it means a PERMANENT_FAILURE likely occurred and already - # triggered the GUI reset. - if not from_error and self.main_window and self.main_window.root.winfo_exists(): - # This provides an immediate feedback if stop was clean from controller side, - # but might be overwritten by adapter's final STATUS_STOPPED. - # Let MainWindow._reset_gui_to_stopped_state handle this when user clicks stop. - # 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.") + # 5. Svuota la coda da messaggi residui (inclusi STATUS_STOPPED o STATUS_PERMANENT_FAILURE). + # Questo svuotamento avviene DOPO il join, quindi il thread adapter non sta più scrivendo. + # È importante processare qui eventuali messaggi di stato finali dall'adapter. + final_adapter_status_processed = False + if self.flight_data_queue: + module_logger.debug("Controller: Processing any final messages from adapter queue post-join...") + while not self.flight_data_queue.empty(): + try: + message = self.flight_data_queue.get_nowait() + self.flight_data_queue.task_done() + + # 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): 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.") - 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: - 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: module_logger.debug("Controller: Closing DataStorage connection during app exit.") diff --git a/flightmonitor/data/config.py b/flightmonitor/data/config.py index b86a7e0..e3c41f2 100644 --- a/flightmonitor/data/config.py +++ b/flightmonitor/data/config.py @@ -1,5 +1,7 @@ # FlightMonitor/data/config.py +from typing import Optional + """ 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. 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 --- diff --git a/flightmonitor/data/opensky_live_adapter.py b/flightmonitor/data/opensky_live_adapter.py index e99711b..4239b67 100644 --- a/flightmonitor/data/opensky_live_adapter.py +++ b/flightmonitor/data/opensky_live_adapter.py @@ -10,6 +10,7 @@ import time import threading from queue import Queue, Full as QueueFull # Import QueueFull for specific exception handling from typing import List, Optional, Dict, Any, Union +import random # Aggiungi per il mock # Relative imports from . import config as app_config @@ -77,6 +78,33 @@ class OpenSkyLiveAdapter(threading.Thread): f"{self.name} initialized. BBox: {self.bounding_box}, " 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): """Helper to put a status message into the output queue.""" @@ -97,9 +125,13 @@ class OpenSkyLiveAdapter(threading.Thread): def stop(self): """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.") - self._send_status_to_queue(STATUS_STOPPING, "Stop signal received, attempting to terminate.") self._stop_event.set() + # Rimuoviamo _send_status_to_queue(STATUS_STOPPING, ...) da qui 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]: """ - Performs API request and returns a structured result. - Result keys: 'data' (List[CanonicalFlightState]) on success, - 'error_type' (str, e.g. STATUS_RATE_LIMITED) on failure, - plus other error details. + Performs API request or generates mock data based on config. + Returns a structured result. """ + # --- 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: return {"error_type": STATUS_API_ERROR_TEMPORARY, "message": "Bounding box not set."} params = { "lamin": self.bounding_box["lat_min"], "lomin": self.bounding_box["lon_min"], "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: 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}") if response.status_code == 429: # Rate limit @@ -175,9 +243,9 @@ class OpenSkyLiveAdapter(threading.Thread): 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} - 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() raw_states_list = response_data.get("states") 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.") else: 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 delay = self._calculate_next_backoff_delay() 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 delay = self._calculate_next_backoff_delay() 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} - except Exception as e: # Catch-all + except Exception as e: self._consecutive_api_errors += 1; self._in_backoff_mode = True delay = self._calculate_next_backoff_delay() 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 def run(self): - module_logger.info(f"{self.name} thread started. Base polling interval: {self.base_polling_interval:.1f}s.") - self._send_status_to_queue(STATUS_STARTING, "Adapter thread started, preparing initial fetch.") + initial_settle_delay_seconds = 0.2 + 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(): 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." - module_logger.critical(f"{self.name}: {perm_fail_msg}") - self._send_status_to_queue(STATUS_PERMANENT_FAILURE, perm_fail_msg) - self.stop() # This will trigger sending STATUS_STOPPING then STATUS_STOPPED + # module_logger.critical(f"{self.name}: {perm_fail_msg}") + print(f"DEBUG_ADAPTER ({self.name}): CRITICAL - {perm_fail_msg}", flush=True) + if not self._stop_event.is_set(): + self._send_status_to_queue(STATUS_PERMANENT_FAILURE, perm_fail_msg) break - # Perform the API request and get a structured result - api_result = self._perform_api_request() # This now sends its own STATUS_FETCHING + api_result = self._perform_api_request() - # Process the result and send appropriate message to the queue - if "data" in api_result: # Success + if self._stop_event.is_set(): + 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"] try: - self.output_queue.put_nowait({ - "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: - module_logger.warning(f"{self.name}: Output queue full. Discarding {len(flight_data_payload)} flight states.") + if not self._stop_event.is_set(): + self.output_queue.put_nowait({ + "type": MSG_TYPE_FLIGHT_DATA, + "payload": flight_data_payload + }) + print(f"DEBUG_ADAPTER ({self.name}): Sent {len(flight_data_payload)} flight states to queue.", flush=True) + # 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: - module_logger.error(f"{self.name}: Error putting flight data into queue: {e}", exc_info=True) - - elif "error_type" in api_result: # An error occurred - # Error details already logged by _perform_api_request - # 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["status_code"] = api_result["error_type"] # Standardize key - # Message is already in api_result['message'] - - try: - self.output_queue.put_nowait(error_details_for_controller) - except QueueFull: - module_logger.warning(f"{self.name}: Output queue full. Discarding error status: {api_result['error_type']}") - except Exception as e: - module_logger.error(f"{self.name}: Error putting error status into queue: {e}", exc_info=True) - else: - module_logger.error(f"{self.name}: Unknown result structure from _perform_api_request: {api_result}") + 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: + error_details_for_controller = api_result.copy() + error_details_for_controller["type"] = MSG_TYPE_ADAPTER_STATUS + error_details_for_controller["status_code"] = api_result["error_type"] + try: + if not self._stop_event.is_set(): + self.output_queue.put_nowait(error_details_for_controller) + # 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: + module_logger.error(f"{self.name}: Error putting error status into queue: {e}", exc_info=True) # Logger OK qui + else: + # 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 if self._in_backoff_mode: 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: time_to_wait_seconds = self.base_polling_interval - module_logger.debug(f"{self.name}: Next fetch cycle in {time_to_wait_seconds:.1f}s.") - - # 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): - module_logger.debug(f"{self.name}: Stop event received during wait period.") - break # Exit while loop if stop event is set + print(f"DEBUG_ADAPTER ({self.name}): Next fetch cycle in {time_to_wait_seconds:.1f}s.", flush=True) - self._send_status_to_queue(STATUS_STOPPED, "Adapter thread terminated.") - module_logger.info(f"{self.name} thread event loop finished.") \ No newline at end of file + + if self._stop_event.wait(timeout=time_to_wait_seconds): + print(f"DEBUG_ADAPTER ({self.name}): Stop event received during wait period. Exiting loop.", flush=True) + break + # --- Fine Loop principale dell'adapter --- + + # --- INIZIO PARTE SUPER SEMPLIFICATA PER DEBUG --- + 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. \ No newline at end of file diff --git a/flightmonitor/gui/main_window.py b/flightmonitor/gui/main_window.py index 47e211a..6a61e8e 100644 --- a/flightmonitor/gui/main_window.py +++ b/flightmonitor/gui/main_window.py @@ -13,7 +13,7 @@ from typing import List, Dict, Optional, Tuple, Any # Relative imports 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 module_logger = get_logger(__name__) # flightmonitor.gui.main_window @@ -195,8 +195,7 @@ class MainWindow: # --- Status Bar (with Semaphore - inside paned_bottom_panel) --- 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)) # pady bottom to separate from log + self.status_bar_frame.pack(side=tk.TOP, fill=tk.X, pady=(0,5)) self.semaphore_canvas = tk.Canvas( 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)) # --- Log Area (ScrolledText Widget - inside paned_bottom_panel) --- - # This frame now directly contains the log widget - # self.log_frame = ttk.LabelFrame(self.paned_bottom_panel, text="Application Log", padding=5) # No longer a LabelFrame - 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 + self.log_frame = ttk.Frame(self.paned_bottom_panel, padding=(5,0,5,5)) + self.log_frame.pack(side=tk.TOP, fill=tk.BOTH, expand=True, padx=0, pady=0) log_font_family = "Consolas" if "Consolas" in tkFont.families() else "Courier New" self.log_text_widget = ScrolledText( @@ -230,17 +225,18 @@ class MainWindow: font=(log_font_family, 9), 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 - 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 self.root.protocol("WM_DELETE_WINDOW", self._on_closing) # Initial calls after all widgets are packed - self._on_mode_change() # This will call _update_map_placeholder based on default mode (Live) - self.update_semaphore_and_status("OK", "System Initialized. Ready.") # Initial status display + self._on_mode_change() + self.update_semaphore_and_status("OK", "System Initialized. Ready.") module_logger.info("MainWindow fully initialized and displayed.") @@ -271,9 +267,29 @@ class MainWindow: module_logger.info("User confirmed quit.") if self.controller and hasattr(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(): 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: module_logger.info("User cancelled quit.") diff --git a/flightmonitor/utils/logger.py b/flightmonitor/utils/logger.py index 2ca56c5..2b1bda3 100644 --- a/flightmonitor/utils/logger.py +++ b/flightmonitor/utils/logger.py @@ -2,154 +2,302 @@ import logging import tkinter as tk 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 -from ..data import config as app_config - -# ... (costanti come prima) ... +# Costanti di default per il logging DEFAULT_LOG_FORMAT = "%(asctime)s - %(name)s - %(levelname)s - %(message)s" DEFAULT_LOG_DATE_FORMAT = "%Y-%m-%d %H:%M:%S" DEFAULT_LOG_LEVEL = logging.INFO -LOG_LEVEL_COLORS = { - logging.DEBUG: getattr(app_config, 'LOG_COLOR_DEBUG', 'gray'), - logging.INFO: getattr(app_config, 'LOG_COLOR_INFO', 'black'), - logging.WARNING: getattr(app_config, 'LOG_COLOR_WARNING', 'orange'), - logging.ERROR: getattr(app_config, 'LOG_COLOR_ERROR', 'red'), - logging.CRITICAL: getattr(app_config, 'LOG_COLOR_CRITICAL', 'red4') +# Colori di default per i livelli di log +LOG_LEVEL_COLORS_DEFAULT = { + logging.DEBUG: "RoyalBlue1", + logging.INFO: "black", + logging.WARNING: "dark orange", + logging.ERROR: "red2", + logging.CRITICAL: "red4" } +# Intervallo per il polling della coda di log nel TkinterTextHandler (in millisecondi) +LOG_QUEUE_POLL_INTERVAL_MS = 100 + class TkinterTextHandler(logging.Handler): - 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__() self.text_widget = text_widget - # Salva un riferimento al thread principale (GUI) - self.gui_thread = threading.current_thread() # O threading.main_thread() se si è sicuri che venga chiamato da lì - # threading.main_thread() è generalmente più sicuro per questo scopo. - - # Verifica se text_widget è valido prima di configurare i tag - if self.text_widget and self.text_widget.winfo_exists(): - for level, color in LOG_LEVEL_COLORS.items(): - if color: - 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.") + self.root_tk_instance = root_tk_instance + self.log_queue = Queue() # Coda interna per i messaggi di log + self.level_colors = level_colors + self._after_id_log_processor: Optional[str] = None + self._is_active = True # MODIFICA: Flag per controllare l'attività dell'handler + if not (self.text_widget and hasattr(self.text_widget, 'winfo_exists') and self.text_widget.winfo_exists()): + print("Warning: TkinterTextHandler initialized with an invalid or non-existent text_widget.") + self._is_active = False + return + + if not (self.root_tk_instance and hasattr(self.root_tk_instance, 'winfo_exists') and self.root_tk_instance.winfo_exists()): + print("Warning: TkinterTextHandler initialized with an invalid or non-existent root_tk_instance.") + self._is_active = False + return + + # Configura i tag per i colori + for level, color_value in self.level_colors.items(): + level_name = logging.getLevelName(level) + if color_value: + try: + self.text_widget.tag_config(level_name, foreground=color_value) + except tk.TclError: + print(f"Warning: Could not configure tag for {level_name} during TkinterTextHandler init.") + pass # Può succedere se il widget è in uno stato strano, ma _is_active dovrebbe proteggere + + # Avvia il processore della coda di log + if self._is_active: + self._process_log_queue() def emit(self, record: logging.LogRecord): - 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 - # E, cosa più importante, se il mainloop è ancora "attivo" o se l'interprete è in shutdown - # Un modo semplice per verificarlo è vedere se il widget può essere acceduto senza errori Tcl. - # O, meglio, usare threading.main_thread().is_alive() è troppo generico. - # L'errore "main thread is not in main loop" è specifico di Tkinter. - # La cosa migliore è provare e catturare l'eccezione TclError. - - if not (self.text_widget and self.text_widget.winfo_exists()): - # 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 + # Non tentare di loggare se il widget o la root sono già stati distrutti + if not (self.text_widget and hasattr(self.text_widget, 'winfo_exists') and self.text_widget.winfo_exists() and \ + self.root_tk_instance and hasattr(self.root_tk_instance, 'winfo_exists') and self.root_tk_instance.winfo_exists()): + # Se il widget non c'è più, il processore della coda smetterà. + # Potremmo stampare sulla console come fallback se necessario, + # ma i messaggi dovrebbero già andare al console handler. + self._is_active = False # Disattiva se i widget non esistono più + return try: - # Questo blocco è la parte critica. Se fallisce, il mainloop potrebbe non essere attivo. - 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.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 + msg = self.format(record) + level_name = record.levelname + self.log_queue.put_nowait((level_name, msg)) except Exception as e: - # Altri errori imprevisti - print(f"Unexpected error in TkinterTextHandler.emit: {e}") - print(f"Original log message (fallback to console): {msg}") + # In caso di errore nell'accodamento (raro con Queue di Python se non piena e usiamo put_nowait) + # o nella formattazione. + print(f"Error in TkinterTextHandler.emit before queueing: {e}") + # Fallback a stderr per il record originale + # Considera se rimuovere questo fallback se causa problemi o è ridondante + # logging.StreamHandler().handle(record) # Attenzione: questo potrebbe causare output duplicato -# ... (setup_logging e get_logger come prima) ... -_tkinter_handler = None -def setup_logging(gui_log_widget: tk.Text = None): - global _tkinter_handler + def _process_log_queue(self): + """ + Processes messages from the internal log queue and writes them to the Text widget. + This method is run in the GUI thread via root.after(). + """ + # MODIFICA: Controlla prima il flag _is_active + if not self._is_active: + if self._after_id_log_processor: # Assicurati di cancellare l'after se esiste + try: + if self.root_tk_instance and hasattr(self.root_tk_instance, 'winfo_exists') and self.root_tk_instance.winfo_exists(): + self.root_tk_instance.after_cancel(self._after_id_log_processor) + except tk.TclError: pass + self._after_id_log_processor = None + return + + # Controlla se il widget e la root esistono ancora + if not (self.text_widget and hasattr(self.text_widget, 'winfo_exists') and self.text_widget.winfo_exists() and \ + self.root_tk_instance and hasattr(self.root_tk_instance, 'winfo_exists') and self.root_tk_instance.winfo_exists()): + # print("Debug: TkinterTextHandler._process_log_queue: Widget or root destroyed. Stopping.") + if self._after_id_log_processor: + try: + # Non c'è bisogno di controllare winfo_exists sulla root qui, + # perché se fallisce il check sopra, potremmo essere in uno stato di distruzione parziale. + self.root_tk_instance.after_cancel(self._after_id_log_processor) + except tk.TclError: + pass # La root potrebbe essere già andata + self._after_id_log_processor = None + self._is_active = False # Disattiva l'handler + return + + try: + while self._is_active: # MODIFICA: Aggiunto controllo _is_active anche qui + try: + level_name, msg = self.log_queue.get_nowait() + except QueueEmpty: + break # Coda vuota + + self.text_widget.configure(state=tk.NORMAL) + self.text_widget.insert(tk.END, msg + "\n", (level_name,)) + self.text_widget.see(tk.END) + self.text_widget.configure(state=tk.DISABLED) + self.log_queue.task_done() # Segnala che l'elemento è stato processato + + except tk.TclError as e: + # Errore Tcl durante l'aggiornamento del widget (es. widget distrutto tra il check e l'uso) + # print(f"TkinterTextHandler TclError in _process_log_queue: {e}") + if self._after_id_log_processor: + try: self.root_tk_instance.after_cancel(self._after_id_log_processor) + except tk.TclError: pass + self._after_id_log_processor = None + self._is_active = False # Disattiva l'handler + return # Non riprogrammare + except Exception as e: + print(f"Unexpected error in TkinterTextHandler._process_log_queue: {e}") + self._is_active = False # Disattiva in caso di errore grave + return # Non riprogrammare + + + # Riprogramma l'esecuzione solo se l'handler è ancora attivo + if self._is_active: + try: + self._after_id_log_processor = self.root_tk_instance.after( + LOG_QUEUE_POLL_INTERVAL_MS, + self._process_log_queue + ) + except tk.TclError: + # La root è stata distrutta, non possiamo riprogrammare + # print("Debug: TkinterTextHandler._process_log_queue: Root destroyed. Cannot reschedule.") + self._after_id_log_processor = None + self._is_active = False # Disattiva + + + def close(self): + """ + Cleans up resources, like stopping the log queue processor. + Called when the handler is removed or logging system shuts down. + """ + # MODIFICA: Imposta _is_active a False per fermare qualsiasi ulteriore elaborazione o riprogrammazione. + self._is_active = False + + if self._after_id_log_processor: + # Tenta di cancellare il callback solo se la root instance esiste ancora. + # Questo previene l'errore Tcl se la root è già stata distrutta + # quando logging.shutdown() chiama questo metodo. + if self.root_tk_instance and hasattr(self.root_tk_instance, 'winfo_exists') and self.root_tk_instance.winfo_exists(): + try: + self.root_tk_instance.after_cancel(self._after_id_log_processor) + except tk.TclError: + # print(f"Debug: TclError during after_cancel in TkinterTextHandler.close (root might be gone or invalid).") + pass + self._after_id_log_processor = None + + # Svuota la coda per evitare che task_done() venga chiamato su una coda non vuota + # se l'applicazione si chiude bruscamente. + while not self.log_queue.empty(): + try: + self.log_queue.get_nowait() + self.log_queue.task_done() + except QueueEmpty: + break + except Exception: # In caso di altri problemi con la coda durante lo svuotamento + break + + super().close() + + +_tkinter_handler_instance: Optional[TkinterTextHandler] = None + +def setup_logging(gui_log_widget: Optional[tk.Text] = None, + root_tk_instance: Optional[tk.Tk] = None): # Aggiunto root_tk_instance + """ + Sets up application-wide logging. + """ + global _tkinter_handler_instance + + # MODIFICA: Spostato l'import qui per evitare import ciclici se config usasse il logger + from ..data import config as app_config + log_level_str = getattr(app_config, 'LOG_LEVEL', 'INFO').upper() log_level = getattr(logging, log_level_str, DEFAULT_LOG_LEVEL) - log_format = getattr(app_config, 'LOG_FORMAT', DEFAULT_LOG_FORMAT) - date_format = getattr(app_config, 'LOG_DATE_FORMAT', DEFAULT_LOG_DATE_FORMAT) - formatter = logging.Formatter(log_format, datefmt=date_format) - - # Usa il logger radice del nostro pacchetto se vogliamo isolarlo. - # Per ora, continuiamo con il root logger globale. - # logger_name_to_configure = 'flightmonitor' # o '' per il root globale - # current_logger = logging.getLogger(logger_name_to_configure) - current_logger = logging.getLogger() # Root logger - - current_logger.setLevel(log_level) - - # Rimuovi solo gli handler che potremmo aver aggiunto noi - # per evitare di rimuovere handler di altre librerie (se si usa il root logger globale) - # È più sicuro se TkinterTextHandler è l'unico handler che potremmo aggiungere più volte - # o se gestiamo esplicitamente quali handler rimuovere. - if _tkinter_handler and _tkinter_handler in current_logger.handlers: - current_logger.removeHandler(_tkinter_handler) - _tkinter_handler = None # Resetta così ne creiamo uno nuovo + log_format_str = getattr(app_config, 'LOG_FORMAT', DEFAULT_LOG_FORMAT) + log_date_format_str = getattr(app_config, 'LOG_DATE_FORMAT', DEFAULT_LOG_DATE_FORMAT) + formatter = logging.Formatter(log_format_str, datefmt=log_date_format_str) - # 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) - + configured_level_colors = { + level: getattr(app_config, f'LOG_COLOR_{logging.getLevelName(level).upper()}', default_color) + for level, default_color in LOG_LEVEL_COLORS_DEFAULT.items() + } + + root_logger = logging.getLogger() + # Rimuovi tutti gli handler esistenti per una configurazione pulita (opzionale, ma spesso utile) + # for handler in root_logger.handlers[:]: + # root_logger.removeHandler(handler) + # handler.close() + root_logger.setLevel(log_level) # Reimposta il livello del root logger + + # Assicurati che _tkinter_handler_instance sia gestito correttamente + if _tkinter_handler_instance: + if _tkinter_handler_instance in root_logger.handlers: + root_logger.removeHandler(_tkinter_handler_instance) + _tkinter_handler_instance.close() + _tkinter_handler_instance = None + + # Configura il console handler se non già presente + # Questo controllo è un po' più robusto per evitare handler duplicati alla console + has_console_handler = any( + isinstance(h, logging.StreamHandler) and not isinstance(h, TkinterTextHandler) + for h in root_logger.handlers + ) if not has_console_handler: console_handler = logging.StreamHandler() console_handler.setFormatter(formatter) - console_handler.setLevel(log_level) - current_logger.addHandler(console_handler) - # current_logger.info("Console logging handler added.") # Logga solo se aggiunto - # else: - # current_logger.debug("Console handler already exists.") + # Imposta un livello per il console handler, potrebbe essere diverso dal root logger + # console_handler.setLevel(log_level) + root_logger.addHandler(console_handler) + if gui_log_widget and root_tk_instance: + if not (isinstance(gui_log_widget, tk.Text) or isinstance(gui_log_widget, ScrolledText)): + # Usa print o un logger di fallback se il logger principale non è ancora pronto + print(f"ERROR: GUI log widget is not a valid tk.Text or ScrolledText instance: {type(gui_log_widget)}") + elif not (hasattr(gui_log_widget, 'winfo_exists') and gui_log_widget.winfo_exists()): + print("WARNING: GUI log widget provided to setup_logging does not exist (winfo_exists is false).") + elif not (hasattr(root_tk_instance, 'winfo_exists') and root_tk_instance.winfo_exists()): + print("WARNING: Root Tk instance provided to setup_logging does not exist.") + else: + _tkinter_handler_instance = TkinterTextHandler( + text_widget=gui_log_widget, + root_tk_instance=root_tk_instance, + level_colors=configured_level_colors + ) + _tkinter_handler_instance.setFormatter(formatter) + # Imposta un livello per il TkinterTextHandler, potrebbe essere diverso + # _tkinter_handler_instance.setLevel(log_level) + root_logger.addHandler(_tkinter_handler_instance) + # Il messaggio di log "GUI logging handler initialized" verrà ora gestito + # dal logger stesso, incluso il TkinterTextHandler se _is_active è True. + root_logger.info("GUI logging handler (thread-safe) initialized and attached.") + elif gui_log_widget and not root_tk_instance: + print("WARNING: GUI log widget provided, but root Tk instance is missing. Cannot initialize GUI logger.") + elif not gui_log_widget and root_tk_instance: + print("DEBUG: Root Tk instance provided, but no GUI log widget. GUI logger not initialized.") - 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: - # 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) -# Il blocco if __name__ == "__main__" per il test di logger.py è meglio rimuoverlo o -# commentarlo pesantemente, dato che dipende da una struttura di pacchetto per -# l'import di app_config e ora ha una logica più complessa. -# Testare attraverso l'esecuzione dell'applicazione principale è più affidabile. \ No newline at end of file +# --- MODIFICA: Nuova Funzione --- +def shutdown_gui_logging(): + """ + Closes and removes the TkinterTextHandler instance from the root logger. + This should be called before the Tkinter root window is destroyed. + """ + global _tkinter_handler_instance + root_logger = logging.getLogger() + if _tkinter_handler_instance: + if _tkinter_handler_instance in root_logger.handlers: + # Logga un messaggio (alla console, dato che stiamo chiudendo quello GUI) + # prima di rimuovere l'handler. + # Potremmo usare un logger temporaneo o print. + print(f"INFO: Closing and removing GUI logging handler ({_tkinter_handler_instance.name}).") + root_logger.removeHandler(_tkinter_handler_instance) + _tkinter_handler_instance.close() # Chiama il metodo close dell'handler + _tkinter_handler_instance = None + print("INFO: GUI logging handler has been shut down.") + else: + print("DEBUG: No active GUI logging handler to shut down.") \ No newline at end of file