summaryrefslogtreecommitdiff
path: root/backend/decky_loader
diff options
context:
space:
mode:
Diffstat (limited to 'backend/decky_loader')
-rw-r--r--backend/decky_loader/helpers.py2
-rw-r--r--backend/decky_loader/loader.py52
-rw-r--r--backend/decky_loader/main.py3
-rw-r--r--backend/decky_loader/plugin/imports/decky.py221
-rw-r--r--backend/decky_loader/plugin/imports/decky.pyi184
-rw-r--r--backend/decky_loader/plugin/plugin.py23
-rw-r--r--backend/decky_loader/plugin/sandboxed_plugin.py32
-rw-r--r--backend/decky_loader/utilities.py24
-rw-r--r--backend/decky_loader/wsrouter.py4
9 files changed, 487 insertions, 58 deletions
diff --git a/backend/decky_loader/helpers.py b/backend/decky_loader/helpers.py
index 0cd6518b..76c3db0a 100644
--- a/backend/decky_loader/helpers.py
+++ b/backend/decky_loader/helpers.py
@@ -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)
diff --git a/backend/decky_loader/loader.py b/backend/decky_loader/loader.py
index e5a5cc7b..13e54289 100644
--- a/backend/decky_loader/loader.py
+++ b/backend/decky_loader/loader.py
@@ -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
- await self.reload_queue.put((plugin.file, plugin.plugin_directory))
+ async def handle_plugin_backend_reload(self, plugin_name: str):
+ plugin = self.plugins[plugin_name]
- return web.Response(status=200) \ No newline at end of file
+ await self.reload_queue.put((plugin.file, plugin.plugin_directory)) \ No newline at end of file
diff --git a/backend/decky_loader/main.py b/backend/decky_loader/main.py
index 71bc75f4..bf34ad17 100644
--- a/backend/decky_loader/main.py
+++ b/backend/decky_loader/main.py
@@ -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")
diff --git a/backend/decky_loader/plugin/imports/decky.py b/backend/decky_loader/plugin/imports/decky.py
new file mode 100644
index 00000000..599d142f
--- /dev/null
+++ b/backend/decky_loader/plugin/imports/decky.py
@@ -0,0 +1,221 @@
+"""
+This module exposes various constants and helpers useful for decky plugins.
+
+* Plugin's settings and configurations should be stored under `DECKY_PLUGIN_SETTINGS_DIR`.
+* Plugin's runtime data should be stored under `DECKY_PLUGIN_RUNTIME_DIR`.
+* Plugin's persistent log files should be stored under `DECKY_PLUGIN_LOG_DIR`.
+
+Avoid writing outside of `DECKY_HOME`, storing under the suggested paths is strongly recommended.
+
+Some basic migration helpers are available: `migrate_any`, `migrate_settings`, `migrate_runtime`, `migrate_logs`.
+
+A logging facility `logger` is available which writes to the recommended location.
+"""
+
+__version__ = '0.1.0'
+
+import os
+import subprocess
+import logging
+import time
+
+from typing import Dict, Any
+
+"""
+Constants
+"""
+
+HOME: str = os.getenv("HOME", default="")
+"""
+The home directory of the effective user running the process.
+Environment variable: `HOME`.
+If `root` was specified in the plugin's flags it will be `/root` otherwise the user whose home decky resides in.
+e.g.: `/home/deck`
+"""
+
+USER: str = os.getenv("USER", default="")
+"""
+The effective username running the process.
+Environment variable: `USER`.
+It would be `root` if `root` was specified in the plugin's flags otherwise the user whose home decky resides in.
+e.g.: `deck`
+"""
+
+DECKY_VERSION: str = os.getenv("DECKY_VERSION", default="")
+"""
+The version of the decky loader.
+Environment variable: `DECKY_VERSION`.
+e.g.: `v2.5.0-pre1`
+"""
+
+DECKY_USER: str = os.getenv("DECKY_USER", default="")
+"""
+The user whose home decky resides in.
+Environment variable: `DECKY_USER`.
+e.g.: `deck`
+"""
+
+DECKY_USER_HOME: str = os.getenv("DECKY_USER_HOME", default="")
+"""
+The home of the user where decky resides in.
+Environment variable: `DECKY_USER_HOME`.
+e.g.: `/home/deck`
+"""
+
+DECKY_HOME: str = os.getenv("DECKY_HOME", default="")
+"""
+The root of the decky folder.
+Environment variable: `DECKY_HOME`.
+e.g.: `/home/deck/homebrew`
+"""
+
+DECKY_PLUGIN_SETTINGS_DIR: str = os.getenv(
+ "DECKY_PLUGIN_SETTINGS_DIR", default="")
+"""
+The recommended path in which to store configuration files (created automatically).
+Environment variable: `DECKY_PLUGIN_SETTINGS_DIR`.
+e.g.: `/home/deck/homebrew/settings/decky-plugin-template`
+"""
+
+DECKY_PLUGIN_RUNTIME_DIR: str = os.getenv(
+ "DECKY_PLUGIN_RUNTIME_DIR", default="")
+"""
+The recommended path in which to store runtime data (created automatically).
+Environment variable: `DECKY_PLUGIN_RUNTIME_DIR`.
+e.g.: `/home/deck/homebrew/data/decky-plugin-template`
+"""
+
+DECKY_PLUGIN_LOG_DIR: str = os.getenv("DECKY_PLUGIN_LOG_DIR", default="")
+"""
+The recommended path in which to store persistent logs (created automatically).
+Environment variable: `DECKY_PLUGIN_LOG_DIR`.
+e.g.: `/home/deck/homebrew/logs/decky-plugin-template`
+"""
+
+DECKY_PLUGIN_DIR: str = os.getenv("DECKY_PLUGIN_DIR", default="")
+"""
+The root of the plugin's directory.
+Environment variable: `DECKY_PLUGIN_DIR`.
+e.g.: `/home/deck/homebrew/plugins/decky-plugin-template`
+"""
+
+DECKY_PLUGIN_NAME: str = os.getenv("DECKY_PLUGIN_NAME", default="")
+"""
+The name of the plugin as specified in the 'plugin.json'.
+Environment variable: `DECKY_PLUGIN_NAME`.
+e.g.: `Example Plugin`
+"""
+
+DECKY_PLUGIN_VERSION: str = os.getenv("DECKY_PLUGIN_VERSION", default="")
+"""
+The version of the plugin as specified in the 'package.json'.
+Environment variable: `DECKY_PLUGIN_VERSION`.
+e.g.: `0.0.1`
+"""
+
+DECKY_PLUGIN_AUTHOR: str = os.getenv("DECKY_PLUGIN_AUTHOR", default="")
+"""
+The author of the plugin as specified in the 'plugin.json'.
+Environment variable: `DECKY_PLUGIN_AUTHOR`.
+e.g.: `John Doe`
+"""
+
+__cur_time = time.strftime("%Y-%m-%d %H.%M.%S")
+DECKY_PLUGIN_LOG: str = os.path.join(DECKY_PLUGIN_LOG_DIR, f"{__cur_time}.log")
+"""
+The path to the plugin's main logfile.
+Environment variable: `DECKY_PLUGIN_LOG`.
+e.g.: `/home/deck/homebrew/logs/decky-plugin-template/plugin.log`
+"""
+
+"""
+Migration helpers
+"""
+
+
+def migrate_any(target_dir: str, *files_or_directories: str) -> dict[str, str]:
+ """
+ Migrate files and directories to a new location and remove old locations.
+ Specified files will be migrated to `target_dir`.
+ Specified directories will have their contents recursively migrated to `target_dir`.
+
+ Returns the mapping of old -> new location.
+ """
+ file_map: dict[str, str] = {}
+ for f in files_or_directories:
+ if not os.path.exists(f):
+ file_map[f] = ""
+ continue
+ if os.path.isdir(f):
+ src_dir = f
+ src_file = "."
+ file_map[f] = target_dir
+ else:
+ src_dir = os.path.dirname(f)
+ src_file = os.path.basename(f)
+ file_map[f] = os.path.join(target_dir, src_file)
+ subprocess.run(["sh", "-c", "mkdir -p \"$3\"; tar -cf - -C \"$1\" \"$2\" | tar -xf - -C \"$3\" && rm -rf \"$4\"",
+ "_", src_dir, src_file, target_dir, f])
+ return file_map
+
+
+def migrate_settings(*files_or_directories: str) -> dict[str, str]:
+ """
+ Migrate files and directories relating to plugin settings to the recommended location and remove old locations.
+ Specified files will be migrated to `DECKY_PLUGIN_SETTINGS_DIR`.
+ Specified directories will have their contents recursively migrated to `DECKY_PLUGIN_SETTINGS_DIR`.
+
+ Returns the mapping of old -> new location.
+ """
+ return migrate_any(DECKY_PLUGIN_SETTINGS_DIR, *files_or_directories)
+
+
+def migrate_runtime(*files_or_directories: str) -> dict[str, str]:
+ """
+ Migrate files and directories relating to plugin runtime data to the recommended location and remove old locations
+ Specified files will be migrated to `DECKY_PLUGIN_RUNTIME_DIR`.
+ Specified directories will have their contents recursively migrated to `DECKY_PLUGIN_RUNTIME_DIR`.
+
+ Returns the mapping of old -> new location.
+ """
+ return migrate_any(DECKY_PLUGIN_RUNTIME_DIR, *files_or_directories)
+
+
+def migrate_logs(*files_or_directories: str) -> dict[str, str]:
+ """
+ Migrate files and directories relating to plugin logs to the recommended location and remove old locations.
+ Specified files will be migrated to `DECKY_PLUGIN_LOG_DIR`.
+ Specified directories will have their contents recursively migrated to `DECKY_PLUGIN_LOG_DIR`.
+
+ Returns the mapping of old -> new location.
+ """
+ return migrate_any(DECKY_PLUGIN_LOG_DIR, *files_or_directories)
+
+
+"""
+Logging
+"""
+
+try:
+ for x in [entry.name for entry in sorted(os.scandir(DECKY_PLUGIN_LOG_DIR),key=lambda x: x.stat().st_mtime, reverse=True) if entry.name.endswith(".log")][4:]:
+ os.unlink(os.path.join(DECKY_PLUGIN_LOG_DIR, x))
+except Exception as e:
+ print(f"Failed to delete old logs: {str(e)}")
+
+logging.basicConfig(filename=DECKY_PLUGIN_LOG,
+ format='[%(asctime)s][%(levelname)s]: %(message)s',
+ force=True)
+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 \ No newline at end of file
diff --git a/backend/decky_loader/plugin/imports/decky.pyi b/backend/decky_loader/plugin/imports/decky.pyi
new file mode 100644
index 00000000..e68b3853
--- /dev/null
+++ b/backend/decky_loader/plugin/imports/decky.pyi
@@ -0,0 +1,184 @@
+"""
+This module exposes various constants and helpers useful for decky plugins.
+
+* Plugin's settings and configurations should be stored under `DECKY_PLUGIN_SETTINGS_DIR`.
+* Plugin's runtime data should be stored under `DECKY_PLUGIN_RUNTIME_DIR`.
+* Plugin's persistent log files should be stored under `DECKY_PLUGIN_LOG_DIR`.
+
+Avoid writing outside of `DECKY_HOME`, storing under the suggested paths is strongly recommended.
+
+Some basic migration helpers are available: `migrate_any`, `migrate_settings`, `migrate_runtime`, `migrate_logs`.
+
+A logging facility `logger` is available which writes to the recommended location.
+"""
+
+__version__ = '0.1.0'
+
+import logging
+
+from typing import Dict, Any
+
+"""
+Constants
+"""
+
+HOME: str
+"""
+The home directory of the effective user running the process.
+Environment variable: `HOME`.
+If `root` was specified in the plugin's flags it will be `/root` otherwise the user whose home decky resides in.
+e.g.: `/home/deck`
+"""
+
+USER: str
+"""
+The effective username running the process.
+Environment variable: `USER`.
+It would be `root` if `root` was specified in the plugin's flags otherwise the user whose home decky resides in.
+e.g.: `deck`
+"""
+
+DECKY_VERSION: str
+"""
+The version of the decky loader.
+Environment variable: `DECKY_VERSION`.
+e.g.: `v2.5.0-pre1`
+"""
+
+DECKY_USER: str
+"""
+The user whose home decky resides in.
+Environment variable: `DECKY_USER`.
+e.g.: `deck`
+"""
+
+DECKY_USER_HOME: str
+"""
+The home of the user where decky resides in.
+Environment variable: `DECKY_USER_HOME`.
+e.g.: `/home/deck`
+"""
+
+DECKY_HOME: str
+"""
+The root of the decky folder.
+Environment variable: `DECKY_HOME`.
+e.g.: `/home/deck/homebrew`
+"""
+
+DECKY_PLUGIN_SETTINGS_DIR: str
+"""
+The recommended path in which to store configuration files (created automatically).
+Environment variable: `DECKY_PLUGIN_SETTINGS_DIR`.
+e.g.: `/home/deck/homebrew/settings/decky-plugin-template`
+"""
+
+DECKY_PLUGIN_RUNTIME_DIR: str
+"""
+The recommended path in which to store runtime data (created automatically).
+Environment variable: `DECKY_PLUGIN_RUNTIME_DIR`.
+e.g.: `/home/deck/homebrew/data/decky-plugin-template`
+"""
+
+DECKY_PLUGIN_LOG_DIR: str
+"""
+The recommended path in which to store persistent logs (created automatically).
+Environment variable: `DECKY_PLUGIN_LOG_DIR`.
+e.g.: `/home/deck/homebrew/logs/decky-plugin-template`
+"""
+
+DECKY_PLUGIN_DIR: str
+"""
+The root of the plugin's directory.
+Environment variable: `DECKY_PLUGIN_DIR`.
+e.g.: `/home/deck/homebrew/plugins/decky-plugin-template`
+"""
+
+DECKY_PLUGIN_NAME: str
+"""
+The name of the plugin as specified in the 'plugin.json'.
+Environment variable: `DECKY_PLUGIN_NAME`.
+e.g.: `Example Plugin`
+"""
+
+DECKY_PLUGIN_VERSION: str
+"""
+The version of the plugin as specified in the 'package.json'.
+Environment variable: `DECKY_PLUGIN_VERSION`.
+e.g.: `0.0.1`
+"""
+
+DECKY_PLUGIN_AUTHOR: str
+"""
+The author of the plugin as specified in the 'plugin.json'.
+Environment variable: `DECKY_PLUGIN_AUTHOR`.
+e.g.: `John Doe`
+"""
+
+DECKY_PLUGIN_LOG: str
+"""
+The path to the plugin's main logfile.
+Environment variable: `DECKY_PLUGIN_LOG`.
+e.g.: `/home/deck/homebrew/logs/decky-plugin-template/plugin.log`
+"""
+
+"""
+Migration helpers
+"""
+
+
+def migrate_any(target_dir: str, *files_or_directories: str) -> dict[str, str]:
+ """
+ Migrate files and directories to a new location and remove old locations.
+ Specified files will be migrated to `target_dir`.
+ Specified directories will have their contents recursively migrated to `target_dir`.
+
+ Returns the mapping of old -> new location.
+ """
+
+
+def migrate_settings(*files_or_directories: str) -> dict[str, str]:
+ """
+ Migrate files and directories relating to plugin settings to the recommended location and remove old locations.
+ Specified files will be migrated to `DECKY_PLUGIN_SETTINGS_DIR`.
+ Specified directories will have their contents recursively migrated to `DECKY_PLUGIN_SETTINGS_DIR`.
+
+ Returns the mapping of old -> new location.
+ """
+
+
+def migrate_runtime(*files_or_directories: str) -> dict[str, str]:
+ """
+ Migrate files and directories relating to plugin runtime data to the recommended location and remove old locations
+ Specified files will be migrated to `DECKY_PLUGIN_RUNTIME_DIR`.
+ Specified directories will have their contents recursively migrated to `DECKY_PLUGIN_RUNTIME_DIR`.
+
+ Returns the mapping of old -> new location.
+ """
+
+
+def migrate_logs(*files_or_directories: str) -> dict[str, str]:
+ """
+ Migrate files and directories relating to plugin logs to the recommended location and remove old locations.
+ Specified files will be migrated to `DECKY_PLUGIN_LOG_DIR`.
+ Specified directories will have their contents recursively migrated to `DECKY_PLUGIN_LOG_DIR`.
+
+ Returns the mapping of old -> new location.
+ """
+
+
+"""
+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.
+ """ \ No newline at end of file
diff --git a/backend/decky_loader/plugin/plugin.py b/backend/decky_loader/plugin/plugin.py
index 6c338106..01fc048c 100644
--- a/backend/decky_loader/plugin/plugin.py
+++ b/backend/decky_loader/plugin/plugin.py
@@ -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, "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": kwargs, "id": request.id }, ensure_ascii=False))
+ 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()
diff --git a/backend/decky_loader/plugin/sandboxed_plugin.py b/backend/decky_loader/plugin/sandboxed_plugin.py
index 1c3c5bd2..d07cdc97 100644
--- a/backend/decky_loader/plugin/sandboxed_plugin.py
+++ b/backend/decky_loader/plugin/sandboxed_plugin.py
@@ -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
- })) \ No newline at end of file
+ return dumps(d, ensure_ascii=False) \ No newline at end of file
diff --git a/backend/decky_loader/utilities.py b/backend/decky_loader/utilities.py
index 12dabd68..774f47db 100644
--- a/backend/decky_loader/utilities.py
+++ b/backend/decky_loader/utilities.py
@@ -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"]
- try:
- args = await request.json()
- except JSONDecodeError:
- args = {}
- res = {}
+ 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:
- 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,
diff --git a/backend/decky_loader/wsrouter.py b/backend/decky_loader/wsrouter.py
index 034667ae..cdfe982c 100644
--- a/backend/decky_loader/wsrouter.py
+++ b/backend/decky_loader/wsrouter.py
@@ -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]]] = {}