From 315849f0fb95c08bec04e459437d873423f088c3 Mon Sep 17 00:00:00 2001 From: Mitch Denny Date: Fri, 16 May 2025 04:00:24 +0000 Subject: [PATCH] Spike. --- .../Backchannel/AppHostBackchannel.cs | 19 +++++++++-- src/Aspire.Cli/Backchannel/CliRpcTarget.cs | 13 ++++++++ src/Aspire.Cli/Commands/PublishCommand.cs | 3 ++ src/Aspire.Cli/Program.cs | 20 +++++++++++- .../Backchannel/AppHostRpcTarget.cs | 32 +++++++++++++++---- .../Backchannel/BackchannelService.cs | 1 + 6 files changed, 79 insertions(+), 9 deletions(-) diff --git a/src/Aspire.Cli/Backchannel/AppHostBackchannel.cs b/src/Aspire.Cli/Backchannel/AppHostBackchannel.cs index ef534b4fd24..ff51b2e7a30 100644 --- a/src/Aspire.Cli/Backchannel/AppHostBackchannel.cs +++ b/src/Aspire.Cli/Backchannel/AppHostBackchannel.cs @@ -18,6 +18,7 @@ internal interface IAppHostBackchannel Task ConnectAsync(string socketPath, CancellationToken cancellationToken); IAsyncEnumerable<(string Id, string StatusText, bool IsComplete, bool IsError)> GetPublishingActivitiesAsync(CancellationToken cancellationToken); Task GetCapabilitiesAsync(CancellationToken cancellationToken); + Task RequestParameterPromptsAsync(CancellationToken cancellationToken); } internal sealed class AppHostBackchannel(ILogger logger, CliRpcTarget target) : IAppHostBackchannel @@ -77,7 +78,7 @@ await rpc.InvokeWithCancellationAsync( return url; } - public async IAsyncEnumerable<(string Resource, string Type, string State, string[] Endpoints)> GetResourceStatesAsync([EnumeratorCancellation]CancellationToken cancellationToken) + public async IAsyncEnumerable<(string Resource, string Type, string State, string[] Endpoints)> GetResourceStatesAsync([EnumeratorCancellation] CancellationToken cancellationToken) { using var activity = _activitySource.StartActivity(); @@ -143,7 +144,7 @@ public async Task ConnectAsync(string socketPath, CancellationToken cancellation } } - public async IAsyncEnumerable<(string Id, string StatusText, bool IsComplete, bool IsError)> GetPublishingActivitiesAsync([EnumeratorCancellation]CancellationToken cancellationToken) + public async IAsyncEnumerable<(string Id, string StatusText, bool IsComplete, bool IsError)> GetPublishingActivitiesAsync([EnumeratorCancellation] CancellationToken cancellationToken) { using var activity = _activitySource.StartActivity(); @@ -179,4 +180,18 @@ public async Task GetCapabilitiesAsync(CancellationToken cancellationT return capabilities; } + + public async Task RequestParameterPromptsAsync(CancellationToken cancellationToken) + { + using var activity = _activitySource.StartActivity(); + + var rpc = await _rpcTaskCompletionSource.Task.ConfigureAwait(false); + + logger.LogDebug("Requesting parameter prompts"); + + await rpc.InvokeWithCancellationAsync( + "RequestParameterPromptsAsync", + Array.Empty(), + cancellationToken).ConfigureAwait(false); + } } diff --git a/src/Aspire.Cli/Backchannel/CliRpcTarget.cs b/src/Aspire.Cli/Backchannel/CliRpcTarget.cs index f23d8ef0387..59835d9caa8 100644 --- a/src/Aspire.Cli/Backchannel/CliRpcTarget.cs +++ b/src/Aspire.Cli/Backchannel/CliRpcTarget.cs @@ -5,4 +5,17 @@ namespace Aspire.Cli.Backchannel; internal class CliRpcTarget { +#pragma warning disable CA1822 // Mark members as static + public async Task GetParameterValue(string parameterName, CancellationToken cancellationToken) +#pragma warning restore CA1822 // Mark members as static + { + if (GetParameterValueCallback is null) + { + throw new InvalidOperationException("GetParameterValueCallback is not set."); + } + + return await GetParameterValueCallback(parameterName).WaitAsync(cancellationToken).ConfigureAwait(false); + } + + public Func>? GetParameterValueCallback { get; set; } = null!; } \ No newline at end of file diff --git a/src/Aspire.Cli/Commands/PublishCommand.cs b/src/Aspire.Cli/Commands/PublishCommand.cs index 006a6312540..cb29241972c 100644 --- a/src/Aspire.Cli/Commands/PublishCommand.cs +++ b/src/Aspire.Cli/Commands/PublishCommand.cs @@ -151,6 +151,9 @@ protected override async Task ExecuteAsync(ParseResult parseResult, Cancell } var backchannel = await backchannelCompletionSource.Task.ConfigureAwait(false); + + await backchannel.RequestParameterPromptsAsync(cancellationToken); + var publishingActivities = backchannel.GetPublishingActivitiesAsync(cancellationToken); var debugMode = parseResult.GetValue("--debug") ?? false; diff --git a/src/Aspire.Cli/Program.cs b/src/Aspire.Cli/Program.cs index 2d7252ac396..f3e104b8439 100644 --- a/src/Aspire.Cli/Program.cs +++ b/src/Aspire.Cli/Program.cs @@ -122,7 +122,7 @@ private static IHost BuildApplication(string[] args) builder.Services.AddSingleton(); builder.Services.AddTransient(); builder.Services.AddTransient(); - builder.Services.AddSingleton(); + builder.Services.AddSingleton(BuildCliRpcTarget); builder.Services.AddTransient(); // Commands. @@ -136,6 +136,24 @@ private static IHost BuildApplication(string[] args) return app; } + private static CliRpcTarget BuildCliRpcTarget(IServiceProvider serviceProvider) + { + var ansiConsole = serviceProvider.GetRequiredService(); + + return new CliRpcTarget() + { + GetParameterValueCallback = async (parameterName) => + { + var textPrompt = new TextPrompt($"Enter value for {parameterName}:") + .AllowEmpty() + .ShowDefaultValue() + .PromptStyle("green"); + + return await ansiConsole.PromptAsync(textPrompt); + } + }; + } + private static IAnsiConsole BuildAnsiConsole(IServiceProvider serviceProvider) { AnsiConsoleSettings settings = new AnsiConsoleSettings() diff --git a/src/Aspire.Hosting/Backchannel/AppHostRpcTarget.cs b/src/Aspire.Hosting/Backchannel/AppHostRpcTarget.cs index 79ad2fced93..ceaefb18488 100644 --- a/src/Aspire.Hosting/Backchannel/AppHostRpcTarget.cs +++ b/src/Aspire.Hosting/Backchannel/AppHostRpcTarget.cs @@ -11,6 +11,7 @@ using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; +using StreamJsonRpc; namespace Aspire.Hosting.Backchannel; @@ -21,9 +22,9 @@ internal class AppHostRpcTarget( PublishingActivityProgressReporter activityReporter, IHostApplicationLifetime lifetime, DistributedApplicationOptions options - ) + ) { - public async IAsyncEnumerable<(string Id, string StatusText, bool IsComplete, bool IsError)> GetPublishingActivitiesAsync([EnumeratorCancellation]CancellationToken cancellationToken) + public async IAsyncEnumerable<(string Id, string StatusText, bool IsComplete, bool IsError)> GetPublishingActivitiesAsync([EnumeratorCancellation] CancellationToken cancellationToken) { while (cancellationToken.IsCancellationRequested == false) { @@ -43,7 +44,7 @@ DistributedApplicationOptions options publishingActivityStatus.IsError ); - if ( publishingActivityStatus.Activity.IsPrimary &&(publishingActivityStatus.IsComplete || publishingActivityStatus.IsError)) + if (publishingActivityStatus.Activity.IsPrimary && (publishingActivityStatus.IsComplete || publishingActivityStatus.IsError)) { // If the activity is complete or an error and it is the primary activity, // we can stop listening for updates. @@ -52,7 +53,7 @@ DistributedApplicationOptions options } } - public async IAsyncEnumerable<(string Resource, string Type, string State, string[] Endpoints)> GetResourceStatesAsync([EnumeratorCancellation]CancellationToken cancellationToken) + public async IAsyncEnumerable<(string Resource, string Type, string State, string[] Endpoints)> GetResourceStatesAsync([EnumeratorCancellation] CancellationToken cancellationToken) { var resourceEvents = resourceNotificationService.WatchAsync(cancellationToken); @@ -69,7 +70,7 @@ DistributedApplicationOptions options logger.LogTrace("Resource {Resource} does not have endpoints.", resourceEvent.Resource.Name); endpoints = Enumerable.Empty(); } - + var endpointUris = endpoints .Where(e => e.AllocatedEndpoint != null) .Select(e => e.AllocatedEndpoint!.UriString) @@ -129,7 +130,7 @@ await resourceNotificationService.WaitForResourceHealthyAsync( if (!StringUtils.TryGetUriFromDelimitedString(dashboardOptions.Value.DashboardUrl, ";", out var dashboardUri)) { logger.LogWarning("Dashboard URL could not be parsed from dashboard options."); - throw new InvalidOperationException("Dashboard URL could not be parsed from dashboard options."); + throw new InvalidOperationException("Dashboard URL could not be parsed from dashboard options."); } var codespacesUrlRewriter = serviceProvider.GetService(); @@ -173,4 +174,23 @@ public Task GetCapabilitiesAsync(CancellationToken cancellationToken) }); } #pragma warning restore CA1822 + + public async Task RequestParameterPromptsAsync(CancellationToken cancellationToken) + { + if (ClientRpc == null) + { + throw new DistributedApplicationException("ClientRpc is not set."); + } + + var model = serviceProvider.GetRequiredService(); + var parameters = model.Resources.OfType().ToList(); + + foreach (var parameter in parameters) + { + var value = await ClientRpc.InvokeWithCancellationAsync("GetParameterValue", [parameter.Name], cancellationToken).ConfigureAwait(false); + _ = value; + } + } + + public JsonRpc? ClientRpc { get; set; } } \ No newline at end of file diff --git a/src/Aspire.Hosting/Backchannel/BackchannelService.cs b/src/Aspire.Hosting/Backchannel/BackchannelService.cs index 77a2b600ead..0c6e012070b 100644 --- a/src/Aspire.Hosting/Backchannel/BackchannelService.cs +++ b/src/Aspire.Hosting/Backchannel/BackchannelService.cs @@ -49,6 +49,7 @@ await eventing.PublishAsync( var stream = new NetworkStream(clientSocket, true); var rpc = JsonRpc.Attach(stream, appHostRpcTarget); _rpc = rpc; + appHostRpcTarget.ClientRpc = rpc; // NOTE: The DistributedApplicationRunner will await this TCS // when a backchannel is expected, and will not stop