Try this:
from subprocess import Popen, PIPE
from threading import Thread, Lock
import tkinter as tk
# Global variables
proc = None
running = True
stdout_buffer = ""
stdout_buffer_lock = Lock()
def stdout_loop():
global stdout_buffer
with stdout_buffer_lock:
# Get the data and clear the buffer:
data, stdout_buffer = stdout_buffer, ""
text.config(state="normal")
text.insert("end", data)
text.see("end")
text.config(state="disabled")
if proc is None:
if len(commands) == 0:
# If we are done with all of the commands:
text.config(cursor="")
root.config(cursor="")
else:
# If we have more commands to do call `start_next_proc`
start_next_proc()
else:
root.after(100, stdout_loop)
def start_next_proc():
global proc, commands
command = commands.pop(0) # Take the first one from the list
proc = Popen(command, shell=True, stdout=PIPE)
new_thread = Thread(target=read_stdout, daemon=True)
new_thread.start()
stdout_loop()
def start_new_proc():
global commands
# For linux use "-c 5". For windows use "-n 5"
commands = ["ping 8.8.8.8 -c 5",
"ping 1.1.1.1 -c 5",
"ping 5.5.5.5 -c 5"]
start_next_proc()
text.config(cursor="clock")
root.config(cursor="clock")
def read_stdout():
global proc, stdout_buffer
while proc.poll() is None:
line = proc.stdout.readline()
with stdout_buffer_lock:
stdout_buffer += line.decode()
proc = None
root = tk.Tk()
text = tk.Text(root, state="disabled")
text.pack()
button = tk.Button(root, text="Run", command=start_new_proc)
button.pack()
root.mainloop()
In the code, I start the process in the main thread and then I start 2 loops. One of the loops is a while
loop that's inside read_stdout
. It reads all of the data out of the process and writes it into stdout_buffer
. The second loops is a tkinter
loop (inside stdout_loop) that reads stdout_buffer
and writes it inside the Text
widget.
I also use a lock to make sure that the buffer doesn't get corrupted.
If tkinter
was thread safe, the code would have been much shorter but it isn't.
If you want it as a class:
from subprocess import Popen, PIPE
from threading import Thread, Lock
import tkinter as tk
class TkinterPopen(tk.Text):
def __init__(self, master, state="disabled", **kwargs):
super().__init__(master, state=state, **kwargs)
self.commands = []
self.proc = None
self.running = True
self.stdout_buffer = ""
self.stdout_buffer_lock = Lock()
def stdout_loop(self) -> None:
with self.stdout_buffer_lock:
# Get the data and clear the buffer:
data, self.stdout_buffer = self.stdout_buffer, ""
state = super().cget("state")
super().config(state="normal")
super().insert("end", data)
super().see("end")
super().config(state=state)
if self.proc is None:
if len(self.commands) == 0:
# If we are done with all of the commands:
super().config(cursor="")
else:
# If we have more commands to do call `start_next_proc`
self.start_next_proc()
else:
super().after(100, self.stdout_loop)
def start_next_proc(self) -> None:
command = self.commands.pop(0) # Take the first one from the list
self.proc = Popen(command, shell=True, stdout=PIPE)
new_thread = Thread(target=self.read_stdout, daemon=True)
new_thread.start()
self.stdout_loop()
def run_commands(self, commands:list) -> None:
self.commands = commands
self.start_next_proc()
super().config(cursor="clock")
def read_stdout(self):
while self.proc.poll() is None:
line = self.proc.stdout.readline()
with self.stdout_buffer_lock:
self.stdout_buffer += line.decode()
self.proc = None
if __name__ == "__main__":
def start_commands():
# For linux use "-c 5". For windows use "-n 5"
commands = ["echo hi",
"ping 1.1.1.1 -n 3",
"ping 5.5.5.5 -n 3"]
tkinter_popen.run_commands(commands)
root = tk.Tk()
tkinter_popen = TkinterPopen(root)
tkinter_popen.pack()
button = tk.Button(root, text="Run", command=start_commands)
button.pack()
root.mainloop()