File: NuGet\NuGetPackagePrefetcher.cs
Web Access
Project: src\src\Aspire.Cli\Aspire.Cli.csproj (aspire)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
using Aspire.Cli.Commands;
using Aspire.Cli.Configuration;
using Aspire.Cli.Packaging;
using Aspire.Cli.Utils;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using SystemCommand = System.CommandLine.Command;
 
namespace Aspire.Cli.NuGet;
 
internal sealed class NuGetPackagePrefetcher(ILogger<NuGetPackagePrefetcher> logger, TimeProvider timeProvider, CliExecutionContext executionContext, IFeatures features, IPackagingService packagingService, ICliUpdateNotifier cliUpdateNotifier) : BackgroundService
{
    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        // Wait for command to be selected
        var command = await WaitForCommandSelectionAsync(stoppingToken);
        if (command is null)
        {
            // Selection only fails when the CLI is shutting down, so there is nothing left to prefetch for.
            return;
        }
 
        var shouldPrefetchTemplates = ShouldPrefetchTemplatePackages(command);
        var shouldPrefetchCli = ShouldPrefetchCliPackages(
            command,
            features.IsFeatureEnabled(KnownFeatures.UpdateNotificationsEnabled, true));
 
        var prefetchTasks = new List<Task>(capacity: 2);
 
        // Prefetch template packages if needed
        if (shouldPrefetchTemplates)
        {
            prefetchTasks.Add(Task.Run(async () =>
            {
                try
                {
                    var channels = await packagingService.GetChannelsAsync(stoppingToken);
 
                    foreach (var channel in channels)
                    {
                        // Discard the results here, we just want them in the cache.
                        _ = await channel.GetTemplatePackagesAsync(executionContext.WorkingDirectory, stoppingToken);
                    }
                }
                catch (OperationCanceledException) when (stoppingToken.IsCancellationRequested)
                {
                    logger.LogTrace("Template package prefetching was cancelled because the CLI is shutting down.");
                }
                catch (Exception ex)
                {
                    logger.LogDebug(ex, "Non-fatal error while prefetching template packages. This is not critical to the operation of the CLI.");
                    // This prefetching is best effort. If it fails we log (above) and then the
                    // background service will exit gracefully. Code paths that depend on this
                    // data will handle the absence of pre-fetched packages gracefully.
                }
            }, stoppingToken));
        }
 
        // Prefetch CLI packages if needed
        if (shouldPrefetchCli)
        {
            prefetchTasks.Add(Task.Run(async () =>
            {
                try
                {
                    await cliUpdateNotifier.CheckForCliUpdatesAsync(
                        workingDirectory: executionContext.WorkingDirectory,
                        cancellationToken: stoppingToken
                        );
                }
                catch (OperationCanceledException) when (stoppingToken.IsCancellationRequested)
                {
                    logger.LogTrace("CLI package prefetching was cancelled because the CLI is shutting down.");
                }
                catch (Exception ex)
                {
                    logger.LogDebug(ex, "Non-fatal error while prefetching CLI packages. This is not critical to the operation of the CLI.");
                }
            }, stoppingToken));
        }
 
        await PreventOrphanedPrefetchingAsync(prefetchTasks, stoppingToken);
    }
 
    /// <summary>
    /// Holds the service open until prefetching finishes, so the CLI cannot exit and leave a NuGet
    /// search child process behind.
    /// </summary>
    private static async Task PreventOrphanedPrefetchingAsync(List<Task> prefetchTasks, CancellationToken stoppingToken)
    {
        try
        {
            await Task.WhenAll(prefetchTasks);
        }
        catch (OperationCanceledException) when (stoppingToken.IsCancellationRequested)
        {
        }
    }
 
    /// <summary>
    /// Waits for the command to be selected, which happens once its action runs. Returns <see langword="null"/>
    /// only when the CLI shuts down first.
    /// </summary>
    private async Task<SystemCommand?> WaitForCommandSelectionAsync(CancellationToken cancellationToken)
    {
        try
        {
            return await executionContext.CommandSelected.Task.WaitAsync(Timeout.InfiniteTimeSpan, timeProvider, cancellationToken);
        }
        catch (OperationCanceledException)
        {
            return null;
        }
    }
 
    private static bool ShouldPrefetchTemplatePackages(SystemCommand? command)
        => command is BaseCommand { PrefetchesTemplatePackageMetadataForInvocation: true };
 
    private static bool ShouldPrefetchCliPackages(SystemCommand? command, bool updateNotificationsEnabled)
        => command is BaseCommand baseCommand && baseCommand.ShouldPrefetchCliPackageMetadata(updateNotificationsEnabled);
}