Вопрос по select, pty, subprocess, python – Использование подпроцесса с select и pty зависает при захвате вывода

5

Я пытаюсь написать программу на Python, способную взаимодействовать с другими программами. Это означает отправку стандартного ввода и получение данных стандартного вывода. Я не могу использовать pexpect (хотя это определенно вдохновило некоторые дизайны). Процесс, который я сейчас использую, заключается в следующем:

Attach a pty to the subprocess's stdout Loop until the subprocess exits by checking subprocess.poll When there is data available in the stdout write that data immediately to the current stdout. Finish!

Я прототипировал некоторый код (ниже), который работает, но, похоже, у меня есть один недостаток, который меня беспокоит. После завершения дочернего процесса родительский процесс зависает, если я не указываю время ожидания при использовании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. The strange random parts are just to simulate a program outputting data at random intervals. You can remove it if you wish. It shouldn't matter:

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()

Спасибо за любую помощь, которую вы все можете предоставить!

Extra note

pty используется, потому что я хочу убедиться, что stdout не буферизован.

Ваш Ответ

4   ответа
10

Прежде всего,os.read блокирует, вопреки тому, что вы заявляете. Тем не менее, он не блокирует послеselect, Такжеos.read в закрытом файле дескриптор всегда возвращает пустую строку, которую вы, возможно, захотите проверить.

Однако реальная проблема заключается в том, что дескриптор главного устройства никогда не закрывается, поэтому последнийselect это тот, который будет блокировать. В редких условиях гонки дочерний процесс завершился междуselect а такжеprocess.poll() и ваша программа выходит красиво. Однако большую часть времени выбор блокирует навсегда.

Если вы установите обработчик сигнала, как предложено izhak, все чертовски вырвется; всякий раз, когда дочерний процесс завершается, запускается обработчик сигнала. После запуска обработчика сигнала исходный системный вызов в этом потоке не может быть продолжен, поэтому при вызове системного вызова возвращается ненулевое значение errno, что часто приводит к возникновению некоторого случайного исключения в python. Теперь, если в другом месте вашей программы вы используете какую-либо библиотеку с любыми системными вызовами, которые не знают, как обрабатывать такие исключения, у вас большие проблемы (любыеos.read например, в любом месте теперь может выдать исключение, даже после успешногоselect).

Принимая во внимание, что случайные исключения генерируются где-то против опроса, я не думаю, что тайм-аут наselect не звучит так плохо В любом случае ваш процесс вряд ли будет единственным (медленным) процессом опроса в системе.

Для моего же улучшения, можете ли вы объяснить, почему мой ответ не удался? Это должно позволить вам избегать использования таймаутов.
Спасибо за фантастическое объяснение. Через некоторое время я понял, что, вероятно, было бы лучше установить тайм-аут. Я попробовал решение izhak, но да, я увидел очень странное поведение после этого. Это очень помогает! ravenac95
Я реализовал ваши предложения вthe answer to a related question
8

Есть ряд вещей, которые вы можете изменить, чтобы сделать ваш код правильным. Самая простая вещь, о которой я могу подумать, это просто закрыть копию родительского процесса ведомого fd после разветвления, чтобы, когда дочерний элемент выходил и закрывал своего собственного ведомого fd, родительский процессselect.select() пометит мастер как доступный для чтения, а последующийos.read() даст пустой результат, и ваша программа будет завершена. (Мастер pty не увидит, что подчиненный конец закрыт, покаboth копии рабов ФД закрыты.)

Итак, всего одна строка:

os.close(slave)

... помещается сразу послеsubprocess.Popen позвони, должен решить твою проблему.

Тем не менее, возможно, есть лучшие ответы, в зависимости от ваших требований. Как кто-то еще заметил, вам не нужен pty только для того, чтобы избежать буферизации. Вы могли бы использовать голыйos.pipe() на местеpty.openpty() (и относиться к возвращаемому значению точно так же). Пустой канал ОС никогда не буферизуется; если дочерний процесс не буферизирует свой вывод, то вашselect() а такжеos.read() вызовы также не видят буферизацию. Вам все еще нужноos.close(slave) линия, хотя.

Но возможно, что вам нужен pty по разным причинам. Если некоторые из ваших дочерних программ ожидают, что они будут выполняться в интерактивном режиме большую часть времени, то они могут проверять, является ли их стандартный вывод pty и ведут себя по-разному в зависимости от ответа (многие обычные утилиты делают это). Если вы действительно хотите, чтобы ребенок думал, что для него выделен терминал, тоpty Модуль это путь. В зависимости от того, как вы будете работатьrunner.py, возможно, вам придется перейти от использованияsubprocess вpty.fork(), чтобы у дочернего элемента был установлен идентификатор сеанса и предварительно открыт pty (или посмотрите исходный код pty.py, чтобы увидеть, что он делает, и продублируйте соответствующие части в вызываемом объекте вашего подпроцесса preexec_fn).

Ну, я попробовал и получил EIO, 80% времени, на Linux 3.2
Действительно, дескриптор раба не был закрыт, и я плохо за то, что не заметил этого. Однако этой строки еще недостаточно, поскольку os.read реагирует на уничтожение дочернего процесса с помощью errno = EIO, поэтому все операции чтения должны быть защищены с помощью try-кроме проверки errno = EIO и причины этого.
Хм, не должно быть никаких причин получать EIO при чтении из канала. Что касается чтения, вы должны просто получить краткое чтение в семантике POSIX (так что в этом случае пустая строка - EOF Python).
Как интересно! Я не могу воспроизвести на Linux 3.2 с чистым образом Ubuntu-Precision-12.04-AMD64-Server-20120616 на EC2, после 200 запусков. EIO предназначен только для аппаратного обеспечения или непредвиденных ошибок FS.
Странный. & quot; Linux ubuntu 3.2.0-26-generic # 41-Ubuntu SMP, четверг, 14 июня 17:49:24 UTC 2012 x86_64 x86_64 x86_64 GNU / Linux & quot ;, сбой 5-го запуска, & quot; Linux 3.1.10-grbfs-custom # 2 SMP Sun Jan 22 18:37:08 EET 2012 x86_64 GNU / Linux & quot; не удалось при первом запуске. Вы уверены, что не запустили output.py случайно (случилось со мной всего минуту назад :). Тем не менее, при запуске родителя, OSError: [Errno 5] Ошибка ввода-вывода при output = os.read (f, 1000)
0

Из того, что я понимаю, вам не нужно использовать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 чтобы избежать петли опроса.

Если программыrunner.py взаимодействует с Python, вы можете добавитьpython -u Команда Popen для включения небуферизованного вывода. Я проверил сoutputter.py и это сработало.
к сожалению, они не всегда будут приложениями на Python: - / ravenac95
panickal: Спасибо за ответ, но на самом деле я хочу убедиться, что все выходные данные не буферизируются, поэтому необходимо использовать pty. Я отредактирую вопрос, чтобы было ясно, что это требование. ravenac95
0

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

import sys
import signal

def handler(signum, frame):
    print 'Child has exited!'
    sys.exit(0)

signal.signal(signal.SIGCHLD, handler)

Сигнал также должен прервать системный вызов блокировки до «выбрать». или "читать" (или чем бы вы ни были), и позволяйте вам делать все, что вам нужно (очистка, выход и т. д.) в функции обработчика.

Похожие вопросы