From c7ca9c7ec2660d279f5e6957ef882390a8783667 Mon Sep 17 00:00:00 2001 From: xXJSONDeruloXx Date: Fri, 11 Sep 2026 11:47:16 -0400 Subject: handle decky uninstall to avoid unlaunchable game wrappers --- py_modules/lsfg_vk/plugin.py | 6 +++--- py_modules/lsfg_vk/wrapper_service.py | 35 +++++++++++++++++++++++++++++++++++ tests/test_plugin_migration.py | 5 +++-- tests/test_wrapper_service.py | 18 ++++++++++++++++++ 4 files changed, 59 insertions(+), 5 deletions(-) diff --git a/py_modules/lsfg_vk/plugin.py b/py_modules/lsfg_vk/plugin.py index 918c3f8..a1fd40f 100644 --- a/py_modules/lsfg_vk/plugin.py +++ b/py_modules/lsfg_vk/plugin.py @@ -34,14 +34,14 @@ class Plugin: async def check_lsfg_vk_installed(self): return self.installation_service.check_installation() - def _cleanup_runtime_state(self): + def _cleanup_runtime_state(self, preserve_wrapper: bool = False): flatpak = self.flatpak_service.remove_plugin_owned_environment() if not flatpak.get("success"): return flatpak.get("error") or "Could not clean up Flatpak support" profiles = self.configuration_service.reset_all_flatpak_configs() if not profiles.get("success"): return profiles.get("error") or "Could not remove Flatpak profiles" - wrapper = self.wrapper_service.purge() + wrapper = self.wrapper_service.neutralize() if preserve_wrapper else self.wrapper_service.purge() if not wrapper.get("success"): return wrapper.get("error") or "Could not remove workaround state" return None @@ -190,7 +190,7 @@ class Plugin: async def _uninstall(self): decky.logger.info("decky-lsfg-vk plugin being uninstalled") try: - error = self._cleanup_runtime_state() + error = self._cleanup_runtime_state(preserve_wrapper=True) if error: decky.logger.warning(f"Preserving lsfg-vk files because uninstall cleanup failed: {error}") return diff --git a/py_modules/lsfg_vk/wrapper_service.py b/py_modules/lsfg_vk/wrapper_service.py index 906e55b..30c2323 100644 --- a/py_modules/lsfg_vk/wrapper_service.py +++ b/py_modules/lsfg_vk/wrapper_service.py @@ -427,3 +427,38 @@ class WrapperService(BaseService): "error": str(error), "removed_files": removed or None, } + + def neutralize(self) -> Dict[str, Any]: + """Leave an owned passthrough wrapper for Decky-level uninstall. + + Decky can remove this plugin without giving the frontend a chance to + clean Steam launch options first. Keeping a dependency-free wrapper + prevents those options from turning into a broken executable path. + """ + try: + with self._lock: + _document, sidecar_exists, _ = self._read_document() + self._assert_wrapper_owned_or_absent() + self._write_file( + self.wrapper_path, + "#!/bin/sh\n" + f"{self.MARKER}\n" + "# Safe passthrough retained for existing Steam launch options.\n" + "exec \"$@\"\n", + 0o755, + ) + if sidecar_exists: + self.sidecar_path.unlink() + return { + "success": True, + "message": "Replaced lsfg-vk wrapper with a safe passthrough", + "error": None, + "removed_files": [str(self.sidecar_path)] if sidecar_exists else [], + } + except Exception as error: + return { + "success": False, + "message": "", + "error": str(error), + "removed_files": None, + } diff --git a/tests/test_plugin_migration.py b/tests/test_plugin_migration.py index 1a7cb29..fc51470 100644 --- a/tests/test_plugin_migration.py +++ b/tests/test_plugin_migration.py @@ -66,13 +66,14 @@ class PluginMigrationTests(unittest.TestCase): plugin.wrapper_service = Mock() plugin.flatpak_service.remove_plugin_owned_environment.return_value = {"success": True} plugin.configuration_service.reset_all_flatpak_configs.return_value = {"success": True} - plugin.wrapper_service.purge.return_value = {"success": True} + plugin.wrapper_service.neutralize.return_value = {"success": True} asyncio.run(plugin._uninstall()) plugin.flatpak_service.remove_plugin_owned_environment.assert_called_once_with() plugin.configuration_service.reset_all_flatpak_configs.assert_called_once_with() - plugin.wrapper_service.purge.assert_called_once_with() + plugin.wrapper_service.neutralize.assert_called_once_with() + plugin.wrapper_service.purge.assert_not_called() plugin.installation_service.cleanup_on_uninstall.assert_called_once_with() finally: self._restore(previous_decky, previous_tomllib, previous_plugin) diff --git a/tests/test_wrapper_service.py b/tests/test_wrapper_service.py index 20a08ff..46e029b 100644 --- a/tests/test_wrapper_service.py +++ b/tests/test_wrapper_service.py @@ -187,6 +187,24 @@ class WrapperServiceTests(unittest.TestCase): self.assertTrue(self.service.wrapper_path.exists()) self.assertTrue(self.service.sidecar_path.exists()) + def test_neutralize_leaves_dependency_free_passthrough_wrapper(self): + self.service.set("123", self._state()) + response = self.service.neutralize() + self.assertTrue(response["success"]) + self.assertFalse(self.service.sidecar_path.exists()) + self.assertTrue(self.service.wrapper_path.exists()) + content = self.service.wrapper_path.read_text(encoding="utf-8") + self.assertIn(self.service.MARKER, content) + self.assertNotIn("LSFGVK_CONFIG", content) + self.assertEqual(self._run(123, "/usr/bin/printf", "ok").stdout, "ok") + + def test_neutralize_refuses_foreign_wrapper(self): + self.service.wrapper_path.write_text("#!/bin/sh\necho foreign\n", encoding="utf-8") + response = self.service.neutralize() + self.assertFalse(response["success"]) + self.assertIn("unowned", response["error"]) + self.assertEqual(self.service.wrapper_path.read_text(encoding="utf-8"), "#!/bin/sh\necho foreign\n") + if __name__ == "__main__": unittest.main() -- cgit v1.2.3