gobject и subprocess.Popen для связи в графическом интерфейсе GTK

Я пытаюсь использовать объект для обеспечения связи между процессом Popen и графическим интерфейсом GTK.

Вдохновленный этим: https://pygabriel.wordpress.com/2009/07/27/redirecting-the-stdout-on-a-gtk-textview/#comment-156

Я реализовал что-то похожее на это:

http://hartree.altervista.org/files/command-textview.py

но я заметил, что gobject использует много циклов ЦП даже после завершения процесса Popen. Просто запустите приведенный выше скрипт и посмотрите на системный монитор Ubuntu.

После некоторой работы с "pty" я придумал это:

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()

У меня есть вопросы:

  • это хорошая идея? Можно ли его использовать и для Windows?

  • можно ли избежать использования pty и просто использовать stdout и не иметь проблемы с высокой загрузкой ЦП?

  • если вы запустите этот скрипт в первый раз, он, кажется, буферизует вывод txt и дает неполный вывод.

Спасибо вам за помощь


person Fabrizio    schedule 09.05.2012    source источник


Ответы (2)


Это для людей, которые наткнулись на этот пост после 2016 года и пытались переписать его на 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()
person rho    schedule 19.07.2016

Используйте небуферизованное чтение с os.read, для этого требуется фактический файловый дескриптор. Ваш fd не является реальным файловым дескриптором, это файловый объект; обычно называют ф.

Если вы хотите убедиться, что процесс мертв, используйте os.kill.

person Ali Afshar    schedule 15.05.2012
comment
Не могли бы вы подробнее рассказать о том, каким должно быть решение? На самом деле я подозреваю, что команда self.proc.kill() на самом деле не убивает процесс, потому что я использую shell=True. Возможный? - person Fabrizio; 15.05.2012
comment
Этот пример не работает, например, если cmd='ls -R /'. И чтобы заставить его работать, вам может понадобиться shell=False, и в этом случае кнопка остановки не будет работать. Итог, не такой уж хороший пример взаимодействия процесса pygtk с графическим интерфейсом. - person Fabrizio; 10.05.2013