PyGObject - 如何在执行期间中断 GLib 生成器函数?

PyGObject - How do I interrupt a GLib generator function during execution?

我正在开发一个简单的 python GUI,用于使用 PyGObject/Gtk + Glade 来试验 gphoto2。我改编了 Gnome 教程,它使用生成器创建伪线程,允许将更新传递到主 Gtk 线程。子进程运行 gphoto2 并将命令传递给它以从 DSLR 相机拍摄一系列照片。我已经设法让程序在两次拍摄之间更新 GUI。但是,我计划进行更长的拍摄,实现取消生成器中间任务的选项会很有用。我将如何以最简单的方式实现它?

我在本[教程]中使用了示例:https://wiki.gnome.org/Projects/PyGObject/Threading

下面是正在编写的代码的一个简单示例。实际应用要大得多。

import gi
import re
import time
import subprocess

gi.require_version("Gtk", "3.0")
from gi.repository import Gio, GLib, Gtk, GObject


def app_main():

    builder = Gtk.Builder()
    builder.add_from_file("example.glade")

    window = builder.get_object("GUI")
    window.connect("destroy", Gtk.main_quit)

    capture_button = builder.get_object("btn_capture")

    def capture():
        cmd = ['gphoto2', '--auto-detect']

        process = subprocess.Popen(cmd, stdout=subprocess.PIPE)
        output = process.stdout.read().decode('utf-8')
        process.wait()

        usb_devices = re.findall('usb:001,' + '[0-9][0-9][0-9]', output)
        working_directory = "/home/richard"

        for i in range(1, 5):
            new_port = '--port=' + usb_devices[0]
            cmd = ['gphoto2', new_port, '--capture-image', '--keep']
            process = subprocess.Popen(cmd, cwd=working_directory)
            process.wait()
            update_progress(f"Photo {i} of 5 taken!\n")

            yield True

        capture_button.set_sensitive(True)

    def update_progress(text):
        output = builder.get_object("txtview_console")
        textviewbuffer = output.get_buffer()
        start_iter = textviewbuffer.get_start_iter()
        textviewbuffer.insert(start_iter, text)
        return False

    def on_capture_clicked(button):
        capture_button.set_sensitive(False)
        time.sleep(1)
        run_generator(capture)

    def on_cancel_clicked(button):
        print("You pressed cancel!")
        # I would like this to cancel the generator some how

    def run_generator(function):
        gen = function()
        GLib.idle_add(lambda: next(gen, False), priority=GLib.PRIORITY_LOW)

    handlers = {
        "on_capture_clicked": on_capture_clicked,
        "on_cancel_clicked": on_cancel_clicked
    }

    builder.connect_signals(handlers)

    window.show_all()


if __name__ == "__main__":

    app_main()
    Gtk.main()

以下是有关如何停止线程的一些示例: https://www.google.at/amp/s/www.geeksforgeeks.org/python-different-ways-to-kill-a-thread/amp/

为了回答我自己的问题,我用如下示例解决了这个问题:

import os
import signal
import time

import gi

gi.require_version("Gtk", "3.0")
from gi.repository import GLib, Gtk, GObject
from multiprocessing import Pipe, Process

proc = 0


class GUI(Gtk.Window):

    def run_process(self, target, **kwargs):

        iterator = kwargs.get('iterator')
        option = kwargs.get('option')
        pid = kwargs.get('pid')

        parent_conn, child_conn = Pipe(duplex=False)

        if target == capture:
            Process(target=target, args=[child_conn, iterator], daemon=True).start()

        if target == manager:
            Process(target=target, args=[child_conn, option, pid], daemon=True).start()

        child_conn.close()

        def read_data(source, condition):
            assert parent_conn.poll()
            try:
                i = parent_conn.recv()
            except EOFError as E:
                print(E)
                return False

            if isinstance(i, list) and not isinstance(i[0], str):
                self.update_progress(f"Photo {i[0]} of {i[1]} taken!\n")

            elif isinstance(i, str):
                self.update_progress(i)

            elif isinstance(i, list) and i[0] == "PID":
                global proc
                proc = i[1]

            return True

        GLib.io_add_watch(parent_conn.fileno(), GObject.IO_IN, read_data)

    def update_progress(self, text):
        output = builder.get_object("txtview_console")
        textviewbuffer = output.get_buffer()
        start_iter = textviewbuffer.get_start_iter()
        textviewbuffer.insert(start_iter, text)
        return False

    def on_capture_clicked(self, button):
        btn_capture = builder.get_object("start_capture_button")
        btn_capture.set_sensitive(False)
        self.run_process(capture, iterator=[1, 11])

    def on_cancel_clicked(self, button):
        global proc

        btn_capture = builder.get_object("start_capture_button")
        btn_capture.set_sensitive(True)

        if proc == 0:
            self.update_progress("No capture running!\n")
        else:
            self.run_process(manager, option=1, pid=proc)
            proc = 0


def capture(c_conn, iterator):
    proc = os.getpid()
    c_conn.send(['PID', proc])

    for i in range(iterator[0], iterator[1]):
        # blocking method or subprocess
        time.sleep(2)
        c_conn.send([i, iterator[1]-1])

    c_conn.send(['PID', 0])


def manager(c_conn, option, pid):
    if pid == 0:
        c_conn.send("No capture running!\n")
        return False
    else:
        if option == 1:
            os.kill(pid, signal.SIGKILL)
            c_conn.send("Capture cancelled!\n")


if __name__ == "__main__":
    builder = Gtk.Builder()
    builder.add_from_file("example.glade")
    builder.connect_signals(GUI())

    win = builder.get_object("GUI")
    win.connect("delete-event", Gtk.main_quit)
    win.show_all()

    Gtk.main()

使用 glade 文件提供 GUI 如下:

<?xml version="1.0" encoding="UTF-8"?>
<!-- Generated with glade 3.22.1 -->
<interface>
  <requires lib="gtk+" version="3.20"/>
  <object class="GtkWindow" id="GUI">
    <property name="can_focus">False</property>
    <child>
      <placeholder/>
    </child>
    <child>
      <object class="GtkGrid">
        <property name="visible">True</property>
        <property name="can_focus">False</property>
        <property name="row_homogeneous">True</property>
        <property name="column_homogeneous">True</property>
        <child>
          <object class="GtkButton" id="start_capture_button">
            <property name="label" translatable="yes">Capture</property>
            <property name="visible">True</property>
            <property name="can_focus">True</property>
            <property name="receives_default">True</property>
            <signal name="clicked" handler="on_capture_clicked" swapped="no"/>
          </object>
          <packing>
            <property name="left_attach">0</property>
            <property name="top_attach">0</property>
          </packing>
        </child>
        <child>
          <object class="GtkButton" id="cancel_capture_button">
            <property name="label" translatable="yes">Cancel</property>
            <property name="visible">True</property>
            <property name="can_focus">True</property>
            <property name="receives_default">True</property>
            <signal name="clicked" handler="on_cancel_clicked" swapped="no"/>
          </object>
          <packing>
            <property name="left_attach">1</property>
            <property name="top_attach">0</property>
          </packing>
        </child>
        <child>
          <object class="GtkScrolledWindow">
            <property name="visible">True</property>
            <property name="can_focus">True</property>
            <property name="shadow_type">in</property>
            <child>
              <object class="GtkTextView" id="txtview_console">
                <property name="visible">True</property>
                <property name="can_focus">True</property>
              </object>
            </child>
          </object>
          <packing>
            <property name="left_attach">0</property>
            <property name="top_attach">1</property>
            <property name="width">2</property>
            <property name="height">2</property>
          </packing>
        </child>
      </object>
    </child>
  </object>
</interface>

上面的示例创建了一个 window,其中包含一个捕获按钮和一个取消按钮。如果 捕获过程 运行 使捕获按钮的灵敏度变为 False 并且阻止过程 运行 使用管道中的进程提供反馈,并打印到文本视图中。在捕获过程中的任何时候,如果按下取消按钮,它将尝试为捕获进程获取全局 pid 号并将其终止。