mirror of
https://github.com/SteamDeckHomebrew/decky-loader.git
synced 2026-07-11 19:02:03 +00:00
fix plugin uninstalls
This commit is contained in:
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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"]}
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
Reference in New Issue
Block a user