Python subprocess readlines()卡住了

32

我要完成的任务是流式传输一个Ruby文件并打印输出。(注意:我不想一次性打印出所有内容)

main.py

from subprocess import Popen, PIPE, STDOUT

import pty
import os

file_path = '/Users/luciano/Desktop/ruby_sleep.rb'

command = ' '.join(["ruby", file_path])

master, slave = pty.openpty()
proc = Popen(command, bufsize=0, shell=True, stdout=slave, stderr=slave, close_fds=True)     
stdout = os.fdopen(master, 'r', 0)

while proc.poll() is None:
    data = stdout.readline()
    if data != "":
        print(data)
    else:
        break

print("This is never reached!")

ruby_sleep.rb

puts "hello"

sleep 2

puts "goodbye!"

问题

流式传输文件正常运行。hello/goodbye输出每隔2秒打印一次,就像脚本应该运行的那样。问题在于readline()在最后挂起,从未退出。我永远无法到达最后一个print。

我知道stackoverflow上有很多类似的问题,但没有一个能解决我的问题。我不太了解整个子进程的事情,请给我一个更具实践性和具体性的答案。

敬礼

编辑

修复意外的代码。(与实际错误无关)


不确定您在将代码粘贴到问题中时是否存在错别字,或者问题是否真实存在。在我看来,if 应该缩进,使其位于循环内部。 - cdarke
谢谢您的注意。这是我复制代码时的一个打字错误。现在已经修复了。对此感到抱歉。 - vermin
我不确定,但你的问题是否与https://dev59.com/Ql7Va4cB1Zd3GeqPIEM5中的问题非常相似? - HerrKaputt
4个回答

35
我假设您使用pty是出于Q:为什么不只使用管道(popen())?中概述的原因(到目前为止,所有其他答案都忽略了您的“注意:我不想一次打印出所有内容”)。
在文档中所说,pty仅适用于Linux:

由于伪终端处理高度依赖于平台,因此只有针对Linux的代码才能进行处理。(Linux代码应该可以在其他平台上运行,但尚未经过测试。)

它在其他操作系统上的效果尚不清楚。
您可以尝试使用pexpect
import sys
import pexpect

pexpect.run("ruby ruby_sleep.rb", logfile=sys.stdout)

或者使用stdbuf在非交互模式下启用行缓冲:
from subprocess import Popen, PIPE, STDOUT

proc = Popen(['stdbuf', '-oL', 'ruby', 'ruby_sleep.rb'],
             bufsize=1, stdout=PIPE, stderr=STDOUT, close_fds=True)
for line in iter(proc.stdout.readline, b''):
    print line,
proc.stdout.close()
proc.wait()

或者根据@Antti Haapala的答案,使用stdlib中的pty

#!/usr/bin/env python
import errno
import os
import pty
from subprocess import Popen, STDOUT

master_fd, slave_fd = pty.openpty()  # provide tty to enable
                                     # line-buffering on ruby's side
proc = Popen(['ruby', 'ruby_sleep.rb'],
             stdin=slave_fd, stdout=slave_fd, stderr=STDOUT, close_fds=True)
os.close(slave_fd)
try:
    while 1:
        try:
            data = os.read(master_fd, 512)
        except OSError as e:
            if e.errno != errno.EIO:
                raise
            break # EIO means EOF on some systems
        else:
            if not data: # EOF
                break
            print('got ' + repr(data))
finally:
    os.close(master_fd)
    if proc.poll() is None:
        proc.kill()
    proc.wait()
print("This is reached!")

所有三个代码示例都会立即打印“hello”(一旦看到第一个EOL)。


保留旧的更复杂的代码示例,因为它可能会在SO上的其他帖子中被引用和讨论

或者使用基于@Antti Haapala的回答pty:

import os
import pty
import select
from subprocess import Popen, STDOUT

master_fd, slave_fd = pty.openpty()  # provide tty to enable
                                     # line-buffering on ruby's side
proc = Popen(['ruby', 'ruby_sleep.rb'],
             stdout=slave_fd, stderr=STDOUT, close_fds=True)
timeout = .04 # seconds
while 1:
    ready, _, _ = select.select([master_fd], [], [], timeout)
    if ready:
        data = os.read(master_fd, 512)
        if not data:
            break
        print("got " + repr(data))
    elif proc.poll() is not None: # select timeout
        assert not select.select([master_fd], [], [], 0)[0] # detect race condition
        break # proc exited
os.close(slave_fd) # can't do it sooner: it leads to errno.EIO error
os.close(master_fd)
proc.wait()

print("This is reached!")

这里使用 if not data: break 的原因是什么?那个情况不会在下一个 while 迭代中被 proc.poll() is not None 捕获吗? - Andy Hayden
询问的原因是为了这个(相关)答案:https://dev59.com/e6_la4cB1Zd3GeqPq06k#52954716 - Andy Hayden
@AndyHayden 忽略最后一个代码示例。它仅出于历史原因而存在(请阅读代码前的注释)。在新代码中(使用while 1循环),不使用p.poll()来中断循环。相关Python subprocess .check_call vs .check_output - jfs
@unutbu,有多个问题:评论中提到的竞态条件、EIO处理、没有清理 - 它们可以像新示例所示那样被修复。 - jfs
@jfs:啊,现在我明白了,这基本上就是你在这里所做的事情:https://dev59.com/SFwZ5IYBdhLWcg3wC8f4#31953436。 - unutbu
显示剩余2条评论

5

不确定您的代码出了什么问题,但是以下内容对我有效:

#!/usr/bin/python

from subprocess import Popen, PIPE
import threading

p = Popen('ls', stdout=PIPE)

class ReaderThread(threading.Thread):

    def __init__(self, stream):
        threading.Thread.__init__(self)
        self.stream = stream

    def run(self):
        while True:
            line = self.stream.readline()
            if len(line) == 0:
                break
            print line,


reader = ReaderThread(p.stdout)
reader.start()

# Wait until subprocess is done
p.wait()

# Wait until we've processed all output
reader.join()

print "Done!"

请注意,我没有安装Ruby,因此无法检查您的实际问题。 不过,ls 命令可以正常工作。

使用 if len(line): 是一个帮助我的好方法。在Python3中,仅使用 if line: 是无效的。 - pevik

2
基本上,您在这里看到的是proc.poll()readline()之间的竞争条件。由于主文件句柄上的输入从未关闭,如果进程在Ruby进程完成输出后尝试在其上执行readline(),则永远不会有任何内容可读取,但管道永远不会关闭。只有在Shell进程在您的代码尝试另一个readline()之前关闭时,代码才能正常工作。

以下是时间轴:

readline()
print-output
poll()
readline()
print-output (last line of real output)
poll() (returns false since process is not done)
readline() (waits for more output)
(process is done, but output pipe still open and no poll ever happens for it).

简单的解决方法是按照文档中建议的那样,仅使用子进程模块,而不与openpty结合使用。

http://docs.python.org/library/subprocess.html

这里有一个非常相似的问题,可以作为进一步研究的参考:
使用 subprocess、select 和 pty 捕获输出时会挂起。(原文链接)

然而,即使不使用 pty,而是使用 readline,代码也会挂起。 - Hans Then
但它正在使用一个pty的一部分上的readline。这就是为什么readline会挂起。如果输出管道被关闭,它将返回EOF,而readline将返回“”。由于输出管道仍然打开,但没有进程提供任何输入,因此它不提供任何输出。 - jmh
不,我重写了代码,以避免使用 pty。但它仍然在 readline 中挂起。只有当我删除 readline 时,代码才能正常工作。 - Hans Then
最后一个链接点赞。不清楚你所说的“只需像文档中建议的那样使用子进程模块,而不是与openpty一起使用”,但由于Ruby端的块缓冲,它可能无法正常工作。请参见我的答案 - jfs

1

试试这个:

proc = Popen(command, bufsize=0, shell=True, stdout=PIPE, close_fds=True)
for line in proc.stdout:
    print line

print("This is most certainly reached!")

正如其他人所指出的那样,readline()在读取数据时会阻塞。即使您的子进程已经死亡,它仍然会这样做。我不确定为什么在执行其他答案中的ls时不会发生这种情况,但也许是因为Ruby解释器检测到它正在写入PIPE,因此它不会自动关闭。


1
由于块缓冲,当子进程死亡时,此代码不会显示“hello”。在您的情况下,readline()将返回'',请尝试使用iter(proc.stdout.readline, b'') - jfs

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接