From 6522ebf0cad1723a278144b6c5d8557cd47e52d6 Mon Sep 17 00:00:00 2001 From: AAGaming Date: Sat, 30 Dec 2023 00:46:59 -0500 Subject: 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. --- backend/decky_loader/helpers.py | 2 +- backend/decky_loader/loader.py | 52 +++--- backend/decky_loader/main.py | 3 +- backend/decky_loader/plugin/imports/decky.py | 221 ++++++++++++++++++++++++ backend/decky_loader/plugin/imports/decky.pyi | 184 ++++++++++++++++++++ backend/decky_loader/plugin/plugin.py | 23 ++- backend/decky_loader/plugin/sandboxed_plugin.py | 32 ++-- backend/decky_loader/utilities.py | 24 +-- backend/decky_loader/wsrouter.py | 4 +- backend/decky_plugin.py | 209 ---------------------- backend/decky_plugin.pyi | 173 ------------------- backend/pyproject.toml | 3 +- 12 files changed, 488 insertions(+), 442 deletions(-) create mode 100644 backend/decky_loader/plugin/imports/decky.py create mode 100644 backend/decky_loader/plugin/imports/decky.pyi delete mode 100644 backend/decky_plugin.py delete mode 100644 backend/decky_plugin.pyi (limited to 'backend') 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]]] = {} diff --git a/backend/decky_plugin.py b/backend/decky_plugin.py deleted file mode 100644 index 35b872a0..00000000 --- a/backend/decky_plugin.py +++ /dev/null @@ -1,209 +0,0 @@ -""" -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 - -""" -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) diff --git a/backend/decky_plugin.pyi b/backend/decky_plugin.pyi deleted file mode 100644 index b311a55a..00000000 --- a/backend/decky_plugin.pyi +++ /dev/null @@ -1,173 +0,0 @@ -""" -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 - -""" -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`.""" diff --git a/backend/pyproject.toml b/backend/pyproject.toml index 9fbd2389..5c8ef169 100644 --- a/backend/pyproject.toml +++ b/backend/pyproject.toml @@ -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/*"] -- cgit v1.2.3