fix problem with stop monitoring and close application
This commit is contained in:
parent
23232b6039
commit
bdea729783
@ -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.")
|
||||||
|
|||||||
@ -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 ---
|
||||||
|
|
||||||
|
|||||||
@ -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.
|
||||||
@ -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.")
|
||||||
|
|
||||||
|
|||||||
@ -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.")
|
||||||
Loading…
Reference in New Issue
Block a user