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

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

  1. Прикрепите pty к стандартному выводу подпроцесса
  2. Цикл до выхода из подпроцесса, проверяя subprocess.poll
    • Когда в stdout есть доступные данные, немедленно запишите эти данные в текущий stdout.
  3. Конец!

Я прототипировал некоторый код (ниже), который работает, но, кажется, есть один недостаток, который меня беспокоит. После завершения дочернего процесса родительский процесс зависает, если я не указываю время ожидания при использовании 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, потому что я хочу убедиться, что стандартный вывод не буферизован.

4 ответа

Решение

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

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

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

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

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

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

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 объекта вашего подпроцесса).

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

import sys
import signal

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

signal.signal(signal.SIGCHLD, handler)

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

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

Другие вопросы по тегам