55# See the file COPYING for more details.
66
77from abc import abstractmethod , ABC
8+ import glob
89import sys
910import os .path
1011import gettext
1920from typing import (
2021 Callable ,
2122 Dict ,
22- List ,
2323 Optional ,
2424 Set ,
2525 TYPE_CHECKING ,
@@ -203,6 +203,22 @@ def cleanupRemovedDisabledAddons(self) -> None:
203203 log .debug (f"Discarding { disabledAddonName } from disabled add-ons as it has been uninstalled." )
204204 self [AddonStateCategory .DISABLED ].discard (disabledAddonName )
205205
206+ def _cleanupInstalledAddons (self ) -> None :
207+ # There should be no pending installs after add-ons have been loaded during initialization.
208+ for path in _getDefaultAddonPaths ():
209+ pendingInstallPaths = glob .glob (f"{ path } /*.{ ADDON_PENDINGINSTALL_SUFFIX } " )
210+ for pendingInstallPath in pendingInstallPaths :
211+ if os .path .exists (pendingInstallPath ):
212+ try :
213+ log .error (f"Removing failed install of { pendingInstallPath } " )
214+ shutil .rmtree (pendingInstallPath , ignore_errors = True )
215+ except OSError :
216+ log .error (f"Failed to remove { pendingInstallPath } " , exc_info = True )
217+
218+ if self [AddonStateCategory .PENDING_INSTALL ]:
219+ log .error (f"Discarding { self [AddonStateCategory .PENDING_INSTALL ]} from pending install add-ons as their install failed." )
220+ self [AddonStateCategory .PENDING_INSTALL ].clear ()
221+
206222 def _cleanupCompatibleAddonsFromDowngrade (self ) -> None :
207223 from addonStore .dataManager import addonDataManager
208224 installedAddons = addonDataManager ._installedAddonsCache .installedAddons
@@ -287,6 +303,7 @@ def initialize():
287303 getAvailableAddons (refresh = True , isFirstLoad = True )
288304 state .cleanupRemovedDisabledAddons ()
289305 state ._cleanupCompatibleAddonsFromDowngrade ()
306+ state ._cleanupInstalledAddons ()
290307 if NVDAState .shouldWriteToDisk ():
291308 state .save ()
292309 initializeModulePackagePaths ()
@@ -303,8 +320,8 @@ def terminate():
303320 pass
304321
305322
306- def _getDefaultAddonPaths () -> List [str ]:
307- """ Returns paths where addons can be found.
323+ def _getDefaultAddonPaths () -> list [str ]:
324+ r """ Returns paths where addons can be found.
308325 For now, only <userConfig>\addons is supported.
309326 """
310327 addon_paths = []
@@ -497,14 +514,26 @@ def __init__(self, path: str):
497514 _report_manifest_errors (self .manifest )
498515 raise AddonError ("Manifest file has errors." )
499516
500- def completeInstall (self ) -> str :
517+ def completeInstall (self ) -> Optional [str ]:
518+ if not os .path .exists (self .pendingInstallPath ):
519+ log .error (f"Pending install path { self .pendingInstallPath } does not exist" )
520+ return None
521+
501522 try :
502523 os .rename (self .pendingInstallPath , self .installPath )
503524 state [AddonStateCategory .PENDING_INSTALL ].discard (self .name )
504525 return self .installPath
505526 except OSError :
506527 log .error (f"Failed to complete addon installation for { self .name } " , exc_info = True )
507528
529+ # Remove pending install folder
530+ try :
531+ log .error (f"Removing failed install of { self .pendingInstallPath } " )
532+ shutil .rmtree (self .pendingInstallPath , ignore_errors = True )
533+ state [AddonStateCategory .PENDING_INSTALL ].discard (self .name )
534+ except OSError :
535+ log .error (f"Failed to remove { self .pendingInstallPath } " , exc_info = True )
536+
508537 def requestRemove (self ):
509538 """Marks this addon for removal on NVDA restart."""
510539 if self .isPendingInstall and not self .isInstalled :
0 commit comments