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?
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 :)"
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
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()
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
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")
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)