使用 PyGTK 实现异步输出阻塞问题及解决方案

83 阅读2分钟

使用 PyGTK 创建一个具有 python 命令行仿真器的 GTK+ GUI 程序。此 python 命令行使用 gtk.TextView 实现,可用于输出打印结果(以及从 TextView 读取命令并执行它们,但此处不涉及输入部分,因为与问题无关)。

huake_00210_.jpg 使用以下技术在真实终端和 python 命令行之间分叉 stdout 流:

r_out, w_out = os.pipe() # 创建管道,因为 sys.stdout 不可读,因此无法轮询它以获取可用输出
w_out_stream = os.fdopen(w_out, "w", 0) # 以 0 长度缓冲区打开管道的写端
sys.stdout = w_out_stream # 现在打印和 sys.stdout.writes 将进入管道
watch = gobject.io_add_watch(r_out, gobject.IO_IN, stdout_callback) # 轮询管道的数据读端并调用 stdout_callback
def stdout_callback(stream, condition):
    data = os.read(stream, 1) # 以字节为单位读取管道
    iter = textbuf.get_end_iter() # textbuf 是 TextView 的缓冲区,将其用作 python 命令行
    textbuf.insert(iter, data) # 将 stdout 输出到 GUI
    sys.__stdout__.write(data) # 将 stdout 输出到“真实”终端 stdout
gtk.main()

这种方法适用于小输出。但不幸的是,当输出相对较大(如数千字节)时,我的应用程序会挂起,且不会显示任何输出。

但是,如果我发送一个 SIGINT,我的输出就会同时出现在 GUI 和真实终端中。显然,我希望在没有 SIGINT 的情况下也能实现这一点。您有什么想法,是什么导致了这样的阻塞?

2、解决方案

外部进程在使用 os.read 时会阻塞 UI,您应该使用 glib.spawn_async 生成进程,并使用 IOChannel 读取输入,类似如下:

from gi.repository import Gtk, GLib

class MySpawned(Gtk.Window):
    def __init__(self):
        Gtk.Window.__init__(self)

        vb = Gtk.VBox(False, 5)
        self.tw = Gtk.TextView()
        bt = Gtk.Button('Run')
        bt.connect('clicked', self.process)

        vb.pack_start(self.tw, True, True, 0)
        vb.pack_start(bt, False, False, 0)

        self.add(vb)
        self.set_size_request(200, 300)
        self.connect('delete-event', Gtk.main_quit)
        self.show_all()

    def run(self):
        Gtk.main()

    def process(self, widget, data=None):
        params = ['python', '-h']

        def write_to_textview(io, condition):
            print condition
            if condition is GLib.IO_IN:
               line = io.readline()
               self.tw.props.buffer.insert_at_cursor(line)
               return True
            elif condition is GLib.IO_HUP|GLib.IO_IN:
                GLib.source_remove(self.source_id)
                return False

        self.source_id = None

        pid, stdin, stdout, stderr = GLib.spawn_async(params,
                                        flags=GLib.SpawnFlags.SEARCH_PATH,                                       
                                        standard_output=True)

        io = GLib.IOChannel(stdout)

        self.source_id = io.add_watch(GLib.IO_IN|GLib.IO_HUP,
                                 write_to_textview,
                                 priority=GLib.PRIORITY_HIGH)

if __name__ == '__main__':
    s = MySpawned()
    s.run()

有很多线程告诉你可以使用线程或其他东西,请不要这样做,上面的例子即使在长进程中也不会阻塞你的 UI,并且输出将被打印在 textview 上,在 windows 上也同样有效(但它会在 GLib 中的错误得到修复之前打开一个丑陋的控制台窗口)。