summaryrefslogtreecommitdiff
path: root/py_modules
diff options
context:
space:
mode:
authorxXJSONDeruloXx <danielhimebauch@gmail.com>2026-09-11 11:08:12 -0400
committerxXJSONDeruloXx <danielhimebauch@gmail.com>2026-09-11 11:08:12 -0400
commit954b2abc47d8772211cb8ecee523900f823184e8 (patch)
treec25109bebc87ccfb9e71a0863a10e81caa695908 /py_modules
parentbc73d150230f92d614962f7384a7430cf64dcd14 (diff)
downloaddecky-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.py26
-rw-r--r--py_modules/lsfg_vk/installation.py31
-rw-r--r--py_modules/lsfg_vk/plugin.py38
-rw-r--r--py_modules/lsfg_vk/wrapper_service.py74
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,
+ }