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

186

Я использую скрипт 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(st log_file = 'log.txt').

DilithiumMatrix
источник
1
Может быть, вы можете использовать Popen.pollкак в предыдущем вопросе переполнения стека .
Пауло Алмейда
Некоторые команды, которые показывают индикацию хода выполнения (например, git), делают это только в том случае, если их вывод является «tty device» (протестировано через libc isatty()). В этом случае вам, возможно, придется открыть псевдо-tty.
Торек
@torek что такое (псевдо) tty?
DilithiumMatrix
2
Устройства в Unix-подобных системах, которые позволяют процессу выдавать себя за пользователя на последовательном порту. Так работает, например, ssh (на стороне сервера). Смотрите библиотеку python pty , а также pexpect .
Торек
Re временного решения: нет необходимости звонить flush, и это нужно читать из Stderr трубы , если подпроцесс производит много выхода Stderr. В поле для комментариев недостаточно места, чтобы объяснить это ...
torek

Ответы:

169

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

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

или

import subprocess
import sys
with open('test.log', 'w') as f:  # replace 'w' with 'wb' for Python 3
    process = subprocess.Popen(your_command, stdout=subprocess.PIPE)
    for line in iter(process.stdout.readline, ''):  # replace '' with b'' for Python 3
        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функции будут блокироваться до тех пор, пока один канал не будет записан в канал, или строка не будет записана в канал соответственно.

Виктор Керкез
источник
1
Тьфу :-) Пиши в файл, читай из него и засыпай в цикле? Также есть шанс, что процесс завершится до того, как вы закончили читать файл.
Гай Сиртон
13
С Python 3 вам нужно iter(process.stdout.readline, b'')(т. Е. Часовой, передаваемый iter, должен быть двоичной строкой, поскольку b'' != ''.
John Mellor
3
Для бинарных потоков сделайте это:for line in iter(process.stdout.readline, b''): sys.stdout.buffer.write(line)
rrlamichhane
6
В дополнение к ответу @JohnMellor, в Python 3 потребовались следующие модификации: process = subprocess.Popen(command, stderr=subprocess.STDOUT, stdout=subprocess.PIPE) for line in iter(process.stdout.readline, b'') sys.stdout.write(line.decode(sys.stdout.encoding))
bergercookie
4
но выход не живой, не так ли? по моему опыту, он просто ожидает завершения процесса и только затем печатает на консоль. Ссылка -> stackoverflow.com/questions/30026045/…
denis631
91

Резюме (или версия "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внутри, но являются «невозможными» дескрипторами, -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это довольно просто. Нужно просто раскрутить подпроцесс и запустить его. Или, если вы перенаправляете на не PIPE- 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, чтобы накапливать выходные данные и доставлять ввод стандартного ввода. Все это выполняется в (единственном) родительском процессе / потоке.

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

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

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

Теперь мы застряли: процесс Python спит, ожидая, чтобы закончить, говоря «go», и подпроцесс также спит, ожидая, чтобы закончить, говоря «Hello! Don't Panic!».

subprocess.PopenКод устраняет эту проблему с резьбонарезным или -выбором / опросом. Когда байты могут пройти по каналам, они идут. Когда они не могут, только поток (не весь процесс) должен спать - или, в случае выбора / опроса, процесс Python ожидает одновременно "может записать" или "данные доступны", записывает в stdin процесса только когда есть место, и читает свои stdout и / или stderr только тогда, когда данные готовы. proc.communicate()Код ( на самом деле , _communicateгде волосатые случаи обрабатываются) возвращает сразу все данные стандартного устройства ввода (если таковые имеются) были посланы и все данные 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 file-descriptor-1, подпроцесс будет следовать за этим перенаправлением. При open(os.devnull, 'w')вызове создается поток, значение которого fileno()больше 2.)

Торек
источник
Хм. Ваша демоверсия, кажется, показывает противоположность претензии в конце. Вы перенаправляете стандартный вывод Python в буфер, но стандартный вывод подпроцесса все еще идет на консоль. Чем это полезно? Я что-то упускаю?
Гай Сиртон
@GuySirton: демонстрация показывает, что стандартный вывод подпроцесса (если он явно не указан sys.stdout) идет в стандартный вывод Python , а не в стандартный вывод программы ( sys.) Python . Что я признаю, это ... странное различие. Есть ли лучший способ выразить это?
Торек
это хорошо знать, но мы действительно хотим захватить вывод подпроцесса, поэтому изменение sys.stdout - это круто, но, думаю, это не поможет. Хорошее наблюдение, чтобы общаться, должно использовать что-то вроде select (), poll или потоков.
Гай Сиртон
Я добавил реализацию с помощью select ()
sivann
20

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

import subprocess
import sys
process = subprocess.Popen(your_command, stdout=subprocess.PIPE)
for line in process.stdout:
    sys.stdout.write(line)
Jughead
источник
Самый элегантный ответ здесь!
Нир
9
Это решение не отображается в режиме реального времени. Он ожидает завершения процесса и отображает все результаты сразу. В решении Виктора Керкеза, если «your_command» отображается постепенно, вывод следует постепенно, пока «your_command» время от времени сбрасывает стандартный вывод (из-за канала).
Эрик Х.
1
@Nir, потому что он не живой.
melMass
Это решение выполняет итерацию по дескриптору по умолчанию, поэтому оно будет обновляться только при обновлении строки в выходных данных. Для обновления на основе символов вам нужно выполнить итерацию по методу read (), как показано в решении Виктора. Но это было излишним для моего варианта использования.
Jughead
11

Если вы можете использовать сторонние библиотеки, вы можете использовать что-то вроде sarge(раскрытие: я его сопровождающий). Эта библиотека позволяет неблокировать доступ к выходным потокам из подпроцессов - она ​​наложена на subprocessмодуль.

Винай Саджип
источник
Прекрасная работа на сарже, кстати. Это действительно решает требование OP, но может быть немного сложным для этого варианта использования.
углубление
Если вы предлагаете инструмент, по крайней мере, покажите пример использования для этого конкретного случая.
Сергей
4

Решение 1: Журнал stdoutИ stderrодновременно в реальном времени

Простое решение, которое записывает как stdout, так и stderr одновременно, построчно в реальном времени в файл журнала.

import subprocess as sp
from concurrent.futures import ThreadPoolExecutor


def log_popen_pipe(p, stdfile):

    with open("mylog.txt", "w") as f:

        while p.poll() is None:
            f.write(stdfile.readline())
            f.flush()

        # Write the rest from the buffer
        f.write(stdfile.read())


with sp.Popen(["ls"], stdout=sp.PIPE, stderr=sp.PIPE, text=True) as p:

    with ThreadPoolExecutor(2) as pool:
        r1 = pool.submit(log_popen_pipe, p, p.stdout)
        r2 = pool.submit(log_popen_pipe, p, p.stderr)
        r1.result()
        r2.result()

Решение 2. Функция read_popen_pipes(), позволяющая выполнять итерации по обоим каналам (stdout / stderr) одновременно в реальном времени

import subprocess as sp
from queue import Queue, Empty
from concurrent.futures import ThreadPoolExecutor


def enqueue_output(file, queue):
    for line in iter(file.readline, ''):
        queue.put(line)
    file.close()


def read_popen_pipes(p):

    with ThreadPoolExecutor(2) as pool:
        q_stdout, q_stderr = Queue(), Queue()

        pool.submit(enqueue_output, p.stdout, q_stdout)
        pool.submit(enqueue_output, p.stderr, q_stderr)

        while True:

            if p.poll() is not None and q_stdout.empty() and q_stderr.empty():
                break

            out_line = err_line = ''

            try:
                out_line = q_stdout.get_nowait()
                err_line = q_stderr.get_nowait()
            except Empty:
                pass

            yield (out_line, err_line)

# The function in use:

with sp.Popen(my_cmd, stdout=sp.PIPE, stderr=sp.PIPE, text=True) as p:

    for out_line, err_line in read_popen_pipes(p):
        print(out_line, end='')
        print(err_line, end='')

    return p.poll()
Rotareti
источник
3

Хорошим, но «тяжеловесным» решением является использование Twisted - см. Дно.

Если вы готовы жить только с чем-то вроде stdout, это должно сработать:

import subprocess
import sys
popenobj = subprocess.Popen(["ls", "-Rl"], stdout=subprocess.PIPE)
while not popenobj.poll():
   stdoutdata = popenobj.stdout.readline()
   if stdoutdata:
      sys.stdout.write(stdoutdata)
   else:
      break
print "Return code", popenobj.returncode

(Если вы используете read (), он пытается прочитать весь «файл», который бесполезен, то, что мы действительно могли бы использовать здесь, это то, что читает все данные, которые сейчас находятся в канале)

Можно также попытаться приблизиться к этому с помощью потоков, например:

import subprocess
import sys
import threading

popenobj = subprocess.Popen("ls", stdout=subprocess.PIPE, shell=True)

def stdoutprocess(o):
   while True:
      stdoutdata = o.stdout.readline()
      if stdoutdata:
         sys.stdout.write(stdoutdata)
      else:
         break

t = threading.Thread(target=stdoutprocess, args=(popenobj,))
t.start()
popenobj.wait()
t.join()
print "Return code", popenobj.returncode

Теперь мы могли бы также добавить stderr, имея два потока.

Однако обратите внимание, что документы подпроцесса не рекомендуют использовать эти файлы напрямую и рекомендуют их использовать communicate()(в основном это касается взаимоблокировок, которые, как я думаю, не является проблемой выше), а решения немного хитрые, так что на самом деле кажется, что модуль подпроцесса не совсем подходит работа (также см .: http://www.python.org/dev/peps/pep-3145/ ), и нам нужно взглянуть на что-то еще.

Более сложным решением является использование Twisted, как показано здесь: https://twistedmatrix.com/documents/11.1.0/core/howto/process.html

То, как вы делаете это с помощью Twisted, - это создание вашего процесса с использованием reactor.spawnprocess()и предоставлением, ProcessProtocolкоторый затем обрабатывает вывод асинхронно. Пример кода Python для Twisted находится здесь: https://twistedmatrix.com/documents/11.1.0/core/howto/listings/process/process.py

Гай Сиртон
источник
Спасибо! Я только что попробовал что-то вроде этого (основываясь на комментарии @PauloAlmeida, но мой вызов subprocess.Popen блокируется - то есть он возвращается к циклу while после возвращения ...
DilithiumMatrix
1
Это не то, что происходит. Он сразу входит в цикл while, затем блокирует read()вызов до тех пор, пока не завершится подпроцесс, а родительский процесс не получит EOFканал.
Alp
@Alp интересно! так что, это.
DilithiumMatrix
Да, я был слишком быстр, чтобы опубликовать это. Это на самом деле не работает должным образом и не может быть легко исправлено. вернуться к чертежному столу.
Гай Сиртон
1
@zhermes: Итак, проблема с read () в том, что он будет пытаться прочитать весь вывод до EOF, что бесполезно. readline () помогает и может быть всем, что вам нужно (очень длинные строки также могут быть проблемой). Вам также нужно следить за буферизацией в процессе, который вы запускаете ...
Гай Сиртон,
3

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

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в конце), пока есть выходной и пустой, если это действительно в конце.

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

kabirbaidhya
источник
3

Исходя из всего вышесказанного, я предлагаю слегка модифицированную версию (python3):

  • цикл read вызывал readline (мне казалось, что предложенное iter-решение навсегда заблокировало меня - Python 3, Windows 7)
  • структурирован, поэтому нет необходимости дублировать обработку прочитанных данных послеNone
  • stderr отправляется в stdout, поэтому оба выходных сигнала читаются
  • Добавлен код для получения значения выхода cmd.

Код:

import subprocess
proc = subprocess.Popen(cmd, shell=True, stdout=subprocess.PIPE,
                        stderr=subprocess.STDOUT, universal_newlines=True)
while True:
    rd = proc.stdout.readline()
    print(rd, end='')  # and whatever you want to do...
    if not rd:  # EOF
        returncode = proc.poll()
        if returncode is not None:
            break
        time.sleep(0.1)  # cmd closed stdout, but not exited yet

# You may want to check on ReturnCode here
mrtnlrsn
источник
returncodeЧасть имеет решающее значение в моем случае.
звездная пыль
2

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

proc = subprocess.Popen(run_command, stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=True)
while proc.poll() is None:
    line = proc.stdout.readline()
    print line
    log_file.write(line + '\n')
# Might still be data on stdout at this point.  Grab any
# remainder.
for line in proc.stdout.read().split('\n'):
    print line
    log_file.write(line + '\n')
# Do whatever you want with proc.stderr here...
горная вершина
источник
2

Почему бы не установить stdoutпрямо на sys.stdout? И если вам нужно также вывести в журнал, вы можете просто переопределить метод записи f.

import sys
import subprocess

class SuperFile(open.__class__):

    def write(self, data):
        sys.stdout.write(data)
        super(SuperFile, self).write(data)

f = SuperFile("log.txt","w+")       
process = subprocess.Popen(command, stdout=f, stderr=f)
xaav
источник
Это не сработает: модуль подпроцесса разветвляется и устанавливает stdoutдескриптор файла на дескриптор файла переданного объекта файла. Метод write никогда не будет вызван (по крайней мере, это то, что делает подпроцесс для stderr, я думаю, что это то же самое для stdout).
t.animal
2

Все вышеперечисленные решения, которые я попробовал, не смогли разделить вывод stderr и stdout (несколько каналов) или были заблокированы навсегда, когда буфер канала ОС был заполнен, что происходит, когда команда, которую вы запускаете, выводит слишком быстро (для Python есть предупреждение об этом) опрос () руководство подпроцесса). Единственный надежный способ, который я нашел, был через select, но это решение только для posix:

import subprocess
import sys
import os
import select
# returns command exit status, stdout text, stderr text
# rtoutput: show realtime output while running
def run_script(cmd,rtoutput=0):
    p = subprocess.Popen(cmd, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
    poller = select.poll()
    poller.register(p.stdout, select.POLLIN)
    poller.register(p.stderr, select.POLLIN)

    coutput=''
    cerror=''
    fdhup={}
    fdhup[p.stdout.fileno()]=0
    fdhup[p.stderr.fileno()]=0
    while sum(fdhup.values()) < len(fdhup):
        try:
            r = poller.poll(1)
        except select.error, err:
            if err.args[0] != EINTR:
                raise
            r=[]
        for fd, flags in r:
            if flags & (select.POLLIN | select.POLLPRI):
                c = os.read(fd, 1024)
                if rtoutput:
                    sys.stdout.write(c)
                    sys.stdout.flush()
                if fd == p.stderr.fileno():
                    cerror+=c
                else:
                    coutput+=c
            else:
                fdhup[fd]=1
    return p.poll(), coutput.strip(), cerror.strip()
sivann
источник
Другая альтернатива состоит в том, чтобы вращать одну нить на трубу. Каждый поток может блокировать ввод / вывод в канале, не блокируя другой поток (ы). Но это вводит свой собственный набор проблем. У всех методов есть неприятности, вы просто выбираете, какой из них вы считаете наименее раздражающим. :-)
Торек
2

Аналогично предыдущим ответам, но следующее решение работало для меня на окнах, использующих Python3, чтобы обеспечить общий метод для печати и входа в систему в реальном времени ( getting-realtime-output-using-python ):

def print_and_log(command, logFile):
    with open(logFile, 'wb') as f:
        command = subprocess.Popen(command, stdout=subprocess.PIPE, shell=True)

        while True:
            output = command.stdout.readline()
            if not output and command.poll() is not None:
                f.close()
                break
            if output:
                f.write(output)
                print(str(output.strip(), 'utf-8'), flush=True)
        return command.poll()
scottysbasement
источник
2

Я думаю, что subprocess.communicateметод немного вводит в заблуждение: он фактически заполняет stdout и stderr, которые вы указали в subprocess.Popen.

Тем не менее, чтение из subprocess.PIPEчто вы можете предоставить в subprocess.Popen«s STDOUT и STDERR параметры в конечном итоге заполнить буфера операционной системы труб и тупиковым приложение (особенно , если у Вас есть несколько процессов / потоков , которые должны использовать subprocess).

Мое предлагаемое решение состоит в том, чтобы предоставить stdout и stderr файлами - и читать содержимое файлов вместо чтения из взаимоблокировки PIPE. Эти файлы могут быть tempfile.NamedTemporaryFile()- к которым также можно получить доступ для чтения, пока они записываются subprocess.communicate.

Ниже приведен пример использования:

        try:
            with ProcessRunner(('python', 'task.py'), env=os.environ.copy(), seconds_to_wait=0.01) as process_runner:
                for out in process_runner:
                    print(out)
        catch ProcessError as e:
            print(e.error_message)
            raise

И это исходный код, который готов к использованию с как можно большим количеством комментариев, чтобы объяснить, что он делает:

Если вы используете Python 2, убедитесь, что сначала установили последнюю версию пакета subprocess32 из pypi.


import os
import sys
import threading
import time
import tempfile
import logging

if os.name == 'posix' and sys.version_info[0] < 3:
    # Support python 2
    import subprocess32 as subprocess
else:
    # Get latest and greatest from python 3
    import subprocess

logger = logging.getLogger(__name__)


class ProcessError(Exception):
    """Base exception for errors related to running the process"""


class ProcessTimeout(ProcessError):
    """Error that will be raised when the process execution will exceed a timeout"""


class ProcessRunner(object):
    def __init__(self, args, env=None, timeout=None, bufsize=-1, seconds_to_wait=0.25, **kwargs):
        """
        Constructor facade to subprocess.Popen that receives parameters which are more specifically required for the
        Process Runner. This is a class that should be used as a context manager - and that provides an iterator
        for reading captured output from subprocess.communicate in near realtime.

        Example usage:


        try:
            with ProcessRunner(('python', task_file_path), env=os.environ.copy(), seconds_to_wait=0.01) as process_runner:
                for out in process_runner:
                    print(out)
        catch ProcessError as e:
            print(e.error_message)
            raise

        :param args: same as subprocess.Popen
        :param env: same as subprocess.Popen
        :param timeout: same as subprocess.communicate
        :param bufsize: same as subprocess.Popen
        :param seconds_to_wait: time to wait between each readline from the temporary file
        :param kwargs: same as subprocess.Popen
        """
        self._seconds_to_wait = seconds_to_wait
        self._process_has_timed_out = False
        self._timeout = timeout
        self._process_done = False
        self._std_file_handle = tempfile.NamedTemporaryFile()
        self._process = subprocess.Popen(args, env=env, bufsize=bufsize,
                                         stdout=self._std_file_handle, stderr=self._std_file_handle, **kwargs)
        self._thread = threading.Thread(target=self._run_process)
        self._thread.daemon = True

    def __enter__(self):
        self._thread.start()
        return self

    def __exit__(self, exc_type, exc_val, exc_tb):
        self._thread.join()
        self._std_file_handle.close()

    def __iter__(self):
        # read all output from stdout file that subprocess.communicate fills
        with open(self._std_file_handle.name, 'r') as stdout:
            # while process is alive, keep reading data
            while not self._process_done:
                out = stdout.readline()
                out_without_trailing_whitespaces = out.rstrip()
                if out_without_trailing_whitespaces:
                    # yield stdout data without trailing \n
                    yield out_without_trailing_whitespaces
                else:
                    # if there is nothing to read, then please wait a tiny little bit
                    time.sleep(self._seconds_to_wait)

            # this is a hack: terraform seems to write to buffer after process has finished
            out = stdout.read()
            if out:
                yield out

        if self._process_has_timed_out:
            raise ProcessTimeout('Process has timed out')

        if self._process.returncode != 0:
            raise ProcessError('Process has failed')

    def _run_process(self):
        try:
            # Start gathering information (stdout and stderr) from the opened process
            self._process.communicate(timeout=self._timeout)
            # Graceful termination of the opened process
            self._process.terminate()
        except subprocess.TimeoutExpired:
            self._process_has_timed_out = True
            # Force termination of the opened process
            self._process.kill()

        self._process_done = True

    @property
    def return_code(self):
        return self._process.returncode


Виктор Клафольц
источник
1

Вот класс, который я использую в одном из моих проектов. Он перенаправляет вывод подпроцесса в журнал. Сначала я попытался просто переписать метод write, но это не работает, поскольку подпроцесс никогда не вызовет его (перенаправление происходит на уровне файлового дескриптора). Поэтому я использую свой собственный канал, аналогичный тому, как это делается в подпроцесс-модуле. Преимущество заключается в инкапсуляции всей логики логирования / печати в адаптере, и вы можете просто передать экземпляры регистратора в Popen:subprocess.Popen("/path/to/binary", stderr = LogAdapter("foo"))

class LogAdapter(threading.Thread):

    def __init__(self, logname, level = logging.INFO):
        super().__init__()
        self.log = logging.getLogger(logname)
        self.readpipe, self.writepipe = os.pipe()

        logFunctions = {
            logging.DEBUG: self.log.debug,
            logging.INFO: self.log.info,
            logging.WARN: self.log.warn,
            logging.ERROR: self.log.warn,
        }

        try:
            self.logFunction = logFunctions[level]
        except KeyError:
            self.logFunction = self.log.info

    def fileno(self):
        #when fileno is called this indicates the subprocess is about to fork => start thread
        self.start()
        return self.writepipe

    def finished(self):
       """If the write-filedescriptor is not closed this thread will
       prevent the whole program from exiting. You can use this method
       to clean up after the subprocess has terminated."""
       os.close(self.writepipe)

    def run(self):
        inputFile = os.fdopen(self.readpipe)

        while True:
            line = inputFile.readline()

            if len(line) == 0:
                #no new data was added
                break

            self.logFunction(line.strip())

Если вам не нужна регистрация, но вы просто хотите использовать ее, print()вы, очевидно, можете удалить большие части кода и сократить класс. Кроме того, можно расширить его __enter__и __exit__метод и вызвать finishedв __exit__таким образом , чтобы вы могли легко использовать его в качестве контекста.

t.animal
источник
1

Ни одно из решений Pythonic не сработало для меня. Оказалось, что proc.stdout.read()или подобное может заблокировать навсегда.

Поэтому я использую teeтак:

subprocess.run('./my_long_running_binary 2>&1 | tee -a my_log_file.txt && exit ${PIPESTATUS}', shell=True, check=True, executable='/bin/bash')

Это решение удобно, если вы уже используете shell=True.

${PIPESTATUS}фиксирует статус успеха всей цепочки команд (доступно только в Bash). Если я опущу && exit ${PIPESTATUS}, то это всегда будет возвращать ноль, так как teeникогда не терпит неудачу.

unbufferможет понадобиться для печати каждой строки непосредственно в терминал, вместо того, чтобы ждать слишком долго, пока «буферный канал» не будет заполнен. Тем не менее, unbuffer проглатывает состояние выхода assert (SIG Abort) ...

2>&1 также записывает stderror в файл.

Mike76
источник