File: Processes\WindowsConsoleProcessJobTests.cs
Web Access
Project: src\tests\Aspire.Cli.Tests\Aspire.Cli.Tests.csproj (Aspire.Cli.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.Runtime.InteropServices;
using System.Runtime.Versioning;
using Aspire.Cli.Processes;
 
namespace Aspire.Cli.Tests.Processes;
 
public class WindowsConsoleProcessJobTests
{
    [Fact]
    [SupportedOSPlatform("windows")]
    public void Constructor_OnWindows_SucceedsAndExposesValidHandle()
    {
        Assert.SkipUnless(RuntimeInformation.IsOSPlatform(OSPlatform.Windows), "Windows-only test.");
 
        using var job = new WindowsConsoleProcessJob();
 
        Assert.NotNull(job.Handle);
        Assert.False(job.Handle.IsInvalid);
        Assert.False(job.Handle.IsClosed);
    }
 
    [Fact]
    [SupportedOSPlatform("windows")]
    public async Task Dispose_KillsAssignedChildProcess()
    {
        Assert.SkipUnless(RuntimeInformation.IsOSPlatform(OSPlatform.Windows), "Windows-only test.");
 
        var job = new WindowsConsoleProcessJob();
        using var spawnedProcess = SpawnJobAssignedChildProcess(job, createNewConsole: true);
 
        // Confirm the child is up before disposing the job — otherwise a fast spawn
        // failure would look identical to successful parent-exit cleanup.
        Assert.False(spawnedProcess.HasExited);
 
        job.Dispose();
 
        // KILL_ON_JOB_CLOSE is reliably observable within a couple of seconds;
        // give a generous window for CI under load.
        await spawnedProcess.WaitForExitAsync().WaitAsync(TimeSpan.FromSeconds(15));
 
        Assert.True(spawnedProcess.HasExited);
    }
 
    [Fact]
    [SupportedOSPlatform("windows")]
    public async Task Dispose_KillsAssignedChildProcessWithoutConsoleIsolation()
    {
        Assert.SkipUnless(RuntimeInformation.IsOSPlatform(OSPlatform.Windows), "Windows-only test.");
 
        var job = new WindowsConsoleProcessJob();
        using var spawnedProcess = SpawnJobAssignedChildProcess(job, createNewConsole: false);
 
        Assert.False(spawnedProcess.HasExited);
 
        job.Dispose();
 
        await spawnedProcess.WaitForExitAsync().WaitAsync(TimeSpan.FromSeconds(15));
 
        Assert.True(spawnedProcess.HasExited);
    }
 
    [Fact]
    [SupportedOSPlatform("windows")]
    public void SpawnProcess_WithJob_AssignsChildToJobAtomicallyAtCreation()
    {
        Assert.SkipUnless(RuntimeInformation.IsOSPlatform(OSPlatform.Windows), "Windows-only test.");
 
        using var job = new WindowsConsoleProcessJob();
 
        using var nulHandle = WindowsProcessInterop.CreateFileW(
            "NUL",
            WindowsProcessInterop.GenericRead | WindowsProcessInterop.GenericWrite,
            WindowsProcessInterop.FileShareRead | WindowsProcessInterop.FileShareWrite,
            nint.Zero,
            WindowsProcessInterop.OpenExisting,
            0,
            nint.Zero);
 
        Assert.False(nulHandle.IsInvalid);
        Assert.True(WindowsProcessInterop.SetHandleInformation(
            nulHandle,
            WindowsProcessInterop.HandleFlagInherit,
            WindowsProcessInterop.HandleFlagInherit));
 
        var nulRawHandle = nulHandle.DangerousGetHandle();
        var stdio = new WindowsProcessInterop.StdioHandles(
            Stdin: nulRawHandle,
            Stdout: nulRawHandle,
            Stderr: nulRawHandle);
 
        var pi = WindowsProcessInterop.SpawnProcess(
            "cmd.exe",
            ["/c", "ping", "-n", "60", "127.0.0.1"],
            Environment.CurrentDirectory,
            stdio,
            environment: null,
            createNewConsole: false,
            job.Handle);
 
        try
        {
            // PROC_THREAD_ATTRIBUTE_JOB_LIST associates the child with the job before it runs, so the
            // membership is observable the instant CreateProcess returns — there is no separate assign
            // step that a parent dying mid-spawn could skip, and the child was never suspended.
            Assert.True(WindowsProcessInterop.IsProcessInJob(pi.hProcess, job.Handle, out var isInJob));
            Assert.True(isInJob);
        }
        finally
        {
            WindowsProcessInterop.TerminateProcess(pi.hProcess, 1);
            WindowsProcessInterop.CloseHandle(pi.hProcess);
            WindowsProcessInterop.CloseHandle(pi.hThread);
        }
    }
 
    [SupportedOSPlatform("windows")]
    private static Process SpawnJobAssignedChildProcess(WindowsConsoleProcessJob job, bool createNewConsole)
    {
        using var nulHandle = WindowsProcessInterop.CreateFileW(
            "NUL",
            WindowsProcessInterop.GenericRead | WindowsProcessInterop.GenericWrite,
            WindowsProcessInterop.FileShareRead | WindowsProcessInterop.FileShareWrite,
            nint.Zero,
            WindowsProcessInterop.OpenExisting,
            0,
            nint.Zero);
 
        Assert.False(nulHandle.IsInvalid);
        Assert.True(WindowsProcessInterop.SetHandleInformation(
            nulHandle,
            WindowsProcessInterop.HandleFlagInherit,
            WindowsProcessInterop.HandleFlagInherit));
 
        var nulRawHandle = nulHandle.DangerousGetHandle();
        var stdio = new WindowsProcessInterop.StdioHandles(
            Stdin: nulRawHandle,
            Stdout: nulRawHandle,
            Stderr: nulRawHandle);
 
        var pi = WindowsProcessInterop.SpawnProcess(
            "cmd.exe",
            ["/c", "ping", "-n", "60", "127.0.0.1"],
            Environment.CurrentDirectory,
            stdio,
            environment: null,
            createNewConsole,
            job.Handle);
 
        try
        {
            return Process.GetProcessById(pi.dwProcessId);
        }
        finally
        {
            WindowsProcessInterop.CloseHandle(pi.hProcess);
            WindowsProcessInterop.CloseHandle(pi.hThread);
        }
    }
}