From 989bb2371d2f0968fb26d342952818a88bea5093 Mon Sep 17 00:00:00 2001 From: Mitch Denny Date: Tue, 8 Apr 2025 02:27:47 +1000 Subject: [PATCH 1/5] Add RPC protocol compat check. (#8577) * Add RPC protocol compat check. * Fix merge conflict. * Fix spelling. * Update DotNetCliRunner.cs Co-authored-by: David Fowler * Improve error message with version info. --------- Co-authored-by: David Fowler --- .../Backchannel/AppHostBackchannel.cs | 29 ++ .../AppHostIncompatibleException.cs | 9 + src/Aspire.Cli/Commands/NewCommand.cs | 8 +- src/Aspire.Cli/Commands/PublishCommand.cs | 354 +++++++++--------- src/Aspire.Cli/Commands/RunCommand.cs | 283 +++++++------- src/Aspire.Cli/DotNetCliRunner.cs | 16 + src/Aspire.Cli/ExitCodeConstants.cs | 1 + src/Aspire.Cli/Utils/AppHostHelper.cs | 12 +- .../{PromptUtils.cs => InteractionUtils.cs} | 16 +- .../Backchannel/AppHostRpcTarget.cs | 27 ++ 10 files changed, 437 insertions(+), 318 deletions(-) create mode 100644 src/Aspire.Cli/Backchannel/AppHostIncompatibleException.cs rename src/Aspire.Cli/Utils/{PromptUtils.cs => InteractionUtils.cs} (66%) diff --git a/src/Aspire.Cli/Backchannel/AppHostBackchannel.cs b/src/Aspire.Cli/Backchannel/AppHostBackchannel.cs index 7558ea489b4..c605bcf02e0 100644 --- a/src/Aspire.Cli/Backchannel/AppHostBackchannel.cs +++ b/src/Aspire.Cli/Backchannel/AppHostBackchannel.cs @@ -106,6 +106,19 @@ public async Task ConnectAsync(Process process, string socketPath, CancellationT var stream = new NetworkStream(socket, true); var rpc = JsonRpc.Attach(stream, target); + var capabilities = await rpc.InvokeWithCancellationAsync( + "GetCapabilitiesAsync", + Array.Empty(), + cancellationToken); + + if (!capabilities.Any(s => s == "baseline.v0")) + { + throw new AppHostIncompatibleException( + $"AppHost is incompatible with the CLI. The AppHost must be updated to a version that supports the baseline.v0 capability.", + "baseline.v0" + ); + } + _rpcTaskCompletionSource.SetResult(rpc); } @@ -145,4 +158,20 @@ public async Task GetPublishersAsync(CancellationToken cancellationTok yield return state; } } + + public async Task GetCapabilitiesAsync(CancellationToken cancellationToken) + { + using var activity = _activitySource.StartActivity(); + + var rpc = await _rpcTaskCompletionSource.Task.ConfigureAwait(false); + + logger.LogDebug("Requesting capabilities"); + + var capabilities = await rpc.InvokeWithCancellationAsync( + "GetCapabilitiesAsync", + Array.Empty(), + cancellationToken).ConfigureAwait(false); + + return capabilities; + } } diff --git a/src/Aspire.Cli/Backchannel/AppHostIncompatibleException.cs b/src/Aspire.Cli/Backchannel/AppHostIncompatibleException.cs new file mode 100644 index 00000000000..29f9624ddc6 --- /dev/null +++ b/src/Aspire.Cli/Backchannel/AppHostIncompatibleException.cs @@ -0,0 +1,9 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Aspire.Cli.Backchannel; + +internal sealed class AppHostIncompatibleException(string message, string requiredCapability) : Exception(message) +{ + public string RequiredCapability { get; } = requiredCapability; +} \ No newline at end of file diff --git a/src/Aspire.Cli/Commands/NewCommand.cs b/src/Aspire.Cli/Commands/NewCommand.cs index 2e095073117..2b81943a465 100644 --- a/src/Aspire.Cli/Commands/NewCommand.cs +++ b/src/Aspire.Cli/Commands/NewCommand.cs @@ -70,7 +70,7 @@ public NewCommand(DotNetCliRunner runner, INuGetPackageCache nuGetPackageCache) } else { - return await PromptUtils.PromptForSelectionAsync( + return await InteractionUtils.PromptForSelectionAsync( "Select a project template:", validTemplates, t => $"{t.TemplateName} ({t.TemplateDescription})", @@ -84,7 +84,7 @@ private static async Task GetProjectNameAsync(ParseResult parseResult, C if (parseResult.GetValue("--name") is not { } name) { var defaultName = new DirectoryInfo(Environment.CurrentDirectory).Name; - name = await PromptUtils.PromptForStringAsync("Enter the project name:", + name = await InteractionUtils.PromptForStringAsync("Enter the project name:", defaultValue: defaultName, cancellationToken: cancellationToken); } @@ -96,7 +96,7 @@ private static async Task GetOutputPathAsync(ParseResult parseResult, st { if (parseResult.GetValue("--output") is not { } outputPath) { - outputPath = await PromptUtils.PromptForStringAsync( + outputPath = await InteractionUtils.PromptForStringAsync( "Enter the output path:", defaultValue: pathAppendage ?? ".", cancellationToken: cancellationToken @@ -114,7 +114,7 @@ private static async Task GetProjectTemplatesVersionAsync(ParseResult pa } else { - version = await PromptUtils.PromptForStringAsync( + version = await InteractionUtils.PromptForStringAsync( "Project templates version:", defaultValue: VersionHelper.GetDefaultTemplateVersion(), validator: (string value) => { diff --git a/src/Aspire.Cli/Commands/PublishCommand.cs b/src/Aspire.Cli/Commands/PublishCommand.cs index cce35f56d32..b09ab140acf 100644 --- a/src/Aspire.Cli/Commands/PublishCommand.cs +++ b/src/Aspire.Cli/Commands/PublishCommand.cs @@ -38,213 +38,225 @@ public PublishCommand(DotNetCliRunner runner) protected override async Task ExecuteAsync(ParseResult parseResult, CancellationToken cancellationToken) { - using var activity = _activitySource.StartActivity(); + (bool IsCompatableAppHost, bool SupportsBackchannel, string? AspireHostingSdkVersion)? appHostCompatabilityCheck = null; - var passedAppHostProjectFile = parseResult.GetValue("--project"); - var effectiveAppHostProjectFile = ProjectFileHelper.UseOrFindAppHostProjectFile(passedAppHostProjectFile); - - if (effectiveAppHostProjectFile is null) + try { - return ExitCodeConstants.FailedToFindProject; - } - - var env = new Dictionary(); - - if (parseResult.GetValue("--wait-for-debugger") ?? false) - { - env[KnownConfigNames.WaitForDebugger] = "true"; - } - - var appHostCompatabilityCheck = await AppHostHelper.CheckAppHostCompatabilityAsync(_runner, effectiveAppHostProjectFile, cancellationToken); - - if (!appHostCompatabilityCheck.IsCompatableAppHost) - { - return ExitCodeConstants.FailedToDotnetRunAppHost; - } - - var buildExitCode = await AppHostHelper.BuildAppHostAsync(_runner, effectiveAppHostProjectFile, cancellationToken); - - if (buildExitCode != 0) - { - AnsiConsole.MarkupLine($"[red bold]:thumbs_down: The project could not be built. For more information run with --debug switch.[/]"); - return ExitCodeConstants.FailedToBuildArtifacts; - } - - var publisher = parseResult.GetValue("--publisher"); - var outputPath = parseResult.GetValue("--output-path"); - var fullyQualifiedOutputPath = Path.GetFullPath(outputPath ?? "."); + using var activity = _activitySource.StartActivity(); - var publishersResult = await AnsiConsole.Status() - .Spinner(Spinner.Known.Dots3) - .SpinnerStyle(Style.Parse("purple")) - .StartAsync<(int ExitCode, string[]? Publishers)>( - publisher is { } ? ":package: Getting publisher..." : ":package: Getting publishers...", - async context => { - - using var getPublishersActivity = _activitySource.StartActivity( - $"{nameof(ExecuteAsync)}-Action-GetPublishers", - ActivityKind.Client); + var passedAppHostProjectFile = parseResult.GetValue("--project"); + var effectiveAppHostProjectFile = ProjectFileHelper.UseOrFindAppHostProjectFile(passedAppHostProjectFile); + + if (effectiveAppHostProjectFile is null) + { + return ExitCodeConstants.FailedToFindProject; + } - var backchannelCompletionSource = new TaskCompletionSource(); - var pendingInspectRun = _runner.RunAsync( - effectiveAppHostProjectFile, - false, - true, - ["--operation", "inspect"], - null, - backchannelCompletionSource, - cancellationToken).ConfigureAwait(false); + var env = new Dictionary(); - var backchannel = await backchannelCompletionSource.Task.ConfigureAwait(false); - var publishers = await backchannel.GetPublishersAsync(cancellationToken).ConfigureAwait(false); - - await backchannel.RequestStopAsync(cancellationToken).ConfigureAwait(false); - var exitCode = await pendingInspectRun; + if (parseResult.GetValue("--wait-for-debugger") ?? false) + { + env[KnownConfigNames.WaitForDebugger] = "true"; + } - return (exitCode, publishers); + appHostCompatabilityCheck = await AppHostHelper.CheckAppHostCompatabilityAsync(_runner, effectiveAppHostProjectFile, cancellationToken); - }).ConfigureAwait(false); + if (!appHostCompatabilityCheck?.IsCompatableAppHost ?? throw new InvalidOperationException("IsCompatableAppHost is null")) + { + return ExitCodeConstants.FailedToDotnetRunAppHost; + } - if (publishersResult.ExitCode != 0) - { - AnsiConsole.MarkupLine($"[red bold]:thumbs_down: The publisher inspection failed with exit code {publishersResult.ExitCode}. For more information run with --debug switch.[/]"); - return ExitCodeConstants.FailedToBuildArtifacts; - } + var buildExitCode = await AppHostHelper.BuildAppHostAsync(_runner, effectiveAppHostProjectFile, cancellationToken); - var publishers = publishersResult.Publishers; - if (publishers is null || publishers.Length == 0) - { - AnsiConsole.MarkupLine("[red bold]:thumbs_down: No publishers were found.[/]"); - return ExitCodeConstants.FailedToBuildArtifacts; - } + if (buildExitCode != 0) + { + AnsiConsole.MarkupLine($"[red bold]:thumbs_down: The project could not be built. For more information run with --debug switch.[/]"); + return ExitCodeConstants.FailedToBuildArtifacts; + } - if (publishers?.Contains(publisher) != true) - { - if (publisher is not null) + var publisher = parseResult.GetValue("--publisher"); + var outputPath = parseResult.GetValue("--output-path"); + var fullyQualifiedOutputPath = Path.GetFullPath(outputPath ?? "."); + + var publishersResult = await AnsiConsole.Status() + .Spinner(Spinner.Known.Dots3) + .SpinnerStyle(Style.Parse("purple")) + .StartAsync<(int ExitCode, string[]? Publishers)>( + publisher is { } ? ":package: Getting publisher..." : ":package: Getting publishers...", + async context => { + + using var getPublishersActivity = _activitySource.StartActivity( + $"{nameof(ExecuteAsync)}-Action-GetPublishers", + ActivityKind.Client); + + var backchannelCompletionSource = new TaskCompletionSource(); + var pendingInspectRun = _runner.RunAsync( + effectiveAppHostProjectFile, + false, + true, + ["--operation", "inspect"], + null, + backchannelCompletionSource, + cancellationToken).ConfigureAwait(false); + + var backchannel = await backchannelCompletionSource.Task.ConfigureAwait(false); + var publishers = await backchannel.GetPublishersAsync(cancellationToken).ConfigureAwait(false); + + await backchannel.RequestStopAsync(cancellationToken).ConfigureAwait(false); + var exitCode = await pendingInspectRun; + + return (exitCode, publishers); + + }).ConfigureAwait(false); + + if (publishersResult.ExitCode != 0) { - AnsiConsole.MarkupLine($"[red bold]:warning: The specified publisher '{publisher}' was not found.[/]"); + AnsiConsole.MarkupLine($"[red bold]:thumbs_down: The publisher inspection failed with exit code {publishersResult.ExitCode}. For more information run with --debug switch.[/]"); + return ExitCodeConstants.FailedToBuildArtifacts; } - var publisherPrompt = new SelectionPrompt() - .Title("Select a publisher:") - .UseConverter(p => p) - .PageSize(10) - .EnableSearch() - .HighlightStyle(Style.Parse("darkmagenta")) - .AddChoices(publishers!); + var publishers = publishersResult.Publishers; + if (publishers is null || publishers.Length == 0) + { + AnsiConsole.MarkupLine("[red bold]:thumbs_down: No publishers were found.[/]"); + return ExitCodeConstants.FailedToBuildArtifacts; + } - publisher = await AnsiConsole.PromptAsync(publisherPrompt, cancellationToken); - } + if (publishers?.Contains(publisher) != true) + { + if (publisher is not null) + { + AnsiConsole.MarkupLine($"[red bold]:warning: The specified publisher '{publisher}' was not found.[/]"); + } - AnsiConsole.MarkupLine($":hammer_and_wrench: Generating artifacts for '{publisher}' publisher..."); + var publisherPrompt = new SelectionPrompt() + .Title("Select a publisher:") + .UseConverter(p => p) + .PageSize(10) + .EnableSearch() + .HighlightStyle(Style.Parse("darkmagenta")) + .AddChoices(publishers!); - var exitCode = await AnsiConsole.Progress() - .AutoRefresh(true) - .Columns( - new TaskDescriptionColumn() { Alignment = Justify.Left }, - new ProgressBarColumn() { Width = 10 }, - new ElapsedTimeColumn()) - .StartAsync(async context => { + publisher = await AnsiConsole.PromptAsync(publisherPrompt, cancellationToken); + } - using var generateArtifactsActivity = _activitySource.StartActivity( - $"{nameof(ExecuteAsync)}-Action-GenerateArtifacts", - ActivityKind.Internal); - - var backchannelCompletionSource = new TaskCompletionSource(); + AnsiConsole.MarkupLine($":hammer_and_wrench: Generating artifacts for '{publisher}' publisher..."); - var launchingAppHostTask = context.AddTask(":play_button: Launching apphost"); - launchingAppHostTask.IsIndeterminate(); - launchingAppHostTask.StartTask(); + var exitCode = await AnsiConsole.Progress() + .AutoRefresh(true) + .Columns( + new TaskDescriptionColumn() { Alignment = Justify.Left }, + new ProgressBarColumn() { Width = 10 }, + new ElapsedTimeColumn()) + .StartAsync(async context => { - var pendingRun = _runner.RunAsync( - effectiveAppHostProjectFile, - false, - true, - ["--publisher", publisher ?? "manifest", "--output-path", fullyQualifiedOutputPath], - env, - backchannelCompletionSource, - cancellationToken); + using var generateArtifactsActivity = _activitySource.StartActivity( + $"{nameof(ExecuteAsync)}-Action-GenerateArtifacts", + ActivityKind.Internal); + + var backchannelCompletionSource = new TaskCompletionSource(); - var backchannel = await backchannelCompletionSource.Task.ConfigureAwait(false); + var launchingAppHostTask = context.AddTask(":play_button: Launching apphost"); + launchingAppHostTask.IsIndeterminate(); + launchingAppHostTask.StartTask(); - launchingAppHostTask.Description = $":check_mark: Launching apphost"; - launchingAppHostTask.Value = 100; - launchingAppHostTask.StopTask(); + var pendingRun = _runner.RunAsync( + effectiveAppHostProjectFile, + false, + true, + ["--publisher", publisher ?? "manifest", "--output-path", fullyQualifiedOutputPath], + env, + backchannelCompletionSource, + cancellationToken); - var publishingActivities = backchannel.GetPublishingActivitiesAsync(cancellationToken); + var backchannel = await backchannelCompletionSource.Task.ConfigureAwait(false); - var progressTasks = new Dictionary(); + launchingAppHostTask.Description = $":check_mark: Launching apphost"; + launchingAppHostTask.Value = 100; + launchingAppHostTask.StopTask(); - await foreach (var publishingActivity in publishingActivities) - { - if (!progressTasks.TryGetValue(publishingActivity.Id, out var progressTask)) - { - progressTask = context.AddTask(publishingActivity.Id); - progressTask.StartTask(); - progressTask.IsIndeterminate(); - progressTasks.Add(publishingActivity.Id, progressTask); - } + var publishingActivities = backchannel.GetPublishingActivitiesAsync(cancellationToken); - progressTask.Description = $":play_button: {publishingActivity.StatusText}"; + var progressTasks = new Dictionary(); - if (publishingActivity.IsComplete && !publishingActivity.IsError) + await foreach (var publishingActivity in publishingActivities) { - progressTask.Description = $":check_mark: {publishingActivity.StatusText}"; - progressTask.Value = 100; - progressTask.StopTask(); + if (!progressTasks.TryGetValue(publishingActivity.Id, out var progressTask)) + { + progressTask = context.AddTask(publishingActivity.Id); + progressTask.StartTask(); + progressTask.IsIndeterminate(); + progressTasks.Add(publishingActivity.Id, progressTask); + } + + progressTask.Description = $":play_button: {publishingActivity.StatusText}"; + + if (publishingActivity.IsComplete && !publishingActivity.IsError) + { + progressTask.Description = $":check_mark: {publishingActivity.StatusText}"; + progressTask.Value = 100; + progressTask.StopTask(); + } + else if (publishingActivity.IsError) + { + progressTask.Description = $"[red bold]:cross_mark: {publishingActivity.StatusText}[/]"; + progressTask.Value = 0; + break; + } + else + { + // Keep going man! + } } - else if (publishingActivity.IsError) + + // When we are running in publish mode we don't want the app host to + // stop itself while we might still be streaming data back across + // the RPC backchannel. So we need to take responsibility for stopping + // the app host. If the CLI exits/crashes without explicitly stopping + // the app host the orphan detector in the app host will kick in. + if (progressTasks.Any(kvp => !kvp.Value.IsFinished)) { - progressTask.Description = $"[red bold]:cross_mark: {publishingActivity.StatusText}[/]"; - progressTask.Value = 0; - break; + // Depending on the failure the publisher may return a zero + // exit code. + await backchannel.RequestStopAsync(cancellationToken).ConfigureAwait(false); + var exitCode = await pendingRun; + + // If we are in the state where we've detected an error because there + // is an incomplete task then we stop the app host, but depending on + // where/how the failure occured, we might still get a zero exit + // code. If we get a non-zero exit code we want to return that + // as it might be useful for diagnostic purposes, however if we don't + // get a non-zero exit code we want to return our built-in exit code + // for failed artifact build. + return exitCode == 0 ? ExitCodeConstants.FailedToBuildArtifacts : exitCode; } else { - // Keep going man! + // If we are here then all the tasks are finished and we can + // stop the app host. + await backchannel.RequestStopAsync(cancellationToken).ConfigureAwait(false); + var exitCode = await pendingRun; + return exitCode; // should be zero for orderly shutdown but we pass it along anyway. } - } - - // When we are running in publish mode we don't want the app host to - // stop itself while we might still be streaming data back across - // the RPC backchannel. So we need to take responsibility for stopping - // the app host. If the CLI exits/crashes without explicitly stopping - // the app host the orphan detector in the app host will kick in. - if (progressTasks.Any(kvp => !kvp.Value.IsFinished)) - { - // Depending on the failure the publisher may return a zero - // exit code. - await backchannel.RequestStopAsync(cancellationToken).ConfigureAwait(false); - var exitCode = await pendingRun; - - // If we are in the state where we've detected an error because there - // is an incomplete task then we stop the app host, but depending on - // where/how the failure occured, we might still get a zero exit - // code. If we get a non-zero exit code we want to return that - // as it might be useful for diagnostic purposes, however if we don't - // get a non-zero exit code we want to return our built-in exit code - // for failed artifact build. - return exitCode == 0 ? ExitCodeConstants.FailedToBuildArtifacts : exitCode; - } - else - { - // If we are here then all the tasks are finished and we can - // stop the app host. - await backchannel.RequestStopAsync(cancellationToken).ConfigureAwait(false); - var exitCode = await pendingRun; - return exitCode; // should be zero for orderly shutdown but we pass it along anyway. - } - }); + }); - if (exitCode != 0) - { - AnsiConsole.MarkupLine($"[red bold]:thumbs_down: Publishing artifacts failed with exit code {exitCode}. For more information run with --debug switch.[/]"); - return ExitCodeConstants.FailedToBuildArtifacts; + if (exitCode != 0) + { + AnsiConsole.MarkupLine($"[red bold]:thumbs_down: Publishing artifacts failed with exit code {exitCode}. For more information run with --debug switch.[/]"); + return ExitCodeConstants.FailedToBuildArtifacts; + } + else + { + AnsiConsole.MarkupLine($"[green bold]:thumbs_up: Successfully published artifacts to: {fullyQualifiedOutputPath}[/]"); + return ExitCodeConstants.Success; + } } - else + catch (AppHostIncompatibleException ex) { - AnsiConsole.MarkupLine($"[green bold]:thumbs_up: Successfully published artifacts to: {fullyQualifiedOutputPath}[/]"); - return ExitCodeConstants.Success; + return InteractionUtils.DisplayIncompatibleVersionError( + ex, + appHostCompatabilityCheck?.AspireHostingSdkVersion ?? throw new InvalidOperationException("AspireHostingSdkVersion is null") + ); } } } diff --git a/src/Aspire.Cli/Commands/RunCommand.cs b/src/Aspire.Cli/Commands/RunCommand.cs index b7d9b1e70c1..4cf26e72d2c 100644 --- a/src/Aspire.Cli/Commands/RunCommand.cs +++ b/src/Aspire.Cli/Commands/RunCommand.cs @@ -36,175 +36,186 @@ public RunCommand(DotNetCliRunner runner) protected override async Task ExecuteAsync(ParseResult parseResult, CancellationToken cancellationToken) { - using var activity = _activitySource.StartActivity(); - - var passedAppHostProjectFile = parseResult.GetValue("--project"); - var effectiveAppHostProjectFile = ProjectFileHelper.UseOrFindAppHostProjectFile(passedAppHostProjectFile); - - if (effectiveAppHostProjectFile is null) + (bool IsCompatableAppHost, bool SupportsBackchannel, string? AspireHostingSdkVersion)? appHostCompatabilityCheck = null; + try { - return ExitCodeConstants.FailedToFindProject; - } - - var env = new Dictionary(); + using var activity = _activitySource.StartActivity(); - var debug = parseResult.GetValue("--debug"); - - var waitForDebugger = parseResult.GetValue("--wait-for-debugger"); + var passedAppHostProjectFile = parseResult.GetValue("--project"); + var effectiveAppHostProjectFile = ProjectFileHelper.UseOrFindAppHostProjectFile(passedAppHostProjectFile); + + if (effectiveAppHostProjectFile is null) + { + return ExitCodeConstants.FailedToFindProject; + } - var forceUseRichConsole = Environment.GetEnvironmentVariable(KnownConfigNames.ForceRichConsole) == "true"; - - var useRichConsole = forceUseRichConsole || !debug && !waitForDebugger; + var env = new Dictionary(); - if (waitForDebugger) - { - env[KnownConfigNames.WaitForDebugger] = "true"; - } + var debug = parseResult.GetValue("--debug"); - try - { - await CertificatesHelper.EnsureCertificatesTrustedAsync(_runner, cancellationToken); - } - catch (Exception ex) - { - AnsiConsole.MarkupLine($"[red bold]:thumbs_down: An error occurred while trusting the certificates: {ex.Message}[/]"); - return ExitCodeConstants.FailedToTrustCertificates; - } + var waitForDebugger = parseResult.GetValue("--wait-for-debugger"); - var appHostCompatabilityCheck = await AppHostHelper.CheckAppHostCompatabilityAsync(_runner, effectiveAppHostProjectFile, cancellationToken); + var forceUseRichConsole = Environment.GetEnvironmentVariable(KnownConfigNames.ForceRichConsole) == "true"; + + var useRichConsole = forceUseRichConsole || !debug && !waitForDebugger; - if (!appHostCompatabilityCheck.IsCompatableAppHost) - { - return ExitCodeConstants.FailedToDotnetRunAppHost; - } + if (waitForDebugger) + { + env[KnownConfigNames.WaitForDebugger] = "true"; + } - var watch = parseResult.GetValue("--watch"); + try + { + await CertificatesHelper.EnsureCertificatesTrustedAsync(_runner, cancellationToken); + } + catch (Exception ex) + { + AnsiConsole.MarkupLine($"[red bold]:thumbs_down: An error occurred while trusting the certificates: {ex.Message}[/]"); + return ExitCodeConstants.FailedToTrustCertificates; + } - var buildExitCode = await AppHostHelper.BuildAppHostAsync(_runner, effectiveAppHostProjectFile, cancellationToken); + appHostCompatabilityCheck = await AppHostHelper.CheckAppHostCompatabilityAsync(_runner, effectiveAppHostProjectFile, cancellationToken); - if (buildExitCode != 0) - { - AnsiConsole.MarkupLine($"[red bold]:thumbs_down: The project could not be built. For more information run with --debug switch.[/]"); - return ExitCodeConstants.FailedToBuildArtifacts; - } + if (!appHostCompatabilityCheck?.IsCompatableAppHost ?? throw new InvalidOperationException("IsCompatableAppHost is null")) + { + return ExitCodeConstants.FailedToDotnetRunAppHost; + } - var backchannelCompletitionSource = new TaskCompletionSource(); + var watch = parseResult.GetValue("--watch"); - var pendingRun = _runner.RunAsync( - effectiveAppHostProjectFile, - watch, - true, - Array.Empty(), - env, - backchannelCompletitionSource, - cancellationToken); + var buildExitCode = await AppHostHelper.BuildAppHostAsync(_runner, effectiveAppHostProjectFile, cancellationToken); - if (useRichConsole) - { - // We wait for the back channel to be created to signal that - // the AppHost is ready to accept requests. - var backchannel = await AnsiConsole.Status() - .Spinner(Spinner.Known.Dots3) - .SpinnerStyle(Style.Parse("purple")) - .StartAsync(":linked_paperclips: Starting Aspire app host...", async context => { - return await backchannelCompletitionSource.Task; - }); - - // We wait for the first update of the console model via RPC from the AppHost. - var dashboardUrls = await AnsiConsole.Status() - .Spinner(Spinner.Known.Dots3) - .SpinnerStyle(Style.Parse("purple")) - .StartAsync(":chart_increasing: Starting Aspire dashboard...", async context => { - return await backchannel.GetDashboardUrlsAsync(cancellationToken); - }); - - AnsiConsole.WriteLine(); - AnsiConsole.MarkupLine($"[green bold]Dashboard[/]:"); - if (dashboardUrls.CodespacesUrlWithLoginToken is not null) + if (buildExitCode != 0) { - AnsiConsole.MarkupLine($":chart_increasing: Direct: [link={dashboardUrls.BaseUrlWithLoginToken}]{dashboardUrls.BaseUrlWithLoginToken}[/]"); - AnsiConsole.MarkupLine($":chart_increasing: Codespaces: [link={dashboardUrls.CodespacesUrlWithLoginToken}]{dashboardUrls.CodespacesUrlWithLoginToken}[/]"); + AnsiConsole.MarkupLine($"[red bold]:thumbs_down: The project could not be built. For more information run with --debug switch.[/]"); + return ExitCodeConstants.FailedToBuildArtifacts; } - else + + var backchannelCompletitionSource = new TaskCompletionSource(); + + var pendingRun = _runner.RunAsync( + effectiveAppHostProjectFile, + watch, + true, + Array.Empty(), + env, + backchannelCompletitionSource, + cancellationToken); + + if (useRichConsole) { - AnsiConsole.MarkupLine($":chart_increasing: [link={dashboardUrls.BaseUrlWithLoginToken}]{dashboardUrls.BaseUrlWithLoginToken}[/]"); - } - AnsiConsole.WriteLine(); + // We wait for the back channel to be created to signal that + // the AppHost is ready to accept requests. + var backchannel = await AnsiConsole.Status() + .Spinner(Spinner.Known.Dots3) + .SpinnerStyle(Style.Parse("purple")) + .StartAsync(":linked_paperclips: Starting Aspire app host...", async context => { + return await backchannelCompletitionSource.Task; + }); + + // We wait for the first update of the console model via RPC from the AppHost. + var dashboardUrls = await AnsiConsole.Status() + .Spinner(Spinner.Known.Dots3) + .SpinnerStyle(Style.Parse("purple")) + .StartAsync(":chart_increasing: Starting Aspire dashboard...", async context => { + return await backchannel.GetDashboardUrlsAsync(cancellationToken); + }); + + AnsiConsole.WriteLine(); + AnsiConsole.MarkupLine($"[green bold]Dashboard[/]:"); + if (dashboardUrls.CodespacesUrlWithLoginToken is not null) + { + AnsiConsole.MarkupLine($":chart_increasing: Direct: [link={dashboardUrls.BaseUrlWithLoginToken}]{dashboardUrls.BaseUrlWithLoginToken}[/]"); + AnsiConsole.MarkupLine($":chart_increasing: Codespaces: [link={dashboardUrls.CodespacesUrlWithLoginToken}]{dashboardUrls.CodespacesUrlWithLoginToken}[/]"); + } + else + { + AnsiConsole.MarkupLine($":chart_increasing: [link={dashboardUrls.BaseUrlWithLoginToken}]{dashboardUrls.BaseUrlWithLoginToken}[/]"); + } + AnsiConsole.WriteLine(); - var table = new Table().Border(TableBorder.Rounded); + var table = new Table().Border(TableBorder.Rounded); - await AnsiConsole.Live(table).StartAsync(async context => { + await AnsiConsole.Live(table).StartAsync(async context => { - var knownResources = new SortedDictionary(); + var knownResources = new SortedDictionary(); - table.AddColumn("Resource"); - table.AddColumn("Type"); - table.AddColumn("State"); - table.AddColumn("Endpoint(s)"); + table.AddColumn("Resource"); + table.AddColumn("Type"); + table.AddColumn("State"); + table.AddColumn("Endpoint(s)"); - var resourceStates = backchannel.GetResourceStatesAsync(cancellationToken); + var resourceStates = backchannel.GetResourceStatesAsync(cancellationToken); - try - { - await foreach(var resourceState in resourceStates) + try { - knownResources[resourceState.Resource] = resourceState; + await foreach(var resourceState in resourceStates) + { + knownResources[resourceState.Resource] = resourceState; - table.Rows.Clear(); + table.Rows.Clear(); - foreach (var knownResource in knownResources) - { - var nameRenderable = new Text(knownResource.Key, new Style().Foreground(Color.White)); - - var typeRenderable = new Text(knownResource.Value.Type, new Style().Foreground(Color.White)); - - var stateRenderable = knownResource.Value.State switch { - "Running" => new Text(knownResource.Value.State, new Style().Foreground(Color.Green)), - "Starting" => new Text(knownResource.Value.State, new Style().Foreground(Color.LightGreen)), - "FailedToStart" => new Text(knownResource.Value.State, new Style().Foreground(Color.Red)), - "Waiting" => new Text(knownResource.Value.State, new Style().Foreground(Color.White)), - "Unhealthy" => new Text(knownResource.Value.State, new Style().Foreground(Color.Yellow)), - "Exited" => new Text(knownResource.Value.State, new Style().Foreground(Color.Grey)), - "Finished" => new Text(knownResource.Value.State, new Style().Foreground(Color.Grey)), - "NotStarted" => new Text(knownResource.Value.State, new Style().Foreground(Color.Grey)), - _ => new Text(knownResource.Value.State ?? "Unknown", new Style().Foreground(Color.Grey)) - }; - - IRenderable endpointsRenderable = new Text("None"); - if (knownResource.Value.Endpoints?.Length > 0) + foreach (var knownResource in knownResources) { - endpointsRenderable = new Rows( - knownResource.Value.Endpoints.Select(e => new Text(e, new Style().Link(e))) - ); + var nameRenderable = new Text(knownResource.Key, new Style().Foreground(Color.White)); + + var typeRenderable = new Text(knownResource.Value.Type, new Style().Foreground(Color.White)); + + var stateRenderable = knownResource.Value.State switch { + "Running" => new Text(knownResource.Value.State, new Style().Foreground(Color.Green)), + "Starting" => new Text(knownResource.Value.State, new Style().Foreground(Color.LightGreen)), + "FailedToStart" => new Text(knownResource.Value.State, new Style().Foreground(Color.Red)), + "Waiting" => new Text(knownResource.Value.State, new Style().Foreground(Color.White)), + "Unhealthy" => new Text(knownResource.Value.State, new Style().Foreground(Color.Yellow)), + "Exited" => new Text(knownResource.Value.State, new Style().Foreground(Color.Grey)), + "Finished" => new Text(knownResource.Value.State, new Style().Foreground(Color.Grey)), + "NotStarted" => new Text(knownResource.Value.State, new Style().Foreground(Color.Grey)), + _ => new Text(knownResource.Value.State ?? "Unknown", new Style().Foreground(Color.Grey)) + }; + + IRenderable endpointsRenderable = new Text("None"); + if (knownResource.Value.Endpoints?.Length > 0) + { + endpointsRenderable = new Rows( + knownResource.Value.Endpoints.Select(e => new Text(e, new Style().Link(e))) + ); + } + + table.AddRow(nameRenderable, typeRenderable, stateRenderable, endpointsRenderable); } - table.AddRow(nameRenderable, typeRenderable, stateRenderable, endpointsRenderable); + context.Refresh(); } - - context.Refresh(); } - } - catch (ConnectionLostException ex) when (ex.InnerException is OperationCanceledException) - { - // This exception will be thrown if the cancellation request reaches the WaitForExitAsync - // call on the process and shuts down the apphost before the JsonRpc connection gets it meaning - // that the apphost side of the RPC connection will be closed. Therefore if we get a - // ConnectionLostException AND the inner exception is an OperationCancelledException we can - // asume that the apphost was shutdown and we can ignore it. - } - catch (OperationCanceledException) - { - // This exception will be thrown if the cancellation request reaches the our side - // of the backchannel side first and the connection is torn down on our-side - // gracefully. We can ignore this exception as well. - } - }); + catch (ConnectionLostException ex) when (ex.InnerException is OperationCanceledException) + { + // This exception will be thrown if the cancellation request reaches the WaitForExitAsync + // call on the process and shuts down the apphost before the JsonRpc connection gets it meaning + // that the apphost side of the RPC connection will be closed. Therefore if we get a + // ConnectionLostException AND the inner exception is an OperationCancelledException we can + // asume that the apphost was shutdown and we can ignore it. + } + catch (OperationCanceledException) + { + // This exception will be thrown if the cancellation request reaches the our side + // of the backchannel side first and the connection is torn down on our-side + // gracefully. We can ignore this exception as well. + } + }); - return await pendingRun; + return await pendingRun; + } + else + { + return await pendingRun; + } } - else + catch (AppHostIncompatibleException ex) { - return await pendingRun; + return InteractionUtils.DisplayIncompatibleVersionError( + ex, + appHostCompatabilityCheck?.AspireHostingSdkVersion ?? throw new InvalidOperationException("AspireHostingSdkVersion is null") + ); } } } \ No newline at end of file diff --git a/src/Aspire.Cli/DotNetCliRunner.cs b/src/Aspire.Cli/DotNetCliRunner.cs index 7833fedf452..3e12e70adf4 100644 --- a/src/Aspire.Cli/DotNetCliRunner.cs +++ b/src/Aspire.Cli/DotNetCliRunner.cs @@ -468,6 +468,22 @@ private async Task StartBackchannelAsync(Process process, string socketPath, Tas // We don't want to spam the logs with our early connection attempts. } } + catch (AppHostIncompatibleException ex) + { + logger.LogError( + ex, + "The app host is incompatible with the CLI and must be updated to a version that supports the {RequiredCapability} capability.", + ex.RequiredCapability + ); + + // If the app host is incompatable then there is no point + // trying to reconnect, we should propogate the exception + // up to the code that needs to back channel so it can display + // and error message to the user. + backchannelCompletionSource.SetException(ex); + + throw; + } } while (await timer.WaitForNextTickAsync(cancellationToken)); } diff --git a/src/Aspire.Cli/ExitCodeConstants.cs b/src/Aspire.Cli/ExitCodeConstants.cs index e48a10aa003..dd6226afc7e 100644 --- a/src/Aspire.Cli/ExitCodeConstants.cs +++ b/src/Aspire.Cli/ExitCodeConstants.cs @@ -14,4 +14,5 @@ internal static class ExitCodeConstants public const int FailedToBuildArtifacts = 6; public const int FailedToFindProject = 7; public const int FailedToTrustCertificates = 8; + public const int AppHostIncompatible = 9; } \ No newline at end of file diff --git a/src/Aspire.Cli/Utils/AppHostHelper.cs b/src/Aspire.Cli/Utils/AppHostHelper.cs index c6824f09609..29908c6fe38 100644 --- a/src/Aspire.Cli/Utils/AppHostHelper.cs +++ b/src/Aspire.Cli/Utils/AppHostHelper.cs @@ -11,39 +11,39 @@ internal static class AppHostHelper { private static readonly ActivitySource s_activitySource = new ActivitySource(nameof(AppHostHelper)); - internal static async Task<(bool IsCompatableAppHost, bool SupportsBackchannel)> CheckAppHostCompatabilityAsync(DotNetCliRunner runner, FileInfo projectFile, CancellationToken cancellationToken) + internal static async Task<(bool IsCompatableAppHost, bool SupportsBackchannel, string? AspireHostingSdkVersion)> CheckAppHostCompatabilityAsync(DotNetCliRunner runner, FileInfo projectFile, CancellationToken cancellationToken) { var appHostInformation = await GetAppHostInformationAsync(runner, projectFile, cancellationToken); if (appHostInformation.ExitCode != 0) { AnsiConsole.MarkupLine($"[red bold]:thumbs_down: The project could not be analyzed due to a build error. For more information run with --debug switch.[/]"); - return (false, false); + return (false, false, null); } if (!appHostInformation.IsAspireHost) { AnsiConsole.MarkupLine($"[red bold]:thumbs_down: The project is not an Aspire app host project.[/]"); - return (false, false); + return (false, false, null); } if (!SemVersion.TryParse(appHostInformation.AspireHostingSdkVersion, out var aspireSdkVersion)) { AnsiConsole.MarkupLine($"[red bold]:thumbs_down: Could not parse Aspire SDK version.[/]"); - return (false, false); + return (false, false, null); } var compatibleRanges = SemVersionRange.Parse("^9.2.0-dev", SemVersionRangeOptions.IncludeAllPrerelease); if (!aspireSdkVersion.Satisfies(compatibleRanges)) { AnsiConsole.MarkupLine($"[red bold]:thumbs_down: The Aspire SDK version '{appHostInformation.AspireHostingSdkVersion}' is not supported. Please update to the latest version.[/]"); - return (false, false); + return (false, false, appHostInformation.AspireHostingSdkVersion); } else { // NOTE: When we go to support < 9.2.0 app hosts this is where we'll make // a determination as to whether the apphsot supports backchannel or not. - return (true, true); + return (true, true, appHostInformation.AspireHostingSdkVersion); } } diff --git a/src/Aspire.Cli/Utils/PromptUtils.cs b/src/Aspire.Cli/Utils/InteractionUtils.cs similarity index 66% rename from src/Aspire.Cli/Utils/PromptUtils.cs rename to src/Aspire.Cli/Utils/InteractionUtils.cs index 4b00b885e1d..cf517af303e 100644 --- a/src/Aspire.Cli/Utils/PromptUtils.cs +++ b/src/Aspire.Cli/Utils/InteractionUtils.cs @@ -1,11 +1,12 @@ // 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.Backchannel; using Spectre.Console; namespace Aspire.Cli.Utils; -internal static class PromptUtils +internal static class InteractionUtils { public static async Task PromptForStringAsync(string promptText, string? defaultValue = null, Func? validator = null, CancellationToken cancellationToken = default) { @@ -42,4 +43,17 @@ public static async Task PromptForSelectionAsync(string promptText, IEnume return await AnsiConsole.PromptAsync(prompt, cancellationToken); } + + public static int DisplayIncompatibleVersionError(AppHostIncompatibleException ex, string appHostHostingSdkVersion) + { + var cliInformationalVersion = VersionHelper.GetDefaultTemplateVersion(); + + AnsiConsole.MarkupLine($"[red bold]:thumbs_down: The app host is not compatible. Consider upgrading the app host or Aspire CLI.[/]"); + Console.WriteLine(); + AnsiConsole.MarkupLine($"\t[bold]Aspire Hosting SDK Version[/]: {appHostHostingSdkVersion}"); + AnsiConsole.MarkupLine($"\t[bold]Aspire CLI Version[/]: {cliInformationalVersion}"); + AnsiConsole.MarkupLine($"\t[bold]Required Capability[/]: {ex.RequiredCapability}"); + Console.WriteLine(); + return ExitCodeConstants.AppHostIncompatible; + } } \ No newline at end of file diff --git a/src/Aspire.Hosting/Backchannel/AppHostRpcTarget.cs b/src/Aspire.Hosting/Backchannel/AppHostRpcTarget.cs index 667742b7210..fe50ca47efb 100644 --- a/src/Aspire.Hosting/Backchannel/AppHostRpcTarget.cs +++ b/src/Aspire.Hosting/Backchannel/AppHostRpcTarget.cs @@ -138,4 +138,31 @@ public async Task GetPublishersAsync(CancellationToken cancellationTok var publishers = e.Advertisements.Select(x => x.Name); return [..publishers]; } + +#pragma warning disable CA1822 + public Task GetCapabilitiesAsync(CancellationToken cancellationToken) + { + // The purpose of this API is to allow the CLI to determine what API surfaces + // the AppHost supports. In 9.2 we'll be saying that you need a 9.2 apphost, + // but the 9.3 CLI might actually support working with 9.2 apphosts. The idea + // is that when the backchannel is established the CLI will call this API + // and store the results. The "baseline.v0" capability is the bare minimum + // that we need as of CLI version 9.2-preview*. + // + // Some capabilties will be opt in. For example in 9.3 we might refine the + // publishing activities API to return more information, or add log streaming + // features. So that would add a new capability that the apphsot can report + // on initial backchannel negotiation and the CLI can adapt its behavior around + // that. There may be scenarios where we need to break compataiblity at which + // point we might increase the baseline version that the apphost reports. + // + // The ability to support a back channel at all is determined by the CLI by + // making sure that the apphost version is at least > 9.2. + + _ = cancellationToken; + return Task.FromResult(new string[] { + "baseline.v0" + }); + } +#pragma warning restore CA1822 } \ No newline at end of file From e703fab423ccae1f6e99e03367ab8b0bf08dba25 Mon Sep 17 00:00:00 2001 From: Mitch Denny Date: Tue, 8 Apr 2025 08:45:23 +1000 Subject: [PATCH 2/5] Fix --watch hangs. (#8585) * Fix --watch hangs. * Don't prebuild in watch mode. * Fix up merge. --- src/Aspire.Cli/Commands/RunCommand.cs | 17 ++++++++++------- src/Aspire.Cli/Utils/AppHostHelper.cs | 2 +- 2 files changed, 11 insertions(+), 8 deletions(-) diff --git a/src/Aspire.Cli/Commands/RunCommand.cs b/src/Aspire.Cli/Commands/RunCommand.cs index 4cf26e72d2c..47b5ef4ae50 100644 --- a/src/Aspire.Cli/Commands/RunCommand.cs +++ b/src/Aspire.Cli/Commands/RunCommand.cs @@ -76,19 +76,22 @@ protected override async Task ExecuteAsync(ParseResult parseResult, Cancell appHostCompatabilityCheck = await AppHostHelper.CheckAppHostCompatabilityAsync(_runner, effectiveAppHostProjectFile, cancellationToken); - if (!appHostCompatabilityCheck?.IsCompatableAppHost ?? throw new InvalidOperationException("IsCompatableAppHost is null")) + if (!appHostCompatabilityCheck?.IsCompatableAppHost ?? throw new InvalidOperationException("IsCompatibleAppHost is null")) { return ExitCodeConstants.FailedToDotnetRunAppHost; } var watch = parseResult.GetValue("--watch"); - var buildExitCode = await AppHostHelper.BuildAppHostAsync(_runner, effectiveAppHostProjectFile, cancellationToken); - - if (buildExitCode != 0) + if (!watch) { - AnsiConsole.MarkupLine($"[red bold]:thumbs_down: The project could not be built. For more information run with --debug switch.[/]"); - return ExitCodeConstants.FailedToBuildArtifacts; + var buildExitCode = await AppHostHelper.BuildAppHostAsync(_runner, effectiveAppHostProjectFile, cancellationToken); + + if (buildExitCode != 0) + { + AnsiConsole.MarkupLine($"[red bold]:thumbs_down: The project could not be built. For more information run with --debug switch.[/]"); + return ExitCodeConstants.FailedToBuildArtifacts; + } } var backchannelCompletitionSource = new TaskCompletionSource(); @@ -96,7 +99,7 @@ protected override async Task ExecuteAsync(ParseResult parseResult, Cancell var pendingRun = _runner.RunAsync( effectiveAppHostProjectFile, watch, - true, + !watch, Array.Empty(), env, backchannelCompletitionSource, diff --git a/src/Aspire.Cli/Utils/AppHostHelper.cs b/src/Aspire.Cli/Utils/AppHostHelper.cs index 29908c6fe38..201088bccca 100644 --- a/src/Aspire.Cli/Utils/AppHostHelper.cs +++ b/src/Aspire.Cli/Utils/AppHostHelper.cs @@ -11,7 +11,7 @@ internal static class AppHostHelper { private static readonly ActivitySource s_activitySource = new ActivitySource(nameof(AppHostHelper)); - internal static async Task<(bool IsCompatableAppHost, bool SupportsBackchannel, string? AspireHostingSdkVersion)> CheckAppHostCompatabilityAsync(DotNetCliRunner runner, FileInfo projectFile, CancellationToken cancellationToken) + internal static async Task<(bool IsCompatibleAppHost, bool SupportsBackchannel, string? AspireHostingSdkVersion)> CheckAppHostCompatabilityAsync(DotNetCliRunner runner, FileInfo projectFile, CancellationToken cancellationToken) { var appHostInformation = await GetAppHostInformationAsync(runner, projectFile, cancellationToken); From b490d056b05860be30e1ff041ac4c69408a0e855 Mon Sep 17 00:00:00 2001 From: Mitch Denny Date: Tue, 8 Apr 2025 08:54:43 +1000 Subject: [PATCH 3/5] Add watch/no-build conflict fix. --- src/Aspire.Cli/DotNetCliRunner.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/Aspire.Cli/DotNetCliRunner.cs b/src/Aspire.Cli/DotNetCliRunner.cs index 3e12e70adf4..e8d947d1f12 100644 --- a/src/Aspire.Cli/DotNetCliRunner.cs +++ b/src/Aspire.Cli/DotNetCliRunner.cs @@ -126,7 +126,9 @@ public async Task RunAsync(FileInfo projectFile, bool watch, bool noBuild, if (watch && noBuild) { - throw new InvalidOperationException("Cannot use --watch and --no-build at the same time."); + var ex = new InvalidOperationException("Cannot use --watch and --no-build at the same time."); + backchannelCompletionSource?.SetException(ex); + throw ex; } var watchOrRunCommand = watch ? "watch" : "run"; From db6fd51d63f46b1b932e8545556d2398207b56f4 Mon Sep 17 00:00:00 2001 From: Mitch Denny Date: Tue, 8 Apr 2025 11:02:53 +1000 Subject: [PATCH 4/5] Fix spelling. --- src/Aspire.Cli/Commands/PublishCommand.cs | 8 ++++---- src/Aspire.Cli/Commands/RunCommand.cs | 8 ++++---- src/Aspire.Cli/Utils/AppHostHelper.cs | 2 +- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/src/Aspire.Cli/Commands/PublishCommand.cs b/src/Aspire.Cli/Commands/PublishCommand.cs index b09ab140acf..17199b47da9 100644 --- a/src/Aspire.Cli/Commands/PublishCommand.cs +++ b/src/Aspire.Cli/Commands/PublishCommand.cs @@ -38,7 +38,7 @@ public PublishCommand(DotNetCliRunner runner) protected override async Task ExecuteAsync(ParseResult parseResult, CancellationToken cancellationToken) { - (bool IsCompatableAppHost, bool SupportsBackchannel, string? AspireHostingSdkVersion)? appHostCompatabilityCheck = null; + (bool IsCompatableAppHost, bool SupportsBackchannel, string? AspireHostingSdkVersion)? appHostCompatibilityCheck = null; try { @@ -59,9 +59,9 @@ protected override async Task ExecuteAsync(ParseResult parseResult, Cancell env[KnownConfigNames.WaitForDebugger] = "true"; } - appHostCompatabilityCheck = await AppHostHelper.CheckAppHostCompatabilityAsync(_runner, effectiveAppHostProjectFile, cancellationToken); + appHostCompatibilityCheck = await AppHostHelper.CheckAppHostCompatibilityAsync(_runner, effectiveAppHostProjectFile, cancellationToken); - if (!appHostCompatabilityCheck?.IsCompatableAppHost ?? throw new InvalidOperationException("IsCompatableAppHost is null")) + if (!appHostCompatibilityCheck?.IsCompatableAppHost ?? throw new InvalidOperationException("IsCompatableAppHost is null")) { return ExitCodeConstants.FailedToDotnetRunAppHost; } @@ -255,7 +255,7 @@ protected override async Task ExecuteAsync(ParseResult parseResult, Cancell { return InteractionUtils.DisplayIncompatibleVersionError( ex, - appHostCompatabilityCheck?.AspireHostingSdkVersion ?? throw new InvalidOperationException("AspireHostingSdkVersion is null") + appHostCompatibilityCheck?.AspireHostingSdkVersion ?? throw new InvalidOperationException("AspireHostingSdkVersion is null") ); } } diff --git a/src/Aspire.Cli/Commands/RunCommand.cs b/src/Aspire.Cli/Commands/RunCommand.cs index 47b5ef4ae50..257808bef09 100644 --- a/src/Aspire.Cli/Commands/RunCommand.cs +++ b/src/Aspire.Cli/Commands/RunCommand.cs @@ -36,7 +36,7 @@ public RunCommand(DotNetCliRunner runner) protected override async Task ExecuteAsync(ParseResult parseResult, CancellationToken cancellationToken) { - (bool IsCompatableAppHost, bool SupportsBackchannel, string? AspireHostingSdkVersion)? appHostCompatabilityCheck = null; + (bool IsCompatableAppHost, bool SupportsBackchannel, string? AspireHostingSdkVersion)? appHostCompatibilityCheck = null; try { using var activity = _activitySource.StartActivity(); @@ -74,9 +74,9 @@ protected override async Task ExecuteAsync(ParseResult parseResult, Cancell return ExitCodeConstants.FailedToTrustCertificates; } - appHostCompatabilityCheck = await AppHostHelper.CheckAppHostCompatabilityAsync(_runner, effectiveAppHostProjectFile, cancellationToken); + appHostCompatibilityCheck = await AppHostHelper.CheckAppHostCompatibilityAsync(_runner, effectiveAppHostProjectFile, cancellationToken); - if (!appHostCompatabilityCheck?.IsCompatableAppHost ?? throw new InvalidOperationException("IsCompatibleAppHost is null")) + if (!appHostCompatibilityCheck?.IsCompatableAppHost ?? throw new InvalidOperationException("IsCompatibleAppHost is null")) { return ExitCodeConstants.FailedToDotnetRunAppHost; } @@ -217,7 +217,7 @@ await AnsiConsole.Live(table).StartAsync(async context => { { return InteractionUtils.DisplayIncompatibleVersionError( ex, - appHostCompatabilityCheck?.AspireHostingSdkVersion ?? throw new InvalidOperationException("AspireHostingSdkVersion is null") + appHostCompatibilityCheck?.AspireHostingSdkVersion ?? throw new InvalidOperationException("AspireHostingSdkVersion is null") ); } } diff --git a/src/Aspire.Cli/Utils/AppHostHelper.cs b/src/Aspire.Cli/Utils/AppHostHelper.cs index 201088bccca..6b75b8d4994 100644 --- a/src/Aspire.Cli/Utils/AppHostHelper.cs +++ b/src/Aspire.Cli/Utils/AppHostHelper.cs @@ -11,7 +11,7 @@ internal static class AppHostHelper { private static readonly ActivitySource s_activitySource = new ActivitySource(nameof(AppHostHelper)); - internal static async Task<(bool IsCompatibleAppHost, bool SupportsBackchannel, string? AspireHostingSdkVersion)> CheckAppHostCompatabilityAsync(DotNetCliRunner runner, FileInfo projectFile, CancellationToken cancellationToken) + internal static async Task<(bool IsCompatibleAppHost, bool SupportsBackchannel, string? AspireHostingSdkVersion)> CheckAppHostCompatibilityAsync(DotNetCliRunner runner, FileInfo projectFile, CancellationToken cancellationToken) { var appHostInformation = await GetAppHostInformationAsync(runner, projectFile, cancellationToken); From 672cfb08a160a7319452cc510d5e6dde70538322 Mon Sep 17 00:00:00 2001 From: Mitch Denny Date: Tue, 8 Apr 2025 11:03:35 +1000 Subject: [PATCH 5/5] Spelling. --- src/Aspire.Cli/Commands/PublishCommand.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Aspire.Cli/Commands/PublishCommand.cs b/src/Aspire.Cli/Commands/PublishCommand.cs index 17199b47da9..4ac12ef9e2e 100644 --- a/src/Aspire.Cli/Commands/PublishCommand.cs +++ b/src/Aspire.Cli/Commands/PublishCommand.cs @@ -61,7 +61,7 @@ protected override async Task ExecuteAsync(ParseResult parseResult, Cancell appHostCompatibilityCheck = await AppHostHelper.CheckAppHostCompatibilityAsync(_runner, effectiveAppHostProjectFile, cancellationToken); - if (!appHostCompatibilityCheck?.IsCompatableAppHost ?? throw new InvalidOperationException("IsCompatableAppHost is null")) + if (!appHostCompatibilityCheck?.IsCompatableAppHost ?? throw new InvalidOperationException("IsCompatibleAppHost is null")) { return ExitCodeConstants.FailedToDotnetRunAppHost; }