Как я могу печатать и отображать вывод подпроцесса и вывод stderr без искажений?

Может быть, есть кто-то в эфире, который может помочь мне с этим. (Я видел ряд подобных вопросов в SO, но ни один из них не касается как стандартной ошибки, так и стандартной ошибки, или не имеет отношения к ситуации, совершенно похожей на мою, отсюда и новый вопрос.)

У меня есть функция Python, которая открывает подпроцесс, ждет его завершения, затем выводит код возврата, а также содержимое стандартного выхода и стандартных каналов ошибок. Пока процесс запущен, я хотел бы также отображать вывод обоих каналов по мере их заполнения. Моя первая попытка привела к чему-то вроде этого:

process = subprocess.Popen(args, stdout=subprocess.PIPE, stderr=subprocess.PIPE)

stdout = str()
stderr = str()
returnCode = None
while True:
    # collect return code and pipe info
    stdoutPiece = process.stdout.read()
    stdout = stdout + stdoutPiece
    stderrPiece = process.stderr.read()
    stderr = stderr + stderrPiece
    returnCode = process.poll()

    # check for the end of pipes and return code
    if stdoutPiece == '' and stderrPiece == '' and returnCode != None:
        return returnCode, stdout, stderr

    if stdoutPiece != '': print(stdoutPiece)
    if stderrPiece != '': print(stderrPiece)

Хотя есть пара проблем с этим. Так как read() читает до EOF, первая строка while цикл не вернется, пока подпроцесс не закроет канал.

Я мог бы заменить read() в пользу read(int) но напечатанный вывод искажается, обрезается в конце прочитанных символов. Я мог бы readline() в качестве замены, но напечатанный вывод искажается чередующимися строками вывода и ошибками, когда одновременно происходит много и того, и другого.

Возможно, есть read-until-end-of-buffer() вариант, о котором я не знаю? Или, может быть, это можно реализовать?

Может быть, лучше всего реализовать sys.stdout обертка, как предлагается в этом ответе на другой пост? Однако я хотел бы использовать только оболочку в этой функции.

Есть другие идеи от сообщества?

Я ценю помощь!:)

РЕДАКТИРОВАТЬ: Решение действительно должно быть кроссплатформенным, но если у вас есть идеи, которых нет, пожалуйста, поделитесь ими, чтобы продолжить мозговой штурм.


Для еще одного из моих царапин на головке подпроцесса Python, взгляните на еще один из моих вопросов об учете затрат времени на подпроцесс.

3 ответа

Решение

Сделайте трубы неблокирующими, используя fcntl.fcntl и использовать select.select подождать, пока данные станут доступны в любом канале. Например:

# Helper function to add the O_NONBLOCK flag to a file descriptor
def make_async(fd):
    fcntl.fcntl(fd, fcntl.F_SETFL, fcntl.fcntl(fd, fcntl.F_GETFL) | os.O_NONBLOCK)

# Helper function to read some data from a file descriptor, ignoring EAGAIN errors
def read_async(fd):
    try:
        return fd.read()
    except IOError, e:
        if e.errno != errno.EAGAIN:
            raise e
        else:
            return ''

process = subprocess.Popen(args, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
make_async(process.stdout)
make_async(process.stderr)

stdout = str()
stderr = str()
returnCode = None

while True:
    # Wait for data to become available 
    select.select([process.stdout, process.stderr], [], [])

    # Try reading some data from each
    stdoutPiece = read_async(process.stdout)
    stderrPiece = read_async(process.stderr)

    if stdoutPiece:
        print stdoutPiece,
    if stderrPiece:
        print stderrPiece,

    stdout += stdoutPiece
    stderr += stderrPiece
    returnCode = process.poll()

    if returnCode != None:
        return (returnCode, stdout, stderr)

Обратите внимание, что fcntl доступно только на Unix-подобных платформах, включая Cygwin.

Если вам нужно, чтобы он работал на Windows без Cygwin, это выполнимо, но гораздо сложнее. Вам придется:

  • Используйте библиотеку pywin32 для вызова нативного Win32 API
  • использование SetNamedPipeHandleState с PIPE_NOWAIT сделать трубы stdout и stderr неблокирующими
  • использование WaitForMultipleObjects вместо select ждать, пока данные станут доступными
  • использование ReadFile читать данные

Объединяя этот ответ с этим, у меня работает следующий код:

import subprocess, sys
p = subprocess.Popen(args, stderr=sys.stdout.fileno(), stdout=subprocess.PIPE)
for line in iter(p.stdout.readline, ""):
 print line,

Когда я проверял это, казалось, что readline() блокирует. Однако я смог получить доступ к stdout и stderr отдельно, используя потоки. Пример кода выглядит следующим образом:

import os
import sys
import subprocess
import threading

class printstd(threading.Thread):
    def __init__(self, std, printstring):
        threading.Thread.__init__(self)
        self.std = std
        self.printstring = printstring
    def run(self):
        while True:
          line = self.std.readline()
          if line != '':
            print self.printstring, line.rstrip()
          else:
            break

pythonfile = os.path.join(os.getcwd(), 'mypythonfile.py')

process = subprocess.Popen([sys.executable,'-u',pythonfile], shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)

print 'Process ID:', process.pid

thread1 = printstd(process.stdout, 'stdout:')
thread2 = printstd(process.stderr, 'stderr:')

thread1.start()
thread2.start()

threads = []

threads.append(thread1)
threads.append(thread2)

for t in threads:
    t.join()

Однако я не уверен, что это потокобезопасно.

Другие вопросы по тегам