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
|
||||
|
||||
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.")
|
||||
|
||||
@ -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 ---
|
||||
|
||||
|
||||
@ -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.")
|
||||
|
||||
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.
|
||||
@ -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.")
|
||||
|
||||
|
||||
@ -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.
|
||||
# --- 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.")
|
||||
Loading…
Reference in New Issue
Block a user