| File: Backchannel\DashboardUrlsHelper.cs | Web Access |
| Project: src\src\Aspire.Hosting\Aspire.Hosting.csproj (Aspire.Hosting) |
// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using Aspire.Hosting.ApplicationModel; using Aspire.Hosting.Dashboard; using Aspire.Hosting.Devcontainers.Codespaces; using Aspire.Hosting.Diagnostics; using Aspire.Hosting.Utils; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; namespace Aspire.Hosting.Backchannel; /// <summary> /// Helper class for retrieving dashboard connection information. /// </summary> internal static class DashboardUrlsHelper { /// <summary> /// Gets all dashboard connection information in a single call. /// Waits for the dashboard to become healthy before returning. /// </summary> /// <param name="serviceProvider">The service provider.</param> /// <param name="logger">The logger for diagnostic output.</param> /// <param name="cancellationToken">A cancellation token.</param> /// <returns>Complete dashboard connection information.</returns> public static async Task<DashboardConnectionInfo> GetDashboardConnectionInfoAsync( IServiceProvider serviceProvider, ILogger logger, CancellationToken cancellationToken = default) { // Check whether the dashboard resource is registered at all. If it isn't (e.g. the user // called DisableDashboard()), return immediately rather than waiting forever for a resource // event that will never arrive. var appModel = serviceProvider.GetService<DistributedApplicationModel>(); var dashboardResource = appModel?.Resources.SingleOrDefault( r => string.Equals(r.Name, KnownResourceNames.AspireDashboard, StringComparisons.ResourceName)) as IResourceWithEndpoints; if (dashboardResource is null) { logger.LogDebug("Dashboard resource is not present in the app model. Returning unavailable state."); return DashboardConnectionInfo.Unhealthy; } var profilingTelemetry = serviceProvider.GetRequiredService<ProfilingTelemetry>(); using var activity = profilingTelemetry.StartDashboardGetConnectionInfo(); var resourceNotificationService = serviceProvider.GetRequiredService<ResourceNotificationService>(); // Wait for the dashboard to be healthy using (var waitActivity = profilingTelemetry.StartDashboardWaitHealthy()) { try { await resourceNotificationService.WaitForResourceHealthyAsync( KnownResourceNames.AspireDashboard, WaitBehavior.StopOnResourceUnavailable, cancellationToken).ConfigureAwait(false); waitActivity.SetDashboardHealthy(true); waitActivity.AddDashboardWaitHealthyCompleted(); } catch (DistributedApplicationException ex) { waitActivity.SetDashboardHealthy(false); waitActivity.AddDashboardWaitHealthyFailed(); waitActivity.SetError(ex); activity.SetDashboardHealthy(false); logger.LogWarning(ex, "An error occurred while waiting for the Aspire Dashboard to become healthy."); return DashboardConnectionInfo.Unhealthy; } } try { var dashboardOptions = serviceProvider.GetService<IOptions<DashboardOptions>>()?.Value; if (dashboardOptions is null) { activity.SetDashboardHealthy(false); activity.SetDashboardUrlSource(ProfilingTelemetry.Values.DashboardUrlSourceNone); logger.LogWarning("Dashboard options not found."); return DashboardConnectionInfo.Unhealthy; } string? apiBaseUrl = null; var apiBaseUrlSource = ProfilingTelemetry.Values.DashboardUrlSourceNone; using (var resolveActivity = profilingTelemetry.StartDashboardResolveUrls()) { if (dashboardResource is not null) { // API endpoint (https or http) - used for Dashboard UI and Telemetry API var httpsEndpoint = dashboardResource.GetEndpoint("https"); var httpEndpoint = dashboardResource.GetEndpoint("http"); var apiEndpoint = httpsEndpoint.Exists ? httpsEndpoint : httpEndpoint; if (apiEndpoint.Exists) { apiBaseUrl = await EndpointHostHelpers.GetUrlWithTargetHostAsync(apiEndpoint, cancellationToken).ConfigureAwait(false); if (!string.IsNullOrEmpty(apiBaseUrl)) { apiBaseUrlSource = ProfilingTelemetry.Values.DashboardUrlSourceResource; } } } // Fall back to configured URL if we couldn't get it from the resource if (string.IsNullOrEmpty(apiBaseUrl)) { if (StringUtils.TryGetUriFromDelimitedString(dashboardOptions.DashboardUrl, ";", out var dashboardUri)) { apiBaseUrl = dashboardUri.GetLeftPart(UriPartial.Authority); apiBaseUrlSource = ProfilingTelemetry.Values.DashboardUrlSourceConfiguration; } } resolveActivity.SetDashboardUrlSource(apiBaseUrlSource); resolveActivity.SetDashboardHasApiBaseUrl(!string.IsNullOrEmpty(apiBaseUrl)); } // Build dashboard URLs. When browser token auth is enabled, include the login token. // When anonymous access is enabled, return the base URL directly. var codespacesUrlRewriter = serviceProvider.GetService<CodespacesUrlRewriter>(); string? baseUrlWithLoginToken = null; string? codespacesUrlWithLoginToken = null; if (!string.IsNullOrEmpty(apiBaseUrl)) { baseUrlWithLoginToken = !string.IsNullOrEmpty(dashboardOptions.DashboardToken) ? $"{apiBaseUrl.TrimEnd('/')}/login?t={dashboardOptions.DashboardToken}" : apiBaseUrl; var rewrittenUrl = codespacesUrlRewriter?.RewriteUrl(baseUrlWithLoginToken); if (rewrittenUrl != baseUrlWithLoginToken) { codespacesUrlWithLoginToken = rewrittenUrl; } } activity.SetDashboardHealthy(true); activity.SetDashboardUrlSource(apiBaseUrlSource); return new DashboardConnectionInfo { IsHealthy = true, ApiBaseUrl = apiBaseUrl, ApiToken = dashboardOptions.ApiKey, BaseUrlWithLoginToken = baseUrlWithLoginToken, CodespacesUrlWithLoginToken = codespacesUrlWithLoginToken }; } catch (Exception ex) { activity.SetError(ex); throw; } } /// <summary> /// Gets the dashboard URLs for the running AppHost. /// Waits for the dashboard to become healthy before returning. /// </summary> /// <param name="serviceProvider">The service provider.</param> /// <param name="logger">The logger for diagnostic output.</param> /// <param name="cancellationToken">A cancellation token.</param> /// <returns>The dashboard URL state including health and resolved dashboard URLs.</returns> public static async Task<DashboardUrlsState> GetDashboardUrlsAsync( IServiceProvider serviceProvider, ILogger logger, CancellationToken cancellationToken = default) { var info = await GetDashboardConnectionInfoAsync(serviceProvider, logger, cancellationToken).ConfigureAwait(false); return new DashboardUrlsState { DashboardHealthy = info.IsHealthy, BaseUrlWithLoginToken = info.BaseUrlWithLoginToken, CodespacesUrlWithLoginToken = info.CodespacesUrlWithLoginToken }; } } /// <summary> /// Contains all dashboard connection information. /// </summary> internal sealed class DashboardConnectionInfo { public static readonly DashboardConnectionInfo Unhealthy = new() { IsHealthy = false }; public bool IsHealthy { get; init; } public string? ApiBaseUrl { get; init; } public string? ApiToken { get; init; } /// <summary> /// Gets the resolved dashboard URL. /// When browser token authentication is enabled, this value includes the login token. /// </summary> public string? BaseUrlWithLoginToken { get; init; } /// <summary> /// Gets the resolved Codespaces dashboard URL, if available. /// When browser token authentication is enabled, this value includes the login token. /// </summary> public string? CodespacesUrlWithLoginToken { get; init; } }