Comment capturer SIGINT en Python?

536

Je travaille sur un script python qui démarre plusieurs processus et connexions à la base de données. De temps en temps, je veux tuer le script avec un signal Ctrl+ C, et je voudrais faire un peu de nettoyage.

En Perl, je ferais ceci:

$SIG{'INT'} = 'exit_gracefully';

sub exit_gracefully {
    print "Caught ^C \n";
    exit (0);
}

Comment puis-je faire l'analogue de cela en Python?

James Thompson
la source

Réponses:

787

Enregistrez votre gestionnaire avec signal.signalcomme ceci:

#!/usr/bin/env python
import signal
import sys

def signal_handler(sig, frame):
    print('You pressed Ctrl+C!')
    sys.exit(0)
signal.signal(signal.SIGINT, signal_handler)
print('Press Ctrl+C')
signal.pause()

Code adapté d' ici .

Vous signaltrouverez plus de documentation sur ici .  

Matt J
la source
13
Pourriez-vous me dire pourquoi utiliser ceci au lieu d'une exception KeyboardInterrupt? N'est-ce pas plus intuitif à utiliser?
noio
35
Noio: 2 raisons. Tout d'abord, SIGINT peut être envoyé à votre processus de plusieurs façons (par exemple, 'kill -s INT <pid>'); Je ne sais pas si KeyboardInterruptException est implémenté en tant que gestionnaire SIGINT ou s'il n'attrape vraiment que les pressions Ctrl + C, mais de toute façon, l'utilisation d'un gestionnaire de signal rend votre intention explicite (au moins, si votre intention est la même que celle des OP). Plus important encore, avec un signal, vous n'avez pas à enrouler les prises d'essai autour de tout pour les faire fonctionner, ce qui peut être plus ou moins une gagnante en composabilité et en génie logiciel général selon la structure de votre application.
Matt J
35
Exemple de pourquoi vous voulez intercepter le signal au lieu d'attraper l'exception. Dites que vous exécutez votre programme et de rediriger la sortie vers un fichier journal, ./program.py > output.log. Lorsque vous appuyez sur Ctrl-C, vous souhaitez que votre programme se termine correctement en lui faisant enregistrer que tous les fichiers de données ont été vidés et marqués comme propres pour confirmer qu'ils sont laissés dans un bon état connu. Mais Ctrl-C envoie SIGINT à tous les processus d'un pipeline, donc le shell peut fermer STDOUT (maintenant "output.log") avant que program.py ne termine l'impression du journal final. Python se plaindra, "échec de fermeture dans le destructeur d'objets de fichier: erreur dans sys.excepthook:".
Noah Spurrier
24
Notez que signal.pause () n'est pas disponible sous Windows. docs.python.org/dev/library/signal.html
May Oakes du
10
-1 licornes pour l'utilisation de signal.pause (), suggère que je devrais attendre un tel appel de blocage au lieu de faire un vrai travail. ;)
Nick T
177

Vous pouvez le traiter comme une exception (KeyboardInterrupt), comme n'importe quelle autre. Créez un nouveau fichier et exécutez-le à partir de votre shell avec le contenu suivant pour voir ce que je veux dire:

import time, sys

x = 1
while True:
    try:
        print x
        time.sleep(.3)
        x += 1
    except KeyboardInterrupt:
        print "Bye"
        sys.exit()
rledley
la source
22
Attention lors de l'utilisation de cette solution. Vous devez également utiliser ce code avant le bloc catch KeyboardInterrupt:, signal.signal(signal.SIGINT, signal.default_int_handler)ou vous allez échouer, car KeyboardInterrupt ne se déclenche pas dans toutes les situations où il doit se déclencher! Les détails sont ici .
Velda
67

Et en tant que gestionnaire de contexte:

import signal

class GracefulInterruptHandler(object):

    def __init__(self, sig=signal.SIGINT):
        self.sig = sig

    def __enter__(self):

        self.interrupted = False
        self.released = False

        self.original_handler = signal.getsignal(self.sig)

        def handler(signum, frame):
            self.release()
            self.interrupted = True

        signal.signal(self.sig, handler)

        return self

    def __exit__(self, type, value, tb):
        self.release()

    def release(self):

        if self.released:
            return False

        signal.signal(self.sig, self.original_handler)

        self.released = True

        return True

Utiliser:

with GracefulInterruptHandler() as h:
    for i in xrange(1000):
        print "..."
        time.sleep(1)
        if h.interrupted:
            print "interrupted!"
            time.sleep(2)
            break

Gestionnaires imbriqués:

with GracefulInterruptHandler() as h1:
    while True:
        print "(1)..."
        time.sleep(1)
        with GracefulInterruptHandler() as h2:
            while True:
                print "\t(2)..."
                time.sleep(1)
                if h2.interrupted:
                    print "\t(2) interrupted!"
                    time.sleep(2)
                    break
        if h1.interrupted:
            print "(1) interrupted!"
            time.sleep(2)
            break

À partir d'ici: https://gist.github.com/2907502

Udi
la source
Il pourrait également lancer un StopIterationpour rompre la boucle la plus intérieure lorsqu'un ctrl-C est pressé, non?
Theo Belaire
@TheoBelaire Au lieu de simplement lancer une StopIteration, je créerais un générateur qui accepte un itérable comme paramètre et enregistre / libère le gestionnaire de signal.
Udi
28

Vous pouvez gérer CTRL+ Cen interceptant l' KeyboardInterruptexception. Vous pouvez implémenter n'importe quel code de nettoyage dans le gestionnaire d'exceptions.

Jay Conrod
la source
21

De la documentation de Python :

import signal
import time

def handler(signum, frame):
    print 'Here you go'

signal.signal(signal.SIGINT, handler)

time.sleep(10) # Press Ctrl+c here
sunqiang
la source
19

Encore un autre extrait

Référé maincomme fonction principale et exit_gracefullycomme gestionnaire CTRL+c

if __name__ == '__main__':
    try:
        main()
    except KeyboardInterrupt:
        pass
    finally:
        exit_gracefully()
Jossef Harush
la source
4
Vous ne devez utiliser que pour les choses qui ne devraient pas arriver. Dans ce cas, KeyboardInterrupt est censé se produire. Ce n'est donc pas une bonne construction.
Tristan
16
@TristanT Dans toute autre langue oui, mais en Python les exceptions ne sont pas seulement pour des choses qui ne sont pas censées se produire. Il est en fait considéré comme bon style en Python d'utiliser des exceptions pour le contrôle de flux (le cas échéant).
Ian Goldby
8

J'ai adapté le code de @udi pour supporter plusieurs signaux (rien d'extraordinaire):

class GracefulInterruptHandler(object):
    def __init__(self, signals=(signal.SIGINT, signal.SIGTERM)):
        self.signals = signals
        self.original_handlers = {}

    def __enter__(self):
        self.interrupted = False
        self.released = False

        for sig in self.signals:
            self.original_handlers[sig] = signal.getsignal(sig)
            signal.signal(sig, self.handler)

        return self

    def handler(self, signum, frame):
        self.release()
        self.interrupted = True

    def __exit__(self, type, value, tb):
        self.release()

    def release(self):
        if self.released:
            return False

        for sig in self.signals:
            signal.signal(sig, self.original_handlers[sig])

        self.released = True
        return True

Ce code prend en charge l'appel d'interruption clavier ( SIGINT) et SIGTERM( kill <process>)

Cyril N.
la source
5

Contrairement à Matt J, sa réponse, j'utilise un objet simple. Cela me donne la possibilité d'analyser ce gestionnaire pour tous les threads qui doivent être arrêtés en sécurité.

class SIGINT_handler():
    def __init__(self):
        self.SIGINT = False

    def signal_handler(self, signal, frame):
        print('You pressed Ctrl+C!')
        self.SIGINT = True


handler = SIGINT_handler()
signal.signal(signal.SIGINT, handler.signal_handler)

Autre part

while True:
    # task
    if handler.SIGINT:
        break
Thomas Devoogdt
la source
Vous devez utiliser un événement ou time.sleep()au lieu de faire une boucle occupée sur une variable.
OlivierM
@OlivierM C'est vraiment spécifique à l'application et certainement pas le point de cet exemple. Par exemple, bloquer des appels ou attendre des fonctions ne gardera pas le CPU occupé. De plus, ce n'est qu'un exemple de la façon dont les choses peuvent être faites. Les interruptions clavier sont souvent suffisantes, comme mentionné dans d'autres réponses.
Thomas Devoogdt
4

Vous pouvez utiliser les fonctions du module de signal intégré de Python pour configurer des gestionnaires de signal en python. Plus précisément, la signal.signal(signalnum, handler)fonction est utilisée pour enregistrer la handlerfonction pour le signal signalnum.

Brandon E Taylor
la source
3

merci pour les réponses existantes, mais ajouté signal.getsignal()

import signal

# store default handler of signal.SIGINT
default_handler = signal.getsignal(signal.SIGINT)
catch_count = 0

def handler(signum, frame):
    global default_handler, catch_count
    catch_count += 1
    print ('wait:', catch_count)
    if catch_count > 3:
        # recover handler for signal.SIGINT
        signal.signal(signal.SIGINT, default_handler)
        print('expecting KeyboardInterrupt')

signal.signal(signal.SIGINT, handler)
print('Press Ctrl+c here')

while True:
    pass
gsw945
la source
3

Si vous voulez vous assurer que votre processus de nettoyage se termine, j'ajouterais à la réponse de Matt J en utilisant un SIG_IGN afin que d'autres SIGINTsoient ignorés, ce qui empêchera votre nettoyage d'être interrompu.

import signal
import sys

def signal_handler(signum, frame):
    signal.signal(signum, signal.SIG_IGN) # ignore additional signals
    cleanup() # give your process a chance to clean up
    sys.exit(0)

signal.signal(signal.SIGINT, signal_handler) # register the signal with the signal handler first
do_stuff()
Josh Correia
la source
0

Personnellement, je ne pouvais pas utiliser try / except KeyboardInterrupt car j'utilisais le mode socket standard (IPC) qui bloque. Le SIGINT a donc été mis en file d'attente, mais n'est venu qu'après avoir reçu des données sur le socket.

La définition d'un gestionnaire de signaux se comporte de la même manière.

En revanche, cela ne fonctionne que pour un terminal réel. D'autres environnements de démarrage peuvent ne pas accepter Ctrl+ Cou pré-gérer le signal.

De plus, il existe des "Exceptions" et "BaseExceptions" en Python, qui diffèrent dans le sens où l'interpréteur doit se fermer proprement, donc certaines exceptions ont une priorité plus élevée que d'autres (les exceptions sont dérivées de BaseException)

Hatebit
la source