Come posso catturare SIGINT in Python?


536

Sto lavorando a uno script Python che avvia diversi processi e connessioni al database. Ogni tanto voglio uccidere lo script con un segnale Ctrl+ Ce mi piacerebbe fare un po 'di pulizia.

In Perl farei questo:

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

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

Come posso fare l'analogo di questo in Python?

Risposte:


787

Registra il tuo gestore con in signal.signalquesto modo:

#!/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()

Codice adattato da qui .

Più documentazione su signalè disponibile qui .  


13
Potresti dirmi perché usarlo al posto di un'eccezione KeyboardInterrupt? Non è più intuitivo da usare?
noio,

35
Noio: 2 motivi. Innanzitutto, SIGINT può essere inviato al tuo processo in vari modi (ad es. 'Kill -s INT <pid>'); Non sono sicuro se KeyboardInterruptException sia implementato come un gestore SIGINT o se catturi davvero solo le pressioni Ctrl + C, ma in entrambi i casi, l'uso di un gestore di segnale rende esplicito il tuo intento (almeno, se il tuo intento è uguale a quello di OP). Ancora più importante, tuttavia, con un segnale non è necessario racchiudere in sé tutto ciò che serve per farli funzionare, il che può essere più o meno una compostabilità e l'ingegneria del software generale vince a seconda della struttura dell'applicazione.
Matt J,

35
Esempio del motivo per cui si desidera intercettare il segnale anziché rilevare l'eccezione. Diciamo che esegue il programma e reindirizzare l'output in un file di log, ./program.py > output.log. Quando si preme Ctrl-C, si desidera che il programma si chiuda correttamente facendo in modo che tutti i file di dati siano stati scaricati e contrassegnati come puliti per confermare che vengono lasciati in un buono stato noto. Ma Ctrl-C invia SIGINT a tutti i processi in una pipeline, quindi la shell potrebbe chiudere STDOUT (ora "output.log") prima che program.py finisca di stampare il registro finale. Python si lamenterà: "chiusura non riuscita nel distruttore di oggetti file: errore in sys.excepthook:".
Noah Spurrier,

24
Si noti che signal.pause () non è disponibile su Windows. docs.python.org/dev/library/signal.html
May Oakes

10
-1 unicorni per l'utilizzo di signal.pause (), suggerisce che dovrei aspettare una chiamata così bloccante invece di fare un vero lavoro. ;)
Nick T

177

Puoi trattarlo come un'eccezione (KeyboardInterrupt), come qualsiasi altro. Crea un nuovo file ed eseguilo dalla shell con i seguenti contenuti per vedere cosa intendo:

import time, sys

x = 1
while True:
    try:
        print x
        time.sleep(.3)
        x += 1
    except KeyboardInterrupt:
        print "Bye"
        sys.exit()

22
Attenzione quando si utilizza questa soluzione. Dovresti usare anche questo codice prima del blocco catch di KeyboardInterrupt: signal.signal(signal.SIGINT, signal.default_int_handler)o fallirai, perché KeyboardInterrupt non si attiva in tutte le situazioni in cui dovrebbe sparare! I dettagli sono qui .
Velda,

67

E come gestore del contesto:

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

Usare:

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

Gestori nidificati:

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

Da qui: https://gist.github.com/2907502


Potrebbe anche lanciare a StopIterationper interrompere il ciclo più interno quando viene premuto un ctrl-C, giusto?
Theo Belaire,

@TheoBelaire Invece di lanciare una StopIteration, creerei un generatore che accetta un iterabile come parametro e registra / rilascia il gestore del segnale.
Udi,

28

Puoi gestire CTRL+ Ccatturando l' KeyboardInterrupteccezione. È possibile implementare qualsiasi codice di pulizia nel gestore delle eccezioni.


21

Dalla documentazione di 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

19

Ancora un altro frammento

Indicato maincome funzione principale e exit_gracefullycome gestore CTRL+c

if __name__ == '__main__':
    try:
        main()
    except KeyboardInterrupt:
        pass
    finally:
        exit_gracefully()

4
Dovresti usare solo tranne cose che non dovrebbero accadere. In questo caso dovrebbe succedere KeyboardInterrupt. Quindi questa non è una buona costruzione.
Tristan,

16
@TristanT In qualsiasi altra lingua sì, ma in Python le eccezioni non sono solo per cose che non dovrebbero accadere. In realtà è considerato un buon stile in Python per utilizzare le eccezioni per il controllo del flusso (ove appropriato).
Ian Goldby,

8

Ho adattato il codice di @udi per supportare più segnali (niente di speciale):

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

Questo codice supporta la chiamata di interruzione della tastiera ( SIGINT) e il SIGTERM( kill <process>)


5

In contrasto con Matt J la sua risposta, io uso un oggetto semplice. Questo mi dà la possibilità di analizzare questo gestore a tutti i thread che devono essere arrestati in modo sicuro.

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)

Altrove

while True:
    # task
    if handler.SIGINT:
        break

Dovresti usare un evento o time.sleep()invece di fare un ciclo occupato su una variabile.
OlivierM,

@OlivierM Questo è davvero specifico dell'applicazione e sicuramente non è il punto di questo esempio. Ad esempio, bloccare le chiamate o attendere le funzioni non manterrà occupata la CPU. Inoltre, questo è solo un esempio di come si possano fare le cose. KeyboardInterrupts è spesso sufficiente, come indicato in altre risposte.
Thomas Devoogdt

4

È possibile utilizzare le funzioni nel modulo di segnale integrato di Python per configurare i gestori di segnale in Python. In particolare, la signal.signal(signalnum, handler)funzione viene utilizzata per registrare la handlerfunzione per il segnale signalnum.


3

grazie per le risposte esistenti, ma aggiunte 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

3

Se vuoi assicurarti che il tuo processo di pulizia finisca, aggiungerei la risposta di Matt J usando un SIG_IGN in modo da SIGINTignorare ulteriormente ciò che impedirà l'interruzione della tua pulizia.

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()

0

Personalmente, non ho potuto usare try / tranne KeyboardInterrupt perché stavo usando la modalità socket standard (IPC) che sta bloccando. Quindi il SIGINT è stato cueued, ma è arrivato solo dopo aver ricevuto i dati sul socket.

L'impostazione di un gestore di segnali si comporta allo stesso modo.

D'altra parte, questo funziona solo per un terminale reale. Altri ambienti di partenza potrebbero non accettare Ctrl+ Co pre-gestire il segnale.

Inoltre, ci sono "Eccezioni" e "BaseExceptions" in Python, che differiscono nel senso che l'interprete deve uscire in modo pulito, quindi alcune eccezioni hanno una priorità più alta rispetto ad altre (Eccezioni derivate da BaseException)

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.