Reading from an os.pipe() directly to a Tkinter text box - python

I am trying to route the output of a separate script to a Tkinter window.
Below is my attempt at the problem. The Tkinter box appears but does not update when the controller (abstracted here) writes to pipeout using os.write(pipeout, msg) .
from Tkinter import *
from controller import controller
import os
def run():
"""Top level run method which initiates program."""
def updateInput():
"""Update the textbox with controller output."""
readFrom = os.fdopen(pipein)
line = readFrom.readline()
text.insert(END, line)
text.after(1000, updateInput)
pipein, pipeout = os.pipe() #pipe for comms between tkinter and controller
pid = os.fork()
if not pid:
#within child process, launch controller with passed pipe
os.close(pipein)
mainController = controller(pipeout)
os.close(pipeout)
root = Tk()
text = Text(root)
text.pack()
text.after(1000, updateInput) #update text box each second
root.mainloop()
if __name__ == "__main__":
run()
The abstracted controller is writing to the pipe via
os.write(self.pipeout, msg)
where self.pipeout has been assigned from self.pipeout = pipeout in the controller class init .

Sounds like you forgot to flush.
self.pipeout.write(msg)
self.pipeout.flush()
Also, make sure the msg ends in a newline.
Edit: are you sure you need a pipe? There's probably neater ways to do whatever you are doing, like threading.

Solution came from using os.read(pipein, 100) instead of os.fdopen(pipein) in updateInput.
line = os.read(pipein, 100)
text.insert(END, line)
text.after(1000, updateInput)

Related

How can I terminate a tkinter mainloop from a different thread

I have the following problem: I would like to create a GUI with tkinter, that reacts to signals, sent from a socket. For example, I would like to be able to terminate the application, when an end signal is received.
For that purpose I have a function, running in a separate thread, that listens for signals and acts accordingly. However, when I try to destroy the tkinter-GUI, the programm stops, and gives this error message:
Fatal Python error: PyEval_RestoreThread: the function must be called with the GIL held, but the GIL is released (the current Python thread state is NULL)
Python runtime state: initialized
I have recreated this minimum working example giving the same behavior:
import tkinter as tk
import time
import threading
class Gui(tk.Frame):
"""Minimal GUI with only a button"""
def __init__(self, master: tk.Tk):
tk.Frame.__init__(self, master)
self.pack()
tk.Button(self, text='Spam').pack()
class Client:
"""Client for handling signals"""
def __init__(self, master: tk.Tk):
self.master = master
self.gui = Gui(self.master)
self.signal = None # Initialize signal
self.thread = threading.Thread(target=self.listen_thread)
self.running = True
self.thread.start()
def listen_thread(self):
"""Listen for signals and handle actions"""
while self.running:
signal = self.signal # Dummy signal, set by external method, instead of received message from socket
if signal == 'end': # End signal received
self.master.destroy() # Destroy tkinter GUI, error occurs here
self.running = False # Terminate while loop
else:
time.sleep(0.2)
def send_signal_after(receiver: Client, delay: float = 2.0):
"""Send a signal to the client after short delay"""
time.sleep(delay)
receiver.signal = 'end'
if __name__ == '__main__':
root = tk.Tk()
client = Client(root)
threading.Thread(target=send_signal_after, args=(client,)).start()
root.mainloop()
if client.thread: # Check if thread is still running, if so, wait for termination
client.thread.join()
I am running this on MacOS 12.1, Python 3.10.
Is there any other way to terminate the application? I know, I could probably use sys.exit(), but I would like to do this in a cleaner way.
Thank you!
So, to understand how to do it, I made an example:
This is the first file (the main one) :
import tkinter as tk
from tkinter import *
import threading
import file2 as file2
def func(gui):
# just some code around here
# start up the program
root = Tk()
# pass the root in the __init__ function from file2
mainGui = file2.file2Class(root)
# Start the new thread
theThread = threading.Thread(target=func, args=([mainGui]))
theThread.daemon = True
theThread.start()
# loop command
tk.mainloop()
And this is file2 :
import tkinter as tk
from tkinter import *
class file2Class:
root = None
def __init__(self, initialRoot):
self.root = initialRoot
self.createUI();
def createUI(self):
# This is an exit button
tk.Button(
self.root,
text="Exit",
font = "Verdana 10 bold",
fg="red",
command=self.root.destroy, # <- this is the exit command
width=25,
height=2).grid(row=0,column=0)
So, the most important thing is to make sure that you pass root in the args of the thread.
I hope I helped you!

Change to non-modal in tkinter

How can I change it to non-modal ?
If button is clicked then cursor doesn't change. If I change it to non-modal then it should be ok.
def start_new_proc():
# text.config(cursor="clock")
root.config(cursor="clock")
text.config(state="normal")
command = "ping 8.8.8.8 -c 1"
proc = Popen(command, shell=True, stdout=PIPE)
for line in iter(proc.stdout.readline, ''):
line = line.decode('utf-8')
if line == '':
break
text.insert("end", line)
proc.wait()
# text.config(cursor="")
root.config(cursor="")
text.config(state="disable")
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()
You can achieve this using threading.
import threading
def ping_func(output):
command = "ping 8.8.8.8 -c 1"
proc = Popen(command, shell=True, stdout=PIPE)
for line in iter(proc.stdout.readline, ''):
line = line.decode('utf-8')
if line == '':
break
output.append(line)
def check_complete():
if not ping_thread.is_alive():
root.config(cursor = "")
button.config(state = "normal")
else:
root.after(100, check_complete)
def check_output():
if output != []:
text.config(state = "normal")
text.insert("end", output.pop(0))
text.config(state = "disabled")
root.after(100, check_output)
def start_new_proc():
#text.config(cursor="clock")
root.config(cursor="clock")
button.config(state = "disabled") #Disable button until process completed
global output, ping_thread
output = []
ping_thread = threading.Thread(target = ping_func, args = (output,))
ping_thread.start()
check_output()
check_complete()
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()
By putting the command execution in a thread, it doesn't hold up the tkinter thread. The function ping_func is what the thread runs. It does what the program did before, but the output is different. Because tkinter isn't thread safe, you can't use tkinter objects in other threads. Therefore we need to pass a variable instead which can then be polled by the tkinter thread. This is what the output list is for. The output from ping_func is appended to output. To check the contents of output and insert it into the Text widget, we need a function check_output. This calls itself every 100ms to check if the contents of output have changed. If they have it inserts the new line into the Text widget. There is also check_complete which checks every 100ms if the thread has ended and changes the cursor and enables the button if it has.
Here is one approach, it uses threading and queue to not block the .mainloop() and to not call tkinter methods from another thread (which may potentially break it):
import tkinter as tk
from subprocess import Popen, PIPE
from threading import Thread
from queue import Queue
def start_new_proc():
# text.config(cursor="clock")
root.config(cursor="clock")
text.config(state="normal")
queue = Queue()
Thread(target=lambda: run_proc(queue)).start()
update_text(queue)
def after_proc():
# text.config(cursor="")
root.config(cursor="")
text.config(state="disable")
def update_text(queue):
data = queue.get()
if data == 'break':
after_proc()
return
text.insert('end', data)
root.after(100, update_text, queue)
def run_proc(queue):
command = "ping 8.8.8.8"
proc = Popen(command, shell=True, stdout=PIPE)
for line in iter(proc.stdout.readline, ''):
line = line.decode('utf-8')
if line == '':
queue.put('break')
break
queue.put(line)
# proc.wait()
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()
Quick explanation:
When you click the button, it calls start_new_proc():
Now first all the configurations are run so now the cursor and text are configured (note that the clock cursor is visible only when the cursor is not on Text widget, since the config for text widget is commented out)
Then a queue object is created, this will allow to safely communicate between threads (and it will get garbage collected once the function "stops")
Then a thread is started where the cmd command is run (oh and I changed it a bit (removed "-c 1") because I couldn't test otherwise). So every iteration the data is put into the queue for the main thread to safely receive it, other stuff is the same, also notice that there is '"break"' put into queue once the loop has to stop, so that the other end knows to stop too (probably better to place some object since "break" can be placed there by line variable)
Then the update_text() function is called which receives data from the queue and then inserts it to the text widget (there is also the "break" check) and then the .after() method for making the function loop without blocking the .mainloop() (and also when "break" apppears then configuration function gets called to config the state and cursor)

How to print messages in tkinter from subprocess?

everyone! I've run into a problem. Let's say I have a script that is launched using subprocess module from tkinter giu like this (params includes the name of the script):
p = Popen(['python.exe'] + params)
During being executed, my script has some messages that I want to be printed in my gui. While I was using console, I just did it with print function like this:
print(f'Connected to {ppm_file}')
What I now need to do, is to make this message be printed in a text widget in tkinter gui.
I suppose it implys using stdout, but I'm a newbie to this concept and find it a little bit hard to understand.
Thanks for your help!
You can capture the console output of the external script and insert the captured output into a Text widget.
Below is an example:
import tkinter as tk
import subprocess
import threading
root = tk.Tk()
logbox = tk.Text(root, width=80, height=20)
logbox.pack()
def capture_output():
# "python -u" make stdout stream to be unbuffered
p = subprocess.Popen(["python", "-u", "other.py"], stdout=subprocess.PIPE)
while p.poll() is None: # process is still running
logbox.insert("end", p.stdout.readline())
logbox.see("end")
logbox.insert("end", "--- done ---\n")
# use thread so that it won't block the application
tk.Button(root, text="Run", command=lambda: threading.Thread(target=capture_output, daemon=True).start()).pack()
root.mainloop()
In the text widget, you can insert by using following method:
txtbox.delete('1.0', END) # to remove any preceding text(ignore if empty)
txtbox.insert(END, 'Connected to {ppm_file}') # insert the text you want
txtbox.insert(END, '\n') # additional insertion for example a newline character

Python multiprocessing and tkinter - how to connect this process (GUI and spawned process)?

long time ago I started to use threading. Thread for any functions in my GUI. But last time I focused on multiprocessing module. The problem here is that when I spawn new process I don't know how to connect with my GUI.
Here is sample code - one button "execute" Thread and it works as expected.
Second button spawn Process but my GUI is not updated (input field is not filled with text).
How to fix that?
How to receive "print" statements from Process?
Thanks!
import tkinter as tk
import time
from multiprocessing import Process, Pool
from threading import Thread
root = tk.Tk()
inputEn = tk.Entry(root)
inputEn.pack()
def runner(txt):
print("runner")
time.sleep(5)
# this doesn't work when I use it with Process
inputEn.insert(0, "{}".format(txt))
print("runner end")
def process():
p = Process(target=runner, args=("process",))
p.start()
def thread():
p = Thread(target=runner, args=("thread",))
p.start()
btnStart = tk.Button(root, text="Start Process", command=process)
btnStart.pack()
btnStart2 = tk.Button(root, text="Start Thread", command=thread)
btnStart2.pack()
if __name__=="__main__":
root.mainloop()
When spawned using process, the child process cannot update the variable of the parent process.
You can experiment by modifying your code :
inputEn.pack()
globalv = 0
def runner(txt):
global globalv
print("runner")
time.sleep(5)
# this doesn't work when I use it with Process
inputEn.insert(0, "{}".format(txt))
globalv = globalv + 1
print 'globalv : ', globalv
globalv will then be updated when runner is called via thread, by not updated when runner is called via process.
You would have to use queue to make child interact with your application.
This is because a child process is instanciated using a copy of the context of the parent process - whereas a thread uses the same context as the parent.
You have not said why multiprocessing is necessary here. If you just want to update a variable, tkinter's after() works well for that. A not so elegant example
import Tkinter as tk
import random
class UpdateLabel(object):
def __init__(self, master):
self.master=master
self.str_var=tk.StringVar()
self.str_var.set("start")
tk.Entry(root, textvariable=self.str_var, width=50).pack()
tk.Button(self.master, text="Exit", bg="orange",
command=self.master.quit).pack()
self.ctr=0
self.change_it()
def change_it(self):
""" use random to simulate getting some kind of data 10 times
"""
this_lit=self.str_var.get()
next_lit=random.choice(range(100))
self.str_var.set(this_lit+", "+str(next_lit))
self.ctr += 1
if self.ctr < 10:
self.master.after(500, self.change_it)
root = tk.Tk()
UL=UpdateLabel(root)
root.mainloop()

Redirect command line results to a tkinter GUI

I have created a program that prints results on command line.
(It is server and it prints log on command line.)
Now, I want to see the same result to GUI .
How can I redirect command line results to GUI?
Please, suggest a trick to easily transform console application to simple GUI.
Note that it should work on Linux and Windows.
You could create a script wrapper that runs your command line program as a sub process, then add the output to something like a text widget.
from tkinter import *
import subprocess as sub
p = sub.Popen('./script',stdout=sub.PIPE,stderr=sub.PIPE)
output, errors = p.communicate()
root = Tk()
text = Text(root)
text.pack()
text.insert(END, output)
root.mainloop()
where script is your program. You can obviously print the errors in a different colour, or something like that.
To display subprocess' output in a GUI while it is still running, a portable stdlib-only solution that works on both Python 2 and 3 has to use a background thread:
#!/usr/bin/python
"""
- read output from a subprocess in a background thread
- show the output in the GUI
"""
import sys
from itertools import islice
from subprocess import Popen, PIPE
from textwrap import dedent
from threading import Thread
try:
import Tkinter as tk
from Queue import Queue, Empty
except ImportError:
import tkinter as tk # Python 3
from queue import Queue, Empty # Python 3
def iter_except(function, exception):
"""Works like builtin 2-argument `iter()`, but stops on `exception`."""
try:
while True:
yield function()
except exception:
return
class DisplaySubprocessOutputDemo:
def __init__(self, root):
self.root = root
# start dummy subprocess to generate some output
self.process = Popen([sys.executable, "-u", "-c", dedent("""
import itertools, time
for i in itertools.count():
print("%d.%d" % divmod(i, 10))
time.sleep(0.1)
""")], stdout=PIPE)
# launch thread to read the subprocess output
# (put the subprocess output into the queue in a background thread,
# get output from the queue in the GUI thread.
# Output chain: process.readline -> queue -> label)
q = Queue(maxsize=1024) # limit output buffering (may stall subprocess)
t = Thread(target=self.reader_thread, args=[q])
t.daemon = True # close pipe if GUI process exits
t.start()
# show subprocess' stdout in GUI
self.label = tk.Label(root, text=" ", font=(None, 200))
self.label.pack(ipadx=4, padx=4, ipady=4, pady=4, fill='both')
self.update(q) # start update loop
def reader_thread(self, q):
"""Read subprocess output and put it into the queue."""
try:
with self.process.stdout as pipe:
for line in iter(pipe.readline, b''):
q.put(line)
finally:
q.put(None)
def update(self, q):
"""Update GUI with items from the queue."""
for line in iter_except(q.get_nowait, Empty): # display all content
if line is None:
self.quit()
return
else:
self.label['text'] = line # update GUI
break # display no more than one line per 40 milliseconds
self.root.after(40, self.update, q) # schedule next update
def quit(self):
self.process.kill() # exit subprocess if GUI is closed (zombie!)
self.root.destroy()
root = tk.Tk()
app = DisplaySubprocessOutputDemo(root)
root.protocol("WM_DELETE_WINDOW", app.quit)
# center window
root.eval('tk::PlaceWindow %s center' % root.winfo_pathname(root.winfo_id()))
root.mainloop()
The essence of the solution is:
put the subprocess output into the queue in a background thread
get the output from the queue in the GUI thread.
i.e., call process.readline() in the background thread -> queue -> update GUI label in the main thread. Related kill-process.py (no polling -- a less portable solution that uses event_generate in a background thread).
Redirecting stdout to a write() method that updates your gui is one way to go, and probably the quickest - although running a subprocess is probably a more elegant solution.
Only redirect stderr once you're really confident it's up and working, though!
Example implimentation (gui file and test script):
test_gui.py:
from Tkinter import *
import sys
sys.path.append("/path/to/script/file/directory/")
class App(Frame):
def run_script(self):
sys.stdout = self
## sys.stderr = self
try:
del(sys.modules["test_script"])
except:
## Yeah, it's a real ugly solution...
pass
import test_script
test_script.HelloWorld()
sys.stdout = sys.__stdout__
## sys.stderr = __stderr__
def build_widgets(self):
self.text1 = Text(self)
self.text1.pack(side=TOP)
self.button = Button(self)
self.button["text"] = "Trigger script"
self.button["command"] = self.run_script
self.button.pack(side=TOP)
def write(self, txt):
self.text1.insert(INSERT, txt)
def __init__(self, master=None):
Frame.__init__(self, master)
self.pack()
self.build_widgets()
root = Tk()
app = App(master = root)
app.mainloop()
test_script.py:
print "Hello world!"
def HelloWorld():
print "HelloWorldFromDef!"
Sorry for my bad English. I actually, used a different way to print Command Prompt output into my new Automation tool.
Please find those steps below.
1> Create a Bat File & redirect its output to a LOG file.
Command Prompt command: tasklist /svc
2> Make read that file with Python 3.x.
`processedFile = open('D:\LOG\taskLog.txt', 'r')
3> The Finale step.
ttk.Label(Tab4, text=[ProcessFile.read()]).place(x=0, y=27)
**Hence please be informed that, I have not include scrollbar into this code yet.
Posting Screenshot:

Categories