Reputation: 2694
I have a non-GUI PyQt application that I want to performs some clean-up tasks and print a message after quitting the event loop.
def main(name):
signal( SIGTERM, onQuit )
signal( SIGINT, onQuit )
logging.basicConfig( level=LOG_LEVEL, format=LOG_FMT.format(name) )
try:
app = QtCore.QCoreApplication(sys.argv)
QtDBus.QDBusConnection.sessionBus().registerService( "{}.{}.{}".format( DBUS_SERVICE,
name,
"CommandRelay" ) )
relay = CommandRelay(name)
except Exception:
log.error( "Failed to create CommandRelay. process exiting." )
raise
try:
app.exec_()
except Exception:
log.error( "Fatal exception occurred while running relay. Exiting." )
raise
finally:
relay.destroy()
log.info( "Exiting process now." )
This application runs as a subprocess to a larger application. When it receives a SIGTERM
signal, it quits the application.
def onQuit( signum, stackframe ):
""" Handle terminate signal """
try:
log.info( "Terminate signal received." )
QtCore.QCoreApplication.quit()
except Exception:
log.exception( "Exception occured while terminating" )
sys.exit(1)
sys.exit(0)
According to the documentation for QCoreApplication.quit()
, the program should then return control to just after the call to exec_()
. But instead, it seems to exit immediately.
Platform is Opensuse 13.2 x86_64.
Upvotes: 2
Views: 611
Reputation: 11979
Your issue probably is that Python handles signals while it's running Python code - basically something like:
However, while your application is in the Qt mainloop, it's inside C++ code, so Python never has a chance to react to the signal.
I know of two solutions for the problem:
This simply executes a little bit of Python code all few seconds using a QTimer:
timer = QTimer()
timer.timeout.connect(lambda: None)
timer.start(1000)
The drawback is that it takes up to 1s to react to signals, and you're "pointlessly" running some code once per second - waking up the CPU all the time might be a problem for e.g. power consumption too, but I don't have any evidence to back up this claim, it's more of a guess.
It's however the only solution which works on Windows.
This uses a QSocketNotifier with a pipe and and signal.set_wakeup_fd to listen to signals.
The complete code I have in my application to handle signals looks something like this:
class SignalHandler(QObject):
"""Handler responsible for handling OS signals (SIGINT, SIGTERM, etc.).
Attributes:
_activated: Whether activate() was called.
_notifier: A QSocketNotifier used for signals on Unix.
_timer: A QTimer used to poll for signals on Windows.
_orig_handlers: A {signal: handler} dict of original signal handlers.
_orig_wakeup_fd: The original wakeup filedescriptor.
"""
def __init__(self, *, app, quitter, parent=None):
super().__init__(parent)
self._notifier = None
self._timer = usertypes.Timer(self, 'python_hacks')
self._orig_handlers = {}
self._activated = False
self._orig_wakeup_fd = None
def activate(self):
"""Set up signal handlers.
On Windows this uses a QTimer to periodically hand control over to
Python so it can handle signals.
On Unix, it uses a QSocketNotifier with os.set_wakeup_fd to get
notified.
"""
self._orig_handlers[signal.SIGINT] = signal.signal(
signal.SIGINT, self.interrupt)
self._orig_handlers[signal.SIGTERM] = signal.signal(
signal.SIGTERM, self.interrupt)
if os.name == 'posix' and hasattr(signal, 'set_wakeup_fd'):
# pylint: disable=import-error,no-member,useless-suppression
import fcntl
read_fd, write_fd = os.pipe()
for fd in (read_fd, write_fd):
flags = fcntl.fcntl(fd, fcntl.F_GETFL)
fcntl.fcntl(fd, fcntl.F_SETFL, flags | os.O_NONBLOCK)
self._notifier = QSocketNotifier(
read_fd, QSocketNotifier.Read, self)
self._notifier.activated.connect(self.handle_signal_wakeup)
self._orig_wakeup_fd = signal.set_wakeup_fd(write_fd)
else:
self._timer.start(1000)
self._timer.timeout.connect(lambda: None)
self._activated = True
def deactivate(self):
"""Deactivate all signal handlers."""
if not self._activated:
return
if self._notifier is not None:
self._notifier.setEnabled(False)
rfd = self._notifier.socket()
wfd = signal.set_wakeup_fd(self._orig_wakeup_fd)
os.close(rfd)
os.close(wfd)
for sig, handler in self._orig_handlers.items():
signal.signal(sig, handler)
self._timer.stop()
self._activated = False
@pyqtSlot()
def handle_signal_wakeup(self):
"""Handle a newly arrived signal.
This gets called via self._notifier when there's a signal.
Python will get control here, so the signal will get handled.
"""
logging.debug("Handling signal wakeup!")
self._notifier.setEnabled(False)
read_fd = self._notifier.socket()
try:
os.read(read_fd, 1)
except OSError:
logging.exception("Failed to read wakeup fd.")
self._notifier.setEnabled(True)
def interrupt(self, signum, _frame):
"""Handler for signals to gracefully shutdown (SIGINT/SIGTERM)."""
# [...]
Upvotes: 1