mirror of
https://github.com/SteamDeckHomebrew/decky-loader.git
synced 2026-06-17 08:47:49 +00:00
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
This commit is contained in:
@@ -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)
|
||||||
@@ -79,21 +79,25 @@ async def get_tabs():
|
|||||||
else:
|
else:
|
||||||
raise Exception("/json did not return 200. {}".format(await res.text()))
|
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()
|
tabs = await get_tabs()
|
||||||
tab = next((i for i in tabs if i.title == tab_name), None)
|
tab = next((i for i in tabs if i.title == tab_name), None)
|
||||||
if not tab:
|
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)}")
|
logger.debug(f"Injected JavaScript Result: {await tab.evaluate_js(js)}")
|
||||||
|
|
||||||
async def tab_has_element(tab_name, element_name):
|
async def tab_has_element(tab_name, element_name):
|
||||||
tabs = await get_tabs()
|
try:
|
||||||
tab = next((i for i in tabs if i.title == tab_name), None)
|
tab = await get_tab(tab_name)
|
||||||
if not tab:
|
except ValueError:
|
||||||
return False
|
return False
|
||||||
res = await tab.evaluate_js(f"document.getElementById('{element_name}') != null")
|
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"]:
|
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"]
|
return res["result"]["result"]["value"]
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ from os import path, listdir
|
|||||||
from importlib.util import spec_from_file_location, module_from_spec
|
from importlib.util import spec_from_file_location, module_from_spec
|
||||||
from logging import getLogger
|
from logging import getLogger
|
||||||
|
|
||||||
from injector import get_tabs
|
from injector import get_tabs, get_tab
|
||||||
|
|
||||||
class FileChangeHandler(FileSystemEventHandler):
|
class FileChangeHandler(FileSystemEventHandler):
|
||||||
def __init__(self, loader, plugin_path) -> None:
|
def __init__(self, loader, plugin_path) -> None:
|
||||||
@@ -190,6 +190,6 @@ class Loader:
|
|||||||
return {"plugins": self.plugins.values()}
|
return {"plugins": self.plugins.values()}
|
||||||
|
|
||||||
async def refresh_iframe(self):
|
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()
|
await tab.open_websocket()
|
||||||
return await tab.evaluate_js("reloadIframe()")
|
return await tab.evaluate_js("reloadIframe()")
|
||||||
+11
-6
@@ -6,8 +6,10 @@ CONFIG = {
|
|||||||
"server_host": getenv("SERVER_HOST", "127.0.0.1"),
|
"server_host": getenv("SERVER_HOST", "127.0.0.1"),
|
||||||
"server_port": int(getenv("SERVER_PORT", "1337")),
|
"server_port": int(getenv("SERVER_PORT", "1337")),
|
||||||
"live_reload": getenv("LIVE_RELOAD", "1") == "1",
|
"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")
|
basicConfig(level=CONFIG["log_level"], format="[%(module)s][%(levelname)s]: %(message)s")
|
||||||
|
|
||||||
from aiohttp.web import Application, run_app, static
|
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 json import loads, dumps
|
||||||
|
|
||||||
from loader import Loader
|
from loader import Loader
|
||||||
from injector import inject_to_tab, get_tabs, tab_has_element
|
from injector import inject_to_tab, get_tab, tab_has_element
|
||||||
from utilities import util_methods
|
from utilities import Utilities
|
||||||
|
from browser import PluginBrowser
|
||||||
|
|
||||||
|
|
||||||
logger = getLogger("Main")
|
logger = getLogger("Main")
|
||||||
@@ -29,6 +32,8 @@ class PluginManager:
|
|||||||
self.loop = get_event_loop()
|
self.loop = get_event_loop()
|
||||||
self.web_app = Application()
|
self.web_app = Application()
|
||||||
self.plugin_loader = Loader(self.web_app, CONFIG["plugin_path"], self.loop, CONFIG["live_reload"])
|
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')))
|
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(self.inject_javascript)
|
||||||
@@ -64,7 +69,7 @@ class PluginManager:
|
|||||||
)
|
)
|
||||||
res["success"] = True
|
res["success"] = True
|
||||||
else:
|
else:
|
||||||
r = await util_methods[method["method"]](**method["args"])
|
r = await self.utilities.util_methods[method["method"]](**method["args"])
|
||||||
res["result"] = r
|
res["result"] = r
|
||||||
res["success"] = True
|
res["success"] = True
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@@ -74,7 +79,7 @@ class PluginManager:
|
|||||||
await self.resolve_method_call(tab, method["id"], res)
|
await self.resolve_method_call(tab, method["id"], res)
|
||||||
|
|
||||||
async def method_call_listener(self):
|
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.open_websocket()
|
||||||
await tab._send_devtools_cmd({"id": 1, "method": "Runtime.discardConsoleEntries"})
|
await tab._send_devtools_cmd({"id": 1, "method": "Runtime.discardConsoleEntries"})
|
||||||
await tab._send_devtools_cmd({"id": 1, "method": "Runtime.enable"})
|
await tab._send_devtools_cmd({"id": 1, "method": "Runtime.enable"})
|
||||||
@@ -99,7 +104,7 @@ class PluginManager:
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
def run(self):
|
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__":
|
if __name__ == "__main__":
|
||||||
PluginManager().run()
|
PluginManager().run()
|
||||||
@@ -16,6 +16,34 @@ function resolveMethodCall(call_id, result) {
|
|||||||
iframe.postMessage({'call_id': call_id, 'result': result}, "http://127.0.0.1:1337");
|
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 () {
|
(function () {
|
||||||
const PLUGIN_ICON = `
|
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">
|
<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];
|
let pluginPage = pages.children[pages.children.length - 1];
|
||||||
pluginPage.innerHTML = createTitle("Plugins");
|
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>`;
|
pluginPage.innerHTML += `<iframe id="plugin_iframe" style="border: none; width: 100%; height: 100%;" src="http://127.0.0.1:1337/plugins/iframe"></iframe>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+20
-13
@@ -1,18 +1,25 @@
|
|||||||
from aiohttp import ClientSession
|
from aiohttp import ClientSession
|
||||||
|
|
||||||
async def http_request(method="", url="", **kwargs):
|
class Utilities:
|
||||||
async with ClientSession() as web:
|
def __init__(self, context) -> None:
|
||||||
res = await web.request(method, url, **kwargs)
|
self.context = context
|
||||||
return {
|
self.util_methods = {
|
||||||
"status": res.status,
|
"ping": self.ping,
|
||||||
"headers": dict(res.headers),
|
"http_request": self.http_request,
|
||||||
"body": await res.text()
|
"confirm_plugin_install": self.confirm_plugin_install
|
||||||
}
|
}
|
||||||
|
|
||||||
async def ping(**kwargs):
|
async def confirm_plugin_install(self, request_id):
|
||||||
return "pong"
|
return await self.context.plugin_browser.confirm_plugin_install(request_id)
|
||||||
|
|
||||||
util_methods = {
|
async def http_request(self, method="", url="", **kwargs):
|
||||||
"ping": ping,
|
async with ClientSession() as web:
|
||||||
"http_request": http_request
|
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"
|
||||||
Reference in New Issue
Block a user