Uninstall functionality (#97)

* feat: POC uninstallation feature

* Fixes, placeholder

* bugfix: wrong function call

* add oncancel and change function called

* clean up plugin uninstall code

* bugfix, uninstall in store

* Limit scope of feature branch

* feat: PluginLoader.unloadPlugin

* problematic logs
This commit is contained in:
botato
2022-07-01 23:43:17 +00:00
committed by GitHub
parent 934a50f683
commit 4daf028e7a
4 changed files with 102 additions and 11 deletions
+29 -3
View File
@@ -1,6 +1,6 @@
from injector import get_tab from injector import get_tab
from logging import getLogger from logging import getLogger
from os import path, rename from os import path, rename, listdir
from shutil import rmtree from shutil import rmtree
from aiohttp import ClientSession, web from aiohttp import ClientSession, web
from io import BytesIO from io import BytesIO
@@ -11,6 +11,8 @@ from time import time
from hashlib import sha256 from hashlib import sha256
from subprocess import Popen from subprocess import Popen
import json
class PluginInstallContext: class PluginInstallContext:
def __init__(self, artifact, name, version, hash) -> None: def __init__(self, artifact, name, version, hash) -> None:
self.artifact = artifact self.artifact = artifact
@@ -25,7 +27,8 @@ class PluginBrowser:
self.install_requests = {} self.install_requests = {}
server_instance.add_routes([ server_instance.add_routes([
web.post("/browser/install_plugin", self.install_plugin) 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):
@@ -39,8 +42,31 @@ class PluginBrowser:
Popen(["chmod", "-R", "555", self.plugin_path]) Popen(["chmod", "-R", "555", self.plugin_path])
return True return True
def find_plugin_folder(self, name):
for folder in listdir(self.plugin_path):
with open(path.join(self.plugin_path, folder, 'plugin.json'), 'r') as f:
plugin = json.load(f)
if plugin['name'] == name:
return path.join(self.plugin_path, folder)
async def uninstall_plugin(self, name):
tab = await get_tab("SP")
await tab.open_websocket()
try:
if type(name) != str:
data = await name.post()
name = data.get("name")
await tab.evaluate_js(f"DeckyPluginLoader.unloadPlugin('{name}')")
rmtree(self.find_plugin_folder(name))
except FileNotFoundError:
self.log.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):
rmtree(path.join(self.plugin_path, name), ignore_errors=True) self.uninstall_plugin(name)
self.log.info(f"Installing {name} (Version: {version})") self.log.info(f"Installing {name} (Version: {version})")
async with ClientSession() as client: async with ClientSession() as client:
self.log.debug(f"Fetching {artifact}") self.log.debug(f"Fetching {artifact}")
@@ -1,6 +1,7 @@
import { SidebarNavigation } from 'decky-frontend-lib'; import { SidebarNavigation } from 'decky-frontend-lib';
import GeneralSettings from './pages/GeneralSettings'; import GeneralSettings from './pages/GeneralSettings';
import PluginList from './pages/PluginList';
export default function SettingsPage() { export default function SettingsPage() {
return ( return (
@@ -13,6 +14,11 @@ export default function SettingsPage() {
content: <GeneralSettings />, content: <GeneralSettings />,
route: '/decky/settings/general', route: '/decky/settings/general',
}, },
{
title: 'Plugins',
content: <PluginList />,
route: '/decky/settings/plugins',
},
]} ]}
/> />
); );
@@ -0,0 +1,32 @@
import { DialogButton, staticClasses } from 'decky-frontend-lib';
import { FaTrash } from 'react-icons/fa';
export default function PluginList() {
const plugins = window.DeckyPluginLoader?.getPlugins();
if (plugins.length === 0) {
return (
<div>
<p>No plugins installed</p>
</div>
);
}
return (
<ul style={{ listStyleType: 'none' }}>
{window.DeckyPluginLoader?.getPlugins().map(({ name }) => (
<li style={{ display: 'flex', flexDirection: 'row', alignItems: 'center' }}>
<span>{name}</span>
<div className={staticClasses.Title} style={{ marginLeft: 'auto', boxShadow: 'none' }}>
<DialogButton
style={{ height: '40px', width: '40px', padding: '10px 12px' }}
onClick={() => window.DeckyPluginLoader.uninstall_plugin(name)}
>
<FaTrash />
</DialogButton>
</div>
</li>
))}
</ul>
);
}
+34 -7
View File
@@ -47,6 +47,10 @@ class PluginLoader extends Logger {
this.routerHook.addRoute('/decky/settings', () => <SettingsPage />); this.routerHook.addRoute('/decky/settings', () => <SettingsPage />);
} }
public getPlugins() {
return this.plugins;
}
public addPluginInstallPrompt(artifact: string, version: string, request_id: string, hash: string) { public addPluginInstallPrompt(artifact: string, version: string, request_id: string, hash: string) {
showModal( showModal(
<ModalRoot <ModalRoot
@@ -66,6 +70,28 @@ class PluginLoader extends Logger {
); );
} }
public uninstall_plugin(name: string) {
showModal(
<ModalRoot
onOK={async () => {
const formData = new FormData();
formData.append('name', name);
await fetch('http://localhost:1337/browser/uninstall_plugin', {
method: 'POST',
body: formData,
});
}}
onCancel={() => {
// do nothing
}}
>
<div className={staticClasses.Title} style={{ flexDirection: 'column' }}>
Uninstall {name}?
</div>
</ModalRoot>,
);
}
public dismountAll() { public dismountAll() {
for (const plugin of this.plugins) { for (const plugin of this.plugins) {
this.log(`Dismounting ${plugin.name}`); this.log(`Dismounting ${plugin.name}`);
@@ -78,6 +104,13 @@ class PluginLoader extends Logger {
this.routerHook.removeRoute('/decky/settings'); this.routerHook.removeRoute('/decky/settings');
} }
public unloadPlugin(name: string) {
const plugin = this.plugins.find((plugin) => plugin.name === name || plugin.name === name.replace('$LEGACY_', ''));
plugin?.onDismount?.();
this.plugins = this.plugins.filter((p) => p !== plugin);
this.deckyState.setPlugins(this.plugins);
}
public async importPlugin(name: string) { public async importPlugin(name: string) {
if (this.reloadLock) { if (this.reloadLock) {
this.log('Reload currently in progress, adding to queue', name); this.log('Reload currently in progress, adding to queue', name);
@@ -89,13 +122,7 @@ class PluginLoader extends Logger {
this.reloadLock = true; this.reloadLock = true;
this.log(`Trying to load ${name}`); this.log(`Trying to load ${name}`);
const oldPlugin = this.plugins.find( this.unloadPlugin(name);
(plugin) => plugin.name === name || plugin.name === name.replace('$LEGACY_', ''),
);
if (oldPlugin) {
oldPlugin.onDismount?.();
this.plugins = this.plugins.filter((plugin) => plugin !== oldPlugin);
}
if (name.startsWith('$LEGACY_')) { if (name.startsWith('$LEGACY_')) {
await this.importLegacyPlugin(name.replace('$LEGACY_', '')); await this.importLegacyPlugin(name.replace('$LEGACY_', ''));