Как я могу печатать и выводить вывод субпроцесса stdout и 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, как предложено в этом ответе на другой пост? Однако я хотел бы использовать оболочку в этой функции.

Любые другие идеи сообщества?

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

EDIT: решение действительно должно быть кросс-платформенным, но если у вас есть идеи, которые этого не делают, пожалуйста, поделитесь ими, чтобы продолжить мозговой штурм.


Для другого одного из моих головоломок с подпроцессом python взгляните на другой из моих вопросов на в котором учтены накладные расходы подпроцесса.

Ответы

Ответ 1

Сделайте блокировку труб с помощью 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 для вызова собственного API Win32
  • Используйте SetNamedPipeHandleState с PIPE_NOWAIT, чтобы блокировки stdout и stderr не блокировались
  • Используйте WaitForMultipleObjects вместо select, чтобы дождаться появления данных.
  • Используйте ReadFile, чтобы прочитать данные

Ответ 2

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

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

Ответ 3

Когда я протестировал его, казалось, что 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()

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