File: Utils\EnvironmentChecker\DeprecatedWorkloadCheck.cs
Web Access
Project: src\src\Aspire.Cli\Aspire.Cli.Tool.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 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) : IEnvironmentCheck
{
    private static readonly TimeSpan s_processTimeout = TimeSpan.FromSeconds(10);
 
    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
            {
                FileName = "dotnet",
                Arguments = "workload list",
                RedirectStandardOutput = true,
                RedirectStandardError = true,
                UseShellExecute = false,
                CreateNoWindow = true
            };
 
            using var process = Process.Start(processInfo);
            if (process is null)
            {
                logger.LogDebug("Failed to start dotnet workload list process");
                // Don't fail the check if we can't run the command - the SDK check will catch SDK issues
                return [];
            }
 
            using var timeoutCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
            timeoutCts.CancelAfter(s_processTimeout);
 
            string output;
            try
            {
                output = await process.StandardOutput.ReadToEndAsync(timeoutCts.Token);
                await process.WaitForExitAsync(timeoutCts.Token);
            }
            catch (OperationCanceledException) when (!cancellationToken.IsCancellationRequested)
            {
                process.Kill();
                logger.LogDebug("dotnet workload list timed out");
                return [];
            }
 
            if (process.ExitCode != 0)
            {
                logger.LogDebug("dotnet workload list exited with code {ExitCode}", process.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(output))
            {
                return [new EnvironmentCheckResult
                {
                    Category = "sdk",
                    Name = "aspire-workload",
                    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("---") ||
                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;
    }
}