Làm cách nào để chụp SIGINT trong Python?


535

Tôi đang làm việc trên một kịch bản python bắt đầu một số quy trình và kết nối cơ sở dữ liệu. Thỉnh thoảng tôi muốn giết tập lệnh bằng tín hiệu Ctrl+ Cvà tôi muốn dọn dẹp.

Trong Perl tôi sẽ làm điều này:

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

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

Làm thế nào để tôi thực hiện tương tự điều này trong Python?

Câu trả lời:


787

Đăng ký xử lý của bạn với signal.signalnhư thế này:

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

Mã thích nghi từ đây .

Thêm tài liệu về signalcó thể được tìm thấy ở đây .  


13
Bạn có thể cho tôi biết lý do tại sao sử dụng điều này thay cho ngoại lệ của KeyboardInterrupt không? Không phải là trực quan hơn để sử dụng?
noio

35
Noio: 2 lý do. Đầu tiên, SIGINT có thể được gửi đến quy trình của bạn theo bất kỳ số cách nào (ví dụ: 'kill -s INT <pid>'); Tôi không chắc chắn nếu KeyboardInterruptException được triển khai như một trình xử lý SIGINT hoặc nếu nó thực sự chỉ bắt được các lần nhấn Ctrl + C, nhưng bằng cách nào đó, sử dụng trình xử lý tín hiệu làm cho ý định của bạn rõ ràng (ít nhất, nếu ý định của bạn giống với OP). Mặc dù vậy, quan trọng hơn, với tín hiệu bạn không cần phải cố gắng nắm bắt mọi thứ để làm cho chúng hoạt động, có thể ít nhiều khả năng có thể kết hợp và kỹ thuật phần mềm nói chung tùy thuộc vào cấu trúc ứng dụng của bạn.
Matt J

35
Ví dụ về lý do tại sao bạn muốn bẫy tín hiệu thay vì bắt Ngoại lệ. Giả sử bạn chạy chương trình của mình và chuyển hướng đầu ra sang tệp nhật ký , ./program.py > output.log. Khi bạn nhấn Ctrl-C, bạn muốn chương trình của mình thoát ra một cách duyên dáng bằng cách ghi nhật ký rằng tất cả các tệp dữ liệu đã được xóa và đánh dấu sạch để xác nhận chúng được để ở trạng thái tốt đã biết. Nhưng Ctrl-C gửi SIGINT cho tất cả các quy trình trong một đường ống, do đó trình bao có thể đóng STDOUT (bây giờ là "output.log") trước khi chương trình hoàn tất việc in nhật ký cuối cùng. Python sẽ phàn nàn, "đóng thất bại trong hàm hủy đối tượng tệp: Lỗi trong sys.ex805thook:".

24
Lưu ý rằng signal.pause () không khả dụng trên Windows. docs.python.org/dev/library/signal.html
tháng Oakes

10
-1 kỳ lân để sử dụng signal.pause (), gợi ý rằng tôi sẽ phải chờ một cuộc gọi chặn như vậy thay vì thực hiện một số công việc thực tế. ;)
Nick T

177

Bạn có thể coi nó như một ngoại lệ (KeyboardInterrupt), giống như bất kỳ cái nào khác. Tạo một tệp mới và chạy nó từ trình bao của bạn với các nội dung sau để xem ý tôi là gì:

import time, sys

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

22
Chú ý khi sử dụng giải pháp này. Bạn cũng nên sử dụng mã này trước khi chặn khối bắt bàn phím: signal.signal(signal.SIGINT, signal.default_int_handler)hoặc bạn sẽ thất bại, vì Bàn phím bị tắt không kích hoạt trong mọi tình huống mà nó sẽ kích hoạt! Chi tiết tại đây .
Velda

67

Và là một người quản lý bối cảnh:

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

Để sử dụng:

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

Xử lý lồng nhau:

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

Từ đây: https://gist.github.com/2907502


Nó cũng có thể ném một vòng StopIterationđể phá vỡ vòng lặp trong cùng khi nhấn ctrl-C, phải không?
Theo Belaire

@TheoBelaire Thay vì chỉ ném StopIteration, tôi sẽ tạo một trình tạo chấp nhận một lần lặp như một tham số và đăng ký / giải phóng trình xử lý tín hiệu.
Udi

28

Bạn có thể xử lý CTRL+ Cbằng cách bắt KeyboardInterruptngoại lệ. Bạn có thể thực hiện bất kỳ mã dọn dẹp nào trong trình xử lý ngoại lệ.


21

Từ tài liệu của 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

18

Một đoạn trích khác

Được gọi mainlà chức năng chính và exit_gracefullylà trình xử lý CTRL+c

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

4
Bạn chỉ nên sử dụng ngoại trừ những thứ không được phép xảy ra. Trong trường hợp này, KeyboardInterrupt được cho là xảy ra. Vì vậy, đây không phải là một công trình tốt.
Tristan

15
@TristanT Trong bất kỳ ngôn ngữ nào khác, vâng, nhưng trong trường hợp ngoại lệ của Python không chỉ dành cho những điều không được phép xảy ra. Nó thực sự được coi là phong cách tốt trong Python để sử dụng các ngoại lệ cho kiểm soát luồng (khi thích hợp).
Ian Goldby

8

Tôi đã điều chỉnh mã từ @udi để hỗ trợ nhiều tín hiệu (không có gì lạ mắt):

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

Mã này hỗ trợ cuộc gọi ngắt bàn phím ( SIGINT) và SIGTERM( kill <process>)


5

Trái ngược với câu trả lời của Matt J , tôi sử dụng một đối tượng đơn giản. Điều này mang lại cho tôi khả năng phân tích cú pháp xử lý này cho tất cả các chủ đề cần được dừng bảo mật.

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)

Ở nơi khác

while True:
    # task
    if handler.SIGINT:
        break

Bạn nên sử dụng một sự kiện hoặc time.sleep()thay vì thực hiện một vòng lặp bận rộn trên một biến.
OlivierM

@OlivierM Đây thực sự là ứng dụng cụ thể và chắc chắn không phải là điểm của ví dụ này. Ví dụ: chặn các cuộc gọi hoặc chờ chức năng sẽ không khiến CPU bận rộn. Hơn nữa, đây chỉ là một ví dụ về cách mọi thứ có thể được thực hiện. Bàn phím thường là đủ, như được đề cập trong các câu trả lời khác.
Thomas Devoogdt

4

Bạn có thể sử dụng các chức năng trong mô-đun tín hiệu tích hợp của Python để thiết lập trình xử lý tín hiệu trong python. Cụ thể signal.signal(signalnum, handler)chức năng được sử dụng để đăng ký handlerchức năng cho tín hiệu signalnum.


3

cảm ơn câu trả lời hiện có, nhưng đã thêm 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

Nếu bạn muốn đảm bảo rằng quá trình dọn dẹp của bạn kết thúc, tôi sẽ thêm vào câu trả lời của Matt J bằng cách sử dụng SIG_IGN để tiếp tục SIGINTbị bỏ qua sẽ khiến việc dọn dẹp của bạn bị gián đoạn.

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

Cá nhân, tôi không thể sử dụng thử / ngoại trừ KeyboardInterrupt vì tôi đang sử dụng chế độ ổ cắm tiêu chuẩn (IPC) đang chặn. Vì vậy, SIGINT đã được gợi ý, nhưng chỉ đến sau khi nhận được dữ liệu trên ổ cắm.

Đặt một bộ xử lý tín hiệu hoạt động giống nhau.

Mặt khác, điều này chỉ hoạt động cho một thiết bị đầu cuối thực tế. Các môi trường bắt đầu khác có thể không chấp nhận Ctrl+ Choặc xử lý trước tín hiệu.

Ngoài ra, có "Ngoại lệ" và "BaseExceptions" trong Python, khác nhau theo nghĩa là trình thông dịch cần thoát khỏi chính nó, vì vậy một số ngoại lệ có mức độ ưu tiên cao hơn các trường hợp khác (Ngoại lệ được lấy từ BaseException)

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.