implement fetch and external resource request apis

This commit is contained in:
AAGaming
2024-05-04 22:39:30 -04:00
parent 2a22f000c1
commit 14ea7b964f
14 changed files with 609 additions and 525 deletions
+1
View File
@@ -1,6 +1,7 @@
{ {
"deckip" : "0.0.0.0", "deckip" : "0.0.0.0",
"deckport" : "22", "deckport" : "22",
"deckuser" : "deck",
"deckpass" : "ssap", "deckpass" : "ssap",
"deckkey" : "-i ${env:HOME}/.ssh/id_rsa", "deckkey" : "-i ${env:HOME}/.ssh/id_rsa",
"deckdir" : "/home/deck" "deckdir" : "/home/deck"
+4 -4
View File
@@ -41,7 +41,7 @@
"deploy" "deploy"
], ],
"detail": "Check for local runs, create a plugins folder", "detail": "Check for local runs, create a plugins folder",
"command": "ssh deck@${config:deckip} -p ${config:deckport} ${config:deckkey} 'python -m ensurepip && python -m pip install --user --upgrade poetry && cd ${config:deckdir}/homebrew/dev/pluginloader/backend && python -m poetry install'", "command": "ssh ${config:deckuser}@${config:deckip} -p ${config:deckport} ${config:deckkey} 'python -m ensurepip && python -m pip install --user --upgrade poetry && cd ${config:deckdir}/homebrew/dev/pluginloader/backend && python -m poetry install'",
"problemMatcher": [] "problemMatcher": []
}, },
{ {
@@ -100,7 +100,7 @@
"dependsOn": [ "dependsOn": [
"checkforsettings" "checkforsettings"
], ],
"command": "ssh deck@${config:deckip} -p ${config:deckport} ${config:deckkey} 'mkdir -p ${config:deckdir}/homebrew/dev/pluginloader && mkdir -p ${config:deckdir}/homebrew/plugins'", "command": "ssh ${config:deckuser}@${config:deckip} -p ${config:deckport} ${config:deckkey} 'mkdir -p ${config:deckdir}/homebrew/dev/pluginloader && mkdir -p ${config:deckdir}/homebrew/plugins'",
"problemMatcher": [] "problemMatcher": []
}, },
{ {
@@ -108,7 +108,7 @@
"detail": "Deploy dev PluginLoader to deck", "detail": "Deploy dev PluginLoader to deck",
"type": "shell", "type": "shell",
"group": "none", "group": "none",
"command": "rsync -azp --delete --rsh='ssh -p ${config:deckport} ${config:deckkey}' --exclude='.git/' --exclude='.github/' --exclude='.vscode/' --exclude='frontend/' --exclude='dist/' --exclude='contrib/' --exclude='*.log' --exclude='backend/**/__pycache__/' --exclude='.gitignore' . deck@${config:deckip}:${config:deckdir}/homebrew/dev/pluginloader", "command": "rsync -azp --delete --force --rsh='ssh -p ${config:deckport} ${config:deckkey}' --exclude='.git/' --exclude='.github/' --exclude='.vscode/' --exclude='frontend/' --exclude='dist/' --exclude='contrib/' --exclude='*.log' --exclude='**/__pycache__/' --exclude='.gitignore' . ${config:deckuser}@${config:deckip}:${config:deckdir}/homebrew/dev/pluginloader",
"problemMatcher": [] "problemMatcher": []
}, },
// RUN // RUN
@@ -120,7 +120,7 @@
"dependsOn": [ "dependsOn": [
"checkforsettings" "checkforsettings"
], ],
"command": "ssh deck@${config:deckip} -p ${config:deckport} ${config:deckkey} 'export PATH=${config:deckdir}/.local/bin:$PATH; export PLUGIN_PATH=${config:deckdir}/homebrew/plugins; export CHOWN_PLUGIN_PATH=0; export LOG_LEVEL=DEBUG; cd ${config:deckdir}/homebrew/dev/pluginloader/backend; echo '${config:deckpass}' | sudo -SE poetry run sh -c \"cd ${config:deckdir}/homebrew/services; python3 ${config:deckdir}/homebrew/dev/pluginloader/backend/main.py\"'", "command": "ssh ${config:deckuser}@${config:deckip} -p ${config:deckport} ${config:deckkey} 'export PATH=${config:deckdir}/.local/bin:$PATH; export PLUGIN_PATH=${config:deckdir}/homebrew/plugins; export CHOWN_PLUGIN_PATH=0; export LOG_LEVEL=DEBUG; cd ${config:deckdir}/homebrew/dev/pluginloader/backend; echo '${config:deckpass}' | poetry run sh -c \"cd ${config:deckdir}/homebrew/services; sudo -SE env \"PATH=\\$PATH\" python3 ${config:deckdir}/homebrew/dev/pluginloader/backend/main.py\"'",
"problemMatcher": [] "problemMatcher": []
}, },
{ {
+13 -8
View File
@@ -1,3 +1,4 @@
from platform import version
import re import re
import ssl import ssl
import uuid import uuid
@@ -14,7 +15,7 @@ from aiohttp import ClientSession
from .localplatform import localplatform from .localplatform import localplatform
from .enums import UserType from .enums import UserType
from logging import getLogger from logging import getLogger
from packaging.version import Version from packaging.version import Version # type: ignore
REMOTE_DEBUGGER_UNIT = "steam-web-debug-portforward.service" REMOTE_DEBUGGER_UNIT = "steam-web-debug-portforward.service"
@@ -36,12 +37,13 @@ def get_csrf_token():
@middleware @middleware
async def csrf_middleware(request: Request, handler: Handler): async def csrf_middleware(request: Request, handler: Handler):
if str(request.method) == "OPTIONS" or \ if str(request.method) == "OPTIONS" or \
request.headers.get('Authentication') == csrf_token or \ request.headers.get('X-Decky-Auth') == csrf_token or \
str(request.rel_url) == "/auth/token" or \ str(request.rel_url) == "/auth/token" or \
str(request.rel_url).startswith("/plugins/load_main/") or \ str(request.rel_url).startswith("/plugins/load_main/") or \
str(request.rel_url).startswith("/static/") or \ str(request.rel_url).startswith("/static/") or \
str(request.rel_url).startswith("/steam_resource/") or \ str(request.rel_url).startswith("/steam_resource/") or \
str(request.rel_url).startswith("/frontend/") or \ str(request.rel_url).startswith("/frontend/") or \
str(request.rel_url.path) == "/fetch" or \
str(request.rel_url.path) == "/ws" or \ str(request.rel_url.path) == "/ws" or \
assets_regex.match(str(request.rel_url)) or \ assets_regex.match(str(request.rel_url)) or \
dist_regex.match(str(request.rel_url)) or \ dist_regex.match(str(request.rel_url)) or \
@@ -61,24 +63,27 @@ def mkdir_as_user(path: str):
localplatform.chown(path) localplatform.chown(path)
# Fetches the version of loader # Fetches the version of loader
# TODO THIS IS ABSOLUTELY TERRIBLE AND NEVER SHOULDVE BEEN MERGED! packaging HAS NO TYPES AND WE COULD LITERALLY JUST USE A REGEX!!!!! REWRITE THIS!!!!!!!!!!!!!
def get_loader_version() -> str: def get_loader_version() -> str:
try: try:
# Normalize Python-style version to conform to Decky style # Normalize Python-style version to conform to Decky style
v = Version(importlib.metadata.version("decky_loader")) v = Version(importlib.metadata.version("decky_loader")) # type: ignore
version_str = f'v{v.major}.{v.minor}.{v.micro}' version_str = f'v{v.major}.{v.minor}.{v.micro}' # type: ignore
if v.pre: if v.pre: # type: ignore
version_str += f'-pre{v.pre[1]}' version_str += f'-pre{v.pre[1]}' # type: ignore
if v.post: if v.post: # type: ignore
version_str += f'-dev{v.post}' version_str += f'-dev{v.post}' # type: ignore
return version_str return version_str
except Exception as e: except Exception as e:
logger.warn(f"Failed to execute get_loader_version(): {str(e)}") logger.warn(f"Failed to execute get_loader_version(): {str(e)}")
return "unknown" return "unknown"
user_agent = f"Decky/{get_loader_version()} (https://decky.xyz)"
# returns the appropriate system python paths # returns the appropriate system python paths
def get_system_pythonpaths() -> list[str]: def get_system_pythonpaths() -> list[str]:
try: try:
+1 -1
View File
@@ -12,7 +12,7 @@ Some basic migration helpers are available: `migrate_any`, `migrate_settings`, `
A logging facility `logger` is available which writes to the recommended location. A logging facility `logger` is available which writes to the recommended location.
""" """
__version__ = '0.1.0' __version__ = '1.0.0'
import os import os
import subprocess import subprocess
@@ -12,7 +12,7 @@ Some basic migration helpers are available: `migrate_any`, `migrate_settings`, `
A logging facility `logger` is available which writes to the recommended location. A logging facility `logger` is available which writes to the recommended location.
""" """
__version__ = '0.1.0' __version__ = '1.0.0'
import logging import logging
+1
View File
@@ -47,6 +47,7 @@ class PluginWrapper:
self.emitted_event_callback: EmittedEventCallbackType = emit_callback self.emitted_event_callback: EmittedEventCallbackType = emit_callback
# TODO enable this after websocket release
self.legacy_method_warning = False self.legacy_method_warning = False
def __str__(self) -> str: def __str__(self) -> str:
+91 -3
View File
@@ -1,6 +1,8 @@
from __future__ import annotations from __future__ import annotations
from os import stat_result from os import stat_result
import uuid import uuid
from urllib.parse import unquote
from json.decoder import JSONDecodeError
from os.path import splitext from os.path import splitext
import re import re
from traceback import format_exc from traceback import format_exc
@@ -8,6 +10,7 @@ from stat import FILE_ATTRIBUTE_HIDDEN # type: ignore
from asyncio import StreamReader, StreamWriter, start_server, gather, open_connection from asyncio import StreamReader, StreamWriter, start_server, gather, open_connection
from aiohttp import ClientSession from aiohttp import ClientSession
from aiohttp.web import Request, StreamResponse, Response, json_response, post
from typing import TYPE_CHECKING, Callable, Coroutine, Dict, Any, List, TypedDict from typing import TYPE_CHECKING, Callable, Coroutine, Dict, Any, List, TypedDict
from logging import getLogger from logging import getLogger
@@ -26,12 +29,17 @@ class FilePickerObj(TypedDict):
filest: stat_result filest: stat_result
is_dir: bool is_dir: bool
decky_header_regex = re.compile("X-Decky-(.*)")
extra_header_regex = re.compile("X-Decky-Header-(.*)")
excluded_default_headers = ["Host", "Origin", "Sec-Fetch-Site", "Sec-Fetch-Mode", "Sec-Fetch-Dest"]
class Utilities: class Utilities:
def __init__(self, context: PluginManager) -> None: def __init__(self, context: PluginManager) -> None:
self.context = context self.context = context
self.legacy_util_methods: Dict[str, Callable[..., Coroutine[Any, Any, Any]]] = { self.legacy_util_methods: Dict[str, Callable[..., Coroutine[Any, Any, Any]]] = {
"ping": self.ping, "ping": self.ping,
"http_request": self.http_request, "http_request": self.http_request_legacy,
"install_plugin": self.install_plugin, "install_plugin": self.install_plugin,
"install_plugins": self.install_plugins, "install_plugins": self.install_plugins,
"cancel_plugin_install": self.cancel_plugin_install, "cancel_plugin_install": self.cancel_plugin_install,
@@ -76,9 +84,33 @@ class Utilities:
context.ws.add_route("utilities/enable_rdt", self.enable_rdt) context.ws.add_route("utilities/enable_rdt", self.enable_rdt)
context.ws.add_route("utilities/get_tab_id", self.get_tab_id) context.ws.add_route("utilities/get_tab_id", self.get_tab_id)
context.ws.add_route("utilities/get_user_info", self.get_user_info) context.ws.add_route("utilities/get_user_info", self.get_user_info)
context.ws.add_route("utilities/http_request", self.http_request) context.ws.add_route("utilities/http_request", self.http_request_legacy)
context.ws.add_route("utilities/_call_legacy_utility", self._call_legacy_utility) context.ws.add_route("utilities/_call_legacy_utility", self._call_legacy_utility)
context.web_app.add_routes([
post("/methods/{method_name}", self._handle_legacy_server_method_call)
])
for method in ('GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'HEAD'):
context.web_app.router.add_route(method, "/fetch", self.http_request)
async def _handle_legacy_server_method_call(self, request: Request) -> Response:
method_name = request.match_info["method_name"]
try:
args = await request.json()
except JSONDecodeError:
args = {}
res = {}
try:
r = await self.legacy_util_methods[method_name](**args)
res["result"] = r
res["success"] = True
except Exception as e:
res["result"] = str(e)
res["success"] = False
return json_response(res)
async def _call_legacy_utility(self, method_name: str, kwargs: Dict[Any, Any]) -> Any: async def _call_legacy_utility(self, method_name: str, kwargs: Dict[Any, Any]) -> Any:
self.logger.debug(f"Calling utility {method_name} with legacy kwargs"); self.logger.debug(f"Calling utility {method_name} with legacy kwargs");
res: Dict[Any, Any] = {} res: Dict[Any, Any] = {}
@@ -114,7 +146,63 @@ class Utilities:
async def uninstall_plugin(self, name: str): async def uninstall_plugin(self, name: str):
return await self.context.plugin_browser.uninstall_plugin(name) return await self.context.plugin_browser.uninstall_plugin(name)
async def http_request(self, method: str, url: str, extra_opts: Any = {}): # Loosely based on https://gist.github.com/mosquito/4dbfacd51e751827cda7ec9761273e95#file-proxy-py
async def http_request(self, req: Request) -> StreamResponse:
if req.headers.get('X-Decky-Auth', '') != helpers.get_csrf_token() and req.query.get('auth', '') != helpers.get_csrf_token():
return Response(text='Forbidden', status=403)
url = req.headers["X-Decky-Fetch-URL"] if "X-Decky-Fetch-URL" in req.headers else unquote(req.query.get('fetch_url', ''))
self.logger.info(f"Preparing {req.method} request to {url}")
headers = dict(req.headers)
headers["User-Agent"] = helpers.user_agent
for excluded_header in excluded_default_headers:
self.logger.debug(f"Excluding default header {excluded_header}")
if excluded_header in headers:
del headers[excluded_header]
if "X-Decky-Fetch-Excluded-Headers" in req.headers:
for excluded_header in req.headers["X-Decky-Fetch-Excluded-Headers"].split(", "):
self.logger.debug(f"Excluding header {excluded_header}")
if excluded_header in headers:
del headers[excluded_header]
for header in req.headers:
match = extra_header_regex.search(header)
if match:
header_name = match.group(1)
header_value = req.headers[header]
self.logger.debug(f"Adding extra header {header_name}: {header_value}")
headers[header_name] = header_value
for header in list(headers.keys()):
match = decky_header_regex.search(header)
if match:
self.logger.debug(f"Removing decky header {header} from request")
del headers[header]
self.logger.debug(f"Final request headers: {headers}")
body = await req.read() # TODO can this also be streamed?
async with ClientSession() as web:
async with web.request(req.method, url, headers=headers, data=body, ssl=helpers.get_ssl_context()) as web_res:
res = StreamResponse(headers=web_res.headers, status=web_res.status)
if web_res.headers.get('Transfer-Encoding', '').lower() == 'chunked':
res.enable_chunked_encoding()
await res.prepare(req)
self.logger.debug(f"Starting stream for {url}")
async for data in web_res.content.iter_any():
await res.write(data)
if data:
await res.drain()
self.logger.debug(f"Finished stream for {url}")
return res
async def http_request_legacy(self, method: str, url: str, extra_opts: Any = {}):
async with ClientSession() as web: async with ClientSession() as web:
res = await web.request(method, url, ssl=helpers.get_ssl_context(), **extra_opts) res = await web.request(method, url, ssl=helpers.get_ssl_context(), **extra_opts)
text = await res.text() text = await res.text()
+1 -1
View File
@@ -117,7 +117,7 @@ class WSRouter:
create_task(self._call_route(data["route"], data["args"], data["id"])) create_task(self._call_route(data["route"], data["args"], data["id"]))
else: else:
error = {"error":f'Route {data["route"]} does not exist.', "name": "RouteNotFoundError", "traceback": None} error = {"error":f'Route {data["route"]} does not exist.', "name": "RouteNotFoundError", "traceback": None}
create_task(self.write({"type": MessageType.ERROR.value, "id": data["id"], "message": error})) create_task(self.write({"type": MessageType.ERROR.value, "id": data["id"], "error": error}))
case _: case _:
self.logger.error("Unknown message type", data) self.logger.error("Unknown message type", data)
finally: finally:
+439 -487
View File
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -9,7 +9,7 @@ a = Analysis(
('locales', 'locales'), ('locales', 'locales'),
('static', 'static'), ('static', 'static'),
] + copy_metadata('decky_loader'), ] + copy_metadata('decky_loader'),
hiddenimports=['logging.handlers', 'sqlite3', 'decky_plugin'], hiddenimports=['logging.handlers', 'sqlite3', 'decky_plugin' 'decky'],
) )
pyz = PYZ(a.pure, a.zipped_data) pyz = PYZ(a.pure, a.zipped_data)
+2 -1
View File
@@ -13,12 +13,13 @@ include = ["decky_loader/static/*"]
[tool.poetry.dependencies] [tool.poetry.dependencies]
python = ">=3.10,<3.13" python = ">=3.10,<3.13"
aiohttp = "^3.8.5" aiohttp = "^3.9.5"
aiohttp-jinja2 = "^1.5.1" aiohttp-jinja2 = "^1.5.1"
aiohttp-cors = "^0.7.0" aiohttp-cors = "^0.7.0"
watchdog = "^2.1.7" watchdog = "^2.1.7"
certifi = "*" certifi = "*"
packaging = "^23.2" packaging = "^23.2"
multidict = "^6.0.5"
[tool.poetry.group.dev.dependencies] [tool.poetry.group.dev.dependencies]
pyinstaller = "^5.13.0" pyinstaller = "^5.13.0"
+49 -13
View File
@@ -42,7 +42,7 @@ const FilePicker = lazy(() => import('./components/modals/filepicker'));
declare global { declare global {
interface Window { interface Window {
__DECKY_SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED_deckyPluginBackendAPIInit?: { __DECKY_SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED_deckyLoaderAPIInit?: {
connect: (version: number, key: string) => any; // Returns the backend API used above, no real point adding types to this. connect: (version: number, key: string) => any; // Returns the backend API used above, no real point adding types to this.
}; };
} }
@@ -51,6 +51,10 @@ declare global {
/** Map of event names to event listeners */ /** Map of event names to event listeners */
type listenerMap = Map<string, Set<(...args: any) => any>>; type listenerMap = Map<string, Set<(...args: any) => any>>;
interface DeckyRequestInit extends RequestInit {
excludedHeaders: string[];
}
const callPluginMethod = DeckyBackend.callable<[pluginName: string, method: string, ...args: any], any>( const callPluginMethod = DeckyBackend.callable<[pluginName: string, method: string, ...args: any], any>(
'loader/call_plugin_method', 'loader/call_plugin_method',
); );
@@ -357,7 +361,7 @@ class PluginLoader extends Logger {
let res = await fetch(`http://127.0.0.1:1337/plugins/${name}/frontend_bundle`, { let res = await fetch(`http://127.0.0.1:1337/plugins/${name}/frontend_bundle`, {
credentials: 'include', credentials: 'include',
headers: { headers: {
Authentication: deckyAuthToken, 'X-Decky-Auth': deckyAuthToken,
}, },
}); });
if (res.ok) { if (res.ok) {
@@ -484,12 +488,31 @@ class PluginLoader extends Logger {
}); });
} }
/* TODO replace with the following flow (or similar) so we can reuse the JS Fetch API // Useful for audio/video streams
frontend --request URL only--> backend (ws method) getExternalResourceURL(url: string) {
backend --new temporary backend URL--> frontend (ws response) return `http://127.0.0.1:1337/fetch?auth=${deckyAuthToken}&fetch_url=${encodeURIComponent(url)}`;
frontend <--> backend <--> target URL (over http!) }
*/
async fetchNoCors(url: string, request: any = {}) { // Same syntax as fetch but only supports the url-based syntax and an object for headers since it's the most common usage pattern
fetch(input: string, init?: DeckyRequestInit | undefined): Promise<Response> {
const headers: { [name: string]: string } = {
...(init?.headers as { [name: string]: string }),
'X-Decky-Auth': deckyAuthToken,
'X-Decky-Fetch-URL': input,
};
if (init?.excludedHeaders) {
headers['X-Decky-Fetch-Excluded-Headers'] = init.excludedHeaders.join(', ');
}
return fetch('http://127.0.0.1:1337/fetch', {
...init,
credentials: 'include',
headers,
});
}
async legacyFetchNoCors(url: string, request: any = {}) {
let method: string; let method: string;
const req = { headers: {}, ...request, data: request.body }; const req = { headers: {}, ...request, data: request.body };
req?.body && delete req.body; req?.body && delete req.body;
@@ -513,10 +536,10 @@ class PluginLoader extends Logger {
initPluginBackendAPI() { initPluginBackendAPI() {
// Things will break *very* badly if plugin code touches this outside of @decky/backend, so lets make that clear. // Things will break *very* badly if plugin code touches this outside of @decky/backend, so lets make that clear.
window.__DECKY_SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED_deckyPluginBackendAPIInit = { window.__DECKY_SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED_deckyLoaderAPIInit = {
connect: (version: number, pluginName: string) => { connect: (version: number, pluginName: string) => {
if (version <= 0) { if (version < 1 || version > 1) {
throw new Error(`Plugin ${pluginName} requested invalid backend api version ${version}.`); throw new Error(`Plugin ${pluginName} requested unsupported backend api version ${version}.`);
} }
const eventListeners: listenerMap = new Map(); const eventListeners: listenerMap = new Map();
@@ -543,9 +566,22 @@ class PluginLoader extends Logger {
set?.delete(listener); set?.delete(listener);
} }
}, },
openFilePicker: this.openFilePicker.bind(this),
executeInTab: DeckyBackend.callable<
[tab: String, runAsync: Boolean, code: string],
{ success: boolean; result: any }
>('utilities/execute_in_tab'),
fetch: this.fetch.bind(this),
getExternalResourceURL: this.getExternalResourceURL.bind(this),
injectCssIntoTab: DeckyBackend.callable<[tab: string, style: string], string>(
'utilities/inject_css_into_tab',
),
removeCssFromTab: DeckyBackend.callable<[tab: string, cssId: string]>('utilities/remove_css_from_tab'),
routerHook: this.routerHook,
toaster: this.toaster,
}; };
this.debug(`${pluginName} connected to backend API.`); this.debug(`${pluginName} connected to loader API.`);
return backendAPI; return backendAPI;
}, },
}; };
@@ -591,7 +627,7 @@ class PluginLoader extends Logger {
args, args,
); );
}, },
fetchNoCors: this.fetchNoCors, fetchNoCors: this.legacyFetchNoCors,
executeInTab: DeckyBackend.callable< executeInTab: DeckyBackend.callable<
[tab: String, runAsync: Boolean, code: string], [tab: String, runAsync: Boolean, code: string],
{ success: boolean; result: any } { success: boolean; result: any }
+1 -1
View File
@@ -32,7 +32,7 @@ declare global {
backend: { backend: {
loadPath: 'http://127.0.0.1:1337/locales/{{lng}}.json', loadPath: 'http://127.0.0.1:1337/locales/{{lng}}.json',
customHeaders: { customHeaders: {
Authentication: deckyAuthToken, 'X-Decky-Auth': deckyAuthToken,
}, },
requestOptions: { requestOptions: {
credentials: 'include', credentials: 'include',
+4 -4
View File
@@ -30,7 +30,7 @@ interface ReplyMessage {
interface ErrorMessage { interface ErrorMessage {
type: MessageType.ERROR; type: MessageType.ERROR;
error: { name: string; message: string; traceback: string | null }; error: { name: string; error: string; traceback: string | null };
id: number; id: number;
} }
@@ -40,8 +40,8 @@ interface ErrorMessage {
export class PyError extends Error { export class PyError extends Error {
pythonTraceback: string | null; pythonTraceback: string | null;
constructor(name: string, message: string, traceback: string | null) { constructor(name: string, error: string, traceback: string | null) {
super(message); super(error);
this.name = `Python ${name}`; this.name = `Python ${name}`;
if (traceback) { if (traceback) {
// traceback will always start with `Traceback (most recent call last):` // traceback will always start with `Traceback (most recent call last):`
@@ -142,7 +142,7 @@ export class WSRouter extends Logger {
case MessageType.ERROR: case MessageType.ERROR:
if (this.runningCalls.has(data.id)) { if (this.runningCalls.has(data.id)) {
let err = new PyError(data.error.name, data.error.message, data.error.traceback); let err = new PyError(data.error.name, data.error.error, data.error.traceback);
this.runningCalls.get(data.id)!.reject(err); this.runningCalls.get(data.id)!.reject(err);
this.runningCalls.delete(data.id); this.runningCalls.delete(data.id);
this.debug(`Rejected PY call ${data.id} with error`, data.error); this.debug(`Rejected PY call ${data.id} with error`, data.error);