in src/NuGet.Core/NuGet.PackageManagement/PackageDownloader.cs [49:238]
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 isPackageSourceMappingEnabled = downloadContext.PackageSourceMapping?.IsEnabled == true;
IReadOnlyList<string> configuredPackageSources = null;
if (isPackageSourceMappingEnabled)
{
configuredPackageSources = downloadContext.PackageSourceMapping.GetConfiguredPackageSources(packageIdentity.Id);
if (configuredPackageSources.Count > 0)
{
var packageSourcesAtPrefix = string.Join(", ", configuredPackageSources);
logger.LogDebug(StringFormatter.Log_PackageSourceMappingMatchFound(packageIdentity.Id, packageSourcesAtPrefix));
}
else
{
logger.LogDebug(StringFormatter.Log_PackageSourceMappingNoMatchFound(packageIdentity.Id));
}
}
while (groups.Count > 0)
{
token.ThrowIfCancellationRequested();
var sourceGroup = groups.Dequeue();
var tasks = new List<Task<DownloadResourceResult>>();
foreach (SourceRepository source in sourceGroup)
{
if (isPackageSourceMappingEnabled)
{
if (configuredPackageSources == null ||
configuredPackageSources.Count == 0 ||
!configuredPackageSources.Contains(source.PackageSource.Name, StringComparer.OrdinalIgnoreCase))
{
// 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();
}
}, token);
}
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);
}
#if IS_DESKTOP
errors.AppendLine($" {tasksLookup[task].PackageSource.Source}: {message}");
#else
errors.AppendLine(CultureInfo.CurrentCulture, $" {tasksLookup[task].PackageSource.Source}: {message}");
#endif
}
throw new FatalProtocolException(errors.ToString());
}
catch
{
linkedTokenSource.Dispose();
throw;
}
}