The net effect of the two
programs of the preceding section is similar to a GUI
program reading the output of a shell command over a pipe file withos.popen
(or thesubprocess
.
Popen
interface upon which it is based).
As we’ll see later, though, sockets also support independent servers,
and can link programs running on remote machines across a network—a much
larger idea we’ll be exploring in
Chapter 12
.
Perhaps subtler and more significant for our GUI exploration here
is the fact that without anafter
timer loop and nonblocking input sources of the sort used in the prior
section, the GUI may become stuck and unresponsive while waiting for
data from the non-GUI program and may not be able to handle more than
one data stream.
For instance, consider theguiStreams
call we wrote in
Example 10-12
to redirect the
output of a shell command spawned withos.popen
to a GUI window. We could use this
with simplistic code like that in
Example 10-26
to capture the
output of a spawned Python program and display it in a separately
running GUI program’s window. This is as concise as it is because it
relies on the read/write loop andGuiOutput
class in
Example 10-12
to both manage the
GUI and read the pipe; it’s essentially the same as one of the options
in that example’s self-test code, but we read the printed output of a
Python program here.
Example 10-26. PP4E\Gui\Tools\pipe-gui1.py
# GUI reader side: route spawned program standard output to a GUI window
from PP4E.Gui.Tools.guiStreams import redirectedGuiShellCmd # uses GuiOutput
redirectedGuiShellCmd('python -u pipe-nongui.py') # -u: unbuffered
Notice the-u
Python
command-line flag used here: it forces the spawned program’s standard
streams to be
unbuffered
, so we get printed text
immediately as it is produced, instead of waiting for the spawned
program to completely finish.
We talked about this option in
Chapter 5
, when discussing deadlocks and pipes.
Recall thatprint
writes tosys.stdout
, which is normally buffered when
connected to a pipe this way. If we don’t use the-u
flag here and the spawned program doesn’t
manually callsys.stdout.flush
, we
won’t see any output in the GUI until the spawned program exits or until
its buffers fill up. If the spawned program is a perpetual loop that
does not exit, we may be waiting a long time for output to appear on the
pipe, and hence, in the GUI.
This approach makes the non-GUI code in
Example 10-27
much simpler: it
just writes to standard output as usual, and it need not be concerned
with creating a socket interface. Compare this with its socket-based
equivalent in
Example 10-24
—the loop is the
same, but we don’t need to connect to sockets first (the spawning parent
reads the normal output stream), and don’t need to manually flush output
as it’s produced (the-u
flag in the spawning parent prevents
buffering).
Example 10-27. PP4E\Gui\Tools\pipe-nongui.py
# non-GUI side: proceed normally, no need for special code
import time
while True: # non-GUI code
print(time.asctime()) # sends to GUI process
time.sleep(2.0) # no need to flush here
Start the GUI script in
Example 10-26
: it launches the
non-GUI program automatically, reads its output as it is created, and
produces the window in
Figure 10-15
—it’s similar to the
socket-based example’s result in
Figure 10-14
, but displays thestr
text strings we get from reading
pipes, not the byte strings of sockets.
Figure 10-15. Messages printed to a GUI from a non-GUI program (command
pipe)
This works, but the GUI is odd—we never callmainloop
ourselves, and we get a default empty
top-level window. In fact, it apparently works at all only because the
tkinterupdate
call issued within the
redirect function enters the Tk event loop momentarily to process
pending events. To do better,
Example 10-28
creates an enclosing
GUI and kicks off an event loop manually by the time the shell command
is spawned; when run, it produces the same output window (
Figure 10-15
).
Example 10-28. PP4E\Gui\Tools\pipe-gui2.py
# GUI reader side: like pipes-gui1, but make root window and mainloop explicit
from tkinter import *
from PP4E.Gui.Tools.guiStreams import redirectedGuiShellCmd
def launch():
redirectedGuiShellCmd('python -u pipe-nongui.py')
window = Tk()
Button(window, text='GO!', command=launch).pack()
window.mainloop()
The-u
unbuffered flag is
crucial here again—without it, you won’t see the text output window. The
GUI will be blocked in the initial pipe input call indefinitely because
the spawned program’s standard output will be queued up in an in-memory
buffer.
On the other hand, this-u
unbuffered flag doesn’t prevent blocking in the prior section’s
socket
scheme, because that example resets streams
to other objects after the spawned program starts; more on this in
Chapter 12
. Also remember that the buffering
argument inos.popen
(andsubprocess.Popen
) controls buffering in the
caller
, not in the spawned program;-u
pertains to the latter.
Either way we code them, however, when the GUIs of
Example 10-26
and
Example 10-28
are run they
become unresponsive for two seconds at a time while they read data
from theos.popen
pipe. In fact,
they are just plain sluggish—window moves, resizes, redraws, raises,
and so on, are delayed for up to two seconds, until the non-GUI
program sends data to the GUI to make the pipe read call return.
Perhaps worse, if you press the “GO!” button twice in the second
version of the GUI, only one window updates itself every two seconds,
because the GUI is stuck in the second button press callback—it never
exits the loop that reads from the pipe until the spawned non-GUI
program exits. Exits are not necessarily graceful either (you get
multiple error messages in the terminal window).
Because of such constraints, to avoid blocked states, a
separately running GUI cannot generally read data directly if its
appearance may be delayed. For instance, in the socket-based scripts
of the prior section (
Example 10-25
), theafter
timer loop allows the GUI to
poll
for data instead of
waiting
, and display it as it arrives. Because it
doesn’t wait for the data to show up, its GUI remains active in
between outputs.
Of course, the real issue here is that the read/write loop in
theguiStreams
utility function
used is too simplistic; issuing a read call within a GUI is generally
prone to blocking. There are a variety of ways we might try to avoid
this.
One candidate fix is to
try to run the redirection loop call in a thread—for
example, by changing thelaunch
function in
Example 10-28
as follows (this is from file
pipe-gui2-thread.py
on the examples
distribution):
def launch():
import _thread
_thread.start_new_thread(redirectedGuiShellCmd, ('python -u pipe-nongui.py',))
But then we would be updating the GUI from a spawned thread,
which, as we’ve learned, is a generally bad idea. Parallel updates can
wreak havoc in GUIs.
If fact, with this change the GUI fails
spectacularly
—it hangs immediately on the first
“GO!” button press on my Windows 7 laptop, becomes unresponsive, and
must be forcibly closed. This happens before (or perhaps during) the
creation of the new pop-up scrolled-text window. When this example was
run on Windows XP for the prior edition of this book, it also hung on
the first “GO!” press occasionally and always hung eventually if you
pressed the button enough times; the process had to be forcibly
killed. Direct GUI updates in threads are not a viable
solution.
Alternatively, we could try to use the Pythonselect.select
call (described in
Chapter 12
) to implement polling for data on the
input pipe; unfortunately,select
works only on sockets in Windows today (it also works on pipes and
other file descriptors in Unix).
In other contexts, a separately spawned GUI might also use
signals to inform the non-GUI program when points of interaction
arise, and vice versa (the Pythonsignal
module andos.kill
call were introduced in
Chapter 5
). The downside with this approach
is that it still requires changes to the non-GUI program to handle the
signals.
Named pipes (the fifo files introduced in
Chapter 5
) are sometimes an alternative to
the socket calls of the original Examples
10-23
through
10-25
, but sockets work on standard
Windows Python, and fifos do not (os.mkfifo
is not available in Windows in
Python 3.1, though it is in Cygwin Python). Even where they do work,
we would still need anafter
timer
loop in the GUI to avoid blocking.
We might also use tkinter’screatefilehandler
to register a callback to
be run when input shows up on the input pipe:
def callback(file, mask):
...read from file here...
import _tkinter, tkinter
_tkinter.createfilehandler(file, tkinter.READABLE, callback)
The file handler creation call is also available withintkinter
and as a method of aTk
instance object. Unfortunately again, as
noted near the end of
Chapter 9
,
this call is not available on Windows and is a Unix-only
alternative.
As a far more general solution to the blocking input delays of
the prior section, the GUI process might instead spawn a thread that
reads the socket or pipe and places the data on a queue. In fact, the
thread techniques we met earlier in this chapter could be used
directly in such a role. This way, the GUI is not blocked while the
thread waits for data to show up, and the thread does not attempt to
update the GUI itself. Moreover, more than one data stream or
long-running activity can overlap in time.
Example 10-29
shows
how. The main trick this script employs is to split up the input and
output parts of the originalredirectedGuiShellCmd
of theguiStreams
module we met earlier in
Example 10-12
. By so doing, the
input portion can be spawned off in a parallel thread and not block
the GUI. The main GUI thread uses anafter
timer loop as usual, to watch for data
to be added by the reader thread to a shared queue. Because the main
thread doesn’t read program output itself, it does not get stuck in
wait states.
Example 10-29. PP4E\Gui\Tools\pipe_gui3.py
"""
read command pipe in a thread and place output on a queue checked in timer loop;
allows script to display program's output without being blocked between its outputs;
spawned programs need not connect or flush, but this approaches complexity of sockets
"""
import _thread as thread, queue, os
from tkinter import Tk
from PP4E.Gui.Tools.guiStreams import GuiOutput
stdoutQueue = queue.Queue() # infinite size
def producer(input):
while True:
line = input.readline() # OK to block: child thread
stdoutQueue.put(line) # empty at end-of-file
if not line: break
def consumer(output, root, term=''):
try:
line = stdoutQueue.get(block=False) # main thread: check queue
except queue.Empty: # 4 times/sec, OK if empty
pass
else:
if not line: # stop loop at end-of-file
output.write(term) # else display next line
return
output.write(line)
root.after(250, lambda: consumer(output, root, term))
def redirectedGuiShellCmd(command, root):
input = os.popen(command, 'r') # start non-GUI program
output = GuiOutput(root)
thread.start_new_thread(producer, (input,)) # start reader thread
consumer(output, root)
if __name__ == '__main__':
win = Tk()
redirectedGuiShellCmd('python -u pipe-nongui.py', win)
win.mainloop()
As usual, we use a queue here to avoid updating the GUI except
in the main thread. Note that we didn’t need a thread or queue in the
prior section’s socket example, just because we’re able to poll a
socket to see whether it has data without blocking; anafter
timer loop was enough. For a
shell-command pipe, though, a thread is an easy way to avoid
blocking.
When run, this program’s self-test code creates aScrolledText
window that displays the
current date and time sent from the
pipes-nongui.py
script in
Example 10-27
. In fact, its
window is identical to that of the prior versions (see
Figure 10-15
). The window is
updated with a new line every two seconds because that’s how often the
spawnedpipes-nongui
script prints
a message tostdout
.
Note how the producer thread callsreadline()
to load just one line at a time.
We can’t use input calls that consume the entire stream all at once
(e.g.,read()
,readlines()
), because such calls would not
return until the program exits and sends end-of-file. Theread(N)
call would work to grab one piece of
the output as well, but we assume that the output stream is text here.
Also notice that the-u
unbuffered
stream flag is used here again, to get output as it is produced;
without it, output won’t show up in the GUI at all because it is
buffered in the spawned program (try it yourself).