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; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using ZB.MOM.WW.ScadaBridge.Communication; using ZB.MOM.WW.ScadaBridge.Communication.Grpc; namespace ZB.MOM.WW.ScadaBridge.LoadHarness.Probes; /// /// The site half of the wire-path stream probe: a real Kestrel HTTP/2 (h2c) listener on /// a loopback ephemeral port, serving the PRODUCTION /// over the site's real . /// /// /// This is the hop deliberately substitutes with a /// controllable in-process reader (which the slow-subscriber scenario needs), and /// therefore the hop the original runs never exercised: the /// SiteStreamEventBatcher pump (R2), protobuf serialization, /// IServerStreamWriter.WriteAsync, and HTTP/2 flow control. The server is built /// through its production DI constructor with default , /// so the batch negotiation (100 events / 25 ms window), channel capacities and the /// concurrency cap are exactly what ships. /// /// /// /// Deliberately NOT in the path: ControlPlaneAuthInterceptor (it lives in the /// Host project and authenticates once per CALL — one header compare per stream /// lifetime, nothing on the per-event path) and TLS (production is h2c too). /// /// public sealed class SiteWireStreamHost : IAsyncDisposable { private readonly WebApplication _app; /// The hosted production gRPC server (drop counters, active stream count). public SiteStreamGrpcServer Server { get; } /// The dialable endpoint, e.g. http://127.0.0.1:54321. public string Endpoint { get; } private SiteWireStreamHost(WebApplication app, SiteStreamGrpcServer server, string endpoint) { _app = app; Server = server; Endpoint = endpoint; } /// /// Builds, starts and readies the listener for one site. /// /// The site whose stream manager and actor system back the server. /// The started host. public static async Task StartAsync(SiteRuntimeFixture site) { var server = new SiteStreamGrpcServer( site.StreamManager, Microsoft.Extensions.Logging.Abstractions.NullLogger.Instance, Options.Create(new CommunicationOptions())); var builder = WebApplication.CreateSlimBuilder(); builder.Logging.ClearProviders(); builder.WebHost.ConfigureKestrel(options => options.Listen(System.Net.IPAddress.Loopback, 0, listenOptions => listenOptions.Protocols = HttpProtocols.Http2)); builder.Services.AddGrpc(); builder.Services.AddSingleton(server); var app = builder.Build(); app.MapGrpcService(); await app.StartAsync(); // Same post-start readiness handoff the production host performs. server.SetReady(site.System); var address = app.Services.GetRequiredService() .Features.Get()?.Addresses.FirstOrDefault() ?? throw new InvalidOperationException("Kestrel bound no address."); return new SiteWireStreamHost(app, server, address); } /// Cancels active streams and stops and disposes the underlying listener. /// A task that represents the asynchronous operation. public async ValueTask DisposeAsync() { // Mirrors the production shutdown ordering: cancel active streams first so // their pumps observe cancellation, then stop the listener. Server.CancelAllStreams(); await _app.StopAsync(); await _app.DisposeAsync(); } }