Problemas interceptando salida de subproceso en tiempo real.

He pasado aproximadamente 6 horas en el desbordamiento de stack, reescribiendo mi código de Python e intentando que esto funcione. Simplemente no lo hace. No importa lo que yo haga.

El objective: lograr que la salida de un subproceso aparezca en tiempo real en un cuadro de texto tkinter.

El problema: no puedo descubrir cómo hacer que el Popen funcione en tiempo real. Parece que se cuelga hasta que se completa el proceso. (Ejecutar por su cuenta, el proceso funciona completamente como se esperaba, por lo que es este el error)

Código relevante:

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() 

Además, siéntase libre de destruir mi código si está mal escrito. Este es mi primer proyecto en Python, no espero ser bueno en el idioma todavía.

El problema aquí es que process.stdout.readline() se bloqueará hasta que haya una línea completa disponible. Esto significa que la line == '' condición line == '' nunca se cumplirá hasta que el proceso finalice. Tienes dos opciones alrededor de esto.

Primero, puede configurar stdout para no bloquear y administrar un búfer usted mismo. Se vería algo como esto. EDITAR: Como Terry Jan Reedy señaló, esta es una solución única de Unix. La segunda alternativa debe ser la preferida.

 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) 

La segunda alternativa es hacer que un hilo separado lea las líneas en una cola. Luego haga que las líneas de actualización salgan de la cola. Se vería algo como esto

 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) 

La ruta de enhebrado es probablemente más segura. No estoy seguro de que establecer la salida estándar en no locking funcione en todas las plataformas.