小编典典

实时发出拦截子流程输出的信息

python

我在堆栈溢出上花费了大约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项目,我预计该语言不会再有什么优势。


阅读 217

收藏
2020-12-20

共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设置为non-blocking可以在所有平台上使用。

2020-12-20