Reputation: 457
I am trying to use a gobject to allow communication between a Popen process and a GTK GUI.
Inspired by this: https://pygabriel.wordpress.com/2009/07/27/redirecting-the-stdout-on-a-gtk-textview/#comment-156
I implemented something similar to this:
http://hartree.altervista.org/files/command-textview.py
but I noticed that the gobject uses lots of CPU cycle even once the Popen process is terminated. Just run the scrip above and watch the Ubuntu System Monitor.
After some work with "pty" I came up with this:
import gtk,pygtk
import subprocess
import gobject
import pty, os, time
class CommandTextView(gtk.TextView):
def __init__(self):
super(CommandTextView,self).__init__()
self.master, self.slave = pty.openpty()
gobject.io_add_watch(os.fdopen(self.master), gobject.IO_IN, self.write_to_buffer)
self.proc = None
def run(self, w, cmd):
if self.proc == None or self.proc.poll() != None: # poll()=None means still running
self.proc = subprocess.Popen(cmd.split(), shell=True, stdout=self.slave, stderr=self.slave)
def stop(self,w):
if type(self.proc) is subprocess.Popen:
self.proc.kill()
while self.proc.poll() == None:
time.sleep(0.1)
self.proc = None
def write_to_buffer(self, fd, condition):
if condition == gobject.IO_IN:
char = fd.readline()
print 'adding:',char
buf = self.get_buffer()
buf.insert_at_cursor(char)
return True
else:
return False
def test():
win=gtk.Window()
vbox = gtk.VBox(False, 0)
win.set_size_request(300,300)
win.connect('delete-event',lambda w,e : gtk.main_quit())
ctv=CommandTextView()
bt1 = gtk.Button('Run')
bt2 = gtk.Button('Stop')
vbox.pack_start(ctv)
vbox.pack_end(bt2,False,False)
vbox.pack_end(bt1,False,False)
win.add(vbox)
bt1.connect("clicked", ctv.run, 'ls -la')
bt2.connect("clicked", ctv.stop)
win.show_all()
gtk.main()
if __name__=='__main__': test()
The questions I have are:
is pty a good idea? Can it be used for Windows too?
is it possible to avoid using pty and just use stdout and not have the high CPU usage problem?
if you run this scrip the first time it seems to buffer the txt output and give an incomplete output.
Thank you for the help
Upvotes: 1
Views: 1830
Reputation: 807
This is for the people who have stumble to this post in after 2016 and was trying to rewrite it into Gtk3.
#!/usr/bin/env python3
import gi
gi.require_version('Gtk', '3.0')
from gi.repository import Gtk
from gi.repository import GObject
import os
import fcntl
import subprocess
def unblock_fd(stream):
fd = stream.fileno()
fl = fcntl.fcntl(fd, fcntl.F_GETFL)
fcntl.fcntl(fd, fcntl.F_SETFL, fl | os.O_NONBLOCK)
class StreamTextBuffer(Gtk.TextBuffer):
'''TextBuffer read command output syncronously'''
def __init__(self):
Gtk.TextBuffer.__init__(self)
self.IO_WATCH_ID = tuple()
def bind_subprocess(self, proc):
unblock_fd(proc.stdout)
watch_id_stdout = GObject.io_add_watch(
channel = proc.stdout,
priority_ = GObject.IO_IN,
condition = self.buffer_update,
# func = lambda *a: print("func") # when the condition is satisfied
# user_data = # user data to pass to func
)
unblock_fd(proc.stderr)
watch_id_stderr = GObject.io_add_watch(
channel = proc.stderr,
priority_ = GObject.IO_IN,
condition = self.buffer_update,
# func = lambda *a: print("func") # when the condition is satisfied
# user_data = # user data to pass to func
)
self.IO_WATCH_ID = (watch_id_stdout, watch_id_stderr)
return self.IO_WATCH_ID
def buffer_update(self, stream, condition):
self.insert_at_cursor(stream.read())
return True # otherwise isn't recalled
def sample():
root = Gtk.Window()
root.set_default_size(400, 260)
root.connect("destroy", Gtk.main_quit)
root.connect( # quit when Esc is pressed
'key_release_event',
lambda w, e: Gtk.main_quit() if e.keyval == 65307 else None
)
layout = Gtk.Box(orientation=1)
scroll = Gtk.ScrolledWindow()
layout.pack_start(scroll, expand=1, fill=1, padding=0)
buff = StreamTextBuffer()
textview = Gtk.TextView.new_with_buffer(buff)
scroll.add(textview)
button_start = Gtk.Button("Execute Command")
layout.add(button_start)
def on_click(widget):
if len(buff.IO_WATCH_ID):
for id_ in buff.IO_WATCH_ID:
# remove subprocess io_watch if not removed will
# creates lots of cpu cycles, when process dies
GObject.source_remove(id_)
buff.IO_WATCH_ID = tuple()
on_click.proc.terminate() # send SIGTERM
widget.set_label("Execute Command")
return
on_click.proc = subprocess.Popen(
[ 'ping', '-c', '3', 'localhost' ],
stdout = subprocess.PIPE,
stderr = subprocess.PIPE,
universal_newlines=True,
)
buff.bind_subprocess(on_click.proc)
widget.set_label("STOP!")
button_start.connect("clicked", on_click)
root.add(layout)
root.show_all()
if __name__ == "__main__":
sample()
Gtk.main()
Upvotes: 2
Reputation: 41643
Use unbuffered reading with os.read
, it takes an actual file descriptor. Your fd is not a real file descriptor, it is a file object; usually called f.
If you want to be sure the process is dead, use os.kill.
Upvotes: 0