GUI popup for uncaught asyncio loop exceptions
This commit is contained in:
		
							
								
								
									
										4
									
								
								TODO.md
									
									
									
									
									
								
							
							
						
						
									
										4
									
								
								TODO.md
									
									
									
									
									
								
							@@ -3,9 +3,6 @@
 | 
				
			|||||||
- Make dark bar extend down pane
 | 
					- Make dark bar extend down pane
 | 
				
			||||||
- Verify default size
 | 
					- Verify default size
 | 
				
			||||||
 | 
					
 | 
				
			||||||
- catch py unretrieved exception
 | 
					 | 
				
			||||||
- call default handler from signin & others
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
## Media
 | 
					## Media
 | 
				
			||||||
 | 
					
 | 
				
			||||||
- nio ClientTimeout
 | 
					- nio ClientTimeout
 | 
				
			||||||
@@ -55,7 +52,6 @@
 | 
				
			|||||||
- `EventImage`s for `m.image` sometimes appear broken, can be made normal
 | 
					- `EventImage`s for `m.image` sometimes appear broken, can be made normal
 | 
				
			||||||
  by switching to another room and coming back
 | 
					  by switching to another room and coming back
 | 
				
			||||||
- First sent message in E2E room is sometimes undecryptable
 | 
					- 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
 | 
					- Pause upload, switch to other room, then come back → wrong state displayed
 | 
				
			||||||
- Pausing uploads doesn't work well, servers end up dropping the connection 
 | 
					- Pausing uploads doesn't work well, servers end up dropping the connection 
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -49,6 +49,15 @@ class CoroutineDone(PyOtherSideEvent):
 | 
				
			|||||||
    traceback: Optional[str]       = None
 | 
					    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
 | 
					@dataclass
 | 
				
			||||||
class ModelUpdated(PyOtherSideEvent):
 | 
					class ModelUpdated(PyOtherSideEvent):
 | 
				
			||||||
    """Indicate that a backend `Model`'s data changed."""
 | 
					    """Indicate that a backend `Model`'s data changed."""
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -12,7 +12,7 @@ from threading import Thread
 | 
				
			|||||||
from typing import Coroutine, Sequence
 | 
					from typing import Coroutine, Sequence
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from .backend import Backend
 | 
					from .backend import Backend
 | 
				
			||||||
from .pyotherside_events import CoroutineDone
 | 
					from .pyotherside_events import CoroutineDone, LoopException
 | 
				
			||||||
 | 
					
 | 
				
			||||||
try:
 | 
					try:
 | 
				
			||||||
    import uvloop
 | 
					    import uvloop
 | 
				
			||||||
@@ -39,9 +39,24 @@ class QMLBridge:
 | 
				
			|||||||
        self.backend: Backend = Backend()
 | 
					        self.backend: Backend = Backend()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        self._loop = asyncio.get_event_loop()
 | 
					        self._loop = asyncio.get_event_loop()
 | 
				
			||||||
 | 
					        self._loop.set_exception_handler(self._loop_exception_handler)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        Thread(target=self._start_asyncio_loop).start()
 | 
					        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:
 | 
					    def _start_asyncio_loop(self) -> None:
 | 
				
			||||||
        asyncio.set_event_loop(self._loop)
 | 
					        asyncio.set_event_loop(self._loop)
 | 
				
			||||||
        self._loop.run_forever()
 | 
					        self._loop.run_forever()
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -16,12 +16,12 @@ BoxPopup {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    property string errorType
 | 
					    property string errorType
 | 
				
			||||||
    property var errorArguments: []
 | 
					    property string message: ""
 | 
				
			||||||
    property string traceback: ""
 | 
					    property string traceback: ""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    HScrollableTextArea {
 | 
					    HScrollableTextArea {
 | 
				
			||||||
        text: traceback || qsTr("No traceback available")
 | 
					        text: [message, traceback].join("\n\n") || qsTr("No info available")
 | 
				
			||||||
        area.readOnly: true
 | 
					        area.readOnly: true
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        Layout.fillWidth: true
 | 
					        Layout.fillWidth: true
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -47,7 +47,7 @@ QtObject {
 | 
				
			|||||||
            utils.makePopup(
 | 
					            utils.makePopup(
 | 
				
			||||||
                "Popups/UnexpectedErrorPopup.qml",
 | 
					                "Popups/UnexpectedErrorPopup.qml",
 | 
				
			||||||
                window,
 | 
					                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) {
 | 
					    function onModelUpdated(syncId, data, serializedSyncId) {
 | 
				
			||||||
        if (serializedSyncId === "Account" || serializedSyncId[0] === "Room") {
 | 
					        if (serializedSyncId === "Account" || serializedSyncId[0] === "Room") {
 | 
				
			||||||
            py.callCoro("get_flat_mainpane_data", [], data => {
 | 
					            py.callCoro("get_flat_mainpane_data", [], data => {
 | 
				
			||||||
 
 | 
				
			|||||||
		Reference in New Issue
	
	Block a user