File: Processes\DetachedProcessLauncher.Windows.cs
Web Access
Project: src\src\Aspire.Cli\Aspire.Cli.csproj (aspire)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
using System.ComponentModel;
using System.Diagnostics;
using System.Runtime.InteropServices;
using System.Runtime.Versioning;
using System.Text;
using Microsoft.Win32.SafeHandles;
 
namespace Aspire.Cli.Processes;
 
internal static partial class DetachedProcessLauncher
{
    /// <summary>
    /// Windows implementation using CreateProcess with STARTUPINFOEX and
    /// PROC_THREAD_ATTRIBUTE_HANDLE_LIST to prevent handle inheritance to grandchildren.
    /// </summary>
    [SupportedOSPlatform("windows")]
    private static Process StartWindows(string fileName, IReadOnlyList<string> arguments, string workingDirectory)
    {
        // Open NUL device for stdout/stderr — child writes go nowhere
        using var nulHandle = CreateFileW(
            "NUL",
            GenericWrite,
            FileShareWrite,
            nint.Zero,
            OpenExisting,
            0,
            nint.Zero);
 
        if (nulHandle.IsInvalid)
        {
            throw new Win32Exception(Marshal.GetLastWin32Error(), "Failed to open NUL device");
        }
 
        // Mark the NUL handle as inheritable (required for STARTUPINFO hStdOutput assignment)
        if (!SetHandleInformation(nulHandle, HandleFlagInherit, HandleFlagInherit))
        {
            throw new Win32Exception(Marshal.GetLastWin32Error(), "Failed to set NUL handle inheritance");
        }
 
        // Initialize a process thread attribute list with 1 slot (HANDLE_LIST)
        var attrListSize = nint.Zero;
        InitializeProcThreadAttributeList(nint.Zero, 1, 0, ref attrListSize);
 
        var attrList = Marshal.AllocHGlobal(attrListSize);
        try
        {
            if (!InitializeProcThreadAttributeList(attrList, 1, 0, ref attrListSize))
            {
                throw new Win32Exception(Marshal.GetLastWin32Error(), "Failed to initialize process thread attribute list");
            }
 
            try
            {
                // Whitelist only the NUL handle for inheritance.
                // The grandchild (AppHost) will inherit this harmless handle instead of
                // any pipes from the caller's process tree.
                var handles = new[] { nulHandle.DangerousGetHandle() };
                var pinnedHandles = GCHandle.Alloc(handles, GCHandleType.Pinned);
                try
                {
                    if (!UpdateProcThreadAttribute(
                        attrList,
                        0,
                        s_procThreadAttributeHandleList,
                        pinnedHandles.AddrOfPinnedObject(),
                        (nint)(nint.Size * handles.Length),
                        nint.Zero,
                        nint.Zero))
                    {
                        throw new Win32Exception(Marshal.GetLastWin32Error(), "Failed to update process thread attribute list");
                    }
 
                    var nulRawHandle = nulHandle.DangerousGetHandle();
 
                    var si = new STARTUPINFOEX();
                    si.cb = Marshal.SizeOf<STARTUPINFOEX>();
                    si.dwFlags = StartfUseStdHandles;
                    si.hStdInput = nint.Zero;
                    si.hStdOutput = nulRawHandle;
                    si.hStdError = nulRawHandle;
                    si.lpAttributeList = attrList;
 
                    // Build the command line string: "fileName" arg1 arg2 ...
                    var commandLine = BuildCommandLine(fileName, arguments);
 
                    var flags = CreateUnicodeEnvironment | ExtendedStartupInfoPresent | CreateNoWindow;
 
                    if (!CreateProcessW(
                        null,
                        commandLine,
                        nint.Zero,
                        nint.Zero,
                        bInheritHandles: true, // TRUE but HANDLE_LIST restricts what's actually inherited
                        flags,
                        nint.Zero,
                        workingDirectory,
                        ref si,
                        out var pi))
                    {
                        throw new Win32Exception(Marshal.GetLastWin32Error(), "Failed to create detached process");
                    }
 
                    Process detachedProcess;
                    try
                    {
                        detachedProcess = Process.GetProcessById(pi.dwProcessId);
                    }
                    finally
                    {
                        // Close the process and thread handles returned by CreateProcess.
                        CloseHandle(pi.hProcess);
                        CloseHandle(pi.hThread);
                    }
 
                    return detachedProcess;
                }
                finally
                {
                    pinnedHandles.Free();
                }
            }
            finally
            {
                DeleteProcThreadAttributeList(attrList);
            }
        }
        finally
        {
            Marshal.FreeHGlobal(attrList);
        }
    }
 
    /// <summary>
    /// Builds a Windows command line string with correct quoting rules.
    /// Adapted from dotnet/runtime PasteArguments.AppendArgument.
    /// </summary>
    private static StringBuilder BuildCommandLine(string fileName, IReadOnlyList<string> arguments)
    {
        var sb = new StringBuilder();
 
        // Quote the executable path
        sb.Append('"').Append(fileName).Append('"');
 
        foreach (var arg in arguments)
        {
            sb.Append(' ');
            AppendArgument(sb, arg);
        }
 
        return sb;
    }
 
    /// <summary>
    /// Appends a correctly-quoted argument to the command line.
    /// Copied from dotnet/runtime src/libraries/System.Private.CoreLib/src/System/PasteArguments.cs
    /// </summary>
    private static void AppendArgument(StringBuilder sb, string argument)
    {
        // Windows command-line parsing rules:
        //   - Backslash is normal except when followed by a quote
        //   - 2N backslashes + quote → N literal backslashes + unescaped quote
        //   - 2N+1 backslashes + quote → N literal backslashes + literal quote
        if (argument.Length != 0 && !argument.AsSpan().ContainsAny(' ', '\t', '"'))
        {
            sb.Append(argument);
            return;
        }
 
        sb.Append('"');
        var idx = 0;
        while (idx < argument.Length)
        {
            var c = argument[idx++];
            if (c == '\\')
            {
                var numBackslash = 1;
                while (idx < argument.Length && argument[idx] == '\\')
                {
                    idx++;
                    numBackslash++;
                }
 
                if (idx == argument.Length)
                {
                    // Trailing backslashes before closing quote — must double them
                    sb.Append('\\', numBackslash * 2);
                }
                else if (argument[idx] == '"')
                {
                    // Backslashes followed by quote — double them + escape the quote
                    sb.Append('\\', numBackslash * 2 + 1);
                    sb.Append('"');
                    idx++;
                }
                else
                {
                    // Backslashes not followed by quote — emit as-is
                    sb.Append('\\', numBackslash);
                }
 
                continue;
            }
 
            if (c == '"')
            {
                sb.Append('\\');
                sb.Append('"');
                continue;
            }
 
            sb.Append(c);
        }
 
        sb.Append('"');
    }
 
    // --- Constants ---
    private const uint GenericWrite = 0x40000000;
    private const uint FileShareWrite = 0x00000002;
    private const uint OpenExisting = 3;
    private const uint HandleFlagInherit = 0x00000001;
    private const uint StartfUseStdHandles = 0x00000100;
    private const uint CreateUnicodeEnvironment = 0x00000400;
    private const uint ExtendedStartupInfoPresent = 0x00080000;
    private const uint CreateNoWindow = 0x08000000;
    private static readonly nint s_procThreadAttributeHandleList = (nint)0x00020002;
 
    // --- Structs ---
 
    [StructLayout(LayoutKind.Sequential)]
    private struct STARTUPINFOEX
    {
        public int cb;
        public nint lpReserved;
        public nint lpDesktop;
        public nint lpTitle;
        public int dwX;
        public int dwY;
        public int dwXSize;
        public int dwYSize;
        public int dwXCountChars;
        public int dwYCountChars;
        public int dwFillAttribute;
        public uint dwFlags;
        public ushort wShowWindow;
        public ushort cbReserved2;
        public nint lpReserved2;
        public nint hStdInput;
        public nint hStdOutput;
        public nint hStdError;
        public nint lpAttributeList;
    }
 
    [StructLayout(LayoutKind.Sequential)]
    private struct PROCESS_INFORMATION
    {
        public nint hProcess;
        public nint hThread;
        public int dwProcessId;
        public int dwThreadId;
    }
 
    // --- P/Invoke declarations ---
 
    [LibraryImport("kernel32.dll", SetLastError = true, StringMarshalling = StringMarshalling.Utf16)]
    private static partial SafeFileHandle CreateFileW(
        string lpFileName,
        uint dwDesiredAccess,
        uint dwShareMode,
        nint lpSecurityAttributes,
        uint dwCreationDisposition,
        uint dwFlagsAndAttributes,
        nint hTemplateFile);
 
    [LibraryImport("kernel32.dll", SetLastError = true)]
    [return: MarshalAs(UnmanagedType.Bool)]
    private static partial bool SetHandleInformation(
        SafeFileHandle hObject,
        uint dwMask,
        uint dwFlags);
 
    [LibraryImport("kernel32.dll", SetLastError = true)]
    [return: MarshalAs(UnmanagedType.Bool)]
    private static partial bool InitializeProcThreadAttributeList(
        nint lpAttributeList,
        int dwAttributeCount,
        int dwFlags,
        ref nint lpSize);
 
    [LibraryImport("kernel32.dll", SetLastError = true)]
    [return: MarshalAs(UnmanagedType.Bool)]
    private static partial bool UpdateProcThreadAttribute(
        nint lpAttributeList,
        uint dwFlags,
        nint attribute,
        nint lpValue,
        nint cbSize,
        nint lpPreviousValue,
        nint lpReturnSize);
 
    [LibraryImport("kernel32.dll", SetLastError = true)]
    private static partial void DeleteProcThreadAttributeList(nint lpAttributeList);
 
    [DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Unicode)]
#pragma warning disable CA1838 // CreateProcessW requires a mutable command line buffer
    private static extern bool CreateProcessW(
        string? lpApplicationName,
        StringBuilder lpCommandLine,
        nint lpProcessAttributes,
        nint lpThreadAttributes,
        bool bInheritHandles,
        uint dwCreationFlags,
        nint lpEnvironment,
        string? lpCurrentDirectory,
        ref STARTUPINFOEX lpStartupInfo,
        out PROCESS_INFORMATION lpProcessInformation);
#pragma warning restore CA1838
 
    [LibraryImport("kernel32.dll", SetLastError = true)]
    [return: MarshalAs(UnmanagedType.Bool)]
    private static partial bool CloseHandle(nint hObject);
}