diff --git a/TODO.md b/TODO.md index 187fc08e..d084897b 100644 --- a/TODO.md +++ b/TODO.md @@ -3,9 +3,6 @@ - Make dark bar extend down pane - Verify default size -- catch py unretrieved exception -- call default handler from signin & others - ## Media - nio ClientTimeout @@ -55,7 +52,6 @@ - `EventImage`s for `m.image` sometimes appear broken, can be made normal by switching to another room and coming back - First sent message in E2E room is sometimes undecryptable -- Handle matrix errors for accept/decline invite buttons and other - Pause upload, switch to other room, then come back → wrong state displayed - Pausing uploads doesn't work well, servers end up dropping the connection diff --git a/src/backend/pyotherside_events.py b/src/backend/pyotherside_events.py index d4ef448d..b2679dac 100644 --- a/src/backend/pyotherside_events.py +++ b/src/backend/pyotherside_events.py @@ -49,6 +49,15 @@ class CoroutineDone(PyOtherSideEvent): traceback: Optional[str] = None +@dataclass +class LoopException(PyOtherSideEvent): + """Indicate that an uncaught exception occured in the asyncio loop.""" + + message: str = field() + exception: Optional[Exception] = field() + traceback: Optional[str] = None + + @dataclass class ModelUpdated(PyOtherSideEvent): """Indicate that a backend `Model`'s data changed.""" diff --git a/src/backend/qml_bridge.py b/src/backend/qml_bridge.py index 1042ac59..30093f27 100644 --- a/src/backend/qml_bridge.py +++ b/src/backend/qml_bridge.py @@ -12,7 +12,7 @@ from threading import Thread from typing import Coroutine, Sequence from .backend import Backend -from .pyotherside_events import CoroutineDone +from .pyotherside_events import CoroutineDone, LoopException try: import uvloop @@ -39,9 +39,24 @@ class QMLBridge: self.backend: Backend = Backend() self._loop = asyncio.get_event_loop() + self._loop.set_exception_handler(self._loop_exception_handler) + Thread(target=self._start_asyncio_loop).start() + def _loop_exception_handler( + self, loop: asyncio.AbstractEventLoop, context: dict, + ) -> None: + if "exception" in context: + err = context["exception"] + trace = "".join( + traceback.format_exception(type(err), err, err.__traceback__), + ) + LoopException(context["message"], err, trace) + + loop.default_exception_handler(context) + + def _start_asyncio_loop(self) -> None: asyncio.set_event_loop(self._loop) self._loop.run_forever() diff --git a/src/gui/Popups/UnexpectedErrorPopup.qml b/src/gui/Popups/UnexpectedErrorPopup.qml index b437201e..9e04de98 100644 --- a/src/gui/Popups/UnexpectedErrorPopup.qml +++ b/src/gui/Popups/UnexpectedErrorPopup.qml @@ -16,12 +16,12 @@ BoxPopup { property string errorType - property var errorArguments: [] + property string message: "" property string traceback: "" HScrollableTextArea { - text: traceback || qsTr("No traceback available") + text: [message, traceback].join("\n\n") || qsTr("No info available") area.readOnly: true Layout.fillWidth: true diff --git a/src/gui/PythonBridge/EventHandlers.qml b/src/gui/PythonBridge/EventHandlers.qml index dcab6731..18b78d09 100644 --- a/src/gui/PythonBridge/EventHandlers.qml +++ b/src/gui/PythonBridge/EventHandlers.qml @@ -47,7 +47,7 @@ QtObject { utils.makePopup( "Popups/UnexpectedErrorPopup.qml", window, - { errorType: type, errorArguments: args, traceback }, + { errorType: type, traceback }, ) } @@ -55,6 +55,25 @@ QtObject { } + function onLoopException(message, error, traceback) { + // No need to log these here, the asyncio exception handler does it + const type = py.getattr(py.getattr(error, "__class__"), "__name__") + + if (window.hideErrorTypes.has(type)) { + console.warn( + "Not showing error popup for this type due to user choice" + ) + return + } + + utils.makePopup( + "Popups/UnexpectedErrorPopup.qml", + window, + { errorType: type, message, traceback }, + ) + } + + function onModelUpdated(syncId, data, serializedSyncId) { if (serializedSyncId === "Account" || serializedSyncId[0] === "Room") { py.callCoro("get_flat_mainpane_data", [], data => {