Démon de destruction de processus multi-plateforme à toute épreuve

J’ai de l’automatisation python, qui génère des sessions telnet que je connecte avec la commande de script linux; Il existe deux ID de processus de script (un parent et un enfant) pour chaque session de journalisation.

Je dois résoudre un problème où, si le script d’automatisation python meurt, les sessions de script ne se ferment jamais d’elles-mêmes; pour une raison quelconque, c’est beaucoup plus difficile qu’il ne devrait l’être.

Jusqu’à présent, j’ai implémenté watchdog.py (voir en bas de la question), qui se démode, et interroge le PID du script Python Automation dans une boucle. Quand il voit le PID Python Automation disparaître de la table de processus du serveur, il tente de tuer les sessions de script .

Mon problème est:

  • script sessions de script génèrent toujours deux processus distincts, l’une des sessions de script est le parent de l’autre session de script .
  • watchdog.py ne tue pas les sessions de script enfant, si je lance des sessions de script partir du script d’automatisation (voir EXEMPLE D’AUTOMATION , ci-dessous)

EXEMPLE D’AUTOMATISATION ( reproduce_bug.py )

 import pexpect as px from subprocess import Popen import code import time import sys import os def read_pid_and_telnet(_child, addr): time.sleep(0.1) # Give the OS time to write the PIDFILE # Read the PID in the PIDFILE fh = open('PIDFILE', 'r') pid = int(''.join(fh.readlines())) fh.close() time.sleep(0.1) # Clean up the PIDFILE os.remove('PIDFILE') _child.expect(['#', '\$'], timeout=3) _child.sendline('telnet %s' % addr) return str(pid) pidlist = list() child1 = px.spawn("""bash -c 'echo $$ > PIDFILE """ """&& exec /usr/bin/script -f LOGFILE1.txt'""") pidlist.append(read_pid_and_telnet(child1, '10.1.1.1')) child2 = px.spawn("""bash -c 'echo $$ > PIDFILE """ """&& exec /usr/bin/script -f LOGFILE2.txt'""") pidlist.append(read_pid_and_telnet(child2, '10.1.1.2')) cmd = "python watchdog.py -o %s -k %s" % (os.getpid(), ','.join(pidlist)) Popen(cmd.split(' ')) print "I started the watchdog with:\n %s" % cmd time.sleep(0.5) raise RuntimeError, "Simulated script crash. Note that script child sessions are hung" 

Maintenant, l’exemple de ce qui se passe lorsque je lance l’automatisation ci-dessus … notez que le PID 30017 génère 30018 et le PID 30020 génèrent 30021. Tous les PID susmentionnés sont des sessions de script .

 [mpenning@Hotcoffee Network]$ python reproduce_bug.py I started the watchdog with: python watchdog.py -o 30016 -k 30017,30020 Traceback (most recent call last): File "reproduce_bug.py", line 35, in  raise RuntimeError, "Simulated script crash. Note that script child sessions are hung" RuntimeError: Simulated script crash. Note that script child sessions are hung [mpenning@Hotcoffee Network]$ 

Après avoir exécuté l’automatisation ci-dessus, toutes les sessions de script enfant sont toujours en cours d’exécution.

 [mpenning@Hotcoffee Models]$ ps auxw | grep script mpenning 30018 0.0 0.0 15832 508 ? S 12:08 0:00 /usr/bin/script -f LOGFILE1.txt mpenning 30021 0.0 0.0 15832 516 ? S 12:08 0:00 /usr/bin/script -f LOGFILE2.txt mpenning 30050 0.0 0.0 7548 880 pts/8 S+ 12:08 0:00 grep script [mpenning@Hotcoffee Models]$ 

Je lance l’automatisation sous Python 2.6.6, sur un système Linux Debian Squeeze (Uname -a: Linux Hotcoffee 2.6.32-5-amd64 #1 SMP Mon Jan 16 16:22:28 UTC 2012 x86_64 GNU/Linux ).

QUESTION:

Il semble que le démon ne survit pas au crash du processus de reproduction. Comment puis-je réparer watchdog.py pour fermer toutes les sessions de script si l’automatisation meurt (comme illustré dans l’exemple ci-dessus)?

Un journal watchdog.py qui illustre le problème (malheureusement, les PID ne coïncident pas avec la question d’origine) …

 [mpenning@Hotcoffee ~]$ cat watchdog.log 2012-02-22,15:17:20.356313 Start watchdog.watch_process 2012-02-22,15:17:20.356541 observe pid = 31339 2012-02-22,15:17:20.356643 kill pids = 31352,31356 2012-02-22,15:17:20.356730 seconds = 2 [mpenning@Hotcoffee ~]$ 

Résolution

Le problème était essentiellement une condition de course. Lorsque j’ai essayé de tuer les processus de script “parent”, ils étaient déjà morts en même temps que l’événement d’automatisation …

Pour résoudre le problème … tout d’abord, le démon watchdog devait identifier la liste complète des enfants à tuer avant d’interroger le PID observé (mon script d’origine a tenté d’identifier les enfants après l’écrasement du PID observé). Ensuite, j’ai dû modifier mon démon watchdog pour permettre à certains processus de script mourir avec le PID observé.


watchdog.py:

 #!/usr/bin/python """ Implement a cross-platform watchdog daemon, which observes a PID and kills other PIDs if the observed PID dies. Example: -------- watchdog.py -o 29322 -k 29345,29346,29348 -s 2 The command checks PID 29322 every 2 seconds and kills PIDs 29345, 29346, 29348 and their children, if PID 29322 dies. Requires: ---------- * https://github.com/giampaolo/psutil * http://pypi.python.org/pypi/python-daemon """ from optparse import OptionParser import datetime as dt import signal import daemon import logging import psutil import time import sys import os class MyFormatter(logging.Formatter): converter=dt.datetime.fromtimestamp def formatTime(self, record, datefmt=None): ct = self.converter(record.created) if datefmt: s = ct.strftime(datefmt) else: t = ct.strftime("%Y-%m-%d %H:%M:%S") s = "%s,%03d" % (t, record.msecs) return s def check_pid(pid): """ Check For the existence of a unix / windows pid.""" try: os.kill(pid, 0) # Kill 0 raises OSError, if pid isn't there... except OSError: return False else: return True def kill_process(logger, pid): try: psu_proc = psutil.Process(pid) except Exception, e: logger.debug('Caught Exception ["%s"] while looking up PID %s' % (e, pid)) return False logger.debug('Sending SIGTERM to %s' % repr(psu_proc)) psu_proc.send_signal(signal.SIGTERM) psu_proc.wait(timeout=None) return True def watch_process(observe, kill, seconds=2): """Kill the process IDs listed in 'kill', when 'observe' dies.""" logger = logging.getLogger(__name__) logger.setLevel(logging.DEBUG) logfile = logging.FileHandler('%s/watchdog.log' % os.getcwd()) logger.addHandler(logfile) formatter = MyFormatter(fmt='%(asctime)s %(message)s',datefmt='%Y-%m-%d,%H:%M:%S.%f') logfile.setFormatter(formatter) logger.debug('Start watchdog.watch_process') logger.debug(' observe pid = %s' % observe) logger.debug(' kill pids = %s' % kill) logger.debug(' seconds = %s' % seconds) children = list() # Get PIDs of all child processes... for childpid in kill.split(','): children.append(childpid) p = psutil.Process(int(childpid)) for subpsu in p.get_children(): children.append(str(subpsu.pid)) # Poll observed PID... while check_pid(int(observe)): logger.debug('Poll PID: %s is alive.' % observe) time.sleep(seconds) logger.debug('Poll PID: %s is *dead*, starting kills of %s' % (observe, ', '.join(children))) for pid in children: # kill all child processes... kill_process(logger, int(pid)) sys.exit(0) # Exit gracefully def run(observe, kill, seconds): with daemon.DaemonContext(detach_process=True, stdout=sys.stdout, working_directory=os.getcwd()): watch_process(observe=observe, kill=kill, seconds=seconds) if __name__=='__main__': parser = OptionParser() parser.add_option("-o", "--observe", dest="observe", type="int", help="PID to be observed", metavar="INT") parser.add_option("-k", "--kill", dest="kill", help="Comma separated list of PIDs to be killed", metavar="TEXT") parser.add_option("-s", "--seconds", dest="seconds", default=2, type="int", help="Seconds to wait between observations (default = 2)", metavar="INT") (options, args) = parser.parse_args() run(options.observe, options.kill, options.seconds) 

Votre problème est que ce script n’est pas détaché du script d’automatisation après la génération, il fonctionne donc comme un enfant et lorsque le parent meurt, il rest ingérable.

Pour gérer le script python exit, vous pouvez utiliser le module atexit . Pour surveiller les processus enfants, vous pouvez utiliser os.wait ou gérer le signal SIGCHLD

Vous pouvez essayer de tuer le groupe de processus complet contenant: le script parent, le script enfant, le script généré par le script et – peut-être – même le processus telnet .

Le manuel kill(2) dit:

Si pid est inférieur à -1, alors sig est envoyé à chaque processus du groupe de processus dont l’ID est -pid.

Donc, l’équivalent de kill -TERM -$PID fera l’affaire.

Oh, le pid dont vous avez besoin est celui du script parent.


modifier

La mise à mort du groupe de processus semble fonctionner pour moi si j’adapte les deux fonctions suivantes dans watchdog.py:

 def kill_process_group(log, pid): log.debug('killing %s' % -pid) os.kill(-pid, 15) return True def watch_process(observe, kill, seconds=2): """Kill the process IDs listed in 'kill', when 'observe' dies.""" logger = logging.getLogger(__name__) logger.setLevel(logging.DEBUG) logfile = logging.FileHandler('%s/watchdog.log' % os.getcwd()) logger.addHandler(logfile) formatter = MyFormatter(fmt='%(asctime)s %(message)s',datefmt='%Y-%m-%d,%H:%M:%S.%f') logfile.setFormatter(formatter) logger.debug('Start watchdog.watch_process') logger.debug(' observe pid = %s' % observe) logger.debug(' kill pids = %s' % kill) logger.debug(' seconds = %s' % seconds) while check_pid(int(observe)): logger.debug('PID: %s is alive.' % observe) time.sleep(seconds) logger.debug('PID: %s is *dead*, starting kills' % observe) for pid in kill.split(','): # Kill the children... kill_process_group(logger, int(pid)) sys.exit(0) # Exit gracefully 

Peut-être que vous pourriez utiliser os.system () et faire un killall dans votre chien de garde pour tuer toutes les instances de / usr / bin / script

À l’inspection, il semble que psu_proc.kill() (en fait send_signal() ) devrait OSError en cas d’échec, mais juste au cas où – avez-vous essayé de vérifier la terminaison avant de définir l’indicateur? Un péché:

 if not psu_proc.is_running(): finished = True