Skip to content

Commit

Permalink
Uninstall functionality (#97)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
adntaha authored Jul 1, 2022
1 parent 934a50f commit 4daf028
Show file tree
Hide file tree
Showing 4 changed files with 102 additions and 11 deletions.
34 changes: 30 additions & 4 deletions backend/browser.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
from injector import get_tab
from logging import getLogger
from os import path, rename
from os import path, rename, listdir
from shutil import rmtree
from aiohttp import ClientSession, web
from io import BytesIO
Expand All @@ -11,6 +11,8 @@
from hashlib import sha256
from subprocess import Popen

import json

class PluginInstallContext:
def __init__(self, artifact, name, version, hash) -> None:
self.artifact = artifact
Expand All @@ -25,7 +27,8 @@ def __init__(self, plugin_path, server_instance) -> None:
self.install_requests = {}

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):
Expand All @@ -39,8 +42,31 @@ def _unzip_to_plugin_dir(self, zip, name, hash):
Popen(["chmod", "-R", "555", self.plugin_path])
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):
rmtree(path.join(self.plugin_path, name), ignore_errors=True)
self.uninstall_plugin(name)
self.log.info(f"Installing {name} (Version: {version})")
async with ClientSession() as client:
self.log.debug(f"Fetching {artifact}")
Expand Down Expand Up @@ -83,4 +109,4 @@ async def confirm_plugin_install(self, request_id):
await self._install(request.artifact, request.name, request.version, request.hash)

def cancel_plugin_install(self, request_id):
self.install_requests.pop(request_id)
self.install_requests.pop(request_id)
6 changes: 6 additions & 0 deletions frontend/src/components/settings/index.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { SidebarNavigation } from 'decky-frontend-lib';

import GeneralSettings from './pages/GeneralSettings';
import PluginList from './pages/PluginList';

export default function SettingsPage() {
return (
Expand All @@ -13,6 +14,11 @@ export default function SettingsPage() {
content: <GeneralSettings />,
route: '/decky/settings/general',
},
{
title: 'Plugins',
content: <PluginList />,
route: '/decky/settings/plugins',
},
]}
/>
);
Expand Down
32 changes: 32 additions & 0 deletions frontend/src/components/settings/pages/PluginList.tsx
Original file line number Diff line number Diff line change
@@ -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>
);
}
41 changes: 34 additions & 7 deletions frontend/src/plugin-loader.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,10 @@ class PluginLoader extends Logger {
this.routerHook.addRoute('/decky/settings', () => <SettingsPage />);
}

public getPlugins() {
return this.plugins;
}

public addPluginInstallPrompt(artifact: string, version: string, request_id: string, hash: string) {
showModal(
<ModalRoot
Expand All @@ -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() {
for (const plugin of this.plugins) {
this.log(`Dismounting ${plugin.name}`);
Expand All @@ -78,6 +104,13 @@ class PluginLoader extends Logger {
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) {
if (this.reloadLock) {
this.log('Reload currently in progress, adding to queue', name);
Expand All @@ -89,13 +122,7 @@ class PluginLoader extends Logger {
this.reloadLock = true;
this.log(`Trying to load ${name}`);

const oldPlugin = this.plugins.find(
(plugin) => plugin.name === name || plugin.name === name.replace('$LEGACY_', ''),
);
if (oldPlugin) {
oldPlugin.onDismount?.();
this.plugins = this.plugins.filter((plugin) => plugin !== oldPlugin);
}
this.unloadPlugin(name);

if (name.startsWith('$LEGACY_')) {
await this.importLegacyPlugin(name.replace('$LEGACY_', ''));
Expand Down

0 comments on commit 4daf028

Please sign in to comment.