55# See the file COPYING for more details.
66
77from abc import abstractmethod , ABC
8- import glob
98import sys
109import os .path
1110import gettext
2019from typing import (
2120 Callable ,
2221 Dict ,
22+ List ,
2323 Optional ,
2424 Set ,
2525 TYPE_CHECKING ,
7474# For more details see appropriate section of the developer guide.
7575isCLIParamKnown = extensionPoints .AccumulatingDecider (defaultDecision = False )
7676
77+ _failedPendingRemovals : CaseInsensitiveSet [str ] = CaseInsensitiveSet ()
78+ _failedPendingInstalls : CaseInsensitiveSet [str ] = CaseInsensitiveSet ()
79+
7780
7881AddonStateDictT = Dict [AddonStateCategory , CaseInsensitiveSet [str ]]
7982
@@ -203,25 +206,6 @@ def cleanupRemovedDisabledAddons(self) -> None:
203206 log .debug (f"Discarding { disabledAddonName } from disabled add-ons as it has been uninstalled." )
204207 self [AddonStateCategory .DISABLED ].discard (disabledAddonName )
205208
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 (
220- f"Discarding { self [AddonStateCategory .PENDING_INSTALL ]} from pending install add-ons "
221- "as their install failed."
222- )
223- self [AddonStateCategory .PENDING_INSTALL ].clear ()
224-
225209 def _cleanupCompatibleAddonsFromDowngrade (self ) -> None :
226210 from addonStore .dataManager import addonDataManager
227211 installedAddons = addonDataManager ._installedAddonsCache .installedAddons
@@ -306,25 +290,32 @@ def initialize():
306290 getAvailableAddons (refresh = True , isFirstLoad = True )
307291 state .cleanupRemovedDisabledAddons ()
308292 state ._cleanupCompatibleAddonsFromDowngrade ()
309- state ._cleanupInstalledAddons ()
310- if NVDAState .shouldWriteToDisk ():
311- state .save ()
312- initializeModulePackagePaths ()
313- if state [AddonStateCategory .PENDING_OVERRIDE_COMPATIBILITY ]:
293+ if missingPendingInstalls := state [AddonStateCategory .PENDING_INSTALL ] - _failedPendingInstalls :
294+ log .error (
295+ "The following add-ons should be installed, "
296+ f"but are no longer present on disk: { ', ' .join (missingPendingInstalls )} "
297+ )
298+ state [AddonStateCategory .PENDING_INSTALL ] -= missingPendingInstalls
299+ if missingPendingOverrideCompat := (
300+ state [AddonStateCategory .PENDING_OVERRIDE_COMPATIBILITY ] - _failedPendingInstalls
301+ ):
314302 log .error (
315303 "The following add-ons which were marked as compatible are no longer installed: "
316- f"{ ', ' .join (state [ AddonStateCategory . PENDING_OVERRIDE_COMPATIBILITY ] )} "
304+ f"{ ', ' .join (missingPendingOverrideCompat )} "
317305 )
318- state [AddonStateCategory .PENDING_OVERRIDE_COMPATIBILITY ].clear ()
306+ state [AddonStateCategory .PENDING_OVERRIDE_COMPATIBILITY ] -= missingPendingOverrideCompat
307+ if NVDAState .shouldWriteToDisk ():
308+ state .save ()
309+ initializeModulePackagePaths ()
319310
320311
321312def terminate ():
322313 """ Terminates the add-ons subsystem. """
323314 pass
324315
325316
326- def _getDefaultAddonPaths () -> list [str ]:
327- r """ Returns paths where addons can be found.
317+ def _getDefaultAddonPaths () -> List [str ]:
318+ """ Returns paths where addons can be found.
328319 For now, only <userConfig>\a ddons is supported.
329320 """
330321 addon_paths = []
@@ -363,9 +354,10 @@ def _getAvailableAddonsFromPath(
363354 ):
364355 try :
365356 a .completeRemove ()
357+ continue
366358 except RuntimeError :
367359 log .exception (f"Failed to remove { name } add-on" )
368- continue
360+ _failedPendingRemovals . add ( name )
369361 if (
370362 isFirstLoad
371363 and (
@@ -376,7 +368,13 @@ def _getAvailableAddonsFromPath(
376368 newPath = a .completeInstall ()
377369 if newPath :
378370 a = Addon (newPath )
379- if isFirstLoad and name in state [AddonStateCategory .PENDING_OVERRIDE_COMPATIBILITY ]:
371+ else : # installation failed
372+ _failedPendingInstalls .add (name )
373+ if (
374+ isFirstLoad
375+ and name in state [AddonStateCategory .PENDING_OVERRIDE_COMPATIBILITY ]
376+ and name not in _failedPendingInstalls
377+ ):
380378 state [AddonStateCategory .OVERRIDE_COMPATIBILITY ].add (name )
381379 state [AddonStateCategory .PENDING_OVERRIDE_COMPATIBILITY ].remove (name )
382380 log .debug (
@@ -517,26 +515,14 @@ def __init__(self, path: str):
517515 _report_manifest_errors (self .manifest )
518516 raise AddonError ("Manifest file has errors." )
519517
520- def completeInstall (self ) -> Optional [str ]:
521- if not os .path .exists (self .pendingInstallPath ):
522- log .error (f"Pending install path { self .pendingInstallPath } does not exist" )
523- return None
524-
518+ def completeInstall (self ) -> str :
525519 try :
526520 os .rename (self .pendingInstallPath , self .installPath )
527521 state [AddonStateCategory .PENDING_INSTALL ].discard (self .name )
528522 return self .installPath
529523 except OSError :
530524 log .error (f"Failed to complete addon installation for { self .name } " , exc_info = True )
531525
532- # Remove pending install folder
533- try :
534- log .error (f"Removing failed install of { self .pendingInstallPath } " )
535- shutil .rmtree (self .pendingInstallPath , ignore_errors = True )
536- state [AddonStateCategory .PENDING_INSTALL ].discard (self .name )
537- except OSError :
538- log .error (f"Failed to remove { self .pendingInstallPath } " , exc_info = True )
539-
540526 def requestRemove (self ):
541527 """Marks this addon for removal on NVDA restart."""
542528 if self .isPendingInstall and not self .isInstalled :
@@ -597,7 +583,7 @@ def addToPackagePath(self, package):
597583 """
598584 # #3090: Ensure that we don't add disabled / blocked add-ons to package path.
599585 # By returning here the addon does not "run"/ become active / registered.
600- if self .isDisabled or self .isBlocked or self .isPendingInstall :
586+ if self .isDisabled or self .isBlocked or self .isPendingInstall or self . name in _failedPendingRemovals :
601587 return
602588
603589 extension_path = os .path .join (self .path , package .__name__ )
0 commit comments