LCOV - code coverage report
Current view: top level - src/pairinteraction_gui - app.py (source / functions) Hit Total Coverage
Test: coverage.info Lines: 0 57 0.0 %
Date: 2025-04-29 15:59:54 Functions: 0 14 0.0 %

          Line data    Source code
       1             : # SPDX-FileCopyrightText: 2025 Pairinteraction Developers
       2             : # SPDX-License-Identifier: LGPL-3.0-or-later
       3             : 
       4           0 : import logging
       5           0 : import os
       6           0 : import signal
       7           0 : from multiprocessing import Process
       8           0 : from types import FrameType
       9           0 : from typing import ClassVar, Optional
      10             : 
      11           0 : from PySide6.QtCore import QObject, QSocketNotifier, QThread, QTimer, Signal
      12           0 : from PySide6.QtWidgets import QApplication
      13             : 
      14           0 : logger = logging.getLogger(__name__)
      15             : 
      16             : 
      17           0 : class MainSignals(QObject):
      18             :     """Signals for the application.
      19             : 
      20             :     We store an instance of this signal class in the Application instance, see app.py.
      21             :     So to access these signals (from anywhere in the application), you can use
      22             :     `Application.instance().signals`.
      23             :     """
      24             : 
      25           0 :     ask_download_database = Signal(str)
      26             : 
      27             : 
      28           0 : class Application(QApplication):
      29             :     """Add some global signals to the QApplication."""
      30             : 
      31           0 :     signals = MainSignals()
      32           0 :     all_processes: ClassVar[set[Process]] = set()
      33           0 :     all_threads: ClassVar[set[QThread]] = set()
      34             : 
      35           0 :     @classmethod
      36           0 :     def instance(cls) -> "Application":  # type: ignore  # overwrite type hints
      37             :         """Return the current instance of the application."""
      38           0 :         return super().instance()  # type: ignore [return-value]
      39             : 
      40           0 :     def allow_ctrl_c(self) -> None:
      41             :         # Create a pipe to communicate between the signal handler and the Qt event loop
      42           0 :         pipe_r, pipe_w = os.pipe()
      43             : 
      44           0 :         def signal_handler(signal: int, frame: Optional[FrameType]) -> None:
      45           0 :             os.write(pipe_w, b"x")  # Write a single byte to the pipe
      46             : 
      47           0 :         signal.signal(signal.SIGINT, signal_handler)
      48             : 
      49           0 :         def handle_signal() -> None:
      50           0 :             os.read(pipe_r, 1)  # Read the byte from the pipe to clear it
      51           0 :             logger.info("Ctrl+C detected in terminal. Shutting down gracefully...")
      52           0 :             self.quit()
      53             : 
      54           0 :         sn = QSocketNotifier(pipe_r, QSocketNotifier.Type.Read, parent=self)
      55           0 :         sn.activated.connect(handle_signal)
      56             : 
      57             :         # Create a timer to ensure the event loop processes events regularly
      58             :         # This makes Ctrl+C work even when the application is idle
      59           0 :         timer = QTimer(self)
      60           0 :         timer.timeout.connect(lambda: None)  # Do nothing, just wake up the event loop
      61           0 :         timer.start(200)
      62             : 
      63           0 :     @staticmethod
      64           0 :     def quit() -> None:
      65             :         """Quit the application."""
      66           0 :         logger.debug("Calling Application.quit().")
      67           0 :         Application.terminate_all_processes()
      68           0 :         Application.terminate_all_threads()
      69           0 :         QApplication.quit()
      70           0 :         logger.debug("Application.quit() done.")
      71             : 
      72           0 :     @staticmethod
      73           0 :     def terminate_all_processes() -> None:
      74             :         """Terminate all processes started by the application."""
      75           0 :         for process in Application.all_processes:
      76           0 :             if process.is_alive():
      77           0 :                 logger.debug("Terminating process %s.", process.pid)
      78           0 :                 process.terminate()
      79           0 :                 process.join(timeout=1)
      80             : 
      81           0 :         Application.all_processes.clear()
      82           0 :         logger.debug("All processes terminated.")
      83             : 
      84           0 :     @staticmethod
      85           0 :     def terminate_all_threads() -> None:
      86             :         """Terminate all threads started by the application."""
      87           0 :         for thread in Application.all_threads:
      88           0 :             if thread.isRunning():
      89           0 :                 logger.debug("Terminating thread %s.", thread)
      90           0 :                 thread.terminate()
      91           0 :                 thread.wait()
      92             : 
      93           0 :         Application.all_threads.clear()
      94           0 :         logger.debug("All threads terminated.")

Generated by: LCOV version 1.16