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