Fabrizio
Fabrizio

Reputation: 457

gobject and subprocess.Popen to communicate in a GTK GUI

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:

Thank you for the help

Upvotes: 1

Views: 1830

Answers (2)

rho
rho

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

Ali Afshar
Ali Afshar

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

Related Questions