Skip to content

Commit

Permalink
Reintroduced general exception handler
Browse files Browse the repository at this point in the history
  • Loading branch information
qstokkink committed Sep 6, 2024
1 parent 97c4bd9 commit 7b62d6c
Show file tree
Hide file tree
Showing 5 changed files with 55 additions and 10 deletions.
7 changes: 6 additions & 1 deletion src/tribler/core/restapi/events_endpoint.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import time
from asyncio import CancelledError, Event, Queue
from contextlib import suppress
from traceback import format_exception
from typing import TYPE_CHECKING, TypedDict

import marshmallow.fields
Expand All @@ -23,6 +24,7 @@
Notification.tunnel_removed,
Notification.watch_folder_corrupt_file,
Notification.tribler_new_version,
Notification.tribler_exception,
Notification.torrent_finished,
Notification.torrent_health_updated,
Notification.tribler_shutdown_state,
Expand Down Expand Up @@ -112,7 +114,10 @@ def error_message(self, reported_error: Exception) -> MessageDict:
"""
return {
"topic": Notification.tribler_exception.value.name,
"kwargs": {"error": str(reported_error)},
"kwargs": {
"error": str(reported_error),
"traceback": "".join(format_exception(type(reported_error), reported_error,
reported_error.__traceback__))},
}

def encode_message(self, message: MessageDict) -> bytes:
Expand Down
36 changes: 34 additions & 2 deletions src/tribler/core/session.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,10 @@

import asyncio
import logging
from asyncio import Event
import sys
from asyncio import AbstractEventLoop, Event
from contextlib import contextmanager
from typing import TYPE_CHECKING, Generator
from typing import TYPE_CHECKING, Any, Generator, Type, cast

import aiohttp
from ipv8.loader import IPv8CommunityLoader
Expand Down Expand Up @@ -38,6 +39,8 @@
from tribler.core.socks5.server import Socks5Server

if TYPE_CHECKING:
from types import TracebackType

from tribler.core.database.store import MetadataStore
from tribler.core.database.tribler_database import TriblerDatabase
from tribler.core.torrent_checker.torrent_checker import TorrentChecker
Expand Down Expand Up @@ -148,6 +151,34 @@ def register_rest_endpoints(self) -> None:
self.rest_manager.add_endpoint(StatisticsEndpoint())
self.rest_manager.add_endpoint(TorrentInfoEndpoint(self.download_manager))

def _except_hook(self, typ: Type[BaseException], value: BaseException, traceback: TracebackType | None) -> None:
"""
Handle an uncaught exception.
Note: at this point the REST interface is available.
Note2: ignored BaseExceptions are BaseExceptionGroup, GeneratorExit, KeyboardInterrupt and SystemExit
"""
if isinstance(value, Exception):
cast(EventsEndpoint, self.rest_manager.get_endpoint("/api/events")).on_tribler_exception(value)

def _asyncio_except_hook(self, loop: AbstractEventLoop, context: dict[str, Any]) -> None:
"""
Handle an uncaught asyncio exception.
Note: at this point the REST interface is available.
"""
exc = context.get("exception")
if isinstance(exc, Exception):
cast(EventsEndpoint, self.rest_manager.get_endpoint("/api/events")).on_tribler_exception(exc)
raise exc

def attach_exception_handler(self) -> None:
"""
Hook ourselves in as the general exception handler.
"""
sys.excepthook = self._except_hook
asyncio.get_running_loop().set_exception_handler(self._asyncio_except_hook)

async def start(self) -> None:
"""
Initialize and launch all components and REST endpoints.
Expand All @@ -157,6 +188,7 @@ async def start(self) -> None:

# REST (1/2)
await self.rest_manager.start()
self.attach_exception_handler()

# Libtorrent
for server in self.socks_servers:
Expand Down
11 changes: 6 additions & 5 deletions src/tribler/test_unit/core/restapi/test_events_endpoint.py
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,7 @@ async def test_establish_connection_with_error(self) -> None:

self.assertEqual(200, response.status)
self.assertEqual((b'event: tribler_exception\n'
b'data: {"error": "test message"}'
b'data: {"error": "test message", "traceback": "ValueError: test message\\n"}'
b'\n\n'), request.payload_writer.captured[1])

async def test_forward_error(self) -> None:
Expand All @@ -133,7 +133,7 @@ async def test_forward_error(self) -> None:
self.assertEqual(200, response.status)
self.assertIsNone(self.endpoint.undelivered_error)
self.assertEqual((b'event: tribler_exception\n'
b'data: {"error": "test message"}'
b'data: {"error": "test message", "traceback": "ValueError: test message\\n"}'
b'\n\n'), request.payload_writer.captured[1])

async def test_error_before_connection(self) -> None:
Expand Down Expand Up @@ -175,9 +175,10 @@ async def test_send_event_illegal_chars(self) -> None:

self.assertEqual(200, response.status)
self.assertIsNone(self.endpoint.undelivered_error)
self.assertEqual((b'event: tribler_exception\n'
b'data: {"error": "Object of type bytes is not JSON serializable"}'
b'\n\n'), request.payload_writer.captured[1])
self.assertTrue(request.payload_writer.captured[1].startswith(
b'event: tribler_exception\n'
b'data: {"error": "Object of type bytes is not JSON serializable", "traceback": "'
))

async def test_forward_notification(self) -> None:
"""
Expand Down
2 changes: 1 addition & 1 deletion src/tribler/ui/src/services/reporting.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ export function handleHTTPError(error: Error | AxiosError) {
if (axios.isAxiosError(error) && error.response?.data?.error?.message){
error_popup_text.textContent = error.response.data.error.message.replace(/(?:\n)/g, '\r\n');
} else {
error_popup_text.textContent = `${error}`;
error_popup_text.textContent = error.message;
}
const error_popup = document.querySelector("#error_popup");
if (error_popup && error_popup.classList.contains("hidden")) {
Expand Down
9 changes: 8 additions & 1 deletion src/tribler/ui/src/services/tribler.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,16 @@ import { File as BTFile } from "@/models/file.model";
import { Path } from "@/models/path.model";
import { GuiSettings, Settings } from "@/models/settings.model";
import { Torrent } from "@/models/torrent.model";
import axios, { AxiosInstance } from "axios";
import axios, { AxiosError, AxiosInstance } from "axios";
import { handleHTTPError } from "./reporting";


const OnError = (event: MessageEvent) => {
const data = JSON.parse(event.data);
handleHTTPError(new Error(data.traceback));
};


export class TriblerService {
private http: AxiosInstance;
private baseURL = "/api";
Expand All @@ -22,6 +28,7 @@ export class TriblerService {
});
this.http.interceptors.response.use(function (response) { return response; }, handleHTTPError);
this.events = new EventSource(this.baseURL + '/events', { withCredentials: true });
this.addEventListener("tribler_exception", OnError);
// Gets the GuiSettings
this.getSettings();
}
Expand Down

0 comments on commit 7b62d6c

Please sign in to comment.