Использование подпроцесса с select и pty зависает при захвате вывода
====================================================================

Я пытаюсь написать программу на 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`:

Это код подпроцесса`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:

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

**Extra note**

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

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

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

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

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



Когда ваш дочерний процесс завершается - ваш родительский процесс получает[SIGCHLD](https://en.wikipedia.org/wiki/SIGCHLD) сигнал. По умолчанию этот сигнал игнорируется, но вы можете перехватить его:

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



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

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

... помещается сразу после`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).



Из того, что я понимаю, вам не нужно использовать`pty`. `runner.py` можно изменить как

`process.stdout.read(1)` можно использовать вместо`process.stdout.readline()` для вывода в реальном времени на символ из подпроцесса.

Примечание. Если вам не требуется вывод в реальном времени из подпроцесса, используйте[Popen.communicate](https://docs.python.org/library/subprocess.html#subprocess.Popen.communicate) чтобы избежать петли опроса.