File: Utils\EnvironmentChecker\DeprecatedWorkloadCheck.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 System.Diagnostics;
using Aspire.Cli.DotNet;
using Microsoft.Extensions.Logging;
 
namespace Aspire.Cli.Utils.EnvironmentChecker;
 
/// <summary>
/// Checks if the deprecated 'aspire' workload is installed.
/// </summary>
/// <remarks>
/// The 'aspire' workload has been deprecated and should be uninstalled.
/// Users with this workload installed may encounter conflicts or confusion.
/// </remarks>
internal sealed class DeprecatedWorkloadCheck(ILogger<DeprecatedWorkloadCheck> logger, IEnvironment environment) : IEnvironmentCheck
{
    internal const string CheckName = "aspire-workload";
 
    private static readonly TimeSpan s_processTimeout = TimeSpan.FromSeconds(10);
    private readonly Func<ProcessStartInfo, CancellationToken, Task<ProcessTextOutput>> _runProcess = Process.RunAndCaptureTextAsync;
 
    internal DeprecatedWorkloadCheck(ILogger<DeprecatedWorkloadCheck> logger, IEnvironment environment, Func<ProcessStartInfo, CancellationToken, Task<ProcessTextOutput>> runProcess)
        : this(logger, environment)
    {
        _runProcess = runProcess;
    }
 
    public int Order => 32; // After SDK check (30), before dev certs (35)
 
    public async Task<IReadOnlyList<EnvironmentCheckResult>> CheckAsync(CancellationToken cancellationToken = default)
    {
        try
        {
            var processInfo = new ProcessStartInfo(DotNetSdkInstaller.ResolveDotNetPath(environment), "workload list")
            {
                RedirectStandardOutput = true,
                RedirectStandardError = true,
                CreateNoWindow = true
            };
 
            using var timeoutCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
            timeoutCts.CancelAfter(s_processTimeout);
 
            ProcessTextOutput result;
            try
            {
                // RunAndCaptureTextAsync kills the process when the timeout fires, so no cleanup is needed here.
                result = await _runProcess(processInfo, timeoutCts.Token);
            }
            catch (OperationCanceledException) when (!cancellationToken.IsCancellationRequested)
            {
                logger.LogDebug("dotnet workload list timed out");
                return [];
            }
 
            if (result.ExitStatus.ExitCode != 0)
            {
                logger.LogDebug("dotnet workload list exited with code {ExitCode}", result.ExitStatus.ExitCode);
                return [];
            }
 
            // Check if the deprecated 'aspire' workload is installed
            // The output format is typically:
            // Installed Workload Id      Manifest Version       Installation Source
            // --------------------------------------------------------------------
            // aspire                     8.0.0/8.0.100          SDK 8.0.100
            if (IsAspireWorkloadInstalled(result.StandardOutput))
            {
                return [new EnvironmentCheckResult
                {
                    Category = EnvironmentCheckCategories.Sdk,
                    Name = CheckName,
                    Status = EnvironmentCheckStatus.Fail,
                    Message = "Deprecated 'aspire' workload is installed",
                    Details = "The 'aspire' workload has been deprecated and causes conflicts with modern Aspire projects.",
                    Fix = "Run: dotnet workload uninstall aspire",
                    Link = "https://aka.ms/aspire-prerequisites"
                }];
            }
 
            // Workload not installed - this is the expected state, no need to report anything
            return [];
        }
        catch (Exception ex)
        {
            logger.LogDebug(ex, "Error checking for aspire workload");
            // Don't fail the check if we can't verify - just skip it
            return [];
        }
    }
 
    /// <summary>
    /// Checks if the 'aspire' workload is present in the output of 'dotnet workload list'.
    /// </summary>
    internal static bool IsAspireWorkloadInstalled(string output)
    {
        if (string.IsNullOrWhiteSpace(output))
        {
            return false;
        }
 
        // Parse each line looking for the 'aspire' workload
        // The format is whitespace-separated columns, with the first column being the workload ID
        var lines = output.Split('\n', StringSplitOptions.RemoveEmptyEntries);
 
        foreach (var line in lines)
        {
            var trimmedLine = line.Trim();
 
            // Skip header lines, separator lines, and informational lines
            if (trimmedLine.StartsWith("Installed", StringComparison.OrdinalIgnoreCase) ||
                trimmedLine.StartsWith("Workload version:", StringComparison.OrdinalIgnoreCase) ||
                trimmedLine.StartsWith("---", StringComparison.Ordinal) ||
                trimmedLine.StartsWith("Use", StringComparison.OrdinalIgnoreCase) ||
                string.IsNullOrWhiteSpace(trimmedLine))
            {
                continue;
            }
 
            // The workload ID is the first column
            var columns = trimmedLine.Split([' ', '\t'], StringSplitOptions.RemoveEmptyEntries);
            if (columns.Length > 0 &&
                string.Equals(columns[0], "aspire", StringComparison.OrdinalIgnoreCase))
            {
                return true;
            }
        }
 
        return false;
    }
}