【问题标题】:Issues intercepting subprocess output in real time实时拦截子流程输出的问题
【发布时间】:2015-02-04 07:57:06
【问题描述】:

我在堆栈溢出方面花了大约 6 个小时,重写了我的 python 代码并试图让它工作。它只是没有。不管我做什么。

目标: 让子进程的输出实时显示在 tkinter 文本框中。

问题: 我不知道如何使 Popen 实时工作。它似乎挂起,直到该过程完成。 (自行运行,过程完全按预期运行,所以只是这个东西有错误)

相关代码:

import os
import tkinter
import tkinter.ttk as tk
import subprocess

class Application (tk.Frame):
    process = 0
    def __init__ (self, master=None):
        tk.Frame.__init__(self, master)
        self.grid()
        self.createWidgets()

    def createWidgets (self):
        self.quitButton = tk.Button(self, text='Quit', command=self.quit)
        self.quitButton.grid()
        self.console = tkinter.Text(self)
        self.console.config(state=tkinter.DISABLED)
        self.console.grid()

    def startProcess (self):
        dir = "C:/folder/"
        self.process = subprocess.Popen([ "python", "-u", dir + "start.py" ], stdout=subprocess.PIPE, stdin=subprocess.PIPE, stderr=subprocess.PIPE, cwd=dir)
        self.updateLines()

    def updateLines (self):
        self.console.config(state=tkinter.NORMAL)
        while True:
            line = self.process.stdout.readline().decode().rstrip()
            if line == '' and self.process.poll() != None:
                break
            else: 
                self.console.insert(tkinter.END, line + "\n")
        self.console.config(state=tkinter.DISABLED)
        self.after(1, self.updateLines)

app = Application()
app.startProcess()
app.mainloop()

另外,如果我的代码写得不好,请随意销毁我的代码。这是我的第一个 python 项目,我不希望自己擅长这门语言。

【问题讨论】:

标签: python python-3.x tkinter subprocess stdout


【解决方案1】:

这里的问题是process.stdout.readline() 将阻塞直到有完整的行可用。这意味着在进程退出之前,条件line == '' 永远不会满足。您有两种选择。

首先,您可以将 stdout 设置为非阻塞并自己管理缓冲区。它看起来像这样。编辑:正如 Terry Jan Reedy 指出的那样,这是一个仅适用于 Unix 的解决方案。应该首选第二种选择。

import fcntl
...

    def startProcess(self):
        self.process = subprocess.Popen(['./subtest.sh'],
            stdout=subprocess.PIPE,
            stdin=subprocess.PIPE,
            stderr=subprocess.PIPE,
            bufsize=0) # prevent any unnecessary buffering

        # set stdout to non-blocking
        fd = self.process.stdout.fileno()
        fl = fcntl.fcntl(fd, fcntl.F_GETFL)
        fcntl.fcntl(fd, fcntl.F_SETFL, fl | os.O_NONBLOCK)

        # schedule updatelines
        self.after(100, self.updateLines)

    def updateLines(self):
        # read stdout as much as we can
        line = ''
        while True:
            buff = self.process.stdout.read(1024)
            if buff:
                buff += line.decode()
            else:
                break

        self.console.config(state=tkinter.NORMAL)
        self.console.insert(tkinter.END, line)
        self.console.config(state=tkinter.DISABLED)

        # schedule callback
        if self.process.poll() is None:
            self.after(100, self.updateLines)

第二种选择是让一个单独的线程将行读入队列。然后从队列中弹出更新行。它看起来像这样

from threading import Thread
from queue import Queue, Empty

def readlines(process, queue):
    while process.poll() is None:
        queue.put(process.stdout.readline())
...

    def startProcess(self):
        self.process = subprocess.Popen(['./subtest.sh'],
            stdout=subprocess.PIPE,
            stdin=subprocess.PIPE,
            stderr=subprocess.PIPE)

        self.queue = Queue()
        self.thread = Thread(target=readlines, args=(self.process, self.queue))
        self.thread.start()

        self.after(100, self.updateLines)

    def updateLines(self):
        try:
            line = self.queue.get(False) # False for non-blocking, raises Empty if empty
            self.console.config(state=tkinter.NORMAL)
            self.console.insert(tkinter.END, line)
            self.console.config(state=tkinter.DISABLED)
        except Empty:
            pass

        if self.process.poll() is None:
            self.after(100, self.updateLines)

线程路由可能更安全。我不确定将 stdout 设置为非阻塞是否适用于所有平台。

【讨论】:

  • 有效!太感谢了。我使用了线程路由,还没有测试过另一个。在 self.after 中使用 100 毫秒而不是更小的数字有什么好处?
  • 另外,既然你似乎对python很了解,你会不会碰巧知道我是否应该使用process.communicate向子进程发送一个新命令?
  • @Aarilight 首先,我完全忘了提及,如果您的子进程是 python 脚本,那么将您的脚本转换为适当的模块可能是一个更好的主意。然后您可以导入它以进行更好的控制并避免所有子流程的东西。关于您的第二个问题,communicate 将阻塞直到子进程退出,这不是您想要的。在这种情况下,您需要使用stdin.write(),但要注意这可能会导致阻塞或死锁。我会尝试为您找到有关何时会发生这种情况的链接。
  • @Aarilight 好的,我会看看我今晚回来时是否对此有任何见解。同时this 是我发现的关于死锁问题的最佳链接。最主要的是确保您的 std* 缓冲区被刷新,因此它们不会填满和阻塞。
  • fcntl 仅适用于 unix。我不知道它是否适用于 OSX,或者是否有另一种方法可以在 Windows 上使文件(管道)不阻塞。
猜你喜欢
  • 1970-01-01
  • 2010-10-06
  • 2020-03-03
  • 2011-01-26
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2020-11-27
相关资源
最近更新 更多