From da755684282b632dd2d31cb2a6b397a338b009fd Mon Sep 17 00:00:00 2001 From: miruka Date: Thu, 29 Oct 2020 04:40:15 -0400 Subject: [PATCH] Fix segfault on coroutine cancelling from QML Since 87fcb0a773f4855cdae7212fa9448a05de57be56, it was possible to call a Python coroutine, but cancel it (due to parent component destruction) before Python even has time to start it. The registred QML callbacks for this coro would then be called, potentially causing a segfault if that callback tried to access the parent component or its properties. --- src/backend/qml_bridge.py | 25 ++++++++++++++++++------- src/gui/PythonBridge/EventHandlers.qml | 2 ++ src/gui/PythonBridge/PythonBridge.qml | 1 + 3 files changed, 21 insertions(+), 7 deletions(-) diff --git a/src/backend/qml_bridge.py b/src/backend/qml_bridge.py index 848bf728..c3f94f5e 100644 --- a/src/backend/qml_bridge.py +++ b/src/backend/qml_bridge.py @@ -21,7 +21,7 @@ import traceback from concurrent.futures import Future from operator import attrgetter from threading import Thread -from typing import Coroutine, Dict, Sequence +from typing import Coroutine, Dict, Sequence, Set import pyotherside @@ -53,6 +53,7 @@ class QMLBridge: self.backend: Backend = Backend() self._running_futures: Dict[str, Future] = {} + self._cancelled_early: Set[str] = set() Thread(target=self._start_asyncio_loop).start() @@ -78,6 +79,10 @@ class QMLBridge: def _call_coro(self, coro: Coroutine, uuid: str) -> None: """Schedule a coroutine to run in our thread and return a `Future`.""" + if uuid in self._cancelled_early: + self._cancelled_early.remove(uuid) + return + def on_done(future: Future) -> None: """Send a PyOtherSide event with the coro's result/exception.""" result = exception = trace = None @@ -101,7 +106,10 @@ class QMLBridge: ) -> None: """Schedule a coroutine from the `QMLBridge.backend` object.""" - self._call_coro(attrgetter(name)(self.backend)(*args), uuid) + if uuid in self._cancelled_early: + self._cancelled_early.remove(uuid) + else: + self._call_coro(attrgetter(name)(self.backend)(*args), uuid) def call_client_coro( @@ -109,17 +117,20 @@ class QMLBridge: ) -> None: """Schedule a coroutine from a `QMLBridge.backend.clients` client.""" - client = self.backend.clients[user_id] - self._call_coro(attrgetter(name)(client)(*args), uuid) + if uuid in self._cancelled_early: + self._cancelled_early.remove(uuid) + else: + client = self.backend.clients[user_id] + self._call_coro(attrgetter(name)(client)(*args), uuid) def cancel_coro(self, uuid: str) -> None: """Cancel a couroutine scheduled by the `QMLBridge` methods.""" - try: + if uuid in self._running_futures: self._running_futures[uuid].cancel() - except KeyError: - log.warning("Couldn't cancel coroutine %s, future not found", uuid) + else: + self._cancelled_early.add(uuid) def pdb(self, additional_data: Sequence = ()) -> None: diff --git a/src/gui/PythonBridge/EventHandlers.qml b/src/gui/PythonBridge/EventHandlers.qml index 96850f79..2349836b 100644 --- a/src/gui/PythonBridge/EventHandlers.qml +++ b/src/gui/PythonBridge/EventHandlers.qml @@ -35,6 +35,8 @@ QtObject { } function onCoroutineDone(uuid, result, error, traceback) { + if (! Globals.pendingCoroutines[uuid]) return + const onSuccess = Globals.pendingCoroutines[uuid].onSuccess const onError = Globals.pendingCoroutines[uuid].onError diff --git a/src/gui/PythonBridge/PythonBridge.qml b/src/gui/PythonBridge/PythonBridge.qml index 029fe8a3..99a9e3e1 100644 --- a/src/gui/PythonBridge/PythonBridge.qml +++ b/src/gui/PythonBridge/PythonBridge.qml @@ -43,6 +43,7 @@ Python { } function cancelCoro(uuid) { + delete Globals.pendingCoroutines[uuid] call("BRIDGE.cancel_coro", [uuid]) }