From 82c5da124fb7889eb11813028c549cda1606f5ec Mon Sep 17 00:00:00 2001 From: Mitch Denny Date: Wed, 30 Apr 2025 00:54:28 +0000 Subject: [PATCH 1/4] Wait for dashboard to be healthy before returning URL via RPC. --- .../Backchannel/AppHostRpcTarget.cs | 26 ++++++++++++++++--- 1 file changed, 23 insertions(+), 3 deletions(-) diff --git a/src/Aspire.Hosting/Backchannel/AppHostRpcTarget.cs b/src/Aspire.Hosting/Backchannel/AppHostRpcTarget.cs index fe50ca47efb..c3102597dba 100644 --- a/src/Aspire.Hosting/Backchannel/AppHostRpcTarget.cs +++ b/src/Aspire.Hosting/Backchannel/AppHostRpcTarget.cs @@ -21,7 +21,8 @@ internal class AppHostRpcTarget( IServiceProvider serviceProvider, IDistributedApplicationEventing eventing, PublishingActivityProgressReporter activityReporter, - IHostApplicationLifetime lifetime + IHostApplicationLifetime lifetime, + DistributedApplicationOptions options ) { public async IAsyncEnumerable<(string Id, string StatusText, bool IsComplete, bool IsError)> GetPublishingActivitiesAsync([EnumeratorCancellation]CancellationToken cancellationToken) @@ -101,6 +102,25 @@ public Task PingAsync(long timestamp, CancellationToken cancellationToken) public Task<(string BaseUrlWithLoginToken, string? CodespacesUrlWithLoginToken)> GetDashboardUrlsAsync() { + return GetDashboardUrlsAsync(CancellationToken.None); + } + + public async Task<(string BaseUrlWithLoginToken, string? CodespacesUrlWithLoginToken)> GetDashboardUrlsAsync(CancellationToken cancellationToken) + { + if (!options.DashboardEnabled) + { + logger.LogError("Dashboard URL requested but dashboard is disabled."); + throw new InvalidOperationException("Dashboard URL requested but dashboard is disabled."); + } + + // Wait for the dashboard to be healthy before we return the URL. This is to avoid + // a race condition when using Codespaces or devcontainers where the dashboard URL + // is displayed before the dashboard port forwarding is actually configured. It is + // also a point of friction to show the URL before the dashboard is ready to be used + // when using Devcontainers/Codespaces because people think that something isn't working + // when in fact they just need to refresh the page. + await resourceNotificationService.WaitForResourceHealthyAsync(KnownResourceNames.AspireDashboard, cancellationToken).ConfigureAwait(false); + var dashboardOptions = serviceProvider.GetService>(); if (dashboardOptions is null) @@ -122,11 +142,11 @@ public Task PingAsync(long timestamp, CancellationToken cancellationToken) if (baseUrlWithLoginToken == codespacesUrlWithLoginToken) { - return Task.FromResult<(string, string?)>((baseUrlWithLoginToken, null)); + return (baseUrlWithLoginToken, null); } else { - return Task.FromResult((baseUrlWithLoginToken, codespacesUrlWithLoginToken)); + return (baseUrlWithLoginToken, codespacesUrlWithLoginToken); } } From 515a54d02a86ed7c736a429b9f395445125ff4d2 Mon Sep 17 00:00:00 2001 From: Mitch Denny Date: Wed, 30 Apr 2025 06:28:24 +0000 Subject: [PATCH 2/4] Add health check to dashboard. --- src/Aspire.Hosting/Aspire.Hosting.csproj | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Aspire.Hosting/Aspire.Hosting.csproj b/src/Aspire.Hosting/Aspire.Hosting.csproj index 5b47ff4cdc4..76868939098 100644 --- a/src/Aspire.Hosting/Aspire.Hosting.csproj +++ b/src/Aspire.Hosting/Aspire.Hosting.csproj @@ -21,6 +21,7 @@ + From c9e3e327790c0f5af16566dda23812ada3e767f6 Mon Sep 17 00:00:00 2001 From: Mitch Denny Date: Wed, 30 Apr 2025 06:28:40 +0000 Subject: [PATCH 3/4] More files for health check. --- .../Dashboard/DashboardLifecycleHook.cs | 3 ++- .../DistributedApplicationBuilder.cs | 15 +++++++++++++++ src/Shared/KnownHealthCheckNames.cs | 12 ++++++++++++ 3 files changed, 29 insertions(+), 1 deletion(-) create mode 100644 src/Shared/KnownHealthCheckNames.cs diff --git a/src/Aspire.Hosting/Dashboard/DashboardLifecycleHook.cs b/src/Aspire.Hosting/Dashboard/DashboardLifecycleHook.cs index 2d22a5db2d1..8506b3a8342 100644 --- a/src/Aspire.Hosting/Dashboard/DashboardLifecycleHook.cs +++ b/src/Aspire.Hosting/Dashboard/DashboardLifecycleHook.cs @@ -127,7 +127,7 @@ private void AddDashboardResource(DistributedApplicationModel model) nameGenerator.EnsureDcpInstancesPopulated(dashboardResource); ConfigureAspireDashboardResource(dashboardResource); - + // Make the dashboard first in the list so it starts as fast as possible. model.Resources.Insert(0, dashboardResource); } @@ -179,6 +179,7 @@ private void ConfigureAspireDashboardResource(IResource dashboardResource) dashboardResource.Annotations.Add(new ResourceSnapshotAnnotation(snapshot)); dashboardResource.Annotations.Add(new EnvironmentCallbackAnnotation(ConfigureEnvironmentVariables)); + dashboardResource.Annotations.Add(new HealthCheckAnnotation(KnownHealthCheckNames.DasboardHealthCheck)); } internal async Task ConfigureEnvironmentVariables(EnvironmentCallbackContext context) diff --git a/src/Aspire.Hosting/DistributedApplicationBuilder.cs b/src/Aspire.Hosting/DistributedApplicationBuilder.cs index 9eb7c04ebc4..fcab487bad2 100644 --- a/src/Aspire.Hosting/DistributedApplicationBuilder.cs +++ b/src/Aspire.Hosting/DistributedApplicationBuilder.cs @@ -19,6 +19,7 @@ using Aspire.Hosting.Lifecycle; using Aspire.Hosting.Orchestrator; using Aspire.Hosting.Publishing; +using Aspire.Hosting.Utils; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; @@ -331,6 +332,20 @@ public DistributedApplicationBuilder(DistributedApplicationOptions options) _innerBuilder.Services.AddLifecycleHook(); _innerBuilder.Services.TryAddEnumerable(ServiceDescriptor.Singleton, ConfigureDefaultDashboardOptions>()); _innerBuilder.Services.TryAddEnumerable(ServiceDescriptor.Singleton, ValidateDashboardOptions>()); + + // Dashboard health check. + _innerBuilder.Services.AddHealthChecks().AddUrlGroup(sp => { + + var dashboardOptions = sp.GetRequiredService>().Value; + if (StringUtils.TryGetUriFromDelimitedString(dashboardOptions.DashboardUrl, ";", out var firstDashboardUrl)) + { + return firstDashboardUrl; + } + else + { + throw new DistributedApplicationException($"The dashboard resource '{KnownResourceNames.AspireDashboard}' does not have endpoints."); + } + }, KnownHealthCheckNames.DasboardHealthCheck); } if (options.EnableResourceLogging) diff --git a/src/Shared/KnownHealthCheckNames.cs b/src/Shared/KnownHealthCheckNames.cs new file mode 100644 index 00000000000..5bf0276584c --- /dev/null +++ b/src/Shared/KnownHealthCheckNames.cs @@ -0,0 +1,12 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Aspire; + +internal static class KnownHealthCheckNames +{ + /// + /// Common name for dashboard health check. + /// + public const string DasboardHealthCheck = "aspire_dashboard_check"; +} From fbf36ecce56003cf696175fc9fa815455ad8592e Mon Sep 17 00:00:00 2001 From: David Fowler Date: Tue, 29 Apr 2025 23:36:25 -0700 Subject: [PATCH 4/4] Update src/Aspire.Hosting/Dashboard/DashboardLifecycleHook.cs --- src/Aspire.Hosting/Dashboard/DashboardLifecycleHook.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Aspire.Hosting/Dashboard/DashboardLifecycleHook.cs b/src/Aspire.Hosting/Dashboard/DashboardLifecycleHook.cs index 8506b3a8342..bd9e337b06f 100644 --- a/src/Aspire.Hosting/Dashboard/DashboardLifecycleHook.cs +++ b/src/Aspire.Hosting/Dashboard/DashboardLifecycleHook.cs @@ -127,7 +127,6 @@ private void AddDashboardResource(DistributedApplicationModel model) nameGenerator.EnsureDcpInstancesPopulated(dashboardResource); ConfigureAspireDashboardResource(dashboardResource); - // Make the dashboard first in the list so it starts as fast as possible. model.Resources.Insert(0, dashboardResource); }