]> git.ipfire.org Git - thirdparty/bird.git/commitdiff
Python GUI: Reworked the asyncio helper completely
authorMaria Matejka <mq@ucw.cz>
Mon, 22 May 2023 10:05:52 +0000 (12:05 +0200)
committerMaria Matejka <mq@ucw.cz>
Tue, 23 May 2023 11:45:04 +0000 (13:45 +0200)
The original asyncio helper in the example is busy-waiting, playing ping-pong
with Qt scheduler for the whole time. Using a separate worker thread instead.

python/gui_view.py

index 84809a3c6efd29662c6df3aa71807353c277ad66..a93e92d17850e96e286b7177a1f5fc93ae60e3c5 100644 (file)
@@ -1,4 +1,4 @@
-from PySide6.QtCore import (Qt, QEvent, QObject, Signal, Slot)
+from PySide6.QtCore import (Qt, QEvent, QObject, QRunnable, QThreadPool, Signal, Slot)
 from PySide6.QtWidgets import (QApplication, QLabel, QMainWindow, QPushButton, QVBoxLayout, QWidget)
 
 from BIRD import BIRD
@@ -7,6 +7,24 @@ import asyncio
 import signal
 import sys
 
+# Async worker thread
+class AsyncWorker(QRunnable):
+    @Slot()
+    def run(self):
+        self.loop = asyncio.new_event_loop()
+        self.loop.run_forever()
+
+    def stop(self):
+        asyncio.run_coroutine_threadsafe(self._stop_internal(), self.loop)
+
+    async def _stop_internal(self):
+        self.loop.stop()
+
+    def dispatch(coro):
+        asyncio.run_coroutine_threadsafe(coro, AsyncWorker.worker.loop)
+
+if not hasattr(AsyncWorker, "worker"):
+    AsyncWorker.worker = AsyncWorker()
 
 
 class MainWindow(QMainWindow):
@@ -30,100 +48,28 @@ class MainWindow(QMainWindow):
         layout.addWidget(self.text, alignment=Qt.AlignmentFlag.AlignCenter)
 
         async_trigger = QPushButton(text="Connect")
-        async_trigger.clicked.connect(self.async_start)
+        async_trigger.clicked.connect(self.connect)
         layout.addWidget(async_trigger, alignment=Qt.AlignmentFlag.AlignCenter)
 
     @Slot()
-    def async_start(self):
-        self.start_signal.emit()
-
-    async def bird_connect(self):
-        async with self.bird as b:
-            await b.version.update()
-            await b.status.update()
-
-            self.text.setText(f"Connected to {b.version.name} {b.version.version}")
-
-        self.done_signal.emit()
-
-class AsyncHelper(QObject):
-
-    class ReenterQtObject(QObject):
-        """ This is a QObject to which an event will be posted, allowing
-            asyncio to resume when the event is handled. event.fn() is
-            the next entry point of the asyncio event loop. """
-        def event(self, event):
-            if event.type() == QEvent.Type.User + 1:
-                event.fn()
-                return True
-            return False
-
-    class ReenterQtEvent(QEvent):
-        """ This is the QEvent that will be handled by the ReenterQtObject.
-            self.fn is the next entry point of the asyncio event loop. """
-        def __init__(self, fn):
-            super().__init__(QEvent.Type(QEvent.Type.User + 1))
-            self.fn = fn
-
-    def __init__(self, worker, entry):
-        super().__init__()
-        self.reenter_qt = self.ReenterQtObject()
-        self.entry = entry
-        self.loop = asyncio.new_event_loop()
-        self.done = False
+    def connect(self):
+        async def f():
+            async with self.bird as b:
+                await b.version.update()
+                await b.status.update()
 
-        self.worker = worker
-        if hasattr(self.worker, "start_signal") and isinstance(self.worker.start_signal, Signal):
-            self.worker.start_signal.connect(self.on_worker_started)
-        if hasattr(self.worker, "done_signal") and isinstance(self.worker.done_signal, Signal):
-            self.worker.done_signal.connect(self.on_worker_done)
+                self.text.setText(f"Connected to {b.version.name} {b.version.version}")
 
-    @Slot()
-    def on_worker_started(self):
-        """ To use asyncio and Qt together, one must run the asyncio
-            event loop as a "guest" inside the Qt "host" event loop. """
-        if not self.entry:
-            raise Exception("No entry point for the asyncio event loop was set.")
-        asyncio.set_event_loop(self.loop)
-        self.loop.create_task(self.entry())
-        self.loop.call_soon(self.next_guest_run_schedule)
-        self.done = False  # Set this explicitly as we might want to restart the guest run.
-        self.loop.run_forever()
-
-    @Slot()
-    def on_worker_done(self):
-        """ When all our current asyncio tasks are finished, we must end
-            the "guest run" lest we enter a quasi idle loop of switching
-            back and forth between the asyncio and Qt loops. We can
-            launch a new guest run by calling launch_guest_run() again. """
-        self.done = True
-
-    def continue_loop(self):
-        """ This function is called by an event posted to the Qt event
-            loop to continue the asyncio event loop. """
-        if not self.done:
-            self.loop.call_soon(self.next_guest_run_schedule)
-            self.loop.run_forever()
-
-    def next_guest_run_schedule(self):
-        """ This function serves to pause and re-schedule the guest
-            (asyncio) event loop inside the host (Qt) event loop. It is
-            registered in asyncio as a callback to be called at the next
-            iteration of the event loop. When this function runs, it
-            first stops the asyncio event loop, then by posting an event
-            on the Qt event loop, it both relinquishes to Qt's event
-            loop and also schedules the asyncio event loop to run again.
-            Upon handling this event, a function will be called that
-            resumes the asyncio event loop. """
-        self.loop.stop()
-        QApplication.postEvent(self.reenter_qt, self.ReenterQtEvent(self.continue_loop))
+        AsyncWorker.dispatch(f())
 
 if __name__ == "__main__":
     app = QApplication(sys.argv)
-    main_window = MainWindow()
-    async_helper = AsyncHelper(main_window, main_window.bird_connect)
+    threadpool = QThreadPool()
+    threadpool.start(AsyncWorker.worker)
 
-    main_window.show()
+    mainwindow = MainWindow()
+    mainwindow.show()
 
     signal.signal(signal.SIGINT, signal.SIG_DFL)
     app.exec()
+    AsyncWorker.worker.stop()