web-dev-qa-db-fra.com

Comment traiter le signal SIGTERM avec élégance?

Supposons que nous ayons un tel démon trivial écrit en python:

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

mainloop()

et nous le démonisons en utilisant start-stop-daemon qui envoie par défaut le signal SIGTERM (TERM) sur --stop.

Supposons que l'étape en cours est #2. Et en ce moment même, nous envoyons un signal TERM.

Qu'est-ce qui se passe est que l'exécution se termine immédiatement.

J'ai constaté que je pouvais gérer l'événement signal en utilisant signal.signal(signal.SIGTERM, handler), mais le fait est que cela interrompt toujours l'exécution en cours et passe le contrôle à handler.

Ma question est donc la suivante: est-il possible de ne pas interrompre l'exécution en cours, mais de gérer le signal TERM dans un thread séparé (?) Afin que je puisse définir shutdown_flag = True afin que mainloop() puisse s'arrêter normalement?

141
zerkms

Une solution propre à utiliser basée sur les classes:

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 __== '__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 :)"
199
Mayank Jaiswal

Premièrement, je ne suis pas certain que vous ayez besoin d'un deuxième thread pour définir le paramètre shutdown_flag. Pourquoi ne pas le définir directement dans le gestionnaire SIGTERM?

Une alternative consiste à lever une exception à partir du gestionnaire SIGTERM, qui sera propagée dans la pile. En supposant que vous ayez la gestion des exceptions appropriée (par exemple, avec des blocs with/contextmanager et try: ... finally:), il devrait s'agir d'un arrêt relativement harmonieux, semblable à celui que vous aviez si vous deviez Ctrl-C votre programme.

Exemple de programme 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"

Maintenant, voyez le comportement 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 <module>
    sleep(1)
KeyboardInterrupt
$ echo $?
1

Cette fois-ci, je l’envoie SIGTERM après 4 itérations avec 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

Cette fois, j'active mon gestionnaire SIGTERM personnalisé et l'envoie SIGTERM:

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

Je pense que vous êtes près d'une solution possible.

Exécutez mainloop dans un thread séparé et étendez-le avec la propriété shutdown_flag. Le signal peut être capturé avec signal.signal(signal.SIGTERM, handler) dans le thread principal (pas dans un thread séparé). Le gestionnaire de signal doit définir shutdown_flag sur True et attendre la fin du thread avec thread.join()

27
moliware

Voici un exemple simple sans threads ni 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
19
thoughtarray

Sur la base des réponses précédentes, j'ai créé un gestionnaire de contexte qui protège contre sigint et 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 __== '__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")
10
Okke

Voici le moyen le plus simple pour moi ... Voici un exemple avec précision pour préciser que cette méthode est utile pour le contrôle de flux.

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)
0
Kron