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:
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"]
+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 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
View File
@@ -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()
+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");
}
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
View File
@@ -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"