Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 29 additions & 0 deletions src/Aspire.Cli/Backchannel/AppHostBackchannel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<string[]>(
"GetCapabilitiesAsync",
Array.Empty<object>(),
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);
}

Expand Down Expand Up @@ -145,4 +158,20 @@ public async Task<string[]> GetPublishersAsync(CancellationToken cancellationTok
yield return state;
}
}

public async Task<string[]> GetCapabilitiesAsync(CancellationToken cancellationToken)
{
using var activity = _activitySource.StartActivity();

var rpc = await _rpcTaskCompletionSource.Task.ConfigureAwait(false);

logger.LogDebug("Requesting capabilities");

var capabilities = await rpc.InvokeWithCancellationAsync<string[]>(
"GetCapabilitiesAsync",
Array.Empty<object>(),
cancellationToken).ConfigureAwait(false);

return capabilities;
}
}
9 changes: 9 additions & 0 deletions src/Aspire.Cli/Backchannel/AppHostIncompatibleException.cs
Original file line number Diff line number Diff line change
@@ -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;
}
8 changes: 4 additions & 4 deletions src/Aspire.Cli/Commands/NewCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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})",
Expand All @@ -84,7 +84,7 @@ private static async Task<string> GetProjectNameAsync(ParseResult parseResult, C
if (parseResult.GetValue<string>("--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);
}
Expand All @@ -96,7 +96,7 @@ private static async Task<string> GetOutputPathAsync(ParseResult parseResult, st
{
if (parseResult.GetValue<string>("--output") is not { } outputPath)
{
outputPath = await PromptUtils.PromptForStringAsync(
outputPath = await InteractionUtils.PromptForStringAsync(
"Enter the output path:",
defaultValue: pathAppendage ?? ".",
cancellationToken: cancellationToken
Expand All @@ -114,7 +114,7 @@ private static async Task<string> GetProjectTemplatesVersionAsync(ParseResult pa
}
else
{
version = await PromptUtils.PromptForStringAsync(
version = await InteractionUtils.PromptForStringAsync(
"Project templates version:",
defaultValue: VersionHelper.GetDefaultTemplateVersion(),
validator: (string value) => {
Expand Down
354 changes: 183 additions & 171 deletions src/Aspire.Cli/Commands/PublishCommand.cs

Large diffs are not rendered by default.

286 changes: 150 additions & 136 deletions src/Aspire.Cli/Commands/RunCommand.cs

Large diffs are not rendered by default.

20 changes: 19 additions & 1 deletion src/Aspire.Cli/DotNetCliRunner.cs
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,9 @@ public async Task<int> 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";
Expand Down Expand Up @@ -468,6 +470,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));
}
Expand Down
1 change: 1 addition & 0 deletions src/Aspire.Cli/ExitCodeConstants.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
12 changes: 6 additions & 6 deletions src/Aspire.Cli/Utils/AppHostHelper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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 IsCompatibleAppHost, bool SupportsBackchannel, string? AspireHostingSdkVersion)> CheckAppHostCompatibilityAsync(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);
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -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<string> PromptForStringAsync(string promptText, string? defaultValue = null, Func<string, ValidationResult>? validator = null, CancellationToken cancellationToken = default)
{
Expand Down Expand Up @@ -42,4 +43,17 @@ public static async Task<T> PromptForSelectionAsync<T>(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;
}
}
27 changes: 27 additions & 0 deletions src/Aspire.Hosting/Backchannel/AppHostRpcTarget.cs
Original file line number Diff line number Diff line change
Expand Up @@ -138,4 +138,31 @@ public async Task<string[]> GetPublishersAsync(CancellationToken cancellationTok
var publishers = e.Advertisements.Select(x => x.Name);
return [..publishers];
}

#pragma warning disable CA1822
public Task<string[]> 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
}
Loading