82

What is the correct way to make my PyQt application quit when killed from the console (Ctrl-C)?

Currently (I have done nothing special to handle unix signals), my PyQt application ignores SIGINT (Ctrl+C). I want it to behave nicely and quit when it is killed. How should I do that?

static_rtti
  • 53,760
  • 47
  • 136
  • 192
  • 7
    I've never understood why almost every python script in the world stops with a control+c except for pyqt apps. No doubt there is a sound reason for that, but in the end it's very annoying. – tokland Feb 08 '11 at 22:13
  • 1
    @tokland : let's solve this once for all :) – static_rtti Feb 08 '11 at 22:16
  • 3
    it appears to a design problem: http://www.mail-archive.com/pyqt@riverbankcomputing.com/msg13757.html. Any solution involving exceptions or similar just feels hacky :-( – tokland Feb 08 '11 at 22:58
  • 4
    you can use Ctrl + \ to kill the app from the terminal. – P.R. Oct 03 '13 at 11:51
  • 1
    Quick and dirty approach: `signal.signal(signal.SIGINT, signal.SIG_DFL)` before entering the Qt message loop. This replaces the Python interpreter's signal handler with the OS default signal handler. KeyboardInterrupt exceptions will no longer be raised, so python code will not have a chance to catch them and clean up, but the process will exit right away even when executing compiled code (like a Qt library). – ʇsәɹoɈ May 25 '22 at 01:57

9 Answers9

55

17.4. signal — Set handlers for asynchronous events

Although Python signal handlers are called asynchronously as far as the Python user is concerned, they can only occur between the “atomic” instructions of the Python interpreter. This means that signals arriving during long calculations implemented purely in C (such as regular expression matches on large bodies of text) may be delayed for an arbitrary amount of time.

That means Python cannot handle signals while the Qt event loop is running. Only when the Python interpreter run (when the QApplication quits, or when a Python function is called from Qt) the signal handler will be called.

A solution is to use a QTimer to let the interpreter run from time to time.

Note that, in the code below, if there are no open windows, the application will quit after the message box regardless of the user's choice because QApplication.quitOnLastWindowClosed() == True. This behaviour can be changed.

import signal
import sys

from PyQt4.QtCore import QTimer
from PyQt4.QtGui import QApplication, QMessageBox

# Your code here

def sigint_handler(*args):
    """Handler for the SIGINT signal."""
    sys.stderr.write('\r')
    if QMessageBox.question(None, '', "Are you sure you want to quit?",
                            QMessageBox.Yes | QMessageBox.No,
                            QMessageBox.No) == QMessageBox.Yes:
        QApplication.quit()

if __name__ == "__main__":
    signal.signal(signal.SIGINT, sigint_handler)
    app = QApplication(sys.argv)
    timer = QTimer()
    timer.start(500)  # You may change this if you wish.
    timer.timeout.connect(lambda: None)  # Let the interpreter run each 500 ms.
    # Your code here.
    sys.exit(app.exec_())

Another possible solution, as pointed by LinearOrbit, is signal.signal(signal.SIGINT, signal.SIG_DFL), but it doesn't allow custom handlers.

Community
  • 1
  • 1
Artur Gaspar
  • 4,407
  • 1
  • 26
  • 28
46

If you simply wish to have ctrl-c close the application - without being "nice"/graceful about it - then from http://www.mail-archive.com/pyqt@riverbankcomputing.com/msg13758.html, you can use this:

import signal
signal.signal(signal.SIGINT, signal.SIG_DFL)

import sys
from PyQt4.QtCore import QCoreApplication
app = QCoreApplication(sys.argv)
app.exec_()

Apparently this works on Linux, Windows and OSX - I have only tested this on Linux so far (and it works).

LinearOrbit
  • 469
  • 4
  • 2
15

18.8.1.1. Execution of Python signal handlers

A Python signal handler does not get executed inside the low-level (C) signal handler. Instead, the low-level signal handler sets a flag which tells the virtual machine to execute the corresponding Python signal handler at a later point(for example at the next bytecode instruction). This has consequences:
[...]
A long-running calculation implemented purely in C (such as regular expression matching on a large body of text) may run uninterrupted for an arbitrary amount of time, regardless of any signals received. The Python signal handlers will be called when the calculation finishes.

The Qt event loop is implemented in C(++). That means, that while it runs and no Python code is called (eg. by a Qt signal connected to a Python slot), the signals are noted, but the Python signal handlers aren't called.

But, since Python 2.6 and in Python 3 you can cause Qt to run a Python function when a signal with a handler is received using signal.set_wakeup_fd().

This is possible, because, contrary to the documentation, the low-level signal handler doesn't only set a flag for the virtual machine, but it may also write a byte into the file descriptor set by set_wakeup_fd(). Python 2 writes a NUL byte, Python 3 writes the signal number.

So by subclassing a Qt class that takes a file descriptor and provides a readReady() signal, like e.g. QAbstractSocket, the event loop will execute a Python function every time a signal (with a handler) is received causing the signal handler to execute nearly instantaneous without need for timers:

import sys, signal, socket
from PyQt4 import QtCore, QtNetwork

class SignalWakeupHandler(QtNetwork.QAbstractSocket):

    def __init__(self, parent=None):
        super().__init__(QtNetwork.QAbstractSocket.UdpSocket, parent)
        self.old_fd = None
        # Create a socket pair
        self.wsock, self.rsock = socket.socketpair(type=socket.SOCK_DGRAM)
        # Let Qt listen on the one end
        self.setSocketDescriptor(self.rsock.fileno())
        # And let Python write on the other end
        self.wsock.setblocking(False)
        self.old_fd = signal.set_wakeup_fd(self.wsock.fileno())
        # First Python code executed gets any exception from
        # the signal handler, so add a dummy handler first
        self.readyRead.connect(lambda : None)
        # Second handler does the real handling
        self.readyRead.connect(self._readSignal)

    def __del__(self):
        # Restore any old handler on deletion
        if self.old_fd is not None and signal and signal.set_wakeup_fd:
            signal.set_wakeup_fd(self.old_fd)

    def _readSignal(self):
        # Read the written byte.
        # Note: readyRead is blocked from occuring again until readData()
        # was called, so call it, even if you don't need the value.
        data = self.readData(1)
        # Emit a Qt signal for convenience
        self.signalReceived.emit(data[0])

    signalReceived = QtCore.pyqtSignal(int)

app = QApplication(sys.argv)
SignalWakeupHandler(app)

signal.signal(signal.SIGINT, lambda sig,_: app.quit())

sys.exit(app.exec_())
Michael Herrmann
  • 4,832
  • 3
  • 38
  • 53
cg909
  • 2,247
  • 19
  • 23
  • Unfortunately, this doesn't work on windows as there is no `socket.socketpair`. (I tried `backports.socketpair`, but this doesn't work either). – coldfix Aug 04 '16 at 10:18
  • On Windows sockets and other file-like handles seem to be handled separately, so you probably need another construct that doesn't use sockets. I don't use Windows, so I can't test what would work. As of Python 3.5 sockets seem to be supported (see https://docs.python.org/3/library/signal.html#signal.set_wakeup_fd ). – cg909 Aug 04 '16 at 22:36
  • I'm getting `ValueError: the fd 10 must be in non-blocking mode` when trying this with Python 3.5.3 on macOS. – Michael Herrmann Jun 12 '17 at 15:59
  • 1
    `self.wsock.setblocking(False)` fixes the problem mentioned in my previous commend. – Michael Herrmann Jun 12 '17 at 16:07
  • Really ugly, but it works ok (linux, python 3.6.8, PyQt5). I just put the class in a separate file. Otherwise my app does not close w/CTRL-C until the GUI gains focus again. Hoping Qt/PyQt incorporates this in the future – brookbot Mar 12 '22 at 09:33
7

I found a way to do this. The idea is to force qt to process events often enough and in a python callabe to catch the SIGINT signal.

import signal, sys
from PyQt4.QtGui import QApplication, QWidget # also works with PySide

# You HAVE TO reimplement QApplication.event, otherwise it does not work.
# I believe that you need some python callable to catch the signal
# or KeyboardInterrupt exception.
class Application(QApplication):
    def event(self, e):
        return QApplication.event(self, e)

app = Application(sys.argv)

# Connect your cleanup function to signal.SIGINT
signal.signal(signal.SIGINT, lambda *a: app.quit())
# And start a timer to call Application.event repeatedly.
# You can change the timer parameter as you like.
app.startTimer(200)

w = QWidget()
w.show()
app.exec_()
parkouss
  • 576
  • 6
  • 5
  • I connected it like this to get a return code too `signal.signal(signal.SIGINT, lambda *a: app.exit(-2))` – dashesy Sep 03 '15 at 00:28
6

The asynchronous approach by cg909 / Michael Herrmann is quite interesting to replace timers. Thus, here is a simplified version which also uses the default type for socket.socketpair (SOCK_STREAM).

class SignalWatchdog(QtNetwork.QAbstractSocket):
def __init__(self):
    """ Propagates system signals from Python to QEventLoop """
    super().__init__(QtNetwork.QAbstractSocket.SctpSocket, None)
    self.writer, self.reader = socket.socketpair()
    self.writer.setblocking(False)
    signal.set_wakeup_fd(self.writer.fileno())  # Python hook
    self.setSocketDescriptor(self.reader.fileno())  # Qt hook
    self.readyRead.connect(lambda: None)  # Dummy function call
Alfalfa
  • 86
  • 2
  • 4
  • 1
    This, together with [cg909's post](https://stackoverflow.com/a/37229299/229511), is the correct answer. – abey Oct 18 '21 at 09:24
2

The answer from Artur Gaspar worked for me when the terminal window was in focus, but would not work when the GUI was in focus. In order to get my GUI to close (which inherits from QWidget) I had to define the following function in the class:

def keyPressEvent(self,event):
    if event.key() == 67 and (event.modifiers() & QtCore.Qt.ControlModifier):
        sigint_handler()

Checking to make sure that the event key is 67 makes sure that 'c' was pressed. Then checking the event modifiers determines whether ctrl was being pressed when 'c' was released.

qwerty9967
  • 732
  • 5
  • 14
1

You can use the standard python unix signals handling mechanism:

import signal 
import sys
def signal_handler(signal, frame):
        print 'You pressed Ctrl+C!'
        sys.exit(0)
signal.signal(signal.SIGINT, signal_handler)
print 'Press Ctrl+C'
while 1:
        continue

where in signal_handler you can free all resources (close all db sessions etc) and gently close your appliction.

Code example taken from here

karolx
  • 508
  • 4
  • 9
  • That doesn't really solve my problem, because I'd like to at least have a handle on my main window in the handler. In your example, you don't... – static_rtti Feb 08 '11 at 22:02
  • If you place this after the app and main window are created, but before you call app._exec(), it does indeed have a handle on your app and main window. – Mr. B Feb 06 '13 at 21:35
1

I think I have a simpler solution:

import signal
import PyQt4.QtGui

def handleIntSignal(signum, frame):
    '''Ask app to close if Ctrl+C is pressed.'''
    PyQt4.QtGui.qApp.closeAllWindows()

signal.signal(signal.SIGINT, handleIntSignal)

This just tells the application to try to close all windows if ctrl+c is pressed. If there is an unsaved document, your app should pop up a save or cancel dialog box as if it were exited.

You may also need to connect the QApplication signal lastWindowClosed() to the slot quit() to get the application to actually exit when the windows are closed.

xioxox
  • 2,526
  • 1
  • 22
  • 22
  • 1
    i haven't tested your solution, but I'm pretty sure it suffers from the same problem of one of the solution above: – static_rtti Feb 16 '11 at 06:45
  • The signal will not be caught before control returns to the python interpreter, ie. before the application returns from sleep; which means the application will have to wait to regain focus to exit. Unacceptable for me. – static_rtti Feb 16 '11 at 06:46
  • After some reflection, I think I will implement Arthur Gaspar's last solution, with a system to easily disable it when debugging. – static_rtti Feb 16 '11 at 06:47
1

You can piggyback on matplotlib's solution.

matplotlib has a function called _maybe_allow_interrupt hidden in matplotlib.backends.backend_qt

from PyQt5 import QtWidgets
from matplotlib.backends.backend_qt import _maybe_allow_interrupt
import sys

app = QtWidgets.QApplication(sys.argv)
mw = QtWidgets.QMainWindow()
mw.show()
with _maybe_allow_interrupt(app):
    app.exec()

Of course since this is not a public function it might change or disappear in future versions of matplotlib, so this is more of a "quick and dirty" solution.

Vinzent
  • 1,070
  • 1
  • 9
  • 14