首页 > 解决方案 > 使用 Python 子进程增量显示进程输出

问题描述

我正在尝试从 Python 自动化脚本中运行“docker-compose pull”,并逐渐显示 Docker 命令在直接从 shell 运行时将打印的相同输出。此命令为系统中找到的每个 Docker 映像打印一行,使用 Docker 映像的下载进度(百分比)增量更新每一行,并在下载完成时将此百分比替换为“完成”。我首先尝试使用 subprocess.poll() 和(阻塞)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")

这可行,但是当所有 Docker 映像下载完成后,只有最后几行(末尾带有“完成”)会打印到屏幕上。

这两种方法都适用于具有更简单输出的命令,例如“ls”。也许问题与这个 Docker 命令如何逐步打印到屏幕上,覆盖已经写好的行有关?通过 Python 脚本运行命令时,是否有一种安全的方法可以在命令行中逐步显示命令的确切输出?

编辑:第二个代码块已更正

标签: pythonsubprocessoutput

解决方案


  1. 始终将 STDIN 作为管道打开,如果您不使用它,请立即关闭它。

  2. p.stdout.read() 将阻塞直到管道关闭,因此您的轮询代码在这里没有任何用处。它需要修改。

  3. 我建议不要使用 shell=True

  4. 代替 *.readline(),尝试使用 *.read(1) 并等待“\n”

当然你可以在 Python 中做你想做的事,问题是如何做。因为,子进程可能对其输出的外观有不同的想法,这就是麻烦开始的时候。例如,进程可能希望在另一端明确地有一个终端,而不是您的进程。或者很多这样简单的废话。此外,缓冲也可能导致问题。您可以尝试以无缓冲模式启动 Python 进行检查。(/usr/bin/python -U)

如果没有任何效果,则使用 pexpect 自动化库而不是子进程。


推荐阅读