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

时间:2013-02-13 16:21:51

标签: python python-2.7 popen

我有一个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()的调用会挂起。也就是说,它将成功处理所有输出,包括说“处理完成”的最终输出行,但随后将再次轮询readline并且永不返回。对于我调用的大多数子进程,此方法在dev机器上正确退出,但是对于一个本身调用许多子进程的复杂bash脚本,始终无法退出。

值得注意的是,popen.returncode在输出结束之前的许多行设置为非None(通常为0)值。所以我不能在设置它时突然退出循环,否则我会失去在进程结束时吐出来的所有东西,并且仍然在缓冲等待阅读。问题是当我在那时刷新缓冲区时,我无法判断我到底是什么时候因为最后一次调用readline()而挂起。呼叫read()也会挂起。调用read(1)可以获取最后一个字符,但也会在最后一行之后挂起。 popen.stdout.closed始终为False。我怎么知道我什么时候结束?

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

为何与众不同?是否由于某种原因未能关闭stdout?子流程可以做些什么来保持它以某种方式打开吗?可能是因为我从开发盒上的终端启动了这个过程,但是在生产中它是通过supervisord作为守护进程启动的吗?这会改变管道的处理方式吗?如果是这样,我该如何规范它们呢?

6 个答案:

答案 0 :(得分:2)

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

当您看到.returncode已设置时,想法是更改模式。一旦你知道主进程已经完成,从缓冲区读取它的所有输出,但不要等待等待。您可以使用select从超时读取管道。设置几秒钟的超时时间,您可以清除缓冲区而不会遇到等待子进程的问题。

答案 1 :(得分:2)

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

你可以做两件事来调试它: *尝试使用最小的脚本而不是当前复杂的脚本重现,或 *使用strace -f -e clone,execve,exit_group运行该复杂脚本并查看该脚本启动的内容,如果任何进程在主脚本中存活(检查主脚本何时调用exit_group,如果strace仍在等待,那么您还有一个孩子活的)。

答案 2 :(得分:1)

在不知道导致问题的“一个复杂的bash脚本”的内容的情况下,确定确切原因的可能性太多了。

但是,如果你在supervisord下运行你的Python脚本,那么关注你声称它有用的事实,那么如果一个子进程试图从stdin读取它可能会卡住,或者只是表现不同stdin是一个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,并在你的最终输出行说“过程完成”时挽救,虽然这有点像黑客。

答案 3 :(得分:1)

我发现拨打read(或readline)的电话有时会挂起,尽管之前正在调用poll。所以我使用调用select来查明是否有可读数据。但是,如果进程已关闭,那么没有超时的select也会挂起。所以我在一个半繁忙的循环中调用select,每次迭代都有一个很小的超时(见下文)。

我不确定你是否可以将它改为readline,因为如果缺少最后的\n,或者在关闭stdin和/或终止之前进程没有关闭stdout,readline可能会挂起它。您可以将它包装在生成器中,并且每次在stdout_collected中遇到\n时,都会产生当前行。

另请注意,在我的实际代码中,我使用伪终端(pty)来包装popen句柄(更严格地伪造用户输入),但它应该没有。

# handle to read from
handle = self.popen.stdout

# how many seconds to wait without data
timeout = 1

begin = datetime.now()
stdout_collected = ""

while self.popen.poll() is None:
    try:
        fds = select.select([handle], [], [], 0.01)[0]
    except select.error, exc:
        print exc
        break

    if len(fds) == 0:
        # select timed out, no new data
        delta = (datetime.now() - begin).total_seconds()
        if delta > timeout:
            return stdout_collected

        # try longer
        continue
    else:
        # have data, timeout counter resets again
        begin = datetime.now()

    for fd in fds:
        if fd == handle:
            data = os.read(handle, 1024)
            # can handle the bytes as they come in here
            # self._handle_stdout(data)
            stdout_collected += data

# process exited
# if using a pseudoterminal, close the handles here
self.popen.wait()

答案 4 :(得分:0)

为什么要将sdterr设置为STDOUT?

对子进程进行communication()调用的真正好处是,您可以检索包含stdout响应以及stderr meesage的元组。

如果逻辑取决于他们的成功或失败,那些可能会有用。

此外,它可以帮助您避免不必遍历线路的痛苦。 Communicate()为您提供了所有内容,并且没有关于是否收到完整消息的未解决的问题

答案 5 :(得分:0)

我用bash子流程写了一个 demo ,可以很容易地探索。 ''的输出中的readline()可识别已关闭的管道,而空行的输出为'\n'

from subprocess import Popen, PIPE, STDOUT
p = Popen(['bash'], stdout=PIPE, stderr=STDOUT)
out = []
while True:
    outdata = p.stdout.readline()
    if not outdata:
        break
    #output_consumer.process_output(outdata)
    print "* " + repr(outdata)
    out.append(outdata)
print "* closed", repr(out)
print "* returncode", p.wait()

输入/输出示例在终止流程之前明确关闭管道 。这就是为什么应该使用 wait() 而不是poll()

[prompt] $ python myscript.py
echo abc
* 'abc\n'
exec 1>&- # close stdout
exec 2>&- # close stderr
* closed ['abc\n']
exit
* returncode 0
[prompt] $

对于这种情况,您的代码确实输出了大量空字符串。


示例:最后一行没有'\n'的快速终止流程:

echo -n abc
exit
* 'abc'
* closed ['abc']
* returncode 0