Implement legacy & modern plugin method calls over WS

This version builds fine and runs all of the 14 plugins I have installed perfectly, so we're really close to having this done.
This commit is contained in:
AAGaming
2023-12-30 00:46:59 -05:00
parent 6042ca56b8
commit 6522ebf0ca
17 changed files with 240 additions and 144 deletions
+2 -2
View File
@@ -159,8 +159,8 @@ backend/static
.vscode/settings.json
# plugins folder for local launches
plugins/*
/plugins/*
act/.directory
act/artifacts/*
bin/act
settings/
/settings/
+9 -5
View File
@@ -37,8 +37,11 @@
"label": "dependencies",
"type": "shell",
"group": "none",
"dependsOn": [
"deploy"
],
"detail": "Check for local runs, create a plugins folder",
"command": "rsync -azp --rsh='ssh -p ${config:deckport} ${config:deckkey}' backend/pyproject.toml backend/poetry.lock deck@${config:deckip}:${config:deckdir}/homebrew/dev/pluginloader && ssh deck@${config:deckip} -p ${config:deckport} ${config:deckkey} 'python -m ensurepip && python -m pip install --upgrade poetry && cd ${config:deckdir}/homebrew/dev/pluginloader/backend && python -m poetry install'",
"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'",
"problemMatcher": []
},
{
@@ -105,7 +108,7 @@
"detail": "Deploy dev PluginLoader to deck",
"type": "shell",
"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/decky_loader/__pycache__/' --exclude='.gitignore' . deck@${config:deckip}:${config:deckdir}/homebrew/dev/pluginloader",
"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",
"problemMatcher": []
},
// RUN
@@ -117,7 +120,7 @@
"dependsOn": [
"checkforsettings"
],
"command": "ssh deck@${config:deckip} -p ${config:deckport} ${config:deckkey} 'export PLUGIN_PATH=${config:deckdir}/homebrew/dev/plugins; export CHOWN_PLUGIN_PATH=0; export LOG_LEVEL=DEBUG; cd ${config:deckdir}/homebrew/services; echo '${config:deckpass}' | sudo -SE python3 ${config:deckdir}/homebrew/dev/pluginloader/backend/main.py'",
"command": "ssh deck@${config:deckip} -p ${config:deckport} ${config:deckkey} 'export PATH=${config:deckdir}/.local/bin:$PATH; export PLUGIN_PATH=${config:deckdir}/homebrew/dev/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\"'",
"problemMatcher": []
},
{
@@ -181,7 +184,8 @@
"buildall",
"createfolders",
"dependencies",
"deploy",
// dependencies runs deploy already
// "deploy",
"runpydeck"
],
"problemMatcher": []
@@ -190,7 +194,7 @@
"label": "act",
"type": "shell",
"group": "none",
"detail": "Run the act thing",
"detail": "Build release artifact using local CI",
"command": "./act/run-act.sh release",
"problemMatcher": []
}
+1 -1
View File
@@ -34,7 +34,7 @@ def get_csrf_token():
@middleware
async def csrf_middleware(request: Request, handler: Handler):
if str(request.method) == "OPTIONS" or request.headers.get('Authentication') == csrf_token or str(request.rel_url) == "/auth/token" or str(request.rel_url).startswith("/plugins/load_main/") or str(request.rel_url).startswith("/static/") or str(request.rel_url).startswith("/steam_resource/") or str(request.rel_url).startswith("/frontend/") or assets_regex.match(str(request.rel_url)) or frontend_regex.match(str(request.rel_url)):
if str(request.method) == "OPTIONS" or request.headers.get('Authentication') == csrf_token or str(request.rel_url) == "/auth/token" or str(request.rel_url).startswith("/plugins/load_main/") or str(request.rel_url).startswith("/static/") or str(request.rel_url).startswith("/steam_resource/") or str(request.rel_url).startswith("/frontend/") or str(request.rel_url.path) == "/ws" or assets_regex.match(str(request.rel_url)) or frontend_regex.match(str(request.rel_url)):
return await handler(request)
return Response(text='Forbidden', status=403)
+29 -25
View File
@@ -4,15 +4,15 @@ from json.decoder import JSONDecodeError
from logging import getLogger
from os import listdir, path
from pathlib import Path
from traceback import print_exc
from typing import Any, Tuple
from traceback import format_exc, print_exc
from typing import Any, Tuple, Dict
from aiohttp import web
from os.path import exists
from watchdog.events import RegexMatchingEventHandler, DirCreatedEvent, DirModifiedEvent, FileCreatedEvent, FileModifiedEvent # type: ignore
from watchdog.observers import Observer # type: ignore
from typing import TYPE_CHECKING
from typing import TYPE_CHECKING, List
if TYPE_CHECKING:
from .main import PluginManager
@@ -92,13 +92,15 @@ class Loader:
server_instance.web_app.add_routes([
web.get("/frontend/{path:.*}", self.handle_frontend_assets),
web.get("/locales/{path:.*}", self.handle_frontend_locales),
web.get("/plugins", self.get_plugins),
web.get("/plugins/{plugin_name}/frontend_bundle", self.handle_frontend_bundle),
web.post("/plugins/{plugin_name}/methods/{method_name}", self.handle_plugin_method_call),
web.get("/plugins/{plugin_name}/assets/{path:.*}", self.handle_plugin_frontend_assets),
web.post("/plugins/{plugin_name}/reload", self.handle_backend_reload_request)
])
server_instance.ws.add_route("loader/get_plugins", self.get_plugins)
server_instance.ws.add_route("loader/reload_plugin", self.handle_plugin_backend_reload)
server_instance.ws.add_route("loader/call_plugin_method", self.handle_plugin_method_call)
server_instance.ws.add_route("loader/call_legacy_plugin_method", self.handle_plugin_method_call_legacy)
async def enable_reload_wait(self):
if self.live_reload:
await sleep(10)
@@ -119,9 +121,9 @@ class Loader:
self.logger.info(f"Language {req_lang} not available, returning an empty dictionary")
return web.json_response(data={}, headers={"Cache-Control": "no-cache"})
async def get_plugins(self, request: web.Request):
async def get_plugins(self):
plugins = list(self.plugins.values())
return web.json_response([{"name": str(i), "version": i.version} for i in plugins])
return [{"name": str(i), "version": i.version} for i in plugins]
async def handle_plugin_frontend_assets(self, request: web.Request):
plugin = self.plugins[request.match_info["plugin_name"]]
@@ -173,29 +175,31 @@ class Loader:
args = await self.reload_queue.get()
self.import_plugin(*args) # type: ignore
async def handle_plugin_method_call(self, request: web.Request):
res = {}
plugin = self.plugins[request.match_info["plugin_name"]]
method_name = request.match_info["method_name"]
try:
method_info = await request.json()
args: Any = method_info["args"]
except JSONDecodeError:
args = {}
async def handle_plugin_method_call_legacy(self, plugin_name: str, method_name: str, kwargs: Dict[Any, Any]):
res: Dict[Any, Any] = {}
plugin = self.plugins[plugin_name]
try:
if method_name.startswith("_"):
raise RuntimeError("Tried to call private method")
res["result"] = await plugin.execute_method(method_name, args)
raise RuntimeError(f"Plugin {plugin.name} tried to call private method {method_name}")
res["result"] = await plugin.execute_legacy_method(method_name, kwargs)
res["success"] = True
except Exception as e:
res["result"] = str(e)
res["success"] = False
return web.json_response(res)
return res
async def handle_backend_reload_request(self, request: web.Request):
plugin_name : str = request.match_info["plugin_name"]
async def handle_plugin_method_call(self, plugin_name: str, method_name: str, *args: List[Any]):
plugin = self.plugins[plugin_name]
try:
if method_name.startswith("_"):
raise RuntimeError(f"Plugin {plugin.name} tried to call private method {method_name}")
result = await plugin.execute_method(method_name, *args)
except Exception as e:
self.logger.error(f"Method {method_name} of plugin {plugin.name} failed with the following exception:\n{format_exc()}")
raise e # throw again to pass the error to the frontend
return result
async def handle_plugin_backend_reload(self, plugin_name: str):
plugin = self.plugins[plugin_name]
await self.reload_queue.put((plugin.file, plugin.plugin_directory))
return web.Response(status=200)
await self.reload_queue.put((plugin.file, plugin.plugin_directory))
+1 -2
View File
@@ -1,7 +1,6 @@
# Change PyInstaller files permissions
import sys
from typing import Dict
from wsrouter import WSRouter
from .localplatform.localplatform import (chmod, chown, service_stop, service_start,
ON_WINDOWS, get_log_level, get_live_reload,
get_server_port, get_server_host, get_chown_plugin_path,
@@ -32,6 +31,7 @@ from .settings import SettingsManager
from .updater import Updater
from .utilities import Utilities
from .customtypes import UserType
from .wsrouter import WSRouter
basicConfig(
@@ -102,7 +102,6 @@ class PluginManager:
# await self.wait_for_server()
logger.debug("Loading plugins")
self.plugin_loader.import_plugins()
# await inject_to_tab("SP", "window.syncDeckyPlugins();")
if self.settings.getSetting("pluginOrder", None) == None:
self.settings.setSetting("pluginOrder", list(self.plugin_loader.plugins.keys()))
logger.debug("Did not find pluginOrder setting, set it to default")
@@ -19,6 +19,8 @@ import subprocess
import logging
import time
from typing import Dict, Any
"""
Constants
"""
@@ -207,3 +209,13 @@ logger: logging.Logger = logging.getLogger()
"""The main plugin logger writing to `DECKY_PLUGIN_LOG`."""
logger.setLevel(logging.INFO)
"""
Event handling
"""
# TODO better docstring im lazy
async def emit_message(message: Dict[Any, Any]) -> None:
"""
Send a message to the frontend.
"""
pass
@@ -16,6 +16,8 @@ __version__ = '0.1.0'
import logging
from typing import Dict, Any
"""
Constants
"""
@@ -171,3 +173,12 @@ Logging
logger: logging.Logger
"""The main plugin logger writing to `DECKY_PLUGIN_LOG`."""
"""
Event handling
"""
# TODO better docstring im lazy
async def emit_message(message: Dict[Any, Any]) -> None:
"""
Send a message to the frontend.
"""
+20 -3
View File
@@ -4,11 +4,12 @@ from logging import getLogger
from os import path
from multiprocessing import Process
from .sandboxed_plugin import SandboxedPlugin
from .method_call_request import MethodCallRequest
from ..localplatform.localsocket import LocalSocket
from typing import Any, Callable, Coroutine, Dict
from typing import Any, Callable, Coroutine, Dict, List
class PluginWrapper:
def __init__(self, file: str, plugin_directory: str, plugin_path: str) -> None:
@@ -39,6 +40,8 @@ class PluginWrapper:
self.emitted_message_callback: Callable[[Dict[Any, Any]], Coroutine[Any, Any, Any]]
self.legacy_method_warning = False
def __str__(self) -> str:
return self.name
@@ -58,13 +61,27 @@ class PluginWrapper:
def set_emitted_message_callback(self, callback: Callable[[Dict[Any, Any]], Coroutine[Any, Any, Any]]):
self.emitted_message_callback = callback
async def execute_method(self, method_name: str, kwargs: Dict[Any, Any]):
async def execute_legacy_method(self, method_name: str, kwargs: Dict[Any, Any]):
if not self.legacy_method_warning:
self.legacy_method_warning = True
self.log.warn(f"Plugin {self.name} is using legacy method calls. This will be removed in a future release.")
if self.passive:
raise RuntimeError("This plugin is passive (aka does not implement main.py)")
request = MethodCallRequest()
await self._socket.get_socket_connection()
await self._socket.write_single_line(dumps({ "method": method_name, "args": kwargs, "id": request.id }, ensure_ascii=False))
await self._socket.write_single_line(dumps({ "method": method_name, "args": kwargs, "id": request.id, "legacy": True }, ensure_ascii=False))
self._method_call_requests[request.id] = request
return await request.wait_for_result()
async def execute_method(self, method_name: str, args: List[Any]):
if self.passive:
raise RuntimeError("This plugin is passive (aka does not implement main.py)")
request = MethodCallRequest()
await self._socket.get_socket_connection()
await self._socket.write_single_line(dumps({ "method": method_name, "args": args, "id": request.id }, ensure_ascii=False))
self._method_call_requests[request.id] = request
return await request.wait_for_result()
+21 -11
View File
@@ -77,17 +77,28 @@ class SandboxedPlugin:
keys = [key for key in sysmodules if key.startswith("decky_loader.")]
for key in keys:
sysmodules[key.replace("decky_loader.", "")] = sysmodules[key]
from .imports import decky
async def emit_message(message: Dict[Any, Any]):
await self._socket.write_single_line_server(dumps({
"id": "0",
"payload": message
}))
# copy the docstring over so we don't have to duplicate it
emit_message.__doc__ = decky.emit_message.__doc__
decky.emit_message = emit_message
sysmodules["decky"] = decky
# provided for compatibility
sysmodules["decky_plugin"] = decky
spec = spec_from_file_location("_", self.file)
assert spec is not None
module = module_from_spec(spec)
assert spec.loader is not None
spec.loader.exec_module(module)
# TODO fix self weirdness once plugin.json versioning is done. need this before WS release!
self.Plugin = module.Plugin
setattr(self.Plugin, "emit_message", self.emit_message)
#TODO: Find how to put emit_message on global namespace so it doesn't pollute Plugin
if hasattr(self.Plugin, "_migration"):
get_event_loop().run_until_complete(self.Plugin._migration(self.Plugin))
if hasattr(self.Plugin, "_main"):
@@ -124,15 +135,14 @@ class SandboxedPlugin:
d: SocketResponseDict = {"res": None, "success": True, "id": data["id"]}
try:
d["res"] = await getattr(self.Plugin, data["method"])(self.Plugin, **data["args"])
if data["legacy"]:
# Legacy kwargs
d["res"] = await getattr(self.Plugin, data["method"])(self.Plugin, **data["args"])
else:
# New args
d["res"] = await getattr(self.Plugin, data["method"])(self.Plugin, *data["args"])
except Exception as e:
d["res"] = str(e)
d["success"] = False
finally:
return dumps(d, ensure_ascii=False)
async def emit_message(self, message: Dict[Any, Any]):
await self._socket.write_single_line_server(dumps({
"id": "0",
"payload": message
}))
return dumps(d, ensure_ascii=False)
+9 -15
View File
@@ -59,10 +59,6 @@ class Utilities:
self.rdt_proxy_task = None
if context:
context.web_app.add_routes([
web.post("/methods/{method_name}", self._handle_server_method_call)
])
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)
@@ -81,22 +77,20 @@ class Utilities:
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_user_info", self.get_user_info)
context.ws.add_route("utilities/http_request", self.http_request)
context.ws.add_route("utilities/_call_legacy_utility", self._call_legacy_utility)
async def _handle_server_method_call(self, request: web.Request):
method_name = request.match_info["method_name"]
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");
res: Dict[Any, Any] = {}
try:
args = await request.json()
except JSONDecodeError:
args = {}
res = {}
try:
r = await self.util_methods[method_name](**args)
r = await self.util_methods[method_name](**kwargs)
res["result"] = r
res["success"] = True
except Exception as e:
res["result"] = str(e)
res["success"] = False
return web.json_response(res)
return res
async def install_plugin(self, artifact: str="", name: str="No name", version: str="dev", hash: str="", install_type: PluginInstallType=PluginInstallType.INSTALL):
return await self.context.plugin_browser.request_plugin_install(
@@ -121,9 +115,9 @@ class Utilities:
async def uninstall_plugin(self, name: str):
return await self.context.plugin_browser.uninstall_plugin(name)
async def http_request(self, method: str="", url: str="", **kwargs: Any):
async def http_request(self, method: str, url: str, extra_opts: Any = {}):
async with ClientSession() as web:
res = await web.request(method, url, ssl=helpers.get_ssl_context(), **kwargs)
res = await web.request(method, url, ssl=helpers.get_ssl_context(), **extra_opts)
text = await res.text()
return {
"status": res.status,
+2 -2
View File
@@ -12,7 +12,7 @@ from dataclasses import dataclass
from traceback import format_exc
from helpers import get_csrf_token
from .helpers import get_csrf_token
class MessageType(IntEnum):
ERROR = -1
@@ -43,7 +43,7 @@ Route = Callable[..., Coroutine[Any, Any, Any]]
class WSRouter:
def __init__(self, loop: AbstractEventLoop, server_instance: Application) -> None:
self.loop = loop
self.ws: WebSocketResponse | None
self.ws: WebSocketResponse | None = None
self.instance_id = 0
self.routes: Dict[str, Route] = {}
# self.subscriptions: Dict[str, Callable[[Any]]] = {}
+1 -2
View File
@@ -6,8 +6,7 @@ license = "GPLv2"
authors = []
packages = [
{include = "decky_loader"},
{include = "decky_plugin.py"},
{include = "decky_plugin.pyi"},
{include = "decky_loader/main.py"}
]
include = ["decky_loader/static/*"]
@@ -35,6 +35,8 @@ async function reinstallPlugin(pluginName: string, currentVersion?: string) {
type PluginTableData = PluginData & { name: string; hidden: boolean; onHide(): void; onShow(): void };
const reloadPluginBackend = window.DeckyBackend.callable<[pluginName: string], void>('loader/reload_plugin');
function PluginInteractables(props: { entry: ReorderableEntry<PluginTableData> }) {
const { t } = useTranslation();
@@ -49,15 +51,9 @@ function PluginInteractables(props: { entry: ReorderableEntry<PluginTableData> }
showContextMenu(
<Menu label={t('PluginListIndex.plugin_actions')}>
<MenuItem
onSelected={() => {
onSelected={async () => {
try {
fetch(`http://127.0.0.1:1337/plugins/${name}/reload`, {
method: 'POST',
credentials: 'include',
headers: {
Authentication: window.deckyAuthToken,
},
});
await reloadPluginBackend(name);
} catch (err) {
console.error('Error Reloading Plugin Backend', err);
}
+14
View File
@@ -18,6 +18,16 @@ export const debug = (name: string, ...args: any[]) => {
);
};
export const warn = (name: string, ...args: any[]) => {
console.warn(
`%c Decky %c ${name} %c`,
'background: #16a085; color: black;',
'background: #ffbb00; color: black;',
'color: blue;',
...args,
);
};
export const error = (name: string, ...args: any[]) => {
console.error(
`%c Decky %c ${name} %c`,
@@ -41,6 +51,10 @@ class Logger {
debug(this.name, ...args);
}
warn(...args: any[]) {
warn(this.name, ...args);
}
error(...args: any[]) {
error(this.name, ...args);
}
+100 -50
View File
@@ -5,6 +5,7 @@ import {
Patch,
QuickAccessTab,
Router,
findSP,
quickAccessMenuClasses,
showModal,
sleep,
@@ -60,7 +61,6 @@ class PluginLoader extends Logger {
constructor() {
super(PluginLoader.name);
this.tabsHook.init();
this.log('Initialized');
const TabBadge = () => {
const { updates, hasLoaderUpdate } = useDeckyState();
@@ -102,9 +102,32 @@ class PluginLoader extends Logger {
initFilepickerPatches();
this.getUserInfo();
Promise.all([this.getUserInfo(), this.updateVersion()])
.then(() => this.loadPlugins())
.then(() => this.checkPluginUpdates())
.then(() => this.log('Initialized'));
}
this.updateVersion();
private getPluginsFromBackend = window.DeckyBackend.callable<[], { name: string; version: string }[]>(
'loader/get_plugins',
);
private async loadPlugins() {
// wait for SP window to exist before loading plugins
while (!findSP()) {
await sleep(100);
}
const plugins = await this.getPluginsFromBackend();
const pluginLoadPromises = [];
const loadStart = performance.now();
for (const plugin of plugins) {
if (!this.hasPlugin(plugin.name)) pluginLoadPromises.push(this.importPlugin(plugin.name, plugin.version, false));
}
await Promise.all(pluginLoadPromises);
const loadEnd = performance.now();
this.log(`Loaded ${plugins.length} plugins in ${loadEnd - loadStart}ms`);
this.checkPluginUpdates();
}
public async getUserInfo() {
@@ -217,9 +240,9 @@ class PluginLoader extends Logger {
if (val) import('./developer').then((developer) => developer.startup());
});
//* Grab and set plugin order
// Grab and set plugin order
getSetting<string[]>('pluginOrder', []).then((pluginOrder) => {
console.log(pluginOrder);
this.debug('pluginOrder: ', pluginOrder);
this.deckyState.setPluginOrder(pluginOrder);
});
@@ -236,15 +259,14 @@ class PluginLoader extends Logger {
}
public unloadPlugin(name: string) {
console.log('Plugin List: ', this.plugins);
const plugin = this.plugins.find((plugin) => plugin.name === name);
plugin?.onDismount?.();
this.plugins = this.plugins.filter((p) => p !== plugin);
this.deckyState.setPlugins(this.plugins);
}
public async importPlugin(name: string, version?: string | undefined) {
if (this.reloadLock) {
public async importPlugin(name: string, version?: string | undefined, useQueue: boolean = true) {
if (useQueue && this.reloadLock) {
this.log('Reload currently in progress, adding to queue', name);
this.pluginReloadQueue.push({ name, version: version });
return;
@@ -255,17 +277,21 @@ class PluginLoader extends Logger {
this.log(`Trying to load ${name}`);
this.unloadPlugin(name);
const startTime = performance.now();
await this.importReactPlugin(name, version);
const endTime = performance.now();
this.deckyState.setPlugins(this.plugins);
this.log(`Loaded ${name}`);
this.log(`Loaded ${name} in ${endTime - startTime}ms`);
} catch (e) {
throw e;
} finally {
this.reloadLock = false;
const nextPlugin = this.pluginReloadQueue.shift();
if (nextPlugin) {
this.importPlugin(nextPlugin.name, nextPlugin.version);
if (useQueue) {
this.reloadLock = false;
const nextPlugin = this.pluginReloadQueue.shift();
if (nextPlugin) {
this.importPlugin(nextPlugin.name, nextPlugin.version);
}
}
}
}
@@ -337,17 +363,14 @@ class PluginLoader extends Logger {
}
async callServerMethod(methodName: string, args = {}) {
const response = await fetch(`http://127.0.0.1:1337/methods/${methodName}`, {
method: 'POST',
credentials: 'include',
headers: {
'Content-Type': 'application/json',
Authentication: window.deckyAuthToken,
},
body: JSON.stringify(args),
});
return response.json();
this.warn(
`Calling ${methodName} via callServerMethod, which is deprecated and will be removed in a future release. Please switch to the backend API.`,
);
return await window.DeckyBackend.call<[methodName: string, kwargs: any], any>(
'utilities/_call_legacy_utility',
methodName,
args,
);
}
openFilePicker(
@@ -355,7 +378,7 @@ class PluginLoader extends Logger {
selectFiles?: boolean,
regex?: RegExp,
): Promise<{ path: string; realpath: string }> {
console.warn('openFilePicker is deprecated and will be removed. Please migrate to openFilePickerV2');
this.warn('openFilePicker is deprecated and will be removed. Please migrate to openFilePickerV2');
if (selectFiles) {
return this.openFilePickerV2(FileSelectionType.FILE, startPath, true, true, regex);
} else {
@@ -405,45 +428,72 @@ class PluginLoader extends Logger {
}
createPluginAPI(pluginName: string) {
return {
const pluginAPI = {
backend: {
call<Args extends any[] = any[], Return = void>(method: string, ...args: Args): Promise<Return> {
return window.DeckyBackend.call<[pluginName: string, method: string, ...args: Args], Return>(
'loader/call_plugin_method',
pluginName,
method,
...args,
);
},
callable<Args extends any[] = any[], Return = void>(method: string): (...args: Args) => Promise<Return> {
return (...args) => pluginAPI.backend.call<Args, Return>(method, ...args);
},
},
routerHook: this.routerHook,
toaster: this.toaster,
// Legacy
callServerMethod: this.callServerMethod,
openFilePicker: this.openFilePicker,
openFilePickerV2: this.openFilePickerV2,
// Legacy
async callPluginMethod(methodName: string, args = {}) {
const response = await fetch(`http://127.0.0.1:1337/plugins/${pluginName}/methods/${methodName}`, {
method: 'POST',
credentials: 'include',
headers: {
'Content-Type': 'application/json',
Authentication: window.deckyAuthToken,
},
body: JSON.stringify({
args,
}),
});
return response.json();
return window.DeckyBackend.call<[pluginName: string, methodName: string, kwargs: any], any>(
'loader/call_legacy_plugin_method',
pluginName,
methodName,
args,
);
},
fetchNoCors(url: string, request: any = {}) {
let args = { method: 'POST', headers: {} };
const req = { ...args, ...request, url, data: request.body };
/* TODO replace with the following flow (or similar) so we can reuse the JS Fetch API
frontend --request URL only--> backend (ws method)
backend --new temporary backend URL--> frontend (ws response)
frontend <--> backend <--> target URL (over http!)
*/
async fetchNoCors(url: string, request: any = {}) {
let method: string;
const req = { headers: {}, ...request, data: request.body };
req?.body && delete req.body;
return this.callServerMethod('http_request', req);
},
executeInTab(tab: string, runAsync: boolean, code: string) {
return this.callServerMethod('execute_in_tab', {
tab,
run_async: runAsync,
code,
});
if (!request.method) {
method = 'POST';
} else {
method = request.method;
delete req.method;
}
// this is terrible but a. we're going to redo this entire method anyway and b. it was already terrible
try {
const ret = await window.DeckyBackend.call<
[method: string, url: string, extra_opts?: any],
{ status: number; headers: { [key: string]: string }; body: string }
>('utilities/http_request', method, url, req);
return { success: true, result: ret };
} catch (e) {
return { success: false, result: e?.toString() };
}
},
executeInTab: window.DeckyBackend.callable<
[tab: String, runAsync: Boolean, code: string],
{ success: boolean; result: any }
>('utilities/execute_in_tab'),
injectCssIntoTab: window.DeckyBackend.callable<[tab: string, style: string], string>(
'utilities/inject_css_into_tab',
),
removeCssFromTab: window.DeckyBackend.callable<[tab: string, cssId: string]>('utilities/remove_css_from_tab'),
};
return pluginAPI;
}
}
-18
View File
@@ -10,7 +10,6 @@ declare global {
DeckyPluginLoader: PluginLoader;
DeckyUpdater?: DeckyUpdater;
importDeckyPlugin: Function;
syncDeckyPlugins: Function;
deckyHasLoaded: boolean;
deckyHasConnectedRDT?: boolean;
deckyAuthToken: string;
@@ -53,23 +52,6 @@ declare global {
window.importDeckyPlugin = function (name: string, version: string) {
window.DeckyPluginLoader?.importPlugin(name, version);
};
window.syncDeckyPlugins = async function () {
const plugins = await (
await fetch('http://127.0.0.1:1337/plugins', {
credentials: 'include',
headers: { Authentication: window.deckyAuthToken },
})
).json();
for (const plugin of plugins) {
if (!window.DeckyPluginLoader.hasPlugin(plugin.name))
window.DeckyPluginLoader?.importPlugin(plugin.name, plugin.version);
}
window.DeckyPluginLoader.checkPluginUpdates();
};
setTimeout(() => window.syncDeckyPlugins(), 5000);
})();
export default i18n;
+4
View File
@@ -1,3 +1,5 @@
import { sleep } from 'decky-frontend-lib';
import Logger from './logger';
declare global {
@@ -161,6 +163,8 @@ export class WSRouter extends Logger {
async onError(error: any) {
this.error('WS DISCONNECTED', error);
// TODO queue up lost messages and send them once we connect again
await sleep(5000);
await this.connect();
}
}