diff --git a/scripts/addons_core/bl_pkg/__init__.py b/scripts/addons_core/bl_pkg/__init__.py index 17198ea6435..a62aa5243fa 100644 --- a/scripts/addons_core/bl_pkg/__init__.py +++ b/scripts/addons_core/bl_pkg/__init__.py @@ -212,11 +212,11 @@ def repos_to_notify(): prefs = bpy.context.preferences extension_repos = prefs.extensions.repos + + repos_remote = [] for repo_item in extension_repos: if not repo_item.enabled: continue - if not repo_item.use_sync_on_startup: - continue if not repo_item.use_remote_url: continue remote_url = repo_item.remote_url @@ -247,8 +247,20 @@ def repos_to_notify(): if repo_is_empty: continue - # NOTE: offline checks are handled by the notification (not here). - repos_notify.append( + repos_remote.append(repo_item) + + # Update all repos together or none, to avoid bothering users + # multiple times in a day. + do_online_sync = False + for repo_item in repos_remote: + if not repo_item.use_sync_on_startup: + continue + if repo_index_outdated(repo_item.directory): + do_online_sync = True + break + + for repo_item in repos_remote: + repos_notify.append(( bl_extension_ops.RepoItem( name=repo_item.name, directory=repo_directory, @@ -257,14 +269,10 @@ def repos_to_notify(): use_cache=repo_item.use_cache, access_token=repo_item.access_token if repo_item.use_access_token else "", ), - ) + repo_item.use_sync_on_startup and do_online_sync, + )) - # Update all repos together or none, to avoid bothering users - # multiple times in a day. - if repo_index_outdated(repo_item.directory): - do_online_sync = True - - return repos_notify, do_online_sync + return repos_notify # ----------------------------------------------------------------------------- @@ -593,11 +601,8 @@ def register(): if not bpy.app.background: if prefs.view.show_extensions_updates: - repos_notify, do_online_sync = repos_to_notify() - if repos_notify: - from . import bl_extension_notify - bl_extension_notify.update_non_blocking(repos=repos_notify, do_online_sync=do_online_sync) - del repos_notify + from . import bl_extension_notify + bl_extension_notify.update_non_blocking(repos_fn=repos_to_notify) def unregister(): diff --git a/scripts/addons_core/bl_pkg/bl_extension_notify.py b/scripts/addons_core/bl_pkg/bl_extension_notify.py index d99a972b8b5..0db4e6beb4c 100644 --- a/scripts/addons_core/bl_pkg/bl_extension_notify.py +++ b/scripts/addons_core/bl_pkg/bl_extension_notify.py @@ -28,7 +28,9 @@ USE_GRACEFUL_EXIT = False # Special value to signal no packages can be updated because all repositories are blocked by being offline. STATE_DATA_ALL_OFFLINE = object() -WM_EXTENSIONS_CHECKING = -1 +# `wmWindowManager.extensions_updates` from C++ +WM_EXTENSIONS_UPDATE_UNSET = -2 +WM_EXTENSIONS_UPDATE_CHECKING = -1 # ----------------------------------------------------------------------------- @@ -142,7 +144,7 @@ def sync_apply_locked(repos_notify, repos_notify_files, unique_ext): return any_lock_errors, any_stale_errors -def sync_status_generator(repos_notify, do_online_sync): +def sync_status_generator(repos_fn): import atexit # Generator results... @@ -153,18 +155,22 @@ def sync_status_generator(repos_notify, do_online_sync): # Setup The Update # # ################ # - repos_notify_orig = repos_notify - if not bpy.app.online_access: - repos_notify = [repo for repo in repos_notify if repo.remote_url.startswith("file://")] - if not repos_notify: - # Special case, early exit. - yield (STATE_DATA_ALL_OFFLINE, 0, ()) - return - yield None - any_offline = len(repos_notify) != len(repos_notify_orig) - del repos_notify_orig + # Calculate the repositories. + # This may be an expensive so yield once before running. + repos_and_do_online = list(repos_fn()) + assert isinstance(repos_and_do_online, list) + + if not bpy.app.online_access: + # Allow file-system only sync. + repos_and_do_online = [ + (repo, do_online_sync) for repo, do_online_sync in repos_and_do_online + if repo.remote_url.startswith("file://") + ] + + if not repos_and_do_online: + return # An extension unique to this session. unique_ext = "@{:x}".format(os.getpid()) @@ -172,7 +178,7 @@ def sync_status_generator(repos_notify, do_online_sync): from functools import partial cmd_batch_partial = [] - for repo_item in repos_notify: + for repo_item, do_online_sync in repos_and_do_online: # Local only repositories should still refresh, but not run the sync. assert repo_item.remote_url cmd_batch_partial.append(partial( @@ -231,7 +237,7 @@ def sync_status_generator(repos_notify, do_online_sync): update_total = -1 any_lock_errors = False - repos_notify_files = [[] for _ in repos_notify] + repos_notify_files = [[] for _ in repos_and_do_online] is_debug = bpy.app.debug while True: @@ -274,15 +280,13 @@ def sync_status_generator(repos_notify, do_online_sync): # ################### # # Finalize The Update # # ################### # + repos_notify = [repo for repo, _do_online_sync in repos_and_do_online] any_lock_errors, any_stale_errors = sync_apply_locked(repos_notify, repos_notify_files, unique_ext) update_total = sync_status_count_outdated_extensions(repos_notify) if any_lock_errors: extra_warnings.append(" Failed to acquire lock!") if any_stale_errors: extra_warnings.append(" Unexpected change in repository!") - - if any_offline: - extra_warnings.append(" Skipping online repositories!") yield (cmd_batch.calc_status_data(), update_total, extra_warnings) else: yield None @@ -309,31 +313,28 @@ class NotifyHandle: "splash_region", "sync_info", - "do_online_sync", - "_repos", + "_repos_fn", "is_complete", "_sync_generator", ) - def __init__(self, repos_notify, do_online_sync): + def __init__(self, repos_fn): self.splash_region = None - self._repos = repos_notify + self._repos_fn = repos_fn self._sync_generator = None self.is_complete = False # status_data, update_count, extra_warnings. self.sync_info = None - self.do_online_sync = do_online_sync def run(self): assert self._sync_generator is None - self._sync_generator = iter(sync_status_generator(self._repos, self.do_online_sync)) + self._sync_generator = iter(sync_status_generator(self._repos_fn)) def run_ensure(self): if self.is_running(): - return False + return self.run() - return True def run_step(self): assert self._sync_generator is not None @@ -349,17 +350,16 @@ class NotifyHandle: def updates_count(self): if self.sync_info is None: - return WM_EXTENSIONS_CHECKING + return WM_EXTENSIONS_UPDATE_CHECKING _status_data, update_count, _extra_warnings = self.sync_info return update_count def ui_text(self): if self.sync_info is None: - return "Checking for Extension Updates", 'NONE', WM_EXTENSIONS_CHECKING + return "Checking for Extension Updates", 'NONE', WM_EXTENSIONS_UPDATE_CHECKING status_data, update_count, extra_warnings = self.sync_info - do_online_sync = self.do_online_sync text, icon = bl_extension_utils.CommandBatch.calc_status_text_icon_from_data( - status_data, update_count, do_online_sync, + status_data, update_count, ) # Not more than 1-2 of these (failed to lock, some repositories offline .. etc). for warning in extra_warnings: @@ -408,12 +408,13 @@ def _ui_refresh_apply(*, notify): def _ui_refresh_timer(): if not _notify_queue: + if wm.extensions_updates == WM_EXTENSIONS_UPDATE_CHECKING: + wm.extensions_updates = WM_EXTENSIONS_UPDATE_UNSET return None wm = bpy.context.window_manager notify = _notify_queue[0] - if notify.run_ensure(): - wm.extensions_updates = WM_EXTENSIONS_CHECKING + notify.run_ensure() default_wait = TIME_WAIT_STEP @@ -425,11 +426,19 @@ def _ui_refresh_timer(): # Nothing changed, no action is needed (waiting for a response). return default_wait + # Some content was found, set checking. + # Avoid doing this early because the icon flickers in cases when + # it's not needed and it gets turned off quickly. + if wm.extensions_updates == WM_EXTENSIONS_UPDATE_UNSET: + wm.extensions_updates = WM_EXTENSIONS_UPDATE_CHECKING + # If the generator exited, either step to the next action or early exit here. if sync_info is ...: _ui_refresh_apply(notify=notify) if len(_notify_queue) <= 1: - # Keep the item because the text should remain displayed for the splash. + # Keep `_notify_queuy[0]` because the text should remain displayed for the splash. + if wm.extensions_updates == WM_EXTENSIONS_UPDATE_CHECKING: + wm.extensions_updates = WM_EXTENSIONS_UPDATE_UNSET return None # Move onto the next item. del _notify_queue[0] @@ -449,17 +458,16 @@ def _ui_refresh_timer(): # Public API -def update_non_blocking(*, repos, do_online_sync): - """ - Perform a non-blocking update on ``repos``. - Updates are queued in case some are already running. - """ - # TODO: it's possible this preferences requests updates just after check-for-updates on startup. - # The checks should be de-duplicated. For now just ensure the checks don't interfere with each other. - assert bool(repos), "Unexpected empty repository list passed in" - _notify_queue.append(NotifyHandle(repos, do_online_sync)) +def update_non_blocking(*, repos_fn): + # Perform a non-blocking update on ``repos``. + # Updates are queued in case some are already running. + # `repos_fn` A generator or function that returns a list of ``(RepoItem, do_online_sync)`` pairs. + # Some repositories don't check for update on startup for e.g. + + _notify_queue.append(NotifyHandle(repos_fn)) if not bpy.app.timers.is_registered(_ui_refresh_timer): bpy.app.timers.register(_ui_refresh_timer, first_interval=TIME_WAIT_INIT, persistent=True) + return True def update_in_progress(): diff --git a/scripts/addons_core/bl_pkg/bl_extension_ui.py b/scripts/addons_core/bl_pkg/bl_extension_ui.py index 0bcef6aaab5..16bfb581cc9 100644 --- a/scripts/addons_core/bl_pkg/bl_extension_ui.py +++ b/scripts/addons_core/bl_pkg/bl_extension_ui.py @@ -309,14 +309,13 @@ class notify_info: pass case None: from .bl_extension_notify import update_non_blocking - if repos_notify := [repo for repo in repos if repo.remote_url]: - update_non_blocking(repos=repos_notify, do_online_sync=True) - notify_info._update_state = False - # Starting. - in_progress = True - else: - # Nothing to do, finished. - notify_info._update_state = True + # Assume nothing to do, finished. + notify_info._update_state = True + if repos_notify := [(repo, True) for repo in repos if repo.remote_url]: + if update_non_blocking(repos_fn=lambda: repos_notify): + # Correct assumption, starting. + notify_info._update_state = False + in_progress = True case False: from .bl_extension_notify import update_in_progress if update_in_progress(): diff --git a/scripts/addons_core/bl_pkg/bl_extension_utils.py b/scripts/addons_core/bl_pkg/bl_extension_utils.py index fb044276104..13df298a379 100644 --- a/scripts/addons_core/bl_pkg/bl_extension_utils.py +++ b/scripts/addons_core/bl_pkg/bl_extension_utils.py @@ -878,7 +878,6 @@ class CommandBatch: def calc_status_text_icon_from_data( status_data: CommandBatch_StatusFlag, update_count: int, - do_online_sync: bool, ) -> Tuple[str, str]: # Generate a nice UI string for a status-bar & splash screen (must be short). # @@ -893,12 +892,11 @@ class CommandBatch: else: fail_text = ", some actions failed" - if status_data.flag == 1 << CommandBatchItem.STATUS_NOT_YET_STARTED or \ - status_data.flag & 1 << CommandBatchItem.STATUS_RUNNING: - if do_online_sync: - return "Checking for Extension Updates Online{:s}".format(fail_text), 'SORTTIME' - else: - return "Checking for Extension Updates{:s}".format(fail_text), 'SORTTIME' + if ( + status_data.flag == (1 << CommandBatchItem.STATUS_NOT_YET_STARTED) or + status_data.flag & (1 << CommandBatchItem.STATUS_RUNNING) + ): + return "Checking for Extension Updates{:s}".format(fail_text), 'SORTTIME' if status_data.flag == 1 << CommandBatchItem.STATUS_COMPLETE: if update_count > 0: