private void QueueWork(IReadOnlyList <SourceResource> sources, PackageIdentity package, bool ignoreExceptions, bool isInstalledPackage) { IReadOnlyList <string> configuredPackageSources = null; if (_areNamespacesEnabled) { configuredPackageSources = _context.PackageNamespacesConfiguration.GetConfiguredPackageSources(package.Id); if (configuredPackageSources != null) { var packageSourcesAtPrefix = string.Join(", ", configuredPackageSources); _context.Log.LogDebug(StringFormatter.Log_PackageNamespaceMatchFound((package.Id), packageSourcesAtPrefix)); } else { _context.Log.LogDebug(StringFormatter.Log_PackageNamespaceNoMatchFound((package.Id))); } } // No-op if the id has already been searched for // Exact versions are not added to the list since we may need to search for the full // set of packages for that id later if it becomes part of the closure later. if (package.HasVersion || _idsSearched.Add(package.Id)) { foreach (SourceResource source in sources) { if (_areNamespacesEnabled && configuredPackageSources != null && !configuredPackageSources.Contains(source.Source.PackageSource.Name, StringComparer.CurrentCultureIgnoreCase)) { // This package's id prefix is not defined in current package source, let's skip. continue; } // Keep track of the order in which these were made var requestId = GetNextRequestId(); var request = new GatherRequest(source, package, ignoreExceptions, requestId, isInstalledPackage); // Order is important here _gatherRequests.Enqueue(request); } } }
/// <summary> /// Asynchronously returns a <see cref="DownloadResourceResult" /> for a given package identity /// and enumerable of source repositories. /// </summary> /// <param name="sources">An enumerable of source repositories.</param> /// <param name="packageIdentity">A package identity.</param> /// <param name="downloadContext">A package download context.</param> /// <param name="globalPackagesFolder">A global packages folder path.</param> /// <param name="logger">A logger.</param> /// <param name="token">A cancellation token.</param> /// <returns>A task that represents the asynchronous operation. /// The task result (<see cref="Task{TResult}.Result" />) returns a <see cref="DownloadResourceResult" /> /// instance.</returns> /// <exception cref="ArgumentNullException">Thrown if <paramref name="sources" /> /// is either <c>null</c> or empty.</exception> /// <exception cref="ArgumentNullException">Thrown if <paramref name="packageIdentity" /> /// is either <c>null</c> or empty.</exception> /// <exception cref="ArgumentNullException">Thrown if <paramref name="downloadContext" /> /// is either <c>null</c> or empty.</exception> /// <exception cref="ArgumentNullException">Thrown if <paramref name="logger" /> /// is either <c>null</c> or empty.</exception> /// <exception cref="OperationCanceledException">Thrown if <paramref name="token" /> /// is cancelled.</exception> public static async Task <DownloadResourceResult> GetDownloadResourceResultAsync( IEnumerable <SourceRepository> sources, PackageIdentity packageIdentity, PackageDownloadContext downloadContext, string globalPackagesFolder, ILogger logger, CancellationToken token) { if (sources == null) { throw new ArgumentNullException(nameof(sources)); } if (packageIdentity == null) { throw new ArgumentNullException(nameof(packageIdentity)); } if (downloadContext == null) { throw new ArgumentNullException(nameof(downloadContext)); } if (logger == null) { throw new ArgumentNullException(nameof(logger)); } var failedTasks = new List <Task <DownloadResourceResult> >(); var tasksLookup = new Dictionary <Task <DownloadResourceResult>, SourceRepository>(); var linkedTokenSource = CancellationTokenSource.CreateLinkedTokenSource(token); try { // Create a group of local sources that will go first, then everything else. var groups = new Queue <List <SourceRepository> >(); var localGroup = new List <SourceRepository>(); var otherGroup = new List <SourceRepository>(); foreach (var source in sources) { if (source.PackageSource.IsLocal) { localGroup.Add(source); } else { otherGroup.Add(source); } } groups.Enqueue(localGroup); groups.Enqueue(otherGroup); bool isPackageNamespaceEnabled = downloadContext.PackageNamespacesConfiguration?.AreNamespacesEnabled == true; IReadOnlyList <string> configuredPackageSources = null; if (isPackageNamespaceEnabled) { configuredPackageSources = downloadContext.PackageNamespacesConfiguration.GetConfiguredPackageSources(packageIdentity.Id); if (configuredPackageSources != null) { var packageSourcesAtPrefix = string.Join(", ", configuredPackageSources); logger.LogDebug(StringFormatter.Log_PackageNamespaceMatchFound(packageIdentity.Id, packageSourcesAtPrefix)); } else { logger.LogDebug(StringFormatter.Log_PackageNamespaceNoMatchFound(packageIdentity.Id)); } } while (groups.Count > 0) { token.ThrowIfCancellationRequested(); var sourceGroup = groups.Dequeue(); var tasks = new List <Task <DownloadResourceResult> >(); foreach (SourceRepository source in sourceGroup) { if (isPackageNamespaceEnabled) { if (configuredPackageSources != null && !configuredPackageSources.Contains(source.PackageSource.Name, StringComparer.CurrentCultureIgnoreCase)) { // This package's id prefix is not defined in current package source, let's skip. continue; } } var task = GetDownloadResourceResultAsync( source, packageIdentity, downloadContext, globalPackagesFolder, logger, linkedTokenSource.Token); tasksLookup.Add(task, source); tasks.Add(task); } while (tasks.Any()) { var completedTask = await Task.WhenAny(tasks); if (completedTask.Status == TaskStatus.RanToCompletion) { tasks.Remove(completedTask); // Cancel the other tasks, since, they may still be running linkedTokenSource.Cancel(); if (tasks.Any()) { // NOTE: Create a Task out of remainingTasks which waits for all the tasks to complete // and disposes the linked token source safely. One of the tasks could try to access // its incoming CancellationToken to register a callback. If the linkedTokenSource was // disposed before being accessed, it will throw an ObjectDisposedException. // At the same time, we do not want to wait for all the tasks to complete before // before this method returns with a DownloadResourceResult. var remainingTasks = Task.Run(async() => { try { await Task.WhenAll(tasks); } catch { // Any exception from one of the remaining tasks is not actionable. // And, this code is running on the threadpool and the task is not awaited on. // Catch all and do nothing. } finally { linkedTokenSource.Dispose(); } }); } return(completedTask.Result); } else { token.ThrowIfCancellationRequested(); // In this case, completedTask did not run to completion. // That is, it faulted or got canceled. Remove it, and try Task.WhenAny again tasks.Remove(completedTask); failedTasks.Add(completedTask); } } } // no matches were found var errors = new StringBuilder(); errors.AppendLine(string.Format(CultureInfo.CurrentCulture, Strings.UnknownPackageSpecificVersion, packageIdentity.Id, packageIdentity.Version.ToNormalizedString())); foreach (var task in failedTasks) { string message; if (task.Exception == null) { message = task.Status.ToString(); } else { message = ExceptionUtilities.DisplayMessage(task.Exception); } errors.AppendLine($" {tasksLookup[task].PackageSource.Source}: {message}"); } throw new FatalProtocolException(errors.ToString()); } catch { linkedTokenSource.Dispose(); throw; } }