diff --git a/src/libraries/Common/src/Interop/Unix/System.Native/Interop.ForkAndExecProcess.cs b/src/libraries/Common/src/Interop/Unix/System.Native/Interop.ForkAndExecProcess.cs index 7f2622b6d49c19..9a95fa3074d57a 100644 --- a/src/libraries/Common/src/Interop/Unix/System.Native/Interop.ForkAndExecProcess.cs +++ b/src/libraries/Common/src/Interop/Unix/System.Native/Interop.ForkAndExecProcess.cs @@ -111,7 +111,7 @@ private static unsafe partial int ForkAndExecProcess( /// Allocates a single native memory block containing both a null-terminated pointer array /// and the UTF-8 encoded string data for the given array of strings. /// - private static unsafe void AllocArgvArray(string[] arr, ref byte** arrPtr) + internal static unsafe void AllocArgvArray(string[] arr, ref byte** arrPtr) { int count = arr.Length; @@ -150,7 +150,7 @@ private static unsafe void AllocArgvArray(string[] arr, ref byte** arrPtr) /// Allocates a single native memory block containing both a null-terminated pointer array /// and the UTF-8 encoded "key=value\0" data for all non-null entries in the environment dictionary. /// - private static unsafe void AllocEnvpArray(IDictionary env, ref byte** arrPtr) + internal static unsafe void AllocEnvpArray(IDictionary env, ref byte** arrPtr) { // First pass: count entries with non-null values and compute total buffer size. int count = 0; diff --git a/src/libraries/System.Diagnostics.Process/ref/System.Diagnostics.Process.cs b/src/libraries/System.Diagnostics.Process/ref/System.Diagnostics.Process.cs index 1294bc9accf1c0..f8be1c4a2cfeae 100644 --- a/src/libraries/System.Diagnostics.Process/ref/System.Diagnostics.Process.cs +++ b/src/libraries/System.Diagnostics.Process/ref/System.Diagnostics.Process.cs @@ -215,6 +215,11 @@ public void Refresh() { } [System.Runtime.Versioning.UnsupportedOSPlatformAttribute("tvos")] [System.Runtime.Versioning.SupportedOSPlatformAttribute("maccatalyst")] // this needs to come after the ios attribute due to limitations in the platform analyzer public static System.Diagnostics.Process? Start(System.Diagnostics.ProcessStartInfo startInfo) { throw null; } + [System.CLSCompliantAttribute(false)] + [System.Runtime.Versioning.UnsupportedOSPlatformAttribute("ios")] + [System.Runtime.Versioning.UnsupportedOSPlatformAttribute("tvos")] + [System.Runtime.Versioning.SupportedOSPlatformAttribute("maccatalyst")] + public static System.Diagnostics.Process Start(System.Diagnostics.ProcessStartInfo startInfo, System.Func callback) { throw null; } [System.Runtime.Versioning.UnsupportedOSPlatformAttribute("ios")] [System.Runtime.Versioning.UnsupportedOSPlatformAttribute("tvos")] [System.Runtime.Versioning.SupportedOSPlatformAttribute("maccatalyst")] // this needs to come after the ios attribute due to limitations in the platform analyzer @@ -287,6 +292,21 @@ public readonly partial struct ProcessOutputLine public bool StandardError { get { throw null; } } public override string ToString() { throw null; } } + public ref partial struct ProcessStartArguments + { + public ProcessStartArguments() { } + [System.CLSCompliantAttribute(false)] + public unsafe void* Arguments { get { throw null; } set { } } + [System.CLSCompliantAttribute(false)] + public unsafe void* EnvironmentVariables { get { throw null; } set { } } + [System.CLSCompliantAttribute(false)] + [System.Runtime.Versioning.UnsupportedOSPlatformAttribute("windows")] + public unsafe byte* ResolvedPath { get { throw null; } set { } } + public System.Diagnostics.ProcessStartInfo ProcessStartInfo { get { throw null; } set { } } + public System.IntPtr StandardError { get { throw null; } set { } } + public System.IntPtr StandardInput { get { throw null; } set { } } + public System.IntPtr StandardOutput { get { throw null; } set { } } + } public enum ProcessPriorityClass { Normal = 32, diff --git a/src/libraries/System.Diagnostics.Process/ref/System.Diagnostics.Process.csproj b/src/libraries/System.Diagnostics.Process/ref/System.Diagnostics.Process.csproj index c94a5333818707..b31936a2e89d09 100644 --- a/src/libraries/System.Diagnostics.Process/ref/System.Diagnostics.Process.csproj +++ b/src/libraries/System.Diagnostics.Process/ref/System.Diagnostics.Process.csproj @@ -1,6 +1,7 @@ $(NetCoreAppCurrent) + true diff --git a/src/libraries/System.Diagnostics.Process/src/Microsoft/Win32/SafeHandles/SafeProcessHandle.Unix.cs b/src/libraries/System.Diagnostics.Process/src/Microsoft/Win32/SafeHandles/SafeProcessHandle.Unix.cs index 580c921aa243ec..e0f92731db6a66 100644 --- a/src/libraries/System.Diagnostics.Process/src/Microsoft/Win32/SafeHandles/SafeProcessHandle.Unix.cs +++ b/src/libraries/System.Diagnostics.Process/src/Microsoft/Win32/SafeHandles/SafeProcessHandle.Unix.cs @@ -218,6 +218,140 @@ internal static SafeProcessHandle StartCore(ProcessStartInfo startInfo, SafeFile out waitStateHolder); } + internal static unsafe SafeProcessHandle StartWithCallback(ProcessStartInfo startInfo, SafeFileHandle? stdinFd, SafeFileHandle? stdoutHandle, SafeFileHandle? stderrHandle, + Func callback, out ProcessWaitState.Holder? waitStateHolder) + { + waitStateHolder = null; + ProcessUtils.EnsureInitialized(); + + string? resolvedFileName = ProcessUtils.ResolvePath(startInfo.FileName); + if (string.IsNullOrEmpty(resolvedFileName)) + { + Interop.ErrorInfo error = Interop.Error.ENOENT.Info(); + throw ProcessUtils.CreateExceptionForErrorStartingProcess(error.GetErrorMessage(), error.RawErrno, startInfo.FileName, startInfo.WorkingDirectory); + } + + string[] argv = ProcessUtils.ParseArgv(startInfo); + bool configuredTerminal = false, usesTerminal = UsesTerminal(stdinFd, stdoutHandle, stderrHandle); + byte** argvPtr = null, envpPtr = null; + byte* resolvedPathBufferPtr = null; + bool stdinRefAdded = false, stdoutRefAdded = false, stderrRefAdded = false; + + try + { + Interop.Sys.AllocArgvArray(argv, ref argvPtr); + Interop.Sys.AllocEnvpArray(startInfo.Environment, ref envpPtr); + + int resolvedPathByteCount = Encoding.UTF8.GetByteCount(resolvedFileName); + // Keep small paths on the stack, while avoiding excessive stack usage for long executable paths. + const int ResolvedPathStackBufferSize = +#if DEBUG + 2; // make sure we test both code paths +#else + 256; +#endif + Span resolvedPathBuffer = resolvedPathByteCount + 1 <= ResolvedPathStackBufferSize + ? stackalloc byte[ResolvedPathStackBufferSize] + : new Span(resolvedPathBufferPtr = (byte*)NativeMemory.Alloc((nuint)(resolvedPathByteCount + 1)), resolvedPathByteCount + 1); + resolvedPathBuffer = resolvedPathBuffer[..(resolvedPathByteCount + 1)]; + + int resolvedPathBytesWritten = Encoding.UTF8.GetBytes(resolvedFileName, resolvedPathBuffer); + Debug.Assert(resolvedPathBytesWritten == resolvedPathByteCount); + resolvedPathBuffer[resolvedPathBytesWritten] = (byte)0; + + int stdinRawFd = -1, stdoutRawFd = -1, stderrRawFd = -1; + + if (stdinFd is not null) + { + stdinFd.DangerousAddRef(ref stdinRefAdded); + stdinRawFd = stdinFd.DangerousGetHandle().ToInt32(); + } + + if (stdoutHandle is not null) + { + stdoutHandle.DangerousAddRef(ref stdoutRefAdded); + stdoutRawFd = stdoutHandle.DangerousGetHandle().ToInt32(); + } + + if (stderrHandle is not null) + { + stderrHandle.DangerousAddRef(ref stderrRefAdded); + stderrRawFd = stderrHandle.DangerousGetHandle().ToInt32(); + } + + fixed (byte* resolvedPathPtr = resolvedPathBuffer) + { + ProcessStartArguments args = new() + { + ResolvedPath = resolvedPathPtr, + Arguments = argvPtr, + EnvironmentVariables = envpPtr, + StandardInput = stdinRawFd, + StandardOutput = stdoutRawFd, + StandardError = stderrRawFd, + ProcessStartInfo = startInfo, + }; + + // Lock to avoid races with OnSigChild + // By using a ReaderWriterLock we allow multiple processes to start concurrently. + ProcessUtils.s_processStartLock.EnterReadLock(); + + try + { + if (usesTerminal) + { + ProcessUtils.ConfigureTerminalForChildProcesses(1); + configuredTerminal = true; + } + + SafeProcessHandle processHandle = callback(args); + if (processHandle is null || processHandle.IsInvalid) + { + throw new ArgumentException(SR.Argument_InvalidHandle, nameof(callback)); + } + + waitStateHolder = new ProcessWaitState.Holder(processHandle.ProcessId, isNewChild: true, usesTerminal); + return processHandle; + } + finally + { + ProcessUtils.s_processStartLock.ExitReadLock(); + } + } + } + catch + { + if (configuredTerminal) + { + // We failed to launch a child that could use the terminal. + ProcessUtils.s_processStartLock.EnterWriteLock(); + ProcessUtils.ConfigureTerminalForChildProcesses(-1); + ProcessUtils.s_processStartLock.ExitWriteLock(); + } + + throw; + } + finally + { + if (stdinRefAdded) + { + stdinFd!.DangerousRelease(); + } + if (stdoutRefAdded) + { + stdoutHandle!.DangerousRelease(); + } + if (stderrRefAdded) + { + stderrHandle!.DangerousRelease(); + } + + NativeMemory.Free(envpPtr); + NativeMemory.Free(argvPtr); + NativeMemory.Free(resolvedPathBufferPtr); + } + } + private static SafeProcessHandle StartWithShellExecute(ProcessStartInfo startInfo, SafeFileHandle? stdinHandle, SafeFileHandle? stdoutHandle, SafeFileHandle? stderrHandle, out ProcessWaitState.Holder? waitStateHolder) { diff --git a/src/libraries/System.Diagnostics.Process/src/Microsoft/Win32/SafeHandles/SafeProcessHandle.Windows.cs b/src/libraries/System.Diagnostics.Process/src/Microsoft/Win32/SafeHandles/SafeProcessHandle.Windows.cs index 6410a78817e170..bad329d41e6036 100644 --- a/src/libraries/System.Diagnostics.Process/src/Microsoft/Win32/SafeHandles/SafeProcessHandle.Windows.cs +++ b/src/libraries/System.Diagnostics.Process/src/Microsoft/Win32/SafeHandles/SafeProcessHandle.Windows.cs @@ -356,6 +356,80 @@ internal static unsafe SafeProcessHandle StartCore(ProcessStartInfo startInfo, S return procSH; } + internal static unsafe SafeProcessHandle StartWithCallback(ProcessStartInfo startInfo, SafeFileHandle stdinHandle, SafeFileHandle stdoutHandle, SafeFileHandle stderrHandle, + Func callback) + { + ValueStringBuilder commandLine = new(stackalloc char[256]); + ProcessUtils.BuildCommandLine(startInfo, ref commandLine); + commandLine.NullTerminate(); + + string? environmentBlock = null; + if (startInfo._environmentVariables != null) + { + environmentBlock = ProcessUtils.GetEnvironmentVariablesBlock(startInfo._environmentVariables!); + } + + nint stdin = -1, stdout = -1, stderr = -1; + bool stdinRefAdded = false, stdoutRefAdded = false, stderrRefAdded = false; + + // Acquire the process lock to avoid accidental handle inheritance issues. + ProcessUtils.s_processStartLock.EnterWriteLock(); + + try + { + ProcessUtils.DuplicateAsInheritableIfNeeded(stdinHandle, ref stdin, ref stdinRefAdded); + ProcessUtils.DuplicateAsInheritableIfNeeded(stdoutHandle, ref stdout, ref stdoutRefAdded); + ProcessUtils.DuplicateAsInheritableIfNeeded(stderrHandle, ref stderr, ref stderrRefAdded); + + ProcessStartArguments args = new() + { + StandardInput = stdin, + StandardOutput = stdout, + StandardError = stderr, + ProcessStartInfo = startInfo, + }; + + fixed (char* commandLinePtr = &commandLine.GetPinnableReference()) + fixed (char* environmentBlockPtr = environmentBlock) + { + args.Arguments = commandLinePtr; + args.EnvironmentVariables = environmentBlockPtr; + + SafeProcessHandle startedProcess = callback(args); + if (startedProcess is null || startedProcess.IsInvalid) + { + throw new ArgumentException(SR.Argument_InvalidHandle, nameof(callback)); + } + + return startedProcess; + } + } + finally + { + // If the provided handle was inheritable, just release the reference we added. + // Otherwise if we created a valid duplicate, close it. + + if (stdinRefAdded) + stdinHandle.DangerousRelease(); + else if (!IsInvalidHandle(stdin)) + Interop.Kernel32.CloseHandle(stdin); + + if (stdoutRefAdded) + stdoutHandle.DangerousRelease(); + else if (!IsInvalidHandle(stdout)) + Interop.Kernel32.CloseHandle(stdout); + + if (stderrRefAdded) + stderrHandle.DangerousRelease(); + else if (!IsInvalidHandle(stderr)) + Interop.Kernel32.CloseHandle(stderr); + + ProcessUtils.s_processStartLock.ExitWriteLock(); + + commandLine.Dispose(); + } + } + private static unsafe SafeProcessHandle StartWithShellExecute(ProcessStartInfo startInfo) { if (!string.IsNullOrEmpty(startInfo.UserName) || startInfo.Password != null) diff --git a/src/libraries/System.Diagnostics.Process/src/Resources/Strings.resx b/src/libraries/System.Diagnostics.Process/src/Resources/Strings.resx index 92a6dd8e10675e..da808cf0554d77 100644 --- a/src/libraries/System.Diagnostics.Process/src/Resources/Strings.resx +++ b/src/libraries/System.Diagnostics.Process/src/Resources/Strings.resx @@ -372,4 +372,7 @@ On Unix, it's impossible to obtain the exit status of a non-child process. + + The callback must return a valid SafeProcessHandle for the newly created process. + diff --git a/src/libraries/System.Diagnostics.Process/src/System.Diagnostics.Process.csproj b/src/libraries/System.Diagnostics.Process/src/System.Diagnostics.Process.csproj index bc1e3b89bb10de..fcd34c1621f3d2 100644 --- a/src/libraries/System.Diagnostics.Process/src/System.Diagnostics.Process.csproj +++ b/src/libraries/System.Diagnostics.Process/src/System.Diagnostics.Process.csproj @@ -24,6 +24,7 @@ + diff --git a/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/Process.Unix.cs b/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/Process.Unix.cs index bd7a9df16cb809..441490801bc4a4 100644 --- a/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/Process.Unix.cs +++ b/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/Process.Unix.cs @@ -358,13 +358,20 @@ private SafeProcessHandle GetProcessHandle() return new SafeProcessHandle(_waitStateHolder!.IncrementRefCount()); } - private bool StartCore(ProcessStartInfo startInfo, SafeFileHandle? stdinHandle, SafeFileHandle? stdoutHandle, SafeFileHandle? stderrHandle, SafeHandle[]? inheritedHandles) + private bool StartCore(ProcessStartInfo startInfo, SafeFileHandle? stdinHandle, SafeFileHandle? stdoutHandle, SafeFileHandle? stderrHandle, SafeHandle[]? inheritedHandles, Func? callback) { - SafeProcessHandle startedProcess = SafeProcessHandle.StartCore(startInfo, stdinHandle, stdoutHandle, stderrHandle, inheritedHandles, out ProcessWaitState.Holder? waitStateHolder); + ProcessWaitState.Holder? waitStateHolder = null; + + SafeProcessHandle startedProcess = callback is null + ? SafeProcessHandle.StartCore(startInfo, stdinHandle, stdoutHandle, stderrHandle, inheritedHandles, out waitStateHolder) + : SafeProcessHandle.StartWithCallback(startInfo, stdinHandle!, stdoutHandle!, stderrHandle!, callback, out waitStateHolder); + Debug.Assert(!startedProcess.IsInvalid); - // SafeProcessHandle has its own copy of the wait state holder, so we need to increment the ref count for our copy. - _waitStateHolder = waitStateHolder!.IncrementRefCount(); + _waitStateHolder = callback is null + ? waitStateHolder!.IncrementRefCount() // SafeProcessHandle has its own copy of the wait state holder, so we need to increment the ref count for our copy. + : waitStateHolder; // we created a dedicated holder + SetProcessHandle(startedProcess); SetProcessId(startedProcess.ProcessId); return true; diff --git a/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/Process.Windows.cs b/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/Process.Windows.cs index cb4207390483fd..335178619fca7a 100644 --- a/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/Process.Windows.cs +++ b/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/Process.Windows.cs @@ -508,9 +508,11 @@ private SafeProcessHandle GetProcessHandle(int access, bool throwIfExited = true private static ConsoleEncoding GetStandardOutputEncoding() => GetEncoding((int)Interop.Kernel32.GetConsoleOutputCP()); - private bool StartCore(ProcessStartInfo startInfo, SafeFileHandle? stdinHandle, SafeFileHandle? stdoutHandle, SafeFileHandle? stderrHandle, SafeHandle[]? inheritedHandles) + private bool StartCore(ProcessStartInfo startInfo, SafeFileHandle? stdinHandle, SafeFileHandle? stdoutHandle, SafeFileHandle? stderrHandle, SafeHandle[]? inheritedHandles, Func? callback) { - SafeProcessHandle startedProcess = SafeProcessHandle.StartCore(startInfo, stdinHandle, stdoutHandle, stderrHandle, inheritedHandles); + SafeProcessHandle startedProcess = callback is null + ? SafeProcessHandle.StartCore(startInfo, stdinHandle, stdoutHandle, stderrHandle, inheritedHandles) + : SafeProcessHandle.StartWithCallback(startInfo, stdinHandle!, stdoutHandle!, stderrHandle!, callback); if (startedProcess.IsInvalid) { diff --git a/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/Process.cs b/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/Process.cs index ef037f21ec64cf..8892a9b3dde57e 100644 --- a/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/Process.cs +++ b/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/Process.cs @@ -1161,7 +1161,17 @@ public bool Start() { Close(); - ProcessStartInfo startInfo = StartInfo; + //Cannot start a new process and store its handle if the object has been disposed, since finalization has been suppressed. + CheckDisposed(); + + return StartCore(StartInfo, callback: null); + } + + [UnsupportedOSPlatform("ios")] + [UnsupportedOSPlatform("tvos")] + [SupportedOSPlatform("maccatalyst")] + private bool StartCore(ProcessStartInfo startInfo, Func? callback) + { startInfo.ThrowIfInvalid(out bool anyRedirection, out SafeHandle[]? inheritedHandles); if (!ProcessUtils.PlatformSupportsProcessStartAndKill) @@ -1169,9 +1179,6 @@ public bool Start() throw new PlatformNotSupportedException(); } - //Cannot start a new process and store its handle if the object has been disposed, since finalization has been suppressed. - CheckDisposed(); - SerializationGuard.ThrowIfDeserializationInProgress("AllowProcessCreation", ref ProcessUtils.s_cachedSerializationSwitch); SafeFileHandle? parentInputPipeHandle = null; @@ -1258,7 +1265,7 @@ public bool Start() ProcessStartInfo.ValidateInheritedHandles(childInputHandle, childOutputHandle, childErrorHandle, inheritedHandles); } - if (!StartCore(startInfo, childInputHandle, childOutputHandle, childErrorHandle, inheritedHandles)) + if (!StartCore(startInfo, childInputHandle, childOutputHandle, childErrorHandle, inheritedHandles, callback)) { return false; } @@ -1385,6 +1392,53 @@ public static Process Start(string fileName, IEnumerable arguments) null; } + /// + /// Starts a new process by preparing all necessary arguments (standard handles, command line, environment) + /// and then invoking the user-supplied to perform the actual process creation system call. + /// The callback receives a instance with the prepared data and must return a + /// representing the created process. + /// + /// The that contains the information used to start the process. + /// + /// A function that receives the prepared and creates the process using any system call of the user's choice. + /// The callback must return a valid for the newly created process. + /// The memory referenced by pointer properties in is only valid for the duration of the callback. + /// + /// A new instance associated with the started process. + /// or is . + /// The returned by the callback is invalid. + /// is set to . + [UnsupportedOSPlatform("ios")] + [UnsupportedOSPlatform("tvos")] + [SupportedOSPlatform("maccatalyst")] + [CLSCompliant(false)] + public static Process Start(ProcessStartInfo startInfo, Func callback) + { + ArgumentNullException.ThrowIfNull(startInfo); + ArgumentNullException.ThrowIfNull(callback); + + if (startInfo.UseShellExecute) + { + throw new InvalidOperationException(SR.Format(SR.UseShellExecuteNotSupportedForScenario, nameof(Start))); + } + + Process process = new(); + process._startInfo = startInfo; + + try + { + process.StartCore(startInfo, callback); + } + catch + { + process.Dispose(); + + throw; + } + + return process; + } + /// /// Make sure we are not watching for process exit. /// diff --git a/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/ProcessStartArguments.cs b/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/ProcessStartArguments.cs new file mode 100644 index 00000000000000..11e50826f5c606 --- /dev/null +++ b/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/ProcessStartArguments.cs @@ -0,0 +1,79 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.Win32.SafeHandles; +using System.Runtime.Versioning; + +namespace System.Diagnostics +{ + /// + /// Provides the prepared arguments for starting a process via a user-supplied callback. + /// This ref struct is populated by the method + /// with the resolved executable path, command-line arguments, environment variables, and standard I/O handles. + /// The user's callback receives this instance and is responsible for invoking the appropriate system call to create the process. + /// + public ref struct ProcessStartArguments + { + public ProcessStartArguments() { } + + /// + /// Gets or sets a pointer to the resolved absolute executable path encoded as null-terminated UTF-8. + /// + /// + /// A pointer to a null-terminated UTF-8 encoded string representing the resolved absolute executable path. + /// + /// + /// The memory pointed to by this property is only valid for the duration of the callback invocation. + /// This property is writable to allow callback implementations to override the resolved path when needed. + /// Do not cache or use this pointer after the callback returns. + /// + [CLSCompliant(false)] + [UnsupportedOSPlatform("windows")] + public unsafe byte* ResolvedPath { get; set; } + + /// + /// Gets or sets a pointer to the command-line arguments for the process. + /// On Windows, this is a pointer to a null-terminated string (the full command line including the executable). + /// On Unix, this is a pointer to a null-terminated array of pointers to null-terminated UTF-8 byte strings (argv). + /// + /// + /// The memory pointed to by this property is only valid for the duration of the callback invocation. + /// + [CLSCompliant(false)] + public unsafe void* Arguments { get; set; } + + /// + /// Gets or sets a pointer to the environment variables block for the new process. + /// On Windows, this is a pointer to a null-terminated string in the format used by CreateProcess + /// (each variable is "name=value\0", terminated by an extra '\0'). + /// On Unix, this is a pointer to a null-terminated array of pointers to null-terminated UTF-8 byte strings ("name=value"). + /// When , the new process inherits the current process's environment. + /// + /// + /// The memory pointed to by this property is only valid for the duration of the callback invocation. + /// + [CLSCompliant(false)] + public unsafe void* EnvironmentVariables { get; set; } + + /// + /// Gets or sets the raw handle to use as the standard input for the new process. + /// + public nint StandardInput { get; set; } + + /// + /// Gets or sets the raw handle to use as the standard output for the new process. + /// + public nint StandardOutput { get; set; } + + /// + /// Gets or sets the raw handle to use as the standard error for the new process. + /// + public nint StandardError { get; set; } + + /// + /// Gets or sets the original provided by the user, + /// allowing the callback to inspect any additional configuration. + /// + public ProcessStartInfo ProcessStartInfo { get; set; } = null!; + } +} diff --git a/src/libraries/System.Diagnostics.Process/tests/ProcessHandlesTests.Unix.cs b/src/libraries/System.Diagnostics.Process/tests/ProcessHandlesTests.Unix.cs index 70bc434b2b3c25..853d490beb248a 100644 --- a/src/libraries/System.Diagnostics.Process/tests/ProcessHandlesTests.Unix.cs +++ b/src/libraries/System.Diagnostics.Process/tests/ProcessHandlesTests.Unix.cs @@ -2,12 +2,107 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.ComponentModel; +using System.Runtime.InteropServices; +using Microsoft.DotNet.RemoteExecutor; +using Microsoft.DotNet.XUnitExtensions; using Microsoft.Win32.SafeHandles; +using Xunit; namespace System.Diagnostics.Tests { public partial class ProcessHandlesTests { + [ConditionalFact(typeof(RemoteExecutor), nameof(RemoteExecutor.IsSupported))] + public unsafe void StartWithCallback_PosixSpawn_CanRedirectOutput() + { + ProcessStartInfo startInfo = new("/bin/sh") + { + ArgumentList = { "-c", "echo hello && echo error >&2" }, + RedirectStandardOutput = true, + RedirectStandardError = true + }; + + using Process process = Process.Start(startInfo, (ProcessStartArguments args) => + { + int result; + + // posix_spawn_file_actions_t is a platform-specific struct (80 bytes on glibc x64, 104 bytes on macOS arm64). + // Use 128 bytes to be safe across platforms; NativeMemory.Alloc provides sufficient native alignment. + const int PosixSpawnFileActionsSize = 128; + void* fileActionsBuffer = NativeMemory.Alloc(PosixSpawnFileActionsSize); + if (fileActionsBuffer is null) + { + throw new OutOfMemoryException(); + } + + try + { + result = posix_spawn_file_actions_init(fileActionsBuffer); + if (result != 0) + { + throw new Win32Exception(result); + } + + try + { + Redirect(fileActionsBuffer, args.StandardInput, 0); + Redirect(fileActionsBuffer, args.StandardOutput, 1); + Redirect(fileActionsBuffer, args.StandardError, 2); + + int pid; + result = posix_spawn(&pid, args.ResolvedPath, fileActionsBuffer, null, (byte**)args.Arguments, (byte**)args.EnvironmentVariables); + + if (result != 0) + { + throw new Win32Exception(result); + } + + // Get SafeProcessHandle from the pid. + // In the future, SafeProcessHandle.Open will be used instead. + Process spawned = Process.GetProcessById(pid); + return spawned.SafeHandle; + } + finally + { + posix_spawn_file_actions_destroy(fileActionsBuffer); + } + } + finally + { + NativeMemory.Free(fileActionsBuffer); + } + }); + + (string output, string error) = process.ReadAllText(); + process.WaitForExit(WaitInMS); + + Assert.Equal("hello\n", output); + Assert.Equal("error\n", error); + Assert.True(process.HasExited); + Assert.Equal(0, process.ExitCode); + } + + private static unsafe void Redirect(void* fileActionsBuffer, nint handle, int fd) + { + int result = posix_spawn_file_actions_adddup2(fileActionsBuffer, (int)handle, fd); + if (result != 0) + { + throw new Win32Exception(result); + } + } + + [DllImport("libc", SetLastError = false)] + private static extern unsafe int posix_spawn(int* pid, byte* path, void* file_actions, void* attrp, byte** argv, byte** envp); + + [DllImport("libc", SetLastError = false)] + private static extern unsafe int posix_spawn_file_actions_init(void* file_actions); + + [DllImport("libc", SetLastError = false)] + private static extern unsafe int posix_spawn_file_actions_destroy(void* file_actions); + + [DllImport("libc", SetLastError = false)] + private static extern unsafe int posix_spawn_file_actions_adddup2(void* file_actions, int fd, int newfd); + private static string GetSafeFileHandleId(SafeFileHandle handle) { if (Interop.Sys.FStat(handle, out Interop.Sys.FileStatus status) != 0) diff --git a/src/libraries/System.Diagnostics.Process/tests/ProcessHandlesTests.Windows.cs b/src/libraries/System.Diagnostics.Process/tests/ProcessHandlesTests.Windows.cs index c49b71627e0a0e..dcdca892aa3827 100644 --- a/src/libraries/System.Diagnostics.Process/tests/ProcessHandlesTests.Windows.cs +++ b/src/libraries/System.Diagnostics.Process/tests/ProcessHandlesTests.Windows.cs @@ -105,6 +105,59 @@ public void ProcessStartedWithInvalidHandles_CanRedirectOutput(bool restrictHand Assert.Equal(RemoteExecutor.SuccessExitCode, RunWithInvalidHandles(process.StartInfo)); } + [Fact] + public unsafe void StartWithCallback_CreateProcess_CanRedirectOutput() + { + ProcessStartInfo startInfo = new("cmd") + { + ArgumentList = { "/c", "echo hello && echo error 1>&2" }, + RedirectStandardOutput = true, + RedirectStandardError = true + }; + + using Process process = Process.Start(startInfo, (ProcessStartArguments args) => + { + Interop.Kernel32.STARTUPINFOEX startupInfoEx = default; + Interop.Kernel32.PROCESS_INFORMATION processInfo = default; + Interop.Kernel32.SECURITY_ATTRIBUTES unused_SecAttrs = default; + + startupInfoEx.StartupInfo.cb = sizeof(Interop.Kernel32.STARTUPINFOEX); + startupInfoEx.StartupInfo.hStdInput = args.StandardInput; + startupInfoEx.StartupInfo.hStdOutput = args.StandardOutput; + startupInfoEx.StartupInfo.hStdError = args.StandardError; + startupInfoEx.StartupInfo.dwFlags = Interop.Advapi32.StartupInfoOptions.STARTF_USESTDHANDLES; + + bool retVal = Interop.Kernel32.CreateProcess( + null, + (char*)args.Arguments, + ref unused_SecAttrs, + ref unused_SecAttrs, + bInheritHandles: true, + Interop.Kernel32.EXTENDED_STARTUPINFO_PRESENT, + (char*)args.EnvironmentVariables, + null, + &startupInfoEx, + &processInfo + ); + + if (!retVal) + { + throw new Win32Exception(); + } + + Interop.Kernel32.CloseHandle(processInfo.hThread); + + return new SafeProcessHandle(processInfo.hProcess, ownsHandle: true); + }); + + (string output, string error) = process.ReadAllText(); + process.WaitForExit(WaitInMS); + + Assert.Equal("hello \r\n", output); + Assert.Equal("error \r\n", error); + Assert.Equal(0, process.ExitCode); + } + private unsafe int RunWithInvalidHandles(ProcessStartInfo startInfo) { const nint INVALID_HANDLE_VALUE = -1;