diff options
| author | jbofill <74568881+jessebofill@users.noreply.github.com> | 2025-12-30 12:29:08 -0700 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2025-12-30 13:29:08 -0600 |
| commit | 9f586a1b97cf9069fbfbeee17e3909baf9e95f66 (patch) | |
| tree | c3477abcd8edec5fdf61251748a4f3bbef165e83 /backend | |
| parent | 789851579b8eaff70c2fb9da999e86d86a2d95bd (diff) | |
| download | decky-loader-9f586a1b97cf9069fbfbeee17e3909baf9e95f66.tar.gz decky-loader-9f586a1b97cf9069fbfbeee17e3909baf9e95f66.zip | |
* implement base frontend changes necessary for plugin disabling
* implement frontend diisable functions/ modal
* plugin disable boilerplate / untested
* Feat disable plugins (#810)
* implement base frontend changes necessary for plugin disabling
* implement frontend diisable functions/ modal
---------
Co-authored-by: Jesse Bofill <jesse_bofill@yahoo.com>
* fix mistakes
* add frontend
* working plugin disable, not tested extensively
* fix uninstalled hidden plugins remaining in list
* hide plugin irrelevant plugin setting menu option when disabled
* fix hidden plugin issues
* reset disabled plugin on uninstall
* fix plugin load on reenable
* move disable settings uninstall cleanup
* add engilsh tranlsations for enable/ disable elements
* fix bug where wrong loadType can get passed to importPlugin
* show correct number of hidden plugins if plugin is both hidden and disabled
* fix: get fresh list of plugin updates when changed in settings plugin list
* fix: fix invalid semver plugin version from preventing latest updates
* retain x position when changing focus in list items that have multiple horizontal focusables
* correction to pluging version checking validation
* make sure disabled plugins get checked for updates
* show number of disabled plugins at bottom of plugin view
* add notice to update modals that disabled plugins will be enabled upon installation
* run formatter
* Update backend/decky_loader/locales/en-US.json
Co-authored-by: EMERALD <hudson.samuels@gmail.com>
* chore: correct filename typo
* chore: change disabled icon
* chore: revert accidental defsettings changes
* format
* add timeout to frontend importPlugin
if a request hangs this prevent it from blocking other plugin loads.
backend diaptch_plugin which calls this for individual plugin load (as opposed to batch) is set to 15s.
other callers of importPlugin are not using timeout, same as before.
* fix plugin update checking loop
---------
Co-authored-by: marios <marios8543@gmail.com>
Co-authored-by: EMERALD <hudson.samuels@gmail.com>
Diffstat (limited to 'backend')
| -rw-r--r-- | backend/decky_loader/browser.py | 6 | ||||
| -rw-r--r-- | backend/decky_loader/loader.py | 9 | ||||
| -rw-r--r-- | backend/decky_loader/locales/en-US.json | 21 | ||||
| -rw-r--r-- | backend/decky_loader/plugin/plugin.py | 1 | ||||
| -rw-r--r-- | backend/decky_loader/utilities.py | 38 |
5 files changed, 66 insertions, 9 deletions
diff --git a/backend/decky_loader/browser.py b/backend/decky_loader/browser.py index 975a917a..fe8ae71a 100644 --- a/backend/decky_loader/browser.py +++ b/backend/decky_loader/browser.py @@ -150,6 +150,7 @@ class PluginBrowser: # plugins_snapshot = self.plugins.copy() # snapshot_string = pformat(plugins_snapshot) # logger.debug("current plugins: %s", snapshot_string) + if name in self.plugins: logger.debug("Plugin %s was found", name) await self.plugins[name].stop(uninstall=True) @@ -345,5 +346,10 @@ class PluginBrowser: if name in plugin_order: plugin_order.remove(name) self.settings.setSetting("pluginOrder", plugin_order) + + disabled_plugins: List[str] = self.settings.getSetting("disabled_plugins", []) + if name in disabled_plugins: + disabled_plugins.remove(name) + self.settings.setSetting("disabled_plugins", disabled_plugins) logger.debug("Removed any settings for plugin %s", name) diff --git a/backend/decky_loader/loader.py b/backend/decky_loader/loader.py index e2e619f7..4574cd1d 100644 --- a/backend/decky_loader/loader.py +++ b/backend/decky_loader/loader.py @@ -78,6 +78,7 @@ class Loader: self.live_reload = live_reload self.reload_queue: ReloadQueue = Queue() self.loop.create_task(self.handle_reloads()) + self.context: PluginManager = server_instance if live_reload: self.observer = Observer() @@ -130,7 +131,7 @@ class Loader: async def get_plugins(self): plugins = list(self.plugins.values()) - return [{"name": str(i), "version": i.version, "load_type": i.load_type} for i in plugins] + return [{"name": str(i), "version": i.version, "load_type": i.load_type, "disabled": i.disabled} for i in plugins] async def handle_plugin_dist(self, request: web.Request): plugin = self.plugins[request.match_info["plugin_name"]] @@ -164,6 +165,10 @@ class Loader: await self.ws.emit(f"loader/plugin_event", {"plugin": plugin.name, "event": event, "args": args}) plugin = PluginWrapper(file, plugin_directory, self.plugin_path, plugin_emitted_event) + if hasattr(self.context, "utilities") and plugin.name in await self.context.utilities.get_setting("disabled_plugins",[]): + plugin.disabled = True + self.plugins[plugin.name] = plugin + return if plugin.name in self.plugins: if not "debug" in plugin.flags and refresh: self.logger.info(f"Plugin {plugin.name} is already loaded and has requested to not be re-loaded") @@ -183,7 +188,7 @@ class Loader: print_exc() async def dispatch_plugin(self, name: str, version: str | None, load_type: int = PluginLoadType.ESMODULE_V1.value): - await self.ws.emit("loader/import_plugin", name, version, load_type) + await self.ws.emit("loader/import_plugin", name, version, load_type, True, 15000) async def import_plugins(self): self.logger.info(f"import plugins from {self.plugin_path}") diff --git a/backend/decky_loader/locales/en-US.json b/backend/decky_loader/locales/en-US.json index 836f4878..1f87fe3b 100644 --- a/backend/decky_loader/locales/en-US.json +++ b/backend/decky_loader/locales/en-US.json @@ -102,6 +102,7 @@ }, "no_hash": "This plugin does not have a hash, you are installing it at your own risk.", "not_installed": "(not installed)", + "disabled": "The plugin will be re-enabled after installation", "overwrite": { "button_idle": "Overwrite", "button_processing": "Overwriting", @@ -133,10 +134,13 @@ "uninstall": "Uninstall", "update_all_one": "Update 1 plugin", "update_all_other": "Update {{count}} plugins", - "update_to": "Update to {{name}}" + "update_to": "Update to {{name}}", + "disable": "Disable", + "enable": "Enable" }, "PluginListLabel": { - "hidden": "Hidden from the quick access menu" + "hidden": "Hidden from the quick access menu", + "disabled": "Plugin disabled" }, "PluginLoader": { "decky_title": "Decky", @@ -152,12 +156,23 @@ "desc": "Are you sure you want to uninstall {{name}}?", "title": "Uninstall {{name}}" }, + "plugin_disable": { + "button": "Disable", + "desc": "Are you sure you want to disable {{name}}?", + "title": "Disable {{name}}", + "error": "Error disabling {{name}}" + }, + "plugin_enable": { + "error": "Error enabling {{name}}" + }, "plugin_update_one": "Updates available for 1 plugin!", "plugin_update_other": "Updates available for {{count}} plugins!" }, "PluginView": { "hidden_one": "1 plugin is hidden from this list", - "hidden_other": "{{count}} plugins are hidden from this list" + "hidden_other": "{{count}} plugins are hidden from this list", + "disabled_one": "1 plugin is disabled", + "disabled_other": "{{count}} plugins are disabled" }, "RemoteDebugging": { "remote_cef": { diff --git a/backend/decky_loader/plugin/plugin.py b/backend/decky_loader/plugin/plugin.py index 61de4b1f..a7edaa45 100644 --- a/backend/decky_loader/plugin/plugin.py +++ b/backend/decky_loader/plugin/plugin.py @@ -41,6 +41,7 @@ class PluginWrapper: self.author = json["author"] self.flags = json["flags"] self.api_version = json["api_version"] if "api_version" in json else 0 + self.disabled = False self.passive = not path.isfile(self.file) diff --git a/backend/decky_loader/utilities.py b/backend/decky_loader/utilities.py index 69c69fe6..75593fd5 100644 --- a/backend/decky_loader/utilities.py +++ b/backend/decky_loader/utilities.py @@ -1,5 +1,5 @@ from __future__ import annotations -from os import stat_result +from os import path, stat_result import uuid from urllib.parse import unquote from json.decoder import JSONDecodeError @@ -8,7 +8,7 @@ import re from traceback import format_exc from stat import FILE_ATTRIBUTE_HIDDEN # pyright: ignore [reportAttributeAccessIssue, reportUnknownVariableType] -from asyncio import StreamReader, StreamWriter, start_server, gather, open_connection +from asyncio import StreamReader, StreamWriter, sleep, start_server, gather, open_connection from aiohttp import ClientSession, hdrs from aiohttp.web import Request, StreamResponse, Response, json_response, post from typing import TYPE_CHECKING, Callable, Coroutine, Dict, Any, List, TypedDict @@ -80,6 +80,8 @@ class Utilities: context.ws.add_route("utilities/restart_webhelper", self.restart_webhelper) context.ws.add_route("utilities/close_cef_socket", self.close_cef_socket) context.ws.add_route("utilities/_call_legacy_utility", self._call_legacy_utility) + context.ws.add_route("utilities/enable_plugin", self.enable_plugin) + context.ws.add_route("utilities/disable_plugin", self.disable_plugin) context.web_app.add_routes([ post("/methods/{method_name}", self._handle_legacy_server_method_call) @@ -214,7 +216,7 @@ class Utilities: async def http_request_legacy(self, method: str, url: str, extra_opts: Any = {}, timeout: int | None = None): async with ClientSession() as web: - res = await web.request(method, url, ssl=helpers.get_ssl_context(), timeout=timeout, **extra_opts) + res = await web.request(method, url, ssl=helpers.get_ssl_context(), timeout=timeout, **extra_opts) # type: ignore text = await res.text() return { "status": res.status, @@ -390,7 +392,6 @@ class Utilities: "total": len(all), } - # Based on https://stackoverflow.com/a/46422554/13174603 def start_rdt_proxy(self, ip: str, port: int): async def pipe(reader: StreamReader, writer: StreamWriter): @@ -474,3 +475,32 @@ class Utilities: async def get_tab_id(self, name: str): return (await get_tab(name)).id + + async def disable_plugin(self, name: str): + disabled_plugins: List[str] = await self.get_setting("disabled_plugins", []) + if name not in disabled_plugins: + disabled_plugins.append(name) + await self.set_setting("disabled_plugins", disabled_plugins) + + await self.context.plugin_loader.plugins[name].stop() + await self.context.ws.emit("loader/disable_plugin", name) + + async def enable_plugin(self, name: str): + plugin_folder = self.context.plugin_browser.find_plugin_folder(name) + assert plugin_folder is not None + plugin_dir = path.join(self.context.plugin_browser.plugin_path, plugin_folder) + + if name in self.context.plugin_loader.plugins: + plugin = self.context.plugin_loader.plugins[name] + if plugin.proc and plugin.proc.is_alive(): + await plugin.stop() + self.context.plugin_loader.plugins.pop(name, None) + await sleep(1) + + disabled_plugins: List[str] = await self.get_setting("disabled_plugins", []) + + if name in disabled_plugins: + disabled_plugins.remove(name) + await self.set_setting("disabled_plugins", disabled_plugins) + + await self.context.plugin_loader.import_plugin(path.join(plugin_dir, "main.py"), plugin_folder)
\ No newline at end of file |
