прямий вихід з команди підпроцесу


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').


1
Можливо, ви можете використовувати, Popen.pollяк у попередньому запитанні про переповнення стека .
Пауло Альмейда

Деякі команди, які показують індикацію прогресу (наприклад, git) роблять це лише у тому випадку, якщо їх вихід "tty device" (тестується через libc isatty()). У такому випадку вам, можливо, доведеться відкрити псевдо-tty.
torek

@torek що таке (псевдо-) tty?
DilithiumMatrix

2
Пристрої в Unix-подібних системах, які дозволяють процесу прикидатися користувачем на послідовному порту. Так працює, наприклад, ssh (серверна сторона). Дивіться бібліотеку python pty , а також pexpect .
torek

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'' != ''.
Джон Меллор

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це відбувається.

(Caveat: це для 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але на тій же трубі, що і 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та має батьківський потік доставити self.stdinвхідні дані (а потім закрити трубку).

У POSIX це використовує, pollякщо є можливість, в іншому випадку selectдля накопичення виводу та доставки вводу stdin. Все це працює в (єдиному) батьківському процесі / потоці.

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

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

Тим часом, припустимо, підпроцес вирішує надрукувати дружнє "Привіт! Не панікуй!" привітання. Він Hпотрапляє в свою трубку stdout, але eзмушує її призупинятись, чекаючи, коли батько прочитає це H, випорожнивши трубу stdout.

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

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

Якщо ви хочете прочитати як stdoutі stderrна двох різних трубах (незалежно від будь-якого stdinперенаправлення), вам також потрібно буде уникати тупикової ситуації. Сценарій тупикової ситуації тут інший - це відбувається, коли підпроцес записує щось довге, 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, оскільки StringIOоб'єкта немає fileno. Другий опустить значення, helloякщо ви додали, stdout=sys.stdoutоскільки sys.stdoutперенаправлено на os.devnull.

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


Хм. Ваша демонстрація, схоже, показує протилежне до твердження, врешті-решт. Ви перенаправляєте stdout Python у буфер, але підпроцесний stdout все ще йде до консолі. Чим це корисно? Я щось пропускаю?
Гай Сіртон

@GuySirton: демо показує , що подпроцесс стандартний висновок (якщо явно не спрямовані на sys.stdout) йде на Пайтона стандартний висновок, а НЕ пітон програми «s ( sys.) стандартний висновок. Я визнаю, це ... дивна відмінність. Чи є кращий спосіб сформулювати це?
torek

це добре знати, але ми дійсно хочемо зафіксувати тут результат підпроцесу, так що зміна sys.stdout - це круто, але не допомагає нам, я думаю. Хороше спостереження, що спілкуватися, має використовувати щось на зразок select (), опитування чи теми.
Гай Сіртон


Я додав реалізацію з 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)

Тут найелегантніша відповідь!
Нір

9
Це рішення не відображається в режимі реального часу. Він чекає, поки процес завершиться, і відобразить весь результат відразу. У рішенні Віктора Керкеса, якщо "your_command" відображається прогресивно, результат виходить прогресивно, доки "your_command" час від часу промиває stdout (через трубу).
Ерік Х.

1
@Nir, тому що це не живе.
melMass

Це рішення ітератує дескриптор за замовчуванням, тому оновиться лише тоді, коли рядок оновиться у висновку. Для оновлення на основі символів вам потрібно перебрати метод read (), як показано у рішенні Віктора. Але це було зайвим для мого використання.
Jughead

11

Якщо ви можете користуватися сторонніми бібліотеками, можливо, ви зможете використовувати щось на кшталт sarge(розкриття: я є його підтримувачем). Ця бібліотека дозволяє не блокувати доступ до вихідних потоків з підпроцесів - вона шарується над subprocessмодулем.


Тонка робота на саржі, BTW. Це дійсно вирішує вимогу ОП, але може бути трохи важким для цього.
поглиблення

Якщо ви пропонуєте інструмент, принаймні покажіть приклад використання для цього конкретного випадку.
Сергій

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()

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 тут: https://twistedmatrix.com/documents/11.1.0/core/howto/listings/process/process.py


Дякую! Я просто спробував щось подібне (грунтуючись на коментарі @PauloAlmeida, але мій дзвінок на підпроцес. Popen блокується - тобто він приходить до циклу час, коли він повертається ...
DilithiumMatrix

1
Це не те, що відбувається. Це введення циклу while відразу, а потім блокування під час read()виклику, поки підпроцес не завершиться і батьківський процес не отримає EOFв трубку.
Альп

@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в кінці) до тих пір, поки є вихід і порожній, якщо він дійсно в кінці.

Сподіваюся, що це комусь допоможе.


3

На основі всього вищесказаного я пропоную трохи модифіковану версію (python3):

  • в той час як цикл виклику читальної лінії (запропоноване рішення ітера для мене, здавалося, назавжди блокується - 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

У returncodeмоєму випадку ця роль була визначальною.
зірка

2

Схоже, для вас буде працювати лінійний вихід, у цьому випадку щось подібне може підійти. (Caveat: це не перевірено.) Це дасть змогу підпроцесу лише в режимі реального часу. Якщо ви хочете мати в режимі реального часу і 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)

Це не буде працювати: модуль підпроцесу розкручує та встановлює stdoutдескриптор файлу дескриптору файлу об'єкта переданого файлу. Метод запису ніколи не буде називатися (принаймні, це те, що підпроцес робить для stderr, я гадаю, що це однаково і для stdout).
t.animal

2

Усі перераховані вище рішення я не намагався або розділити вихід stderr і stdout, (декілька труб), або назавжди заблокований, коли буфер труб в ОС був заповнений, що відбувається, коли команда, яку ви виконуєте, виводить занадто швидко (для цього є попередження на python опитування () посібник з підпроцесу). Єдиний надійний спосіб, який я знайшов, - це через select, але це лише позікс-рішення:

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()

Інша альтернатива - відкручувати по одній різьбі на трубу. Кожна різьба може блокувати введення / вивід на трубі, не блокуючи інші нитки. Але це вводить власну сукупність питань. У всіх методах є роздратування, ви просто вибираєте, який із них ви найменше дратуєте. :-)
torek

2

Подібно до попередніх відповідей, але наступне рішення працювало для мене у Windows, використовуючи Python3, щоб забезпечити загальний метод друку та входу в режимі реального часу (get -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()

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

Ось клас, який я використовую в одному зі своїх проектів. Він перенаправляє вихід підпроцесу до журналу. Спочатку я спробував просто перезаписати метод запису, але це не працює, оскільки підпроцес ніколи його не викличе (перенаправлення відбувається на рівні fileescriptor). Тому я використовую власну трубку, подібно до того, як це робиться в підпроцес-модулі. Це має перевагу в інкапсуляції всієї логіки реєстрації / друку в адаптері, і ви можете просто передати екземпляри реєстратора 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__такий спосіб , щоб ви могли легко використовувати його в якості контексту.


1

Жодне з пітонічних рішень не працювало на мене. Виявилося, що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 ковтає статус виходу asrt (SIG Abort) ...

2>&1 також записує stderror у файл.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.