Использование подпроцесса с select и pty зависает при захвате вывода
Я пытаюсь написать программу python, способную взаимодействовать с другими программами. Это означает отправку stdin и получение данных stdout. Я не могу использовать pexpect (хотя он определенно вдохновил часть дизайна). Процесс, который я сейчас использую, заключается в следующем:
- Прикрепите pty к подпроцессу stdout
- Петля, пока подпроцесс не завершится, проверив
subprocess.poll
- Когда есть данные, доступные в stdout, немедленно записывайте эти данные в текущий stdout.
- Готово!
Я прототипировал какой-то код (ниже), который работает, но, похоже, имеет один изъян, который подтачивает меня. После завершения дочернего процесса родительский процесс зависает, если я не укажу таймаут при использовании select.select
. Я бы предпочел не устанавливать тайм-аут. Это просто немного грязно. Тем не менее, все другие способы, с помощью которых я пытался обойти эту проблему, похоже, не работают. Кажется, что Pexpect обходит его, используя os.execv
и pty.fork
вместо subprocess.Popen
и pty.openpty
решение, которое я не предпочитаю. Я делаю что-то неправильно с тем, как я проверяю жизнь подпроцесса? Является ли мой подход неправильным?
Код, который я использую, приведен ниже. Я использую это в Mac OS X 10.6.8, но мне также нужно работать с Ubuntu 12.04.
Это подпроцессор runner.py
:
import subprocess
import select
import pty
import os
import sys
def main():
master, slave = pty.openpty()
process = subprocess.Popen(['python', 'outputter.py'],
stdin=subprocess.PIPE,
stdout=slave, stderr=slave, close_fds=True)
while process.poll() is None:
# Just FYI timeout is the last argument to select.select
rlist, wlist, xlist = select.select([master], [], [])
for f in rlist:
output = os.read(f, 1000) # This is used because it doesn't block
sys.stdout.write(output)
sys.stdout.flush()
print "**ALL COMPLETED**"
if __name__ == '__main__':
main()
Это код подпроцесса outputter.py
. Странные случайные части - это просто симулировать программу, выводящую данные с произвольными интервалами. Вы можете удалить его, если хотите. Это не имеет значения:
import time
import sys
import random
def main():
lines = ['hello', 'there', 'what', 'are', 'you', 'doing']
for line in lines:
sys.stdout.write(line + random.choice(['', '\n']))
sys.stdout.flush()
time.sleep(random.choice([1,2,3,4,5])/20.0)
sys.stdout.write("\ndone\n")
sys.stdout.flush()
if __name__ == '__main__':
main()
Спасибо за любую помощь, которую вы можете предоставить всем!
Дополнительная заметка
pty используется, потому что я хочу, чтобы stdout не буферизовался.
Ответы
Ответ 1
Прежде всего, os.read
блокирует, вопреки тому, что вы заявляете. Однако после select
он не блокируется. Кроме того, os.read
в закрытом дескрипторе файла всегда возвращает пустую строку, которую вы можете проверить.
Однако реальная проблема заключается в том, что дескриптор ведущего устройства никогда не закрывается, поэтому окончательный select
является тем, который будет блокироваться. В условиях редкой расы дочерний процесс выходил между select
и process.poll()
, и ваша программа отлично прекращается. Большую часть времени, однако, блокирует выбор навсегда.
Если вы установите обработчик сигнала, как было предложено izhak, все ад разрывается; всякий раз, когда дочерний процесс завершается, выполняется обработчик сигнала. После запуска обработчика сигнала исходный системный вызов в этом потоке не может быть продолжен, так что вызов syscall возвращает ненулевое значение errno, что часто приводит к тому, что какое-то случайное исключение выбрасывается в python. Теперь, если в другом месте вашей программы вы используете какую-либо библиотеку с любыми системными вызовами блокировки, которые не знают, как обрабатывать такие исключения, у вас большие проблемы (любой os.read
, например, где-либо теперь можно исключить исключение даже после успешной select
).
Взвешивание с случайными исключениями, отброшенными в любом месте, против опроса немного, я не думаю, что тайм-аут на select
не звучит так плохо. В любом случае, ваш процесс все равно будет единственным (медленным) процессом опроса в системе.
Ответ 2
Есть несколько вещей, которые вы можете изменить, чтобы сделать свой код правильным. Самое простое, что я могу придумать, это просто закрыть копию родительского процесса ведомого fd после форсирования, так что, когда ребенок выйдет и закроет свой собственный ведомый fd, родительский select.select()
отметит мастер как доступный для чтения, и последующий os.read()
даст пустой результат, и ваша программа завершится. (Мастер pty не будет считать конец подчиненного устройства закрытым до тех пор, пока обе копии ведомого fd не будут закрыты.)
Итак, только одна строка:
os.close(slave)
.., помещенный сразу после вызова subprocess.Popen
, должен исправить вашу проблему.
Однако есть, возможно, лучшие ответы, в зависимости от ваших требований. Как заметил кто-то другой, вам не нужно pty, чтобы избежать буферизации. Вы можете использовать голый os.pipe()
вместо pty.openpty()
(и обрабатывать возвращаемое значение точно так же). Голая OS-трубка никогда не будет буферизироваться; если дочерний процесс не буферизует свой вывод, тогда ваши вызовы select()
и os.read()
также не будут видеть буферизацию. Тем не менее вам понадобится строка os.close(slave)
.
Но возможно, что вам нужно pty по разным причинам. Если некоторые из ваших дочерних программ ожидают, что они будут выполняться в интерактивном режиме большую часть времени, тогда они могут проверять, является ли их stdin pty и ведет себя по-разному в зависимости от ответа (это делает множество общих утилит). Если вы действительно хотите, чтобы ребенок подумал, что у него есть выделенный для него терминал, то модуль pty
- это путь. В зависимости от того, как вы будете запускать runner.py
, вам может потребоваться переключиться с subprocess
на pty.fork()
, чтобы у ребенка был установлен его идентификатор сеанса, а pty предварительно открыт (или посмотреть источник для pty.py чтобы увидеть, что он делает, и дублировать соответствующие части вашего объекта подпроцесса preexec_fn, вызываемого).
Ответ 3
Из того, что я понимаю, вам не нужно использовать pty
. runner.py
можно изменить как
import subprocess
import sys
def main():
process = subprocess.Popen(['python', 'outputter.py'],
stdin=subprocess.PIPE,
stdout=subprocess.PIPE, stderr=subprocess.PIPE)
while process.poll() is None:
output = process.stdout.readline()
sys.stdout.write(output)
sys.stdout.flush()
print "**ALL COMPLETED**"
if __name__ == '__main__':
main()
process.stdout.read(1)
может использоваться вместо process.stdout.readline()
для вывода в реальном времени для каждого символа из подпроцесса.
Примечание. Если вы не требуете вывода в реальном времени из подпроцесса, используйте Popen.communicate, чтобы избежать цикла опроса.
Ответ 4
Когда ваш дочерний процесс завершается - ваш родительский процесс получает сигнал SIGCHLD. По умолчанию этот сигнал игнорируется, но вы можете его перехватить:
import sys
import signal
def handler(signum, frame):
print 'Child has exited!'
sys.exit(0)
signal.signal(signal.SIGCHLD, handler)
Сигнал также должен отключить системный столбец блокировки, чтобы "выбрать" или "прочитать" (или что бы вы ни находились), и позволить вам делать все, что вам нужно (очистка, выход и т.д.) в функции обработчика.