Отображать вывод процесса постепенно, используя подпроцесс Python - PullRequest
1 голос
/ 29 марта 2020

Я пытаюсь запустить "docker -compose pull" из скрипта автоматизации Python и постепенно отображать тот же вывод, который выводит команда Docker, если она запускается непосредственно из оболочки. Эта команда печатает строку для каждого изображения Docker, найденного в системе, постепенно обновляет каждую строку с прогрессом загрузки изображения Docker (в процентах) и заменяет этот процент на "выполнено", когда загрузка завершена. Сначала я попытался получить вывод команды с помощью вызовов subprocess.poll () и (blocking) readline ():

import shlex
import subprocess

def run(command, shell=False):

    p = subprocess.Popen(shlex.split(command), stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=shell)
    while True:
        # print one output line  
        output_line = p.stdout.readline().decode('utf8')
        error_output_line = p.stderr.readline().decode('utf8')
        if output_line:
            print(output_line.strip())
        if error_output_line:
            print(error_output_line.strip())

        # check if process finished
        return_code = p.poll()
        if return_code is not None and output_line == '' and error_output_line == '':
            break

    if return_code > 0:
        print("%s failed, error code %d" % (command, return_code))


run("docker-compose pull")

Код застревает в первом (блокирующем) вызове readline (). Затем я попытался сделать то же самое, не блокируя:

import select
import shlex
import subprocess
import sys
import time

def run(command, shell=False):

    p = subprocess.Popen(shlex.split(command), stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=shell)
    io_poller = select.poll()
    io_poller.register(p.stdout.fileno(), select.POLLIN)
    io_poller.register(p.stderr.fileno(), select.POLLIN)
    while True:
        # poll IO for output
        io_events_list = []
        while not io_events_list:
            time.sleep(1)
            io_events_list = io_poller.poll(0)

        # print new output
        for event in io_events_list:
            # must be tested because non-registered events (eg POLLHUP) can also be returned
            if event[1] & select.POLLIN: 
                if event[0] == p.stdout.fileno():
                   output_str = p.stdout.read(1).decode('utf8')
                   print(output_str, end="") 
                if event[0] == p.stderr.fileno():
                   error_output_str = p.stderr.read(1).decode('utf8')
                   print(error_output_str, end="")

        # check if process finished
        # when subprocess finishes, iopoller.poll(0) returns a list with 2 select.POLLHUP events
        # (one for stdout, one for stderr) and does not enter in the inner loop
        return_code = p.poll()
        if return_code is not None:
            break

    if return_code > 0:
        print("%s failed, error code %d" % (command, return_code))


run("docker-compose pull")

Это работает, но только последние строки (с "done" в конце) выводятся на экран, когда все загруженные изображения Docker имеют было завершено.

Оба метода отлично работают с командой с более простым выводом, таким как "ls". Может быть, проблема связана с тем, как эта команда Docker постепенно печатает на экране, перезаписывая уже написанные строки? Существует ли безопасный способ постепенно отображать точный вывод команды в командной строке при ее запуске с помощью сценария Python?

РЕДАКТИРОВАТЬ: 2-й блок кода был исправлен

Ответы [ 2 ]

0 голосов
/ 29 марта 2020

Я нашел решение, основанное на первом блоке кода моего вопроса:

def run(command,shell=False):

    p = subprocess.Popen(shlex.split(command), stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=shell)
    while True:
        # read one char at a time  
        output_line = p.stderr.read(1).decode("utf8")
        if output_line != "":
            print(output_line,end="")
        else:
            # check if process finished
            return_code = p.poll()
            if return_code is not None:
                if return_code > 0:
                    raise Exception("Command %s failed" % command)
                break

    return return_code

Обратите внимание, что docker -compose использует stderr для печати своего прогресса вместо stdout. @Dalen объяснил, что некоторые приложения делают это, когда хотят, чтобы их результаты были где-то переданы, например, в файл, но также хотят иметь возможность показать свой прогресс.

0 голосов
/ 29 марта 2020
  1. Всегда открывайте STDIN как канал, и если вы его не используете, немедленно закрывайте его.

  2. p.stdout.read () будет блокироваться до тех пор, пока Канал закрыт, поэтому ваш код опроса здесь не помогает. Требуются модификации.

  3. Я предлагаю не использовать shell = True

  4. Вместо * .readline (), попробуйте с * .read ( 1) и ждите "\ n"

Конечно, вы можете делать то, что вы хотите в Python, вопрос в том, как. Потому что у дочернего процесса могут быть разные представления о том, как должен выглядеть его вывод, именно тогда начинаются проблемы. Например, процесс может явно хотеть терминал на другом конце, а не ваш процесс. Или много такой простой ерунды. Кроме того, буферизация может также вызвать проблемы. Вы можете попробовать запустить Python в небуферизованном режиме, чтобы проверить. (/ usr / bin / python -U)

Если ничего не работает, используйте библиотеку автоматизации pexpect вместо подпроцесса.

...