在popen.stdout.readlin上检测流的结尾

2024-10-01 00:25:07 发布

您现在位置:Python中文网/ 问答频道 /正文

我有一个python程序,它使用Popen启动子进程,并在生成时几乎实时地消耗它们的输出。相关回路的代码为:

def run(self, output_consumer):
    self.prepare_to_run()
    popen_args = self.get_popen_args()
    logging.debug("Calling popen with arguments %s" % popen_args)
    self.popen = subprocess.Popen(**popen_args)
    while True:
        outdata = self.popen.stdout.readline()
        if not outdata and self.popen.returncode is not None:
            # Terminate when we've read all the output and the returncode is set
            break
        output_consumer.process_output(outdata)
        self.popen.poll()  # updates returncode so we can exit the loop
    output_consumer.finish(self.popen.returncode)
    self.post_run()

def get_popen_args(self):
    return {
        'args': self.command,
        'shell': False, # Just being explicit for security's sake
        'bufsize': 0,   # More likely to see what's being printed as it happens
                        # Not guarantted since the process itself might buffer its output
                        # run `python -u` to unbuffer output of a python processes
        'cwd': self.get_cwd(),
        'env': self.get_environment(),
        'stdout': subprocess.PIPE,
        'stderr': subprocess.STDOUT,
        'close_fds': True,  # Doesn't seem to matter
    }

这在我的生产机器上非常有效,但是在我的开发机器上,当某些子进程完成时,.readline()的调用挂起。也就是说,它将成功地处理所有输出,包括最后的输出行“process complete”,但随后将再次轮询readline,并且永远不会返回。对于我调用的大多数子进程,此方法在dev机器上正确退出,但对于一个本身调用许多子进程的复杂bash脚本,始终无法退出。

值得注意的是,popen.returncode在输出结束之前的许多行被设置为非None(通常是0)值。所以我不能在设置好后就跳出循环,否则我就失去了在进程结束时被吐出的所有东西,并且仍然在等待读取。问题是,当我在刷新缓冲区时,我无法判断何时结束,因为对readline()的最后一个调用挂起。调用read()也挂起。调用read(1)使我得到最后一个字符,但也挂在最后一行之后。popen.stdout.closed总是False。我怎么知道我什么时候结束?

所有系统都在Ubuntu 12.04LTS上运行python 2.7.3。FWIW,stderr正在使用stderr=subprocess.STDOUTstdout合并。

为什么不同?是不是因为某种原因未能关闭stdout?子流程能做些什么来保持它的开放性吗?可能是因为我从dev box上的终端启动进程,但在生产中,它是通过supervisord作为守护进程启动的吗?这会改变管道的处理方式吗?如果会,我该如何规范它们?


Tags: thetorunselfoutputreadlineget进程
3条回答

主代码循环看起来不错。可能是管道没有关闭,因为另一个进程正在保持其打开状态。例如,如果脚本启动写入stdout的后台进程,则管道不会关闭。是否确定没有其他子进程仍在运行?

一个想法是当你看到.returncode已经设置好时改变模式。一旦知道主进程已经完成,就从缓冲区读取它的所有输出,但不要等待太久。您可以使用select在超时的情况下读取管道。设置几秒钟的超时,您就可以清除缓冲区,而不必等待子进程。

如果不知道导致问题的“一个复杂bash脚本”的内容,就有太多的可能性来确定确切的原因。

然而,关注这样一个事实:如果在supervisord下运行Python脚本,那么如果子进程试图从stdin读取,那么它可能会被卡住,或者如果stdin是tty,那么它的行为可能会不同,而tty(我猜)supervisord将从/dev/null重定向。

这个最小的例子似乎能更好地处理这样的情况:我的例子test.sh运行试图从stdin读取的子进程。。。

import os
import subprocess

f = subprocess.Popen(args='./test.sh',
                     shell=False,
                     bufsize=0,
                     stdin=open(os.devnull, 'rb'),
                     stdout=subprocess.PIPE,
                     stderr=subprocess.STDOUT,
                     close_fds=True)

while 1:
    s = f.stdout.readline()
    if not s and f.returncode is not None:
        break
    print s.strip()
    f.poll()
print "done %d" % f.returncode

否则,您总是可以回到使用non-blocking read的方式,当您得到最后一个输出行时,它会说“进程完成”,尽管这有点麻烦。

如果使用readline()或read(),则不应挂起。不需要检查returncode或poll()。如果它在您知道流程完成时挂起,那么它很可能是一个保持管道打开的子流程,正如其他人之前所说的。

有两件事可以调试: *尝试使用最小的脚本而不是当前复杂的脚本进行复制,或者 *用strace -f -e clone,execve,exit_group运行那个复杂的脚本,看看该脚本启动的是什么,以及是否有任何进程在主脚本中幸存下来(检查主脚本何时调用exit_group,如果strace在此之后仍在等待,则有一个子脚本仍在活动)。

相关问题 更多 >