fix plugin uninstalls

This commit is contained in:
AAGaming
2024-07-04 01:27:13 -04:00
parent 2b2a1d9e39
commit a45df8566e
7 changed files with 54 additions and 26 deletions
+1
View File
@@ -147,6 +147,7 @@ class PluginBrowser:
except Exception as e: except Exception as e:
logger.error(f"Plugin {name} in {plugin_dir} was not uninstalled") logger.error(f"Plugin {name} in {plugin_dir} was not uninstalled")
logger.error(f"Error at {str(e)}", exc_info=e) logger.error(f"Error at {str(e)}", exc_info=e)
finally:
if self.loader.watcher: if self.loader.watcher:
self.loader.watcher.disabled = False self.loader.watcher.disabled = False
@@ -88,6 +88,8 @@ class UnixSocket:
except asyncio.IncompleteReadError as err: except asyncio.IncompleteReadError as err:
line.extend(err.partial) line.extend(err.partial)
break break
except asyncio.CancelledError:
break
else: else:
break break
+4 -2
View File
@@ -109,7 +109,7 @@ class PluginWrapper:
def start(self): def start(self):
if self.passive: if self.passive:
return self 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.proc.start()
self._listener_task = create_task(self._response_listener()) self._listener_task = create_task(self._response_listener())
return self return self
@@ -121,9 +121,11 @@ class PluginWrapper:
if hasattr(self, "_socket"): if hasattr(self, "_socket"):
await self._socket.write_single_line(dumps({ "stop": True, "uninstall": uninstall }, ensure_ascii=False)) await self._socket.write_single_line(dumps({ "stop": True, "uninstall": uninstall }, ensure_ascii=False))
await self._socket.close_socket_connection() await self._socket.close_socket_connection()
if self.proc:
self.proc.join()
await self.kill_if_still_running()
if hasattr(self, "_listener_task"): if hasattr(self, "_listener_task"):
self._listener_task.cancel() self._listener_task.cancel()
await self.kill_if_still_running()
async def kill_if_still_running(self): async def kill_if_still_running(self):
time = 0 time = 0
@@ -1,12 +1,12 @@
from os import path, environ 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 importlib.util import module_from_spec, spec_from_file_location
from json import dumps, loads from json import dumps, loads
from logging import getLogger from logging import getLogger
from sys import exit, path as syspath, modules as sysmodules from sys import exit, path as syspath, modules as sysmodules
from traceback import format_exc from traceback import format_exc
from asyncio import (get_event_loop, new_event_loop, from asyncio import (get_event_loop, new_event_loop,
set_event_loop, sleep) set_event_loop)
from setproctitle import setproctitle, setthreadtitle from setproctitle import setproctitle, setthreadtitle
from .messages import SocketResponseDict, SocketMessageType from .messages import SocketResponseDict, SocketMessageType
@@ -19,6 +19,8 @@ from typing import List, TypeVar, Any
DataType = TypeVar("DataType") DataType = TypeVar("DataType")
original_term_handler = getsignal(SIGTERM)
class SandboxedPlugin: class SandboxedPlugin:
def __init__(self, def __init__(self,
name: str, name: str,
@@ -46,6 +48,8 @@ class SandboxedPlugin:
self._socket = socket self._socket = socket
try: try:
# Ignore signals meant for parent Process
# TODO SURELY there's a better way to do this.
signal(SIGINT, SIG_IGN) signal(SIGINT, SIG_IGN)
signal(SIGTERM, SIG_IGN) signal(SIGTERM, SIG_IGN)
@@ -120,7 +124,14 @@ class SandboxedPlugin:
except: except:
self.log.error("Failed to start " + self.name + "!\n" + format_exc()) self.log.error("Failed to start " + self.name + "!\n" + format_exc())
exit(0) exit(0)
try:
get_event_loop().run_forever() 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): async def _unload(self):
try: try:
@@ -156,6 +167,8 @@ class SandboxedPlugin:
data = loads(message) data = loads(message)
if "stop" in data: 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}.") self.log.info(f"Calling Loader unload function for {self.name}.")
await self._unload() await self._unload()
@@ -163,10 +176,10 @@ class SandboxedPlugin:
self.log.info("Calling Loader uninstall function.") self.log.info("Calling Loader uninstall function.")
await self._uninstall() await self._uninstall()
get_event_loop().stop() self.log.debug("Stopping event loop")
while get_event_loop().is_running():
await sleep(0.1) loop = get_event_loop()
get_event_loop().close() loop.call_soon_threadsafe(loop.stop)
exit(0) exit(0)
d: SocketResponseDict = {"type": SocketMessageType.RESPONSE, "res": None, "success": True, "id": data["id"]} d: SocketResponseDict = {"type": SocketMessageType.RESPONSE, "res": None, "success": True, "id": data["id"]}
+3 -3
View File
@@ -1,6 +1,6 @@
from logging import getLogger from logging import getLogger
from asyncio import AbstractEventLoop, create_task from asyncio import AbstractEventLoop
from aiohttp import WSCloseCode, WSMsgType, WSMessage from aiohttp import WSCloseCode, WSMsgType, WSMessage
from aiohttp.web import Application, WebSocketResponse, Request, Response, get from aiohttp.web import Application, WebSocketResponse, Request, Response, get
@@ -114,10 +114,10 @@ class WSRouter:
# do stuff with the message # do stuff with the message
if data["route"] in self.routes: if data["route"] in self.routes:
self.logger.debug(f'Started PY call {data["route"]} ID {data["id"]}') 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: else:
error = {"error":f'Route {data["route"]} does not exist.', "name": "RouteNotFoundError", "traceback": None} 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 _: case _:
self.logger.error("Unknown message type", data) self.logger.error("Unknown message type", data)
finally: finally:
@@ -58,13 +58,24 @@ const PluginInstallModal: FC<PluginInstallModalProps> = ({
await onCancel(); await onCancel();
}} }}
strTitle={ strTitle={
<div> <div style={{display: "flex", flexDirection: "row", alignItems: "center", width: "100%"}}>
<TranslationHelper <TranslationHelper
transClass={TranslationClass.PLUGIN_INSTALL_MODAL} transClass={TranslationClass.PLUGIN_INSTALL_MODAL}
transText="title" transText="title"
i18nArgs={{ artifact: artifact }} i18nArgs={{ artifact: artifact }}
installType={installType} installType={installType}
/> />
{loading && (
<div style={{marginLeft: "auto"}}>
<ProgressBarWithInfo
layout="inline"
bottomSeparator="none"
nProgress={percentage}
sOperationText={downloadInfo}
/>
</div>
)}
</div> </div>
} }
strOKButtonText={ strOKButtonText={
@@ -98,14 +109,6 @@ const PluginInstallModal: FC<PluginInstallModalProps> = ({
installType={installType} installType={installType}
/> />
</div> </div>
{loading && (
<ProgressBarWithInfo
layout="inline"
bottomSeparator="none"
nProgress={percentage}
sOperationText={downloadInfo}
/>
)}
{hash == 'False' && <span style={{ color: 'red' }}>{t('PluginInstallModal.no_hash')}</span>} {hash == 'False' && <span style={{ color: 'red' }}>{t('PluginInstallModal.no_hash')}</span>}
</ConfirmModal> </ConfirmModal>
); );
@@ -1,5 +1,5 @@
import { ConfirmModal } from '@decky/ui'; import { ConfirmModal, Spinner } from '@decky/ui';
import { FC } from 'react'; import { FC, useState } from 'react';
import { uninstallPlugin } from '../../plugin'; import { uninstallPlugin } from '../../plugin';
@@ -12,10 +12,12 @@ interface PluginUninstallModalProps {
} }
const PluginUninstallModal: FC<PluginUninstallModalProps> = ({ name, title, buttonText, description, closeModal }) => { const PluginUninstallModal: FC<PluginUninstallModalProps> = ({ name, title, buttonText, description, closeModal }) => {
const [uninstalling, setUninstalling] = useState<boolean>(false);
return ( return (
<ConfirmModal <ConfirmModal
closeModal={closeModal} closeModal={closeModal}
onOK={async () => { onOK={async () => {
setUninstalling(true);
await uninstallPlugin(name); await uninstallPlugin(name);
// uninstalling a plugin resets the hidden setting for it server-side // 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 // 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(); await DeckyPluginLoader.hiddenPluginsService.invalidate();
closeModal?.(); 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} strOKButtonText={buttonText}
> >
{description} {description}