Skip to content

Commit

Permalink
Add-on store: Clean up failed installs (#15921)
Browse files Browse the repository at this point in the history
Fixes #15719

Summary of the issue:
If an add-ons install failed, it might never be cleaned up, causing NVDA to be constantly expecting there are add-ons pending install.
This causes a warning dialog that NVDA must be restarted when closing the add-on store.

Description of user facing changes
Pending installs that fail are removed and cleaned up, ensuring it is easy to attempt a future install.
The warning dialog no longer fires as NVDA cleans up failed installs.

Description of development approach
Clear the pending install set and delete pending install add-on folders after loading add-ons.
When loading add-ons, all installs should complete, and there should be no new pending installs from the user.

If an install fails, also attempt to clean up the files immediately after.
  • Loading branch information
seanbudd committed Dec 14, 2023
1 parent ec8951e commit 81e17be
Showing 1 changed file with 36 additions and 4 deletions.
40 changes: 36 additions & 4 deletions source/addonHandler/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
# See the file COPYING for more details.

from abc import abstractmethod, ABC
import glob
import sys
import os.path
import gettext
Expand All @@ -19,7 +20,6 @@
from typing import (
Callable,
Dict,
List,
Optional,
Set,
TYPE_CHECKING,
Expand Down Expand Up @@ -203,6 +203,25 @@ def cleanupRemovedDisabledAddons(self) -> None:
log.debug(f"Discarding {disabledAddonName} from disabled add-ons as it has been uninstalled.")
self[AddonStateCategory.DISABLED].discard(disabledAddonName)

def _cleanupInstalledAddons(self) -> None:
# There should be no pending installs after add-ons have been loaded during initialization.
for path in _getDefaultAddonPaths():
pendingInstallPaths = glob.glob(f"{path}/*.{ADDON_PENDINGINSTALL_SUFFIX}")
for pendingInstallPath in pendingInstallPaths:
if os.path.exists(pendingInstallPath):
try:
log.error(f"Removing failed install of {pendingInstallPath}")
shutil.rmtree(pendingInstallPath, ignore_errors=True)
except OSError:
log.error(f"Failed to remove {pendingInstallPath}", exc_info=True)

if self[AddonStateCategory.PENDING_INSTALL]:
log.error(
f"Discarding {self[AddonStateCategory.PENDING_INSTALL]} from pending install add-ons "
"as their install failed."
)
self[AddonStateCategory.PENDING_INSTALL].clear()

def _cleanupCompatibleAddonsFromDowngrade(self) -> None:
from addonStore.dataManager import addonDataManager
installedAddons = addonDataManager._installedAddonsCache.installedAddons
Expand Down Expand Up @@ -287,6 +306,7 @@ def initialize():
getAvailableAddons(refresh=True, isFirstLoad=True)
state.cleanupRemovedDisabledAddons()
state._cleanupCompatibleAddonsFromDowngrade()
state._cleanupInstalledAddons()
if NVDAState.shouldWriteToDisk():
state.save()
initializeModulePackagePaths()
Expand All @@ -303,8 +323,8 @@ def terminate():
pass


def _getDefaultAddonPaths() -> List[str]:
""" Returns paths where addons can be found.
def _getDefaultAddonPaths() -> list[str]:
r""" Returns paths where addons can be found.
For now, only <userConfig>\addons is supported.
"""
addon_paths = []
Expand Down Expand Up @@ -497,14 +517,26 @@ def __init__(self, path: str):
_report_manifest_errors(self.manifest)
raise AddonError("Manifest file has errors.")

def completeInstall(self) -> str:
def completeInstall(self) -> Optional[str]:
if not os.path.exists(self.pendingInstallPath):
log.error(f"Pending install path {self.pendingInstallPath} does not exist")
return None

try:
os.rename(self.pendingInstallPath, self.installPath)
state[AddonStateCategory.PENDING_INSTALL].discard(self.name)
return self.installPath
except OSError:
log.error(f"Failed to complete addon installation for {self.name}", exc_info=True)

# Remove pending install folder
try:
log.error(f"Removing failed install of {self.pendingInstallPath}")
shutil.rmtree(self.pendingInstallPath, ignore_errors=True)
state[AddonStateCategory.PENDING_INSTALL].discard(self.name)
except OSError:
log.error(f"Failed to remove {self.pendingInstallPath}", exc_info=True)

def requestRemove(self):
"""Marks this addon for removal on NVDA restart."""
if self.isPendingInstall and not self.isInstalled:
Expand Down

0 comments on commit 81e17be

Please sign in to comment.