¿Cómo procesar la señal SIGTERM con gracia?

Supongamos que tenemos un demonio tan trivial escrito en python:

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

y lo demoramos usando el start-stop-daemon que, por defecto, envía la señal SIGTERM ( TERM ) en --stop .

Supongamos que el paso actual realizado es #2 . Y en este mismo momento estamos enviando señal TERM .

Lo que pasa es que la ejecución termina inmediatamente.

Descubrí que puedo manejar el evento de señal usando signal.signal(signal.SIGTERM, handler) pero la cosa es que todavía interrumpe la ejecución actual y pasa el control al handler .

Entonces, mi pregunta es: ¿es posible no interrumpir la ejecución actual pero manejar la señal TERM en un hilo separado (?) Para poder establecer shutdown_flag = True modo que mainloop() tenido la oportunidad de detenerse correctamente?

Una clase basada en una solución limpia para usar:

 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 True: time.sleep(1) print("doing something in a loop ...") if killer.kill_now: break print "End of the program. I was killed gracefully :)" 

En primer lugar, no estoy seguro de que necesite un segundo hilo para configurar shutdown_flag. ¿Por qué no configurarlo directamente en el controlador SIGTERM?

Una alternativa es generar una excepción desde el controlador SIGTERM , que se propagará en la stack. Suponiendo que tenga un manejo de excepciones adecuado (por ejemplo, con / contextmanager y try: ... finally: bloques), este debería ser un cierre bastante elegante, similar a si estuviera en Ctrl-C su progtwig.

Ejemplo de progtwig de signals-test.pysignals-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" 

Ahora vea el comportamiento Ctrl-C:

 $ ./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  sleep(1) KeyboardInterrupt $ echo $? 1 

Esta vez lo envío SIGTERM después de 4 iteraciones con 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 

Esta vez habilito mi controlador SIGTERM personalizado y lo envío SIGTERM :

 $ ./signals-test.py handle_signal Hello Iteration #1 Iteration #2 Iteration #3 Iteration #4 Goodbye $ echo $? 0 

Creo que estás cerca de una posible solución.

Ejecute mainloop en un hilo separado y mainloop con la propiedad shutdown_flag . La señal se puede capturar con signal.signal(signal.SIGTERM, handler) en el hilo principal (no en un hilo separado). El manejador de señales debe establecer shutdown_flag en True y esperar a que el hilo termine con thread.join()

Aquí hay un ejemplo simple sin hilos o clases.

 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 

Basándome en las respuestas anteriores, he creado un administrador de contexto que protege de sigint y 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") 

Encontré la manera más fácil para mí. Aquí un ejemplo con tenedor para mayor claridad que de esta manera es útil para el control de flujo.

 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)