| File: TestServices\NativeClientTestHost.cs | Web Access |
| Project: src\tests\Aspire.Hosting.Native.Core.Tests\Aspire.Hosting.Native.Core.Tests.csproj (Aspire.Hosting.Native.Core.Tests) |
// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System.Diagnostics; using System.Net; using System.Security.Cryptography; using Aspire.Hosting.Native.Rpc; using Microsoft.Extensions.Logging.Abstractions; using Aspire.Hosting.Native.Runtime; namespace Aspire.Hosting.Native.Core.Tests.TestServices; /// <summary>Runs the actual native socket protocol, optionally against a separately published executable.</summary> internal sealed class NativeClientTestHost(TimeProvider timeProvider) : IAsyncDisposable { private readonly CancellationTokenSource _lifetime = new(); private NativeRpcListener? _listener; private Task? _listening; private Process? _process; private Task<string>? _stderr; private Task<string>? _stdout; public string Token { get; } = RandomNumberGenerator.GetHexString(64); public EndPoint Endpoint { get; private set; } = null!; public static Task<NativeClientTestHost> StartAsync() => StartCoreAsync(Environment.GetEnvironmentVariable("ASPIRE_NATIVE_CSHARP_TEST_SERVER"), null, TimeProvider.System, null); public static Task<NativeClientTestHost> StartWithWorkloadsAsync(string executable, string dcpPath) => StartCoreAsync(executable, dcpPath, TimeProvider.System, null); public static Task<NativeClientTestHost> StartWithWorkloadsAsync(string executable, string dcpPath, IReadOnlyDictionary<string, string> environment) => StartCoreAsync(executable, dcpPath, TimeProvider.System, environment); public static Task<NativeClientTestHost> StartWithOptionsAsync(Aspire.Hosting.Native.Server.NativeServerOptions options) => StartCoreAsync(null, null, TimeProvider.System, null, options); public static Task<NativeClientTestHost> StartWithExecutorAsync(IWorkloadExecutor executor, Aspire.Hosting.Native.Server.NativeServerOptions options) => StartCoreAsync(null, null, TimeProvider.System, null, options, executor); private static async Task<NativeClientTestHost> StartCoreAsync(string? executablePath, string? dcpPath, TimeProvider timeProvider, IReadOnlyDictionary<string, string>? environment, Aspire.Hosting.Native.Server.NativeServerOptions? options = null, IWorkloadExecutor? executor = null) { var host = new NativeClientTestHost(timeProvider); try { if (executablePath is { Length: > 0 } executable) { // Both native implementations expose the same --port transport and // authentication environment. There is no backend-specific client path. var start = new ProcessStartInfo(executable) { RedirectStandardOutput = true, RedirectStandardError = true, UseShellExecute = false }; start.ArgumentList.Add("--port"); start.ArgumentList.Add("0"); start.Environment["ASPIRE_NATIVE_RPC_TOKEN"] = host.Token; foreach (var name in new[] { "ASPIRE_NATIVE_DCP_PATH", "ASPIRE_DASHBOARD_PATH", "ASPIRE_NATIVE_DASHBOARD_PORT", "ASPIRE_BACKCHANNEL_PATH", "REMOTE_APP_HOST_SOCKET_PATH", "ASPIRE_NATIVE_SERVER_OPTIONS_PATH" }) { start.Environment.Remove(name); } if (dcpPath is not null) { start.Environment["ASPIRE_NATIVE_DCP_PATH"] = dcpPath; } if (environment is not null) { foreach (var variable in environment) { start.Environment[variable.Key] = variable.Value; } } host._process = Process.Start(start) ?? throw new InvalidOperationException("The native test server did not start."); host._stderr = host._process.StandardError.ReadToEndAsync(); using var startup = new CancellationTokenSource(TimeSpan.FromSeconds(15), timeProvider); // Direct native launches announce: // Native AppHost server listening on 127.0.0.1:<allocated-port>. // This discovers transport coordinates only; readiness is authenticated RPC. const string prefix = "Native AppHost server listening on "; while (await host._process.StandardOutput.ReadLineAsync(startup.Token) is { } line) { if (line.StartsWith(prefix, StringComparison.Ordinal) && IPEndPoint.TryParse(line[prefix.Length..].TrimEnd('.'), out var endpoint)) { host.Endpoint = endpoint; host._stdout = host._process.StandardOutput.ReadToEndAsync(); return host; } } throw new InvalidOperationException("The native test server did not publish its endpoint."); } host._listener = options is null ? new NativeRpcListener(new IPEndPoint(IPAddress.Loopback, 0), host.Token, NullLogger.Instance) : new NativeRpcListener(new IPEndPoint(IPAddress.Loopback, 0), host.Token, NullLogger.Instance, new Aspire.Hosting.Native.Api.NativeApplicationServer(executor ?? UnavailableWorkloadExecutor.Instance, options.Runtime), null, options); host.Endpoint = host._listener.EndPoint; host._listening = host._listener.RunAsync(host._lifetime.Token); return host; } catch { await host.DisposeAsync(); throw; } } public async ValueTask DisposeAsync() { await _lifetime.CancelAsync(); _listener?.Dispose(); if (_listening is not null) { await _listening.WaitAsync(TimeSpan.FromSeconds(15), timeProvider); } if (_process is { } process) { // Only this host's explicitly tracked process is terminated. if (!process.HasExited) { process.Kill(entireProcessTree: true); } await process.WaitForExitAsync().WaitAsync(TimeSpan.FromSeconds(15), timeProvider); if (_stdout is not null) { await _stdout; } if (_stderr is not null) { await _stderr; } process.Dispose(); } _lifetime.Dispose(); } }