mirror of
https://github.com/SteamDeckHomebrew/decky-loader.git
synced 2026-07-11 21:22:01 +00:00
JS -> Python WS now functional
This commit is contained in:
@@ -70,7 +70,7 @@ class FileChangeHandler(RegexMatchingEventHandler):
|
|||||||
self.maybe_reload(src_path)
|
self.maybe_reload(src_path)
|
||||||
|
|
||||||
class Loader:
|
class Loader:
|
||||||
def __init__(self, server_instance: PluginManager, plugin_path: str, loop: AbstractEventLoop, live_reload: bool = False) -> None:
|
def __init__(self, server_instance: PluginManager, ws: WSRouter, plugin_path: str, loop: AbstractEventLoop, live_reload: bool = False) -> None:
|
||||||
self.loop = loop
|
self.loop = loop
|
||||||
self.logger = getLogger("Loader")
|
self.logger = getLogger("Loader")
|
||||||
self.plugin_path = plugin_path
|
self.plugin_path = plugin_path
|
||||||
@@ -88,10 +88,7 @@ class Loader:
|
|||||||
self.observer.start()
|
self.observer.start()
|
||||||
self.loop.create_task(self.enable_reload_wait())
|
self.loop.create_task(self.enable_reload_wait())
|
||||||
|
|
||||||
self.ws = WSRouter()
|
server_instance.add_routes([
|
||||||
|
|
||||||
server_instance.web_app.add_routes([
|
|
||||||
web.get("/ws", self.ws.handle),
|
|
||||||
web.get("/frontend/{path:.*}", self.handle_frontend_assets),
|
web.get("/frontend/{path:.*}", self.handle_frontend_assets),
|
||||||
web.get("/locales/{path:.*}", self.handle_frontend_locales),
|
web.get("/locales/{path:.*}", self.handle_frontend_locales),
|
||||||
web.get("/plugins", self.get_plugins),
|
web.get("/plugins", self.get_plugins),
|
||||||
@@ -101,6 +98,15 @@ class Loader:
|
|||||||
web.post("/plugins/{plugin_name}/reload", self.handle_backend_reload_request)
|
web.post("/plugins/{plugin_name}/reload", self.handle_backend_reload_request)
|
||||||
])
|
])
|
||||||
|
|
||||||
|
ws.add_route("test", self.test_method)
|
||||||
|
|
||||||
|
async def test_method():
|
||||||
|
await sleep(2)
|
||||||
|
|
||||||
|
return {
|
||||||
|
"test data": True
|
||||||
|
}
|
||||||
|
|
||||||
async def enable_reload_wait(self):
|
async def enable_reload_wait(self):
|
||||||
if self.live_reload:
|
if self.live_reload:
|
||||||
await sleep(10)
|
await sleep(10)
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
# Change PyInstaller files permissions
|
# Change PyInstaller files permissions
|
||||||
import sys
|
import sys
|
||||||
from typing import Dict
|
from typing import Dict
|
||||||
|
from wsrouter import WSRouter
|
||||||
from .localplatform.localplatform import (chmod, chown, service_stop, service_start,
|
from .localplatform.localplatform import (chmod, chown, service_stop, service_start,
|
||||||
ON_WINDOWS, get_log_level, get_live_reload,
|
ON_WINDOWS, get_log_level, get_live_reload,
|
||||||
get_server_port, get_server_host, get_chown_plugin_path,
|
get_server_port, get_server_host, get_chown_plugin_path,
|
||||||
@@ -63,7 +64,8 @@ class PluginManager:
|
|||||||
allow_credentials=True
|
allow_credentials=True
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
self.plugin_loader = Loader(self, plugin_path, self.loop, get_live_reload())
|
self.ws = WSRouter(self.loop, self.web_app)
|
||||||
|
self.plugin_loader = Loader(self, self.ws, plugin_path, self.loop, get_live_reload())
|
||||||
self.settings = SettingsManager("loader", path.join(get_privileged_path(), "settings"))
|
self.settings = SettingsManager("loader", path.join(get_privileged_path(), "settings"))
|
||||||
self.plugin_browser = PluginBrowser(plugin_path, self.plugin_loader.plugins, self.plugin_loader, self.settings)
|
self.plugin_browser = PluginBrowser(plugin_path, self.plugin_loader.plugins, self.plugin_loader, self.settings)
|
||||||
self.utilities = Utilities(self)
|
self.utilities = Utilities(self)
|
||||||
|
|||||||
@@ -63,7 +63,11 @@ class Utilities:
|
|||||||
web.post("/methods/{method_name}", self._handle_server_method_call)
|
web.post("/methods/{method_name}", self._handle_server_method_call)
|
||||||
])
|
])
|
||||||
|
|
||||||
async def _handle_server_method_call(self, request: web.Request):
|
context.ws.add_route("utilities/ping", self.ping)
|
||||||
|
context.ws.add_route("utilities/settings/get", self.get_setting)
|
||||||
|
context.ws.add_route("utilities/settings/set", self.set_setting)
|
||||||
|
|
||||||
|
async def _handle_server_method_call(self, request):
|
||||||
method_name = request.match_info["method_name"]
|
method_name = request.match_info["method_name"]
|
||||||
try:
|
try:
|
||||||
args = await request.json()
|
args = await request.json()
|
||||||
|
|||||||
@@ -1,15 +1,18 @@
|
|||||||
from logging import getLogger
|
from logging import getLogger
|
||||||
|
|
||||||
from asyncio import Future
|
from asyncio import AbstractEventLoop, Future
|
||||||
|
|
||||||
from aiohttp import web, WSMsgType
|
from aiohttp import WSMsgType
|
||||||
|
from aiohttp.web import Application, WebSocketResponse, Request, Response, get
|
||||||
|
|
||||||
from enum import Enum
|
from enum import Enum
|
||||||
|
|
||||||
from typing import Dict, Any, Callable
|
from typing import Dict
|
||||||
|
|
||||||
from traceback import format_exc
|
from traceback import format_exc
|
||||||
|
|
||||||
|
from helpers import get_csrf_token
|
||||||
|
|
||||||
class MessageType(Enum):
|
class MessageType(Enum):
|
||||||
# Call-reply
|
# Call-reply
|
||||||
CALL = 0
|
CALL = 0
|
||||||
@@ -23,7 +26,8 @@ class MessageType(Enum):
|
|||||||
# see wsrouter.ts for typings
|
# see wsrouter.ts for typings
|
||||||
|
|
||||||
class WSRouter:
|
class WSRouter:
|
||||||
def __init__(self) -> None:
|
def __init__(self, loop: AbstractEventLoop, server_instance: Application) -> None:
|
||||||
|
self.loop = loop
|
||||||
self.ws = None
|
self.ws = None
|
||||||
self.req_id = 0
|
self.req_id = 0
|
||||||
self.routes = {}
|
self.routes = {}
|
||||||
@@ -31,12 +35,25 @@ class WSRouter:
|
|||||||
# self.subscriptions: Dict[str, Callable[[Any]]] = {}
|
# self.subscriptions: Dict[str, Callable[[Any]]] = {}
|
||||||
self.logger = getLogger("WSRouter")
|
self.logger = getLogger("WSRouter")
|
||||||
|
|
||||||
async def add_route(self, name, route):
|
server_instance.add_routes([
|
||||||
|
get("/ws", self.handle)
|
||||||
|
])
|
||||||
|
|
||||||
|
async def write(self, dta: Dict[str, any]):
|
||||||
|
await self.ws.send_json(dta)
|
||||||
|
|
||||||
|
def add_route(self, name: str, route):
|
||||||
self.routes[name] = route
|
self.routes[name] = route
|
||||||
|
|
||||||
async def handle(self, request):
|
def remove_route(self, name: str):
|
||||||
|
del self.routes[name]
|
||||||
|
|
||||||
|
async def handle(self, request: Request):
|
||||||
|
# Auth is a query param as JS WebSocket doesn't support headers
|
||||||
|
if request.rel_url.query["auth"] != get_csrf_token():
|
||||||
|
return Response(text='Forbidden', status='403')
|
||||||
self.logger.debug('Websocket connection starting')
|
self.logger.debug('Websocket connection starting')
|
||||||
ws = web.WebSocketResponse()
|
ws = WebSocketResponse()
|
||||||
await ws.prepare(request)
|
await ws.prepare(request)
|
||||||
self.logger.debug('Websocket connection ready')
|
self.logger.debug('Websocket connection ready')
|
||||||
|
|
||||||
@@ -58,29 +75,29 @@ class WSRouter:
|
|||||||
# TODO DO NOT RELY ON THIS!
|
# TODO DO NOT RELY ON THIS!
|
||||||
break
|
break
|
||||||
else:
|
else:
|
||||||
match data.type:
|
|
||||||
case MessageType.CALL:
|
|
||||||
# do stuff with the message
|
|
||||||
data = msg.json()
|
data = msg.json()
|
||||||
if self.routes[data.route]:
|
match data["type"]:
|
||||||
|
case MessageType.CALL.value:
|
||||||
|
# do stuff with the message
|
||||||
|
if self.routes[data["route"]]:
|
||||||
try:
|
try:
|
||||||
res = await self.routes[data.route](*data.args)
|
res = await self.routes[data["route"]](*data["args"])
|
||||||
await self.write({"type": MessageType.REPLY, "id": data.id, "result": res})
|
await self.write({"type": MessageType.REPLY.value, "id": data["id"], "result": res})
|
||||||
self.logger.debug(f"Started PY call {data.route} ID {data.id}")
|
self.logger.debug(f'Started PY call {data["route"]} ID {data["id"]}')
|
||||||
except:
|
except:
|
||||||
await self.write({"type": MessageType.ERROR, "id": data.id, "error": format_exc()})
|
await self.write({"type": MessageType.ERROR.value, "id": data["id"], "error": format_exc()})
|
||||||
else:
|
else:
|
||||||
await self.write({"type": MessageType.ERROR, "id": data.id, "error": "Route does not exist."})
|
await self.write({"type": MessageType.ERROR.value, "id": data["id"], "error": "Route does not exist."})
|
||||||
case MessageType.REPLY:
|
case MessageType.REPLY.value:
|
||||||
if self.running_calls[data.id]:
|
if self.running_calls[data["id"]]:
|
||||||
self.running_calls[data.id].set_result(data.result)
|
self.running_calls[data["id"]].set_result(data["result"])
|
||||||
del self.running_calls[data.id]
|
del self.running_calls[data["id"]]
|
||||||
self.logger.debug(f"Resolved JS call {data.id} with value {str(data.result)}")
|
self.logger.debug(f'Resolved JS call {data["id"]} with value {str(data["result"])}')
|
||||||
case MessageType.ERROR:
|
case MessageType.ERROR.value:
|
||||||
if self.running_calls[data.id]:
|
if self.running_calls[data["id"]]:
|
||||||
self.running_calls[data.id].set_exception(data.error)
|
self.running_calls[data["id"]].set_exception(data["error"])
|
||||||
del self.running_calls[data.id]
|
del self.running_calls[data["id"]]
|
||||||
self.logger.debug(f"Errored JS call {data.id} with error {data.error}")
|
self.logger.debug(f'Errored JS call {data["id"]} with error {data["error"]}')
|
||||||
|
|
||||||
case _:
|
case _:
|
||||||
self.logger.error("Unknown message type", data)
|
self.logger.error("Unknown message type", data)
|
||||||
@@ -94,5 +111,17 @@ class WSRouter:
|
|||||||
self.logger.debug('Websocket connection closed')
|
self.logger.debug('Websocket connection closed')
|
||||||
return ws
|
return ws
|
||||||
|
|
||||||
async def write(self, dta: Dict[str, any]):
|
async def call(self, route: str, *args):
|
||||||
await self.ws.send_json(dta)
|
future = Future()
|
||||||
|
|
||||||
|
self.req_id += 1
|
||||||
|
|
||||||
|
id = self.req_id
|
||||||
|
|
||||||
|
self.running_calls[id] = future
|
||||||
|
|
||||||
|
self.logger.debug(f'Calling JS method {route} with args {str(args)}')
|
||||||
|
|
||||||
|
self.write({ "type": MessageType.CALL.value, "route": route, "args": args, "id": id })
|
||||||
|
|
||||||
|
return await future
|
||||||
|
|||||||
@@ -34,6 +34,7 @@ import Toaster from './toaster';
|
|||||||
import { VerInfo, callUpdaterMethod } from './updater';
|
import { VerInfo, callUpdaterMethod } from './updater';
|
||||||
import { getSetting, setSetting } from './utils/settings';
|
import { getSetting, setSetting } from './utils/settings';
|
||||||
import TranslationHelper, { TranslationClass } from './utils/TranslationHelper';
|
import TranslationHelper, { TranslationClass } from './utils/TranslationHelper';
|
||||||
|
import { WSRouter } from './wsrouter';
|
||||||
|
|
||||||
const StorePage = lazy(() => import('./components/store/Store'));
|
const StorePage = lazy(() => import('./components/store/Store'));
|
||||||
const SettingsPage = lazy(() => import('./components/settings'));
|
const SettingsPage = lazy(() => import('./components/settings'));
|
||||||
@@ -48,6 +49,8 @@ class PluginLoader extends Logger {
|
|||||||
public toaster: Toaster = new Toaster();
|
public toaster: Toaster = new Toaster();
|
||||||
private deckyState: DeckyState = new DeckyState();
|
private deckyState: DeckyState = new DeckyState();
|
||||||
|
|
||||||
|
public ws: WSRouter = new WSRouter();
|
||||||
|
|
||||||
public hiddenPluginsService = new HiddenPluginsService(this.deckyState);
|
public hiddenPluginsService = new HiddenPluginsService(this.deckyState);
|
||||||
public notificationService = new NotificationService(this.deckyState);
|
public notificationService = new NotificationService(this.deckyState);
|
||||||
|
|
||||||
@@ -102,9 +105,11 @@ class PluginLoader extends Logger {
|
|||||||
|
|
||||||
initFilepickerPatches();
|
initFilepickerPatches();
|
||||||
|
|
||||||
|
this.ws.connect().then(() => {
|
||||||
this.getUserInfo();
|
this.getUserInfo();
|
||||||
|
|
||||||
this.updateVersion();
|
this.updateVersion();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public async getUserInfo() {
|
public async getUserInfo() {
|
||||||
|
|||||||
@@ -1,24 +1,8 @@
|
|||||||
interface GetSettingArgs<T> {
|
|
||||||
key: string;
|
|
||||||
default: T;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface SetSettingArgs<T> {
|
|
||||||
key: string;
|
|
||||||
value: T;
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function getSetting<T>(key: string, def: T): Promise<T> {
|
export async function getSetting<T>(key: string, def: T): Promise<T> {
|
||||||
const res = (await window.DeckyPluginLoader.callServerMethod('get_setting', {
|
const res = await window.DeckyPluginLoader.ws.call<[string, T], T>('utilities/settings/get', key, def);
|
||||||
key,
|
return res;
|
||||||
default: def,
|
|
||||||
} as GetSettingArgs<T>)) as { result: T };
|
|
||||||
return res.result;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function setSetting<T>(key: string, value: T): Promise<void> {
|
export async function setSetting<T>(key: string, value: T): Promise<void> {
|
||||||
await window.DeckyPluginLoader.callServerMethod('set_setting', {
|
await window.DeckyPluginLoader.ws.call<[string, T], void>('utilities/settings/set', key, value);
|
||||||
key,
|
|
||||||
value,
|
|
||||||
} as SetSettingArgs<T>);
|
|
||||||
}
|
}
|
||||||
|
|||||||
+24
-15
@@ -41,10 +41,11 @@ interface PromiseResolver<T> {
|
|||||||
promise: Promise<T>;
|
promise: Promise<T>;
|
||||||
}
|
}
|
||||||
|
|
||||||
class WSRouter extends Logger {
|
export class WSRouter extends Logger {
|
||||||
routes: Map<string, (...args: any) => any> = new Map();
|
routes: Map<string, (...args: any) => any> = new Map();
|
||||||
runningCalls: Map<number, PromiseResolver<any>> = new Map();
|
runningCalls: Map<number, PromiseResolver<any>> = new Map();
|
||||||
ws?: WebSocket;
|
ws?: WebSocket;
|
||||||
|
connectPromise?: Promise<void>;
|
||||||
// Used to map results and errors to calls
|
// Used to map results and errors to calls
|
||||||
reqId: number = 0;
|
reqId: number = 0;
|
||||||
constructor() {
|
constructor() {
|
||||||
@@ -52,30 +53,35 @@ class WSRouter extends Logger {
|
|||||||
}
|
}
|
||||||
|
|
||||||
connect() {
|
connect() {
|
||||||
this.ws = new WebSocket('ws://127.0.0.1:1337/ws');
|
return (this.connectPromise = new Promise<void>((resolve) => {
|
||||||
|
// Auth is a query param as JS WebSocket doesn't support headers
|
||||||
|
this.ws = new WebSocket(`ws://127.0.0.1:1337/ws?auth=${window.deckyAuthToken}`);
|
||||||
|
|
||||||
|
this.ws.addEventListener('open', () => {
|
||||||
|
this.debug('WS Connected');
|
||||||
|
resolve();
|
||||||
|
delete this.connectPromise;
|
||||||
|
});
|
||||||
this.ws.addEventListener('message', this.onMessage.bind(this));
|
this.ws.addEventListener('message', this.onMessage.bind(this));
|
||||||
this.ws.addEventListener('close', this.onError.bind(this));
|
this.ws.addEventListener('close', this.onError.bind(this));
|
||||||
this.ws.addEventListener('message', this.onError.bind(this));
|
// this.ws.addEventListener('error', this.onError.bind(this));
|
||||||
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
createPromiseResolver<T>(): PromiseResolver<T> {
|
createPromiseResolver<T>(): PromiseResolver<T> {
|
||||||
let resolver: PromiseResolver<T>;
|
let resolver: Partial<PromiseResolver<T>> = {};
|
||||||
const promise = new Promise<T>((resolve, reject) => {
|
const promise = new Promise<T>((resolve, reject) => {
|
||||||
resolver = {
|
resolver.resolve = resolve;
|
||||||
promise,
|
resolver.reject = reject;
|
||||||
resolve,
|
|
||||||
reject,
|
|
||||||
};
|
|
||||||
this.debug('Created new PromiseResolver');
|
|
||||||
});
|
});
|
||||||
this.debug('Returning new PromiseResolver');
|
resolver.promise = promise;
|
||||||
// The promise will always run first
|
// The promise will always run first
|
||||||
// @ts-expect-error 2454
|
// @ts-expect-error 2454
|
||||||
return resolver;
|
return resolver;
|
||||||
}
|
}
|
||||||
|
|
||||||
write(data: Message) {
|
async write(data: Message) {
|
||||||
|
if (this.connectPromise) await this.connectPromise;
|
||||||
this.ws?.send(JSON.stringify(data));
|
this.ws?.send(JSON.stringify(data));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -129,9 +135,9 @@ class WSRouter extends Logger {
|
|||||||
} catch (e) {
|
} catch (e) {
|
||||||
this.error('Error parsing WebSocket message', e);
|
this.error('Error parsing WebSocket message', e);
|
||||||
}
|
}
|
||||||
this.call<[number, number], string>('methodName', 1, 2);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// this.call<[number, number], string>('methodName', 1, 2);
|
||||||
call<Args extends any[] = any[], Return = void>(route: string, ...args: Args): Promise<Return> {
|
call<Args extends any[] = any[], Return = void>(route: string, ...args: Args): Promise<Return> {
|
||||||
const resolver = this.createPromiseResolver<Return>();
|
const resolver = this.createPromiseResolver<Return>();
|
||||||
|
|
||||||
@@ -139,12 +145,15 @@ class WSRouter extends Logger {
|
|||||||
|
|
||||||
this.runningCalls.set(id, resolver);
|
this.runningCalls.set(id, resolver);
|
||||||
|
|
||||||
|
this.debug(`Calling PY method ${route} with args`, args);
|
||||||
|
|
||||||
this.write({ type: MessageType.CALL, route, args, id });
|
this.write({ type: MessageType.CALL, route, args, id });
|
||||||
|
|
||||||
return resolver.promise;
|
return resolver.promise;
|
||||||
}
|
}
|
||||||
|
|
||||||
onError(error: any) {
|
async onError(error: any) {
|
||||||
this.error('WS ERROR', error);
|
this.error('WS DISCONNECTED', error);
|
||||||
|
await this.connect();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user