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.
This commit is contained in:
miruka 2020-10-29 04:40:15 -04:00
parent b94570f853
commit da75568428
3 changed files with 21 additions and 7 deletions

View File

@ -21,7 +21,7 @@ import traceback
from concurrent.futures import Future from concurrent.futures import Future
from operator import attrgetter from operator import attrgetter
from threading import Thread from threading import Thread
from typing import Coroutine, Dict, Sequence from typing import Coroutine, Dict, Sequence, Set
import pyotherside import pyotherside
@ -53,6 +53,7 @@ class QMLBridge:
self.backend: Backend = Backend() self.backend: Backend = Backend()
self._running_futures: Dict[str, Future] = {} self._running_futures: Dict[str, Future] = {}
self._cancelled_early: Set[str] = set()
Thread(target=self._start_asyncio_loop).start() Thread(target=self._start_asyncio_loop).start()
@ -78,6 +79,10 @@ class QMLBridge:
def _call_coro(self, coro: Coroutine, uuid: str) -> None: def _call_coro(self, coro: Coroutine, uuid: str) -> None:
"""Schedule a coroutine to run in our thread and return a `Future`.""" """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: def on_done(future: Future) -> None:
"""Send a PyOtherSide event with the coro's result/exception.""" """Send a PyOtherSide event with the coro's result/exception."""
result = exception = trace = None result = exception = trace = None
@ -101,7 +106,10 @@ class QMLBridge:
) -> None: ) -> None:
"""Schedule a coroutine from the `QMLBridge.backend` object.""" """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( def call_client_coro(
@ -109,17 +117,20 @@ class QMLBridge:
) -> None: ) -> None:
"""Schedule a coroutine from a `QMLBridge.backend.clients` client.""" """Schedule a coroutine from a `QMLBridge.backend.clients` client."""
client = self.backend.clients[user_id] if uuid in self._cancelled_early:
self._call_coro(attrgetter(name)(client)(*args), uuid) 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: def cancel_coro(self, uuid: str) -> None:
"""Cancel a couroutine scheduled by the `QMLBridge` methods.""" """Cancel a couroutine scheduled by the `QMLBridge` methods."""
try: if uuid in self._running_futures:
self._running_futures[uuid].cancel() self._running_futures[uuid].cancel()
except KeyError: else:
log.warning("Couldn't cancel coroutine %s, future not found", uuid) self._cancelled_early.add(uuid)
def pdb(self, additional_data: Sequence = ()) -> None: def pdb(self, additional_data: Sequence = ()) -> None:

View File

@ -35,6 +35,8 @@ QtObject {
} }
function onCoroutineDone(uuid, result, error, traceback) { function onCoroutineDone(uuid, result, error, traceback) {
if (! Globals.pendingCoroutines[uuid]) return
const onSuccess = Globals.pendingCoroutines[uuid].onSuccess const onSuccess = Globals.pendingCoroutines[uuid].onSuccess
const onError = Globals.pendingCoroutines[uuid].onError const onError = Globals.pendingCoroutines[uuid].onError

View File

@ -43,6 +43,7 @@ Python {
} }
function cancelCoro(uuid) { function cancelCoro(uuid) {
delete Globals.pendingCoroutines[uuid]
call("BRIDGE.cancel_coro", [uuid]) call("BRIDGE.cancel_coro", [uuid])
} }