Refractor plugin backend (#111)

* refractor uninstall plugin backend

* refractor plugin installation method

* Change formatting in browser.py

* Manually format main.py

* Manually format utilities.py

* remove inconsistency

* remove unnecessary linebreaks

* lol what

* last minute pythoning

* Fix async missing

* lint

* more refractor

* await forgotten

* fix: menu not disappearing after first click

* lint

* bug: fix double click on uninstall

* depricate request installs

* basic patch notes viewer, lazy-load settings and store, build frontend as esmodule, add lazy-loaded react-markdown, backend changes to accomodate ESModule frontend

* refractor uninstall plugin backend

* Change formatting in browser.py

* Manually format main.py

* Manually format utilities.py

* remove unnecessary linebreaks

* lol what

* last minute pythoning

* Fix async missing

* rebase onto main

* fix error, fix React crash if patch notes are opened before remote version info is loaded

Co-authored-by: TrainDoctor <traindoctor@protonmail.com>
Co-authored-by: AAGaming <aa@mail.catvibers.me>
This commit is contained in:
botato
2022-08-27 00:01:23 -04:00
committed by GitHub
parent d4d1c2bbab
commit b7d7ca04e1
6 changed files with 86 additions and 119 deletions
+9 -26
View File
@@ -28,16 +28,11 @@ class PluginInstallContext:
self.hash = hash self.hash = hash
class PluginBrowser: class PluginBrowser:
def __init__(self, plugin_path, server_instance, plugins) -> None: def __init__(self, plugin_path, plugins) -> None:
self.plugin_path = plugin_path self.plugin_path = plugin_path
self.plugins = plugins self.plugins = plugins
self.install_requests = {} self.install_requests = {}
server_instance.add_routes([
web.post("/browser/install_plugin", self.install_plugin),
web.post("/browser/uninstall_plugin", self.uninstall_plugin)
])
def _unzip_to_plugin_dir(self, zip, name, hash): def _unzip_to_plugin_dir(self, zip, name, hash):
zip_hash = sha256(zip.getbuffer()).hexdigest() zip_hash = sha256(zip.getbuffer()).hexdigest()
if hash and (zip_hash != hash): if hash and (zip_hash != hash):
@@ -64,23 +59,17 @@ class PluginBrowser:
async def uninstall_plugin(self, name): async def uninstall_plugin(self, name):
tab = await get_tab("SP") tab = await get_tab("SP")
try: try:
if type(name) != str:
data = await name.post()
name = data.get("name", "undefined")
logger.info("uninstalling " + name) logger.info("uninstalling " + name)
logger.info(" at dir " + self.find_plugin_folder(name)) logger.info(" at dir " + self.find_plugin_folder(name))
await tab.evaluate_js(f"DeckyPluginLoader.unloadPlugin('{name}')") await tab.evaluate_js(f"DeckyPluginLoader.unloadPlugin('{name}')")
if self.plugins[name]: if self.plugins[name]:
self.plugins[name].stop() self.plugins[name].stop()
self.plugins.pop(name, None) self.plugins.remove(name)
rmtree(self.find_plugin_folder(name)) rmtree(self.find_plugin_folder(name))
except FileNotFoundError: except FileNotFoundError:
logger.warning(f"Plugin {name} not installed, skipping uninstallation") logger.warning(f"Plugin {name} not installed, skipping uninstallation")
return web.Response(text="Requested plugin uninstall")
async def _install(self, artifact, name, version, hash): async def _install(self, artifact, name, version, hash):
try: try:
await self.uninstall_plugin(name) await self.uninstall_plugin(name)
@@ -95,22 +84,16 @@ class PluginBrowser:
data = await res.read() data = await res.read()
logger.debug(f"Read {len(data)} bytes") logger.debug(f"Read {len(data)} bytes")
res_zip = BytesIO(data) res_zip = BytesIO(data)
with ProcessPoolExecutor() as executor: logger.debug("Unzipping...")
logger.debug("Unzipping...") ret = self._unzip_to_plugin_dir(res_zip, name, hash)
ret = self._unzip_to_plugin_dir(res_zip, name, hash) if ret:
if ret: logger.info(f"Installed {name} (Version: {version})")
logger.info(f"Installed {name} (Version: {version})") await inject_to_tab("SP", "window.syncDeckyPlugins()")
await inject_to_tab("SP", "window.syncDeckyPlugins()") else:
else: self.log.fatal(f"SHA-256 Mismatch!!!! {name} (Version: {version})")
logger.fatal(f"SHA-256 Mismatch!!!! {name} (Version: {version})")
else: else:
logger.fatal(f"Could not fetch from URL. {await res.text()}") logger.fatal(f"Could not fetch from URL. {await res.text()}")
async def install_plugin(self, request):
data = await request.post()
get_event_loop().create_task(self.request_plugin_install(data.get("artifact", ""), data.get("name", "No name"), data.get("version", "dev"), data.get("hash", False)))
return web.Response(text="Requested plugin install")
async def request_plugin_install(self, artifact, name, version, hash): async def request_plugin_install(self, artifact, name, version, hash):
request_id = str(time()) request_id = str(time())
self.install_requests[request_id] = PluginInstallContext(artifact, name, version, hash) self.install_requests[request_id] = PluginInstallContext(artifact, name, version, hash)
+15 -7
View File
@@ -36,10 +36,15 @@ 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")
],
} }
basicConfig(level=CONFIG["log_level"], format="[%(module)s][%(levelname)s]: %(message)s") basicConfig(
level=CONFIG["log_level"],
format="[%(module)s][%(levelname)s]: %(message)s"
)
logger = getLogger("Main") logger = getLogger("Main")
@@ -55,11 +60,14 @@ class PluginManager:
self.web_app = Application() self.web_app = Application()
self.web_app.middlewares.append(csrf_middleware) self.web_app.middlewares.append(csrf_middleware)
self.cors = aiohttp_cors.setup(self.web_app, defaults={ self.cors = aiohttp_cors.setup(self.web_app, defaults={
"https://steamloopback.host": aiohttp_cors.ResourceOptions(expose_headers="*", "https://steamloopback.host": aiohttp_cors.ResourceOptions(
allow_headers="*", allow_credentials=True) expose_headers="*",
allow_headers="*",
allow_credentials=True
)
}) })
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, self.plugin_loader.plugins) self.plugin_browser = PluginBrowser(CONFIG["plugin_path"], self.plugin_loader.plugins)
self.settings = SettingsManager("loader", path.join(HOMEBREW_PATH, "settings")) self.settings = SettingsManager("loader", path.join(HOMEBREW_PATH, "settings"))
self.utilities = Utilities(self) self.utilities = Utilities(self)
self.updater = Updater(self) self.updater = Updater(self)
@@ -75,7 +83,7 @@ class PluginManager:
self.web_app.add_routes([get("/auth/token", self.get_auth_token)]) self.web_app.add_routes([get("/auth/token", self.get_auth_token)])
for route in list(self.web_app.router.routes()): for route in list(self.web_app.router.routes()):
self.cors.add(route) self.cors.add(route)
self.web_app.add_routes([static("/static", path.join(path.dirname(__file__), 'static'))]) self.web_app.add_routes([static("/static", path.join(path.dirname(__file__), 'static'))])
self.web_app.add_routes([static("/legacy", path.join(path.dirname(__file__), 'legacy'))]) self.web_app.add_routes([static("/legacy", path.join(path.dirname(__file__), 'legacy'))])
@@ -99,7 +107,7 @@ class PluginManager:
async def load_plugins(self): async def load_plugins(self):
await self.wait_for_server() await self.wait_for_server()
self.plugin_loader.import_plugins() self.plugin_loader.import_plugins()
#await inject_to_tab("SP", "window.syncDeckyPlugins();") # await inject_to_tab("SP", "window.syncDeckyPlugins();")
async def loader_reinjector(self): async def loader_reinjector(self):
await sleep(2) await sleep(2)
+18 -4
View File
@@ -7,14 +7,17 @@ from injector import inject_to_tab
import helpers import helpers
import subprocess import subprocess
class Utilities: class Utilities:
def __init__(self, context) -> None: def __init__(self, context) -> None:
self.context = context self.context = context
self.util_methods = { self.util_methods = {
"ping": self.ping, "ping": self.ping,
"http_request": self.http_request, "http_request": self.http_request,
"install_plugin": self.install_plugin,
"cancel_plugin_install": self.cancel_plugin_install, "cancel_plugin_install": self.cancel_plugin_install,
"confirm_plugin_install": self.confirm_plugin_install, "confirm_plugin_install": self.confirm_plugin_install,
"uninstall_plugin": self.uninstall_plugin,
"execute_in_tab": self.execute_in_tab, "execute_in_tab": self.execute_in_tab,
"inject_css_into_tab": self.inject_css_into_tab, "inject_css_into_tab": self.inject_css_into_tab,
"remove_css_from_tab": self.remove_css_from_tab, "remove_css_from_tab": self.remove_css_from_tab,
@@ -45,12 +48,23 @@ class Utilities:
res["success"] = False res["success"] = False
return web.json_response(res) return web.json_response(res)
async def install_plugin(self, artifact="", name="No name", version="dev", hash=False):
return await self.context.plugin_browser.request_plugin_install(
artifact=artifact,
name=name,
version=version,
hash=hash
)
async def confirm_plugin_install(self, request_id): async def confirm_plugin_install(self, request_id):
return await self.context.plugin_browser.confirm_plugin_install(request_id) return await self.context.plugin_browser.confirm_plugin_install(request_id)
def cancel_plugin_install(self, request_id): def cancel_plugin_install(self, request_id):
return self.context.plugin_browser.cancel_plugin_install(request_id) return self.context.plugin_browser.cancel_plugin_install(request_id)
async def uninstall_plugin(self, name):
return await self.context.plugin_browser.uninstall_plugin(name)
async def http_request(self, method="", url="", **kwargs): async def http_request(self, method="", url="", **kwargs):
async with ClientSession() as web: async with ClientSession() as web:
async with web.request(method, url, ssl=helpers.get_ssl_context(), **kwargs) as res: async with web.request(method, url, ssl=helpers.get_ssl_context(), **kwargs) as res:
@@ -74,12 +88,12 @@ class Utilities:
return { return {
"success": True, "success": True,
"result" : result["result"]["result"].get("value") "result": result["result"]["result"].get("value")
} }
except Exception as e: except Exception as e:
return { return {
"success": False, "success": False,
"result": e "result": e
} }
async def inject_css_into_tab(self, tab, style): async def inject_css_into_tab(self, tab, style):
@@ -104,7 +118,7 @@ class Utilities:
return { return {
"success": True, "success": True,
"result" : css_id "result": css_id
} }
except Exception as e: except Exception as e:
return { return {
@@ -17,46 +17,38 @@ function PatchNotesModal({ versionInfo, closeModal }: { versionInfo: VerInfo | n
return ( return (
<Focusable onCancelButton={closeModal}> <Focusable onCancelButton={closeModal}>
<Carousel <Carousel
fnItemRenderer={(id: number, ...args: any[]) => { fnItemRenderer={(id: number) => (
console.log(args, versionInfo); <Focusable
return ( onActivate={() => {}}
<Focusable style={{
onActivate={() => {}} marginTop: '40px',
style={{ height: 'calc( 100% - 40px )',
marginTop: '40px', overflowY: 'scroll',
height: 'calc( 100% - 40px )', display: 'flex',
overflowY: 'scroll', justifyContent: 'center',
display: 'flex', margin: '40px',
justifyContent: 'center', }}
margin: '40px', >
}} <div>
> <h1>{versionInfo?.all?.[id]?.name}</h1>
<div> {versionInfo?.all?.[id]?.body ? (
<h1>{versionInfo?.all?.[id]?.name}</h1> <Suspense fallback={<Spinner style={{ width: '24', height: '24' }} />}>
{versionInfo?.all?.[id]?.body ? ( <MarkdownRenderer>{versionInfo.all[id].body}</MarkdownRenderer>
<Suspense fallback={<Spinner style={{ width: '24', height: '24' }} />}> </Suspense>
<MarkdownRenderer>{versionInfo.all[id].body}</MarkdownRenderer> ) : (
</Suspense> 'no patch notes for this version'
) : ( )}
'no patch notes for this version' </div>
)} </Focusable>
</div> )}
</Focusable> fnGetId={(id) => id}
);
}}
fnGetId={(id) => {
return id;
}}
nNumItems={versionInfo?.all?.length} nNumItems={versionInfo?.all?.length}
nHeight={window.innerHeight - 150} nHeight={window.innerHeight - 150}
nItemHeight={window.innerHeight - 200} nItemHeight={window.innerHeight - 200}
nItemMarginX={0} nItemMarginX={0}
initialColumn={0} initialColumn={0}
autoFocus={true} autoFocus={true}
fnGetColumnWidth={(...args: any[]) => { fnGetColumnWidth={() => window.innerWidth}
console.log('cw', args);
return window.innerWidth;
}}
/> />
</Focusable> </Focusable>
); );
@@ -98,8 +90,8 @@ export default function UpdaterSettings() {
return ( return (
<> <>
<Field <Field
onOptionsActionDescription="Patch Notes" onOptionsActionDescription={versionInfo?.all ? 'Patch Notes' : undefined}
onOptionsButton={showPatchNotes} onOptionsButton={versionInfo?.all ? showPatchNotes : undefined}
label="Updates" label="Updates"
description={ description={
versionInfo && ( versionInfo && (
+1 -10
View File
@@ -150,16 +150,7 @@ class PluginLoader extends Logger {
showModal( showModal(
<ModalRoot <ModalRoot
onOK={async () => { onOK={async () => {
const formData = new FormData(); await this.callServerMethod('uninstall_plugin', { name });
formData.append('name', name);
await fetch('http://localhost:1337/browser/uninstall_plugin', {
method: 'POST',
body: formData,
credentials: 'include',
headers: {
Authentication: window.deckyAuthToken,
},
});
}} }}
onCancel={() => { onCancel={() => {
// do nothing // do nothing
+13 -34
View File
@@ -42,17 +42,10 @@ export function getLegacyPluginList(): Promise<LegacyStorePlugin[]> {
} }
export async function installFromURL(url: string) { export async function installFromURL(url: string) {
const formData = new FormData();
const splitURL = url.split('/'); const splitURL = url.split('/');
formData.append('name', splitURL[splitURL.length - 1].replace('.zip', '')); await window.DeckyPluginLoader.callServerMethod('install_plugin', {
formData.append('artifact', url); name: splitURL[splitURL.length - 1].replace('.zip', ''),
await fetch('http://localhost:1337/browser/install_plugin', { artifact: url,
method: 'POST',
body: formData,
credentials: 'include',
headers: {
Authentication: window.deckyAuthToken,
},
}); });
} }
@@ -60,18 +53,11 @@ export function requestLegacyPluginInstall(plugin: LegacyStorePlugin, selectedVe
showModal( showModal(
<ModalRoot <ModalRoot
onOK={() => { onOK={() => {
const formData = new FormData(); window.DeckyPluginLoader.callServerMethod('install_plugin', {
formData.append('name', plugin.artifact); name: plugin.artifact,
formData.append('artifact', `https://github.com/${plugin.artifact}/archive/refs/tags/${selectedVer}.zip`); artifact: `https://github.com/${plugin.artifact}/archive/refs/tags/${selectedVer}.zip`,
formData.append('version', selectedVer); version: selectedVer,
formData.append('hash', plugin.versions[selectedVer]); hash: plugin.versions[selectedVer],
fetch('http://localhost:1337/browser/install_plugin', {
method: 'POST',
body: formData,
credentials: 'include',
headers: {
Authentication: window.deckyAuthToken,
},
}); });
}} }}
onCancel={() => { onCancel={() => {
@@ -89,18 +75,11 @@ export function requestLegacyPluginInstall(plugin: LegacyStorePlugin, selectedVe
} }
export async function requestPluginInstall(plugin: string, selectedVer: StorePluginVersion) { export async function requestPluginInstall(plugin: string, selectedVer: StorePluginVersion) {
const formData = new FormData(); await window.DeckyPluginLoader.callServerMethod('install_plugin', {
formData.append('name', plugin); name: plugin,
formData.append('artifact', `https://cdn.tzatzikiweeb.moe/file/steam-deck-homebrew/versions/${selectedVer.hash}.zip`); artifact: `https://cdn.tzatzikiweeb.moe/file/steam-deck-homebrew/versions/${selectedVer.hash}.zip`,
formData.append('version', selectedVer.name); version: selectedVer.name,
formData.append('hash', selectedVer.hash); hash: selectedVer.hash,
await fetch('http://localhost:1337/browser/install_plugin', {
method: 'POST',
body: formData,
credentials: 'include',
headers: {
Authentication: window.deckyAuthToken,
},
}); });
} }