6

我有一个 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。FWIW,stderr正在与stdoutusing合并stderr=subprocess.STDOUT

为什么有区别?stdout是否因为某种原因无法关闭?子子流程能否以某种方式使其保持打开状态?可能是因为我是从我的开发盒上的终端启动进程,但在生产中它是作为守护进程启动的supervisord?这会改变管道的处理方式吗?如果是这样,我该如何规范它们?

4

6 回答 6

3

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

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

于 2013-04-25T22:57:06.773 回答
2

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

但是,如果您在 下运行 Python 脚本,则关注您声称它有效的事实,supervisord那么如果子进程试图从 stdin 读取,它可能会卡住,或者如果 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

否则,您总是可以回退到使用非阻塞读取,并在您的最终输出行显示“进程完成”时退出,尽管这有点小技巧。

于 2013-04-26T10:45:43.383 回答
2

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

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

于 2013-04-26T22:19:39.777 回答
1

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

我不确定您是否可以将其调整为 readline,因为如果 final\n丢失,或者进程在关闭其 stdin 和/或终止它之前没有关闭其 stdout,readline 可能会挂起。您可以将其包装在生成器中,并且每次\n在 stdout_collected 中遇到 a 时,都生成当前行。

另请注意,在我的实际代码中,我使用伪终端 (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()
于 2013-04-30T13:06:59.013 回答
0

为什么将 sdterr 设置为 STDOUT?

对子进程进行communicate() 调用的真正好处是您能够检索包含stdout 响应和stderr 消息的元组。

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

此外,它可以使您免于不得不遍历行的痛苦。Communicate() 为您提供一切,并且没有关于是否收到完整消息的未解决问题

于 2013-04-27T09:44:51.197 回答
0

我用 bash 子进程编写了一个演示,可以轻松探索。在的输出中可以识别封闭的管道,而空行的输出是。 ''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
于 2013-04-30T18:42:07.340 回答