From f2b859b409cbcedfbbb6a612ab6bb759c56592e6 Mon Sep 17 00:00:00 2001 From: WerWolvTranslationBot <124004309+WerWolvTranslationBot@users.noreply.github.com> Date: Sat, 10 Aug 2024 09:37:27 +0200 Subject: [PATCH 01/27] Translated using Weblate (Spanish) (#678) Currently translated at 93.0% (147 of 158 strings) Translation: Decky/Decky Translate-URL: https://weblate.werwolv.net/projects/decky/decky/es/ Co-authored-by: Matsu --- backend/decky_loader/locales/es-ES.json | 67 +++++++++++++++++++++++-- 1 file changed, 64 insertions(+), 3 deletions(-) diff --git a/backend/decky_loader/locales/es-ES.json b/backend/decky_loader/locales/es-ES.json index f565e3a0..ae1fc1cd 100644 --- a/backend/decky_loader/locales/es-ES.json +++ b/backend/decky_loader/locales/es-ES.json @@ -12,9 +12,40 @@ "disabling": "Desactivando DevTools de React", "enabling": "Activando DevTools de React" }, + "DropdownMultiselect": { + "button": { + "back": "Volver" + } + }, + "FilePickerError": { + "errors": { + "file_not_found": "La ruta especificada no es válida. Por favor revísala e introdúcela correctamente.", + "perm_denied": "No tienes acceso a la ruta especificada. Por favor revisa si tu usuario (deck en Steam Deck) tiene el permiso correspondiente para acceder a la ruta/archivo especificado." + } + }, "FilePickerIndex": { + "file": { + "select": "Selecciona este archivo" + }, + "files": { + "all_files": "Todos los archivos", + "file_type": "Tipo de archivo", + "show_hidden": "Mostrar archivos ocultos" + }, + "filter": { + "created_asce": "Creado (Más antiguo)", + "created_desc": "Creado (Más reciente)", + "modified_asce": "Modificado (Más antiguo)", + "modified_desc": "Modificado (Más reciente)", + "name_asce": "Z-A", + "name_desc": "A-Z", + "size_asce": "Tamaño (Más pequeño)", + "size_desc": "Tamaño (Más grande)" + }, "folder": { - "select": "Usar esta carpeta" + "label": "Carpeta", + "select": "Usar esta carpeta", + "show_more": "Mostrar más archivos" } }, "MultiplePluginsInstallModal": { @@ -71,16 +102,23 @@ } }, "PluginListIndex": { + "freeze": "Congelar actualizaciones", + "hide": "Acceso rápido: Esconder", "no_plugin": "¡No hay plugins instalados!", "plugin_actions": "Acciones de plugin", "reinstall": "Reinstalar", "reload": "Recargar", + "show": "Acceso rápido: Mostrar", + "unfreeze": "Permitir actualizaciones", "uninstall": "Desinstalar", "update_all_many": "Actualizar {{count}} plugins", "update_all_one": "Actualizar 1 plugin", "update_all_other": "Actualizar {{count}} plugins", "update_to": "Actualizar a {{name}}" }, + "PluginListLabel": { + "hidden": "Escondido del menú de acceso rápido" + }, "PluginLoader": { "decky_title": "Decky", "decky_update_available": "¡Actualización {{tag_name}} disponible!", @@ -147,6 +185,11 @@ "developer_mode": { "label": "Modo desarrollador" }, + "notifications": { + "decky_updates_label": "Actualización de Decky disponible", + "header": "Notificaciones", + "plugin_updates_label": "Actualizaciones de plugin disponibles" + }, "other": { "header": "Otros" }, @@ -157,9 +200,17 @@ "SettingsIndex": { "developer_title": "Desarrollador", "general_title": "General", - "plugins_title": "Plugins" + "plugins_title": "Plugins", + "testing_title": "En pruebas" }, "Store": { + "download_progress_info": { + "download_zip": "Descargando plugin", + "installing_plugin": "Instalando plugin", + "open_zip": "Abriendo archivo zip", + "start": "Iniciando", + "uninstalling_previous": "Desinstalando copia previa" + }, "store_contrib": { "desc": "Si desea contribuir a la tienda de plugins de Decky, mira el repositorio SteamDeckHomebrew/decky-plugin-template en GitHub. Hay información acerca del desarrollo y distribución en el archivo README.", "label": "Contribuyendo" @@ -183,9 +234,16 @@ "about": "Información", "alph_asce": "Alfabéticamente (Z-A)", "alph_desc": "Alfabéticamente (A-Z)", + "date_asce": "Más antiguo primero", + "date_desc": "Más reciente primero", + "downloads_asce": "Menos descargados primero", + "downloads_desc": "Más descargados primero", "title": "Navegar" }, - "store_testing_cta": "¡Por favor considera probar plugins nuevos para ayudar al equipo de Decky Loader!" + "store_testing_cta": "¡Por favor considera probar plugins nuevos para ayudar al equipo de Decky Loader!", + "store_testing_warning": { + "label": "Bienvenido al canal En Pruebas de la Tienda" + } }, "StoreSelect": { "custom_store": { @@ -199,6 +257,9 @@ "testing": "Pruebas" } }, + "Testing": { + "download": "Descargar" + }, "Updater": { "decky_updates": "Actualizaciones de Decky", "no_patch_notes_desc": "No hay notas de parche para esta versión", From 3656f541e690e31fa7cec6f063905cc9686dceeb Mon Sep 17 00:00:00 2001 From: WerWolvTranslationBot <124004309+WerWolvTranslationBot@users.noreply.github.com> Date: Tue, 13 Aug 2024 00:04:45 +0200 Subject: [PATCH 02/27] Translations update from Weblate (#681) * Translated using Weblate (Spanish) Currently translated at 100.0% (158 of 158 strings) Translation: Decky/Decky Translate-URL: https://weblate.werwolv.net/projects/decky/decky/es/ * Translated using Weblate (Czech) Currently translated at 100.0% (158 of 158 strings) Translation: Decky/Decky Translate-URL: https://weblate.werwolv.net/projects/decky/decky/cs/ --------- Co-authored-by: Matsu Co-authored-by: Meiton --- backend/decky_loader/locales/cs-CZ.json | 15 ++++++++- backend/decky_loader/locales/es-ES.json | 44 ++++++++++++++++--------- 2 files changed, 42 insertions(+), 17 deletions(-) diff --git a/backend/decky_loader/locales/cs-CZ.json b/backend/decky_loader/locales/cs-CZ.json index 6ffd2486..375ec84c 100644 --- a/backend/decky_loader/locales/cs-CZ.json +++ b/backend/decky_loader/locales/cs-CZ.json @@ -205,6 +205,15 @@ "testing_title": "Testování" }, "Store": { + "download_progress_info": { + "download_zip": "Stahování pluginu", + "increment_count": "Zvyšující se počet stahování", + "installing_plugin": "Instalování pluginu", + "open_zip": "Otevírání ZIP souboru", + "parse_zip": "Analýza ZIP souboru", + "start": "Inicializace", + "uninstalling_previous": "Odinstalování předchozí kopie" + }, "store_contrib": { "desc": "Pokud byste chtěli přispět do obchodu Decky Plugin Store, podívejte se na repozitář SteamDeckHomebrew/decky-plugin-template na GitHubu. Informace o vývoji a distribuci jsou k dispozici v README.", "label": "Přispívání" @@ -253,7 +262,11 @@ } }, "Testing": { - "download": "Stáhnout" + "download": "Stáhnout", + "error": "Chyba při instalaci PR", + "header": "Následující verze Decky Loaderu jsou vytvořeny z otevřených Pull Requestů třetích stran. Tým Decky Loaderu neověřil jejich funkčnost ani zabezpečení a mohou být zastaralé.", + "loading": "Načítání Pull Requestů...", + "start_download_toast": "Stahování PR #{{id}}" }, "TitleView": { "decky_store_desc": "Otevřít obchod Decky", diff --git a/backend/decky_loader/locales/es-ES.json b/backend/decky_loader/locales/es-ES.json index ae1fc1cd..288ed740 100644 --- a/backend/decky_loader/locales/es-ES.json +++ b/backend/decky_loader/locales/es-ES.json @@ -4,7 +4,7 @@ "label": "Canal de actualización", "prerelease": "Prelanzamiento", "stable": "Estable", - "testing": "Pruebas" + "testing": "En pruebas" } }, "Developer": { @@ -20,7 +20,8 @@ "FilePickerError": { "errors": { "file_not_found": "La ruta especificada no es válida. Por favor revísala e introdúcela correctamente.", - "perm_denied": "No tienes acceso a la ruta especificada. Por favor revisa si tu usuario (deck en Steam Deck) tiene el permiso correspondiente para acceder a la ruta/archivo especificado." + "perm_denied": "No tienes acceso a la ruta especificada. Por favor revisa si tu usuario (deck en Steam Deck) tiene el permiso correspondiente para acceder a la ruta/archivo especificado.", + "unknown": "Ha ocurrido un error desconocido. El error sin procesar es:{{raw_error}}" } }, "FilePickerIndex": { @@ -75,9 +76,9 @@ } }, "PluginCard": { - "plugin_full_access": "Este plugin tiene acceso completo a su Steam Deck.", + "plugin_full_access": "Este plugin tiene acceso completo a tu Steam Deck.", "plugin_install": "Instalar", - "plugin_no_desc": "No se proporcionó una descripción.", + "plugin_no_desc": "No se ha proporcionado una descripción.", "plugin_version_label": "Versión de Plugin" }, "PluginInstallModal": { @@ -105,7 +106,7 @@ "freeze": "Congelar actualizaciones", "hide": "Acceso rápido: Esconder", "no_plugin": "¡No hay plugins instalados!", - "plugin_actions": "Acciones de plugin", + "plugin_actions": "Acciones de Plugin", "reinstall": "Reinstalar", "reload": "Recargar", "show": "Acceso rápido: Mostrar", @@ -121,7 +122,7 @@ }, "PluginLoader": { "decky_title": "Decky", - "decky_update_available": "¡Actualización {{tag_name}} disponible!", + "decky_update_available": "¡Actualización para {{tag_name}} disponible!", "error": "Error", "plugin_error_uninstall": "Al cargar {{name}} se ha producido una excepción como se muestra arriba. Esto suele significar que el plugin requiere una actualización para la nueva versión de SteamUI. Comprueba si hay una actualización disponible o valora eliminarlo en los ajustes de Decky, en la sección Plugins.", "plugin_load_error": { @@ -138,19 +139,19 @@ "plugin_update_other": "¡Actualizaciones disponibles para {{count}} plugins!" }, "PluginView": { - "hidden_many": "", - "hidden_one": "", - "hidden_other": "" + "hidden_many": "{{count}} plugins están escondidos de esta lista", + "hidden_one": "1 plugin está escondido de esta lista", + "hidden_other": "{{count}} plugins están escondidos de esta lista" }, "RemoteDebugging": { "remote_cef": { - "desc": "Permitir acceso no autenticado al CEF debugger a cualquier persona en su red", + "desc": "Permitir acceso no autenticado al depurador del CEF a cualquier persona en tu red", "label": "Permitir depuración remota del CEF" } }, "SettingsDeveloperIndex": { "cef_console": { - "button": "Abrir consola", + "button": "Abrir Consola", "desc": "Abre la consola del CEF. Solamente es útil para propósitos de depuración. Las cosas que hagas aquí pueden ser potencialmente peligrosas y solo se debería usar si eres un desarrollador de plugins, o uno te ha dirigido aquí.", "label": "Consola CEF" }, @@ -206,13 +207,15 @@ "Store": { "download_progress_info": { "download_zip": "Descargando plugin", + "increment_count": "Incrementando el contador de descargas", "installing_plugin": "Instalando plugin", "open_zip": "Abriendo archivo zip", + "parse_zip": "Analizando archivo zip", "start": "Iniciando", "uninstalling_previous": "Desinstalando copia previa" }, "store_contrib": { - "desc": "Si desea contribuir a la tienda de plugins de Decky, mira el repositorio SteamDeckHomebrew/decky-plugin-template en GitHub. Hay información acerca del desarrollo y distribución en el archivo README.", + "desc": "Si desea contribuir a la tienda de plugins de Decky, consulta el repositorio SteamDeckHomebrew/decky-plugin-template en GitHub. Hay información disponible acerca del desarrollo y distribución en el archivo README.", "label": "Contribuyendo" }, "store_filter": { @@ -224,7 +227,7 @@ }, "store_sort": { "label": "Ordenar", - "label_def": "Actualizado por última vez (Nuevos)" + "label_def": "Actualizado por última vez (Más reciente)" }, "store_source": { "desc": "El código fuente de los plugins está disponible en el repositiorio SteamDeckHomebrew/decky-plugin-database en GitHub.", @@ -242,6 +245,7 @@ }, "store_testing_cta": "¡Por favor considera probar plugins nuevos para ayudar al equipo de Decky Loader!", "store_testing_warning": { + "desc": "Puedes usar este canal de la tienda para probar versiones inestables de plugins. Recuerda compartir tu experiencia en GitHub con el fin de poder actualizar el plugin para todos los usuarios.", "label": "Bienvenido al canal En Pruebas de la Tienda" } }, @@ -253,12 +257,20 @@ "store_channel": { "custom": "Personalizada", "default": "Por defecto", - "label": "Canál de la tienda", - "testing": "Pruebas" + "label": "Canal de la Tienda", + "testing": "En pruebas" } }, "Testing": { - "download": "Descargar" + "download": "Descargar", + "error": "Error instalando PR", + "header": "Las siguientes versiones de Decky Loader han sido compiladas de solicitudes Pull de terceros. El equipo de Decky Loader no ha verificado su funcionalidad o seguridad, y es posible que estén desactulizadas.", + "loading": "Cargando abrir Solicitudes de Pull...", + "start_download_toast": "Descargando PR #{{id}}" + }, + "TitleView": { + "decky_store_desc": "Abrir la tienda de Decky", + "settings_desc": "Abrir los ajustes de Decky" }, "Updater": { "decky_updates": "Actualizaciones de Decky", From e5e75cc16e01b9d23a483b9504e3d44e2e8756bd Mon Sep 17 00:00:00 2001 From: AAGaming Date: Tue, 13 Aug 2024 21:14:17 -0400 Subject: [PATCH 03/27] fix oopsie breaking decky on the latest beta --- frontend/src/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/index.ts b/frontend/src/index.ts index 029a731c..97357b82 100644 --- a/frontend/src/index.ts +++ b/frontend/src/index.ts @@ -7,7 +7,7 @@ interface Window { (async () => { // Wait for main webpack chunks to definitely be loaded console.time('[Decky:Boot] Waiting for main Webpack chunks...'); - while (!window.webpackChunksteamui || window.webpackChunksteamui.length < 8) { + while (!window.webpackChunksteamui || window.webpackChunksteamui.length < 5) { await new Promise((r) => setTimeout(r, 10)); // Can't use DFL sleep here. } console.timeEnd('[Decky:Boot] Waiting for main Webpack chunks...'); From 7c9b68c1ddada6d978ecf8f0d0e274580e72d842 Mon Sep 17 00:00:00 2001 From: AAGaming Date: Tue, 20 Aug 2024 14:55:59 -0400 Subject: [PATCH 04/27] only grab the first 8 lines of the component stack --- frontend/src/utils/errors.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/frontend/src/utils/errors.ts b/frontend/src/utils/errors.ts index a99fd0b1..f77a5e08 100644 --- a/frontend/src/utils/errors.ts +++ b/frontend/src/utils/errors.ts @@ -22,7 +22,8 @@ export function getLikelyErrorSourceFromValveError(error: ValveError): ErrorSour } export function getLikelyErrorSourceFromValveReactError(error: ValveReactErrorInfo): ErrorSource { - return getLikelyErrorSource(error?.error?.stack + '\n' + error.info.componentStack); + // get the first 10 lines of the componentStack to avoid matching against the decky router wrapper for any route errors deeper in the tree + return getLikelyErrorSource(error?.error?.stack + '\n' + error.info.componentStack?.split("\n").slice(0, 8).join("\n")); } export function getLikelyErrorSource(error?: string): ErrorSource { From 927f912eb353ecb347b8cfd6ecad3c49651039c1 Mon Sep 17 00:00:00 2001 From: AAGaming Date: Wed, 21 Aug 2024 14:40:42 -0400 Subject: [PATCH 05/27] lint --- frontend/src/utils/errors.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/frontend/src/utils/errors.ts b/frontend/src/utils/errors.ts index f77a5e08..da19f810 100644 --- a/frontend/src/utils/errors.ts +++ b/frontend/src/utils/errors.ts @@ -23,7 +23,9 @@ export function getLikelyErrorSourceFromValveError(error: ValveError): ErrorSour export function getLikelyErrorSourceFromValveReactError(error: ValveReactErrorInfo): ErrorSource { // get the first 10 lines of the componentStack to avoid matching against the decky router wrapper for any route errors deeper in the tree - return getLikelyErrorSource(error?.error?.stack + '\n' + error.info.componentStack?.split("\n").slice(0, 8).join("\n")); + return getLikelyErrorSource( + error?.error?.stack + '\n' + error.info.componentStack?.split('\n').slice(0, 8).join('\n'), + ); } export function getLikelyErrorSource(error?: string): ErrorSource { From 4842a599e03b5981baea32768f3bfe64612fd932 Mon Sep 17 00:00:00 2001 From: WerWolvTranslationBot <124004309+WerWolvTranslationBot@users.noreply.github.com> Date: Wed, 28 Aug 2024 12:25:51 +0200 Subject: [PATCH 06/27] Translated using Weblate (Russian) (#690) Currently translated at 100.0% (158 of 158 strings) Translation: Decky/Decky Translate-URL: https://weblate.werwolv.net/projects/decky/decky/ru/ Co-authored-by: Andrew --- backend/decky_loader/locales/ru-RU.json | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/backend/decky_loader/locales/ru-RU.json b/backend/decky_loader/locales/ru-RU.json index a1d06aba..798d5fc2 100644 --- a/backend/decky_loader/locales/ru-RU.json +++ b/backend/decky_loader/locales/ru-RU.json @@ -205,6 +205,15 @@ "testing_title": "Тестирование" }, "Store": { + "download_progress_info": { + "download_zip": "Скачивание плагина", + "increment_count": "Увеличение количества загрузок", + "installing_plugin": "Установка плагина", + "open_zip": "Открытие zip файла", + "parse_zip": "Распаковка zip файла", + "start": "Инициализация", + "uninstalling_previous": "Удаление предыдущей копии" + }, "store_contrib": { "desc": "Если вы хотите внести свой вклад в магазин плагинов Decky, проверьте репозиторий SteamDeckHomebrew/decky-plugin-template на GitHub. Информация о разработке и распространении доступна в README.", "label": "Помощь проекту" @@ -253,7 +262,11 @@ } }, "Testing": { - "download": "Загрузить" + "download": "Загрузить", + "error": "Ошибка при установке PR", + "header": "Данные версии Decky Loader созданы на основе сторонних pull requst. Команда Decky Loader не проверяла их функциональность и безопасность, и они могут быть устаревшими.", + "loading": "Загрузка открытых pull requst'ов...", + "start_download_toast": "Загрузка PR#{{id}}" }, "TitleView": { "decky_store_desc": "Открыть магазин Decky", From 016ed6e998de25c3a2d5caf119b4489c281b3ba5 Mon Sep 17 00:00:00 2001 From: Sims <38142618+suchmememanyskill@users.noreply.github.com> Date: Sun, 1 Sep 2024 20:15:49 +0200 Subject: [PATCH 07/27] Fix shutdown timeouts (#695) Co-authored-by: AAGaming --- backend/decky_loader/loader.py | 7 ++- .../decky_loader/localplatform/localsocket.py | 23 +++++--- backend/decky_loader/main.py | 8 ++- backend/decky_loader/plugin/plugin.py | 56 +++++++++++-------- .../decky_loader/plugin/sandboxed_plugin.py | 12 +--- dist/install_prerelease.sh | 3 + dist/install_release.sh | 3 + dist/plugin_loader-prerelease.service | 1 + dist/plugin_loader-release.service | 1 + 9 files changed, 70 insertions(+), 44 deletions(-) diff --git a/backend/decky_loader/loader.py b/backend/decky_loader/loader.py index fcd36346..6a324e23 100644 --- a/backend/decky_loader/loader.py +++ b/backend/decky_loader/loader.py @@ -104,10 +104,15 @@ async def shutdown_plugins(self): async def enable_reload_wait(self): if self.live_reload: await sleep(10) - if self.watcher: + if self.watcher and self.live_reload: self.logger.info("Hot reload enabled") self.watcher.disabled = False + async def disable_reload(self): + if self.watcher: + self.watcher.disabled = True + self.live_reload = False + async def handle_frontend_assets(self, request: web.Request): file = Path(__file__).parent.joinpath("static").joinpath(request.match_info["path"]) return web.FileResponse(file, headers={"Cache-Control": "no-cache"}) diff --git a/backend/decky_loader/localplatform/localsocket.py b/backend/decky_loader/localplatform/localsocket.py index b25b275a..74e65406 100644 --- a/backend/decky_loader/localplatform/localsocket.py +++ b/backend/decky_loader/localplatform/localsocket.py @@ -7,22 +7,24 @@ BUFFER_LIMIT = 2 ** 20 # 1 MiB class UnixSocket: - def __init__(self, on_new_message: Callable[[str], Coroutine[Any, Any, Any]]): + def __init__(self): ''' on_new_message takes 1 string argument. It's return value gets used, if not None, to write data to the socket. Method should be async ''' self.socket_addr = f"/tmp/plugin_socket_{time.time()}" - self.on_new_message = on_new_message + self.on_new_message = None self.socket = None self.reader = None self.writer = None self.server_writer = None self.open_lock = asyncio.Lock() + self.active = True - async def setup_server(self): + async def setup_server(self, on_new_message: Callable[[str], Coroutine[Any, Any, Any]]): try: + self.on_new_message = on_new_message self.socket = await asyncio.start_unix_server(self._listen_for_method_call, path=self.socket_addr, limit=BUFFER_LIMIT) except asyncio.CancelledError: await self.close_socket_connection() @@ -58,6 +60,8 @@ async def close_socket_connection(self): if self.socket: self.socket.close() await self.socket.wait_closed() + + self.active = False async def read_single_line(self) -> str|None: reader, _ = await self.get_socket_connection() @@ -81,7 +85,7 @@ async def write_single_line(self, message : str): async def _read_single_line(self, reader: asyncio.StreamReader) -> str: line = bytearray() - while True: + while self.active: try: line.extend(await reader.readuntil()) except asyncio.LimitOverrunError: @@ -91,7 +95,7 @@ async def _read_single_line(self, reader: asyncio.StreamReader) -> str: line.extend(err.partial) break except asyncio.CancelledError: - break + raise else: break @@ -111,7 +115,7 @@ async def write_single_line_server(self, message: str): async def _listen_for_method_call(self, reader: asyncio.StreamReader, writer: asyncio.StreamWriter): self.server_writer = writer - while True: + while self.active and self.on_new_message: def _(task: asyncio.Task[str|None]): res = task.result() @@ -122,18 +126,19 @@ def _(task: asyncio.Task[str|None]): asyncio.create_task(self.on_new_message(line)).add_done_callback(_) class PortSocket (UnixSocket): - def __init__(self, on_new_message: Callable[[str], Coroutine[Any, Any, Any]]): + def __init__(self): ''' on_new_message takes 1 string argument. It's return value gets used, if not None, to write data to the socket. Method should be async ''' - super().__init__(on_new_message) + super().__init__() self.host = "127.0.0.1" self.port = random.sample(range(40000, 60000), 1)[0] - async def setup_server(self): + async def setup_server(self, on_new_message: Callable[[str], Coroutine[Any, Any, Any]]): try: + self.on_new_message = on_new_message self.socket = await asyncio.start_server(self._listen_for_method_call, host=self.host, port=self.port, limit=BUFFER_LIMIT) except asyncio.CancelledError: await self.close_socket_connection() diff --git a/backend/decky_loader/main.py b/backend/decky_loader/main.py index c268b387..983d3dca 100644 --- a/backend/decky_loader/main.py +++ b/backend/decky_loader/main.py @@ -101,6 +101,8 @@ async def startup(_: Application): self.web_app.add_routes([static("/static", path.join(path.dirname(__file__), 'static'))]) async def handle_crash(self): + if not self.reinject: + return new_time = time() if (new_time - self.last_webhelper_exit < 60): self.webhelper_crash_count += 1 @@ -118,9 +120,13 @@ async def handle_crash(self): async def shutdown(self, _: Application): try: logger.info(f"Shutting down...") + logger.info("Disabling reload...") + await self.plugin_loader.disable_reload() + logger.info("Killing plugins...") await self.plugin_loader.shutdown_plugins() - await self.ws.disconnect() + logger.info("Disconnecting from WS...") self.reinject = False + await self.ws.disconnect() if self.js_ctx_tab: await self.js_ctx_tab.close_websocket() self.js_ctx_tab = None diff --git a/backend/decky_loader/plugin/plugin.py b/backend/decky_loader/plugin/plugin.py index a9a9ce29..e22eca3f 100644 --- a/backend/decky_loader/plugin/plugin.py +++ b/backend/decky_loader/plugin/plugin.py @@ -1,8 +1,10 @@ -from asyncio import CancelledError, Task, create_task, sleep +from asyncio import CancelledError, Task, create_task, sleep, get_event_loop, wait from json import dumps, load, loads from logging import getLogger from os import path from multiprocessing import Process +from time import time +from traceback import format_exc from .sandboxed_plugin import SandboxedPlugin from .messages import MethodCallRequest, SocketMessageType @@ -42,8 +44,7 @@ def __init__(self, file: str, plugin_directory: str, plugin_path: str, emit_call self.sandboxed_plugin = SandboxedPlugin(self.name, self.passive, self.flags, self.file, self.plugin_directory, self.plugin_path, self.version, self.author, self.api_version) self.proc: Process | None = None - # TODO: Maybe make LocalSocket not require on_new_message to make this cleaner - self._socket = LocalSocket(self.sandboxed_plugin.on_new_message) + self._socket = LocalSocket() self._listener_task: Task[Any] self._method_call_requests: Dict[str, MethodCallRequest] = {} @@ -65,7 +66,7 @@ def __str__(self) -> str: return self.name async def _response_listener(self): - while True: + while self._socket.active: try: line = await self._socket.read_single_line() if line != None: @@ -115,29 +116,40 @@ def start(self): return self async def stop(self, uninstall: bool = False): - self.log.info(f"Stopping plugin {self.name}") - if self.passive: - return - 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() + try: + start_time = time() + if self.passive: + return + + _, pending = await wait([ + create_task(self._socket.write_single_line(dumps({ "stop": True, "uninstall": uninstall }, ensure_ascii=False))) + ], timeout=1) + + if hasattr(self, "_listener_task"): + self._listener_task.cancel() + + await self.kill_if_still_running() + + for pending_task in pending: + pending_task.cancel() + + self.log.info(f"Plugin {self.name} has been stopped in {time() - start_time:.1f}s") + except Exception as e: + self.log.error(f"Error during shutdown for plugin {self.name}: {str(e)}\n{format_exc()}") async def kill_if_still_running(self): - time = 0 + start_time = time() + sigtermed = False while self.proc and self.proc.is_alive(): - await sleep(0.1) - time += 1 - if time == 100: - self.log.warn(f"Plugin {self.name} still alive 10 seconds after stop request! Sending SIGTERM!") + elapsed_time = time() - start_time + if elapsed_time >= 5 and not sigtermed: + sigtermed = True + self.log.warn(f"Plugin {self.name} still alive 5 seconds after stop request! Sending SIGTERM!") self.terminate() - elif time == 200: - self.log.warn(f"Plugin {self.name} still alive 20 seconds after stop request! Sending SIGKILL!") + elif elapsed_time >= 10: + self.log.warn(f"Plugin {self.name} still alive 10 seconds after stop request! Sending SIGKILL!") self.terminate(True) + await sleep(0.1) def terminate(self, kill: bool = False): if self.proc and self.proc.is_alive(): diff --git a/backend/decky_loader/plugin/sandboxed_plugin.py b/backend/decky_loader/plugin/sandboxed_plugin.py index 93691a44..23575900 100644 --- a/backend/decky_loader/plugin/sandboxed_plugin.py +++ b/backend/decky_loader/plugin/sandboxed_plugin.py @@ -1,6 +1,5 @@ import sys from os import path, environ -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 @@ -19,8 +18,6 @@ DataType = TypeVar("DataType") -original_term_handler = getsignal(SIGTERM) - class SandboxedPlugin: def __init__(self, name: str, @@ -48,11 +45,6 @@ def initialize(self, socket: LocalSocket): 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) - setproctitle(f"{self.name} ({self.file})") setthreadtitle(self.name) @@ -120,7 +112,7 @@ async def emit(event: str, *args: Any) -> None: get_event_loop().create_task(self.Plugin._main()) else: get_event_loop().create_task(self.Plugin._main(self.Plugin)) - get_event_loop().create_task(socket.setup_server()) + get_event_loop().create_task(socket.setup_server(self.on_new_message)) except: self.log.error("Failed to start " + self.name + "!\n" + format_exc()) sys.exit(0) @@ -167,8 +159,6 @@ async def on_new_message(self, message : str) -> str|None: 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() diff --git a/dist/install_prerelease.sh b/dist/install_prerelease.sh index 950c25aa..9e5ce9cc 100644 --- a/dist/install_prerelease.sh +++ b/dist/install_prerelease.sh @@ -34,10 +34,13 @@ curl -L https://raw.githubusercontent.com/SteamDeckHomebrew/decky-loader/main/di cat > "${HOMEBREW_FOLDER}/services/plugin_loader-backup.service" <<- EOM [Unit] Description=SteamDeck Plugin Loader +After=network.target [Service] Type=simple User=root Restart=always +KillMode=process +TimeoutStopSec=45 ExecStart=${HOMEBREW_FOLDER}/services/PluginLoader WorkingDirectory=${HOMEBREW_FOLDER}/services Environment=UNPRIVILEGED_PATH=${HOMEBREW_FOLDER} diff --git a/dist/install_release.sh b/dist/install_release.sh index 46a47867..61f85488 100644 --- a/dist/install_release.sh +++ b/dist/install_release.sh @@ -34,10 +34,13 @@ curl -L https://raw.githubusercontent.com/SteamDeckHomebrew/decky-loader/main/di cat > "${HOMEBREW_FOLDER}/services/plugin_loader-backup.service" <<- EOM [Unit] Description=SteamDeck Plugin Loader +After=network.target [Service] Type=simple User=root Restart=always +KillMode=process +TimeoutStopSec=45 ExecStart=${HOMEBREW_FOLDER}/services/PluginLoader WorkingDirectory=${HOMEBREW_FOLDER}/services Environment=UNPRIVILEGED_PATH=${HOMEBREW_FOLDER} diff --git a/dist/plugin_loader-prerelease.service b/dist/plugin_loader-prerelease.service index 594925dd..78970909 100644 --- a/dist/plugin_loader-prerelease.service +++ b/dist/plugin_loader-prerelease.service @@ -5,6 +5,7 @@ After=network.target Type=simple User=root Restart=always +KillMode=process TimeoutStopSec=45 ExecStart=${HOMEBREW_FOLDER}/services/PluginLoader WorkingDirectory=${HOMEBREW_FOLDER}/services diff --git a/dist/plugin_loader-release.service b/dist/plugin_loader-release.service index 6f94d4e1..d8f69dca 100644 --- a/dist/plugin_loader-release.service +++ b/dist/plugin_loader-release.service @@ -5,6 +5,7 @@ After=network.target Type=simple User=root Restart=always +KillMode=process TimeoutStopSec=45 ExecStart=${HOMEBREW_FOLDER}/services/PluginLoader WorkingDirectory=${HOMEBREW_FOLDER}/services From 6ae6f5ee67cbc7b0b7dbbebf7939d86ec4aa6722 Mon Sep 17 00:00:00 2001 From: AAGaming Date: Sun, 1 Sep 2024 14:17:11 -0400 Subject: [PATCH 08/27] chore(backend): .warn -> .warning --- backend/decky_loader/helpers.py | 4 ++-- backend/decky_loader/injector.py | 6 +++--- backend/decky_loader/localplatform/localplatformlinux.py | 6 +++--- backend/decky_loader/main.py | 4 ++-- backend/decky_loader/plugin/plugin.py | 6 +++--- backend/decky_loader/wsrouter.py | 6 +++--- 6 files changed, 16 insertions(+), 16 deletions(-) diff --git a/backend/decky_loader/helpers.py b/backend/decky_loader/helpers.py index 8ca77632..14572004 100644 --- a/backend/decky_loader/helpers.py +++ b/backend/decky_loader/helpers.py @@ -84,7 +84,7 @@ def get_loader_version() -> str: return version_str except Exception as e: - logger.warn(f"Failed to execute get_loader_version(): {str(e)}") + logger.warning(f"Failed to execute get_loader_version(): {str(e)}") return "unknown" user_agent = f"Decky/{get_loader_version()} (https://decky.xyz)" @@ -102,7 +102,7 @@ def get_system_pythonpaths() -> list[str]: versions = [x.strip() for x in proc.stdout.decode().strip().split("\n")] return [x for x in versions if x and not x.isspace()] except Exception as e: - logger.warn(f"Failed to execute get_system_pythonpaths(): {str(e)}") + logger.warning(f"Failed to execute get_system_pythonpaths(): {str(e)}") return [] # Download Remote Binaries to local Plugin diff --git a/backend/decky_loader/injector.py b/backend/decky_loader/injector.py index 7d1a40c1..39b5b15d 100644 --- a/backend/decky_loader/injector.py +++ b/backend/decky_loader/injector.py @@ -46,7 +46,7 @@ async def listen_for_message(self): async for message in self.websocket: data = message.json() yield data - logger.warn(f"The Tab {self.title} socket has been disconnected while listening for messages.") + logger.warning(f"The Tab {self.title} socket has been disconnected while listening for messages.") await self.close_websocket() async def _send_devtools_cmd(self, dc: Dict[str, Any], receive: bool = True): @@ -381,10 +381,10 @@ async def get_tabs() -> List[Tab]: na = True await sleep(5) except ClientOSError: - logger.warn(f"The request to {BASE_ADDRESS}/json was reset") + logger.warning(f"The request to {BASE_ADDRESS}/json was reset") await sleep(1) except TimeoutError: - logger.warn(f"The request to {BASE_ADDRESS}/json timed out") + logger.warning(f"The request to {BASE_ADDRESS}/json timed out") await sleep(1) else: break diff --git a/backend/decky_loader/localplatform/localplatformlinux.py b/backend/decky_loader/localplatform/localplatformlinux.py index 2c92124f..1aeb3169 100644 --- a/backend/decky_loader/localplatform/localplatformlinux.py +++ b/backend/decky_loader/localplatform/localplatformlinux.py @@ -203,7 +203,7 @@ def get_unprivileged_path() -> str: path = None if path == None: - logger.warn("Unprivileged path is not properly configured. Defaulting to /home/deck/homebrew") + logger.warning("Unprivileged path is not properly configured. Defaulting to /home/deck/homebrew") path = "/home/deck/homebrew" # We give up os.makedirs(path, exist_ok=True) @@ -225,7 +225,7 @@ def get_unprivileged_user() -> str: break if user == None: - logger.warn("Unprivileged user is not properly configured. Defaulting to 'deck'") + logger.warning("Unprivileged user is not properly configured. Defaulting to 'deck'") user = 'deck' return user @@ -238,7 +238,7 @@ def get_unprivileged_user() -> str: async def close_cef_socket(): async with close_cef_socket_lock: if _get_effective_user_id() != 0: - logger.warn("Can't close CEF socket as Decky isn't running as root.") + logger.warning("Can't close CEF socket as Decky isn't running as root.") return # Look for anything listening TCP on port 8080 lsof = run(["lsof", "-F", "-iTCP:8080", "-sTCP:LISTEN"], capture_output=True, text=True) diff --git a/backend/decky_loader/main.py b/backend/decky_loader/main.py index 983d3dca..b86411e1 100644 --- a/backend/decky_loader/main.py +++ b/backend/decky_loader/main.py @@ -106,7 +106,7 @@ async def handle_crash(self): new_time = time() if (new_time - self.last_webhelper_exit < 60): self.webhelper_crash_count += 1 - logger.warn(f"webhelper crashed within a minute from last crash! crash count: {self.webhelper_crash_count}") + logger.warning(f"webhelper crashed within a minute from last crash! crash count: {self.webhelper_crash_count}") else: self.webhelper_crash_count = 0 self.last_webhelper_exit = new_time @@ -147,7 +147,7 @@ async def cancel_task(task: Task[Any]): pass logger.debug(f"Task {task} finished") except: - logger.warn(f"Failed to cancel task {task}:\n" + format_exc()) + logger.warning(f"Failed to cancel task {task}:\n" + format_exc()) pass if current: tasks.remove(current) diff --git a/backend/decky_loader/plugin/plugin.py b/backend/decky_loader/plugin/plugin.py index e22eca3f..cc950e8a 100644 --- a/backend/decky_loader/plugin/plugin.py +++ b/backend/decky_loader/plugin/plugin.py @@ -85,7 +85,7 @@ async def _response_listener(self): async def execute_legacy_method(self, method_name: str, kwargs: Dict[Any, Any]): if not self.legacy_method_warning: self.legacy_method_warning = True - self.log.warn(f"Plugin {self.name} is using legacy method calls. This will be removed in a future release.") + self.log.warning(f"Plugin {self.name} is using legacy method calls. This will be removed in a future release.") if self.passive: raise RuntimeError("This plugin is passive (aka does not implement main.py)") @@ -144,10 +144,10 @@ async def kill_if_still_running(self): elapsed_time = time() - start_time if elapsed_time >= 5 and not sigtermed: sigtermed = True - self.log.warn(f"Plugin {self.name} still alive 5 seconds after stop request! Sending SIGTERM!") + self.log.warning(f"Plugin {self.name} still alive 5 seconds after stop request! Sending SIGTERM!") self.terminate() elif elapsed_time >= 10: - self.log.warn(f"Plugin {self.name} still alive 10 seconds after stop request! Sending SIGKILL!") + self.log.warning(f"Plugin {self.name} still alive 10 seconds after stop request! Sending SIGKILL!") self.terminate(True) await sleep(0.1) diff --git a/backend/decky_loader/wsrouter.py b/backend/decky_loader/wsrouter.py index 8d20a24d..8a019b44 100644 --- a/backend/decky_loader/wsrouter.py +++ b/backend/decky_loader/wsrouter.py @@ -50,7 +50,7 @@ async def write(self, data: Dict[str, Any]): if self.ws != None: await self.ws.send_json(data) else: - self.logger.warn("Dropping message as there is no connected socket: %s", data) + self.logger.warning("Dropping message as there is no connected socket: %s", data) def add_route(self, name: str, route: Route): self.routes[name] = route @@ -69,9 +69,9 @@ async def _call_route(self, route: str, args: ..., call_id: int): if instance_id != self.instance_id: try: - self.logger.warn("Ignoring %s reply from stale instance %d with args %s and response %s", route, instance_id, args, res) + self.logger.warning("Ignoring %s reply from stale instance %d with args %s and response %s", route, instance_id, args, res) except: - self.logger.warn("Ignoring %s reply from stale instance %d (failed to log event data)", route, instance_id) + self.logger.warning("Ignoring %s reply from stale instance %d (failed to log event data)", route, instance_id) finally: return From c1f7ca7f2079fd5960c67d612f2551e033281051 Mon Sep 17 00:00:00 2001 From: AAGaming Date: Sun, 1 Sep 2024 14:18:33 -0400 Subject: [PATCH 09/27] chore(backend): remove unused import in plugin.py --- backend/decky_loader/plugin/plugin.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/decky_loader/plugin/plugin.py b/backend/decky_loader/plugin/plugin.py index cc950e8a..28be710d 100644 --- a/backend/decky_loader/plugin/plugin.py +++ b/backend/decky_loader/plugin/plugin.py @@ -1,4 +1,4 @@ -from asyncio import CancelledError, Task, create_task, sleep, get_event_loop, wait +from asyncio import CancelledError, Task, create_task, sleep, wait from json import dumps, load, loads from logging import getLogger from os import path From a6e4bcf0527a998261698c555113a3f652d280d9 Mon Sep 17 00:00:00 2001 From: Sims <38142618+suchmememanyskill@users.noreply.github.com> Date: Mon, 2 Sep 2024 01:45:47 +0200 Subject: [PATCH 10/27] Fix updater taking a long time (#696) Replaces subprocess with asyncio.subprocess in some localplatformlinux functions and improves shutdown handling Co-authored-by: AAGaming --- .../localplatform/localplatformlinux.py | 42 ++++++++++++------- .../localplatform/localplatformwin.py | 2 +- backend/decky_loader/main.py | 7 ++-- .../decky_loader/plugin/sandboxed_plugin.py | 33 ++++++++++----- backend/decky_loader/updater.py | 7 +++- 5 files changed, 60 insertions(+), 31 deletions(-) diff --git a/backend/decky_loader/localplatform/localplatformlinux.py b/backend/decky_loader/localplatform/localplatformlinux.py index 1aeb3169..63a07292 100644 --- a/backend/decky_loader/localplatform/localplatformlinux.py +++ b/backend/decky_loader/localplatform/localplatformlinux.py @@ -1,11 +1,21 @@ from re import compile -from asyncio import Lock +from asyncio import Lock, create_subprocess_exec +from asyncio.subprocess import PIPE, DEVNULL, STDOUT, Process +from subprocess import call as call_sync import os, pwd, grp, sys, logging -from subprocess import call, run, DEVNULL, PIPE, STDOUT +from typing import IO, Any, Mapping from ..enums import UserType logger = logging.getLogger("localplatform") +# subprocess._ENV +ENV = Mapping[str, str] +ProcessIO = int | IO[Any] | None +async def run(args: list[str], stdin: ProcessIO = DEVNULL, stdout: ProcessIO = PIPE, stderr: ProcessIO = PIPE, env: ENV | None = None) -> tuple[Process, bytes | None, bytes | None]: + proc = await create_subprocess_exec(args[0], *(args[1:]), stdin=stdin, stdout=stdout, stderr=stderr, env=env) + proc_stdout, proc_stderr = await proc.communicate() + return (proc, proc_stdout, proc_stderr) + # Get the user id hosting the plugin loader def _get_user_id() -> int: return pwd.getpwnam(_get_user()).pw_uid @@ -54,7 +64,7 @@ def chown(path : str, user : UserType = UserType.HOST_USER, recursive : bool = else: raise Exception("Unknown User Type") - result = call(["chown", "-R", user_str, path] if recursive else ["chown", user_str, path]) + result = call_sync(["chown", "-R", user_str, path] if recursive else ["chown", user_str, path]) return result == 0 def chmod(path : str, permissions : int, recursive : bool = True) -> bool: @@ -131,13 +141,17 @@ def setuid(user : UserType = UserType.HOST_USER): os.setuid(user_id) async def service_active(service_name : str) -> bool: - res = run(["systemctl", "is-active", service_name], stdout=DEVNULL, stderr=DEVNULL) + res, _, _ = await run(["systemctl", "is-active", service_name], stdout=DEVNULL, stderr=DEVNULL) return res.returncode == 0 -async def service_restart(service_name : str) -> bool: - call(["systemctl", "daemon-reload"]) +async def service_restart(service_name : str, block : bool = True) -> bool: + await run(["systemctl", "daemon-reload"]) cmd = ["systemctl", "restart", service_name] - res = run(cmd, stdout=PIPE, stderr=STDOUT) + + if not block: + cmd.append("--no-block") + + res, _, _ = await run(cmd, stdout=PIPE, stderr=STDOUT) return res.returncode == 0 async def service_stop(service_name : str) -> bool: @@ -146,7 +160,7 @@ async def service_stop(service_name : str) -> bool: return True cmd = ["systemctl", "stop", service_name] - res = run(cmd, stdout=PIPE, stderr=STDOUT) + res, _, _ = await run(cmd, stdout=PIPE, stderr=STDOUT) return res.returncode == 0 async def service_start(service_name : str) -> bool: @@ -155,13 +169,13 @@ async def service_start(service_name : str) -> bool: return True cmd = ["systemctl", "start", service_name] - res = run(cmd, stdout=PIPE, stderr=STDOUT) + res, _, _ = await run(cmd, stdout=PIPE, stderr=STDOUT) return res.returncode == 0 async def restart_webhelper() -> bool: logger.info("Restarting steamwebhelper") # TODO move to pkill - res = run(["killall", "-s", "SIGTERM", "steamwebhelper"], stdout=DEVNULL, stderr=DEVNULL) + res, _, _ = await run(["killall", "-s", "SIGTERM", "steamwebhelper"], stdout=DEVNULL, stderr=DEVNULL) return res.returncode == 0 def get_privileged_path() -> str: @@ -241,12 +255,12 @@ async def close_cef_socket(): logger.warning("Can't close CEF socket as Decky isn't running as root.") return # Look for anything listening TCP on port 8080 - lsof = run(["lsof", "-F", "-iTCP:8080", "-sTCP:LISTEN"], capture_output=True, text=True) - if lsof.returncode != 0 or len(lsof.stdout) < 1: + lsof, stdout, _ = await run(["lsof", "-F", "-iTCP:8080", "-sTCP:LISTEN"], stdout=PIPE) + if not stdout or lsof.returncode != 0 or len(stdout) < 1: logger.error(f"lsof call failed in close_cef_socket! return code: {str(lsof.returncode)}") return - lsof_data = cef_socket_lsof_regex.match(lsof.stdout) + lsof_data = cef_socket_lsof_regex.match(stdout.decode()) if not lsof_data: logger.error("lsof regex match failed in close_cef_socket!") @@ -258,7 +272,7 @@ async def close_cef_socket(): logger.info(f"Closing CEF socket with PID {pid} and FD {fd}") # Use gdb to inject a close() call for the socket fd into steamwebhelper - gdb_ret = run(["gdb", "--nx", "-p", pid, "--batch", "--eval-command", f"call (int)close({fd})"], env={"LD_LIBRARY_PATH": ""}) + gdb_ret, _, _ = await run(["gdb", "--nx", "-p", pid, "--batch", "--eval-command", f"call (int)close({fd})"], env={"LD_LIBRARY_PATH": ""}) if gdb_ret.returncode != 0: logger.error(f"Failed to close CEF socket with gdb! return code: {str(gdb_ret.returncode)}", exc_info=True) diff --git a/backend/decky_loader/localplatform/localplatformwin.py b/backend/decky_loader/localplatform/localplatformwin.py index 52ade07c..7e3bd31c 100644 --- a/backend/decky_loader/localplatform/localplatformwin.py +++ b/backend/decky_loader/localplatform/localplatformwin.py @@ -28,7 +28,7 @@ async def service_stop(service_name : str) -> bool: async def service_start(service_name : str) -> bool: return True # Stubbed -async def service_restart(service_name : str) -> bool: +async def service_restart(service_name : str, block : bool = True) -> bool: if service_name == "plugin_loader": sys.exit(42) diff --git a/backend/decky_loader/main.py b/backend/decky_loader/main.py index b86411e1..315b7d29 100644 --- a/backend/decky_loader/main.py +++ b/backend/decky_loader/main.py @@ -138,16 +138,17 @@ async def shutdown(self, _: Application): tasks = all_tasks() current = current_task() async def cancel_task(task: Task[Any]): - logger.debug(f"Cancelling task {task}") + name = task.get_coro().__qualname__ + logger.debug(f"Cancelling task {name}") try: task.cancel() try: await task except CancelledError: pass - logger.debug(f"Task {task} finished") + logger.debug(f"Task {name} finished") except: - logger.warning(f"Failed to cancel task {task}:\n" + format_exc()) + logger.warning(f"Failed to cancel task {name}:\n" + format_exc()) pass if current: tasks.remove(current) diff --git a/backend/decky_loader/plugin/sandboxed_plugin.py b/backend/decky_loader/plugin/sandboxed_plugin.py index 23575900..b730fa98 100644 --- a/backend/decky_loader/plugin/sandboxed_plugin.py +++ b/backend/decky_loader/plugin/sandboxed_plugin.py @@ -4,8 +4,9 @@ from json import dumps, loads from logging import getLogger from traceback import format_exc -from asyncio import (get_event_loop, new_event_loop, +from asyncio import (ensure_future, get_event_loop, new_event_loop, set_event_loop) +from signal import SIGINT, SIGTERM from setproctitle import setproctitle, setthreadtitle from .messages import SocketResponseDict, SocketMessageType @@ -38,6 +39,7 @@ def __init__(self, self.version = version self.author = author self.api_version = api_version + self.shutdown_running = False self.log = getLogger("sandboxed_plugin") @@ -48,7 +50,11 @@ def initialize(self, socket: LocalSocket): setproctitle(f"{self.name} ({self.file})") setthreadtitle(self.name) - set_event_loop(new_event_loop()) + loop = new_event_loop() + set_event_loop(loop) + # When running Decky manually in a terminal, ctrl-c will trigger this, so we have to handle it properly + loop.add_signal_handler(SIGINT, lambda: ensure_future(self.shutdown())) + loop.add_signal_handler(SIGTERM, lambda: ensure_future(self.shutdown())) if self.passive: return setgid(UserType.ROOT if "root" in self.flags else UserType.HOST_USER) @@ -155,22 +161,27 @@ async def _uninstall(self): self.log.error("Failed to uninstall " + self.name + "!\n" + format_exc()) pass - async def on_new_message(self, message : str) -> str|None: - data = loads(message) - - if "stop" in data: + async def shutdown(self, uninstall: bool = False): + if not self.shutdown_running: + self.shutdown_running = True self.log.info(f"Calling Loader unload function for {self.name}.") await self._unload() - if data.get('uninstall'): + if uninstall: self.log.info("Calling Loader uninstall function.") await self._uninstall() - self.log.debug("Stopping event loop") + self.log.debug("Stopping event loop") - loop = get_event_loop() - loop.call_soon_threadsafe(loop.stop) - sys.exit(0) + loop = get_event_loop() + loop.call_soon_threadsafe(loop.stop) + sys.exit(0) + + async def on_new_message(self, message : str) -> str|None: + data = loads(message) + + if "stop" in data: + await self.shutdown(data.get('uninstall')) d: SocketResponseDict = {"type": SocketMessageType.RESPONSE, "res": None, "success": True, "id": data["id"]} try: diff --git a/backend/decky_loader/updater.py b/backend/decky_loader/updater.py index 5cd25e72..75f9618b 100644 --- a/backend/decky_loader/updater.py +++ b/backend/decky_loader/updater.py @@ -24,6 +24,7 @@ class RemoteVerAsset(TypedDict): name: str + size: int browser_download_url: str class RemoteVer(TypedDict): tag_name: str @@ -198,11 +199,13 @@ async def do_update(self): version = self.remoteVer["tag_name"] download_url = None + size_in_bytes = None download_filename = "PluginLoader" if ON_LINUX else "PluginLoader.exe" for x in self.remoteVer["assets"]: if x["name"] == download_filename: download_url = x["browser_download_url"] + size_in_bytes = x["size"] break if download_url == None: @@ -238,10 +241,10 @@ async def do_update(self): os.mkdir(path.join(getcwd(), ".systemd")) shutil.move(service_file_path, path.join(getcwd(), ".systemd")+"/plugin_loader.service") - await self.download_decky_binary(download_url, version) + await self.download_decky_binary(download_url, version, size_in_bytes=size_in_bytes) async def do_restart(self): - await service_restart("plugin_loader") + await service_restart("plugin_loader", block=False) async def do_shutdown(self): await service_stop("plugin_loader") From d06494885a8932dd01571b40de88eed3a6cd095b Mon Sep 17 00:00:00 2001 From: AAGaming Date: Sun, 1 Sep 2024 20:40:12 -0400 Subject: [PATCH 11/27] fix external links softlocking the ui in testing store cta --- frontend/src/components/ExternalLink.tsx | 16 ++++++++++++++++ frontend/src/components/store/PluginCard.tsx | 5 +++-- frontend/src/components/store/Store.tsx | 9 +++++---- 3 files changed, 24 insertions(+), 6 deletions(-) create mode 100644 frontend/src/components/ExternalLink.tsx diff --git a/frontend/src/components/ExternalLink.tsx b/frontend/src/components/ExternalLink.tsx new file mode 100644 index 00000000..a223fc77 --- /dev/null +++ b/frontend/src/components/ExternalLink.tsx @@ -0,0 +1,16 @@ +import { Navigation } from '@decky/ui'; +import { AnchorHTMLAttributes, FC } from 'react'; + +const ExternalLink: FC> = (props) => { + return ( + { + e.preventDefault(); + props.onClick ? props.onClick(e) : props.href && Navigation.NavigateToExternalWeb(props.href); + }} + /> + ); +}; + +export default ExternalLink; diff --git a/frontend/src/components/store/PluginCard.tsx b/frontend/src/components/store/PluginCard.tsx index dd54aa65..6e2a3510 100644 --- a/frontend/src/components/store/PluginCard.tsx +++ b/frontend/src/components/store/PluginCard.tsx @@ -4,6 +4,7 @@ import { useTranslation } from 'react-i18next'; import { InstallType } from '../../plugin'; import { StorePlugin, StorePluginVersion, requestPluginInstall } from '../../store'; +import ExternalLink from '../ExternalLink'; interface PluginCardProps { plugin: StorePlugin; @@ -108,7 +109,7 @@ const PluginCard: FC = ({ plugin }) => { }} > {t('PluginCard.plugin_full_access')}{' '} - = ({ plugin }) => { }} > deckbrew.xyz/root - + )} diff --git a/frontend/src/components/store/Store.tsx b/frontend/src/components/store/Store.tsx index d6342bba..1094b243 100644 --- a/frontend/src/components/store/Store.tsx +++ b/frontend/src/components/store/Store.tsx @@ -14,6 +14,7 @@ import { useTranslation } from 'react-i18next'; import logo from '../../../assets/plugin_store.png'; import Logger from '../../logger'; import { SortDirections, SortOptions, Store, StorePlugin, getPluginList, getStore } from '../../store'; +import ExternalLink from '../ExternalLink'; import PluginCard from './PluginCard'; const logger = new Logger('Store'); @@ -207,7 +208,7 @@ const BrowseTab: FC<{ setPluginCount: Dispatch> }>

{t('Store.store_testing_warning.label')}

{`${t('Store.store_testing_warning.desc')} `} - > }> }} > decky.xyz/testing - + )} @@ -269,7 +270,7 @@ const AboutTab: FC<{}> = () => { Testing {t('Store.store_testing_cta')}{' '} - = () => { }} > decky.xyz/testing - + {t('Store.store_contrib.label')} {t('Store.store_contrib.desc')} From 81ffe11106a118719433c53ade2ad9c2fc8dee81 Mon Sep 17 00:00:00 2001 From: AAGaming Date: Wed, 4 Sep 2024 08:45:26 -0400 Subject: [PATCH 12/27] This shouldn't have applied to stable --- dist/plugin_loader-release.service | 2 -- 1 file changed, 2 deletions(-) diff --git a/dist/plugin_loader-release.service b/dist/plugin_loader-release.service index d8f69dca..f427b27a 100644 --- a/dist/plugin_loader-release.service +++ b/dist/plugin_loader-release.service @@ -5,8 +5,6 @@ After=network.target Type=simple User=root Restart=always -KillMode=process -TimeoutStopSec=45 ExecStart=${HOMEBREW_FOLDER}/services/PluginLoader WorkingDirectory=${HOMEBREW_FOLDER}/services Environment=UNPRIVILEGED_PATH=${HOMEBREW_FOLDER} From 0e40374b106f63ee7485bbc571a9db3c0e55f979 Mon Sep 17 00:00:00 2001 From: AAGaming Date: Wed, 4 Sep 2024 08:45:54 -0400 Subject: [PATCH 13/27] This also shouldn't have applied to stabls --- dist/install_release.sh | 2 -- 1 file changed, 2 deletions(-) diff --git a/dist/install_release.sh b/dist/install_release.sh index 61f85488..6b31a0c4 100644 --- a/dist/install_release.sh +++ b/dist/install_release.sh @@ -39,8 +39,6 @@ After=network.target Type=simple User=root Restart=always -KillMode=process -TimeoutStopSec=45 ExecStart=${HOMEBREW_FOLDER}/services/PluginLoader WorkingDirectory=${HOMEBREW_FOLDER}/services Environment=UNPRIVILEGED_PATH=${HOMEBREW_FOLDER} From bcc14848c58518668f04d67412e16f98cbdd9d34 Mon Sep 17 00:00:00 2001 From: TrainDoctor <11465594+TrainDoctor@users.noreply.github.com> Date: Thu, 5 Sep 2024 12:09:39 -0700 Subject: [PATCH 14/27] Create plugin-info.sh Add plugin-info script for debugging, thanks @Jaynator495! --- scripts/plugin-info.sh | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 scripts/plugin-info.sh diff --git a/scripts/plugin-info.sh b/scripts/plugin-info.sh new file mode 100644 index 00000000..2a2feadb --- /dev/null +++ b/scripts/plugin-info.sh @@ -0,0 +1,21 @@ +#!/bin/bash +# Adapted from a script provided by Jaynator495. +# Make sure to place in home directory, chmod +x plugin-info.sh and then run with ./plugin-info.sh +# Define the directory to scan +directory_to_scan="~/homebrew/plugins" + +# Loop through each subdirectory (one level deep) +for dir in "$directory_to_scan"/*/; do + # Check if package.json exists in the subdirectory + if [ -f "${dir}package.json" ]; then + # Extract name and version from the package.json file using jq + name=$(jq -r '.name' "${dir}package.json") + version=$(jq -r '.version' "${dir}package.json") + + # Output the name and version + echo "Directory: ${dir}" + echo "Package Name: $name" + echo "Version: $version" + echo "-----------------------------" + fi +done From 494f8dac5e373371df7656bdd379696ffd530b3c Mon Sep 17 00:00:00 2001 From: TrainDoctor <11465594+TrainDoctor@users.noreply.github.com> Date: Thu, 5 Sep 2024 12:14:33 -0700 Subject: [PATCH 15/27] Update bug_report.yml Add new field requiring all installed plugins to be listed. --- .github/ISSUE_TEMPLATE/bug_report.yml | 23 ++++++++++++++++++++--- 1 file changed, 20 insertions(+), 3 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index 1b34f7d1..76e7bb4f 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -57,18 +57,35 @@ body: validations: required: true + - type: input + attributes: + label: Decky Loader Version + description: Specify the exact version of Decky. + placeholder: v3.0.0-pre12 + validations: + required: true + + - type: textarea + attributes: + label: Plugin Info + description: Include all plugins installed including their version. Helpful script here: https://raw.githubusercontent.com/SteamDeckHomebrew/decky-loader/main/scripts/plugin-info.sh + placeholder: | + If you don't want to collect this info manually you can download plugin-info.sh from our main repo and place it into your home directory, chmod +x plugin-info.sh and then run it with ./plugin-info.sh + validations: + required: true + - type: input attributes: label: Have you modified the read-only filesystem at any point? - description: Describe how here, if you haven't done anything you can leave this blank - placeholder: Yes, I've installed neofetch via pacman. + description: "Describe how here, if you haven't done anything you can leave this blank" + placeholder: "Yes, I've installed neofetch via pacman." validations: required: false - type: textarea attributes: label: Backend Logs - description: Please reboot your deck (if possible) when attempting to recreate the issue, then run ``cd ~ && journalctl -b0 -u plugin_loader.service > deckylog.txt``. This will save the log file to ``~`` aka ``/home/deck``. Please upload the file here + description: Please reboot your deck (if possible) when attempting to recreate the issue, then run ``cd ~ && journalctl -b0 -u plugin_loader.service > deckylog.txt``. This will save the log file to ``~`` aka ``/home/deck``. Please upload the file here. placeholder: deckylog.txt validations: required: true From f7a47127a7ef2015325b827bbf86ac0b1a627701 Mon Sep 17 00:00:00 2001 From: TrainDoctor <11465594+TrainDoctor@users.noreply.github.com> Date: Thu, 5 Sep 2024 12:19:12 -0700 Subject: [PATCH 16/27] Update bug_report.yml --- .github/ISSUE_TEMPLATE/bug_report.yml | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index 76e7bb4f..9d420568 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -68,9 +68,8 @@ body: - type: textarea attributes: label: Plugin Info - description: Include all plugins installed including their version. Helpful script here: https://raw.githubusercontent.com/SteamDeckHomebrew/decky-loader/main/scripts/plugin-info.sh - placeholder: | - If you don't want to collect this info manually you can download plugin-info.sh from our main repo and place it into your home directory, chmod +x plugin-info.sh and then run it with ./plugin-info.sh + description: "Include all plugins installed including their version. Helpful script here: https://github.com/SteamDeckHomebrew/decky-loader/blob/main/scripts/plugin-info.sh" + placeholder: "If you don't want to collect this info manually you can download plugin-info.sh from our main repo and place it into your home directory, chmod +x plugin-info.sh and then run it with ./plugin-info.sh" validations: required: true From 1f5d5f9f1acf589ba35364e835ece143a5805632 Mon Sep 17 00:00:00 2001 From: TrainDoctor <11465594+TrainDoctor@users.noreply.github.com> Date: Thu, 5 Sep 2024 12:20:32 -0700 Subject: [PATCH 17/27] Update bug_report.yml --- .github/ISSUE_TEMPLATE/bug_report.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index 9d420568..dd0ff925 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -70,8 +70,8 @@ body: label: Plugin Info description: "Include all plugins installed including their version. Helpful script here: https://github.com/SteamDeckHomebrew/decky-loader/blob/main/scripts/plugin-info.sh" placeholder: "If you don't want to collect this info manually you can download plugin-info.sh from our main repo and place it into your home directory, chmod +x plugin-info.sh and then run it with ./plugin-info.sh" - validations: - required: true + validations: + required: true - type: input attributes: From 6b78e0ae9c2afd03ac8f1c2bcf49fd4c73466d63 Mon Sep 17 00:00:00 2001 From: TrainDoctor <11465594+TrainDoctor@users.noreply.github.com> Date: Thu, 5 Sep 2024 12:21:58 -0700 Subject: [PATCH 18/27] Update bug_report.yml --- .github/ISSUE_TEMPLATE/bug_report.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index dd0ff925..fb0aa8e7 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -69,7 +69,7 @@ body: attributes: label: Plugin Info description: "Include all plugins installed including their version. Helpful script here: https://github.com/SteamDeckHomebrew/decky-loader/blob/main/scripts/plugin-info.sh" - placeholder: "If you don't want to collect this info manually you can download plugin-info.sh from our main repo and place it into your home directory, chmod +x plugin-info.sh and then run it with ./plugin-info.sh" + placeholder: "If you don't want to collect this info manually you can download a helpful script linked in this item's description and place it into your home directory, chmod +x plugin-info.sh and then run it with ./plugin-info.sh" validations: required: true From 1d7af36a2b275841c89815233c46cc7f105c5ddf Mon Sep 17 00:00:00 2001 From: AAGaming Date: Wed, 11 Sep 2024 19:38:58 -0400 Subject: [PATCH 19/27] add cache bust param to index.js of esmodule plugins should fix hot reload --- frontend/src/plugin-loader.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/plugin-loader.tsx b/frontend/src/plugin-loader.tsx index 1dc73372..ab086b15 100644 --- a/frontend/src/plugin-loader.tsx +++ b/frontend/src/plugin-loader.tsx @@ -421,7 +421,7 @@ class PluginLoader extends Logger { try { switch (loadType) { case PluginLoadType.ESMODULE_V1: - const plugin_exports = await import(`http://127.0.0.1:1337/plugins/${name}/dist/index.js`); + const plugin_exports = await import(`http://127.0.0.1:1337/plugins/${name}/dist/index.js?t=${Date.now()}`); let plugin = plugin_exports.default(); this.plugins.push({ From ef4ca204bdce6b06072b9fb28d709742052a0c2c Mon Sep 17 00:00:00 2001 From: AAGaming Date: Wed, 11 Sep 2024 20:16:49 -0400 Subject: [PATCH 20/27] potentially fix startup race condition --- frontend/src/index.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/frontend/src/index.ts b/frontend/src/index.ts index 97357b82..4a5fc7f8 100644 --- a/frontend/src/index.ts +++ b/frontend/src/index.ts @@ -16,8 +16,10 @@ interface Window { console.time('[Decky:Boot] Waiting for React root mount...'); let root; while ( + // Does React root node exist? !(root = document.getElementById('root')) || - !(root as any)[Object.keys(root).find((k) => k.startsWith('__reactContainer$')) as string] + // Does it have a child element? + !(root as any)[Object.keys(root).find((k) => k.startsWith('__reactContainer$')) as string].child ) { await new Promise((r) => setTimeout(r, 10)); // Can't use DFL sleep here. } From 508408ad5ae3da687080210d4ad68608eda65d85 Mon Sep 17 00:00:00 2001 From: AAGaming Date: Wed, 11 Sep 2024 20:35:24 -0400 Subject: [PATCH 21/27] use signals to shut down plugins instead of sending a socket message should reduce or outright prevent shutdown stalls --- backend/decky_loader/plugin/plugin.py | 27 ++++++++++--------- .../decky_loader/plugin/sandboxed_plugin.py | 9 ++++--- 2 files changed, 20 insertions(+), 16 deletions(-) diff --git a/backend/decky_loader/plugin/plugin.py b/backend/decky_loader/plugin/plugin.py index 28be710d..ba23fff9 100644 --- a/backend/decky_loader/plugin/plugin.py +++ b/backend/decky_loader/plugin/plugin.py @@ -120,18 +120,25 @@ async def stop(self, uninstall: bool = False): start_time = time() if self.passive: return + self.log.info(f"Shutting down {self.name}") - _, pending = await wait([ - create_task(self._socket.write_single_line(dumps({ "stop": True, "uninstall": uninstall }, ensure_ascii=False))) - ], timeout=1) + pending: set[Task[None]] | None = None; + + if uninstall: + _, pending = await wait([ + create_task(self._socket.write_single_line(dumps({ "uninstall": uninstall }, ensure_ascii=False))) + ], timeout=1) + + self.terminate() # the plugin process will handle SIGTERM and shut down cleanly without a socket message if hasattr(self, "_listener_task"): self._listener_task.cancel() await self.kill_if_still_running() - for pending_task in pending: - pending_task.cancel() + if pending: + for pending_task in pending: + pending_task.cancel() self.log.info(f"Plugin {self.name} has been stopped in {time() - start_time:.1f}s") except Exception as e: @@ -139,18 +146,14 @@ async def stop(self, uninstall: bool = False): async def kill_if_still_running(self): start_time = time() - sigtermed = False while self.proc and self.proc.is_alive(): elapsed_time = time() - start_time - if elapsed_time >= 5 and not sigtermed: - sigtermed = True - self.log.warning(f"Plugin {self.name} still alive 5 seconds after stop request! Sending SIGTERM!") - self.terminate() - elif elapsed_time >= 10: - self.log.warning(f"Plugin {self.name} still alive 10 seconds after stop request! Sending SIGKILL!") + if elapsed_time >= 5: + self.log.warning(f"Plugin {self.name} still alive 5 seconds after stop request! Sending SIGKILL!") self.terminate(True) await sleep(0.1) + def terminate(self, kill: bool = False): if self.proc and self.proc.is_alive(): if kill: diff --git a/backend/decky_loader/plugin/sandboxed_plugin.py b/backend/decky_loader/plugin/sandboxed_plugin.py index b730fa98..6223fd04 100644 --- a/backend/decky_loader/plugin/sandboxed_plugin.py +++ b/backend/decky_loader/plugin/sandboxed_plugin.py @@ -40,6 +40,7 @@ def __init__(self, self.author = author self.api_version = api_version self.shutdown_running = False + self.uninstalling = False self.log = getLogger("sandboxed_plugin") @@ -161,13 +162,13 @@ async def _uninstall(self): self.log.error("Failed to uninstall " + self.name + "!\n" + format_exc()) pass - async def shutdown(self, uninstall: bool = False): + async def shutdown(self): if not self.shutdown_running: self.shutdown_running = True self.log.info(f"Calling Loader unload function for {self.name}.") await self._unload() - if uninstall: + if self.uninstalling: self.log.info("Calling Loader uninstall function.") await self._uninstall() @@ -180,8 +181,8 @@ async def shutdown(self, uninstall: bool = False): async def on_new_message(self, message : str) -> str|None: data = loads(message) - if "stop" in data: - await self.shutdown(data.get('uninstall')) + if "uninstall" in data: + self.uninstalling = data.get("uninstall") d: SocketResponseDict = {"type": SocketMessageType.RESPONSE, "res": None, "success": True, "id": data["id"]} try: From 9df5f000683eb9fc33b573c1298836a185a03caf Mon Sep 17 00:00:00 2001 From: AAGaming Date: Wed, 11 Sep 2024 21:00:19 -0400 Subject: [PATCH 22/27] drop TimeoutStopSec to 15s --- dist/plugin_loader-prerelease.service | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dist/plugin_loader-prerelease.service b/dist/plugin_loader-prerelease.service index 78970909..3010ce39 100644 --- a/dist/plugin_loader-prerelease.service +++ b/dist/plugin_loader-prerelease.service @@ -6,7 +6,7 @@ Type=simple User=root Restart=always KillMode=process -TimeoutStopSec=45 +TimeoutStopSec=15 ExecStart=${HOMEBREW_FOLDER}/services/PluginLoader WorkingDirectory=${HOMEBREW_FOLDER}/services Environment=UNPRIVILEGED_PATH=${HOMEBREW_FOLDER} From c487a6e15ab86f846a99b25186dbfbd24d5c3f36 Mon Sep 17 00:00:00 2001 From: AAGaming Date: Wed, 11 Sep 2024 21:02:41 -0400 Subject: [PATCH 23/27] deprecate install scripts in repo (use decky-installer/cli instead, they're the same scripts but more up to date) --- dist/install_prerelease.sh | 71 ++------------------------------------ dist/install_release.sh | 69 ++---------------------------------- 2 files changed, 4 insertions(+), 136 deletions(-) diff --git a/dist/install_prerelease.sh b/dist/install_prerelease.sh index 9e5ce9cc..bad3546a 100644 --- a/dist/install_prerelease.sh +++ b/dist/install_prerelease.sh @@ -1,70 +1,3 @@ #!/bin/sh - -[ "$UID" -eq 0 ] || exec sudo "$0" "$@" - -echo "Installing Steam Deck Plugin Loader pre-release..." - -USER_DIR="$(getent passwd $SUDO_USER | cut -d: -f6)" -HOMEBREW_FOLDER="${USER_DIR}/homebrew" - -# Create folder structure -rm -rf "${HOMEBREW_FOLDER}/services" -sudo -u $SUDO_USER mkdir -p "${HOMEBREW_FOLDER}/services" -sudo -u $SUDO_USER mkdir -p "${HOMEBREW_FOLDER}/plugins" -touch "${USER_DIR}/.steam/steam/.cef-enable-remote-debugging" - -# Download latest release and install it -RELEASE=$(curl -s 'https://api.github.com/repos/SteamDeckHomebrew/decky-loader/releases' | jq -r "first(.[] | select(.prerelease == "true"))") -VERSION=$(jq -r '.tag_name' <<< ${RELEASE} ) -DOWNLOADURL=$(jq -r '.assets[].browser_download_url | select(endswith("PluginLoader"))' <<< ${RELEASE}) - -printf "Installing version %s...\n" "${VERSION}" -curl -L $DOWNLOADURL --output ${HOMEBREW_FOLDER}/services/PluginLoader -chmod +x ${HOMEBREW_FOLDER}/services/PluginLoader -echo $VERSION > ${HOMEBREW_FOLDER}/services/.loader.version - -systemctl --user stop plugin_loader 2> /dev/null -systemctl --user disable plugin_loader 2> /dev/null - -systemctl stop plugin_loader 2> /dev/null -systemctl disable plugin_loader 2> /dev/null - -curl -L https://raw.githubusercontent.com/SteamDeckHomebrew/decky-loader/main/dist/plugin_loader-prerelease.service --output ${HOMEBREW_FOLDER}/services/plugin_loader-prerelease.service - -cat > "${HOMEBREW_FOLDER}/services/plugin_loader-backup.service" <<- EOM -[Unit] -Description=SteamDeck Plugin Loader -After=network.target -[Service] -Type=simple -User=root -Restart=always -KillMode=process -TimeoutStopSec=45 -ExecStart=${HOMEBREW_FOLDER}/services/PluginLoader -WorkingDirectory=${HOMEBREW_FOLDER}/services -Environment=UNPRIVILEGED_PATH=${HOMEBREW_FOLDER} -Environment=PRIVILEGED_PATH=${HOMEBREW_FOLDER} -Environment=LOG_LEVEL=DEBUG -[Install] -WantedBy=multi-user.target -EOM - -if [[ -f "${HOMEBREW_FOLDER}/services/plugin_loader-prerelease.service" ]]; then - printf "Grabbed latest prerelease service.\n" - sed -i -e "s|\${HOMEBREW_FOLDER}|${HOMEBREW_FOLDER}|" "${HOMEBREW_FOLDER}/services/plugin_loader-prerelease.service" - cp -f "${HOMEBREW_FOLDER}/services/plugin_loader-prerelease.service" "/etc/systemd/system/plugin_loader.service" -else - printf "Could not curl latest prerelease systemd service, using built-in service as a backup!\n" - rm -f "/etc/systemd/system/plugin_loader.service" - cp "${HOMEBREW_FOLDER}/services/plugin_loader-backup.service" "/etc/systemd/system/plugin_loader.service" -fi - -mkdir -p ${HOMEBREW_FOLDER}/services/.systemd -cp ${HOMEBREW_FOLDER}/services/plugin_loader-prerelease.service ${HOMEBREW_FOLDER}/services/.systemd/plugin_loader-prerelease.service -cp ${HOMEBREW_FOLDER}/services/plugin_loader-backup.service ${HOMEBREW_FOLDER}/services/.systemd/plugin_loader-backup.service -rm ${HOMEBREW_FOLDER}/services/plugin_loader-backup.service ${HOMEBREW_FOLDER}/services/plugin_loader-prerelease.service - -systemctl daemon-reload -systemctl start plugin_loader -systemctl enable plugin_loader +echo This script is deprecated! Use https://github.com/SteamDeckHomebrew/decky-installer/raw/main/cli/install_prerelease.sh instead! +exit 1 \ No newline at end of file diff --git a/dist/install_release.sh b/dist/install_release.sh index 6b31a0c4..4f2c3b17 100644 --- a/dist/install_release.sh +++ b/dist/install_release.sh @@ -1,68 +1,3 @@ #!/bin/sh - -[ "$UID" -eq 0 ] || exec sudo "$0" "$@" - -echo "Installing Steam Deck Plugin Loader release..." - -USER_DIR="$(getent passwd $SUDO_USER | cut -d: -f6)" -HOMEBREW_FOLDER="${USER_DIR}/homebrew" - -# Create folder structure -rm -rf "${HOMEBREW_FOLDER}/services" -sudo -u $SUDO_USER mkdir -p "${HOMEBREW_FOLDER}/services" -sudo -u $SUDO_USER mkdir -p "${HOMEBREW_FOLDER}/plugins" -touch "${USER_DIR}/.steam/steam/.cef-enable-remote-debugging" - -# Download latest release and install it -RELEASE=$(curl -s 'https://api.github.com/repos/SteamDeckHomebrew/decky-loader/releases' | jq -r "first(.[] | select(.prerelease == "false"))") -VERSION=$(jq -r '.tag_name' <<< ${RELEASE} ) -DOWNLOADURL=$(jq -r '.assets[].browser_download_url | select(endswith("PluginLoader"))' <<< ${RELEASE}) - -printf "Installing version %s...\n" "${VERSION}" -curl -L $DOWNLOADURL --output ${HOMEBREW_FOLDER}/services/PluginLoader -chmod +x ${HOMEBREW_FOLDER}/services/PluginLoader -echo $VERSION > ${HOMEBREW_FOLDER}/services/.loader.version - -systemctl --user stop plugin_loader 2> /dev/null -systemctl --user disable plugin_loader 2> /dev/null - -systemctl stop plugin_loader 2> /dev/null -systemctl disable plugin_loader 2> /dev/null - -curl -L https://raw.githubusercontent.com/SteamDeckHomebrew/decky-loader/main/dist/plugin_loader-release.service --output ${HOMEBREW_FOLDER}/services/plugin_loader-release.service - -cat > "${HOMEBREW_FOLDER}/services/plugin_loader-backup.service" <<- EOM -[Unit] -Description=SteamDeck Plugin Loader -After=network.target -[Service] -Type=simple -User=root -Restart=always -ExecStart=${HOMEBREW_FOLDER}/services/PluginLoader -WorkingDirectory=${HOMEBREW_FOLDER}/services -Environment=UNPRIVILEGED_PATH=${HOMEBREW_FOLDER} -Environment=PRIVILEGED_PATH=${HOMEBREW_FOLDER} -Environment=LOG_LEVEL=INFO -[Install] -WantedBy=multi-user.target -EOM - -if [[ -f "${HOMEBREW_FOLDER}/services/plugin_loader-release.service" ]]; then - printf "Grabbed latest release service.\n" - sed -i -e "s|\${HOMEBREW_FOLDER}|${HOMEBREW_FOLDER}|" "${HOMEBREW_FOLDER}/services/plugin_loader-release.service" - cp -f "${HOMEBREW_FOLDER}/services/plugin_loader-release.service" "/etc/systemd/system/plugin_loader.service" -else - printf "Could not curl latest release systemd service, using built-in service as a backup!\n" - rm -f "/etc/systemd/system/plugin_loader.service" - cp "${HOMEBREW_FOLDER}/services/plugin_loader-backup.service" "/etc/systemd/system/plugin_loader.service" -fi - -mkdir -p ${HOMEBREW_FOLDER}/services/.systemd -cp ${HOMEBREW_FOLDER}/services/plugin_loader-release.service ${HOMEBREW_FOLDER}/services/.systemd/plugin_loader-release.service -cp ${HOMEBREW_FOLDER}/services/plugin_loader-backup.service ${HOMEBREW_FOLDER}/services/.systemd/plugin_loader-backup.service -rm ${HOMEBREW_FOLDER}/services/plugin_loader-backup.service ${HOMEBREW_FOLDER}/services/plugin_loader-release.service - -systemctl daemon-reload -systemctl start plugin_loader -systemctl enable plugin_loader +echo This script is deprecated! Use https://github.com/SteamDeckHomebrew/decky-installer/raw/main/cli/install_release.sh instead! +exit 1 \ No newline at end of file From caac379b08728f7a739971fb7046cfe91a91ad6c Mon Sep 17 00:00:00 2001 From: AAGaming Date: Wed, 11 Sep 2024 21:50:48 -0400 Subject: [PATCH 24/27] just sleep 500ms for now to work around startup race condition --- frontend/src/index.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/frontend/src/index.ts b/frontend/src/index.ts index 4a5fc7f8..8d67f175 100644 --- a/frontend/src/index.ts +++ b/frontend/src/index.ts @@ -27,6 +27,7 @@ interface Window { if (!window.SP_REACT) { console.debug('[Decky:Boot] Setting up Webpack & React globals...'); + await new Promise((r) => setTimeout(r, 500)); // Can't use DFL sleep here. // deliberate partial import const DFLWebpack = await import('@decky/ui/dist/webpack'); window.SP_REACT = DFLWebpack.findModule((m) => m.Component && m.PureComponent && m.useLayoutEffect); From d2c5aef58b69072b9c20c6d4a6bc50728b442740 Mon Sep 17 00:00:00 2001 From: TrainDoctor <11465594+TrainDoctor@users.noreply.github.com> Date: Wed, 11 Sep 2024 19:49:39 -0700 Subject: [PATCH 25/27] Update release.yml --- .github/workflows/release.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 5753000f..b8a0f724 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -36,10 +36,10 @@ jobs: uses: actions/checkout@v4 - name: Install semver-tool asdf - uses: asdf-vm/actions/install@v1 + uses: asdf-vm/actions/install@v3 with: tool_versions: | - semver 3.3.0 + semver 3.4.0 - name: Get latest release uses: rez0n/actions-github-release@main From 1284075d024615dc3f10974acf7e1a6c59a5f909 Mon Sep 17 00:00:00 2001 From: AAGaming Date: Wed, 11 Sep 2024 22:54:16 -0400 Subject: [PATCH 26/27] update release systemd service --- dist/plugin_loader-release.service | 2 ++ 1 file changed, 2 insertions(+) diff --git a/dist/plugin_loader-release.service b/dist/plugin_loader-release.service index f427b27a..5bb1b795 100644 --- a/dist/plugin_loader-release.service +++ b/dist/plugin_loader-release.service @@ -5,6 +5,8 @@ After=network.target Type=simple User=root Restart=always +KillMode=process +TimeoutStopSec=15 ExecStart=${HOMEBREW_FOLDER}/services/PluginLoader WorkingDirectory=${HOMEBREW_FOLDER}/services Environment=UNPRIVILEGED_PATH=${HOMEBREW_FOLDER} From e87ce625fb33b838fdb3e337653956673b464e58 Mon Sep 17 00:00:00 2001 From: Sims <38142618+suchmememanyskill@users.noreply.github.com> Date: Sat, 14 Sep 2024 01:59:35 +0200 Subject: [PATCH 27/27] Test (#701) --- backend/decky_loader/plugin/sandboxed_plugin.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/backend/decky_loader/plugin/sandboxed_plugin.py b/backend/decky_loader/plugin/sandboxed_plugin.py index 6223fd04..85ad6e13 100644 --- a/backend/decky_loader/plugin/sandboxed_plugin.py +++ b/backend/decky_loader/plugin/sandboxed_plugin.py @@ -11,7 +11,7 @@ from .messages import SocketResponseDict, SocketMessageType from ..localplatform.localsocket import LocalSocket -from ..localplatform.localplatform import setgid, setuid, get_username, get_home_path +from ..localplatform.localplatform import setgid, setuid, get_username, get_home_path, ON_LINUX from ..enums import UserType from .. import helpers, settings, injector # pyright: ignore [reportUnusedImport] @@ -54,10 +54,13 @@ def initialize(self, socket: LocalSocket): loop = new_event_loop() set_event_loop(loop) # When running Decky manually in a terminal, ctrl-c will trigger this, so we have to handle it properly - loop.add_signal_handler(SIGINT, lambda: ensure_future(self.shutdown())) - loop.add_signal_handler(SIGTERM, lambda: ensure_future(self.shutdown())) + if ON_LINUX: + loop.add_signal_handler(SIGINT, lambda: ensure_future(self.shutdown())) + loop.add_signal_handler(SIGTERM, lambda: ensure_future(self.shutdown())) + if self.passive: return + setgid(UserType.ROOT if "root" in self.flags else UserType.HOST_USER) setuid(UserType.ROOT if "root" in self.flags else UserType.HOST_USER) # export a bunch of environment variables to help plugin developers