Jak przechwycić SIGINT w Pythonie?

534

Pracuję nad skryptem Pythona, który uruchamia kilka procesów i połączeń z bazą danych. Co jakiś czas chcę zabijać skrypt sygnałem Ctrl+ Ci chciałbym zrobić porządki.

W Perlu zrobiłbym to:

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

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

Jak zrobić analogię tego w Pythonie?

James Thompson
źródło

Odpowiedzi:

787

Zarejestruj swój przewodnik w signal.signalten sposób:

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

Kod dostosowany stąd .

Więcej dokumentacji signalmożna znaleźć tutaj .  

Matt J
źródło
13
Czy możesz mi powiedzieć, po co używać tego zamiast wyjątku KeyboardInterrupt? Czy nie jest to bardziej intuicyjne w użyciu?
noio
35
Noio: 2 powody. Po pierwsze, SIGINT można wysłać do procesu na wiele sposobów (np. „Kill -s INT <pid>”); Nie jestem pewien, czy KeyboardInterruptException jest zaimplementowany jako moduł obsługi SIGINT, czy też tak naprawdę przechwytuje tylko naciśnięcia klawiszy Ctrl + C, ale w obu przypadkach użycie procedury obsługi sygnału wyraźnie określa twoją intencję (przynajmniej jeśli twoja intencja jest taka sama jak OP). Co ważniejsze, z sygnałem, że nie musisz owijać wszystkich prób, aby działały, co może być mniej więcej kompozycją i ogólną wygraną inżynierii oprogramowania, w zależności od struktury aplikacji.
Matt J
35
Przykład, dlaczego chcesz złapać sygnał w pułapkę zamiast złapać wyjątek. Załóżmy, uruchomić program i przekierować dane wyjściowe do pliku dziennika ./program.py > output.log. Kiedy naciskasz Ctrl-C , chcesz, aby Twój program z wdziękiem zakończył działanie, rejestrując, że wszystkie pliki danych zostały opróżnione i oznaczone jako czyste, aby potwierdzić, że pozostały w znanym dobrym stanie. Ale Ctrl-C wysyła SIGINT do wszystkich procesów w potoku, więc powłoka może zamknąć STDOUT (teraz „output.log”), zanim program.py zakończy drukowanie ostatniego dziennika. Python narzeka: „zamknięcie nie powiodło się w niszczeniu obiektu pliku: Błąd w sys.excepthook:”.
Noah Spurrier
24
Zauważ, że signal.pause () jest niedostępny w systemie Windows. docs.python.org/dev/library/signal.html
May Oakes
10
-1 jednorożce za użycie signal.pause () sugerują, że musiałbym poczekać na takie blokujące wywołanie zamiast wykonywać jakąś prawdziwą pracę. ;)
Nick T
177

Możesz traktować to jak wyjątek (KeyboardInterrupt), jak każdy inny. Utwórz nowy plik i uruchom go ze swojej powłoki z następującą zawartością, aby zobaczyć, co mam na myśli:

import time, sys

x = 1
while True:
    try:
        print x
        time.sleep(.3)
        x += 1
    except KeyboardInterrupt:
        print "Bye"
        sys.exit()
Rledley
źródło
22
Uwaga przy korzystaniu z tego rozwiązania. Powinieneś także użyć tego kodu przed blokowaniem przechwytywania KeyboardInterrupt: signal.signal(signal.SIGINT, signal.default_int_handler)albo zawiedziesz się, ponieważ KeyboardInterrupt nie uruchamia się w każdej sytuacji, w której powinien zadziałać! Szczegóły są tutaj .
Velda
67

I jako menedżer kontekstu:

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

Używać:

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

Zagnieżdżone procedury obsługi:

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

Stąd: https://gist.github.com/2907502

Udi
źródło
Może także rzucić a, StopIterationaby przerwać najbardziej wewnętrzną pętlę po naciśnięciu Ctrl-C, prawda?
Theo Belaire
@TheoBelaire Zamiast rzucić StopIteration, stworzyłbym generator, który przyjmuje iterowalny parametr i rejestruje / zwalnia moduł obsługi sygnału.
Udi
28

Możesz obsłużyć CTRL+ C, wychwytując KeyboardInterruptwyjątek. Możesz zaimplementować dowolny kod czyszczenia w module obsługi wyjątków.

Jay Conrod
źródło
21

Z dokumentacji Pythona :

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
źródło
18

Jeszcze inny urywek

O których mowa mainw funkcji głównej i exit_gracefullyjako CTRL+ cobsługi

if __name__ == '__main__':
    try:
        main()
    except KeyboardInterrupt:
        pass
    finally:
        exit_gracefully()
Jossef Harush
źródło
4
Powinieneś używać tylko z wyjątkiem rzeczy, które nie powinny się zdarzyć. W takim przypadku powinno dojść do KeyboardInterrupt. To nie jest dobra konstrukcja.
Tristan
15
@TristanT W każdym innym języku tak, ale w Pythonie wyjątki dotyczą nie tylko rzeczy, które nie powinny się zdarzyć. Właściwie uważa się, że dobrym stylem w Pythonie jest stosowanie wyjątków do kontroli przepływu (w stosownych przypadkach).
Ian Goldby
8

Dostosowałem kod z @udi do obsługi wielu sygnałów (nic szczególnego):

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

Ten kod obsługuje wywołanie przerwania klawiatury ( SIGINT) i SIGTERM( kill <process>)

Cyryl N.
źródło
5

W przeciwieństwie do jego odpowiedzi Matta J używam prostego przedmiotu. Daje mi to możliwość parsowania tego modułu obsługi do wszystkich wątków, które muszą zostać zatrzymane.

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)

Gdzie indziej

while True:
    # task
    if handler.SIGINT:
        break
Thomas Devoogdt
źródło
Powinieneś użyć zdarzenia lub time.sleep()zamiast robić zajętą ​​pętlę na zmiennej.
OlivierM
@OlivierM To jest naprawdę specyficzne dla aplikacji i zdecydowanie nie jest celem tego przykładu. Na przykład blokowanie połączeń lub oczekiwanie na funkcje nie spowoduje zajętości procesora. Ponadto jest to tylko przykład tego, jak można to zrobić. Przerwania klawiatury są często wystarczające, jak wspomniano w innych odpowiedziach.
Thomas Devoogdt,
4

Możesz użyć funkcji we wbudowanym module sygnałowym Pythona, aby skonfigurować procedury obsługi sygnałów w Pythonie. W szczególności signal.signal(signalnum, handler)funkcja służy do zarejestrowania handlerfunkcji dla sygnału signalnum.

Brandon E. Taylor
źródło
3

dziękuję za istniejące odpowiedzi, ale dodane 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
źródło
3

Jeśli chcesz mieć pewność, że proces czyszczenia dobiegnie końca, dodam odpowiedź Matt'a J, używając SIG_IGN , aby dalsze SIGINTignorowanie zapobiegało przerwaniu czyszczenia.

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
źródło
0

Osobiście nie mogłem użyć try / oprócz KeyboardInterrupt, ponieważ korzystałem z trybu standardowego gniazda (IPC), który blokuje. Tak więc SIGINT został wskazany, ale pojawił się dopiero po otrzymaniu danych na gnieździe.

Ustawienie procedury obsługi sygnału zachowuje się tak samo.

Z drugiej strony działa to tylko dla faktycznego terminala. Inne środowiska początkowe mogą nie akceptować Ctrl+ Club wstępnie obsługiwać sygnał.

Ponadto w Pythonie istnieją „wyjątki” i „wyjątki podstawowe”, które różnią się tym, że interpreter musi sam wyjść bezproblemowo, więc niektóre wyjątki mają wyższy priorytet niż inne (wyjątki pochodzą od wyjątku BaseException)

Hatebit
źródło