summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authormarios <marios8543@gmail.com>2022-04-18 15:57:51 +0300
committerGitHub <noreply@github.com>2022-04-18 15:57:51 +0300
commitfa776f0d0b7b4ca7071f0aa912be84e81caf6b61 (patch)
tree535f6f0bb71ab97f17a0ef21eb8f0491ae4ca87d
parent4576fed01baac066a8550018a0922739cc2f8985 (diff)
downloaddecky-loader-fa776f0d0b7b4ca7071f0aa912be84e81caf6b61.tar.gz
decky-loader-fa776f0d0b7b4ca7071f0aa912be84e81caf6b61.zip
Callsigns (#37)v1.2.0
* Plugin callsigns, filechangehandler thread bug fix, plugin file perms - Plugins are now assigned a callsign (a random string), which they use for all internal identification, like resource fetching and method calls. This is to ensure that plugins only access their own resources and methods. - Made FileChangeHandler send off events to a queue, that is then consumed by the Loader, instead of calling import_plugin on its own, since that caused weird issues with the event loop and the thread watchdog is using. - Plugins are now owned by root and have read-only permissions. This is handled automatically. * Improved general look and feel of plugin tab * Make all plugin entries have the same padding between them * Make "No plugins installed" text look the same as "No new notifications" Co-authored-by: WerWolv <werwolv98@gmail.com>
-rw-r--r--plugin_loader/browser.py3
-rw-r--r--plugin_loader/loader.py52
-rw-r--r--plugin_loader/main.py36
-rw-r--r--plugin_loader/plugin.py16
-rw-r--r--plugin_loader/static/plugin_page.js32
-rw-r--r--plugin_loader/templates/plugin_view.html96
-rw-r--r--requirements.txt3
7 files changed, 136 insertions, 102 deletions
diff --git a/plugin_loader/browser.py b/plugin_loader/browser.py
index 7fc8773b..ffec26b3 100644
--- a/plugin_loader/browser.py
+++ b/plugin_loader/browser.py
@@ -9,6 +9,7 @@ from concurrent.futures import ProcessPoolExecutor
from asyncio import get_event_loop
from time import time
from hashlib import sha256
+from subprocess import Popen
class PluginInstallContext:
def __init__(self, gh_url, version, hash) -> None:
@@ -35,6 +36,8 @@ class PluginBrowser:
zip_file = ZipFile(zip)
zip_file.extractall(self.plugin_path)
rename(path.join(self.plugin_path, zip_file.namelist()[0]), path.join(self.plugin_path, name))
+ Popen(["chown", "-R", "deck:deck", self.plugin_path])
+ Popen(["chmod", "-R", "555", self.plugin_path])
return True
async def _install(self, artifact, version, hash):
diff --git a/plugin_loader/loader.py b/plugin_loader/loader.py
index ae609301..f060b5b6 100644
--- a/plugin_loader/loader.py
+++ b/plugin_loader/loader.py
@@ -2,19 +2,21 @@ from aiohttp import web
from aiohttp_jinja2 import template
from watchdog.observers.polling import PollingObserver as Observer
from watchdog.events import FileSystemEventHandler
-
+from asyncio import Queue
from os import path, listdir
from logging import getLogger
+from time import time
from injector import get_tabs, get_tab
from plugin import PluginWrapper
+from traceback import print_exc
class FileChangeHandler(FileSystemEventHandler):
- def __init__(self, loader, plugin_path) -> None:
+ def __init__(self, queue, plugin_path) -> None:
super().__init__()
self.logger = getLogger("file-watcher")
- self.loader : Loader = loader
self.plugin_path = plugin_path
+ self.queue = queue
def on_created(self, event):
src_path = event.src_path
@@ -31,7 +33,7 @@ class FileChangeHandler(FileSystemEventHandler):
rel_path = path.relpath(src_path, path.commonprefix([self.plugin_path, src_path]))
plugin_dir = path.split(rel_path)[0]
main_file_path = path.join(self.plugin_path, plugin_dir, "main.py")
- self.loader.import_plugin(main_file_path, plugin_dir, refresh=True)
+ self.queue.put_nowait((main_file_path, plugin_dir, True))
def on_modified(self, event):
src_path = event.src_path
@@ -46,7 +48,7 @@ class FileChangeHandler(FileSystemEventHandler):
# file that changed is not necessarily the one that needs to be reloaded
self.logger.debug(f"file modified: {src_path}")
plugin_dir = path.split(path.relpath(src_path, path.commonprefix([self.plugin_path, src_path])))[0]
- self.loader.import_plugin(path.join(self.plugin_path, plugin_dir, "main.py"), plugin_dir, refresh=True)
+ self.queue.put_nowait((path.join(self.plugin_path, plugin_dir, "main.py"), plugin_dir, True))
class Loader:
def __init__(self, server_instance, plugin_path, loop, live_reload=False) -> None:
@@ -55,16 +57,18 @@ class Loader:
self.plugin_path = plugin_path
self.logger.info(f"plugin_path: {self.plugin_path}")
self.plugins = {}
+ self.callsigns = {}
self.import_plugins()
if live_reload:
+ self.reload_queue = Queue()
self.observer = Observer()
- self.observer.schedule(FileChangeHandler(self, plugin_path), self.plugin_path, recursive=True)
+ self.observer.schedule(FileChangeHandler(self.reload_queue, plugin_path), self.plugin_path, recursive=True)
self.observer.start()
+ self.loop.create_task(self.handle_reloads())
server_instance.add_routes([
web.get("/plugins/iframe", self.plugin_iframe_route),
- web.get("/plugins/reload", self.reload_plugins),
web.get("/plugins/load_main/{name}", self.load_plugin_main_view),
web.get("/plugins/plugin_resource/{name}/{path:.+}", self.handle_sub_route),
web.get("/plugins/load_tile/{name}", self.load_plugin_tile_view),
@@ -75,18 +79,23 @@ class Loader:
try:
plugin = PluginWrapper(file, plugin_directory, self.plugin_path)
if plugin.name in self.plugins:
- if not "hot_reload" in plugin.flags and refresh:
+ 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")
return
else:
- self.plugins[plugin.name].stop(self.loop)
+ self.plugins[plugin.name].stop()
self.plugins.pop(plugin.name, None)
+ self.callsigns.pop(plugin.callsign, None)
if plugin.passive:
self.logger.info(f"Plugin {plugin.name} is passive")
- self.plugins[plugin.name] = plugin.start(self.loop)
+ callsign = str(time())
+ plugin.callsign = callsign
+ self.plugins[plugin.name] = plugin.start()
+ self.callsigns[callsign] = plugin
self.logger.info(f"Loaded {plugin.name}")
except Exception as e:
self.logger.error(f"Could not load {file}. {e}")
+ print_exc()
finally:
if refresh:
self.loop.create_task(self.refresh_iframe())
@@ -99,14 +108,15 @@ class Loader:
self.logger.info(f"found plugin: {directory}")
self.import_plugin(path.join(self.plugin_path, directory, "main.py"), directory)
- async def reload_plugins(self, request=None):
- self.logger.info("Re-importing plugins.")
- self.import_plugins()
+ async def handle_reloads(self):
+ while True:
+ args = await self.reload_queue.get()
+ self.import_plugin(*args)
- async def handle_plugin_method_call(self, plugin_name, method_name, **kwargs):
+ async def handle_plugin_method_call(self, callsign, method_name, **kwargs):
if method_name.startswith("_"):
raise RuntimeError("Tried to call private method")
- return await self.plugins[plugin_name].execute_method(method_name, kwargs)
+ return await self.callsigns[callsign].execute_method(method_name, kwargs)
async def get_steam_resource(self, request):
tab = (await get_tabs())[0]
@@ -116,7 +126,7 @@ class Loader:
return web.Response(text=str(e), status=400)
async def load_plugin_main_view(self, request):
- plugin = self.plugins[request.match_info["name"]]
+ plugin = self.callsigns[request.match_info["name"]]
# open up the main template
with open(path.join(self.plugin_path, plugin.plugin_directory, plugin.main_view_html), 'r') as template:
@@ -124,14 +134,14 @@ class Loader:
# setup the main script, plugin, and pull in the template
ret = f"""
<script src="/static/library.js"></script>
- <script>const plugin_name = '{plugin.name}' </script>
- <base href="http://127.0.0.1:1337/plugins/plugin_resource/{plugin.name}/">
+ <script>const plugin_name = '{plugin.callsign}' </script>
+ <base href="http://127.0.0.1:1337/plugins/plugin_resource/{plugin.callsign}/">
{template_data}
"""
return web.Response(text=ret, content_type="text/html")
async def handle_sub_route(self, request):
- plugin = self.plugins[request.match_info["name"]]
+ plugin = self.callsigns[request.match_info["name"]]
route_path = request.match_info["path"]
self.logger.info(path)
@@ -144,7 +154,7 @@ class Loader:
return web.Response(text=ret)
async def load_plugin_tile_view(self, request):
- plugin = self.plugins[request.match_info["name"]]
+ plugin = self.callsigns[request.match_info["name"]]
inner_content = ""
@@ -160,7 +170,7 @@ class Loader:
<head>
<link rel="stylesheet" href="/static/styles.css">
<script src="/static/library.js"></script>
- <script>const plugin_name = '{plugin.name}';</script>
+ <script>const plugin_name = '{plugin.callsign}';</script>
</head>
<body style="height: fit-content; display: block;">
{inner_content}
diff --git a/plugin_loader/main.py b/plugin_loader/main.py
index 37a4ad1d..638457a4 100644
--- a/plugin_loader/main.py
+++ b/plugin_loader/main.py
@@ -18,15 +18,19 @@ from jinja2 import FileSystemLoader
from os import path
from asyncio import get_event_loop, sleep
from json import loads, dumps
+from subprocess import Popen
from loader import Loader
from injector import inject_to_tab, get_tab, tab_has_element
from utilities import Utilities
from browser import PluginBrowser
-
logger = getLogger("Main")
+async def chown_plugin_dir(_):
+ Popen(["chown", "-R", "deck:deck", CONFIG["plugin_path"]])
+ Popen(["chmod", "-R", "555", CONFIG["plugin_path"]])
+
class PluginManager:
def __init__(self) -> None:
self.loop = get_event_loop()
@@ -37,6 +41,7 @@ class PluginManager:
jinja_setup(self.web_app, loader=FileSystemLoader(path.join(path.dirname(__file__), 'templates')))
self.web_app.on_startup.append(self.inject_javascript)
+ self.web_app.on_startup.append(chown_plugin_dir)
self.web_app.add_routes([static("/static", path.join(path.dirname(__file__), 'static'))])
self.loop.create_task(self.method_call_listener())
self.loop.create_task(self.loader_reinjector())
@@ -47,6 +52,21 @@ class PluginManager:
if context["message"] == "Unclosed connection":
return
loop.default_exception_handler(context)
+
+ async def loader_reinjector(self):
+ while True:
+ await sleep(1)
+ if not await tab_has_element("QuickAccess", "plugin_iframe"):
+ logger.info("Plugin loader isn't present in Steam anymore, reinjecting...")
+ await self.inject_javascript()
+
+ async def inject_javascript(self, request=None):
+ try:
+ await inject_to_tab("QuickAccess", open(path.join(path.dirname(__file__), "static/library.js"), "r").read())
+ await inject_to_tab("QuickAccess", open(path.join(path.dirname(__file__), "static/plugin_page.js"), "r").read())
+ except:
+ logger.info("Failed to inject JavaScript into tab")
+ pass
async def resolve_method_call(self, tab, call_id, response):
await tab._send_devtools_cmd({
@@ -88,20 +108,6 @@ class PluginManager:
if not "id" in data and data["method"] == "Runtime.consoleAPICalled" and data["params"]["type"] == "debug":
method = loads(data["params"]["args"][0]["value"])
self.loop.create_task(self.handle_method_call(method, tab))
-
- async def loader_reinjector(self):
- while True:
- await sleep(1)
- if not await tab_has_element("QuickAccess", "plugin_iframe"):
- logger.info("Plugin loader isn't present in Steam anymore, reinjecting...")
- await self.inject_javascript()
-
- async def inject_javascript(self, request=None):
- try:
- await inject_to_tab("QuickAccess", open(path.join(path.dirname(__file__), "static/plugin_page.js"), "r").read())
- except:
- logger.info("Failed to inject JavaScript into tab")
- pass
def run(self):
return run_app(self.web_app, host=CONFIG["server_host"], port=CONFIG["server_port"], loop=self.loop, access_log=None)
diff --git a/plugin_loader/plugin.py b/plugin_loader/plugin.py
index 033d36fc..0f8880f3 100644
--- a/plugin_loader/plugin.py
+++ b/plugin_loader/plugin.py
@@ -1,5 +1,5 @@
from importlib.util import spec_from_file_location, module_from_spec
-from asyncio import get_event_loop, start_unix_server, open_unix_connection, sleep, Lock
+from asyncio import get_event_loop, new_event_loop, set_event_loop, start_unix_server, open_unix_connection, sleep, Lock
from os import path, setuid
from json import loads, dumps, load
from concurrent.futures import ProcessPoolExecutor
@@ -25,6 +25,7 @@ class PluginWrapper:
self.passive = not path.isfile(self.file)
def _init(self):
+ set_event_loop(new_event_loop())
if self.passive:
return
setuid(0 if "root" in self.flags else 1000)
@@ -46,6 +47,8 @@ class PluginWrapper:
data = loads((await reader.readline()).decode("utf-8"))
if "stop" in data:
get_event_loop().stop()
+ while get_event_loop().is_running():
+ await sleep(0)
get_event_loop().close()
return
d = {"res": None, "success": True}
@@ -67,17 +70,16 @@ class PluginWrapper:
except:
await sleep(0)
- def start(self, loop):
+ def start(self):
if self.passive:
return self
- executor = ProcessPoolExecutor()
- loop.run_in_executor(
- executor,
+ get_event_loop().run_in_executor(
+ ProcessPoolExecutor(),
self._init
)
return self
- def stop(self, loop):
+ def stop(self):
if self.passive:
return
async def _(self):
@@ -85,7 +87,7 @@ class PluginWrapper:
self.writer.write((dumps({"stop": True})+"\n").encode("utf-8"))
await self.writer.drain()
self.writer.close()
- loop.create_task(_(self))
+ get_event_loop().create_task(_(self))
async def execute_method(self, method_name, kwargs):
if self.passive:
diff --git a/plugin_loader/static/plugin_page.js b/plugin_loader/static/plugin_page.js
index 62c24bfe..0531f04e 100644
--- a/plugin_loader/static/plugin_page.js
+++ b/plugin_loader/static/plugin_page.js
@@ -19,20 +19,28 @@ function installPlugin(request_id) {
function addPluginInstallPrompt(artifact, version, request_id) {
let text = `
- <div id="plugin_install_prompt_${request_id}" style="display: block; background: #304375; border-radius: 5px;">
- <h3 style="padding-left: 1rem;">Install plugin</h3>
- <ul style="padding-left: 10px; padding-right: 10px; padding-bottom: 20px; margin: 0;">
- <li>${artifact}</li>
- <li>${version}</li>
- </ul>
- <div style="text-align: center; padding-bottom: 10px;">
- <button onclick="installPlugin('${request_id}')" style="display: inline-block; background-color: green;">Install</button>
- <button onclick="document.getElementById('plugin_install_list').removeChild(document.getElementById('plugin_install_prompt_${request_id}'))"
- style="display: inline-block; background-color: red;">Ignore</button>
- </div>
+ <link rel="stylesheet" href="/static/styles.css">
+
+ <div id="plugin_install_prompt_${request_id}" style="background-color: #0c131b; display: block; border: 1px solid #22262f; box-shadow: 0px 0px 8px #202020; width: calc(100% - 50px); padding: 0px 10px 10px 10px;">
+ <h3>Install Plugin?</h3>
+ <p style="font-size: 12px;">
+ ${artifact}
+ Version: ${version}
+ </p>
+ <button type="button" tabindex="0" class="DialogButton _DialogLayout Secondary basicdialog_Button_1Ievp Focusable"
+ onclick="installPlugin('${request_id}')">
+ Install
+ </button>
+ <p style="margin: 2px;"></p>
+ <button type="button" tabindex="0" class="DialogButton _DialogLayout Secondary basicdialog_Button_1Ievp Focusable"
+ onclick="document.getElementById('plugin_install_list').removeChild(document.getElementById('plugin_install_prompt_${request_id}'))">
+ Cancel
+ </button>
</div>
`;
- document.getElementById('plugin_install_list').innerHTML += text;
+ document.getElementById('plugin_install_list').innerHTML = text;
+
+ execute_in_tab('SP', false, 'FocusNavController.DispatchVirtualButtonClick(28)')
}
(function () {
diff --git a/plugin_loader/templates/plugin_view.html b/plugin_loader/templates/plugin_view.html
index 6016a7ae..9d7ba1bc 100644
--- a/plugin_loader/templates/plugin_view.html
+++ b/plugin_loader/templates/plugin_view.html
@@ -7,64 +7,70 @@
});
}, false);
- function loadPlugin(name) {
+ function loadPlugin(callsign, name) {
this.parent.postMessage("PLUGIN_LOADER__"+name, "https://steamloopback.host");
- location.href = `/plugins/load_main/${name}`;
+ location.href = `/plugins/load_main/${callsign}`;
}
</script>
{% if not plugins|length %}
-<div class="basicdialog_Field_ugL9c basicdialog_WithChildrenBelow_1RjOd basicdialog_InlineWrapShiftsChildrenBelow_3a6QZ basicdialog_ExtraPaddingOnChildrenBelow_2-owv basicdialog_StandardPadding_1HrfN basicdialog_HighlightOnFocus_1xh2W Panel Focusable"
- style="--indent-level:0;">
- <div class="basicdialog_FieldChildren_279n8" style="color: white; font-size: large; padding-top: 10px;">
- No plugins installed :(
+ <div class="quickaccessmenu_TabGroupPanel_1QO7b Panel Focusable">
+ <div class="quickaccesscontrols_EmptyNotifications_3ZjbM" style="padding-top:7px;">
+ No plugins installed
+ </div>
</div>
-</div>
{% endif %}
<div class="quickaccessmenu_TabGroupPanel_1QO7b Panel Focusable">
{% for plugin in plugins %}
- {% if plugin.tile_view_html|length %}
- <div class="quickaccesscontrols_PanelSectionRow_26R5w">
- <div onclick="loadPlugin('{{ plugin.name }}')"
- class="basicdialog_Field_ugL9c basicdialog_WithChildrenBelow_1RjOd basicdialog_InlineWrapShiftsChildrenBelow_3a6QZ basicdialog_ExtraPaddingOnChildrenBelow_2-owv basicdialog_StandardPadding_1HrfN basicdialog_HighlightOnFocus_1xh2W Panel Focusable"
- style="--indent-level:0;">
- <iframe id="tile_view_iframe_{{ plugin.name }}" style="display:block; padding: 0; border: none;" scrolling="no"
- src="/plugins/load_tile/{{ plugin.name }}"></iframe>
- <script>
- (function() {
- let iframe = document.getElementById("tile_view_iframe_{{ plugin.name }}");
- tile_iframes.push(document.getElementById("tile_view_iframe_{{ plugin.name }}"));
- iframe.onload = function() {
- let html = iframe.contentWindow.document.children[0];
- let last_height = 0;
- setInterval(function() {
- let height = iframe.contentWindow.document.children[0].scrollHeight;
- if (height != last_height) {
- iframe.height = height + "px";
- last_height = height;
- }
- }, 100);
- iframe.contentWindow.document.body.onclick = function () {
- loadPlugin('{{ plugin.name }}');
- };
- }
- })();
- </script>
+ {% if plugin.tile_view_html|length %}
+ <div class="quickaccesscontrols_PanelSectionRow_26R5w">
+ <div onclick="loadPlugin('{{ plugin.callsign }}', '{{ plugin.name }}')"
+ class="basicdialog_Field_ugL9c basicdialog_WithChildrenBelow_1RjOd basicdialog_InlineWrapShiftsChildrenBelow_3a6QZ basicdialog_ExtraPaddingOnChildrenBelow_2-owv basicdialog_StandardPadding_1HrfN basicdialog_HighlightOnFocus_1xh2W Panel Focusable"
+ style="--indent-level:0; margin: 0px; padding: 0px; padding-top: 8px;">
+ <iframe id="tile_view_iframe_{{ plugin.callsign }}"
+ scrolling="no" marginwidth="0" marginheight="0"
+ hspace="0" vspace="0" frameborder="0"
+ style="border-radius: 2px;"
+ src="/plugins/load_tile/{{ plugin.callsign }}">
+ </iframe>
+ <script>
+ (function() {
+ let iframe = document.getElementById("tile_view_iframe_{{ plugin.callsign }}");
+ tile_iframes.push(document.getElementById("tile_view_iframe_{{ plugin.callsign }}"));
+
+ iframe.onload = function() {
+ let html = iframe.contentWindow.document.children[0];
+ let last_height = 0;
+
+ setInterval(function() {
+ let height = iframe.contentWindow.document.children[0].scrollHeight;
+ if (height != last_height) {
+ iframe.height = height + "px";
+ last_height = height;
+ }
+ }, 100);
+
+ iframe.contentWindow.document.body.onclick = function () {
+ loadPlugin('{{ plugin.callsign }}', '{{ plugin.name }}');
+ };
+ }
+ })();
+ </script>
+ </div>
</div>
- </div>
{% else %}
- <div class="quickaccesscontrols_PanelSectionRow_26R5w">
- <div onclick="loadPlugin('{{ plugin.name }}')"
- class="basicdialog_Field_ugL9c basicdialog_WithChildrenBelow_1RjOd basicdialog_InlineWrapShiftsChildrenBelow_3a6QZ basicdialog_ExtraPaddingOnChildrenBelow_2-owv basicdialog_StandardPadding_1HrfN basicdialog_HighlightOnFocus_1xh2W Panel Focusable"
- style="--indent-level:0;">
- <div class="basicdialog_FieldChildren_279n8">
- <button type="button" tabindex="0"
- class="DialogButton _DialogLayout Secondary basicdialog_Button_1Ievp Focusable">{{ plugin.name
- }}</button>
+ <div class="quickaccesscontrols_PanelSectionRow_26R5w">
+ <div onclick="loadPlugin('{{ plugin.callsign }}', '{{ plugin.name }}')"
+ class="basicdialog_Field_ugL9c basicdialog_WithChildrenBelow_1RjOd basicdialog_InlineWrapShiftsChildrenBelow_3a6QZ basicdialog_ExtraPaddingOnChildrenBelow_2-owv basicdialog_StandardPadding_1HrfN basicdialog_HighlightOnFocus_1xh2W Panel Focusable"
+ style="--indent-level:0; margin: 0px; padding: 0px; padding-top: 8px;">
+ <div class="basicdialog_FieldChildren_279n8">
+ <button type="button" tabindex="0"
+ class="DialogButton _DialogLayout Secondary basicdialog_Button_1Ievp Focusable">{{ plugin.name }}
+ </button>
+ </div>
</div>
</div>
- </div>
{% endif %}
- {% endfor %}
+ {% endfor %}
</div>
diff --git a/requirements.txt b/requirements.txt
index 579ebc0b..c77a53ed 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,4 +1,3 @@
aiohttp==3.8.1
aiohttp-jinja2==1.5.0
-watchdog==2.1.7
-multiprocess==0.70.12.2 \ No newline at end of file
+watchdog==2.1.7 \ No newline at end of file