偷看子进程的标准输出.Popen 对象行为不正确,我错过了什么吗?



确切地说,在读取它包含的所有内容之前,它不会更新(但前提是流至少被读取过一次(,这使得它实际上无法正常工作。

请原谅这个奇怪的例子,但我目前正在尝试编写一个简单的图形 ping 监视器:

import tkinter as tk
from subprocess import Popen, PIPE, STDOUT
import shlex, re
from sys import stdout, platform
class Ping(object):
def __init__(self):
if platform == "win32":
command = shlex.split("ping -w 999 -t 8.8.8.8")
elif platform == "linux" or platform == "osx":
command = shlex.split("ping -W 1 8.8.8.8")
self.ping = Popen(command, stdout=PIPE, stderr=STDOUT, shell=True)
self.ping.stdout.readline()
self.ping.stdout.readline()
def get_next_ping(self):
has_line = str.find(self.ping.stdout.peek().decode("ascii", "ignore"), "n") != -1
if not has_line:
print(self.ping.stdout.peek()) # Debug statement
return None
else:
line = self.ping.stdout.readline().decode("ascii", "ignore")
print(line) # Debug statement
try: return int(float(re.findall("([0-9]+)[^m]?ms", line)[0]))
except IndexError: return -1
class App(tk.Tk):
def __init__(self):
tk.Tk.__init__(self)
self.pingmon = Ping()
self.bind("<ButtonPress-1>", self.check_buffer)
def check_buffer(self, event):
print(self.pingmon.get_next_ping())
app=App()
app.mainloop()

在此示例中,单击时,将轮询子进程以查看是否有新行(包含输出 ping 或超时消息(可用。如果运行项目并立即开始单击,您会注意到peek()的输出已停止更新并且始终处于b'Reply from 8.8.8.8: '状态。

我也尝试了另一种方法来检查peek输出的长度,但它显然永远不会等于零,所以这也是毫无价值的。

此外,我试图调用流flush()方法,但它似乎也没有任何帮助

最终的结果是subprocess.Popen.stdout.peek()似乎功能失调,不能用于窥视输出的预期目的,但 Python 是一种成熟的语言,我不希望在其中发现这种错误,我错过了什么吗?如果没有,我该如何解决此问题?

答案

只需使用readline()方法。 如果不存在行,则返回空字节对象 -b''

readline()的用法示例:

from subprocess import Popen, PIPE, STDOUT
curdir = Popen(['pwd'], stdout=PIPE, stderr=STDOUT)
print(curdir.stdout.readline())
print(curdir.stdout.readline())
print(curdir.stdout.readline())

这将输出(在 Python 3 上(:

b'/home/shmulikn'
b''
b''

对于您的情况,这是get_next_ping()func 更新的(也稍微更改了正则表达式(

def get_next_ping(self):
line = self.ping.stdout.readline()
if not line:
return
line = line.decode('utf-8', 'ignore')
print(line)  # Debug statement
try:
return int(float(re.search(r'([0-9.]+)[^m]?ms', line).group(1)))
except (IndexError, AttributeError):
return -1

无阻塞

如果你关心阻止操作,请看这个所以回答

您可以使用 Unix 上的select模块以非阻塞方式从 stdout 读取,或者运行后台线程来更新缓冲区以进行读取。

使用线程进行缓冲的示例

class Ping(object):
def __init__(self):
if platform == "win32":
command = shlex.split("ping -w 999 -t 8.8.8.8")
elif platform == "linux" or platform == "osx":
command = shlex.split("ping -W 1 8.8.8.8")
self.ping = Popen(command, stdout=PIPE, stderr=STDOUT, shell=True)
self.ping.stdout.readline()
self.ping.stdout.readline()
self.lines = []  # lines will be added here by the background thread
self.lines_lock = Lock()  # avoid race conditions on .pop()
self.lines_reader_thread = Thread(target=self._readlines)  # start the background thread
self.lines_reader_thread.daemon = True
self.lines_reader_thread.start()
def _readlines(self):
line = None
while line or line is None:
line = self.ping.stdout.readline().decode()
with self.lines_lock:
self.lines.append(line)
def get_next_ping(self):
with self.lines_lock:
if not self.lines:
return
line = self.lines.pop()
print(line)  # Debug statement
try:
return int(float(re.search(r'([0-9.]+)[^m]?ms', line).group(1)))
except (IndexError, AttributeError):
return -1

建议

  1. 使用现有的 Python 库进行 ping 而不是解析 stdout。 某些库需要在 Linux 下以 root 身份运行,这对您来说可能是一个限制。
  2. 一次
  3. 发送一个 ping,而不是长时间运行的后台 ping 进程。 这样您就可以使用subprocess.check_output().
  4. 避免在popen()上使用shell=True,将未经净化的输入传递给它可能会导致命令注入。

@Llamageddon我认为需要移动文件指针以刷新缓冲区,如果不是has_line检查,请使用readline().Peek 不会推进指针,因此您基本上有一个错误,该错误将在空文件缓冲区处保持"峰值"。

if not has_line:
print(self.ping.stdout.peek()) # Debug statement
self.ping.stdout.readline() # Should refresh the filebuffer.
return None

re:peek()可用于查看大文件缓冲区,考虑到响应大小,它可能不适合您的工作;但是,我认为当peek()不是"功能失调且不可用"时的一个很好的例子:)是当缓冲区中的行长度为 100,000 个字符并且查看前 100 个字符足以评估如何处理该行(即跳过它或应用其他逻辑(时。 Peak将允许我们执行外观和评估,同时最大限度地减少阻塞时间。

最新更新