2012-06-22 3 views
5

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

  1. не Приложить PTY на стандартный вывод подпроцесса в
  2. Loop до выходов подпроцесса, проверяя subprocess.poll
    • Когда имеется в стандартном выводе пишет, что данные данные немедленно к текущему этапу.
  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() 

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

Дополнительное примечание

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

ответ

10

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

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

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

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

+0

Спасибо за фантастическое объяснение. Через какое-то время я понял, что, скорее всего, лучше всего установить тайм-аут. Я попробовал решение izhak, но да, после этого я увидел очень странное поведение. Это помогает много! – ravenac95

+0

Для моего собственного улучшения, можете ли вы объяснить, почему мой ответ был коротким? Это должно позволить вам избежать использования тайм-аутов. –

+0

Я внедрил ваши предложения в [ответ на соответствующий вопрос] (http://stackoverflow.com/a/12471855/4279) – jfs

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, чтобы избежать цикла опроса.

+0

panickal: Спасибо за ответ, но на самом деле я хочу убедиться, что любой выход не буферизирован, следовательно, требуется pty. Я отредактирую вопрос, чтобы было ясно, что это требование. – ravenac95

+0

Если программы 'runner.py' взаимодействуют с python, вы можете добавить' python -u' в команду Popen для включения небуферизованного вывода. Я тестировал с помощью 'outputter.py', и он работал. – panickal

+0

, к сожалению, они не всегда будут приложениями python: -/ – ravenac95

0

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

import sys 
import signal 

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

signal.signal(signal.SIGCHLD, handler) 

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

8

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

Таким образом, только одна строка:

os.close(slave) 

..placed сразу же после 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 объекта subprocess).

+0

Действительно, подчиненный дескриптор не был закрыт, а мой плохой, чтобы не заметить его. Однако эта строка еще не достаточно сама по себе, так как os.read реагирует на убийство дочернего процесса с errno = EIO, поэтому все чтения должны быть защищены с помощью try-except check for errno = EIO и причины, по которой он находится. –

+0

Хмм, не должно быть никаких оснований для получения EIO при чтении из трубы. На стороне чтения вы должны просто получить короткое чтение в соответствии с семантикой POSIX (так что в этом случае пустая строка - EOF python). –

+1

Ну, я попробовал, и я получил EIO, 80% времени, на linux 3.2 –

Смежные вопросы