Skip to content

Commit 0623631

Browse files
[release/9.2] Add RPC protocol compat check. (#8604)
* Add RPC protocol compat check. (#8577) * Add RPC protocol compat check. * Fix merge conflict. * Fix spelling. * Update DotNetCliRunner.cs Co-authored-by: David Fowler <[email protected]> * Improve error message with version info. --------- Co-authored-by: David Fowler <[email protected]> * Fix --watch hangs. (#8585) * Fix --watch hangs. * Don't prebuild in watch mode. * Fix up merge. * Add watch/no-build conflict fix. * Fix spelling. * Spelling. --------- Co-authored-by: David Fowler <[email protected]>
1 parent 97a83d3 commit 0623631

File tree

10 files changed

+443
-319
lines changed

10 files changed

+443
-319
lines changed

src/Aspire.Cli/Backchannel/AppHostBackchannel.cs

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,19 @@ public async Task ConnectAsync(Process process, string socketPath, CancellationT
106106
var stream = new NetworkStream(socket, true);
107107
var rpc = JsonRpc.Attach(stream, target);
108108

109+
var capabilities = await rpc.InvokeWithCancellationAsync<string[]>(
110+
"GetCapabilitiesAsync",
111+
Array.Empty<object>(),
112+
cancellationToken);
113+
114+
if (!capabilities.Any(s => s == "baseline.v0"))
115+
{
116+
throw new AppHostIncompatibleException(
117+
$"AppHost is incompatible with the CLI. The AppHost must be updated to a version that supports the baseline.v0 capability.",
118+
"baseline.v0"
119+
);
120+
}
121+
109122
_rpcTaskCompletionSource.SetResult(rpc);
110123
}
111124

@@ -145,4 +158,20 @@ public async Task<string[]> GetPublishersAsync(CancellationToken cancellationTok
145158
yield return state;
146159
}
147160
}
161+
162+
public async Task<string[]> GetCapabilitiesAsync(CancellationToken cancellationToken)
163+
{
164+
using var activity = _activitySource.StartActivity();
165+
166+
var rpc = await _rpcTaskCompletionSource.Task.ConfigureAwait(false);
167+
168+
logger.LogDebug("Requesting capabilities");
169+
170+
var capabilities = await rpc.InvokeWithCancellationAsync<string[]>(
171+
"GetCapabilitiesAsync",
172+
Array.Empty<object>(),
173+
cancellationToken).ConfigureAwait(false);
174+
175+
return capabilities;
176+
}
148177
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
namespace Aspire.Cli.Backchannel;
5+
6+
internal sealed class AppHostIncompatibleException(string message, string requiredCapability) : Exception(message)
7+
{
8+
public string RequiredCapability { get; } = requiredCapability;
9+
}

src/Aspire.Cli/Commands/NewCommand.cs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,7 @@ public NewCommand(DotNetCliRunner runner, INuGetPackageCache nuGetPackageCache)
7070
}
7171
else
7272
{
73-
return await PromptUtils.PromptForSelectionAsync(
73+
return await InteractionUtils.PromptForSelectionAsync(
7474
"Select a project template:",
7575
validTemplates,
7676
t => $"{t.TemplateName} ({t.TemplateDescription})",
@@ -84,7 +84,7 @@ private static async Task<string> GetProjectNameAsync(ParseResult parseResult, C
8484
if (parseResult.GetValue<string>("--name") is not { } name)
8585
{
8686
var defaultName = new DirectoryInfo(Environment.CurrentDirectory).Name;
87-
name = await PromptUtils.PromptForStringAsync("Enter the project name:",
87+
name = await InteractionUtils.PromptForStringAsync("Enter the project name:",
8888
defaultValue: defaultName,
8989
cancellationToken: cancellationToken);
9090
}
@@ -96,7 +96,7 @@ private static async Task<string> GetOutputPathAsync(ParseResult parseResult, st
9696
{
9797
if (parseResult.GetValue<string>("--output") is not { } outputPath)
9898
{
99-
outputPath = await PromptUtils.PromptForStringAsync(
99+
outputPath = await InteractionUtils.PromptForStringAsync(
100100
"Enter the output path:",
101101
defaultValue: pathAppendage ?? ".",
102102
cancellationToken: cancellationToken
@@ -114,7 +114,7 @@ private static async Task<string> GetProjectTemplatesVersionAsync(ParseResult pa
114114
}
115115
else
116116
{
117-
version = await PromptUtils.PromptForStringAsync(
117+
version = await InteractionUtils.PromptForStringAsync(
118118
"Project templates version:",
119119
defaultValue: VersionHelper.GetDefaultTemplateVersion(),
120120
validator: (string value) => {

src/Aspire.Cli/Commands/PublishCommand.cs

Lines changed: 183 additions & 171 deletions
Large diffs are not rendered by default.

src/Aspire.Cli/Commands/RunCommand.cs

Lines changed: 150 additions & 136 deletions
Large diffs are not rendered by default.

src/Aspire.Cli/DotNetCliRunner.cs

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -126,7 +126,9 @@ public async Task<int> RunAsync(FileInfo projectFile, bool watch, bool noBuild,
126126

127127
if (watch && noBuild)
128128
{
129-
throw new InvalidOperationException("Cannot use --watch and --no-build at the same time.");
129+
var ex = new InvalidOperationException("Cannot use --watch and --no-build at the same time.");
130+
backchannelCompletionSource?.SetException(ex);
131+
throw ex;
130132
}
131133

132134
var watchOrRunCommand = watch ? "watch" : "run";
@@ -468,6 +470,22 @@ private async Task StartBackchannelAsync(Process process, string socketPath, Tas
468470
// We don't want to spam the logs with our early connection attempts.
469471
}
470472
}
473+
catch (AppHostIncompatibleException ex)
474+
{
475+
logger.LogError(
476+
ex,
477+
"The app host is incompatible with the CLI and must be updated to a version that supports the {RequiredCapability} capability.",
478+
ex.RequiredCapability
479+
);
480+
481+
// If the app host is incompatable then there is no point
482+
// trying to reconnect, we should propogate the exception
483+
// up to the code that needs to back channel so it can display
484+
// and error message to the user.
485+
backchannelCompletionSource.SetException(ex);
486+
487+
throw;
488+
}
471489

472490
} while (await timer.WaitForNextTickAsync(cancellationToken));
473491
}

src/Aspire.Cli/ExitCodeConstants.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,4 +14,5 @@ internal static class ExitCodeConstants
1414
public const int FailedToBuildArtifacts = 6;
1515
public const int FailedToFindProject = 7;
1616
public const int FailedToTrustCertificates = 8;
17+
public const int AppHostIncompatible = 9;
1718
}

src/Aspire.Cli/Utils/AppHostHelper.cs

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -11,39 +11,39 @@ internal static class AppHostHelper
1111
{
1212
private static readonly ActivitySource s_activitySource = new ActivitySource(nameof(AppHostHelper));
1313

14-
internal static async Task<(bool IsCompatableAppHost, bool SupportsBackchannel)> CheckAppHostCompatabilityAsync(DotNetCliRunner runner, FileInfo projectFile, CancellationToken cancellationToken)
14+
internal static async Task<(bool IsCompatibleAppHost, bool SupportsBackchannel, string? AspireHostingSdkVersion)> CheckAppHostCompatibilityAsync(DotNetCliRunner runner, FileInfo projectFile, CancellationToken cancellationToken)
1515
{
1616
var appHostInformation = await GetAppHostInformationAsync(runner, projectFile, cancellationToken);
1717

1818
if (appHostInformation.ExitCode != 0)
1919
{
2020
AnsiConsole.MarkupLine($"[red bold]:thumbs_down: The project could not be analyzed due to a build error. For more information run with --debug switch.[/]");
21-
return (false, false);
21+
return (false, false, null);
2222
}
2323

2424
if (!appHostInformation.IsAspireHost)
2525
{
2626
AnsiConsole.MarkupLine($"[red bold]:thumbs_down: The project is not an Aspire app host project.[/]");
27-
return (false, false);
27+
return (false, false, null);
2828
}
2929

3030
if (!SemVersion.TryParse(appHostInformation.AspireHostingSdkVersion, out var aspireSdkVersion))
3131
{
3232
AnsiConsole.MarkupLine($"[red bold]:thumbs_down: Could not parse Aspire SDK version.[/]");
33-
return (false, false);
33+
return (false, false, null);
3434
}
3535

3636
var compatibleRanges = SemVersionRange.Parse("^9.2.0-dev", SemVersionRangeOptions.IncludeAllPrerelease);
3737
if (!aspireSdkVersion.Satisfies(compatibleRanges))
3838
{
3939
AnsiConsole.MarkupLine($"[red bold]:thumbs_down: The Aspire SDK version '{appHostInformation.AspireHostingSdkVersion}' is not supported. Please update to the latest version.[/]");
40-
return (false, false);
40+
return (false, false, appHostInformation.AspireHostingSdkVersion);
4141
}
4242
else
4343
{
4444
// NOTE: When we go to support < 9.2.0 app hosts this is where we'll make
4545
// a determination as to whether the apphsot supports backchannel or not.
46-
return (true, true);
46+
return (true, true, appHostInformation.AspireHostingSdkVersion);
4747
}
4848
}
4949

src/Aspire.Cli/Utils/PromptUtils.cs renamed to src/Aspire.Cli/Utils/InteractionUtils.cs

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
11
// Licensed to the .NET Foundation under one or more agreements.
22
// The .NET Foundation licenses this file to you under the MIT license.
33

4+
using Aspire.Cli.Backchannel;
45
using Spectre.Console;
56

67
namespace Aspire.Cli.Utils;
78

8-
internal static class PromptUtils
9+
internal static class InteractionUtils
910
{
1011
public static async Task<string> PromptForStringAsync(string promptText, string? defaultValue = null, Func<string, ValidationResult>? validator = null, CancellationToken cancellationToken = default)
1112
{
@@ -42,4 +43,17 @@ public static async Task<T> PromptForSelectionAsync<T>(string promptText, IEnume
4243

4344
return await AnsiConsole.PromptAsync(prompt, cancellationToken);
4445
}
46+
47+
public static int DisplayIncompatibleVersionError(AppHostIncompatibleException ex, string appHostHostingSdkVersion)
48+
{
49+
var cliInformationalVersion = VersionHelper.GetDefaultTemplateVersion();
50+
51+
AnsiConsole.MarkupLine($"[red bold]:thumbs_down: The app host is not compatible. Consider upgrading the app host or Aspire CLI.[/]");
52+
Console.WriteLine();
53+
AnsiConsole.MarkupLine($"\t[bold]Aspire Hosting SDK Version[/]: {appHostHostingSdkVersion}");
54+
AnsiConsole.MarkupLine($"\t[bold]Aspire CLI Version[/]: {cliInformationalVersion}");
55+
AnsiConsole.MarkupLine($"\t[bold]Required Capability[/]: {ex.RequiredCapability}");
56+
Console.WriteLine();
57+
return ExitCodeConstants.AppHostIncompatible;
58+
}
4559
}

src/Aspire.Hosting/Backchannel/AppHostRpcTarget.cs

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -138,4 +138,31 @@ public async Task<string[]> GetPublishersAsync(CancellationToken cancellationTok
138138
var publishers = e.Advertisements.Select(x => x.Name);
139139
return [..publishers];
140140
}
141+
142+
#pragma warning disable CA1822
143+
public Task<string[]> GetCapabilitiesAsync(CancellationToken cancellationToken)
144+
{
145+
// The purpose of this API is to allow the CLI to determine what API surfaces
146+
// the AppHost supports. In 9.2 we'll be saying that you need a 9.2 apphost,
147+
// but the 9.3 CLI might actually support working with 9.2 apphosts. The idea
148+
// is that when the backchannel is established the CLI will call this API
149+
// and store the results. The "baseline.v0" capability is the bare minimum
150+
// that we need as of CLI version 9.2-preview*.
151+
//
152+
// Some capabilties will be opt in. For example in 9.3 we might refine the
153+
// publishing activities API to return more information, or add log streaming
154+
// features. So that would add a new capability that the apphsot can report
155+
// on initial backchannel negotiation and the CLI can adapt its behavior around
156+
// that. There may be scenarios where we need to break compataiblity at which
157+
// point we might increase the baseline version that the apphost reports.
158+
//
159+
// The ability to support a back channel at all is determined by the CLI by
160+
// making sure that the apphost version is at least > 9.2.
161+
162+
_ = cancellationToken;
163+
return Task.FromResult(new string[] {
164+
"baseline.v0"
165+
});
166+
}
167+
#pragma warning restore CA1822
141168
}

0 commit comments

Comments
 (0)