Como processar o sinal SIGTERM normalmente?

197

Vamos supor que temos um daemon tão trivial escrito em python:

def mainloop():
    while True:
        # 1. do
        # 2. some
        # 3. important
        # 4. job
        # 5. sleep

mainloop()

e nós o daemonizamos usando o start-stop-daemonqual, por padrão, envia sinal SIGTERM( TERM) --stop.

Vamos supor que a etapa atual executada seja #2. E neste exato momento estamos enviando TERMsinal.

O que acontece é que a execução termina imediatamente.

Descobri que posso lidar com o evento de sinal usando, signal.signal(signal.SIGTERM, handler)mas o fato é que ele ainda interrompe a execução atual e passa o controle para handler.

Então, minha pergunta é - é possível não interromper a execução atual, mas manipular o TERMsinal em um thread separado (?) Para que eu pudesse configurar shutdown_flag = Truepara que mainloop()tivesse a chance de parar normalmente?

zerkms
fonte
2
Eu fiz o que você estava pedindo antes, usando signalfde mascarando a entrega do SIGTERMprocesso.
Eric Urban

Respostas:

276

Uma solução limpa de usar baseada em classe:

import signal
import time

class GracefulKiller:
  kill_now = False
  def __init__(self):
    signal.signal(signal.SIGINT, self.exit_gracefully)
    signal.signal(signal.SIGTERM, self.exit_gracefully)

  def exit_gracefully(self,signum, frame):
    self.kill_now = True

if __name__ == '__main__':
  killer = GracefulKiller()
  while not killer.kill_now:
    time.sleep(1)
    print("doing something in a loop ...")

  print("End of the program. I was killed gracefully :)")
Mayank Jaiswal
fonte
1
Obrigado pela ideia! Eu usei uma abordagem modificada no protetor de reinicialização. github.com/ryran/reboot-guard/blob/master/rguard#L284:L304
rsaw
7
Essa é a melhor resposta (não são necessários threads) e deve ser a abordagem preferida na primeira tentativa.
Jose.angel.jimenez 12/12
2
@ Mausy5043 Python permite que você não tenha parênteses para definir classes. Embora seja perfeitamente adequado para o python 3.x, mas para o python 2.x, a melhor prática é usar "classe XYZ (objeto):". A razão é: docs.python.org/2/reference/datamodel.html#newstyle
Jaiswal
2
Acompanhamento, para mantê-lo motivado, obrigado. Eu uso isso o tempo todo.
Chrisfauerbach
2
Na pior das hipóteses, isso significaria simplesmente fazer outra iteração antes de desligar normalmente. O Falsevalor é definido apenas uma vez e, em seguida, só pode ir de Falso para Verdadeiro, para que o acesso múltiplo não seja um problema.
Alceste_ 25/07/19
52

Primeiro, não tenho certeza de que você precise de um segundo thread para definir o shutdown_flag.
Por que não configurá-lo diretamente no manipulador SIGTERM?

Uma alternativa é gerar uma exceção do SIGTERMmanipulador, que será propagado pela pilha. Supondo que você tenha um tratamento adequado de exceções (por exemplo, com with/ contextmanagere try: ... finally:blocos), este deve ser um desligamento bastante simples, semelhante a se você estivesse no Ctrl+Cseu programa.

Exemplo de programa signals-test.py:

#!/usr/bin/python

from time import sleep
import signal
import sys


def sigterm_handler(_signo, _stack_frame):
    # Raises SystemExit(0):
    sys.exit(0)

if sys.argv[1] == "handle_signal":
    signal.signal(signal.SIGTERM, sigterm_handler)

try:
    print "Hello"
    i = 0
    while True:
        i += 1
        print "Iteration #%i" % i
        sleep(1)
finally:
    print "Goodbye"

Agora veja o Ctrl+Ccomportamento:

$ ./signals-test.py default
Hello
Iteration #1
Iteration #2
Iteration #3
Iteration #4
^CGoodbye
Traceback (most recent call last):
  File "./signals-test.py", line 21, in <module>
    sleep(1)
KeyboardInterrupt
$ echo $?
1

Desta vez, eu o envio SIGTERMapós 4 iterações com kill $(ps aux | grep signals-test | awk '/python/ {print $2}'):

$ ./signals-test.py default
Hello
Iteration #1
Iteration #2
Iteration #3
Iteration #4
Terminated
$ echo $?
143

Dessa vez, habilito meu SIGTERMmanipulador personalizado e o envio SIGTERM:

$ ./signals-test.py handle_signal
Hello
Iteration #1
Iteration #2
Iteration #3
Iteration #4
Goodbye
$ echo $?
0
Will Manley
fonte
3
"Por que não configurá-lo diretamente no manipulador SIGTERM" --- porque o encadeamento do trabalhador interrompia em um local aleatório. Se você colocar várias instruções em seu loop de trabalho, verá que sua solução encerra um trabalhador em uma posição aleatória, o que deixa o trabalho em um estado desconhecido.
Zerkms 04/07/2018
Funciona bem para mim, também no contexto do Docker. Obrigado!
Marian
4
Se você acabou de definir um sinalizador e não gerar uma exceção, será o mesmo que com o encadeamento. Então, usar thread é supérfluo aqui.
Suor 26/05
28

Eu acho que você está perto de uma possível solução.

Executar mainloopem um segmento separado e estendê-lo com a propriedade shutdown_flag. O sinal pode ser capturado signal.signal(signal.SIGTERM, handler)na linha principal (não em uma linha separada). O manipulador de sinal deve ser definido shutdown_flagcomo True e aguardar que o encadeamento termine comthread.join()

moliware
fonte
4
Yep, um segmento separado é como eu finalmente resolvido, graças
zerkms
7
Threads não são necessários aqui. Em um único programa de encadeamento, você pode primeiro registrar um manipulador de sinal (registrar um manipulador de sinal não está bloqueando) e depois gravar o mainloop. A função do manipulador de sinal deve definir um sinalizador quando e o loop deve verificar esse sinalizador. Eu colei uma solução baseada em classe para o mesmo aqui .
Mayank Jaiswal
2
De maneira alguma é necessário ter um segundo encadeamento. Registre o manipulador de sinal.
Oneloop 31/08/16
página útil: g-loaded.eu/2016/11/24/…
Kamil Sindi
26

Aqui está um exemplo simples, sem threads ou classes.

import signal

run = True

def handler_stop_signals(signum, frame):
    global run
    run = False

signal.signal(signal.SIGINT, handler_stop_signals)
signal.signal(signal.SIGTERM, handler_stop_signals)

while run:
    pass # do stuff including other IO stuff
thoughtarray
fonte
11

Com base nas respostas anteriores, criei um gerenciador de contexto que protege contra sigint e sigterm.

import logging
import signal
import sys


class TerminateProtected:
    """ Protect a piece of code from being killed by SIGINT or SIGTERM.
    It can still be killed by a force kill.

    Example:
        with TerminateProtected():
            run_func_1()
            run_func_2()

    Both functions will be executed even if a sigterm or sigkill has been received.
    """
    killed = False

    def _handler(self, signum, frame):
        logging.error("Received SIGINT or SIGTERM! Finishing this block, then exiting.")
        self.killed = True

    def __enter__(self):
        self.old_sigint = signal.signal(signal.SIGINT, self._handler)
        self.old_sigterm = signal.signal(signal.SIGTERM, self._handler)

    def __exit__(self, type, value, traceback):
        if self.killed:
            sys.exit(0)
        signal.signal(signal.SIGINT, self.old_sigint)
        signal.signal(signal.SIGTERM, self.old_sigterm)


if __name__ == '__main__':
    print("Try pressing ctrl+c while the sleep is running!")
    from time import sleep
    with TerminateProtected():
        sleep(10)
        print("Finished anyway!")
    print("This only prints if there was no sigint or sigterm")
Okke
fonte
4

Encontrei a maneira mais fácil para mim. Aqui está um exemplo com garfo para maior clareza de que esse caminho é útil para o controle de fluxo.

import signal
import time
import sys
import os

def handle_exit(sig, frame):
    raise(SystemExit)

def main():
    time.sleep(120)

signal.signal(signal.SIGTERM, handle_exit)

p = os.fork()
if p == 0:
    main()
    os._exit()

try:
    os.waitpid(p, 0)
except (KeyboardInterrupt, SystemExit):
    print('exit handled')
    os.kill(p, 15)
    os.waitpid(p, 0)
Kron
fonte
0

A solução mais simples que encontrei, inspirada nas respostas acima, é

class SignalHandler:

    def __init__(self):

        # register signal handlers
        signal.signal(signal.SIGINT, self.exit_gracefully)
        signal.signal(signal.SIGTERM, self.exit_gracefully)

        self.logger = Logger(level=ERROR)

    def exit_gracefully(self, signum, frame):
        self.logger.info('captured signal %d' % signum)
        traceback.print_stack(frame)

        ###### do your resources clean up here! ####

        raise(SystemExit)
Loretoparisi
fonte