| File: Dashboard\DcpDashboard.cs | Web Access |
| Project: src\src\Aspire.Hosting.Native.Server\Aspire.Hosting.Native.Server.csproj (Aspire.Hosting.Native.Server) |
// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System.Net; using System.Net.Sockets; using System.Text.Json; using Aspire.Hosting.Native.Cli; using Aspire.Hosting.Native.Dcp; using Aspire.Hosting.Native.Server; using Aspire.Hosting.Native.Runtime; using Microsoft.Extensions.Logging; namespace Aspire.Hosting.Native.Dashboard; /// <summary>Launches the Dashboard as a DCP executable and reports authenticated frontend readiness.</summary> internal sealed class DcpDashboard(ILogger logger) { private NativeCliDashboardUrls _urls = new() { DashboardHealthy = false }; private string? _name; private string? _service; public NativeCliDashboardUrls Urls => Volatile.Read(ref _urls); public string? ApiToken { get; private set; } public async Task StartAsync(DcpApiClient client, string dashboardPath, Uri resourceService, NativeDashboardCredentials credentials, NativeServerOptions options, CancellationToken cancellationToken) { var name = "dashboard-" + Guid.NewGuid().ToString("N"); var service = name + "-frontend"; var spec = DcpDashboardConfiguration.Create(dashboardPath, service, resourceService, credentials); _name = name; _service = service; var browserToken = credentials.BrowserToken; var dashboardApiToken = credentials.DashboardApiKey; await client.CreateAsync(DcpResourceCollection.Services, service, JsonSerializer.SerializeToElement( new DcpServiceSpec(WorkloadProtocol.Tcp, DcpAddressAllocationMode.Localhost), DcpJsonContext.Default.DcpServiceSpec), null, cancellationToken).ConfigureAwait(false); await client.CreateAsync(DcpResourceCollection.Executables, name, JsonSerializer.SerializeToElement(spec, DcpJsonContext.Default.DcpWorkloadSpec), new Dictionary<string, string> { ["service-producer"] = JsonSerializer.Serialize( new[] { new DcpServiceProducer(service, IPAddress.Loopback.ToString(), null) }, DcpJsonContext.Default.DcpServiceProducerArray) }, cancellationToken).ConfigureAwait(false); using var http = new HttpClient(new HttpClientHandler { AllowAutoRedirect = false }) { Timeout = Timeout.InfiniteTimeSpan }; while (true) { var process = await client.GetAsync(DcpResourceCollection.Executables, name, cancellationToken).ConfigureAwait(false) ?? throw new InvalidOperationException("DCP removed the Dashboard during startup."); if ((process.Status?.State ?? DcpProcessState.Unknown).IsTerminal()) { throw new InvalidOperationException("The DCP-owned Dashboard failed to start."); } var allocation = await client.GetAsync(DcpResourceCollection.Services, service, cancellationToken).ConfigureAwait(false); if (process.Status is { State: DcpProcessState.Running, Pid: > 0 } && allocation?.Status is { EffectivePort: > 0 and <= 65535 } endpoint) { var address = endpoint.EffectiveAddress ?? throw new InvalidDataException("The Dashboard allocation has no address."); if (!address.Equals("localhost", StringComparison.OrdinalIgnoreCase) && (!IPAddress.TryParse(address, out var host) || !IPAddress.IsLoopback(host))) { throw new InvalidDataException("DCP must allocate a loopback Dashboard endpoint."); } var frontend = new UriBuilder("http", address, endpoint.EffectivePort.Value).Uri; try { using var requestTimeout = new TimeProviderCancellation(options.ControllerRequestTimeout, options.TimeProvider, cancellationToken); using var response = await http.GetAsync(new Uri(frontend, "health"), requestTimeout.Token).ConfigureAwait(false); if (response.IsSuccessStatusCode) { ApiToken = dashboardApiToken; Volatile.Write(ref _urls, new NativeCliDashboardUrls { DashboardHealthy = true, BaseUrlWithLoginToken = new Uri(frontend, "login?t=" + Uri.EscapeDataString(browserToken)).AbsoluteUri }); logger.LogInformation("DCP-owned Dashboard ({ProcessId}) listening on {Address}.", process.Status.Pid, frontend); return; } if (response.StatusCode != HttpStatusCode.ServiceUnavailable) { throw new InvalidOperationException($"Dashboard readiness failed ({(int)response.StatusCode})."); } } catch (HttpRequestException exception) when (exception.InnerException is SocketException { SocketErrorCode: SocketError.ConnectionRefused }) { logger.LogDebug("Waiting for the DCP-owned Dashboard listener."); } } await Task.Delay(options.RetryInterval, options.TimeProvider, cancellationToken).ConfigureAwait(false); } } public async Task StopAsync(DcpApiClient client, CancellationToken cancellationToken) { Volatile.Write(ref _urls, new NativeCliDashboardUrls { DashboardHealthy = false }); if (_name is not null) { await client.DeleteAsync(DcpResourceCollection.Executables, _name, cancellationToken).ConfigureAwait(false); } if (_service is not null) { await client.DeleteAsync(DcpResourceCollection.Services, _service, cancellationToken).ConfigureAwait(false); } } }