summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorAAGaming <aagaming@riseup.net>2024-07-04 01:27:13 -0400
committerAAGaming <aagaming@riseup.net>2024-07-04 01:27:13 -0400
commita45df8566ed207d126e5992bb7c870acd09a1864 (patch)
tree52f63323244cd72ec9cd774144e131e4ed05fb51
parent2b2a1d9e39addd03c357a1ba08f6241b980a2759 (diff)
downloaddecky-loader-a45df8566ed207d126e5992bb7c870acd09a1864.tar.gz
decky-loader-a45df8566ed207d126e5992bb7c870acd09a1864.zip
fix plugin uninstalls
-rw-r--r--backend/decky_loader/browser.py5
-rw-r--r--backend/decky_loader/localplatform/localsocket.py2
-rw-r--r--backend/decky_loader/plugin/plugin.py6
-rw-r--r--backend/decky_loader/plugin/sandboxed_plugin.py27
-rw-r--r--backend/decky_loader/wsrouter.py6
-rw-r--r--frontend/src/components/modals/PluginInstallModal.tsx21
-rw-r--r--frontend/src/components/modals/PluginUninstallModal.tsx13
7 files changed, 54 insertions, 26 deletions
diff --git a/backend/decky_loader/browser.py b/backend/decky_loader/browser.py
index 28a591a0..af23ed0c 100644
--- a/backend/decky_loader/browser.py
+++ b/backend/decky_loader/browser.py
@@ -147,8 +147,9 @@ class PluginBrowser:
except Exception as e:
logger.error(f"Plugin {name} in {plugin_dir} was not uninstalled")
logger.error(f"Error at {str(e)}", exc_info=e)
- if self.loader.watcher:
- self.loader.watcher.disabled = False
+ finally:
+ if self.loader.watcher:
+ self.loader.watcher.disabled = False
async def _install(self, artifact: str, name: str, version: str, hash: str):
await self.loader.ws.emit("loader/plugin_download_start", name)
diff --git a/backend/decky_loader/localplatform/localsocket.py b/backend/decky_loader/localplatform/localsocket.py
index 96dbbab6..c0b4faa1 100644
--- a/backend/decky_loader/localplatform/localsocket.py
+++ b/backend/decky_loader/localplatform/localsocket.py
@@ -88,6 +88,8 @@ class UnixSocket:
except asyncio.IncompleteReadError as err:
line.extend(err.partial)
break
+ except asyncio.CancelledError:
+ break
else:
break
diff --git a/backend/decky_loader/plugin/plugin.py b/backend/decky_loader/plugin/plugin.py
index f4b01ad3..a9a9ce29 100644
--- a/backend/decky_loader/plugin/plugin.py
+++ b/backend/decky_loader/plugin/plugin.py
@@ -109,7 +109,7 @@ class PluginWrapper:
def start(self):
if self.passive:
return self
- self.proc = Process(target=self.sandboxed_plugin.initialize, args=[self._socket], daemon=True)
+ self.proc = Process(target=self.sandboxed_plugin.initialize, args=[self._socket])
self.proc.start()
self._listener_task = create_task(self._response_listener())
return self
@@ -121,9 +121,11 @@ class PluginWrapper:
if hasattr(self, "_socket"):
await self._socket.write_single_line(dumps({ "stop": True, "uninstall": uninstall }, ensure_ascii=False))
await self._socket.close_socket_connection()
+ if self.proc:
+ self.proc.join()
+ await self.kill_if_still_running()
if hasattr(self, "_listener_task"):
self._listener_task.cancel()
- await self.kill_if_still_running()
async def kill_if_still_running(self):
time = 0
diff --git a/backend/decky_loader/plugin/sandboxed_plugin.py b/backend/decky_loader/plugin/sandboxed_plugin.py
index 07b04e7d..32330a9a 100644
--- a/backend/decky_loader/plugin/sandboxed_plugin.py
+++ b/backend/decky_loader/plugin/sandboxed_plugin.py
@@ -1,12 +1,12 @@
from os import path, environ
-from signal import SIG_IGN, SIGINT, SIGTERM, signal
+from signal import SIG_IGN, SIGINT, SIGTERM, getsignal, signal
from importlib.util import module_from_spec, spec_from_file_location
from json import dumps, loads
from logging import getLogger
from sys import exit, path as syspath, modules as sysmodules
from traceback import format_exc
from asyncio import (get_event_loop, new_event_loop,
- set_event_loop, sleep)
+ set_event_loop)
from setproctitle import setproctitle, setthreadtitle
from .messages import SocketResponseDict, SocketMessageType
@@ -19,6 +19,8 @@ from typing import List, TypeVar, Any
DataType = TypeVar("DataType")
+original_term_handler = getsignal(SIGTERM)
+
class SandboxedPlugin:
def __init__(self,
name: str,
@@ -46,6 +48,8 @@ class SandboxedPlugin:
self._socket = socket
try:
+ # Ignore signals meant for parent Process
+ # TODO SURELY there's a better way to do this.
signal(SIGINT, SIG_IGN)
signal(SIGTERM, SIG_IGN)
@@ -120,7 +124,14 @@ class SandboxedPlugin:
except:
self.log.error("Failed to start " + self.name + "!\n" + format_exc())
exit(0)
- get_event_loop().run_forever()
+ try:
+ get_event_loop().run_forever()
+ except SystemExit:
+ pass
+ except:
+ self.log.error("Loop exited for " + self.name + "!\n" + format_exc())
+ finally:
+ get_event_loop().close()
async def _unload(self):
try:
@@ -156,6 +167,8 @@ class SandboxedPlugin:
data = loads(message)
if "stop" in data:
+ # Incase the loader needs to terminate our process soon
+ signal(SIGTERM, original_term_handler)
self.log.info(f"Calling Loader unload function for {self.name}.")
await self._unload()
@@ -163,10 +176,10 @@ class SandboxedPlugin:
self.log.info("Calling Loader uninstall function.")
await self._uninstall()
- get_event_loop().stop()
- while get_event_loop().is_running():
- await sleep(0.1)
- get_event_loop().close()
+ self.log.debug("Stopping event loop")
+
+ loop = get_event_loop()
+ loop.call_soon_threadsafe(loop.stop)
exit(0)
d: SocketResponseDict = {"type": SocketMessageType.RESPONSE, "res": None, "success": True, "id": data["id"]}
diff --git a/backend/decky_loader/wsrouter.py b/backend/decky_loader/wsrouter.py
index b513723f..8d20a24d 100644
--- a/backend/decky_loader/wsrouter.py
+++ b/backend/decky_loader/wsrouter.py
@@ -1,6 +1,6 @@
from logging import getLogger
-from asyncio import AbstractEventLoop, create_task
+from asyncio import AbstractEventLoop
from aiohttp import WSCloseCode, WSMsgType, WSMessage
from aiohttp.web import Application, WebSocketResponse, Request, Response, get
@@ -114,10 +114,10 @@ class WSRouter:
# do stuff with the message
if data["route"] in self.routes:
self.logger.debug(f'Started PY call {data["route"]} ID {data["id"]}')
- create_task(self._call_route(data["route"], data["args"], data["id"]))
+ self.loop.create_task(self._call_route(data["route"], data["args"], data["id"]))
else:
error = {"error":f'Route {data["route"]} does not exist.', "name": "RouteNotFoundError", "traceback": None}
- create_task(self.write({"type": MessageType.ERROR.value, "id": data["id"], "error": error}))
+ self.loop.create_task(self.write({"type": MessageType.ERROR.value, "id": data["id"], "error": error}))
case _:
self.logger.error("Unknown message type", data)
finally:
diff --git a/frontend/src/components/modals/PluginInstallModal.tsx b/frontend/src/components/modals/PluginInstallModal.tsx
index c6c90264..d9a3b433 100644
--- a/frontend/src/components/modals/PluginInstallModal.tsx
+++ b/frontend/src/components/modals/PluginInstallModal.tsx
@@ -58,13 +58,24 @@ const PluginInstallModal: FC<PluginInstallModalProps> = ({
await onCancel();
}}
strTitle={
- <div>
+ <div style={{display: "flex", flexDirection: "row", alignItems: "center", width: "100%"}}>
<TranslationHelper
transClass={TranslationClass.PLUGIN_INSTALL_MODAL}
transText="title"
i18nArgs={{ artifact: artifact }}
installType={installType}
/>
+ {loading && (
+ <div style={{marginLeft: "auto"}}>
+ <ProgressBarWithInfo
+ layout="inline"
+
+ bottomSeparator="none"
+ nProgress={percentage}
+ sOperationText={downloadInfo}
+ />
+ </div>
+ )}
</div>
}
strOKButtonText={
@@ -98,14 +109,6 @@ const PluginInstallModal: FC<PluginInstallModalProps> = ({
installType={installType}
/>
</div>
- {loading && (
- <ProgressBarWithInfo
- layout="inline"
- bottomSeparator="none"
- nProgress={percentage}
- sOperationText={downloadInfo}
- />
- )}
{hash == 'False' && <span style={{ color: 'red' }}>{t('PluginInstallModal.no_hash')}</span>}
</ConfirmModal>
);
diff --git a/frontend/src/components/modals/PluginUninstallModal.tsx b/frontend/src/components/modals/PluginUninstallModal.tsx
index f943ad9c..a0d04102 100644
--- a/frontend/src/components/modals/PluginUninstallModal.tsx
+++ b/frontend/src/components/modals/PluginUninstallModal.tsx
@@ -1,5 +1,5 @@
-import { ConfirmModal } from '@decky/ui';
-import { FC } from 'react';
+import { ConfirmModal, Spinner } from '@decky/ui';
+import { FC, useState } from 'react';
import { uninstallPlugin } from '../../plugin';
@@ -12,10 +12,12 @@ interface PluginUninstallModalProps {
}
const PluginUninstallModal: FC<PluginUninstallModalProps> = ({ name, title, buttonText, description, closeModal }) => {
+ const [uninstalling, setUninstalling] = useState<boolean>(false);
return (
<ConfirmModal
closeModal={closeModal}
onOK={async () => {
+ setUninstalling(true);
await uninstallPlugin(name);
// uninstalling a plugin resets the hidden setting for it server-side
// we invalidate here so if you re-install it, you won't have an out-of-date hidden filter
@@ -23,7 +25,12 @@ const PluginUninstallModal: FC<PluginUninstallModalProps> = ({ name, title, butt
await DeckyPluginLoader.hiddenPluginsService.invalidate();
closeModal?.();
}}
- strTitle={title}
+ bOKDisabled={uninstalling}
+ bCancelDisabled={uninstalling}
+ strTitle={<div style={{display: "flex", flexDirection: "row", alignItems: "center", width: "100%"}}>
+ {title}
+ {uninstalling && <Spinner width="24px" height="24px" style={{marginLeft: "auto"}}/>}
+ </div>}
strOKButtonText={buttonText}
>
{description}