| 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; } }