Выполнить команду и получить ее stdout, stderr отдельно в почти реальном времени, как в терминале

Я пытаюсь найти способ в Python для запуска других программ таким образом, что:

  1. Stdout и stderr запускаемой программы могут быть зарегистрированы отдельно.
  2. Stdout и stderr запускаемой программы можно просматривать почти в реальном времени, так что, если дочерний процесс зависает, пользователь может видеть. (т.е. мы не ждем завершения выполнения, прежде чем печатать stdout/stderr для пользователя)
  3. Бонусные критерии: запускаемая программа не знает, что она запускается через python, и, следовательно, не будет делать неожиданные вещи (например, чанкировать ее вывод вместо печати в режиме реального времени или завершаться, потому что она требует, чтобы терминал просматривал ее вывод), Этот небольшой критерий в значительной степени означает, что нам нужно использовать pty, я думаю.

Вот то, что я получил до сих пор... Метод 1:

def method1(command):
    ## subprocess.communicate() will give us the stdout and stderr sepurately, 
    ## but we will have to wait until the end of command execution to print anything.
    ## This means if the child process hangs, we will never know....
    proc=subprocess.Popen(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=True, executable='/bin/bash')
    stdout, stderr = proc.communicate() # record both, but no way to print stdout/stderr in real-time
    print ' ######### REAL-TIME ######### '
    ########         Not Possible
    print ' ########## RESULTS ########## '
    print 'STDOUT:'
    print stdout
    print 'STDOUT:'
    print stderr

Способ 2

def method2(command):
    ## Using pexpect to run our command in a pty, we can see the child stdout in real-time,
    ## however we cannot see the stderr from "curl google.com", presumably because it is not connected to a pty?
    ## Furthermore, I do not know how to log it beyond writing out to a file (p.logfile). I need the stdout and stderr
    ## as strings, not files on disk! On the upside, pexpect would give alot of extra functionality (if it worked!)
    proc = pexpect.spawn('/bin/bash', ['-c', command])
    print ' ######### REAL-TIME ######### '
    proc.interact()
    print ' ########## RESULTS ########## '
    ########         Not Possible

Способ 3:

def method3(command):
    ## This method is very much like method1, and would work exactly as desired
    ## if only proc.xxx.read(1) wouldn't block waiting for something. Which it does. So this is useless.
    proc=subprocess.Popen(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=True, executable='/bin/bash')
    print ' ######### REAL-TIME ######### '
    out,err,outbuf,errbuf = '','','',''
    firstToSpeak = None
    while proc.poll() == None:
            stdout = proc.stdout.read(1) # blocks
            stderr = proc.stderr.read(1) # also blocks
            if firstToSpeak == None:
                if stdout != '': firstToSpeak = 'stdout'; outbuf,errbuf = stdout,stderr
                elif stderr != '': firstToSpeak = 'stderr'; outbuf,errbuf = stdout,stderr
            else:
                if (stdout != '') or (stderr != ''): outbuf += stdout; errbuf += stderr
                else:
                    out += outbuf; err += errbuf;
                    if firstToSpeak == 'stdout': sys.stdout.write(outbuf+errbuf);sys.stdout.flush()
                    else: sys.stdout.write(errbuf+outbuf);sys.stdout.flush()
                    firstToSpeak = None
    print ''
    print ' ########## RESULTS ########## '
    print 'STDOUT:'
    print out
    print 'STDERR:'
    print err

Чтобы опробовать эти методы, вам нужно будет import sys,subprocess,pexpect

pexpect чисто python и может иметься с

sudo pip install pexpect

Я думаю, что решение будет включать модуль Python Pty - что-то вроде черного искусства, что я не могу найти никого, кто знает, как использовать. Возможно, SO знает :) В качестве хедз-апа я рекомендую использовать 'curl www.google.com' в качестве команды тестирования, потому что по какой-то причине она выводит свой статус на stderr: D


UPDATE-1:
Хорошо, поэтому библиотека pty не подходит для потребления человеком. Документы, по сути, являются исходным кодом. Любое представленное решение, которое является блокирующим и не асинхронным, здесь не будет работать. Метод Threads/Queue от Padraic Cunningham отлично работает, хотя добавить поддержку pty невозможно - и он "грязный" (цитируя Freenode #python). Кажется, что единственное решение, подходящее для производственного стандартного кода, - это использование Twisted Framework, который даже поддерживает pty в качестве логического переключателя для запуска процессов точно так же, как если бы они были вызваны из оболочки. Но добавление Twisted в проект требует полного переписывания всего кода. Это полный облом:/

UPDATE-2:

Было предоставлено два ответа, один из которых касается первых двух критериев и будет хорошо работать, когда вам просто нужны и stdout, и stderr, используя Threads and Queue. Другой ответ использует select, неблокирующий метод для чтения файловых дескрипторов, и pty, метод, чтобы "обмануть" порожденный процесс, заставив его поверить, что он работает в реальном терминале, как если бы он был запущен непосредственно из Bash - но может или может не иметь побочных эффектов. Я хотел бы принять оба ответа, потому что "правильный" метод действительно зависит от ситуации и того, почему вы в первую очередь выполняете подпроцесс, но, увы, я мог принять только один.

Ответ 1

Stdout и stderr запускаемой программы могут быть зарегистрированы отдельно.

Вы не можете использовать pexpect потому что и stdout, и stderr обращаются к одному и тому же pty и после этого невозможно разделить их.

Stdout и stderr запускаемой программы можно просматривать почти в реальном времени, так что, если дочерний процесс зависает, пользователь может видеть. (т.е. мы не ждем завершения выполнения, прежде чем печатать stdout/stderr для пользователя)

Если вывод подпроцесса не является tty, то, вероятно, он использует буферизацию блоков и, следовательно, если он не выдает много выходных данных, он не будет "в реальном времени", например, если буфер 4K, то ваш родитель Процесс Python ничего не увидит, пока дочерний процесс не напечатает 4K-символы и буфер не переполнится или не будет сброшен явно (внутри подпроцесса). Этот буфер находится внутри дочернего процесса, и нет стандартных способов управлять им извне. Вот картинка, которая показывает буферы stdio и конвейерный буфер для command 1 | command2 Оболочка command 1 | command2:

pipe/stdio buffers

Выполняемая программа не знает, что она запускается через python, и, таким образом, не будет делать неожиданные вещи (например, чанкировать ее вывод вместо печати в реальном времени или завершаться, потому что она требует, чтобы терминал просматривал ее вывод).

Кажется, вы имели в виду противоположное, т.е. Вероятно, что ваш дочерний процесс разбивает свой вывод на части вместо того, чтобы как можно быстрее очищать каждую строку вывода, если вывод перенаправляется в канал (когда вы используете stdout=PIPE в Python). Это означает, что используемые по умолчанию потоки или асинхронные решения не будут работать, как в вашем случае.

Есть несколько вариантов, чтобы обойти это:

  • команда может принять аргумент командной строки, такой как grep --line-buffered или python -u, чтобы отключить блочную буферизацию.

  • stdbuf работает для некоторых программ, т.е. вы можете запустить ['stdbuf', '-oL', '-eL'] + command используя решение для потоков или асинхронности, описанное выше, и вы должны получить stdout, stderr отдельно, а строки должны появиться в -реальное время:

    #!/usr/bin/env python3
    import os
    import sys
    from select import select
    from subprocess import Popen, PIPE
    
    with Popen(['stdbuf', '-oL', '-e0', 'curl', 'www.google.com'],
               stdout=PIPE, stderr=PIPE) as p:
        readable = {
            p.stdout.fileno(): sys.stdout.buffer, # log separately
            p.stderr.fileno(): sys.stderr.buffer,
        }
        while readable:
            for fd in select(readable, [], [])[0]:
                data = os.read(fd, 1024) # read available
                if not data: # EOF
                    del readable[fd]
                else: 
                    readable[fd].write(data)
                    readable[fd].flush()
    
  • наконец, вы можете попробовать pty + select решение с двумя pty:

    #!/usr/bin/env python3
    import errno
    import os
    import pty
    import sys
    from select import select
    from subprocess import Popen
    
    masters, slaves = zip(pty.openpty(), pty.openpty())
    with Popen([sys.executable, '-c', r'''import sys, time
    print('stdout', 1) # no explicit flush
    time.sleep(.5)
    print('stderr', 2, file=sys.stderr)
    time.sleep(.5)
    print('stdout', 3)
    time.sleep(.5)
    print('stderr', 4, file=sys.stderr)
    '''],
               stdin=slaves[0], stdout=slaves[0], stderr=slaves[1]):
        for fd in slaves:
            os.close(fd) # no input
        readable = {
            masters[0]: sys.stdout.buffer, # log separately
            masters[1]: sys.stderr.buffer,
        }
        while readable:
            for fd in select(readable, [], [])[0]:
                try:
                    data = os.read(fd, 1024) # read available
                except OSError as e:
                    if e.errno != errno.EIO:
                        raise #XXX cleanup
                    del readable[fd] # EIO means EOF on some systems
                else:
                    if not data: # EOF
                        del readable[fd]
                    else:
                        readable[fd].write(data)
                        readable[fd].flush()
    for fd in masters:
        os.close(fd)
    

    Я не знаю, каковы побочные эффекты использования разных pty для stdout, stderr. Вы можете попробовать, достаточно ли одного pty в вашем случае, например, установить stderr=PIPE и использовать p.stderr.fileno() вместо p.stderr.fileno() masters[1]. Комментарий в источнике sh говорит о том, что есть проблемы, если stderr not in {STDOUT, pipe}

Ответ 2

Если вы хотите читать из stderr и stdout и получать результат отдельно, вы можете использовать Thread с очередью, не слишком проверенный, но что-то вроде следующего:

import threading
import queue

def run(fd, q):
    for line in iter(fd.readline, ''):
        q.put(line)
    q.put(None)


def create(fd):
    q = queue.Queue()
    t = threading.Thread(target=run, args=(fd, q))
    t.daemon = True
    t.start()
    return q, t


process = Popen(["curl","www.google.com"], stdout=PIPE, stderr=PIPE,
                universal_newlines=True)

std_q, std_out = create(process.stdout)
err_q, err_read = create(process.stderr)

while std_out.is_alive() or err_read.is_alive():
        for line in iter(std_q.get, None):
            print(line)
        for line in iter(err_q.get, None):
            print(line)

Ответ 3

В то время как ответ Дж. Ф. Себастьяна, безусловно, решает суть проблемы, я запускаю python 2.7 (что не было в исходных критериях), поэтому я просто бросаю это туда любым другим утомленным путешественникам, которые просто хотят вырезать/вставить некоторый код. Я не тестировал это все еще, но по всем командам, которые я пробовал, кажется, работает отлично:) вы можете захотеть изменить .decode('ascii') на .decode('utf-8') - im, все еще проверяя это.

#!/usr/bin/env python2.7
import errno
import os
import pty
import sys
from select import select
import subprocess
stdout = ''
stderr = ''
command = 'curl google.com ; sleep 5 ; echo "hey"'
masters, slaves = zip(pty.openpty(), pty.openpty())
p = subprocess.Popen(command, stdin=slaves[0], stdout=slaves[0], stderr=slaves[1], shell=True, executable='/bin/bash')
for fd in slaves: os.close(fd)

readable = { masters[0]: sys.stdout, masters[1]: sys.stderr }
try:
    print ' ######### REAL-TIME ######### '
    while readable:
        for fd in select(readable, [], [])[0]:
            try: data = os.read(fd, 1024)
            except OSError as e:
                if e.errno != errno.EIO: raise
                del readable[fd]
            finally:
                if not data: del readable[fd]
                else:
                    if fd == masters[0]: stdout += data.decode('ascii')
                    else: stderr += data.decode('ascii')
                    readable[fd].write(data)
                    readable[fd].flush()
except: pass
finally:
    p.wait()
    for fd in masters: os.close(fd)
    print ''
    print ' ########## RESULTS ########## '
    print 'STDOUT:'
    print stdout
    print 'STDERR:'
    print stderr