Надежный кроссплатформенный процесс kill daemon - PullRequest
9 голосов
/ 22 февраля 2012

У меня есть некоторая автоматизация Python, которая порождает telnet сеансы, которые я регистрирую с помощью команды linux script; есть два script идентификатора процесса (родительский и дочерний) для каждого сеанса регистрации.

Мне нужно решить проблему, когда в случае смерти сценария автоматизации python сеансы script никогда не закрываются сами по себе; по какой-то причине это намного сложнее, чем должно быть.

Пока что я реализовал watchdog.py (см. Нижнюю часть вопроса), который демонизирует себя и опрашивает PID скрипта автоматизации Python в цикле. Когда он видит, что PID автоматизации Python исчезает из таблицы процессов сервера, он пытается уничтожить сеансы script.

Моя проблема:

  • script сеансы всегда порождают два отдельных процесса, один из сеансов script является родителем другого сеанса script.
  • watchdog.py не будет убивать ребенка script сеансов, если я начну script сеансов из сценария автоматизации (см. ПРИМЕР АВТОМАТИЗАЦИИ , ниже)

ПРИМЕР АВТОМАТИЗАЦИИ (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"

Теперь пример того, что происходит, когда я запускаю автоматизацию, приведенную выше ... обратите внимание, что PID 30017 порождает 30018, а PID 30020 порождает 30021. Все вышеупомянутые PID - это 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 <module>
    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]$

После того как я запустил вышеописанную автоматизацию, все дочерние script сеансы все еще работают.

[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]$

Я запускаю автоматизацию под Python 2.6.6 в системе 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).

ВОПРОС:

Кажется, что демон не переживает крах процесса нереста. Как я могу исправить watchdog.py, чтобы закрыть все сессии скрипта, если автоматизация умирает (как показано в примере выше)?

A watchdog.py журнал, который иллюстрирует проблему (к сожалению, PID не совпадают с исходным вопросом) ...

[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 ~]$

Разрешение

По сути, проблема заключалась в состоянии гонки. Когда я попытался уничтожить «родительские» процессы script, они уже умерли по совпадению с событием автоматизации ...

Чтобы решить эту проблему ... во-первых, демону-наблюдателю необходимо было идентифицировать весь список детей, которые должны быть убиты до опроса наблюдаемого PID (мой оригинальный скрипт пытался идентифицировать детей после того, как наблюдаемый PID потерпел крах). Затем мне пришлось изменить мой сторожевой демон, чтобы учесть вероятность того, что некоторые процессы script могут умереть с наблюдаемым PID.


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)

Ответы [ 4 ]

2 голосов
/ 23 февраля 2012

Ваша проблема в том, что скрипт не отсоединяется от скрипта автоматизации после создания, поэтому он работает как дочерний, а когда родитель умирает, он остается неуправляемым.

Для обработки выхода из скрипта Python вы можете использовать atexit модуль.Для контроля выхода дочерних процессов вы можете использовать os.wait или обрабатывать сигнал SIGCHLD

1 голос
/ 23 февраля 2012

Вы можете попытаться убить весь процесс группа , содержащая: родительский script, дочерний script, bash, порожденный script и - возможно - даже telnet процесс.

Руководство kill(2) гласит:

Если pid меньше -1, то sig отправляется каждому процессу в группе процессов с идентификатором -pid.

Таким образом, эквивалент kill -TERM -$PID сделает работу.

О, пид вам нужен родитель script.


Редактировать

Мне кажется, что уничтожение группы процессов сработает, если я адаптирую следующие две функции в 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
0 голосов
/ 22 февраля 2012

При проверке кажется , что psu_proc.kill() (на самом деле send_signal()) должно поднять OSError в случае сбоя, но на всякий случай - вы пытались проверить завершение перед установкой флага? Как в:

if not psu_proc.is_running():
  finished = True
0 голосов
/ 22 февраля 2012

Возможно, вы могли бы использовать os.system () и выполнить killall в вашем сторожевом таймере, чтобы уничтожить все экземпляры / usr / bin / script

...