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:
tza
2022-04-07 22:38:26 +03:00
parent 0f14f2707b
commit c65427e693
6 changed files with 149 additions and 27 deletions
+76
View File
@@ -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)
+10 -6
View File
@@ -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"]
+2 -2
View File
@@ -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
View File
@@ -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()
+30
View File
@@ -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
View File
@@ -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"