检测popen.stdout.readline流结束
我有一个用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
时就一直不返回。这个方法在开发机器上对我调用的大多数子进程都能正常退出,但对于一个复杂的bash脚本,它自己又调用了很多子进程时,总是无法正常退出。
值得注意的是,popen.returncode
在输出结束前的很多行就被设置为一个非 None
的值(通常是 0
)。所以我不能仅仅在这个值被设置时就跳出循环,否则我会丢失在进程结束时输出的所有内容,这些内容仍然在缓冲区中等待读取。问题是,当我在那个时候刷新缓冲区时,我无法判断是否到了结束,因为最后一次调用 readline()
会卡住。调用 read()
也会卡住。调用 read(1)
能让我获取到最后一个字符,但在最后一行之后也会卡住。popen.stdout.closed
始终是 False
。我该如何判断是否到了结束呢?
所有系统都在 Ubuntu 12.04LTS 上运行 Python 2.7.3。顺便提一下,stderr
是通过 stderr=subprocess.STDOUT
与 stdout
合并的。
为什么会有这样的差异?是因为某种原因没有关闭 stdout
吗?子子进程可能做了什么让它保持打开状态吗?可能是因为我在开发机器的终端中启动进程,而在生产环境中是通过 supervisord
作为守护进程启动的吗?这会改变管道的处理方式吗?如果是的话,我该如何使它们正常化?
6 个回答
如果你使用readline()或read(),程序应该不会卡住。也不需要检查返回代码或轮询。如果你知道程序已经结束,但它还是卡住了,那很可能是有一个子进程让你的管道保持打开状态,正如其他人之前提到的。
你可以做两件事来调试这个问题:
* 尝试用一个简单的脚本来重现这个问题,而不是用现在这个复杂的脚本,或者
* 用strace -f -e clone,execve,exit_group
来运行那个复杂的脚本,看看这个脚本在启动什么进程,以及是否有任何进程在主脚本结束后还在运行(检查主脚本调用exit_group时,如果strace还在等待,那说明有一个子进程还活着)。
在不知道那个“复杂的 bash 脚本”具体内容的情况下,确实有太多可能性让人很难确定问题的确切原因。
不过,既然你提到在 supervisord
下运行你的 Python 脚本是可以正常工作的,那可能是因为某个子进程在尝试从标准输入(stdin)读取数据时卡住了,或者在标准输入是一个终端(tty)时表现得不一样。我猜 supervisord
会把输入重定向到 /dev/null
,也就是不让它读取任何输入。
这个简单的例子似乎在处理我的例子 test.sh
运行子进程尝试从标准输入读取的情况时表现得更好……
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
另外,你也可以选择使用一种叫做 非阻塞读取 的方法,当你看到输出的最后一行显示“进程完成”时就退出,虽然这有点像是变通的办法。
主要的代码循环看起来没问题。可能是因为有其他进程在使用这个管道,所以它没有关闭。比如说,如果你的脚本启动了一个后台进程去写入 stdout
,那么这个管道就不会关闭。你确定没有其他子进程还在运行吗?
一个建议是,当你看到 .returncode
被设置时,改变一下模式。确认主进程完成后,从缓冲区读取所有输出,但不要卡在那儿等着。你可以使用 select 来设置一个超时,从管道中读取数据。设置几秒钟的超时,这样你就可以清空缓冲区,而不会被卡在等待子进程的状态。