Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
112 commits
Select commit Hold shift + click to select a range
be23cf0
List addons in store
feerrenrut May 24, 2022
c17174d
Update data on background thread.
feerrenrut May 24, 2022
f51cd7c
Installation of Add-ons
feerrenrut Jun 8, 2022
a8d2b58
Show more accurate status for items in Addon store.
feerrenrut Jun 10, 2022
17824e4
Serialize installation step for add-on store
feerrenrut Jul 22, 2022
140a2e6
Handle missing addonStore dir
feerrenrut Aug 4, 2022
abb6f41
fix translator comments
feerrenrut Aug 4, 2022
d1677a4
Add note about download chunk size
feerrenrut Aug 8, 2022
e70f86b
Merge remote-tracking branch 'origin/master' into addonStore-base
feerrenrut Dec 9, 2022
296961d
Review actions
feerrenrut Dec 12, 2022
a4346a9
make widths more comfortable
feerrenrut Dec 12, 2022
067a834
fix link conversion on Windows 7
feerrenrut Dec 12, 2022
68df9c5
Add requests to requirements.txt explicitly
feerrenrut Dec 12, 2022
173572d
Merge remote-tracking branch 'origin/master' into addonStore-base
seanbudd Feb 28, 2023
6ecdb0b
Add-on store: support for optional homepage and licenseURL (#14683)
seanbudd Mar 1, 2023
3ec6d46
create support for latest endpoint and dev channel (#14684)
seanbudd Mar 1, 2023
6e14a93
Addon store: Invalidate cache based on NVDA API Version (#14685)
seanbudd Mar 1, 2023
1d1693b
Merge remote-tracking branch 'origin/master' into addonStore-base
seanbudd Apr 3, 2023
9f24148
Addon store: Handle lost selection when filtering (#14777)
seanbudd Apr 3, 2023
d56f5eb
Addon store: Implement updating add-ons (#14778)
seanbudd Apr 3, 2023
666efa5
Merge remote-tracking branch 'origin/master' into addonStore-base
seanbudd Apr 4, 2023
3f4cf4d
Merge remote-tracking branch 'origin/master' into addonStore-base
seanbudd Apr 4, 2023
811f278
Add-on store: bypass the compatibility checks for an add-on (#14797)
seanbudd Apr 18, 2023
ef72ee2
Add-on store: Group by add-on status, install from external source (#…
seanbudd Apr 18, 2023
3ecd094
Merge remote-tracking branch 'origin/master' into addonStore-base
seanbudd Apr 18, 2023
7a8a653
Merge remote-tracking branch 'origin/master' into addonStore-base
seanbudd Apr 19, 2023
2feb0ff
Add-on store: Add setting to view incompatible add-ons available to d…
seanbudd Apr 20, 2023
7b970fa
Merge remote-tracking branch 'origin/master' into addonStore-base
seanbudd Apr 20, 2023
0c3a2d9
Add-on store: fix missing typing
seanbudd Apr 20, 2023
309cc8d
Add-on store: minor addonHandler fixup (#14845)
seanbudd Apr 20, 2023
5689f87
Add-on store: Notify user when download/fetch fails (#14846)
seanbudd Apr 20, 2023
587afd7
Add-on store: check SHA256 when downloading add-on (#14847)
seanbudd Apr 20, 2023
b00fec5
Add-on store: Fixup #14846
seanbudd Apr 21, 2023
31bdfae
Add-on store: add missing pgettext wrappings
seanbudd Apr 24, 2023
a50f52e
Add-on store: Disable add-ons manager (#14853)
seanbudd Apr 24, 2023
140908a
Add-on store: Display add-on incompatible reason (#14869)
seanbudd Apr 27, 2023
345e77e
Add-on store: Use close button instead of OK/Cancel (#14854)
seanbudd Apr 27, 2023
98a5988
Add-on Store: Add filter by channel (#14852)
seanbudd Apr 27, 2023
86017eb
Add-on store: Fixup translator comment
seanbudd Apr 27, 2023
539cd88
Merge remote-tracking branch 'origin/master' into addonStore-base
seanbudd Apr 27, 2023
66a6b34
Add-on store: fix up references to add-ons manager
seanbudd Apr 28, 2023
1c7c312
Add User Guide documentation
seanbudd Apr 28, 2023
6439cbb
Add-on store: filter legacy add-ons (#14882)
seanbudd May 1, 2023
5b70155
Add-on store: make state pickling safer for downgrading (#14868)
seanbudd May 1, 2023
c70809c
Merge remote-tracking branch 'origin/master' into addonStore-base
seanbudd May 1, 2023
8792796
Add-on store: Make add-on version contextual on views (#14891)
seanbudd May 1, 2023
f54fd1f
Add-on store: refactor and code cleanup
seanbudd May 2, 2023
e479e65
Add-on store: mark new code as private
seanbudd May 2, 2023
03567da
Add-on store: Minor fixups
seanbudd May 3, 2023
e7077b0
Merge branch 'addonStore-base' into addDocumentation
seanbudd May 3, 2023
ea847dd
Add-on Store: Update channel filter when changing status filter
seanbudd May 8, 2023
ac7e12f
Add-on store: Add accelerator key for add-ons list
seanbudd May 8, 2023
caf9b23
Add-on store: Fixup sorting - case/locale insensitive
seanbudd May 8, 2023
04c3363
Add-on store: Add accelerator key for other details
seanbudd May 8, 2023
66b8cda
Add-on store: Turn details links into buttons
seanbudd May 8, 2023
c3c3e3b
Add-on store: Show help for installed views only
seanbudd May 8, 2023
90a8933
Add-on store: Re-focus when opening add-on store dialog twice
seanbudd May 8, 2023
ade51ed
Add-on store: progress dialog for installing add-ons
seanbudd May 8, 2023
5eabc23
Add-on store: Hide disable button for removed addons
seanbudd May 8, 2023
bde9a69
Add-on store: Toggle accelerator key for action buttons with visibility
seanbudd May 8, 2023
ec016eb
Add-on store: Prioritise pending remove
seanbudd May 8, 2023
ec6526c
Add-on store: Use notebook tabs for status filters
seanbudd May 9, 2023
e99ca1e
Add-on store: maximize dialog
seanbudd May 9, 2023
438cfa6
Add-on store: Fix-up tab paging
seanbudd May 10, 2023
eb44aab
Add-on store: Include status tab in dialog title
seanbudd May 10, 2023
249e638
Add-on store: Show links as plain text
seanbudd May 10, 2023
d185497
Add-on store: fixup requires restart state
seanbudd May 15, 2023
54c87b6
Merge branch 'addDocumentation' into addonStore-base
seanbudd May 15, 2023
046837b
Add-on store: fix context menu
seanbudd May 16, 2023
7ee93f0
Add-on store: 'pending removal' string fix
seanbudd May 16, 2023
dbbef29
Add-on store: accelerate key for homepage
seanbudd May 16, 2023
975ebc1
Add-on store: encourage users to manually focus status control
seanbudd May 16, 2023
3811d38
Add-on store: add external channel
seanbudd May 16, 2023
c39c8ff
Add-on store: update status when removing add-ons
seanbudd May 16, 2023
19a28c7
Merge remote-tracking branch 'origin/master' into addonStore-base
seanbudd May 16, 2023
5a737a3
Add-on store: reset selection when changing views
seanbudd May 16, 2023
729c29c
Add-on store: fix up install paths
seanbudd May 17, 2023
0b0c6bd
Add-on store: create incompatible tab to replace disabled tab
seanbudd May 18, 2023
64dd10c
Add-on store: improve visual column widths
seanbudd May 18, 2023
4374cca
Add-on store: enable minimize button for resizable settings dialogs
seanbudd May 18, 2023
bbc7257
Add-on store: Improve installed add-on caching
seanbudd May 18, 2023
f22a54c
Add-on store: Filter by disable/enable
seanbudd May 18, 2023
b4223e9
Add-on store: display available add-ons if no installed add-ons
seanbudd May 18, 2023
764abb1
Add-on store: open context menu from enter on selected add-on
seanbudd May 18, 2023
ac0e5d5
Add-on store: add keys for column sorting
seanbudd May 18, 2023
6da7724
Add-on store: Add more accelerator keys
seanbudd May 23, 2023
00c8415
Add-on store: turn action buttons into context menu
seanbudd May 25, 2023
ba6396e
Add-on store: set default channel as stable for updatable add-ons
seanbudd May 25, 2023
50be870
Add-on store: Have 2 version fields: installed & available
seanbudd May 25, 2023
5b36e30
Add-on store: Move incompatible add-ons filter from settings dialog
seanbudd May 26, 2023
f75845d
Add-on store: Improve loading experience for available add-ons
seanbudd May 26, 2023
b1cccd4
Add-on store: fetch and cache using NVDA language
seanbudd May 26, 2023
ed096ef
Add-on store: Add 'All' option for enabled filter
seanbudd May 26, 2023
204ccb6
Add-on store: fix caching
seanbudd May 29, 2023
9e4152b
Add-on store: fix various install issues
seanbudd May 29, 2023
a0d2b2a
Add-on store: improve installed status checks for actions
seanbudd May 30, 2023
7009a95
Add-on store: focus list on tab page change
seanbudd May 30, 2023
9110c92
Add-on store: move search control to it's own line
seanbudd May 30, 2023
69d8f8f
Add-on store: fixup translation comments
seanbudd May 30, 2023
e6eb1f2
Add-on store: improve documentation and context help
seanbudd May 30, 2023
0b8f8e3
Add-on store: don't delete cache during upgrade
seanbudd May 30, 2023
35c8838
Add-on store: documentation fixups
seanbudd May 30, 2023
1aca3f7
Add-on store: filter remove add-ons to installed views
seanbudd May 31, 2023
81d1748
Add-on store: filter remove add-ons to installed views
seanbudd May 31, 2023
643fa00
Add-on store: address review feedback
seanbudd Jun 1, 2023
1849e1a
Add-on store: fix up docs
seanbudd Jun 1, 2023
becccf5
Merge remote-tracking branch 'origin/master' into addonStore-base
seanbudd Jun 1, 2023
62986cd
Merge remote-tracking branch 'origin/master' into addonStore-base
seanbudd Jun 1, 2023
60581ac
Add-on store: Improve disk management for launcher and secure copies …
seanbudd Jun 2, 2023
094085e
Merge remote-tracking branch 'origin/master' into addonStore-base
seanbudd Jun 2, 2023
36a95d9
Add-on store: update user guide
seanbudd Jun 5, 2023
4cb5654
fix spelling
seanbudd Jun 5, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
Serialize installation step for add-on store
Installing add-ons must happen on the main thread. They must be installed one by one.
Downloads may happen concurrently, moving off the main thread allows NVDA to stay responsive.

Description of change:
Use a ThreadPoolExecutor to enable downloading in the background.
This will allow scaling up the number of threads as required.
Provided an abstraction to make cancellation/tracking of complete simpler.
Addons are installed (if download has completed) when the user tries to exit the dialog.
  • Loading branch information
feerrenrut committed Aug 4, 2022
commit 17824e47929d228cb717d4c6159531c0cdb1c281
6 changes: 5 additions & 1 deletion source/addonAPIVersion.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,11 @@

AddonApiVersionT = Tuple[int, int, int]

CURRENT: AddonApiVersionT = (buildVersion.version_year, buildVersion.version_major, buildVersion.version_minor)
CURRENT: AddonApiVersionT = (
buildVersion.version_year,
buildVersion.version_major,
buildVersion.version_minor
)
BACK_COMPAT_TO: AddonApiVersionT = (2022, 1, 0)
"""
As BACK_COMPAT_TO is incremented, the changed / removed parts / or reasoning should be added below.
Expand Down
135 changes: 112 additions & 23 deletions source/addonStore/dataManager.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,16 +6,23 @@
import enum
import json
import os
import shutil
from concurrent.futures import (
Future,
ThreadPoolExecutor
)
from datetime import datetime, timedelta

from logHandler import log
import requests
Comment thread
feerrenrut marked this conversation as resolved.
Outdated

import addonAPIVersion
import typing
from typing import (
Optional,
List,
Dict,
Callable,
Tuple,
)
from .models import (
_createModelFromData,
Expand Down Expand Up @@ -46,17 +53,119 @@ class CachedAddonModel:
cachedAt: datetime


class AddonFileDownloader:
OnCompleteT = Callable[[AddonDetailsModel, Optional[os.PathLike]], None]

def __init__(self, cacheDir: os.PathLike):
self._cacheDir = cacheDir
self.progress: Dict[AddonDetailsModel, int] = {} # Number of chunks received
self._pending: Dict[Future, Tuple[AddonDetailsModel, AddonFileDownloader.OnCompleteT]] = {}
self.complete: Dict[AddonDetailsModel, os.PathLike] = {} # Path to downloaded file
self._executor = ThreadPoolExecutor(
max_workers=1,
thread_name_prefix="AddonDownloader",
)

def download(
self,
addonData: AddonDetailsModel,
onComplete: OnCompleteT
):
self.progress[addonData] = 0
f: Future = self._executor.submit(
self._download, addonData,
)
self._pending[f] = addonData, onComplete
f.add_done_callback(self._done)

def _done(self, downloadAddonFuture: Future):
isCancelled = downloadAddonFuture not in self._pending
addonId = "CANCELLED" if isCancelled else self._pending[downloadAddonFuture][0].addonId
log.debug(f"Done called for {addonId}")
if isCancelled:
log.debug("Download was cancelled, not calling onComplete")
return
if not downloadAddonFuture.done() or downloadAddonFuture.cancelled():
log.error("Logic error with download in BG thread.")
return
if downloadAddonFuture.exception():
log.error(f"Unhandled exception in _download", exc_info=downloadAddonFuture.exception())
return
addonData, onComplete = self._pending.pop(downloadAddonFuture)
del self.progress[addonData]
cacheFilePath: Optional[os.PathLike] = downloadAddonFuture.result()
self.complete[addonData] = cacheFilePath
onComplete(addonData, cacheFilePath)

def cancelAll(self):
log.debug("Cancelling all")
for f in self._pending.keys():
f.cancel()
self._executor.shutdown(wait=False)
self._executor = None
self.progress.clear()
self._pending.clear()

def _download(self, addonData: AddonDetailsModel) -> Optional[os.PathLike]:
log.debug(f"starting download: {addonData.addonId}")
cacheFilePath = os.path.join(
self._cacheDir,
self._getCacheFilenameForAddon(addonData)
)
inProgressFilePath = cacheFilePath + ".download"
if addonData not in self.progress:
log.debug("the download was cancelled before it started.")
return None # The download was cancelled
try:
with requests.get(addonData.addonURL, stream=True) as r:
with open(inProgressFilePath, 'wb') as fd:
for chunk in r.iter_content(chunk_size=128000):
Comment thread
feerrenrut marked this conversation as resolved.
Outdated
log.debug(f"Chunk download: {addonData.addonId}")
fd.write(chunk)
if addonData in self.progress:
self.progress[addonData] += 1
else:
log.debug(f"Cancelled download: {addonData.addonId}")
return None # The download was cancelled
except requests.exceptions.RequestException as e:
log.debugWarning(f"Unable to download addon file: {e}")
return None
except OSError as e:
log.debugWarning(f"Unable to save addon file ({inProgressFilePath}): {e}")
return None
log.debug(f"Download complete: {inProgressFilePath}")
if os.path.exists(cacheFilePath):
log.debug(f"Cache file already exists, deleting prior to rename: {cacheFilePath}")
os.remove(cacheFilePath)
os.rename(src=inProgressFilePath, dst=cacheFilePath)
log.debug(f"Cache file available: {cacheFilePath}")
return typing.cast(os.PathLike, cacheFilePath)

@staticmethod
def _getCacheFilenameForAddon(addonData: AddonDetailsModel) -> str:
return f"{addonData.addonId}-{addonData.versionName}.nvda-addon"

def __del__(self):
if self._executor is not None:
self._executor.shutdown(wait=False)
self._executor = None


class DataManager:
_cacheFilename: str = "_cachedLatestAvailableAddons.json"
_cachePeriod = timedelta(hours=6)

def __init__(self, cacheDirLocation: os.PathLike):
def __init__(self, userConfigLocation: os.PathLike):
cacheDirLocation = os.path.join(userConfigLocation, "addonStore")
self._lang = "en"
self._preferredChannel = Channel.ALL
self._cacheFile = os.path.join(cacheDirLocation, DataManager._cacheFilename)
self._addonDownloadCacheDir = cacheDirLocation
self._addonDownloadCacheDir = os.path.join(cacheDirLocation, "_dl")
self._availableAddonCache: Optional[CachedAddonModel] = self._getCachedAddonData()

def getFileDownloader(self) -> AddonFileDownloader:
return AddonFileDownloader(self._addonDownloadCacheDir)

def _getLatestAvailableAddonsData(self) -> Optional[bytes]:
url = _getAddonStoreURL(self._preferredChannel, self._lang, _getCurrentApiVersionForURL())
try:
Expand Down Expand Up @@ -111,23 +220,3 @@ def getLatestAvailableAddons(self) -> List[AddonDetailsModel]:
cachedAt=fetchTime,
)
return self._availableAddonCache.availableAddons

def _getCacheFilenameForAddon(self, addonData: AddonDetailsModel) -> str:
return f"{addonData.addonId}-{addonData.versionName}.nvda-addon"

def downloadAddonFile(self, addonData: AddonDetailsModel):
localFilePath = os.path.join(
self._addonDownloadCacheDir,
self._getCacheFilenameForAddon(addonData)
)
try:
with requests.get(addonData.addonURL, stream=True) as r:
with open(localFilePath, 'wb') as f:
shutil.copyfileobj(r.raw, f)
except requests.exceptions.RequestException as e:
log.debugWarning(f"Unable to download addon file: {e}")
return None
except OSError as e:
log.debugWarning(f"Unable to save addon file ({localFilePath}): {e}")
return None
return localFilePath
9 changes: 7 additions & 2 deletions source/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -848,6 +848,11 @@ def _terminate(module, name=None):
except:
log.exception("Error terminating %s" % name)


def isMainThread() -> bool:
return threading.get_ident() == mainThreadId


def requestPump():
"""Request a core pump.
This will perform any queued activity.
Expand All @@ -858,7 +863,7 @@ def requestPump():
if not _pump or _isPumpPending:
return
_isPumpPending = True
if threading.get_ident() == mainThreadId:
if isMainThread():
_pump.Start(PUMP_MAX_DELAY, True)
return
# This isn't the main thread. wx timers cannot be run outside the main thread.
Expand All @@ -882,7 +887,7 @@ def callLater(delay, callable, *args, **kwargs):
# If NVDA has not fully initialized yet, the wxApp may not be initialized.
# wx.CallLater and wx.CallAfter requires the wxApp to be initialized.
raise NVDANotInitializedError("Cannot schedule callable, wx.App is not initialized")
if threading.get_ident() == mainThreadId:
if isMainThread():
return wx.CallLater(delay, _callLaterExec, callable, args, kwargs)
else:
return wx.CallAfter(wx.CallLater,delay, _callLaterExec, callable, args, kwargs)
Expand Down
94 changes: 63 additions & 31 deletions source/gui/addonStoreGui/viewModels.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,17 @@
# See the file COPYING for more details.
import typing
import enum
from os import (
PathLike,
)
from enum import (
Enum,
)
from typing import (
List,
Optional,
Dict,
Tuple,
)
import threading

Expand Down Expand Up @@ -47,6 +51,8 @@ class AvailableAddonStatus(DisplayStringEnum):
# noinspection PyArgumentList
DOWNLOAD_FAILED = enum.auto()
# noinspection PyArgumentList
DOWNLOAD_SUCCESS = enum.auto()
# noinspection PyArgumentList
INSTALLING = enum.auto()
# noinspection PyArgumentList
INSTALL_FAILED = enum.auto()
Expand All @@ -71,6 +77,8 @@ def _displayStringLabels(self) -> Dict[Enum, str]:
# Translators: Status for addons shown in the add-on store dialog
AvailableAddonStatus.DOWNLOAD_FAILED: _("Download failed"),
# Translators: Status for addons shown in the add-on store dialog
AvailableAddonStatus.DOWNLOAD_SUCCESS: _("Downloaded"),
# Translators: Status for addons shown in the add-on store dialog
AvailableAddonStatus.INSTALLING: _("Installing"),
# Translators: Status for addons shown in the add-on store dialog
AvailableAddonStatus.INSTALL_FAILED: _("Install failed"),
Expand Down Expand Up @@ -179,7 +187,7 @@ def __init__(
def _itemDataUpdated(self, addonListItemVM: AddonListItemVM):
addonId: str = addonListItemVM.Id
log.debug(f"status: {addonListItemVM.status}")
log.debug(f"equal instances: {addonListItemVM == self._addons[addonId]}")
assert addonListItemVM == self._addons[addonId], "Must be the same instance."
if addonId in self._addonsFilteredOrdered:
log.debug("calling")
Comment thread
feerrenrut marked this conversation as resolved.
Outdated
index = self._addonsFilteredOrdered.index(addonId)
Expand Down Expand Up @@ -462,6 +470,9 @@ def __init__(self, dataManager: DataManager):
self._addons: List[AddonDetailsModel] = []
self._filteredStatuses = [None, AvailableAddonStatus.RUNNING]

self._downloader = dataManager.getFileDownloader()
self._pendingInstalls: List[Tuple[AddonListItemVM, PathLike]] = []

self.listVM: AddonListVM = AddonListVM(
addons=_createListItemVMs(self._addons, self._filteredStatuses)
)
Expand Down Expand Up @@ -493,47 +504,62 @@ def _makeActionsList(self):
AddonActionVM(
# Translators: Label for a button that installs the selected addon
displayName=_("Install"),
actionHandler=self.installAddon,
actionHandler=self.getAddon,
validCheck=self.isInstallActionValid,
listItemVM=selectedListItem
),
]

def _installAddonInBG(self, listItemVM: AddonListItemVM):
try:
for status in self._doInstallAddon(listItemVM):
log.debug(f"{listItemVM.Id} status: {status}")
listItemVM.status = status
except TranslatedError as e:
log.debugWarning(f"Error during installation of {listItemVM.Id}", exc_info=True)
# ensure calling on the main thread.
core.callLater(delay=0, callable=self.hasError.notify, error=e)
def getAddon(self, listItemVM: AddonListItemVM):
listItemVM.status = AvailableAddonStatus.DOWNLOADING
log.debug(f"{listItemVM.Id} status: {listItemVM.status}")
self._downloader.download(listItemVM.model, self._downloadComplete)

def installAddon(self, listItemVM: AddonListItemVM):
threading.Thread(
>
name="install addon",
args=[listItemVM],
).start()
def _downloadComplete(self, addonDetails: AddonDetailsModel, fileDownloaded: Optional[PathLike]):
listItemVM: Optional[AddonListItemVM] = self.listVM._addons[addonDetails.addonId]
if listItemVM is None:
log.error(f"No list item VM for addon with id: {addonDetails.addonId}")
return

def _doInstallAddon(self, addon: AddonListItemVM) -> typing.Generator[AvailableAddonStatus, None, None]:
yield AvailableAddonStatus.DOWNLOADING
fileDownloaded = self._dataManager.downloadAddonFile(addon.model)
if fileDownloaded is None:
yield AvailableAddonStatus.DOWNLOAD_FAILED
raise TranslatedError(
displayMessage=pgettext(
"addonStore",
"Unable to download addon."
)
listItemVM.status = AvailableAddonStatus.DOWNLOAD_FAILED
log.debugWarning(f"Error during download of {listItemVM.Id}", exc_info=True)
e = TranslatedError(
# Translators: A message shown when downloading an add-on fails
displayMessage=pgettext("addonStore", "Unable to download add-on.")
)
yield AvailableAddonStatus.INSTALLING
# ensure calling on the main thread.
core.callLater(delay=0, callable=self.hasError.notify, error=e)
return

listItemVM.status = AvailableAddonStatus.DOWNLOAD_SUCCESS
log.debug(f"Queuing add-on for install on dialog exit: {listItemVM.Id}")
# Add-ons can have "installTasks", which often call the GUI assuming they are on the main thread.
self._pendingInstalls.append((listItemVM, fileDownloaded))

def installPending(self):
if not core.isMainThread():
# Add-ons can have "installTasks", which often call the GUI assuming they are on the main thread.
log.error("installation must happen on main thread.")
for listItemVM, fileDownloaded in self._pendingInstalls:
self._doInstall(listItemVM, fileDownloaded)

def _doInstall(self, listItemVM: AddonListItemVM, fileDownloaded: PathLike):
if not core.isMainThread():
# Add-ons can have "installTasks", which often call the GUI assuming they are on the main thread.
log.error("installation must happen on main thread.")
return
listItemVM.status = AvailableAddonStatus.INSTALLING
log.debug(f"{listItemVM.Id} status: {listItemVM.status}")
try:
installAddon(fileDownloaded)
except TranslatedError as e:
yield AvailableAddonStatus.INSTALL_FAILED
raise e
yield AvailableAddonStatus.INSTALLED
listItemVM.status = AvailableAddonStatus.INSTALL_FAILED
log.debug(f"{listItemVM.Id} status: {listItemVM.status}")
self.hasError.notify(error=e)
return
listItemVM.status = AvailableAddonStatus.INSTALLED
log.debug(f"{listItemVM.Id} status: {listItemVM.status}")

def refresh(self):
threading.Thread(target=self._getAddonsInBG, name="getAddonData").start()
Expand All @@ -550,6 +576,11 @@ def _getAddonsInBG(self):
self.detailsVM.listItem = self.listVM.getSelection()
log.debug("completed refresh")

def cancelDownloads(self):
for a in self._downloader.progress.keys():
self.listVM._addons[a.addonId].status = AvailableAddonStatus.AVAILABLE
self._downloader.cancelAll()


class TranslatedError(Exception):
def __init__(self, displayMessage: str):
Expand Down Expand Up @@ -603,13 +634,14 @@ def getPreviouslyInstalledAddonById(addonId: str) -> Optional[addonHandler.Addon
return None


def installAddon(addonPath: str) -> None:
def installAddon(addonPath: PathLike) -> None:
""" Installs the addon at path.
Any error messages / warnings are presented to the user via a GUI message box.
If attempting to install an addon that is pending removal, it will no longer be pending removal.
@note See also L{gui.addonGui.installAddon}
@raise TranslatedError on failure
"""
addonPath = typing.cast(str, addonPath)
bundle = getAddonBundleToInstallIfValid(addonPath)
prevAddon = getPreviouslyInstalledAddonById(addonId=bundle.name)

Expand Down
Loading