diff options
| author | xXJSONDeruloXx <danielhimebauch@gmail.com> | 2026-09-11 11:08:12 -0400 |
|---|---|---|
| committer | xXJSONDeruloXx <danielhimebauch@gmail.com> | 2026-09-11 11:08:12 -0400 |
| commit | 954b2abc47d8772211cb8ecee523900f823184e8 (patch) | |
| tree | c25109bebc87ccfb9e71a0863a10e81caa695908 /py_modules | |
| parent | bc73d150230f92d614962f7384a7430cf64dcd14 (diff) | |
| download | decky-lsfg-vk-954b2abc47d8772211cb8ecee523900f823184e8.tar.gz decky-lsfg-vk-954b2abc47d8772211cb8ecee523900f823184e8.zip | |
better uninstall cleanup
Diffstat (limited to 'py_modules')
| -rw-r--r-- | py_modules/lsfg_vk/configuration.py | 26 | ||||
| -rw-r--r-- | py_modules/lsfg_vk/installation.py | 31 | ||||
| -rw-r--r-- | py_modules/lsfg_vk/plugin.py | 38 | ||||
| -rw-r--r-- | py_modules/lsfg_vk/wrapper_service.py | 74 |
4 files changed, 147 insertions, 22 deletions
diff --git a/py_modules/lsfg_vk/configuration.py b/py_modules/lsfg_vk/configuration.py index 17dcaf3..d1852a0 100644 --- a/py_modules/lsfg_vk/configuration.py +++ b/py_modules/lsfg_vk/configuration.py @@ -72,20 +72,30 @@ class ConfigurationService(BaseService): self.log.error(f"Error reading game configs: {error}") return self._error_response(dict, str(error), games=[]) + def update_global_config(self, config: Dict[str, Any]) -> Dict[str, Any]: + try: + data = self._get_profile_data() + merged_config = {**data["global_config"], **config} + validated = self._public_config(merged_config) + data["global_config"] = { + "dll": validated["dll"], + "no_fp16": validated["no_fp16"], + } + self._save_profile_data(data) + return self._success_response(dict, global_config=dict(data["global_config"])) + except Exception as error: + return self._error_response(dict, str(error), global_config=None) + def update_game_config(self, appid: str, game_name: str, config: Dict[str, Any]) -> Dict[str, Any]: try: data = self._get_profile_data() old_name, _ = self._profile_for_appid(data, appid) name = self._profile_name(data, appid, game_name) - merged_config = {**data["global_config"], **config} + merged_config = {**data["global_config"], **{key: value for key, value in config.items() if key != "no_fp16"}} if not config.get("dll"): merged_config["dll"] = data["global_config"].get("dll", "") validated = self._public_config(merged_config) validated["active_in"] = [str(appid)] - data["global_config"] = { - "dll": validated["dll"], - "no_fp16": validated["no_fp16"], - } if old_name and old_name != name: data["profiles"].pop(old_name, None) data["profiles"][name] = validated @@ -114,15 +124,11 @@ class ConfigurationService(BaseService): try: data = self._get_profile_data() name = self.flatpak_profile_name(app_id) - merged_config = {**data["global_config"], **config} + merged_config = {**data["global_config"], **{key: value for key, value in config.items() if key != "no_fp16"}} if not config.get("dll"): merged_config["dll"] = data["global_config"].get("dll", "") validated = self._public_config(merged_config) validated["active_in"] = [] - data["global_config"] = { - "dll": validated["dll"], - "no_fp16": validated["no_fp16"], - } data["profiles"][name] = validated self._save_profile_data(data) return self._success_response(dict, app_id=str(app_id), profile=name, exists=True, config=validated) diff --git a/py_modules/lsfg_vk/installation.py b/py_modules/lsfg_vk/installation.py index a73706c..e7366ee 100644 --- a/py_modules/lsfg_vk/installation.py +++ b/py_modules/lsfg_vk/installation.py @@ -162,6 +162,25 @@ class InstallationService(BaseService): for path in (self.legacy_lib_file, self.legacy_json_file): self._remove_if_exists(path) + def _prune_empty_directories(self) -> None: + # Only prune directories created by this plugin. Never remove a + # non-empty directory because the user's other tools may use it. + candidates = ( + self.config_dir, + self.local_bin_dir, + self.local_lib_dir, + self.local_share_dir, + self.user_home / LOCAL_SHARE / "applications", + self.user_home / LOCAL_SHARE / "icons/hicolor/256x256/apps", + ) + for directory in candidates: + try: + if directory.is_dir() and not directory.is_symlink(): + directory.rmdir() + self.log.info(f"Removed empty directory {directory}") + except OSError: + continue + def check_installation(self) -> InstallationCheckResponse: try: installation_error = None @@ -200,9 +219,12 @@ class InstallationService(BaseService): self.user_home / LOCAL_SHARE / "icons/hicolor/256x256/apps" / UI_ICON_FILENAME, self.legacy_lib_file, self.legacy_json_file, + self.legacy_script_path, + self.config_file_path, ) if self._remove_if_exists(path) ] + self._prune_empty_directories() if not removed: return self._success_response( UninstallationResponse, @@ -221,9 +243,14 @@ class InstallationService(BaseService): removed_files=None, ) - def cleanup_on_uninstall(self) -> None: + def cleanup_on_uninstall(self) -> bool: try: - self.uninstall() + result = self.uninstall() + if not result.get("success"): + self.log.error(f"Error cleaning up lsfg-vk files during uninstall: {result.get('error')}") + return False + return True except Exception as error: self.log.error(f"Error cleaning up lsfg-vk files during uninstall: {error}") self.log.error(traceback.format_exc()) + return False diff --git a/py_modules/lsfg_vk/plugin.py b/py_modules/lsfg_vk/plugin.py index d20fabf..918c3f8 100644 --- a/py_modules/lsfg_vk/plugin.py +++ b/py_modules/lsfg_vk/plugin.py @@ -34,21 +34,35 @@ class Plugin: async def check_lsfg_vk_installed(self): return self.installation_service.check_installation() - async def uninstall_lsfg_vk(self): + def _cleanup_runtime_state(self): 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() + if not wrapper.get("success"): + return wrapper.get("error") or "Could not remove workaround state" + return None + + async def uninstall_lsfg_vk(self): + error = self._cleanup_runtime_state() + if error: return { "success": False, "message": "", - "error": flatpak.get("error") or "Could not clean up Flatpak support", + "error": error, "removed_files": None, } - self.configuration_service.reset_all_flatpak_configs() return self.installation_service.uninstall() async def get_game_configs(self): return self.configuration_service.get_game_configs() + async def update_global_config(self, config: Dict[str, Any]): + return self.configuration_service.update_global_config(config) + async def get_installed_games(self): return self.steam_service.get_installed_games() @@ -64,13 +78,17 @@ class Plugin: async def get_workaround_state(self, appid: str): return self.wrapper_service.get(appid) + async def get_workaround_apps(self): + return self.wrapper_service.list_apps() + async def set_workaround_state( self, appid: str, state: Dict[str, Any], command_token_added: bool = False, + non_steam: bool = False, ): - return self.wrapper_service.set(appid, state, command_token_added) + return self.wrapper_service.set(appid, state, command_token_added, non_steam) async def remove_workaround_state(self, appid: str): return self.wrapper_service.remove(appid) @@ -172,13 +190,13 @@ class Plugin: async def _uninstall(self): decky.logger.info("decky-lsfg-vk plugin being uninstalled") try: - result = self.flatpak_service.remove_plugin_owned_environment() - if result.get("success"): - self.configuration_service.reset_all_flatpak_configs() - else: - decky.logger.warning(result.get("error")) + error = self._cleanup_runtime_state() + if error: + decky.logger.warning(f"Preserving lsfg-vk files because uninstall cleanup failed: {error}") + return except Exception as error: - decky.logger.error(f"Error during Flatpak cleanup: {error}") + decky.logger.error(f"Error during lsfg-vk cleanup: {error}") + return self.installation_service.cleanup_on_uninstall() decky.logger.info("decky-lsfg-vk plugin uninstall cleanup completed") diff --git a/py_modules/lsfg_vk/wrapper_service.py b/py_modules/lsfg_vk/wrapper_service.py index ebe9526..906e55b 100644 --- a/py_modules/lsfg_vk/wrapper_service.py +++ b/py_modules/lsfg_vk/wrapper_service.py @@ -87,9 +87,12 @@ class WrapperService(BaseService): entry = { "state": cls._validate_state(raw.get("state")), "command_token_added": raw.get("command_token_added", False), + "non_steam": raw.get("non_steam", False), } if type(entry["command_token_added"]) is not bool: raise ValueError("command_token_added must be a boolean") + if type(entry["non_steam"]) is not bool: + raise ValueError("non_steam must be a boolean") return entry @classmethod @@ -263,6 +266,7 @@ class WrapperService(BaseService): "wrapper_path": self.WRAPPER_TOKEN, "wrapper_owned": self._wrapper_marker() if document["apps"] else False, "command_token_added": entry.get("command_token_added", False) if entry else False, + "non_steam": entry.get("non_steam", False) if entry else False, } def get(self, appid: str) -> Dict[str, Any]: @@ -281,6 +285,7 @@ class WrapperService(BaseService): "state": None, "wrapper_path": self.WRAPPER_TOKEN, "wrapper_owned": False, + "non_steam": False, } def set( @@ -288,18 +293,22 @@ class WrapperService(BaseService): appid: str, state: Dict[str, Any], command_token_added: bool = False, + non_steam: bool = False, ) -> Dict[str, Any]: try: normalized = self._valid_appid(appid) validated_state = self._validate_state(state) if type(command_token_added) is not bool: raise ValueError("command_token_added must be a boolean") + if type(non_steam) is not bool: + raise ValueError("non_steam must be a boolean") with self._lock: self._assert_wrapper_owned_or_absent() document, _, _ = self._read_document() document["apps"][normalized] = { "state": validated_state, "command_token_added": command_token_added, + "non_steam": non_steam, } self._write_pair(document) return self._response(document, normalized) @@ -312,6 +321,7 @@ class WrapperService(BaseService): "state": None, "wrapper_path": self.WRAPPER_TOKEN, "wrapper_owned": False, + "non_steam": False, } def remove(self, appid: str) -> Dict[str, Any]: @@ -334,6 +344,7 @@ class WrapperService(BaseService): "state": None, "wrapper_path": self.WRAPPER_TOKEN, "wrapper_owned": False, + "non_steam": False, } def repair(self) -> Dict[str, Any]: @@ -353,3 +364,66 @@ class WrapperService(BaseService): "wrapper_path": self.WRAPPER_TOKEN, "wrapper_owned": False, } + + def list_apps(self) -> Dict[str, Any]: + try: + with self._lock: + document, _, _ = self._read_document() + self._assert_wrapper_owned_or_absent() + apps = [ + { + "appid": appid, + "non_steam": entry.get("non_steam", False), + "command_token_added": entry.get("command_token_added", False), + } + for appid, entry in document["apps"].items() + ] + return { + "success": True, + "message": "", + "error": None, + "apps": apps, + "wrapper_path": self.WRAPPER_TOKEN, + } + except Exception as error: + return { + "success": False, + "message": "", + "error": str(error), + "apps": [], + "wrapper_path": self.WRAPPER_TOKEN, + } + + def purge(self) -> Dict[str, Any]: + """Remove the plugin-owned wrapper and its sidecar during uninstall. + + This is deliberately separate from ``remove``: normal profile removal + leaves a safe passthrough wrapper for the remaining profiles, while an + uninstall should remove the wrapper entirely. Both files are + validated before anything is removed so a user's replacement wrapper + or damaged state is left untouched. + """ + removed = [] + try: + with self._lock: + _document, sidecar_exists, _ = self._read_document() + wrapper_owned = self._assert_wrapper_owned_or_absent() + if wrapper_owned: + self.wrapper_path.unlink() + removed.append(str(self.wrapper_path)) + if sidecar_exists: + self.sidecar_path.unlink() + removed.append(str(self.sidecar_path)) + return { + "success": True, + "message": "Removed lsfg-vk workaround state", + "error": None, + "removed_files": removed, + } + except Exception as error: + return { + "success": False, + "message": "", + "error": str(error), + "removed_files": removed or None, + } |
