File: Dashboard\NativeDashboardHost.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.Security.Cryptography;
using System.Text;
using Aspire.Hosting.Native.Api;
using Aspire.Hosting.Native.Server;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Hosting.Server;
using Microsoft.AspNetCore.Hosting.Server.Features;
using Microsoft.AspNetCore.Server.Kestrel.Core;
using Microsoft.Extensions.DependencyInjection;
 
namespace Aspire.Hosting.Native.Dashboard;
 
/// <summary>Hosts the existing Dashboard protocol outside the BCL-only application kernel.</summary>
internal sealed class NativeDashboardHost : IAsyncDisposable
{
    private readonly WebApplication _application;
    private readonly NativeServerOptions _options;
    private readonly CancellationTokenSource _requests;
    public Uri Address { get; }
 
    private NativeDashboardHost(WebApplication application, Uri address, NativeServerOptions options, CancellationTokenSource requests)
    {
        _application = application;
        Address = address;
        _options = options;
        _requests = requests;
    }
 
    public static Task<NativeDashboardHost> StartAsync(NativeApplicationServer server, string applicationName,
        string apiKey, int port, CancellationToken cancellationToken)
        => StartAsync(server, applicationName, apiKey, port, new(), cancellationToken);
 
    public static async Task<NativeDashboardHost> StartAsync(NativeApplicationServer server, string applicationName,
        string apiKey, int port, NativeServerOptions options, CancellationToken cancellationToken)
    {
        options.Validate();
        ArgumentException.ThrowIfNullOrWhiteSpace(apiKey);
        ArgumentOutOfRangeException.ThrowIfNegative(port);
        ArgumentOutOfRangeException.ThrowIfGreaterThan(port, 65535);
        var builder = WebApplication.CreateSlimBuilder(new WebApplicationOptions { Args = [] });
        builder.WebHost.ConfigureKestrel(options =>
            options.Listen(IPAddress.Loopback, port, listen => listen.Protocols = HttpProtocols.Http2));
        // ASP.NET Core's container is private to this protocol adapter. Integrations
        // receive scoped ATS capabilities, never framework services or this container.
        builder.Services.AddGrpc(options =>
        {
            options.MaxReceiveMessageSize = 256 * 1024;
            options.MaxSendMessageSize = 4 * 1024 * 1024;
        });
        builder.Services.AddSingleton(new NativeDashboardService(server, applicationName, options.Runtime));
        var application = builder.Build();
        var requests = new CancellationTokenSource();
        var expected = Encoding.UTF8.GetBytes(apiKey);
        application.Use(async (context, next) =>
        {
            if (requests.IsCancellationRequested)
            {
                context.Response.StatusCode = 503;
                return;
            }
            // Resource-service watches normally survive revisions. They must not survive server
            // shutdown: leaving them attached until the Dashboard exits consumes its entire grace
            // period before DCP and the native server can finish their own cleanup.
            using var lifetime = CancellationTokenSource.CreateLinkedTokenSource(context.RequestAborted, requests.Token);
            context.RequestAborted = lifetime.Token;
            var supplied = context.Request.Headers["x-resource-service-api-key"];
            if (supplied.Count != 1 || supplied[0] is not { } value ||
                !CryptographicOperations.FixedTimeEquals(expected, Encoding.UTF8.GetBytes(value)))
            {
                // Match the resource-service API-key header used by the Dashboard.
                context.Response.StatusCode = 401;
                return;
            }
            await next(context).ConfigureAwait(false);
        });
        application.MapGrpcService<NativeDashboardService>();
        try
        {
            await application.StartAsync(cancellationToken).ConfigureAwait(false);
            var addresses = application.Services.GetRequiredService<IServer>().Features.Get<IServerAddressesFeature>()!;
 
            return new NativeDashboardHost(application, new Uri(addresses.Addresses.Single()), options, requests);
        }
        catch
        {
            await application.DisposeAsync().ConfigureAwait(false);
            requests.Dispose();
            throw;
        }
    }
 
    internal void BeginShutdown() => _requests.Cancel();
 
    public async ValueTask DisposeAsync()
    {
        BeginShutdown();
        using var timeout = new CancellationTokenSource(_options.DashboardShutdownTimeout, _options.TimeProvider);
        try
        {
            await _application.StopAsync(timeout.Token).ConfigureAwait(false);
        }
        finally
        {
            await _application.DisposeAsync().ConfigureAwait(false);
            _requests.Dispose();
        }
    }
}