summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authortza <marios8543@gmail.com>2022-04-07 22:38:26 +0300
committertza <marios8543@gmail.com>2022-04-07 22:38:26 +0300
commitc65427e693daf3ee96cfd707e74efcb8d9d985f2 (patch)
treea61d643ea63517dbac2c2035c88802e631843b85
parent0f14f2707bf0c50d6fbbfda3da66394953ae4128 (diff)
downloaddecky-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.py76
-rw-r--r--plugin_loader/injector.py16
-rw-r--r--plugin_loader/loader.py4
-rw-r--r--plugin_loader/main.py17
-rw-r--r--plugin_loader/static/plugin_page.js30
-rw-r--r--plugin_loader/utilities.py33
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