Вопрос-Ответ

live output from subprocess command

оперативный вывод из команды подпроцесса

Я использую скрипт на Python в качестве драйвера для кода гидродинамики. Когда приходит время запускать симуляцию, я использую subprocess.Popen для запуска кода, сбора выходных данных из stdout и stderr в subprocess.PIPE --- затем я могу распечатать (и сохранить в лог-файл) выходную информацию и проверить, нет ли ошибок. Проблема в том, что я понятия не имею, как продвигается код. Если я запускаю его непосредственно из командной строки, он выдает мне информацию о том, на какой итерации он выполняется, в какое время, каков следующий временной шаг и т.д.

Есть ли способ как сохранить вывод (для ведения журнала и проверки ошибок), так и создать вывод в реальном времени?

Соответствующий раздел моего кода:

ret_val = subprocess.Popen( run_command, stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=True )
output, errors = ret_val.communicate()
log_file.write(output)
print output
if( ret_val.returncode ):
print "RUN failed\n\n%s\n\n" % (errors)
success = False

if( errors ): log_file.write("\n\n%s\n\n" % errors)

Изначально я передавал run_command через tee, чтобы копия отправлялась непосредственно в файл журнала, а поток по-прежнему выводился непосредственно на терминал - но таким образом я не могу сохранить никаких ошибок (насколько я знаю).


Пока что мое временное решение:

ret_val = subprocess.Popen( run_command, stdout=log_file, stderr=subprocess.PIPE, shell=True )
while not ret_val.poll():
log_file.flush()

затем в другом терминале запустите tail -f log.txt (s.t. log_file = 'log.txt').

Переведено автоматически
Ответ 1

TLDR для Python 3:

import subprocess
import sys

with open("test.log", "wb") as f:
process = subprocess.Popen(your_command, stdout=subprocess.PIPE)
for c in iter(lambda: process.stdout.read(1), b""):
sys.stdout.buffer.write(c)
f.buffer.write(c)

У вас есть два способа сделать это, либо создав итератор из read или readline функций и выполнив:

import subprocess
import sys

# replace "w" with "wb" for Python 3
with open("test.log", "w") as f:
process = subprocess.Popen(your_command, stdout=subprocess.PIPE)
# replace "" with b'' for Python 3
for c in iter(lambda: process.stdout.read(1), ""):
sys.stdout.write(c)
f.write(c)

или

import subprocess
import sys

# replace "w" with "wb" for Python 3
with open("test.log", "w") as f:
process = subprocess.Popen(your_command, stdout=subprocess.PIPE)
# replace "" with b"" for Python 3
for line in iter(process.stdout.readline, ""):
sys.stdout.write(line)
f.write(line)

Или вы можете создать reader и writer файл. Передайте writer в Popen и прочитайте из reader

import io
import time
import subprocess
import sys

filename = "test.log"
with io.open(filename, "wb") as writer, io.open(filename, "rb", 1) as reader:
process = subprocess.Popen(command, stdout=writer)
while process.poll() is None:
sys.stdout.write(reader.read())
time.sleep(0.5)
# Read the remaining
sys.stdout.write(reader.read())

Таким образом, у вас будут данные, записанные в test.log так же, как и в стандартном выводе.

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

Ответ 2

Краткое описание (или версия "tl; dr"): это легко, когда есть не более одного subprocess.PIPE, в противном случае это сложно.

Возможно, пришло время немного объяснить, как subprocess.Popen это работает.

(Предостережение: это для Python 2.x, хотя 3.x похож; и я довольно нечетко разбираюсь в варианте Windows. Я намного лучше разбираюсь в материалах POSIX.)

Popen Функция должна обрабатывать потоки ввода-вывода от нуля до трех одновременно. Они обозначаются stdin, stdout и stderr как обычно.

Вы можете предоставить:


  • None указывает, что вы не хотите перенаправлять поток. Вместо этого он унаследует их, как обычно. Обратите внимание, что, по крайней мере, в системах POSIX это не означает, что он будет использовать Python sys.stdout, только фактический стандартный вывод Python; смотрите Демонстрацию в конце.

  • Значение int. Это "необработанный" файловый дескриптор (по крайней мере, в POSIX). (Примечание: PIPE и STDOUT на самом деле int являются s внутри, но являются "невозможными" дескрипторами, -1 и -2.)

  • Поток - на самом деле, любой объект с fileno методом. Popen найдет дескриптор для этого потока, используя stream.fileno(), а затем продолжит работу как для int значения.

  • subprocess.PIPE, указывающий, что Python должен создать канал.

  • subprocess.STDOUT (только для stderr): скажите Python использовать тот же дескриптор, что и для stdout. Это имеет смысл только в том случае, если вы указали (не None) значение для stdout, и даже тогда оно необходимо только в том случае, если вы установили stdout=subprocess.PIPE. (В противном случае вы можете просто указать тот же аргумент, который вы указали для stdout, например, Popen(..., stdout=stream, stderr=stream).)

Самые простые случаи (без каналов)

Если вы ничего не перенаправляете (оставьте все три None значения по умолчанию или укажите явно None), Pipe сделать это довольно просто. Нужно просто отключить подпроцесс и дать ему запуститься. Или, если вы перенаправляете на не-PIPEan int или поток fileno() — это все равно легко, поскольку ОС выполняет всю работу. Python просто нужно отключить подпроцесс, подключив его stdin, stdout и / или stderr к предоставленным файловым дескрипторам.

Все еще простой пример: один канал

Если вы перенаправляете только один поток, Pipe все еще довольно просто. Давайте выбирать по одному потоку за раз и наблюдать.

Предположим, вы хотите предоставить некоторые stdin, но пусть stdout и stderr не перенаправляются или переходят к файловому дескриптору. Как родительский процесс, ваша программа на Python просто должна использовать write() для отправки данных по каналу. Вы можете сделать это самостоятельно, например:

proc = subprocess.Popen(cmd, stdin=subprocess.PIPE)
proc.stdin.write('here, have some data\n') # etc

или вы можете передать данные stdin в proc.communicate(), который затем выполняет stdin.write показанное выше. Выходные данные не возвращаются, поэтому communicate() есть только одна другая реальная задача: она также закрывает канал для вас. (Если вы не вызываете proc.communicate() вы должны вызвать proc.stdin.close(), чтобы закрыть канал, чтобы подпроцесс знал, что данные больше не поступают.)

Предположим, вы хотите захватить, stdout но оставить stdin и stderr в покое. Опять же, это просто: просто вызывайте proc.stdout.read() (или эквивалентно), пока больше не будет вывода. Поскольку proc.stdout() это обычный поток ввода-вывода Python, вы можете использовать в нем все обычные конструкции, например:

for line in proc.stdout:

или, опять же, вы можете использовать proc.communicate(), который просто выполняет read() за вас.

Если вы хотите только захватывать stderr, это работает так же, как с stdout.

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

proc = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)

В этом случае subprocess "обманывает"! Что ж, он должен это делать, так что на самом деле это не обман: он запускает подпроцесс как с его stdout, так и с его stderr, направленными в (одиночный) дескриптор канала, который возвращается к его родительскому процессу (Python). На родительской стороне снова есть только один дескриптор канала для чтения выходных данных. Все выходные данные "stderr" отображаются в proc.stdout, и если вы вызовете proc.communicate(), результатом stderr (второе значение в кортеже) будет None, а не строка.

Сложные случаи: два или более канала

Все проблемы возникают, когда вы хотите использовать как минимум два канала. Фактически, в subprocess самом коде есть этот бит:

def communicate(self, input=None):
...
# Optimization: If we are only using one pipe, or no pipe at
# all, using select() or threads is unnecessary.
if [self.stdin, self.stdout, self.stderr].count(None) >= 2:

Но, увы, здесь мы создали как минимум два, а может и три разных канала, поэтому count(None) возвращает либо 1, либо 0. Мы должны действовать сложным способом.

В Windows это используется threading.Thread для накопления результатов для self.stdout и self.stderr, и родительский поток доставляет self.stdin входные данные (а затем закрывает канал).

В POSIX это использует poll если доступно, в противном случае select для накопления выходных данных и доставки входных данных stdin. Все это выполняется в (единственном) родительском процессе / потоке.

Здесь необходимы потоки или poll / select, чтобы избежать взаимоблокировки. Предположим, например, что мы перенаправили все три потока в три отдельных канала. Предположим далее, что существует небольшое ограничение на количество данных, которые могут быть загружены в канал, прежде чем процесс записи будет приостановлен, ожидая, пока процесс чтения "очистит" канал с другого конца. Давайте установим это небольшое ограничение в один байт, просто для иллюстрации. (На самом деле так все и работает, за исключением того, что ограничение намного больше одного байта.)

Если родительский процесс (Python) пытается записать несколько байтов, скажем, 'go\n'в proc.stdin, вводится первый байт, а затем второй заставляет процесс Python приостанавливаться, ожидая, пока подпроцесс прочитает первый байт, опустошая канал.

Между тем, предположим, что подпроцесс решает напечатать дружелюбное приветствие "Привет! Не паникуйте!". H переходит в канал стандартного вывода, но e заставляет его приостанавливаться, ожидая, пока его родительский файл прочитает это H, опустошая канал стандартного вывода.

Теперь мы застряли: процесс Python находится в спящем режиме, ожидая, когда закончит говорить "go", и подпроцесс также находится в спящем режиме, ожидая, когда закончит говорить "Hello! Не паникуйте!".

subprocess.Popen Код позволяет избежать этой проблемы с помощью потоковой передачи или выбора / опроса. Когда байты могут передаваться по каналам, они передаются. Когда они не могут, только поток (не весь процесс) должен переходить в спящий режим - или, в случае выбора / опроса, процесс Python одновременно ожидает "может записать" или "данные доступны", записывает в stdin процесса только тогда, когда есть место, и считывает его stdout и / или stderr только тогда, когда данные готовы. proc.communicate() Код (фактически _communicate, в котором обрабатываются сложные случаи) возвращает результат после отправки всех данных stdin (если таковые имеются) и накопления всех данных stdout и / или stderr.

Если вы хотите читать оба stdout и stderr по двум разным каналам (независимо от любого stdin перенаправления), вам также нужно будет избегать взаимоблокировки. Сценарий взаимоблокировки здесь отличается — он возникает, когда подпроцесс записывает что-то длинное в stderr пока вы извлекаете данные из stdout, или наоборот — но он все еще существует.


Демонстрация

Я обещал продемонстрировать, что без перенаправления Python subprocessвыполняет запись в базовый стандартный вывод, а не sys.stdout. Итак, вот некоторый код:

from cStringIO import StringIO
import os
import subprocess
import sys

def show1():
print 'start show1'
save = sys.stdout
sys.stdout = StringIO()
print 'sys.stdout being buffered'
proc = subprocess.Popen(['echo', 'hello'])
proc.wait()
in_stdout = sys.stdout.getvalue()
sys.stdout = save
print 'in buffer:', in_stdout

def show2():
print 'start show2'
save = sys.stdout
sys.stdout = open(os.devnull, 'w')
print 'after redirect sys.stdout'
proc = subprocess.Popen(['echo', 'hello'])
proc.wait()
sys.stdout = save

show1()
show2()

При запуске:

$ python out.py
start show1
hello
in buffer: sys.stdout being buffered

start show2
hello

Обратите внимание, что первая процедура завершится неудачей, если вы добавите stdout=sys.stdout, поскольку у StringIO объекта нет fileno. Во втором будет опущен, hello если вы добавите, stdout=sys.stdout поскольку sys.stdout был перенаправлен на os.devnull.

(Если вы перенаправите файловый дескриптор Python-1, подпроцесс будет следовать этому перенаправлению. open(os.devnull, 'w') Вызов создает поток, fileno() размер которого больше 2.)

Ответ 3

Мы также можем использовать итератор файла по умолчанию для чтения стандартного вывода вместо использования iter construct с readline().

import subprocess
import sys

process = subprocess.Popen(
your_command, stdout=subprocess.PIPE, stderr=subprocess.STDOUT
)
for line in process.stdout:
sys.stdout.write(line)
Ответ 4

В дополнение ко всем этим ответам, один простой подход также может быть следующим:

process = subprocess.Popen(your_command, stdout=subprocess.PIPE)

while process.stdout.readable():
line = process.stdout.readline()

if not line:
break

print(line.strip())

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

Ключевым моментом здесь является то, что readline() возвращает строку (с \n в конце) до тех пор, пока есть вывод, и пустую, если она действительно находится в конце.

Надеюсь, это кому-то поможет.

python subprocess