# app/utils/cancelable_thread.py """ Cancelable Thread Base Classes ================================ Thread sicuri con supporto per cancellazione graceful. Sostituisce l'uso pericoloso di QThread.terminate(). Features: - Cancellazione graceful con flag - Timeout configurabile - Pulizia automatica risorse - Signal standardizzati """ import logging from PyQt6.QtCore import QThread, pyqtSignal from typing import Callable, Any, Dict logger = logging.getLogger(__name__) class CancelableThread(QThread): """ Base class per thread che supportano cancellazione graceful. Usage: class MyDataLoader(CancelableThread): data_loaded = pyqtSignal(object) def __init__(self, param): super().__init__() self.param = param def do_work(self): # Fai il lavoro qui # Controlla periodicamente self.is_cancelled for i in range(1000): if self.is_cancelled: logger.info("Lavoro cancellato") return None # ... processa dati ... return result # Uso: thread = MyDataLoader(param) thread.start() # Per cancellare: thread.cancel() thread.wait(2000) # Attendi max 2s """ # Segnali comuni error_occurred = pyqtSignal(str) finished_signal = pyqtSignal() # Rinominato per evitare conflitto con QThread.finished def __init__(self): super().__init__() self._is_cancelled = False self._cleanup_callbacks = [] @property def is_cancelled(self) -> bool: """Ritorna True se il thread è stato cancellato""" return self._is_cancelled def cancel(self): """ Richiede la cancellazione del thread. Il thread si fermerà al prossimo check di is_cancelled. """ logger.info(f"{self.__class__.__name__}: Cancellazione richiesta") self._is_cancelled = True def register_cleanup(self, callback: Callable): """ Registra una callback da chiamare in caso di cancellazione. Args: callback: Funzione da chiamare senza parametri Example: def close_db(): db.close() thread.register_cleanup(close_db) """ self._cleanup_callbacks.append(callback) def do_work(self) -> Any: """ Override questo metodo nelle sottoclassi per implementare il lavoro. Ricordati di controllare periodicamente self.is_cancelled. Returns: Il risultato del lavoro (può essere None se cancellato) Raises: Exception: Se si verificano errori """ raise NotImplementedError("Le sottoclassi devono implementare do_work()") def run(self): """ Metodo run standard di QThread. NON OVERRIDE questo metodo, usa do_work() invece. """ try: if self._is_cancelled: logger.info(f"{self.__class__.__name__}: Cancellato prima dell'avvio") return logger.debug(f"{self.__class__.__name__}: Avvio lavoro") result = self.do_work() if self._is_cancelled: logger.info(f"{self.__class__.__name__}: Lavoro cancellato") self._run_cleanup() else: logger.debug(f"{self.__class__.__name__}: Lavoro completato") # Emit del risultato è responsabilità delle sottoclassi in do_work() except Exception as e: logger.error(f"{self.__class__.__name__}: Errore - {e}", exc_info=True) self.error_occurred.emit(str(e)) finally: self.finished_signal.emit() def _run_cleanup(self): """Esegue tutte le cleanup callback registrate""" for callback in self._cleanup_callbacks: try: callback() except Exception as e: logger.error(f"Errore durante cleanup callback: {e}", exc_info=True) class DataLoaderThread(CancelableThread): """ Thread generico per caricamento dati con supporto cancellazione. Example: def load_data(filters): # Logica di caricamento return dataframe thread = DataLoaderThread(load_data, filters={'year': 2024}) thread.data_loaded.connect(on_data_ready) thread.error_occurred.connect(on_error) thread.start() # Per cancellare: thread.cancel() """ data_loaded = pyqtSignal(object) # Emette il risultato def __init__(self, loader_func: Callable, **kwargs): """ Args: loader_func: Funzione che carica i dati (deve accettare **kwargs) **kwargs: Parametri da passare a loader_func """ super().__init__() self.loader_func = loader_func self.loader_params = kwargs def do_work(self): """Esegue il caricamento dati""" if self.is_cancelled: return None logger.info(f"Caricamento dati con params: {self.loader_params}") try: # Esegui la funzione di caricamento result = self.loader_func(**self.loader_params) if not self.is_cancelled: self.data_loaded.emit(result) return result except Exception as e: logger.error(f"Errore caricamento dati: {e}", exc_info=True) self.error_occurred.emit(str(e)) return None class QueryThread(CancelableThread): """ Thread specializzato per query database con supporto cancellazione. Features aggiuntive rispetto a DataLoaderThread: - Gestione connessione database - Cleanup automatico connessioni - Progress tracking opzionale Example: def run_query(db_mgr, filters): query = "SELECT * FROM table WHERE year = :year" return db_mgr.execute_query(query, filters) thread = QueryThread(run_query, db_mgr, filters={'year': 2024}) thread.query_completed.connect(on_results) thread.start() """ query_completed = pyqtSignal(object) progress_updated = pyqtSignal(int) # 0-100 def __init__(self, query_func: Callable, *args, **kwargs): """ Args: query_func: Funzione che esegue la query *args: Argomenti posizionali per query_func **kwargs: Argomenti keyword per query_func """ super().__init__() self.query_func = query_func self.query_args = args self.query_kwargs = kwargs def do_work(self): """Esegue la query""" if self.is_cancelled: return None logger.info(f"Esecuzione query: {self.query_func.__name__}") try: # Esegui la query result = self.query_func(*self.query_args, **self.query_kwargs) if not self.is_cancelled: self.query_completed.emit(result) return result except Exception as e: logger.error(f"Errore query: {e}", exc_info=True) self.error_occurred.emit(str(e)) return None # ========== HELPER UTILITIES ========== def safe_terminate_thread(thread: QThread, wait_ms: int = 2000, force: bool = False): """ Termina un thread in modo sicuro. Args: thread: Thread da terminare wait_ms: Millisecondi da attendere per graceful shutdown force: Se True, usa terminate() se il thread non si ferma Returns: True se il thread è stato fermato, False altrimenti """ if not thread or not thread.isRunning(): return True # Se supporta cancellazione, provaci if hasattr(thread, 'cancel'): logger.info(f"Cancellazione graceful di {thread.__class__.__name__}") thread.cancel() # Attendi finished = thread.wait(wait_ms) if not finished and force: logger.warning(f"{thread.__class__.__name__} non terminato gracefully, force terminate") thread.terminate() thread.wait(1000) # Attendi ancora un po' return False return finished def cleanup_threads(*threads: QThread, wait_ms: int = 2000): """ Cleanup di multipli thread in parallelo. Args: *threads: Thread da pulire wait_ms: Tempo di attesa per thread Example: cleanup_threads(thread1, thread2, thread3, wait_ms=3000) """ logger.info(f"Cleanup di {len(threads)} thread") # Cancella tutti for thread in threads: if thread and hasattr(thread, 'cancel'): thread.cancel() # Attendi tutti for thread in threads: if thread and thread.isRunning(): if not thread.wait(wait_ms): logger.warning(f"{thread.__class__.__name__} non terminato entro {wait_ms}ms") thread.terminate() thread.wait(500)