diff options
| author | tza <marios8543@gmail.com> | 2022-04-07 22:38:26 +0300 |
|---|---|---|
| committer | tza <marios8543@gmail.com> | 2022-04-07 22:38:26 +0300 |
| commit | c65427e693daf3ee96cfd707e74efcb8d9d985f2 (patch) | |
| tree | a61d643ea63517dbac2c2035c88802e631843b85 | |
| parent | 0f14f2707bf0c50d6fbbfda3da66394953ae4128 (diff) | |
| download | decky-loader-c65427e693daf3ee96cfd707e74efcb8d9d985f2.tar.gz decky-loader-c65427e693daf3ee96cfd707e74efcb8d9d985f2.zip | |
initial browser/installer commit, injector get_tab and stateful utils
- Integrated plugin downloader/installer. It accepts POST requests at /browser/install_plugin, containing an artifact (basically an author/repo string like you'd find on github), and a release version, then fetches the zip file from the repo releases and unzips it inside the plugin dir, after asking for user confirmation (pop-up message in the plugin menu).
- Injector get_tab method. Basically get_tabs with the usual search for a specific tab. Decided to implement this because it was needed again and again, and we kept pasting the same list search one-liner.
- Utilities now have access to the main PluginManager class
| -rw-r--r-- | plugin_loader/browser.py | 76 | ||||
| -rw-r--r-- | plugin_loader/injector.py | 16 | ||||
| -rw-r--r-- | plugin_loader/loader.py | 4 | ||||
| -rw-r--r-- | plugin_loader/main.py | 17 | ||||
| -rw-r--r-- | plugin_loader/static/plugin_page.js | 30 | ||||
| -rw-r--r-- | plugin_loader/utilities.py | 33 |
6 files changed, 149 insertions, 27 deletions
diff --git a/plugin_loader/browser.py b/plugin_loader/browser.py new file mode 100644 index 00000000..238bdb67 --- /dev/null +++ b/plugin_loader/browser.py @@ -0,0 +1,76 @@ +from injector import get_tab +from logging import getLogger +from os import path, rename +from shutil import rmtree +from aiohttp import ClientSession, web +from io import BytesIO +from zipfile import ZipFile +from concurrent.futures import ProcessPoolExecutor +from asyncio import get_event_loop +from time import time + +class PluginInstallContext: + def __init__(self, gh_url, version) -> None: + self.gh_url = gh_url + self.version = version + +class PluginBrowser: + def __init__(self, plugin_path, server_instance, store_url) -> None: + self.log = getLogger("browser") + self.plugin_path = plugin_path + self.store_url = store_url + self.install_requests = {} + + server_instance.add_routes([ + web.post("/browser/install_plugin", self.install_plugin), + web.get("/browser/iframe", self.redirect_to_store) + ]) + + def _unzip_to_plugin_dir(self, zip, name): + 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))) + + async def _install(self, artifact, version): + name = artifact.split("/")[-1] + rmtree(path.join(self.plugin_path, name), ignore_errors=True) + self.log.info("Installing {} (Version: {})".format(artifact, version)) + async with ClientSession() as client: + url = "https://github.com/{}/archive/refs/tags/{}.zip".format(artifact, version) + self.log.debug("Fetching {}".format(url)) + res = await client.get(url) + if res.status == 200: + self.log.debug("Got 200. Reading...") + data = await res.read() + self.log.debug("Read {} bytes".format(len(data))) + res_zip = BytesIO(data) + with ProcessPoolExecutor() as executor: + self.log.debug("Unzipping...") + await get_event_loop().run_in_executor( + executor, + self._unzip_to_plugin_dir, + res_zip, + name + ) + self.log.info("Installed {} (Version: {})".format(artifact, version)) + else: + self.log.fatal("Could not fetch from github. {}".format(await res.text())) + + async def redirect_to_store(self, request): + return web.Response(status=302, headers={"Location": self.store_url}) + + async def install_plugin(self, request): + data = await request.post() + get_event_loop().create_task(self.request_plugin_install(data["artifact"], data["version"])) + return web.Response(text="Requested plugin install") + + async def request_plugin_install(self, artifact, version): + request_id = str(time()) + self.install_requests[request_id] = PluginInstallContext(artifact, version) + tab = await get_tab("QuickAccess") + await tab.open_websocket() + await tab.evaluate_js("addPluginInstallPrompt('{}', '{}', '{}')".format(artifact, version, request_id)) + + async def confirm_plugin_install(self, request_id): + request = self.install_requests.pop(request_id) + await self._install(request.gh_url, request.version)
\ No newline at end of file diff --git a/plugin_loader/injector.py b/plugin_loader/injector.py index 6ca9e8e9..85da1e6a 100644 --- a/plugin_loader/injector.py +++ b/plugin_loader/injector.py @@ -79,21 +79,25 @@ async def get_tabs(): else: raise Exception("/json did not return 200. {}".format(await res.text())) -async def inject_to_tab(tab_name, js): +async def get_tab(tab_name): tabs = await get_tabs() tab = next((i for i in tabs if i.title == tab_name), None) if not tab: - raise ValueError("Tab {} not found in running tabs".format(tab_name)) + raise ValueError("Tab {} not found".format(tab_name)) + return tab + +async def inject_to_tab(tab_name, js): + tab = await get_tab(tab_name) logger.debug(f"Injected JavaScript Result: {await tab.evaluate_js(js)}") async def tab_has_element(tab_name, element_name): - tabs = await get_tabs() - tab = next((i for i in tabs if i.title == tab_name), None) - if not tab: + try: + tab = await get_tab(tab_name) + except ValueError: return False res = await tab.evaluate_js(f"document.getElementById('{element_name}') != null") if not "result" in res or not "result" in res["result"] or not "value" in res["result"]["result"]: - return False; + return False return res["result"]["result"]["value"] diff --git a/plugin_loader/loader.py b/plugin_loader/loader.py index fb4e3fa8..948e1e0f 100644 --- a/plugin_loader/loader.py +++ b/plugin_loader/loader.py @@ -7,7 +7,7 @@ from os import path, listdir from importlib.util import spec_from_file_location, module_from_spec from logging import getLogger -from injector import get_tabs +from injector import get_tabs, get_tab class FileChangeHandler(FileSystemEventHandler): def __init__(self, loader, plugin_path) -> None: @@ -190,6 +190,6 @@ class Loader: return {"plugins": self.plugins.values()} async def refresh_iframe(self): - tab = next((i for i in await get_tabs() if i.title == "QuickAccess"), None) + tab = await get_tab("QuickAccess") await tab.open_websocket() return await tab.evaluate_js("reloadIframe()")
\ No newline at end of file diff --git a/plugin_loader/main.py b/plugin_loader/main.py index a5d044b1..1d622098 100644 --- a/plugin_loader/main.py +++ b/plugin_loader/main.py @@ -6,8 +6,10 @@ CONFIG = { "server_host": getenv("SERVER_HOST", "127.0.0.1"), "server_port": int(getenv("SERVER_PORT", "1337")), "live_reload": getenv("LIVE_RELOAD", "1") == "1", - "log_level": {"CRITICAL": 50, "ERROR": 40, "WARNING":30, "INFO": 20, "DEBUG": 10}[getenv("LOG_LEVEL", "INFO")] + "log_level": {"CRITICAL": 50, "ERROR": 40, "WARNING":30, "INFO": 20, "DEBUG": 10}[getenv("LOG_LEVEL", "INFO")], + "store_url": getenv("STORE_URL", "https://sdh.tzatzi.me/browse") } + basicConfig(level=CONFIG["log_level"], format="[%(module)s][%(levelname)s]: %(message)s") from aiohttp.web import Application, run_app, static @@ -18,8 +20,9 @@ from asyncio import get_event_loop, sleep from json import loads, dumps from loader import Loader -from injector import inject_to_tab, get_tabs, tab_has_element -from utilities import util_methods +from injector import inject_to_tab, get_tab, tab_has_element +from utilities import Utilities +from browser import PluginBrowser logger = getLogger("Main") @@ -29,6 +32,8 @@ class PluginManager: self.loop = get_event_loop() self.web_app = Application() self.plugin_loader = Loader(self.web_app, CONFIG["plugin_path"], self.loop, CONFIG["live_reload"]) + self.plugin_browser = PluginBrowser(CONFIG["plugin_path"], self.web_app, CONFIG["store_url"]) + self.utilities = Utilities(self) jinja_setup(self.web_app, loader=FileSystemLoader(path.join(path.dirname(__file__), 'templates'))) self.web_app.on_startup.append(self.inject_javascript) @@ -64,7 +69,7 @@ class PluginManager: ) res["success"] = True else: - r = await util_methods[method["method"]](**method["args"]) + r = await self.utilities.util_methods[method["method"]](**method["args"]) res["result"] = r res["success"] = True except Exception as e: @@ -74,7 +79,7 @@ class PluginManager: await self.resolve_method_call(tab, method["id"], res) async def method_call_listener(self): - tab = next((i for i in await get_tabs() if i.title == "QuickAccess"), None) + tab = await get_tab("QuickAccess") await tab.open_websocket() await tab._send_devtools_cmd({"id": 1, "method": "Runtime.discardConsoleEntries"}) await tab._send_devtools_cmd({"id": 1, "method": "Runtime.enable"}) @@ -99,7 +104,7 @@ class PluginManager: pass def run(self): - return run_app(self.web_app, host=CONFIG["server_host"], port=CONFIG["server_port"], loop=self.loop) + return run_app(self.web_app, host=CONFIG["server_host"], port=CONFIG["server_port"], loop=self.loop, access_log=None) if __name__ == "__main__": PluginManager().run()
\ No newline at end of file diff --git a/plugin_loader/static/plugin_page.js b/plugin_loader/static/plugin_page.js index 0d595ac9..49d1aa89 100644 --- a/plugin_loader/static/plugin_page.js +++ b/plugin_loader/static/plugin_page.js @@ -16,6 +16,34 @@ function resolveMethodCall(call_id, result) { iframe.postMessage({'call_id': call_id, 'result': result}, "http://127.0.0.1:1337"); } +function installPlugin(request_id) { + let id = `${new Date().getTime()}`; + console.debug(JSON.stringify({ + "id": id, + "method": "confirm_plugin_install", + "args": {"request_id": request_id} + })); + document.getElementById('plugin_install_list').removeChild(document.getElementById(`plugin_install_prompt_${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> + </div> + `; + document.getElementById('plugin_install_list').innerHTML += text; +} + (function () { const PLUGIN_ICON = ` <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-plugin" viewBox="0 0 16 16"> @@ -35,6 +63,8 @@ function resolveMethodCall(call_id, result) { let pluginPage = pages.children[pages.children.length - 1]; pluginPage.innerHTML = createTitle("Plugins"); + pluginPage.innerHTML += `<div id="plugin_install_list" style="position: fixed; height: 100%; z-index: 99; transform: translate(5%, 0);"></div>` + pluginPage.innerHTML += `<iframe id="plugin_iframe" style="border: none; width: 100%; height: 100%;" src="http://127.0.0.1:1337/plugins/iframe"></iframe>`; } diff --git a/plugin_loader/utilities.py b/plugin_loader/utilities.py index 4e9a1ac0..a4abf5bc 100644 --- a/plugin_loader/utilities.py +++ b/plugin_loader/utilities.py @@ -1,18 +1,25 @@ from aiohttp import ClientSession -async def http_request(method="", url="", **kwargs): - async with ClientSession() as web: - res = await web.request(method, url, **kwargs) - return { - "status": res.status, - "headers": dict(res.headers), - "body": await res.text() +class Utilities: + def __init__(self, context) -> None: + self.context = context + self.util_methods = { + "ping": self.ping, + "http_request": self.http_request, + "confirm_plugin_install": self.confirm_plugin_install } -async def ping(**kwargs): - return "pong" + async def confirm_plugin_install(self, request_id): + return await self.context.plugin_browser.confirm_plugin_install(request_id) -util_methods = { - "ping": ping, - "http_request": http_request -}
\ No newline at end of file + async def http_request(self, method="", url="", **kwargs): + async with ClientSession() as web: + res = await web.request(method, url, **kwargs) + return { + "status": res.status, + "headers": dict(res.headers), + "body": await res.text() + } + + async def ping(self, **kwargs): + return "pong"
\ No newline at end of file |
