File: TestServices\NativeProviderProcessTestHost.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.Globalization;
 
namespace Aspire.Hosting.Native.Core.Tests.TestServices;
 
internal sealed class NativeProviderProcessTestHost(Process process, Process? descendant) : IAsyncDisposable
{
    public Process Process { get; } = process;
    public Process? Descendant { get; } = descendant;
 
    public static async Task<NativeProviderProcessTestHost> StartAsync(bool retainOutput)
    {
        var start = new ProcessStartInfo("/bin/sh")
        {
            UseShellExecute = false,
            RedirectStandardOutput = true,
            RedirectStandardError = true
        };
        start.ArgumentList.Add("-c");
        // The shell exits while its owned sleep child retains both redirected
        // pipes. stdout contains only the child's decimal PID followed by '\n'.
        start.ArgumentList.Add(retainOutput ? "sleep 60 & printf '%s\\n' \"$!\"; exit 0" : "exit 0");
        var process = System.Diagnostics.Process.Start(start) ?? throw new InvalidOperationException("The provider fixture did not start.");
        Process? descendant = null;
        try
        {
            if (retainOutput)
            {
                var line = await process.StandardOutput.ReadLineAsync(TestContext.Current.CancellationToken)
                    ?? throw new InvalidDataException("The provider fixture did not report its descendant.");
                descendant = System.Diagnostics.Process.GetProcessById(int.Parse(line, CultureInfo.InvariantCulture));
            }
            await process.WaitForExitStatusAsync().WaitAsync(TimeSpan.FromSeconds(10), TestContext.Current.CancellationToken);
 
            return new(process, descendant);
        }
        catch
        {
            try
            {
                if (!process.HasExited)
                {
                    try
                    {
                        process.Kill(entireProcessTree: true);
                    }
                    catch (InvalidOperationException) when (process.HasExited)
                    {
                    }
                }
                await process.WaitForExitStatusAsync().WaitAsync(TimeSpan.FromSeconds(10));
            }
            finally
            {
                await new NativeProviderProcessTestHost(process, descendant).DisposeAsync();
            }
            throw;
        }
    }
 
    public async ValueTask DisposeAsync()
    {
        try
        {
            if (Descendant is { } child)
            {
                try
                {
                    if (!child.HasExited)
                    {
                        child.Kill();
                    }
                }
                catch (InvalidOperationException) when (child.HasExited)
                {
                }
                // Unix cannot retrieve an exit status for this non-child process.
                await child.WaitForExitAsync().WaitAsync(TimeSpan.FromSeconds(10));
            }
        }
        finally
        {
            Descendant?.Dispose();
            Process.Dispose();
        }
    }
}