Como capturar o SIGINT em Python?

536

Estou trabalhando em um script python que inicia vários processos e conexões com o banco de dados. De vez em quando eu quero matar o script com um sinal Ctrl+ C, e gostaria de fazer uma limpeza.

No Perl, eu faria o seguinte:

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

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

Como faço o análogo disso em Python?

James Thompson
fonte

Respostas:

787

Registre seu manipulador da signal.signalseguinte maneira:

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

Código adaptado daqui .

Mais documentação signalpode ser encontrada aqui .  

Matt J
fonte
13
Você poderia me dizer por que usar isso em vez de uma exceção KeyboardInterrupt? Não é mais intuitivo de usar?
Noio
35
Noio: 2 razões. Primeiro, o SIGINT pode ser enviado ao seu processo de várias maneiras (por exemplo, 'kill -s INT <pid>'); Não tenho certeza se KeyboardInterruptException é implementado como um manipulador SIGINT ou se ele realmente captura apenas as teclas Ctrl + C, mas, de qualquer maneira, o uso de um manipulador de sinal torna sua intenção explícita (pelo menos, se sua intenção for a mesma do OP). Mais importante, porém, com um sinal, você não precisa agrupar try-catchs em torno de tudo para fazê-los funcionar, o que pode ser mais ou menos uma composição e uma vitória geral da engenharia de software, dependendo da estrutura do seu aplicativo.
Matt J
35
Exemplo de por que você deseja capturar o sinal em vez de capturar a exceção. Digamos que você execute seu programa e redirecione a saída para um arquivo de log ./program.py > output.log,. Quando você pressiona Ctrl-C, deseja que seu programa saia normalmente, registrando que todos os arquivos de dados foram liberados e marcados como limpos para confirmar que estão em bom estado. Mas o Ctrl-C envia o SIGINT a todos os processos em um pipeline, para que o shell possa fechar STDOUT (agora "output.log") antes que o program.py termine de imprimir o log final. O Python irá reclamar, "falha no fechamento do destruidor de objetos de arquivo: Erro no sys.excepthook:".
Noah Spurrier
24
Observe que signal.pause () não está disponível no Windows. docs.python.org/dev/library/signal.html
May Oakes
10
-1 unicórnios por usar signal.pause (), sugere que eu teria que esperar em uma chamada de bloqueio em vez de fazer algum trabalho real. ;)
Nick T
177

Você pode tratá-lo como uma exceção (KeyboardInterrupt), como qualquer outro. Crie um novo arquivo e execute-o no seu shell com o seguinte conteúdo para ver o que quero dizer:

import time, sys

x = 1
while True:
    try:
        print x
        time.sleep(.3)
        x += 1
    except KeyboardInterrupt:
        print "Bye"
        sys.exit()
rledley
fonte
22
Atenção ao usar esta solução. Você também deve usar este código antes do bloco de captura do KeyboardInterrupt: signal.signal(signal.SIGINT, signal.default_int_handler)ou falhará, porque o KeyboardInterrupt não é acionado em todas as situações em que deve ser acionado! Os detalhes estão aqui .
Velda
67

E como gerente de contexto:

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

Usar:

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

Manipuladores aninhados:

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

A partir daqui: https://gist.github.com/2907502

Udi
fonte
Também pode ser usado StopIterationpara quebrar o loop mais interno quando um ctrl-C é pressionado, certo?
Theo Belaire
@TheoBelaire Em vez de lançar uma StopIteration, eu criaria um gerador que aceita um iterável como parâmetro e registra / libera o manipulador de sinal.
Udi
28

Você pode manipular CTRL+ Ccapturando a KeyboardInterruptexceção. Você pode implementar qualquer código de limpeza no manipulador de exceções.

Jay Conrod
fonte
21

Da documentação do 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
fonte
19

Ainda outro trecho

Referido maincomo a função principal e exit_gracefullycomo o manipulador CTRL+c

if __name__ == '__main__':
    try:
        main()
    except KeyboardInterrupt:
        pass
    finally:
        exit_gracefully()
Jossef Harush
fonte
4
Você só deve usar, exceto para coisas que não deveriam acontecer. Nesse caso, o KeyboardInterrupt deve acontecer. Portanto, essa não é uma boa construção.
Tristan
16
@TristanT Em qualquer outro idioma, sim, mas em Python as exceções não são apenas para coisas que não deveriam acontecer. Na verdade, é considerado um bom estilo no Python usar exceções para controle de fluxo (quando apropriado).
Home
8

Eu adaptei o código do @udi para suportar vários sinais (nada extravagante):

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

Esse código suporta as chamadas de interrupção do teclado ( SIGINT) e SIGTERM( kill <process>)

Cyril N.
fonte
5

Em contraste com Matt J, sua resposta, eu uso um objeto simples. Isso me dá a possibilidade de analisar esse manipulador para todos os threads que precisam ser interrompidos na segurança.

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)

Em outro lugar

while True:
    # task
    if handler.SIGINT:
        break
Thomas Devoogdt
fonte
Você deve usar um evento ou em time.sleep()vez de fazer um loop ocupado em uma variável.
OlivierM
@ OlivierM Este é realmente um aplicativo específico e definitivamente não é o objetivo deste exemplo. Por exemplo, bloquear chamadas ou aguardar funções não manterá a CPU ocupada. Além disso, este é apenas um exemplo de como as coisas podem ser feitas. Interrupções de teclado são muitas vezes suficientes, como mencionado em outras respostas.
Thomas Devoogdt 31/05/19
4

Você pode usar as funções no módulo de sinal interno do Python para configurar manipuladores de sinal no python. Especificamente, a signal.signal(signalnum, handler)função é usada para registrar a handlerfunção para sinal signalnum.

Brandon E Taylor
fonte
3

obrigado pelas respostas existentes, mas adicionei 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
fonte
3

Se você quiser garantir que seu processo de limpeza termine, acrescentarei a resposta de Matt J usando um SIG_IGN para que outros itensSIGINT sejam ignorados, o que impedirá que sua limpeza seja interrompida.

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
fonte
0

Pessoalmente, não consegui usar try / except KeyboardInterrupt porque estava usando o modo de soquete padrão (IPC) que está bloqueando. Portanto, o SIGINT foi seguido, mas veio somente após o recebimento de dados no soquete.

Definir um manipulador de sinal se comporta da mesma maneira.

Por outro lado, isso funciona apenas para um terminal real. Outros ambientes de partida pode não aceitar Ctrl+C ou manipular previamente o sinal.

Além disso, existem "Exceções" e "BaseExceptions" no Python, que diferem no sentido de que o intérprete precisa sair de forma limpa, portanto algumas exceções têm uma prioridade mais alta que outras (as exceções são derivadas de BaseException)

Hatebit
fonte