Bagaimana cara menangkap SIGINT dengan Python?


536

Saya sedang mengerjakan skrip python yang memulai beberapa proses dan koneksi basis data. Sesekali saya ingin membunuh skrip dengan aCtrlC sinyal + , dan saya ingin melakukan pembersihan.

Di Perl saya akan melakukan ini:

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

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

Bagaimana saya melakukan analog ini dengan Python?

Jawaban:


787

Daftarkan pawang Anda dengan signal.signalseperti ini:

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

Kode diadaptasi dari sini .

Dokumentasi lebih lanjut signaldapat ditemukan di sini .  


13
Bisakah Anda memberi tahu saya mengapa menggunakan ini sebagai pengganti pengecualian KeyboardInterrupt? Bukankah itu lebih intuitif untuk digunakan?
noio

35
Noio: 2 alasan. Pertama, SIGINT dapat dikirim ke proses Anda dalam beberapa cara (mis., 'Kill -s INT <pid>'); Saya tidak yakin apakah KeyboardInterruptException diimplementasikan sebagai penangan SIGINT atau jika benar-benar hanya menangkap penekanan Ctrl + C, tetapi bagaimanapun, menggunakan penangan sinyal membuat maksud Anda eksplisit (setidaknya, jika maksud Anda sama dengan OP). Lebih penting lagi, dengan sinyal Anda tidak harus membungkus try-catch di sekitar segalanya untuk membuatnya bekerja, yang bisa lebih atau kurang kompabilitas dan rekayasa perangkat lunak umum menang tergantung pada struktur aplikasi Anda.
Matt J

35
Contoh mengapa Anda ingin menjebak sinyal alih-alih menangkap Pengecualian. Katakanlah Anda menjalankan program Anda dan mengarahkan output ke file log ./program.py > output.log,. Ketika Anda menekan Ctrl-C, Anda ingin program Anda keluar dengan anggun dengan membuatnya mencatat bahwa semua file data telah dihapus dan ditandai bersih untuk mengonfirmasi bahwa semuanya dibiarkan dalam kondisi baik yang diketahui. Tetapi Ctrl-C mengirimkan SIGINT ke semua proses dalam satu pipeline, sehingga shell dapat menutup STDOUT (sekarang "output.log") sebelum program.py selesai mencetak log terakhir. Python akan mengeluh, "tutup gagal pada penghancur objek file: Kesalahan di sys.excepthook:".
Noah Spurrier

24
Perhatikan bahwa signal.pause () tidak tersedia di Windows. docs.python.org/dev/library/signal.html
May Oakes

10
-1 unicorn untuk menggunakan signal.pause (), menunjukkan bahwa saya harus menunggu panggilan yang menghalangi daripada melakukan beberapa pekerjaan nyata. ;)
Nick T

177

Anda dapat memperlakukannya seperti pengecualian (KeyboardInterrupt), seperti yang lainnya. Buat file baru dan jalankan dari shell Anda dengan konten berikut untuk melihat apa yang saya maksud:

import time, sys

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

22
Perhatian saat menggunakan solusi ini. Anda juga harus menggunakan kode ini sebelum tangkapan tangkap KeyboardInterrupt:, signal.signal(signal.SIGINT, signal.default_int_handler)atau Anda akan gagal, karena KeyboardInterrupt tidak memecat dalam setiap situasi di mana ia harus memecat! Detailnya ada di sini .
Velda

67

Dan sebagai manajer konteks:

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

Menggunakan:

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

Penangan bersarang:

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

Dari sini: https://gist.github.com/2907502


Bisa juga melempar StopIterationuntuk mematahkan loop terdalam ketika ctrl-C ditekan, kan?
Theo Belaire

@TheoBelaire Alih-alih hanya melempar StopIteration, saya akan membuat generator yang menerima iterable sebagai parameter dan mendaftarkan / melepaskan pengendali sinyal.
Udi

28

Anda dapat menangani CTRL+ Cdengan menangkap KeyboardInterruptpengecualian. Anda bisa menerapkan kode pembersihan apa pun di penangan pengecualian.


21

Dari dokumentasi 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

Cuplikan Lain

Disebut mainsebagai fungsi utama dan exit_gracefullysebagai CTRL+ cpenangan

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

4
Anda hanya boleh menggunakan kecuali untuk hal-hal yang tidak seharusnya terjadi. Dalam hal ini, KeyboardInterrupt seharusnya terjadi. Jadi ini bukan konstruksi yang bagus.
Tristan

16
@ Christian Dalam bahasa lain, ya, tapi dengan Python pengecualian bukan hanya untuk hal-hal yang tidak seharusnya terjadi. Sebenarnya dianggap gaya yang baik di Python untuk menggunakan pengecualian untuk kontrol aliran (jika perlu).
Ian Goldby

8

Saya mengadaptasi kode dari @udi untuk mendukung beberapa sinyal (tidak ada yang mewah):

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

Kode ini mendukung panggilan interupsi keyboard ( SIGINT) dan SIGTERM( kill <process>)


5

Berbeda dengan Matt J jawabannya, saya menggunakan objek sederhana. Ini memberi saya kemungkinan untuk mengurai handler ini ke semua utas yang perlu dihentikan keamanannya.

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)

Di tempat lain

while True:
    # task
    if handler.SIGINT:
        break

Anda harus menggunakan acara atau time.sleep()bukannya melakukan loop sibuk pada suatu variabel.
OlivierM

@ OlivierM Ini benar-benar aplikasi spesifik dan jelas bukan poin dari contoh ini. Misalnya, memblokir panggilan atau menunggu fungsi tidak akan membuat CPU sibuk. Lebih jauh lagi, ini hanyalah contoh bagaimana hal-hal dapat dilakukan. KeyboardInterrupts sudah cukup, seperti disebutkan dalam jawaban lain.
Thomas Devoogdt

4

Anda dapat menggunakan fungsi-fungsi dalam modul sinyal built-in Python untuk mengatur penangan sinyal dengan python. Secara khusus signal.signal(signalnum, handler)fungsi ini digunakan untuk mendaftarkan handlerfungsi untuk sinyal signalnum.


3

terima kasih atas jawaban yang ada, tetapi ditambahkan 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

Jika Anda ingin memastikan bahwa proses pembersihan Anda selesai, saya akan menambahkan jawaban Matt J dengan menggunakan SIG_IGN sehingga lebih lanjut SIGINTdiabaikan yang akan mencegah pembersihan Anda terganggu.

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

Secara pribadi, saya tidak bisa menggunakan try / kecuali KeyboardInterrupt karena saya menggunakan mode standard socket (IPC) yang memblokir. Jadi SIGINT dikeluarkan, tetapi datang hanya setelah menerima data pada soket.

Mengatur pengatur sinyal berperilaku sama.

Di sisi lain, ini hanya berfungsi untuk terminal aktual. Lingkungan awal lainnya mungkin tidak menerima Ctrl+C , atau menangani sinyal sebelumnya.

Juga, ada "Pengecualian" dan "BaseExceptions" dalam Python, yang berbeda dalam arti bahwa penerjemah perlu keluar dengan bersih sendiri, sehingga beberapa pengecualian memiliki prioritas yang lebih tinggi daripada yang lain (Pengecualian berasal dari BaseException)

Dengan menggunakan situs kami, Anda mengakui telah membaca dan memahami Kebijakan Cookie dan Kebijakan Privasi kami.
Licensed under cc by-sa 3.0 with attribution required.