Подтвердить что ты не робот

Убедитесь, что Python script с подпроцессами умирает на SIGINT

У меня есть команда, которую я обертываю в script и нерест из Python script с помощью subprocess.Popen. Я пытаюсь убедиться, что он умирает, если пользователь выдает SIGINT.

Я мог бы выяснить, был ли процесс прерван хотя бы двумя способами:

а. Умрите, если обернутая команда имеет ненулевой статус выхода (не работает, поскольку script, кажется, всегда возвращает 0)

В. Сделайте что-то особенное с SIGINT в родительском Python script, а не просто прерывая подпроцесс. Я пробовал следующее:

import sys
import signal
import subprocess

def interrupt_handler(signum, frame):
    print "While there is a 'script' subprocess alive, this handler won't executes"
    sys.exit(1)
signal.signal(signal.SIGINT, interrupt_handler)

for n in range( 10 ):
    print "Going to sleep for 2 second...Ctrl-C to exit the sleep cycles"

    # exit 1 if we make it to the end of our sleep
    cmd = [ 'script', '-q', '-c', "sleep 2 && (exit 1)", '/dev/null']
    p = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE)

    while True:
        if p.poll() != None :
            break
        else :
            pass

    # Exiting on non-zero exit status would suffice
    print "Exit status (script always exits zero, despite what happened to the wrapped command):", p.returncode

Мне бы хотелось нажать Ctrl-C, чтобы выйти из python script. Вместо этого происходит подпроцесс, а script продолжается.

4b9b3361

Ответ 1

Этот хак будет работать, но он уродливый...

Измените команду:

success_flag = '/tmp/success.flag'
cmd = [ 'script', '-q', '-c', "sleep 2 && touch " + success_flag, '/dev/null']

И поставьте

if os.path.isfile( success_flag ) :
    os.remove( success_flag )
else :
    return

в конце цикла for

Ответ 2

Подпроцесс по умолчанию входит в одну и ту же группу процессов, и только один может управлять и принимать сигналы от терминала, поэтому существует несколько различных решений.

Настройка stdin как PIPE (в отличие от наследования от родительского процесса), это предотвратит получение дочерним процессом сигналов, связанных с ним.

subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, stdin=subprocess.PIPE)

Отсоединение от родительской группы процессов, ребенок больше не будет получать сигналы

def preexec_function():
    os.setpgrp()

subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, preexec_fn=preexec_function)

Явное игнорирование сигналов в дочернем процессе

def preexec_function():
    signal.signal(signal.SIGINT, signal.SIG_IGN)

subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, preexec_fn=preexec_function)

Однако это может быть перезаписано дочерним процессом.

Ответ 3

Кулачная вещь; существует метод send_signal() объекта Popen. Если вы хотите отправить сигнал тому, который вы запустили, используйте этот метод для его отправки.

Второе; более глубокая проблема с тем, как вы настраиваете связь с вашим подпроцессом, а затем, гм, не общаетесь с ним. Вы не можете смело сказать субпроцессу отправить свой вывод на subprocess.PIPE, а затем не читать из труб. Трубки UNIX буферизуются (как правило, буфер 4K?), И если подпроцесс заполняет буфер, а процесс на другом конце канала не считывает буферные данные, подпроцесс будет отвис (блокировка с точки зрения наблюдателя ) на следующей записи в трубу. Таким образом, обычный шаблон при использовании subprocess.PIPE заключается в вызове communicate() объекта Popen.

Не обязательно использовать subprocess.PIPE, если вы хотите вернуть данные из подпроцесса. Приятный трюк состоит в том, чтобы использовать класс tempfile.TemporaryFile для создания неназванного временного файла (он действительно открывает файл и сразу же удаляет inode из файловой системы, поэтому у вас есть доступ к файлу, но никто не может его открыть. может сделать что-то вроде:

with tempfile.TemporaryFile() as iofile:
    p = Popen(cmd, stdout=iofile, stderr=iofile)
    while True:
        if p.poll() is not None:
            break
        else:
            time.sleep(0.1) # without some sleep, this polling is VERY busy...

Затем вы можете прочитать содержимое своего временного файла (ищите его до начала, чтобы быть уверенным, что вы в начале), когда вы знаете, что подпроцесс вышел, вместо того, чтобы использовать каналы. Проблема буферизации каналов не будет проблемой, если выход подпроцесса будет в файл (временный или нет).

Вот пример вашего рифа, который я думаю, делает то, что вы хотите. Обработчик сигнала просто повторяет сигналы, которые захватываются родительским процессом (в этом примере SIGINT и SIGTERM) ко всем текущим подпроцессам (в этой программе должен быть только один) и устанавливает флаг уровня модуля, говорящий о завершении работы на следующем возможность. Поскольку я использую subprocess.PIPE I/O, я вызываю communicate() на объект Popen.

#!/usr/bin/env python

from subprocess import Popen, PIPE
import signal
import sys

current_subprocs = set()
shutdown = False

def handle_signal(signum, frame):
    # send signal recieved to subprocesses
    global shutdown
    shutdown = True
    for proc in current_subprocs:
        if proc.poll() is None:
            proc.send_signal(signum)


signal.signal(signal.SIGINT, handle_signal)
signal.signal(signal.SIGTERM, handle_signal)

for _ in range(10):
    if shutdown:
        break
    cmd = ["sleep", "2"]
    p = Popen(cmd, stdout=PIPE, stderr=PIPE)
    current_subprocs.add(p)
    out, err = p.communicate()
    current_subprocs.remove(p)
    print "subproc returncode", p.returncode

И называя его (с Ctrl-C в третьем 2-секундном интервале):

% python /tmp/proctest.py 
subproc returncode 0
subproc returncode 0
^Csubproc returncode -2

Ответ 4

Если у вас нет обработки python, которая будет выполняться после того, как ваш процесс будет создан (как в вашем примере), тогда самый простой способ - использовать os.execvp вместо модуля подпроцесса. Ваш подпроцесс будет полностью заменять ваш процесс python, и он будет единственным, что можно поймать SIGINT.

Ответ 5

Я нашел a -e переключатель в man-странице script:

-e      Return the exit code of the child process. Uses the same format
         as bash termination on signal termination exit code is 128+n.

Не слишком уверен, что такое 128 + n, но, похоже, он возвращает 130 для ctrl-c. Таким образом, изменение вашего cmd будет

cmd = [ 'script', '-e', '-q', '-c', "sleep 2 && (exit 1)", '/dev/null']

и положив

if p.returncode == 130:
    break

в конце цикла for, похоже, делает то, что вы хотите.