mirror of
https://github.com/SteamDeckHomebrew/decky-loader.git
synced 2026-06-17 00:37: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:
|
||||
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"]
|
||||
|
||||
@@ -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()")
|
||||
+11
-6
@@ -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()
|
||||
@@ -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>`;
|
||||
}
|
||||
|
||||
|
||||
+20
-13
@@ -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
|
||||
}
|
||||
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"
|
||||
Reference in New Issue
Block a user