Убедитесь, что 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 продолжается.
Ответы
Ответ 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, похоже, делает то, что вы хотите.