Skip to content

Commit 01bb1c5

Browse files
authored
Merge 122ad56 into 4881dd4
2 parents 4881dd4 + 122ad56 commit 01bb1c5

2 files changed

Lines changed: 63 additions & 49 deletions

File tree

source/addonHandler/__init__.py

Lines changed: 31 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@
55
# See the file COPYING for more details.
66

77
from abc import abstractmethod, ABC
8-
import glob
98
import sys
109
import os.path
1110
import gettext
@@ -20,6 +19,7 @@
2019
from typing import (
2120
Callable,
2221
Dict,
22+
List,
2323
Optional,
2424
Set,
2525
TYPE_CHECKING,
@@ -74,6 +74,9 @@
7474
# For more details see appropriate section of the developer guide.
7575
isCLIParamKnown = extensionPoints.AccumulatingDecider(defaultDecision=False)
7676

77+
_failedPendingRemovals: CaseInsensitiveSet[str] = CaseInsensitiveSet()
78+
_failedPendingInstalls: CaseInsensitiveSet[str] = CaseInsensitiveSet()
79+
7780

7881
AddonStateDictT = 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

321312
def 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>\addons 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__)

source/core.py

Lines changed: 32 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,8 @@ def __bool__(self):
7070

7171

7272
def doStartupDialogs():
73+
import wx
74+
7375
import config
7476
import gui
7577

@@ -88,7 +90,6 @@ def handleReplaceCLIArg(cliArgument: str) -> bool:
8890
if not isParamKnown:
8991
unknownCLIParams.append(param)
9092
if unknownCLIParams:
91-
import wx
9293
gui.messageBox(
9394
# Translators: Shown when NVDA has been started with unknown command line parameters.
9495
_("The following command line parameters are unknown to NVDA: {params}").format(
@@ -100,7 +101,6 @@ def handleReplaceCLIArg(cliArgument: str) -> bool:
100101
wx.OK | wx.ICON_ERROR
101102
)
102103
if config.conf.baseConfigError:
103-
import wx
104104
gui.messageBox(
105105
# Translators: A message informing the user that there are errors in the configuration file.
106106
_("Your configuration file contains errors. "
@@ -118,7 +118,6 @@ def handleReplaceCLIArg(cliArgument: str) -> bool:
118118
gui.mainFrame.onToggleSpeechViewerCommand(evt=None)
119119
import inputCore
120120
if inputCore.manager.userGestureMap.lastUpdateContainedError:
121-
import wx
122121
gui.messageBox(_("Your gesture map file contains errors.\n"
123122
"More details about the errors can be found in the log file."),
124123
_("gesture map File Error"), wx.OK|wx.ICON_EXCLAMATION)
@@ -130,14 +129,43 @@ def handleReplaceCLIArg(cliArgument: str) -> bool:
130129
if updateCheck and not config.conf['update']['askedAllowUsageStats']:
131130
# a callback to save config after the usage stats question dialog has been answered.
132131
def onResult(ID):
133-
import wx
134132
if ID in (wx.ID_YES,wx.ID_NO):
135133
try:
136134
config.conf.save()
137135
except:
138136
pass
139137
# Ask the user if usage stats can be collected.
140138
gui.runScriptModalDialog(gui.startupDialogs.AskAllowUsageStatsDialog(None), onResult)
139+
addonFailureMessages: list[str] = []
140+
failedUpdates = addonHandler._failedPendingInstalls.intersection(addonHandler._failedPendingRemovals)
141+
failedInstalls = addonHandler._failedPendingInstalls - failedUpdates
142+
failedRemovals = addonHandler._failedPendingRemovals - failedUpdates
143+
if failedUpdates:
144+
addonFailureMessages.append(
145+
# Translators: Shown when one or more add-ons failed to update.
146+
_("Following add-ons failed to update: {}").format(", ".join(failedUpdates))
147+
)
148+
if failedRemovals:
149+
addonFailureMessages.append(
150+
# Translators: Shown when one or more add-ons failed to be uninstalled.
151+
_("Following add-ons failed to uninstall: {}").format(", ".join(failedRemovals))
152+
)
153+
if failedInstalls:
154+
addonFailureMessages.append(
155+
# Translators: Shown when one or more add-ons failed to be installed.
156+
_("Following add-ons failed to be installed: {}").format(", ".join(failedInstalls))
157+
)
158+
159+
if addonFailureMessages:
160+
gui.messageBox(
161+
_(
162+
# Translators: Shown when one or more actions on add-ons failed.
163+
"Some operations on add-ons failed. See the log file for more details.\n{}"
164+
).format("\n".join(addonFailureMessages)),
165+
# Translators: Title of message shown when requested action on add-ons failed.
166+
_("Add-on failures"),
167+
wx.ICON_ERROR | wx.OK
168+
)
141169

142170

143171
@dataclass

0 commit comments

Comments
 (0)