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

я использую скрипт 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 failednn%snn" % (errors)
    success = False

if( errors ): log_file.write("nn%snn" % errors)

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


Edit:

временное решение:

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 (С. Т. log_file = 'log.txt').

12 ответов


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

import subprocess
import sys
with open('test.log', 'w') as f:
    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:
    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 до 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 функции будут блокировать, пока один символ не будет записан в канал или строка не будет записана в канал соответственно.


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

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

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

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

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

  • None, указывая, что вы не хотите перенаправлять поток. Он унаследует их, как обычно. Обратите внимание, что в системах POSIX, по крайней мере, это не означает, что он будет использовать Python sys.stdout, просто в Python фактический stdout; см. демонстрацию в конце.
  • An int значение. Это" сырой " файловый дескриптор (по крайней мере, в POSIX). (Примечание:PIPE и STDOUT на самом деле ints внутренне, но являются "невозможными" дескрипторами, -1 и -2.)
  • поток-действительно, любой объект с fileno метод. Popen найдет дескриптор для этого потока, используя stream.fileno(), а затем продолжайте как для int значение.
  • subprocess.PIPE, указывая, что Python должен создать канал.
  • subprocess.STDOUT (for stderr только): скажите Python использовать тот же дескриптор, что и для stdout. Это имеет смысл, только если вы обеспечил (неNone) для stdout, и даже тогда, это только нужны если вы устанавливаете stdout=subprocess.PIPE. (В противном случае вы можете просто предоставить тот же аргумент, что и для stdout, например, Popen(..., stdout=stream, stderr=stream).)

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

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

все еще легкий случай: одна труба

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

Предположим, вы хотите поставить некоторые stdin, но давайте stdout и stderr go un-redirected или перейдите к файловому дескриптору. В качестве родительского процесса ваша программа 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 и . Опять же, это просто: просто позвоните proc.stdout.read() (или эквивалент), пока не будет больше выхода. С proc.stdout() является обычным потоком ввода-вывода Python, вы можете использовать все обычные конструкции на нем, например:

for line in proc.stdout:

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

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

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

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, и имеет Родительский поток deliver self.stdin входные данные (а затем закройте канал).

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

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

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

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

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

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

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


Демо

я обещал продемонстрировать, что, не перенаправленный, Python subprocesses напишите в базовый stdout, а не 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, как


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


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

import subprocess
import sys
process = subprocess.Popen(your_command, stdout=subprocess.PIPE)
for line in process.stdout:
    sys.stdout.write(line)

хорошим, но "тяжеловесным" решением является использование 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 находится здесь: https://twistedmatrix.com/documents/11.1.0/core/howto/listings/process/process.py


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

почему бы не установить 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)

вот класс, который я использую в одном из моих проектов. Он перенаправляет вывод подпроцесса в журнал. Сначала я попытался просто перезаписать метод записи, но это не работает, так как подпроцесс никогда не вызовет его (перенаправление происходит на уровне filedescriptor). Поэтому я использую свою собственную трубу, аналогично тому, как это делается в подпроцессе-модуле. Это имеет преимущество инкапсуляции всей логики ведения журнала / печати в адаптере, и вы можете просто передать экземпляры регистратора в 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__ чтобы вы могли легко использовать его в качестве контекста.


все вышеперечисленные решения, которые я пробовал, не смогли отделить вывод stderr и stdout (несколько каналов) или заблокированы навсегда, когда буфер канала ОС был заполнен, что происходит, когда команда, которую вы запускаете, выводит слишком быстро (есть предупреждение для этого на python poll() руководство по подпроцессу). Единственный надежный способ, который я нашел, был через 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()

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

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

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

    if not line:
        break

    print(line.strip())

цикл через читаемый поток, пока он читается, и если он получает пустой результат, остановите его.

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

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


Я выйду на конечности здесь и предположить, используя check_call. Согласно документации, он запускает операцию блокировки и подходит для этой цели довольно хорошо.

выполнить команду с аргументами. Дождитесь завершения команды. Если код возврата был равен нулю, затем return, иначе вызовите CalledProcessError. Объект CalledProcessError будет иметь код возврата в атрибут код возврата.

cmd = "some_crazy_long_script.sh"
args = {
    'shell': True,
    'cwd': if_you_need_it
}

subprocess.check_call(cmd, **args)

import sys
import subprocess

f = open("log.txt","w+")
process = subprocess.Popen(command, stdout=subprocess.PIPE)
for line in iter(process.stdout.readline, ''):
    sys.stdout.write(line)
    f.write(line.replace("\n",""))
f.close()