diff options
| -rw-r--r-- | plugin_loader/browser.py | 3 | ||||
| -rw-r--r-- | plugin_loader/loader.py | 52 | ||||
| -rw-r--r-- | plugin_loader/main.py | 36 | ||||
| -rw-r--r-- | plugin_loader/plugin.py | 16 | ||||
| -rw-r--r-- | plugin_loader/static/plugin_page.js | 32 | ||||
| -rw-r--r-- | plugin_loader/templates/plugin_view.html | 96 | ||||
| -rw-r--r-- | requirements.txt | 3 |
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 |
