diff options
Diffstat (limited to 'backend/decky_loader/plugin')
| -rw-r--r-- | backend/decky_loader/plugin/imports/decky.py | 221 | ||||
| -rw-r--r-- | backend/decky_loader/plugin/imports/decky.pyi | 184 | ||||
| -rw-r--r-- | backend/decky_loader/plugin/plugin.py | 23 | ||||
| -rw-r--r-- | backend/decky_loader/plugin/sandboxed_plugin.py | 32 |
4 files changed, 446 insertions, 14 deletions
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 |
