diff --git a/plugin_loader/browser.py b/plugin_loader/browser.py index 7fc8773ba..ffec26b30 100644 --- a/plugin_loader/browser.py +++ b/plugin_loader/browser.py @@ -9,6 +9,7 @@ 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 @@ def _unzip_to_plugin_dir(self, zip, name, hash): 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 ae6093017..f060b5b65 100644 --- a/plugin_loader/loader.py +++ b/plugin_loader/loader.py @@ -2,19 +2,21 @@ 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 @@ def on_created(self, event): 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 @@ def on_modified(self, event): # 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 @@ def __init__(self, server_instance, plugin_path, loop, live_reload=False) -> Non 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 @@ def import_plugin(self, file, plugin_directory, refresh=False): 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 @@ def import_plugins(self): 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 @@ async def get_steam_resource(self, request): 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 @@ async def load_plugin_main_view(self, request): # setup the main script, plugin, and pull in the template ret = f""" - - + + {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 @@ async def handle_sub_route(self, request): 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 @@ async def load_plugin_tile_view(self, request): - + {inner_content} diff --git a/plugin_loader/main.py b/plugin_loader/main.py index 37a4ad1d6..638457a4f 100644 --- a/plugin_loader/main.py +++ b/plugin_loader/main.py @@ -18,15 +18,19 @@ 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 @@ def __init__(self) -> None: 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 @@ def exception_handler(self, loop, context): 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 @@ async def method_call_listener(self): 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 033d36fce..0f8880f39 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 @@ def __init__(self, file, plugin_directory, plugin_path) -> None: 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 @@ async def _listen_for_method_call(self, reader, writer): 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 @@ async def _open_socket_if_not_exists(self): 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 @@ async def _(self): 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 62c24bfe9..0531f04e7 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 = ` -
-

Install plugin

- -
- - -
+ + +
+

Install Plugin?

+

+ ${artifact} + Version: ${version} +

+ +

+
`; - 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 6016a7aef..9d7ba1bc4 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}`; } {% if not plugins|length %} -
-
- No plugins installed :( +
+
+ No plugins installed +
-
{% endif %}
{% for plugin in plugins %} - {% if plugin.tile_view_html|length %} -
-
- - + {% if plugin.tile_view_html|length %} +
+
+ + +
-
{% else %} -
-
-
- +
+
+
+ +
-
{% endif %} - {% endfor %} + {% endfor %}
diff --git a/requirements.txt b/requirements.txt index 579ebc0be..c77a53eda 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