【问题标题】:Display process output incrementally using Python subprocess使用 Python 子进程增量显示进程输出
【发布时间】:2020-03-28 21:42:25
【问题描述】:

我正在尝试从 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 脚本运行命令时,是否有一种安全的方法可以在命令行中逐步显示命令的确切输出?

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

【问题讨论】:

    标签: python subprocess output


    【解决方案1】:
    1. 始终将 STDIN 作为管道打开,如果您不使用它,请立即关闭它。

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

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

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

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

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

    【讨论】:

    • (2) 是我的第二个代码块中问题的根源,直到管道关闭,我才知道 read() 被阻塞。谢谢!顺便说一句,我的第一个代码块也不起作用,因为(1)我按顺序运行 p.stdout.readline() 和 p.stderr.readline() 并且代码卡在其中一个中,因为进程只写入其中一个当没有错误并且(2)我使用readline()而不是read()并且“docker-compose pull”只为初始显示写入换行符时,后续行更新没有换行符并且没有显示。此外,docker-compose 奇怪地打印到 stderr 而不是 stdout
    • 进程写入stderr并不奇怪。 stderr 不只专注于错误。想象一下,您有一个过程,您希望将其结果传递到某个地方,例如一个文件,但您还希望能够看到进度。您将进度写入标准错误并将标准输出重定向到管道。一个例子是通过管道传输音频/视频流时的 ffmpeg。
    • 很抱歉没有注意到您试图同时阅读 err 和 out ,否则我会指出这一点。您可以通过启动线程来解决此问题。一个读取stderr,另一个读取stdout,结果被放入队列中。您使用锁来正确同步线程。主线程监视队列并决定如何处理结果。例如对于主线程 --> while p.poll()==None: res = some_queue.pop(0);如果 res[0]=="stderr": 打印 res[1]; p.stdout.close(); p.stderr.close(); break ... 监视器线程必须管理由强制管道关闭引起的 IOErrors。你也可以使用 p.terminate()
    【解决方案2】:

    根据我的问题的第一个代码块,我找到了解决方案:

    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 解释说,一些应用程序会这样做,因为他们希望他们的结果可以通过管道传输到某个地方,例如文件,但也希望能够显示他们的进度。

    【讨论】:

    • 请注意 *.decode(...) 在这里什么都不做。您一次得到一个字符,但 ANSII/ASCII 范围之外的 UTF-8 字符由 2 个字节组成,因此, *.decode() 没有什么可解码的,只有一个字节可用。正如我在另一条评论中解释的那样,将状态打印到 stderr 并没有什么奇怪的。
    • 我尝试删除 decode() 调用,我得到了很多字节而不是字符(使用 Python 2)。我不知道通常使用 stderr 来打印状态。对我来说这似乎有点骇人听闻,但我理解它的用处。我会更新我的答案。
    • 这很不寻常,因为通常不需要这个,正如你正确地说的那样,hackish 解决方案。从历史上看,stderr 意味着错误,但只是练习需要更多。还有其他可能的解决方案,但是当您有需要很长时间的任务并且支持管道时,则期望 stderr 和 stdout 上的所有状态仅用于结果。在一些非常疯狂的可能性中,这是最简单的解决方案。而且,如果你发现自己需要这种输出,这是最简单的方法。其他黑客是丑陋和肮脏的。 :D
    猜你喜欢
    • 2018-10-02
    • 1970-01-01
    • 2014-11-22
    • 1970-01-01
    • 2011-07-30
    • 1970-01-01
    • 2014-02-10
    • 2018-05-28
    • 2019-05-19
    相关资源
    最近更新 更多