【问题标题】:Python: catch exception from subprocess while iterating through stdoutPython:在迭代标准输出时从子进程中捕获异常
【发布时间】:2021-01-29 04:14:05
【问题描述】:

我正在尝试从子进程中获取异常。如果我使用.communicate,我可以得到它,但我想避免使用它,因为我正在流式传输来自子进程的输出,并且不想等到整个子进程完成。还假设整个子流程可能需要很长时间。想知道如何在从子进程流式传输标准输出时捕获引发的异常。

考虑下面的例子,所以我想让版本 #1 工作,版本 #2 有点工作,但不希望那样。

在 main.py 中

import subprocess


class ExtProcess():
    def __init__(self, *args):
        self.proc = subprocess.Popen(['python', *args], stdout=subprocess.PIPE)

    def __iter__(self):
        return self

    def __next__(self):
        while True:
            line = self.proc.stdout.readline()
            if self.proc.returncode:
                raise Exception("error here")
            if not line:
                raise StopIteration
            return line


def run():
    ## version #1
    reader = ExtProcess("sample_extproc.py")
    for d in reader:
        print(f"got: {d}")

    ## version #2
    # proc = subprocess.Popen(['python', "sample_extproc.py"], stdout=subprocess.PIPE, stderr=subprocess.PIPE)
    # output, error = proc.communicate()
    # print("got:", output)
    # if proc.returncode:
    #     raise Exception(error)

def main():
    try:
        print("start...")
        run()
        print("complete...")
    except Exception as e:
        print(f"Package midstream error here: {str(e)}")
    finally:
        print("clean up...")


if __name__ == "__main__":
    main()

在 sample_extproc.py 中

for x in range(10):
    print(x)
    if x == 3:
        raise RuntimeError("catch me")

我想从版本 #1 中获得类似以下内容的输出:

start...
got: b'0\r\n'
got: b'1\r\n'
got: b'2\r\n'
got: b'3\r\n'
Package midstream error here: b'Traceback (most recent call last):\r\n  File "sample_extproc.py", line 4, in <module>\r\n    raise RuntimeError("catch me")\r\nRuntimeError: catch me\r\n'
clean up...

基本上,它从子进程中遍历标准输出,然后在发生异常时打印异常,然后继续执行清理。

【问题讨论】:

  • 程序在退出之前不会返回退出状态。你的子进程在抛出异常时会退出吗?
  • ...因此,在您仍在读取数据时尝试检查 returncode 没有什么意义。当然,进程可以在退出之前关闭其标准输出,或者在退出时仍有几十个字节在 FIFO 中,但超过几毫秒的情况是非常不寻常的,即使这样,父进程也不会知道退出状态,直到它调用wait() 系统调用来检索它(当从僵尸条目中获取退出进程的PID时,它在退出之间的时间留在进程表中调用及其父进程读取该状态)。
  • 我确实想问“您的更大目标是什么?”可能适合这里。我喜欢“回答所提出的问题”,但我不禁觉得还有另一种方法可以解决这个问题(取决于它是什么)。
  • ...如果您的子进程在看到异常后立即退出,则该异常不会“在中间”,而是在流的末尾,所以您可以去直接从您的for line in proc.self.stdout: 循环退出到p.wait(),并在wait 返回后立即检查p.returncode
  • @CharlesDuffy 抱歉,刚刚明白你提到的内容。所以是的,如果在我循环通过标准输出之后,它实际上可以工作,有 p.wait,然后检查 p.returncode

标签: python subprocess stderr


【解决方案1】:

以下是我的问题的答案,完全基于@CharlesDuffy 的评论:

简而言之,确保在ExtProcess 类中有stderr=subprocess.PIPE,那么答案在版本#3,在迭代标准输出后,我们利用.wait()returncode 来检查是否有错误,如果是这样引发异常,则从stderr.read() 中获取错误以在父/主中捕获。

import subprocess

class ExtProcess():
    def __init__(self, *args):
        self.proc = subprocess.Popen(['python', *args], stdout=subprocess.PIPE, stderr=subprocess.PIPE)

    def __iter__(self):
        return self

    def __next__(self):
        while True:
            line = self.proc.stdout.readline()
            if not line:
                raise StopIteration
            return line


def run():
    ## version #1
    # reader = ExtProcess("sample_extproc.py")
    # for d in reader:
    #     print(f"got: {d}")

    ## version #2
    # proc = subprocess.Popen(['python', "sample_extproc.py"], stdout=subprocess.PIPE, stderr=subprocess.PIPE)
    # output, error = proc.communicate()
    # print("got:", output)
    # if proc.returncode:
    #     raise Exception(error)

    ## version #3
    reader = ExtProcess("sample_extproc.py")
    for d in reader:
        print(f"got: {d}")
    reader.proc.wait()
    if reader.proc.returncode:
       raise Exception(reader.proc.stderr.read())

def main():
    try:
        print("start...")
        run()
        print("complete...")
    except Exception as e:
        print(f"Package midstream error here: {str(e)}")
    finally:
        print("clean up...")


if __name__ == "__main__":
    main()

【讨论】:

  • 因此,阅读此代码时唯一让我担心的是,如果您的程序在关闭 stdout 之前尝试写入的内容多于流向 stderr 的内容,则该写入可能会阻塞,因为没有任何内容在读取它( stderr 不仅用于错误——它也是“诊断内容”(如日志)所属的地方);因此,如果程序在对 stderr 的写入完成之前没有完成对 stdout 的写入(或没有尝试关闭 stdout),您可能会陷入死锁。
  • @CharlesDuffy 嗯,我不确定我是否完全理解,但是我可以使用的东西有超时吗?我假设每当我到达 p.wait() 时,stdout 已经达到了 StopIteration,所以我真的不应该在 p.wait() 等待太久
  • 潜在的问题是子进程死锁。尝试使用一个子进程,该子进程在执行过程中向 stderr 写入几 kb 数据——除非父进程中的某些东西当时主动从 stderr 中读取,否则它写入该数据的尝试将挂起;当您在 stdout 退出之前根本不从 stderr 读取数据时,这意味着您不能安全地运行在 stdout 仍然打开的情况下将超过 FIFO 缓冲区的数据写入 stderr 的子进程。
  • 另一种方式是使用selectorsstackoverflow.com/questions/31833897/… 中的一些答案进入其中。只需从 stderr 读取到您的父进程并存储数据以供以后参考就足够了。
  • ...使用selectors同时从stdout和stderr读取,或者在后台从proc.stderr读取的线程,等等是为了确保stderr 在子进程写入时被消耗,因此子进程不能挂起尝试等待附加到其 stderr 的完整 FIFO 缓冲区有空间来写入更多数据(而父进程不努力读取和因此完全清空该 FIFO)。
猜你喜欢
  • 2010-12-09
  • 2011-11-28
  • 2016-02-15
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2016-07-26
  • 2020-10-18
  • 1970-01-01
相关资源
最近更新 更多