JS -> Python WS now functional

This commit is contained in:
AAGaming
2023-07-10 18:41:56 -04:00
committed by marios8543
parent 05b41b3410
commit 1921e7ec56
7 changed files with 112 additions and 73 deletions
+11 -5
View File
@@ -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)
+3 -1
View File
@@ -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)
+5 -1
View File
@@ -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()
+57 -28
View File
@@ -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
+5
View File
@@ -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() {
+3 -19
View File
@@ -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
View File
@@ -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();
} }
} }