From bc13558c52871ee5a683fcd7501077a785a6cd36 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 26 Feb 2026 21:11:27 +0000 Subject: [PATCH 01/21] Initial plan From 4780b7efe9b132392f4b8d7d19d72a12733a93c1 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 26 Feb 2026 21:41:10 +0000 Subject: [PATCH 02/21] Implement SafeProcessHandle APIs for macOS Co-authored-by: adamsitnik <6011991+adamsitnik@users.noreply.github.com> --- .../Interop.ForkAndExecProcess.cs | 50 +- .../System.Native/Interop.SpawnProcess.cs | 51 ++ .../SafeHandles/SafeProcessHandle.Unix.cs | 293 +++++++++- .../src/System.Diagnostics.Process.csproj | 4 + .../src/System/Diagnostics/Process.Unix.cs | 17 +- .../System/Diagnostics/ProcessUtils.Unix.cs | 64 +++ .../tests/SafeProcessHandleTests.Unix.cs | 81 +++ .../tests/SafeProcessHandleTests.cs | 70 ++- .../System.Diagnostics.Process.Tests.csproj | 1 + src/native/libs/Common/pal_config.h.in | 6 + src/native/libs/System.Native/entrypoints.c | 8 + src/native/libs/System.Native/pal_process.c | 509 +++++++++++++++++- src/native/libs/System.Native/pal_process.h | 71 +++ .../libs/System.Native/pal_process_wasi.c | 52 ++ src/native/libs/configure.cmake | 42 ++ 15 files changed, 1228 insertions(+), 91 deletions(-) create mode 100644 src/libraries/Common/src/Interop/Unix/System.Native/Interop.SpawnProcess.cs create mode 100644 src/libraries/System.Diagnostics.Process/tests/SafeProcessHandleTests.Unix.cs 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 bc947d0ef4ef49..3ccddabe1826d3 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 @@ -21,8 +21,8 @@ internal static unsafe int ForkAndExecProcess( int result = -1; try { - AllocNullTerminatedArray(argv, ref argvPtr); - AllocNullTerminatedArray(envp, ref envpPtr); + System.Diagnostics.ProcessUtils.AllocNullTerminatedArray(argv, ref argvPtr); + System.Diagnostics.ProcessUtils.AllocNullTerminatedArray(envp, ref envpPtr); fixed (uint* pGroups = groups) { result = ForkAndExecProcess( @@ -35,8 +35,8 @@ internal static unsafe int ForkAndExecProcess( } finally { - FreeArray(envpPtr, envp.Length); - FreeArray(argvPtr, argv.Length); + System.Diagnostics.ProcessUtils.FreeArray(envpPtr, envp.Length); + System.Diagnostics.ProcessUtils.FreeArray(argvPtr, argv.Length); } } @@ -46,47 +46,5 @@ private static unsafe partial int ForkAndExecProcess( int redirectStdin, int redirectStdout, int redirectStderr, int setUser, uint userId, uint groupId, uint* groups, int groupsLength, out int lpChildPid, out int stdinFd, out int stdoutFd, out int stderrFd); - - private static unsafe void AllocNullTerminatedArray(string[] arr, ref byte** arrPtr) - { - nuint arrLength = (nuint)arr.Length + 1; // +1 is for null termination - - // Allocate the unmanaged array to hold each string pointer. - // It needs to have an extra element to null terminate the array. - // Zero the memory so that if any of the individual string allocations fails, - // we can loop through the array to free any that succeeded. - // The last element will remain null. - arrPtr = (byte**)NativeMemory.AllocZeroed(arrLength, (nuint)sizeof(byte*)); - - // Now copy each string to unmanaged memory referenced from the array. - // We need the data to be an unmanaged, null-terminated array of UTF8-encoded bytes. - for (int i = 0; i < arr.Length; i++) - { - string str = arr[i]; - - int byteLength = Encoding.UTF8.GetByteCount(str); - arrPtr[i] = (byte*)NativeMemory.Alloc((nuint)byteLength + 1); //+1 for null termination - - int bytesWritten = Encoding.UTF8.GetBytes(str, new Span(arrPtr[i], byteLength)); - Debug.Assert(bytesWritten == byteLength); - - arrPtr[i][bytesWritten] = (byte)'\0'; // null terminate - } - } - - private static unsafe void FreeArray(byte** arr, int length) - { - if (arr != null) - { - // Free each element of the array - for (int i = 0; i < length; i++) - { - NativeMemory.Free(arr[i]); - } - - // And then the array itself - NativeMemory.Free(arr); - } - } } } diff --git a/src/libraries/Common/src/Interop/Unix/System.Native/Interop.SpawnProcess.cs b/src/libraries/Common/src/Interop/Unix/System.Native/Interop.SpawnProcess.cs new file mode 100644 index 00000000000000..8a6db042197878 --- /dev/null +++ b/src/libraries/Common/src/Interop/Unix/System.Native/Interop.SpawnProcess.cs @@ -0,0 +1,51 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Runtime.InteropServices; +using Microsoft.Win32.SafeHandles; + +internal static partial class Interop +{ + internal static partial class Sys + { + [LibraryImport(Libraries.SystemNative, EntryPoint = "SystemNative_SpawnProcess", StringMarshalling = StringMarshalling.Utf8, SetLastError = true)] + internal static unsafe partial int SpawnProcess( + string path, + byte** argv, + byte** envp, + int stdinFd, + int stdoutFd, + int stderrFd, + string? workingDir, + out int pid, + out int pidfd, + int killOnParentDeath, + int createSuspended, + int createNewProcessGroup, + int detached, + int* inheritedHandles, + int inheritedHandlesCount); + + [LibraryImport(Libraries.SystemNative, EntryPoint = "SystemNative_SendSignal", SetLastError = true)] + internal static partial int SendSignal(int pidfd, int pid, PosixSignal managedSignal); + + [LibraryImport(Libraries.SystemNative, EntryPoint = "SystemNative_WaitForExitAndReap", SetLastError = true)] + internal static partial int WaitForExitAndReap(SafeProcessHandle pidfd, int pid, out int exitCode, out int signal); + + [LibraryImport(Libraries.SystemNative, EntryPoint = "SystemNative_TryWaitForExit", SetLastError = true)] + internal static partial int TryWaitForExit(SafeProcessHandle pidfd, int pid, int timeoutMs, out int exitCode, out int signal); + + [LibraryImport(Libraries.SystemNative, EntryPoint = "SystemNative_TryWaitForExitCancellable", SetLastError = true)] + internal static partial int TryWaitForExitCancellable(SafeProcessHandle pidfd, int pid, int cancelPipeFd, out int exitCode, out int signal); + + [LibraryImport(Libraries.SystemNative, EntryPoint = "SystemNative_WaitForExitOrKillOnTimeout", SetLastError = true)] + internal static partial int WaitForExitOrKillOnTimeout(SafeProcessHandle pidfd, int pid, int timeoutMs, out int exitCode, out int signal, out int hasTimedout); + + [LibraryImport(Libraries.SystemNative, EntryPoint = "SystemNative_TryGetExitCode", SetLastError = true)] + internal static partial int TryGetExitCode(SafeProcessHandle pidfd, int pid, out int exitCode, out int signal); + + [LibraryImport(Libraries.SystemNative, EntryPoint = "SystemNative_OpenProcess", SetLastError = true)] + internal static partial int OpenProcess(int pid, out int outPidfd); + } +} 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 1fe9f5434d25a5..cef2be9a4d5f7c 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 @@ -21,6 +21,9 @@ public sealed partial class SafeProcessHandle : SafeHandleZeroOrMinusOneIsInvali // Process.{Safe}Handle to initialize and use a WaitHandle to successfully use it on // Unix as well to wait for the process to complete. + // Use int.MinValue instead of -1 because SafeProcessHandle derives from SafeHandleZeroOrMinusOneIsInvalid. + internal const int NoPidFd = int.MinValue; + private readonly SafeWaitHandle? _handle; private readonly bool _releaseRef; @@ -32,6 +35,12 @@ internal SafeProcessHandle(int processId, SafeWaitHandle handle) : handle.DangerousAddRef(ref _releaseRef); } + private SafeProcessHandle(int pidfd, int pid) + : this(existingHandle: (IntPtr)pidfd, ownsHandle: true) + { + ProcessId = pid; + } + protected override bool ReleaseHandle() { if (_releaseRef) @@ -39,27 +48,289 @@ protected override bool ReleaseHandle() Debug.Assert(_handle is not null); _handle.DangerousRelease(); } - return true; + + return (int)handle switch + { + NoPidFd => true, + _ => Interop.Sys.Close((IntPtr)(int)handle) == 0, + }; + } + + private static SafeProcessHandle OpenCore(int processId) + { + int result = Interop.Sys.OpenProcess(processId, out int pidfd); + + if (result == -1) + { + int errno = Marshal.GetLastPInvokeError(); + throw new Win32Exception(errno); + } + + return new SafeProcessHandle(pidfd, processId); } - private static SafeProcessHandle OpenCore(int processId) => throw new NotImplementedException(); + private static SafeProcessHandle StartCore(ProcessStartOptions options, SafeFileHandle inputHandle, SafeFileHandle outputHandle, SafeFileHandle errorHandle, bool createSuspended) + { + // Prepare arguments array (argv) + string[] argv = [options.FileName, .. options.Arguments]; - private static SafeProcessHandle StartCore(ProcessStartOptions options, SafeFileHandle inputHandle, SafeFileHandle outputHandle, SafeFileHandle errorHandle, bool createSuspended) => throw new NotImplementedException(); + // Prepare environment array (envp) only if the user has accessed it + // If not accessed, pass null to use the current environment (environ) + string[]? envp = options.HasEnvironmentBeenAccessed ? ProcessUtils.CreateEnvp(options.Environment) : null; - private ProcessExitStatus WaitForExitCore() => throw new NotImplementedException(); + // Get file descriptors for stdin/stdout/stderr + int stdInFd = (int)inputHandle.DangerousGetHandle(); + int stdOutFd = (int)outputHandle.DangerousGetHandle(); + int stdErrFd = (int)errorHandle.DangerousGetHandle(); - private bool TryWaitForExitCore(int milliseconds, [NotNullWhen(true)] out ProcessExitStatus? exitStatus) => throw new NotImplementedException(); + return StartProcessInternal(options.FileName, argv, envp, options, stdInFd, stdOutFd, stdErrFd, createSuspended); + } - private ProcessExitStatus WaitForExitOrKillOnTimeoutCore(int milliseconds) => throw new NotImplementedException(); + private static unsafe SafeProcessHandle StartProcessInternal(string resolvedPath, string[] argv, string[]? envp, + ProcessStartOptions options, int stdinFd, int stdoutFd, int stderrFd, bool createSuspended) + { + byte** argvPtr = null; + byte** envpPtr = null; + int* inheritedHandlesPtr = null; + int inheritedHandlesCount = 0; - private Task WaitForExitAsyncCore(CancellationToken cancellationToken) => throw new NotImplementedException(); + try + { + ProcessUtils.AllocNullTerminatedArray(argv, ref argvPtr); - private Task WaitForExitOrKillOnCancellationAsyncCore(CancellationToken cancellationToken) => throw new NotImplementedException(); + // Only allocate envp if the user has accessed the environment + if (envp is not null) + { + ProcessUtils.AllocNullTerminatedArray(envp, ref envpPtr); + } - internal bool KillCore(bool throwOnError, bool entireProcessGroup = false) => throw new NotImplementedException(); + // Allocate and copy inherited handles if provided + if (options.HasInheritedHandlesBeenAccessed && options.InheritedHandles.Count > 0) + { + inheritedHandlesCount = options.InheritedHandles.Count; + inheritedHandlesPtr = (int*)NativeMemory.Alloc((nuint)inheritedHandlesCount, (nuint)sizeof(int)); - private void ResumeCore() => throw new NotImplementedException(); + for (int i = 0; i < inheritedHandlesCount; i++) + { + inheritedHandlesPtr[i] = (int)options.InheritedHandles[i].DangerousGetHandle(); + } + } + + // Call native library to spawn process + int result = Interop.Sys.SpawnProcess( + resolvedPath, + argvPtr, + envpPtr, + stdinFd, + stdoutFd, + stderrFd, + options.WorkingDirectory, + out int pid, + out int pidfd, + options.KillOnParentExit ? 1 : 0, + createSuspended ? 1 : 0, + options.CreateNewProcessGroup ? 1 : 0, + 0, // detached + inheritedHandlesPtr, + inheritedHandlesCount); + + if (result == -1) + { + int errorCode = Marshal.GetLastPInvokeError(); + throw new Win32Exception(errorCode); + } + + return new SafeProcessHandle(pidfd == -1 ? NoPidFd : pidfd, pid); + } + finally + { + ProcessUtils.FreeArray(envpPtr, envp?.Length ?? 0); + ProcessUtils.FreeArray(argvPtr, argv.Length); + NativeMemory.Free(inheritedHandlesPtr); + } + } + + private ProcessExitStatus WaitForExitCore() + { + switch (Interop.Sys.WaitForExitAndReap(this, ProcessId, out int exitCode, out int rawSignal)) + { + case -1: + int errno = Marshal.GetLastPInvokeError(); + throw new Win32Exception(errno); + default: + return new(exitCode, false, rawSignal != 0 ? (PosixSignal)rawSignal : null); + } + } + + private bool TryWaitForExitCore(int milliseconds, [NotNullWhen(true)] out ProcessExitStatus? exitStatus) + { + switch (Interop.Sys.TryWaitForExit(this, ProcessId, milliseconds, out int exitCode, out int rawSignal)) + { + case -1: + int errno = Marshal.GetLastPInvokeError(); + throw new Win32Exception(errno); + case 1: // timeout + exitStatus = null; + return false; + default: + exitStatus = new(exitCode, false, rawSignal != 0 ? (PosixSignal)rawSignal : null); + return true; + } + } + + private ProcessExitStatus WaitForExitOrKillOnTimeoutCore(int milliseconds) + { + switch (Interop.Sys.WaitForExitOrKillOnTimeout(this, ProcessId, milliseconds, out int exitCode, out int rawSignal, out int hasTimedout)) + { + case -1: + int errno = Marshal.GetLastPInvokeError(); + throw new Win32Exception(errno); + default: + return new(exitCode, hasTimedout == 1, rawSignal != 0 ? (PosixSignal)rawSignal : null); + } + } + + private async Task WaitForExitAsyncCore(CancellationToken cancellationToken) + { + if (!cancellationToken.CanBeCanceled) + { + return await Task.Run(() => WaitForExitCore(), cancellationToken).ConfigureAwait(false); + } - private void SendSignalCore(PosixSignal signal, bool entireProcessGroup) => throw new NotImplementedException(); + unsafe + { + int* fds = stackalloc int[2]; + if (Interop.Sys.Pipe(fds, Interop.Sys.PipeFlags.O_CLOEXEC) != 0) + { + throw new Win32Exception(Marshal.GetLastPInvokeError()); + } + + SafeFileHandle readHandle = new SafeFileHandle((IntPtr)fds[Interop.Sys.ReadEndOfPipe], ownsHandle: true); + SafeFileHandle writeHandle = new SafeFileHandle((IntPtr)fds[Interop.Sys.WriteEndOfPipe], ownsHandle: true); + + using (readHandle) + using (writeHandle) + { + using CancellationTokenRegistration registration = cancellationToken.Register(static state => + { + ((SafeFileHandle)state!).Close(); // Close the write end of the pipe to signal cancellation + }, writeHandle); + + return await Task.Run(() => + { + switch (Interop.Sys.TryWaitForExitCancellable(this, ProcessId, (int)readHandle.DangerousGetHandle(), out int exitCode, out int rawSignal)) + { + case -1: + int errno = Marshal.GetLastPInvokeError(); + throw new Win32Exception(errno); + case 1: // canceled + throw new OperationCanceledException(cancellationToken); + default: + return new ProcessExitStatus(exitCode, false, rawSignal != 0 ? (PosixSignal)rawSignal : null); + } + }, cancellationToken).ConfigureAwait(false); + } + } + } + + private async Task WaitForExitOrKillOnCancellationAsyncCore(CancellationToken cancellationToken) + { + if (!cancellationToken.CanBeCanceled) + { + return await Task.Run(() => WaitForExitCore(), cancellationToken).ConfigureAwait(false); + } + + unsafe + { + int* fds = stackalloc int[2]; + if (Interop.Sys.Pipe(fds, Interop.Sys.PipeFlags.O_CLOEXEC) != 0) + { + throw new Win32Exception(Marshal.GetLastPInvokeError()); + } + + SafeFileHandle readHandle = new SafeFileHandle((IntPtr)fds[Interop.Sys.ReadEndOfPipe], ownsHandle: true); + SafeFileHandle writeHandle = new SafeFileHandle((IntPtr)fds[Interop.Sys.WriteEndOfPipe], ownsHandle: true); + + using (readHandle) + using (writeHandle) + { + using CancellationTokenRegistration registration = cancellationToken.Register(static state => + { + ((SafeFileHandle)state!).Close(); // Close the write end of the pipe to signal cancellation + }, writeHandle); + + return await Task.Run(() => + { + switch (Interop.Sys.TryWaitForExitCancellable(this, ProcessId, (int)readHandle.DangerousGetHandle(), out int exitCode, out int rawSignal)) + { + case -1: + int errno = Marshal.GetLastPInvokeError(); + throw new Win32Exception(errno); + case 1: // canceled + bool wasKilled = KillCore(throwOnError: false); + ProcessExitStatus status = WaitForExitCore(); + return new ProcessExitStatus(status.ExitCode, wasKilled, status.Signal); + default: + return new ProcessExitStatus(exitCode, false, rawSignal != 0 ? (PosixSignal)rawSignal : null); + } + }, cancellationToken).ConfigureAwait(false); + } + } + } + + internal bool KillCore(bool throwOnError, bool entireProcessGroup = false) + { + // If entireProcessGroup is true, send to -pid (negative pid), don't use pidfd. + int pidfd = entireProcessGroup ? NoPidFd : (int)this.handle; + int pid = entireProcessGroup ? -ProcessId : ProcessId; + int result = Interop.Sys.SendSignal(pidfd, pid, PosixSignal.SIGKILL); + if (result == 0) + { + return true; + } + + const int ESRCH = 3; + int errno = Marshal.GetLastPInvokeError(); + if (errno == ESRCH) + { + return false; // Process already exited + } + + if (!throwOnError) + { + return false; + } + + throw new Win32Exception(errno); + } + + private void ResumeCore() + { + // Resume a suspended process by sending SIGCONT + int result = Interop.Sys.SendSignal((int)this.handle, ProcessId, PosixSignal.SIGCONT); + if (result == 0) + { + return; + } + + int errno = Marshal.GetLastPInvokeError(); + throw new Win32Exception(errno); + } + + private void SendSignalCore(PosixSignal signal, bool entireProcessGroup) + { + // If entireProcessGroup is true, send to -pid (negative pid), don't use pidfd. + int pidfd = entireProcessGroup ? NoPidFd : (int)this.handle; + int pid = entireProcessGroup ? -ProcessId : ProcessId; + int result = Interop.Sys.SendSignal(pidfd, pid, signal); + + if (result == 0) + { + return; + } + + int errno = Marshal.GetLastPInvokeError(); + throw new Win32Exception(errno); + } } } 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 1ad9beb82fb3f6..710f2ff5254ec9 100644 --- a/src/libraries/System.Diagnostics.Process/src/System.Diagnostics.Process.csproj +++ b/src/libraries/System.Diagnostics.Process/src/System.Diagnostics.Process.csproj @@ -266,6 +266,10 @@ Link="Common\Interop\Unix\Interop.SysConf.cs" /> + + Converts the environment variables information from a ProcessStartInfo into an envp array. /// The ProcessStartInfo. /// The envp array. - private static string[] CreateEnvp(ProcessStartInfo psi) - { - var envp = new string[psi.Environment.Count]; - int index = 0; - foreach (KeyValuePair pair in psi.Environment) - { - // Ignore null values for consistency with Environment.SetEnvironmentVariable - if (pair.Value != null) - { - envp[index++] = pair.Key + "=" + pair.Value; - } - } - // Resize the array in case we skipped some entries - Array.Resize(ref envp, index); - return envp; - } + private static string[] CreateEnvp(ProcessStartInfo psi) => ProcessUtils.CreateEnvp(psi.Environment); private static string? ResolveExecutableForShellExecute(string filename, string? workingDirectory) { diff --git a/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/ProcessUtils.Unix.cs b/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/ProcessUtils.Unix.cs index eb4c06a033566e..53e838c31d5546 100644 --- a/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/ProcessUtils.Unix.cs +++ b/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/ProcessUtils.Unix.cs @@ -1,12 +1,76 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System.Collections.Generic; +using System.Diagnostics; using System.IO; +using System.Runtime.InteropServices; +using System.Text; namespace System.Diagnostics { internal static partial class ProcessUtils { + /// Converts the environment variables information into an envp array. + internal static string[] CreateEnvp(IDictionary environment) + { + var envp = new string[environment.Count]; + int index = 0; + foreach (KeyValuePair pair in environment) + { + // Ignore null values for consistency with Environment.SetEnvironmentVariable + if (pair.Value is not null) + { + envp[index++] = pair.Key + "=" + pair.Value; + } + } + // Resize the array in case we skipped some entries + Array.Resize(ref envp, index); + return envp; + } + + internal static unsafe void AllocNullTerminatedArray(string[] arr, ref byte** arrPtr) + { + nuint arrLength = (nuint)arr.Length + 1; // +1 is for null termination + + // Allocate the unmanaged array to hold each string pointer. + // It needs to have an extra element to null terminate the array. + // Zero the memory so that if any of the individual string allocations fails, + // we can loop through the array to free any that succeeded. + // The last element will remain null. + arrPtr = (byte**)NativeMemory.AllocZeroed(arrLength, (nuint)sizeof(byte*)); + + // Now copy each string to unmanaged memory referenced from the array. + // We need the data to be an unmanaged, null-terminated array of UTF8-encoded bytes. + for (int i = 0; i < arr.Length; i++) + { + string str = arr[i]; + + int byteLength = Encoding.UTF8.GetByteCount(str); + arrPtr[i] = (byte*)NativeMemory.Alloc((nuint)byteLength + 1); //+1 for null termination + + int bytesWritten = Encoding.UTF8.GetBytes(str, new Span(arrPtr[i], byteLength)); + Debug.Assert(bytesWritten == byteLength); + + arrPtr[i][bytesWritten] = (byte)'\0'; // null terminate + } + } + + internal static unsafe void FreeArray(byte** arr, int length) + { + if (arr != null) + { + // Free each element of the array + for (int i = 0; i < length; i++) + { + NativeMemory.Free(arr[i]); + } + + // And then the array itself + NativeMemory.Free(arr); + } + } + private static bool IsExecutable(string fullPath) { Interop.Sys.FileStatus fileinfo; diff --git a/src/libraries/System.Diagnostics.Process/tests/SafeProcessHandleTests.Unix.cs b/src/libraries/System.Diagnostics.Process/tests/SafeProcessHandleTests.Unix.cs new file mode 100644 index 00000000000000..b14565521e1da5 --- /dev/null +++ b/src/libraries/System.Diagnostics.Process/tests/SafeProcessHandleTests.Unix.cs @@ -0,0 +1,81 @@ +// 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.IO; +using System.Runtime.InteropServices; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Win32.SafeHandles; +using Xunit; + +namespace System.Diagnostics.Tests +{ + [PlatformSpecific(TestPlatforms.OSX)] + public partial class SafeProcessHandleTests + { + [Fact] + public static void SendSignal_SIGTERM_TerminatesProcess() + { + ProcessStartOptions options = new("sleep") { Arguments = { "60" } }; + + using SafeProcessHandle processHandle = SafeProcessHandle.Start(options, input: null, output: null, error: null); + + processHandle.Signal(PosixSignal.SIGTERM); + + ProcessExitStatus exitStatus = processHandle.WaitForExitOrKillOnTimeout(TimeSpan.FromSeconds(5)); + + Assert.Equal(PosixSignal.SIGTERM, exitStatus.Signal); + Assert.True(exitStatus.ExitCode > 128, $"Exit code {exitStatus.ExitCode} should indicate signal termination (>128)"); + } + + [Fact] + public static void SendSignal_SIGINT_TerminatesProcess() + { + ProcessStartOptions options = new("sleep") { Arguments = { "60" } }; + + using SafeProcessHandle processHandle = SafeProcessHandle.Start(options, input: null, output: null, error: null); + + processHandle.Signal(PosixSignal.SIGINT); + + ProcessExitStatus exitStatus = processHandle.WaitForExitOrKillOnTimeout(TimeSpan.FromSeconds(5)); + + Assert.Equal(PosixSignal.SIGINT, exitStatus.Signal); + Assert.True(exitStatus.ExitCode > 128, $"Exit code {exitStatus.ExitCode} should indicate signal termination (>128)"); + } + + [Fact] + public static void Signal_InvalidSignal_ThrowsArgumentOutOfRangeException() + { + ProcessStartOptions options = new("sleep") { Arguments = { "1" } }; + + using SafeProcessHandle processHandle = SafeProcessHandle.Start(options, input: null, output: null, error: null); + + PosixSignal invalidSignal = (PosixSignal)100; + + Assert.Throws(() => processHandle.Signal(invalidSignal)); + + processHandle.WaitForExit(); + } + + [Fact] + public static void SendSignal_ToExitedProcess_ThrowsWin32Exception() + { + ProcessStartOptions options = new("echo") { Arguments = { "test" } }; + + using SafeFileHandle nullHandle = File.OpenNullHandle(); + using SafeProcessHandle processHandle = SafeProcessHandle.Start( + options, + input: null, + output: nullHandle, + error: nullHandle); + + processHandle.WaitForExit(); + + Win32Exception exception = Assert.Throws(() => processHandle.Signal(PosixSignal.SIGTERM)); + + // ESRCH error code is 3 on Unix systems + Assert.Equal(3, exception.NativeErrorCode); + } + } +} diff --git a/src/libraries/System.Diagnostics.Process/tests/SafeProcessHandleTests.cs b/src/libraries/System.Diagnostics.Process/tests/SafeProcessHandleTests.cs index 9bb10cc3ab2f14..4da9570e5fac87 100644 --- a/src/libraries/System.Diagnostics.Process/tests/SafeProcessHandleTests.cs +++ b/src/libraries/System.Diagnostics.Process/tests/SafeProcessHandleTests.cs @@ -11,17 +11,27 @@ namespace System.Diagnostics.Tests { - [PlatformSpecific(TestPlatforms.Windows)] + [PlatformSpecific(TestPlatforms.OSX | TestPlatforms.Windows)] public partial class SafeProcessHandleTests { - private static ProcessStartOptions CreateTenSecondSleep() => PlatformDetection.IsWindowsNanoServer - ? new("ping") { Arguments = { "127.0.0.1", "-n", "11" } } - : new("powershell") { Arguments = { "-InputFormat", "None", "-Command", "Start-Sleep 10" } }; + private static ProcessStartOptions CreateTenSecondSleep() + { + if (OperatingSystem.IsWindows()) + { + return PlatformDetection.IsWindowsNanoServer + ? new("ping") { Arguments = { "127.0.0.1", "-n", "11" } } + : new("powershell") { Arguments = { "-InputFormat", "None", "-Command", "Start-Sleep 10" } }; + } + + return new("sleep") { Arguments = { "60" } }; + } [Fact] public static void Start_WithNoArguments_Succeeds() { - ProcessStartOptions options = new("hostname"); + ProcessStartOptions options = OperatingSystem.IsWindows() + ? new("hostname") + : new("echo") { Arguments = { "test" } }; using SafeProcessHandle processHandle = SafeProcessHandle.Start(options, input: null, output: null, error: null); @@ -41,7 +51,14 @@ public static void Kill_KillsRunningProcess() ProcessExitStatus exitStatus = processHandle.WaitForExitOrKillOnTimeout(TimeSpan.FromSeconds(5)); Assert.False(exitStatus.Canceled); - Assert.Equal(-1, exitStatus.ExitCode); + if (OperatingSystem.IsWindows()) + { + Assert.Equal(-1, exitStatus.ExitCode); + } + else + { + Assert.NotEqual(0, exitStatus.ExitCode); + } } [Fact] @@ -77,7 +94,9 @@ public static void WaitForExit_Called_After_Kill_ReturnsExitCodeImmediately() [Fact] public static void Kill_OnAlreadyExitedProcess_ReturnsFalse() { - ProcessStartOptions options = new("cmd.exe") { Arguments = { "/c", "echo test" } }; + ProcessStartOptions options = OperatingSystem.IsWindows() + ? new("cmd.exe") { Arguments = { "/c", "echo test" } } + : new("echo") { Arguments = { "test" } }; using SafeProcessHandle processHandle = SafeProcessHandle.Start(options, input: null, output: null, error: null); @@ -104,7 +123,9 @@ public static void WaitForExitOrKillOnTimeout_KillsOnTimeout() [Fact] public static void WaitForExit_WaitsIndefinitelyForProcessToComplete() { - ProcessStartOptions options = new("cmd.exe") { Arguments = { "/c", "echo test" } }; + ProcessStartOptions options = OperatingSystem.IsWindows() + ? new("cmd.exe") { Arguments = { "/c", "echo test" } } + : new("echo") { Arguments = { "test" } }; using SafeProcessHandle processHandle = SafeProcessHandle.Start(options, input: null, output: null, error: null); @@ -118,7 +139,9 @@ public static void WaitForExit_WaitsIndefinitelyForProcessToComplete() [Fact] public static void TryWaitForExit_ReturnsTrueWhenProcessExitsBeforeTimeout() { - ProcessStartOptions options = new("cmd.exe") { Arguments = { "/c", "echo test" } }; + ProcessStartOptions options = OperatingSystem.IsWindows() + ? new("cmd.exe") { Arguments = { "/c", "echo test" } } + : new("echo") { Arguments = { "test" } }; using SafeProcessHandle processHandle = SafeProcessHandle.Start(options, input: null, output: null, error: null); @@ -156,7 +179,9 @@ public static void TryWaitForExit_ReturnsFalseWhenProcessDoesNotExitBeforeTimeou [Fact] public static void WaitForExitOrKillOnTimeout_DoesNotKillWhenProcessExitsBeforeTimeout() { - ProcessStartOptions options = new("cmd.exe") { Arguments = { "/c", "echo test" } }; + ProcessStartOptions options = OperatingSystem.IsWindows() + ? new("cmd.exe") { Arguments = { "/c", "echo test" } } + : new("echo") { Arguments = { "test" } }; using SafeProcessHandle processHandle = SafeProcessHandle.Start(options, input: null, output: null, error: null); @@ -179,7 +204,10 @@ public static void WaitForExitOrKillOnTimeout_KillsAndWaitsWhenTimeoutOccurs() Assert.InRange(stopwatch.Elapsed, TimeSpan.FromMilliseconds(270), TimeSpan.FromSeconds(2)); Assert.True(exitStatus.Canceled, "Process should be marked as canceled when killed due to timeout"); Assert.NotEqual(0, exitStatus.ExitCode); - Assert.Equal(-1, exitStatus.ExitCode); + if (OperatingSystem.IsWindows()) + { + Assert.Equal(-1, exitStatus.ExitCode); + } } [Fact] @@ -226,7 +254,9 @@ await Assert.ThrowsAnyAsync(async () => [Fact] public static async Task WaitForExitAsync_CompletesNormallyWhenProcessExits() { - ProcessStartOptions options = new("cmd.exe") { Arguments = { "/c", "echo test" } }; + ProcessStartOptions options = OperatingSystem.IsWindows() + ? new("cmd.exe") { Arguments = { "/c", "echo test" } } + : new("echo") { Arguments = { "test" } }; using SafeProcessHandle processHandle = SafeProcessHandle.Start(options, input: null, output: null, error: null); @@ -241,7 +271,9 @@ public static async Task WaitForExitAsync_CompletesNormallyWhenProcessExits() [Fact] public static async Task WaitForExitAsync_WithoutCancellationToken_CompletesNormally() { - ProcessStartOptions options = new("cmd.exe") { Arguments = { "/c", "echo test" } }; + ProcessStartOptions options = OperatingSystem.IsWindows() + ? new("cmd.exe") { Arguments = { "/c", "echo test" } } + : new("echo") { Arguments = { "test" } }; using SafeProcessHandle processHandle = SafeProcessHandle.Start(options, input: null, output: null, error: null); @@ -255,7 +287,9 @@ public static async Task WaitForExitAsync_WithoutCancellationToken_CompletesNorm [Fact] public static async Task WaitForExitOrKillOnCancellationAsync_CompletesNormallyWhenProcessExits() { - ProcessStartOptions options = new("cmd.exe") { Arguments = { "/c", "echo test" } }; + ProcessStartOptions options = OperatingSystem.IsWindows() + ? new("cmd.exe") { Arguments = { "/c", "echo test" } } + : new("echo") { Arguments = { "test" } }; using SafeProcessHandle processHandle = SafeProcessHandle.Start(options, input: null, output: null, error: null); @@ -270,7 +304,9 @@ public static async Task WaitForExitOrKillOnCancellationAsync_CompletesNormallyW [Fact] public static void KillOnParentExit_CanBeSetToTrue() { - ProcessStartOptions options = new("cmd.exe") { Arguments = { "/c", "echo test" }, KillOnParentExit = true }; + ProcessStartOptions options = OperatingSystem.IsWindows() + ? new("cmd.exe") { Arguments = { "/c", "echo test" }, KillOnParentExit = true } + : new("echo") { Arguments = { "test" }, KillOnParentExit = true }; Assert.True(options.KillOnParentExit); @@ -283,7 +319,9 @@ public static void KillOnParentExit_CanBeSetToTrue() [Fact] public static void KillOnParentExit_DefaultsToFalse() { - ProcessStartOptions options = new("cmd.exe") { Arguments = { "/c", "echo test" } }; + ProcessStartOptions options = OperatingSystem.IsWindows() + ? new("cmd.exe") { Arguments = { "/c", "echo test" } } + : new("echo") { Arguments = { "test" } }; Assert.False(options.KillOnParentExit); } diff --git a/src/libraries/System.Diagnostics.Process/tests/System.Diagnostics.Process.Tests.csproj b/src/libraries/System.Diagnostics.Process/tests/System.Diagnostics.Process.Tests.csproj index c64461f79c39ea..7434ec01884ee7 100644 --- a/src/libraries/System.Diagnostics.Process/tests/System.Diagnostics.Process.Tests.csproj +++ b/src/libraries/System.Diagnostics.Process/tests/System.Diagnostics.Process.Tests.csproj @@ -67,6 +67,7 @@ + 10.5 diff --git a/src/native/libs/System.Native/entrypoints.c b/src/native/libs/System.Native/entrypoints.c index 8414814970ea5c..80d4cd75e86df6 100644 --- a/src/native/libs/System.Native/entrypoints.c +++ b/src/native/libs/System.Native/entrypoints.c @@ -225,6 +225,14 @@ static const Entry s_sysNative[] = DllImportEntry(SystemNative_SchedSetAffinity) DllImportEntry(SystemNative_SchedGetAffinity) DllImportEntry(SystemNative_GetProcessPath) + DllImportEntry(SystemNative_SpawnProcess) + DllImportEntry(SystemNative_SendSignal) + DllImportEntry(SystemNative_WaitForExitAndReap) + DllImportEntry(SystemNative_TryWaitForExit) + DllImportEntry(SystemNative_TryWaitForExitCancellable) + DllImportEntry(SystemNative_WaitForExitOrKillOnTimeout) + DllImportEntry(SystemNative_TryGetExitCode) + DllImportEntry(SystemNative_OpenProcess) DllImportEntry(SystemNative_GetNonCryptographicallySecureRandomBytes) DllImportEntry(SystemNative_GetCryptographicallySecureRandomBytes) DllImportEntry(SystemNative_GetUnixRelease) diff --git a/src/native/libs/System.Native/pal_process.c b/src/native/libs/System.Native/pal_process.c index 845c4ed3293622..c2d5e6705a665a 100644 --- a/src/native/libs/System.Native/pal_process.c +++ b/src/native/libs/System.Native/pal_process.c @@ -20,10 +20,11 @@ #if HAVE_CRT_EXTERNS_H #include #endif -#if HAVE_PIPE2 #include -#endif #include +#include +#include +#include #if HAVE_SCHED_SETAFFINITY || HAVE_SCHED_GETAFFINITY #include @@ -39,6 +40,14 @@ #include #endif +#if HAVE_SYS_EVENT_H +#include +#endif + +#if HAVE_POSIX_SPAWN +#include +#endif + #include // Validate that our SysLogPriority values are correct for the platform @@ -902,3 +911,499 @@ char* SystemNative_GetProcessPath(void) { return minipal_getexepath(); } + +// Map managed PosixSignal enum values to native signal numbers +static int map_managed_signal_to_native(int managed_signal) +{ + switch (managed_signal) + { + case -1: return SIGHUP; + case -2: return SIGINT; + case -3: return SIGQUIT; + case -4: return SIGTERM; + case -5: return SIGCHLD; + case -6: return SIGCONT; + case -7: return SIGWINCH; + case -8: return SIGTTIN; + case -9: return SIGTTOU; + case -10: return SIGTSTP; + case -11: return SIGKILL; + case -12: return SIGSTOP; + default: return 0; + } +} + +static int map_native_signal_to_managed(int native_signal) +{ + switch (native_signal) + { + case SIGHUP: return -1; + case SIGINT: return -2; + case SIGQUIT: return -3; + case SIGTERM: return -4; + case SIGCHLD: return -5; + case SIGCONT: return -6; + case SIGWINCH: return -7; + case SIGTTIN: return -8; + case SIGTTOU: return -9; + case SIGTSTP: return -10; + case SIGKILL: return -11; + case SIGSTOP: return -12; + default: return 0; + } +} + +int32_t SystemNative_SpawnProcess( + const char* path, + char* const argv[], + char* const envp[], + int32_t stdin_fd, + int32_t stdout_fd, + int32_t stderr_fd, + const char* working_dir, + int32_t* out_pid, + int32_t* out_pidfd, + int32_t kill_on_parent_death, + int32_t create_suspended, + int32_t create_new_process_group, + int32_t detached, + const int32_t* inherited_handles, + int32_t inherited_handles_count) +{ +#if HAVE_POSIX_SPAWN && HAVE_POSIX_SPAWN_CLOEXEC_DEFAULT && HAVE_POSIX_SPAWN_FILE_ACTIONS_ADDINHERIT_NP + // ========== POSIX_SPAWN PATH (macOS) ========== + +#if !HAVE_POSIX_SPAWN_START_SUSPENDED + if (create_suspended) + { + errno = ENOTSUP; + return -1; + } +#endif + +#ifndef POSIX_SPAWN_SETSID + if (detached) + { + errno = ENOTSUP; + return -1; + } +#endif + + pid_t child_pid; + posix_spawn_file_actions_t file_actions; + posix_spawnattr_t attr; + int result; + + if ((result = posix_spawnattr_init(&attr)) != 0) + { + errno = result; + return -1; + } + + short flags = POSIX_SPAWN_CLOEXEC_DEFAULT | POSIX_SPAWN_SETSIGDEF; +#if HAVE_POSIX_SPAWN_START_SUSPENDED + if (create_suspended) + { + flags |= POSIX_SPAWN_START_SUSPENDED; + } +#endif + if (detached) + { +#ifdef POSIX_SPAWN_SETSID + flags |= POSIX_SPAWN_SETSID; +#endif + } + if (create_new_process_group) + { + flags |= POSIX_SPAWN_SETPGROUP; + } + if ((result = posix_spawnattr_setflags(&attr, flags)) != 0) + { + int saved_errno = result; + posix_spawnattr_destroy(&attr); + errno = saved_errno; + return -1; + } + + if (create_new_process_group) + { + if ((result = posix_spawnattr_setpgroup(&attr, 0)) != 0) + { + int saved_errno = result; + posix_spawnattr_destroy(&attr); + errno = saved_errno; + return -1; + } + } + + // Reset all signal handlers to default + sigset_t all_signals; + sigfillset(&all_signals); + if ((result = posix_spawnattr_setsigdefault(&attr, &all_signals)) != 0) + { + int saved_errno = result; + posix_spawnattr_destroy(&attr); + errno = saved_errno; + return -1; + } + + if ((result = posix_spawn_file_actions_init(&file_actions)) != 0) + { + int saved_errno = result; + posix_spawnattr_destroy(&attr); + errno = saved_errno; + return -1; + } + + // Redirect stdin/stdout/stderr + if ((result = posix_spawn_file_actions_adddup2(&file_actions, stdin_fd, 0)) != 0 + || (result = posix_spawn_file_actions_adddup2(&file_actions, stdout_fd, 1)) != 0 + || (result = posix_spawn_file_actions_adddup2(&file_actions, stderr_fd, 2)) != 0) + { + int saved_errno = result; + posix_spawn_file_actions_destroy(&file_actions); + posix_spawnattr_destroy(&attr); + errno = saved_errno; + return -1; + } + + // Add user-provided inherited handles + if (inherited_handles != NULL && inherited_handles_count > 0) + { + for (int i = 0; i < inherited_handles_count; i++) + { + int fd = inherited_handles[i]; + if (fd != 0 && fd != 1 && fd != 2) + { + if ((result = posix_spawn_file_actions_addinherit_np(&file_actions, fd)) != 0) + { + int saved_errno = result; + posix_spawn_file_actions_destroy(&file_actions); + posix_spawnattr_destroy(&attr); + errno = saved_errno; + return -1; + } + } + } + } + + // Change working directory if specified + if (working_dir != NULL) + { +#if HAVE_POSIX_SPAWN_FILE_ACTIONS_ADDCHDIR_NP + if ((result = posix_spawn_file_actions_addchdir_np(&file_actions, working_dir)) != 0) + { + int saved_errno = result; + posix_spawn_file_actions_destroy(&file_actions); + posix_spawnattr_destroy(&attr); + errno = saved_errno; + return -1; + } +#else + posix_spawn_file_actions_destroy(&file_actions); + posix_spawnattr_destroy(&attr); + errno = ENOTSUP; + return -1; +#endif + } + + // Spawn the process + // If envp is NULL, use the current environment + char* const* env; + if (envp != NULL) + { + env = envp; + } + else + { +#if HAVE_CRT_EXTERNS_H + env = *_NSGetEnviron(); +#else + extern char **environ; + env = environ; +#endif + } + result = posix_spawn(&child_pid, path, &file_actions, &attr, argv, env); + + posix_spawn_file_actions_destroy(&file_actions); + posix_spawnattr_destroy(&attr); + + if (result != 0) + { + errno = result; + return -1; + } + + // kill_on_parent_death is not supported with posix_spawn on macOS + (void)kill_on_parent_death; + + if (out_pid != NULL) + { + *out_pid = child_pid; + } + if (out_pidfd != NULL) + { + *out_pidfd = -1; // pidfd not supported on macOS + } + return 0; +#else + // posix_spawn with required features not available + (void)path; + (void)argv; + (void)envp; + (void)stdin_fd; + (void)stdout_fd; + (void)stderr_fd; + (void)working_dir; + (void)out_pid; + (void)out_pidfd; + (void)kill_on_parent_death; + (void)create_suspended; + (void)create_new_process_group; + (void)detached; + (void)inherited_handles; + (void)inherited_handles_count; + errno = ENOTSUP; + return -1; +#endif +} + +int32_t SystemNative_SendSignal(int32_t pidfd, int32_t pid, int32_t managed_signal) +{ + int native_signal = map_managed_signal_to_native(managed_signal); + if (native_signal == 0) + { + errno = EINVAL; + return -1; + } + + (void)pidfd; + return kill(pid, native_signal); +} + +static int map_wait_status(int status, int32_t* out_exitCode, int32_t* out_signal) +{ + if (WIFEXITED(status)) + { + *out_exitCode = WEXITSTATUS(status); + *out_signal = 0; + return 0; + } + else if (WIFSIGNALED(status)) + { + int sig = WTERMSIG(status); + *out_exitCode = 128 + sig; + *out_signal = map_native_signal_to_managed(sig); + return 0; + } + return -1; +} + +int32_t SystemNative_TryGetExitCode(int32_t pidfd, int32_t pid, int32_t* out_exitCode, int32_t* out_signal) +{ + (void)pidfd; + int status; + int ret; + while ((ret = waitpid(pid, &status, WNOHANG)) < 0 && errno == EINTR); + + if (ret > 0) + { + return map_wait_status(status, out_exitCode, out_signal); + } + return -1; +} + +int32_t SystemNative_WaitForExitAndReap(int32_t pidfd, int32_t pid, int32_t* out_exitCode, int32_t* out_signal) +{ + (void)pidfd; + int status; + int ret; + while ((ret = waitpid(pid, &status, 0)) < 0 && errno == EINTR); + + if (ret != -1) + { + return map_wait_status(status, out_exitCode, out_signal); + } + return -1; +} + +#if HAVE_KQUEUE +static int create_kqueue_cloexec(void) +{ + int queue = kqueue(); + if (queue == -1) + { + return -1; + } + + if (fcntl(queue, F_SETFD, FD_CLOEXEC) == -1) + { + int saved_errno = errno; + close(queue); + errno = saved_errno; + return -1; + } + + return queue; +} +#endif + +int32_t SystemNative_TryWaitForExitCancellable(int32_t pidfd, int32_t pid, int32_t cancelPipeFd, int32_t* out_exitCode, int32_t* out_signal) +{ + int ret; +#if HAVE_KQUEUE + int queue = create_kqueue_cloexec(); + if (queue == -1) + { + return -1; + } + + struct kevent change_list[2]; + memset(change_list, 0, sizeof(change_list)); + + change_list[0].ident = (uintptr_t)pid; + change_list[0].filter = EVFILT_PROC; + change_list[0].fflags = NOTE_EXIT; + change_list[0].flags = EV_ADD | EV_CLEAR; + + change_list[1].ident = (uintptr_t)cancelPipeFd; + change_list[1].filter = EVFILT_READ; + change_list[1].flags = EV_ADD | EV_CLEAR; + + struct kevent event_list; + memset(&event_list, 0, sizeof(event_list)); + + while ((ret = kevent(queue, change_list, 2, &event_list, 1, NULL)) < 0 && errno == EINTR); + + if (ret < 0) + { + int saved_errno = errno; + close(queue); + + if (saved_errno == ESRCH && SystemNative_TryGetExitCode(pidfd, pid, out_exitCode, out_signal) != -1) + { + return 0; + } + errno = saved_errno; + return -1; + } + + close(queue); + + if (event_list.filter == EVFILT_READ && event_list.ident == (uintptr_t)cancelPipeFd) + { + return 1; // Cancellation requested + } +#else + (void)pidfd; + (void)pid; + (void)cancelPipeFd; + (void)out_exitCode; + (void)out_signal; + errno = ENOTSUP; + return -1; +#endif + + return SystemNative_WaitForExitAndReap(pidfd, pid, out_exitCode, out_signal); +} + +int32_t SystemNative_TryWaitForExit(int32_t pidfd, int32_t pid, int32_t timeout_ms, int32_t* out_exitCode, int32_t* out_signal) +{ + int ret; +#if HAVE_KQUEUE + int queue = create_kqueue_cloexec(); + if (queue == -1) + { + return -1; + } + + struct kevent change_list; + memset(&change_list, 0, sizeof(change_list)); + change_list.ident = (uintptr_t)pid; + change_list.filter = EVFILT_PROC; + change_list.fflags = NOTE_EXIT; + change_list.flags = EV_ADD | EV_CLEAR; + + struct kevent event_list; + memset(&event_list, 0, sizeof(event_list)); + + struct timespec timeout; + timeout.tv_sec = timeout_ms / 1000; + timeout.tv_nsec = (timeout_ms % 1000) * 1000 * 1000; + + while ((ret = kevent(queue, &change_list, 1, &event_list, 1, &timeout)) < 0 && errno == EINTR); + + if (ret < 0) + { + int saved_errno = errno; + close(queue); + + if (saved_errno == ESRCH && SystemNative_TryGetExitCode(pidfd, pid, out_exitCode, out_signal) != -1) + { + return 0; + } + errno = saved_errno; + return -1; + } + + close(queue); +#else + (void)pidfd; + (void)pid; + (void)timeout_ms; + (void)out_exitCode; + (void)out_signal; + errno = ENOTSUP; + return -1; +#endif + + if (ret == 0) + { + return 1; // Timeout + } + + return SystemNative_WaitForExitAndReap(pidfd, pid, out_exitCode, out_signal); +} + +int32_t SystemNative_WaitForExitOrKillOnTimeout(int32_t pidfd, int32_t pid, int32_t timeout_ms, int32_t* out_exitCode, int32_t* out_signal, int32_t* out_timeout) +{ + *out_timeout = 0; + int ret = SystemNative_TryWaitForExit(pidfd, pid, timeout_ms, out_exitCode, out_signal); + if (ret != 1) + { + return ret; // Either process exited (0) or error occurred (-1) + } + + *out_timeout = 1; + ret = SystemNative_SendSignal(pidfd, pid, map_native_signal_to_managed(SIGKILL)); + + if (ret == -1) + { + if (errno == ESRCH) + { + *out_timeout = 0; + } + else + { + return -1; + } + } + + return SystemNative_WaitForExitAndReap(pidfd, pid, out_exitCode, out_signal); +} + +int32_t SystemNative_OpenProcess(int32_t pid, int32_t* out_pidfd) +{ + *out_pidfd = -1; + + siginfo_t info; + memset(&info, 0, sizeof(info)); + int waitid_ret = waitid(P_PID, (id_t)pid, &info, WNOHANG | WNOWAIT | WEXITED | WSTOPPED | WCONTINUED); + + if (waitid_ret != 0) + { + return -1; + } + + return 0; +} diff --git a/src/native/libs/System.Native/pal_process.h b/src/native/libs/System.Native/pal_process.h index 6ef66da7acf84f..fa4eab0f0b3abe 100644 --- a/src/native/libs/System.Native/pal_process.h +++ b/src/native/libs/System.Native/pal_process.h @@ -244,3 +244,74 @@ PALEXPORT int32_t SystemNative_SchedGetAffinity(int32_t pid, intptr_t* mask); * resolving symbolic links. The caller is responsible for releasing the buffer. */ PALEXPORT char* SystemNative_GetProcessPath(void); + +/** + * Spawns a new process. + * + * Returns 0 on success, -1 on error (errno is set). + */ +PALEXPORT int32_t SystemNative_SpawnProcess( + const char* path, + char* const argv[], + char* const envp[], + int32_t stdin_fd, + int32_t stdout_fd, + int32_t stderr_fd, + const char* working_dir, + int32_t* out_pid, + int32_t* out_pidfd, + int32_t kill_on_parent_death, + int32_t create_suspended, + int32_t create_new_process_group, + int32_t detached, + const int32_t* inherited_handles, + int32_t inherited_handles_count); + +/** + * Sends a signal to a process. + * + * Returns 0 on success, -1 on error (errno is set). + */ +PALEXPORT int32_t SystemNative_SendSignal(int32_t pidfd, int32_t pid, int32_t managed_signal); + +/** + * Waits for a process to exit and reaps it. + * + * Returns 0 on success, -1 on error (errno is set). + */ +PALEXPORT int32_t SystemNative_WaitForExitAndReap(int32_t pidfd, int32_t pid, int32_t* out_exitCode, int32_t* out_signal); + +/** + * Tries to wait for a process to exit with a timeout. + * + * Returns 0 if process exited, 1 on timeout, -1 on error (errno is set). + */ +PALEXPORT int32_t SystemNative_TryWaitForExit(int32_t pidfd, int32_t pid, int32_t timeout_ms, int32_t* out_exitCode, int32_t* out_signal); + +/** + * Tries to wait for a process to exit with cancellation support. + * + * Returns 0 if process exited, 1 on cancellation, -1 on error (errno is set). + */ +PALEXPORT int32_t SystemNative_TryWaitForExitCancellable(int32_t pidfd, int32_t pid, int32_t cancelPipeFd, int32_t* out_exitCode, int32_t* out_signal); + +/** + * Waits for a process to exit or kills it on timeout. + * + * Returns 0 on success, -1 on error (errno is set). + */ +PALEXPORT int32_t SystemNative_WaitForExitOrKillOnTimeout(int32_t pidfd, int32_t pid, int32_t timeout_ms, int32_t* out_exitCode, int32_t* out_signal, int32_t* out_timeout); + +/** + * Tries to get the exit code of a process without blocking. + * + * Returns 0 if exit code is available, -1 if process is still running or error. + */ +PALEXPORT int32_t SystemNative_TryGetExitCode(int32_t pidfd, int32_t pid, int32_t* out_exitCode, int32_t* out_signal); + +/** + * Opens an existing child process by its process ID. + * + * Returns 0 on success, -1 on error (errno is set). + */ +PALEXPORT int32_t SystemNative_OpenProcess(int32_t pid, int32_t* out_pidfd); diff --git a/src/native/libs/System.Native/pal_process_wasi.c b/src/native/libs/System.Native/pal_process_wasi.c index 569a0066eec69e..2251311e94a556 100644 --- a/src/native/libs/System.Native/pal_process_wasi.c +++ b/src/native/libs/System.Native/pal_process_wasi.c @@ -125,3 +125,55 @@ char* SystemNative_GetProcessPath(void) { return minipal_getexepath(); } + +int32_t SystemNative_SpawnProcess(const char* path, char* const argv[], char* const envp[], + int32_t stdin_fd, int32_t stdout_fd, int32_t stderr_fd, const char* working_dir, + int32_t* out_pid, int32_t* out_pidfd, int32_t kill_on_parent_death, int32_t create_suspended, + int32_t create_new_process_group, int32_t detached, const int32_t* inherited_handles, + int32_t inherited_handles_count) +{ + errno = ENOTSUP; + return -1; +} + +int32_t SystemNative_SendSignal(int32_t pidfd, int32_t pid, int32_t managed_signal) +{ + errno = ENOTSUP; + return -1; +} + +int32_t SystemNative_WaitForExitAndReap(int32_t pidfd, int32_t pid, int32_t* out_exitCode, int32_t* out_signal) +{ + errno = ENOTSUP; + return -1; +} + +int32_t SystemNative_TryWaitForExit(int32_t pidfd, int32_t pid, int32_t timeout_ms, int32_t* out_exitCode, int32_t* out_signal) +{ + errno = ENOTSUP; + return -1; +} + +int32_t SystemNative_TryWaitForExitCancellable(int32_t pidfd, int32_t pid, int32_t cancelPipeFd, int32_t* out_exitCode, int32_t* out_signal) +{ + errno = ENOTSUP; + return -1; +} + +int32_t SystemNative_WaitForExitOrKillOnTimeout(int32_t pidfd, int32_t pid, int32_t timeout_ms, int32_t* out_exitCode, int32_t* out_signal, int32_t* out_timeout) +{ + errno = ENOTSUP; + return -1; +} + +int32_t SystemNative_TryGetExitCode(int32_t pidfd, int32_t pid, int32_t* out_exitCode, int32_t* out_signal) +{ + errno = ENOTSUP; + return -1; +} + +int32_t SystemNative_OpenProcess(int32_t pid, int32_t* out_pidfd) +{ + errno = ENOTSUP; + return -1; +} diff --git a/src/native/libs/configure.cmake b/src/native/libs/configure.cmake index 4da74e115c6db8..f39f2a86d7d417 100644 --- a/src/native/libs/configure.cmake +++ b/src/native/libs/configure.cmake @@ -498,6 +498,48 @@ check_symbol_exists( HAVE_KQUEUE) set(CMAKE_REQUIRED_LIBRARIES ${PREVIOUS_CMAKE_REQUIRED_LIBRARIES}) +check_include_files("sys/event.h" HAVE_SYS_EVENT_H) + +# Check for posix_spawn features (macOS) +if(CLR_CMAKE_TARGET_APPLE) + check_symbol_exists(posix_spawn "spawn.h" HAVE_POSIX_SPAWN) + + check_c_source_compiles(" + #include + int main(void) { + #ifdef POSIX_SPAWN_CLOEXEC_DEFAULT + return 0; + #else + #error POSIX_SPAWN_CLOEXEC_DEFAULT not defined + #endif + } + " HAVE_POSIX_SPAWN_CLOEXEC_DEFAULT) + + check_symbol_exists(posix_spawn_file_actions_addchdir_np "spawn.h" HAVE_POSIX_SPAWN_FILE_ACTIONS_ADDCHDIR_NP) + + check_c_source_compiles(" + #include + int main(void) { + posix_spawn_file_actions_t actions; + posix_spawn_file_actions_init(&actions); + posix_spawn_file_actions_addinherit_np(&actions, 3); + posix_spawn_file_actions_destroy(&actions); + return 0; + } + " HAVE_POSIX_SPAWN_FILE_ACTIONS_ADDINHERIT_NP) + + check_c_source_compiles(" + #include + int main(void) { + #ifdef POSIX_SPAWN_START_SUSPENDED + return 0; + #else + #error POSIX_SPAWN_START_SUSPENDED not defined + #endif + } + " HAVE_POSIX_SPAWN_START_SUSPENDED) +endif() + check_symbol_exists( disconnectx "sys/socket.h" From 0d98c0cd8ce00a7a9484b5799aea0c57e3a48f18 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 26 Feb 2026 21:56:38 +0000 Subject: [PATCH 03/21] Fix unreachable code warnings in pal_process.c Co-authored-by: adamsitnik <6011991+adamsitnik@users.noreply.github.com> --- .../SafeHandles/SafeProcessHandle.Unix.cs | 110 ++++++++---------- .../tests/SafeProcessHandleTests.Unix.cs | 6 +- src/native/libs/System.Native/pal_process.c | 20 ++-- 3 files changed, 67 insertions(+), 69 deletions(-) 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 cef2be9a4d5f7c..4cb2f078e058a4 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 @@ -194,42 +194,32 @@ private async Task WaitForExitAsyncCore(CancellationToken can { if (!cancellationToken.CanBeCanceled) { - return await Task.Run(() => WaitForExitCore(), cancellationToken).ConfigureAwait(false); + return await Task.Run(WaitForExitCore, cancellationToken).ConfigureAwait(false); } - unsafe + CreatePipe(out SafeFileHandle readHandle, out SafeFileHandle writeHandle); + + using (readHandle) + using (writeHandle) { - int* fds = stackalloc int[2]; - if (Interop.Sys.Pipe(fds, Interop.Sys.PipeFlags.O_CLOEXEC) != 0) + using CancellationTokenRegistration registration = cancellationToken.Register(static state => { - throw new Win32Exception(Marshal.GetLastPInvokeError()); - } - - SafeFileHandle readHandle = new SafeFileHandle((IntPtr)fds[Interop.Sys.ReadEndOfPipe], ownsHandle: true); - SafeFileHandle writeHandle = new SafeFileHandle((IntPtr)fds[Interop.Sys.WriteEndOfPipe], ownsHandle: true); + ((SafeFileHandle)state!).Close(); // Close the write end of the pipe to signal cancellation + }, writeHandle); - using (readHandle) - using (writeHandle) + return await Task.Run(() => { - using CancellationTokenRegistration registration = cancellationToken.Register(static state => - { - ((SafeFileHandle)state!).Close(); // Close the write end of the pipe to signal cancellation - }, writeHandle); - - return await Task.Run(() => + switch (Interop.Sys.TryWaitForExitCancellable(this, ProcessId, (int)readHandle.DangerousGetHandle(), out int exitCode, out int rawSignal)) { - switch (Interop.Sys.TryWaitForExitCancellable(this, ProcessId, (int)readHandle.DangerousGetHandle(), out int exitCode, out int rawSignal)) - { - case -1: - int errno = Marshal.GetLastPInvokeError(); - throw new Win32Exception(errno); - case 1: // canceled - throw new OperationCanceledException(cancellationToken); - default: - return new ProcessExitStatus(exitCode, false, rawSignal != 0 ? (PosixSignal)rawSignal : null); - } - }, cancellationToken).ConfigureAwait(false); - } + case -1: + int errno = Marshal.GetLastPInvokeError(); + throw new Win32Exception(errno); + case 1: // canceled + throw new OperationCanceledException(cancellationToken); + default: + return new ProcessExitStatus(exitCode, false, rawSignal != 0 ? (PosixSignal)rawSignal : null); + } + }, cancellationToken).ConfigureAwait(false); } } @@ -237,45 +227,47 @@ private async Task WaitForExitOrKillOnCancellationAsyncCore(C { if (!cancellationToken.CanBeCanceled) { - return await Task.Run(() => WaitForExitCore(), cancellationToken).ConfigureAwait(false); + return await Task.Run(WaitForExitCore, cancellationToken).ConfigureAwait(false); } - unsafe + CreatePipe(out SafeFileHandle readHandle, out SafeFileHandle writeHandle); + + using (readHandle) + using (writeHandle) { - int* fds = stackalloc int[2]; - if (Interop.Sys.Pipe(fds, Interop.Sys.PipeFlags.O_CLOEXEC) != 0) + using CancellationTokenRegistration registration = cancellationToken.Register(static state => { - throw new Win32Exception(Marshal.GetLastPInvokeError()); - } + ((SafeFileHandle)state!).Close(); // Close the write end of the pipe to signal cancellation + }, writeHandle); - SafeFileHandle readHandle = new SafeFileHandle((IntPtr)fds[Interop.Sys.ReadEndOfPipe], ownsHandle: true); - SafeFileHandle writeHandle = new SafeFileHandle((IntPtr)fds[Interop.Sys.WriteEndOfPipe], ownsHandle: true); - - using (readHandle) - using (writeHandle) + return await Task.Run(() => { - using CancellationTokenRegistration registration = cancellationToken.Register(static state => + switch (Interop.Sys.TryWaitForExitCancellable(this, ProcessId, (int)readHandle.DangerousGetHandle(), out int exitCode, out int rawSignal)) { - ((SafeFileHandle)state!).Close(); // Close the write end of the pipe to signal cancellation - }, writeHandle); + case -1: + int errno = Marshal.GetLastPInvokeError(); + throw new Win32Exception(errno); + case 1: // canceled + bool wasKilled = KillCore(throwOnError: false); + ProcessExitStatus status = WaitForExitCore(); + return new ProcessExitStatus(status.ExitCode, wasKilled, status.Signal); + default: + return new ProcessExitStatus(exitCode, false, rawSignal != 0 ? (PosixSignal)rawSignal : null); + } + }, cancellationToken).ConfigureAwait(false); + } + } - return await Task.Run(() => - { - switch (Interop.Sys.TryWaitForExitCancellable(this, ProcessId, (int)readHandle.DangerousGetHandle(), out int exitCode, out int rawSignal)) - { - case -1: - int errno = Marshal.GetLastPInvokeError(); - throw new Win32Exception(errno); - case 1: // canceled - bool wasKilled = KillCore(throwOnError: false); - ProcessExitStatus status = WaitForExitCore(); - return new ProcessExitStatus(status.ExitCode, wasKilled, status.Signal); - default: - return new ProcessExitStatus(exitCode, false, rawSignal != 0 ? (PosixSignal)rawSignal : null); - } - }, cancellationToken).ConfigureAwait(false); - } + private static unsafe void CreatePipe(out SafeFileHandle readHandle, out SafeFileHandle writeHandle) + { + int* fds = stackalloc int[2]; + if (Interop.Sys.Pipe(fds, Interop.Sys.PipeFlags.O_CLOEXEC) != 0) + { + throw new Win32Exception(Marshal.GetLastPInvokeError()); } + + readHandle = new SafeFileHandle((IntPtr)fds[Interop.Sys.ReadEndOfPipe], ownsHandle: true); + writeHandle = new SafeFileHandle((IntPtr)fds[Interop.Sys.WriteEndOfPipe], ownsHandle: true); } internal bool KillCore(bool throwOnError, bool entireProcessGroup = false) diff --git a/src/libraries/System.Diagnostics.Process/tests/SafeProcessHandleTests.Unix.cs b/src/libraries/System.Diagnostics.Process/tests/SafeProcessHandleTests.Unix.cs index b14565521e1da5..f56d0c7aa0953e 100644 --- a/src/libraries/System.Diagnostics.Process/tests/SafeProcessHandleTests.Unix.cs +++ b/src/libraries/System.Diagnostics.Process/tests/SafeProcessHandleTests.Unix.cs @@ -1,6 +1,7 @@ // 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.IO; using System.Runtime.InteropServices; @@ -11,10 +12,10 @@ namespace System.Diagnostics.Tests { - [PlatformSpecific(TestPlatforms.OSX)] public partial class SafeProcessHandleTests { [Fact] + [PlatformSpecific(TestPlatforms.OSX)] public static void SendSignal_SIGTERM_TerminatesProcess() { ProcessStartOptions options = new("sleep") { Arguments = { "60" } }; @@ -30,6 +31,7 @@ public static void SendSignal_SIGTERM_TerminatesProcess() } [Fact] + [PlatformSpecific(TestPlatforms.OSX)] public static void SendSignal_SIGINT_TerminatesProcess() { ProcessStartOptions options = new("sleep") { Arguments = { "60" } }; @@ -45,6 +47,7 @@ public static void SendSignal_SIGINT_TerminatesProcess() } [Fact] + [PlatformSpecific(TestPlatforms.OSX)] public static void Signal_InvalidSignal_ThrowsArgumentOutOfRangeException() { ProcessStartOptions options = new("sleep") { Arguments = { "1" } }; @@ -59,6 +62,7 @@ public static void Signal_InvalidSignal_ThrowsArgumentOutOfRangeException() } [Fact] + [PlatformSpecific(TestPlatforms.OSX)] public static void SendSignal_ToExitedProcess_ThrowsWin32Exception() { ProcessStartOptions options = new("echo") { Arguments = { "test" } }; diff --git a/src/native/libs/System.Native/pal_process.c b/src/native/libs/System.Native/pal_process.c index c2d5e6705a665a..68285faf5445bf 100644 --- a/src/native/libs/System.Native/pal_process.c +++ b/src/native/libs/System.Native/pal_process.c @@ -1294,7 +1294,10 @@ int32_t SystemNative_TryWaitForExitCancellable(int32_t pidfd, int32_t pid, int32 { return 1; // Cancellation requested } + + return SystemNative_WaitForExitAndReap(pidfd, pid, out_exitCode, out_signal); #else + (void)ret; (void)pidfd; (void)pid; (void)cancelPipeFd; @@ -1303,8 +1306,6 @@ int32_t SystemNative_TryWaitForExitCancellable(int32_t pidfd, int32_t pid, int32 errno = ENOTSUP; return -1; #endif - - return SystemNative_WaitForExitAndReap(pidfd, pid, out_exitCode, out_signal); } int32_t SystemNative_TryWaitForExit(int32_t pidfd, int32_t pid, int32_t timeout_ms, int32_t* out_exitCode, int32_t* out_signal) @@ -1347,7 +1348,15 @@ int32_t SystemNative_TryWaitForExit(int32_t pidfd, int32_t pid, int32_t timeout_ } close(queue); + + if (ret == 0) + { + return 1; // Timeout + } + + return SystemNative_WaitForExitAndReap(pidfd, pid, out_exitCode, out_signal); #else + (void)ret; (void)pidfd; (void)pid; (void)timeout_ms; @@ -1356,13 +1365,6 @@ int32_t SystemNative_TryWaitForExit(int32_t pidfd, int32_t pid, int32_t timeout_ errno = ENOTSUP; return -1; #endif - - if (ret == 0) - { - return 1; // Timeout - } - - return SystemNative_WaitForExitAndReap(pidfd, pid, out_exitCode, out_signal); } int32_t SystemNative_WaitForExitOrKillOnTimeout(int32_t pidfd, int32_t pid, int32_t timeout_ms, int32_t* out_exitCode, int32_t* out_signal, int32_t* out_timeout) From b42521987cc48927d816e39638e5e3a0c016e604 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 26 Feb 2026 21:59:57 +0000 Subject: [PATCH 04/21] Fix ReleaseHandle to not close handle when _releaseRef is true (old Process path) Co-authored-by: adamsitnik <6011991+adamsitnik@users.noreply.github.com> --- .../src/Microsoft/Win32/SafeHandles/SafeProcessHandle.Unix.cs | 1 + 1 file changed, 1 insertion(+) 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 4cb2f078e058a4..89579160b0d636 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 @@ -47,6 +47,7 @@ protected override bool ReleaseHandle() { Debug.Assert(_handle is not null); _handle.DangerousRelease(); + return true; } return (int)handle switch From 28845d4656097c7cd4a7b4cf3d67039439d89fa1 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 27 Feb 2026 10:26:17 +0000 Subject: [PATCH 05/21] Address review feedback: remove detached param, simplify Win32Exception throws, update tests, add CI branch trigger Co-authored-by: adamsitnik <6011991+adamsitnik@users.noreply.github.com> --- eng/pipelines/runtime.yml | 1 + .../System.Native/Interop.SpawnProcess.cs | 1 - .../SafeHandles/SafeProcessHandle.Unix.cs | 30 +++++++------------ .../src/System/Diagnostics/Process.Unix.cs | 7 +---- .../tests/SafeProcessHandleTests.Unix.cs | 13 +++----- .../tests/SafeProcessHandleTests.cs | 23 +++++++++----- src/native/libs/System.Native/pal_process.c | 16 ---------- src/native/libs/System.Native/pal_process.h | 1 - .../libs/System.Native/pal_process_wasi.c | 2 +- 9 files changed, 33 insertions(+), 61 deletions(-) diff --git a/eng/pipelines/runtime.yml b/eng/pipelines/runtime.yml index f18fa283f8881f..023cd16ed69d17 100644 --- a/eng/pipelines/runtime.yml +++ b/eng/pipelines/runtime.yml @@ -35,6 +35,7 @@ pr: include: - main - release/*.* + - copilot/*.* paths: include: - '*' diff --git a/src/libraries/Common/src/Interop/Unix/System.Native/Interop.SpawnProcess.cs b/src/libraries/Common/src/Interop/Unix/System.Native/Interop.SpawnProcess.cs index 8a6db042197878..8deac9d2ad22a2 100644 --- a/src/libraries/Common/src/Interop/Unix/System.Native/Interop.SpawnProcess.cs +++ b/src/libraries/Common/src/Interop/Unix/System.Native/Interop.SpawnProcess.cs @@ -23,7 +23,6 @@ internal static unsafe partial int SpawnProcess( int killOnParentDeath, int createSuspended, int createNewProcessGroup, - int detached, int* inheritedHandles, int inheritedHandlesCount); 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 89579160b0d636..8965a593929da2 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 @@ -63,8 +63,7 @@ private static SafeProcessHandle OpenCore(int processId) if (result == -1) { - int errno = Marshal.GetLastPInvokeError(); - throw new Win32Exception(errno); + throw new Win32Exception(); } return new SafeProcessHandle(pidfd, processId); @@ -131,14 +130,12 @@ private static unsafe SafeProcessHandle StartProcessInternal(string resolvedPath options.KillOnParentExit ? 1 : 0, createSuspended ? 1 : 0, options.CreateNewProcessGroup ? 1 : 0, - 0, // detached inheritedHandlesPtr, inheritedHandlesCount); if (result == -1) { - int errorCode = Marshal.GetLastPInvokeError(); - throw new Win32Exception(errorCode); + throw new Win32Exception(); } return new SafeProcessHandle(pidfd == -1 ? NoPidFd : pidfd, pid); @@ -156,8 +153,7 @@ private ProcessExitStatus WaitForExitCore() switch (Interop.Sys.WaitForExitAndReap(this, ProcessId, out int exitCode, out int rawSignal)) { case -1: - int errno = Marshal.GetLastPInvokeError(); - throw new Win32Exception(errno); + throw new Win32Exception(); default: return new(exitCode, false, rawSignal != 0 ? (PosixSignal)rawSignal : null); } @@ -168,8 +164,7 @@ private bool TryWaitForExitCore(int milliseconds, [NotNullWhen(true)] out Proces switch (Interop.Sys.TryWaitForExit(this, ProcessId, milliseconds, out int exitCode, out int rawSignal)) { case -1: - int errno = Marshal.GetLastPInvokeError(); - throw new Win32Exception(errno); + throw new Win32Exception(); case 1: // timeout exitStatus = null; return false; @@ -184,8 +179,7 @@ private ProcessExitStatus WaitForExitOrKillOnTimeoutCore(int milliseconds) switch (Interop.Sys.WaitForExitOrKillOnTimeout(this, ProcessId, milliseconds, out int exitCode, out int rawSignal, out int hasTimedout)) { case -1: - int errno = Marshal.GetLastPInvokeError(); - throw new Win32Exception(errno); + throw new Win32Exception(); default: return new(exitCode, hasTimedout == 1, rawSignal != 0 ? (PosixSignal)rawSignal : null); } @@ -213,8 +207,7 @@ private async Task WaitForExitAsyncCore(CancellationToken can switch (Interop.Sys.TryWaitForExitCancellable(this, ProcessId, (int)readHandle.DangerousGetHandle(), out int exitCode, out int rawSignal)) { case -1: - int errno = Marshal.GetLastPInvokeError(); - throw new Win32Exception(errno); + throw new Win32Exception(); case 1: // canceled throw new OperationCanceledException(cancellationToken); default: @@ -246,8 +239,7 @@ private async Task WaitForExitOrKillOnCancellationAsyncCore(C switch (Interop.Sys.TryWaitForExitCancellable(this, ProcessId, (int)readHandle.DangerousGetHandle(), out int exitCode, out int rawSignal)) { case -1: - int errno = Marshal.GetLastPInvokeError(); - throw new Win32Exception(errno); + throw new Win32Exception(); case 1: // canceled bool wasKilled = KillCore(throwOnError: false); ProcessExitStatus status = WaitForExitCore(); @@ -264,7 +256,7 @@ private static unsafe void CreatePipe(out SafeFileHandle readHandle, out SafeFil int* fds = stackalloc int[2]; if (Interop.Sys.Pipe(fds, Interop.Sys.PipeFlags.O_CLOEXEC) != 0) { - throw new Win32Exception(Marshal.GetLastPInvokeError()); + throw new Win32Exception(); } readHandle = new SafeFileHandle((IntPtr)fds[Interop.Sys.ReadEndOfPipe], ownsHandle: true); @@ -306,8 +298,7 @@ private void ResumeCore() return; } - int errno = Marshal.GetLastPInvokeError(); - throw new Win32Exception(errno); + throw new Win32Exception(); } private void SendSignalCore(PosixSignal signal, bool entireProcessGroup) @@ -322,8 +313,7 @@ private void SendSignalCore(PosixSignal signal, bool entireProcessGroup) return; } - int errno = Marshal.GetLastPInvokeError(); - throw new Win32Exception(errno); + throw new Win32Exception(); } } } 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 30a4f9a5214962..ea80e0180cfc73 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 @@ -384,7 +384,7 @@ private bool StartCore(ProcessStartInfo startInfo) } int stdinFd = -1, stdoutFd = -1, stderrFd = -1; - string[] envp = CreateEnvp(startInfo); + string[] envp = ProcessUtils.CreateEnvp(startInfo.Environment); string? cwd = !string.IsNullOrWhiteSpace(startInfo.WorkingDirectory) ? startInfo.WorkingDirectory : null; bool setCredentials = !string.IsNullOrEmpty(startInfo.UserName); @@ -604,11 +604,6 @@ private static string[] ParseArgv(ProcessStartInfo psi, string? resolvedExe = nu return argvList.ToArray(); } - /// Converts the environment variables information from a ProcessStartInfo into an envp array. - /// The ProcessStartInfo. - /// The envp array. - private static string[] CreateEnvp(ProcessStartInfo psi) => ProcessUtils.CreateEnvp(psi.Environment); - private static string? ResolveExecutableForShellExecute(string filename, string? workingDirectory) { // Determine if filename points to an executable file. diff --git a/src/libraries/System.Diagnostics.Process/tests/SafeProcessHandleTests.Unix.cs b/src/libraries/System.Diagnostics.Process/tests/SafeProcessHandleTests.Unix.cs index f56d0c7aa0953e..8a605aa8c1705a 100644 --- a/src/libraries/System.Diagnostics.Process/tests/SafeProcessHandleTests.Unix.cs +++ b/src/libraries/System.Diagnostics.Process/tests/SafeProcessHandleTests.Unix.cs @@ -18,9 +18,7 @@ public partial class SafeProcessHandleTests [PlatformSpecific(TestPlatforms.OSX)] public static void SendSignal_SIGTERM_TerminatesProcess() { - ProcessStartOptions options = new("sleep") { Arguments = { "60" } }; - - using SafeProcessHandle processHandle = SafeProcessHandle.Start(options, input: null, output: null, error: null); + using SafeProcessHandle processHandle = SafeProcessHandle.Start(CreateTenSecondSleep(), input: null, output: null, error: null); processHandle.Signal(PosixSignal.SIGTERM); @@ -34,9 +32,7 @@ public static void SendSignal_SIGTERM_TerminatesProcess() [PlatformSpecific(TestPlatforms.OSX)] public static void SendSignal_SIGINT_TerminatesProcess() { - ProcessStartOptions options = new("sleep") { Arguments = { "60" } }; - - using SafeProcessHandle processHandle = SafeProcessHandle.Start(options, input: null, output: null, error: null); + using SafeProcessHandle processHandle = SafeProcessHandle.Start(CreateTenSecondSleep(), input: null, output: null, error: null); processHandle.Signal(PosixSignal.SIGINT); @@ -50,14 +46,13 @@ public static void SendSignal_SIGINT_TerminatesProcess() [PlatformSpecific(TestPlatforms.OSX)] public static void Signal_InvalidSignal_ThrowsArgumentOutOfRangeException() { - ProcessStartOptions options = new("sleep") { Arguments = { "1" } }; - - using SafeProcessHandle processHandle = SafeProcessHandle.Start(options, input: null, output: null, error: null); + using SafeProcessHandle processHandle = SafeProcessHandle.Start(CreateTenSecondSleep(), input: null, output: null, error: null); PosixSignal invalidSignal = (PosixSignal)100; Assert.Throws(() => processHandle.Signal(invalidSignal)); + processHandle.Kill(); processHandle.WaitForExit(); } diff --git a/src/libraries/System.Diagnostics.Process/tests/SafeProcessHandleTests.cs b/src/libraries/System.Diagnostics.Process/tests/SafeProcessHandleTests.cs index 4da9570e5fac87..c585cc969abc96 100644 --- a/src/libraries/System.Diagnostics.Process/tests/SafeProcessHandleTests.cs +++ b/src/libraries/System.Diagnostics.Process/tests/SafeProcessHandleTests.cs @@ -23,15 +23,13 @@ private static ProcessStartOptions CreateTenSecondSleep() : new("powershell") { Arguments = { "-InputFormat", "None", "-Command", "Start-Sleep 10" } }; } - return new("sleep") { Arguments = { "60" } }; + return new("sleep") { Arguments = { "10" } }; } [Fact] public static void Start_WithNoArguments_Succeeds() { - ProcessStartOptions options = OperatingSystem.IsWindows() - ? new("hostname") - : new("echo") { Arguments = { "test" } }; + ProcessStartOptions options = new("pwd"); using SafeProcessHandle processHandle = SafeProcessHandle.Start(options, input: null, output: null, error: null); @@ -57,7 +55,9 @@ public static void Kill_KillsRunningProcess() } else { - Assert.NotEqual(0, exitStatus.ExitCode); + Assert.Equal(PosixSignal.SIGKILL, exitStatus.Signal); + // Exit code for signal termination is 128 + signal_number (native signal number, not enum value) + Assert.True(exitStatus.ExitCode > 128, $"Exit code {exitStatus.ExitCode} should indicate signal termination (>128)"); } } @@ -208,6 +208,13 @@ public static void WaitForExitOrKillOnTimeout_KillsAndWaitsWhenTimeoutOccurs() { Assert.Equal(-1, exitStatus.ExitCode); } + else + { + // On Unix, the process should have been killed with SIGKILL + Assert.Equal(PosixSignal.SIGKILL, exitStatus.Signal); + // Exit code for signal termination is 128 + signal_number (native signal number, not enum value) + Assert.True(exitStatus.ExitCode > 128, $"Exit code {exitStatus.ExitCode} should indicate signal termination (>128)"); + } } [Fact] @@ -305,8 +312,10 @@ public static async Task WaitForExitOrKillOnCancellationAsync_CompletesNormallyW public static void KillOnParentExit_CanBeSetToTrue() { ProcessStartOptions options = OperatingSystem.IsWindows() - ? new("cmd.exe") { Arguments = { "/c", "echo test" }, KillOnParentExit = true } - : new("echo") { Arguments = { "test" }, KillOnParentExit = true }; + ? new("cmd.exe") { Arguments = { "/c", "echo test" } } + : new("echo") { Arguments = { "test" } }; + + options.KillOnParentExit = true; Assert.True(options.KillOnParentExit); diff --git a/src/native/libs/System.Native/pal_process.c b/src/native/libs/System.Native/pal_process.c index 68285faf5445bf..ac2b24c5d8c2ad 100644 --- a/src/native/libs/System.Native/pal_process.c +++ b/src/native/libs/System.Native/pal_process.c @@ -966,7 +966,6 @@ int32_t SystemNative_SpawnProcess( int32_t kill_on_parent_death, int32_t create_suspended, int32_t create_new_process_group, - int32_t detached, const int32_t* inherited_handles, int32_t inherited_handles_count) { @@ -981,14 +980,6 @@ int32_t SystemNative_SpawnProcess( } #endif -#ifndef POSIX_SPAWN_SETSID - if (detached) - { - errno = ENOTSUP; - return -1; - } -#endif - pid_t child_pid; posix_spawn_file_actions_t file_actions; posix_spawnattr_t attr; @@ -1007,12 +998,6 @@ int32_t SystemNative_SpawnProcess( flags |= POSIX_SPAWN_START_SUSPENDED; } #endif - if (detached) - { -#ifdef POSIX_SPAWN_SETSID - flags |= POSIX_SPAWN_SETSID; -#endif - } if (create_new_process_group) { flags |= POSIX_SPAWN_SETPGROUP; @@ -1160,7 +1145,6 @@ int32_t SystemNative_SpawnProcess( (void)kill_on_parent_death; (void)create_suspended; (void)create_new_process_group; - (void)detached; (void)inherited_handles; (void)inherited_handles_count; errno = ENOTSUP; diff --git a/src/native/libs/System.Native/pal_process.h b/src/native/libs/System.Native/pal_process.h index fa4eab0f0b3abe..e00716852693c5 100644 --- a/src/native/libs/System.Native/pal_process.h +++ b/src/native/libs/System.Native/pal_process.h @@ -263,7 +263,6 @@ PALEXPORT int32_t SystemNative_SpawnProcess( int32_t kill_on_parent_death, int32_t create_suspended, int32_t create_new_process_group, - int32_t detached, const int32_t* inherited_handles, int32_t inherited_handles_count); diff --git a/src/native/libs/System.Native/pal_process_wasi.c b/src/native/libs/System.Native/pal_process_wasi.c index 2251311e94a556..94f2761d90e425 100644 --- a/src/native/libs/System.Native/pal_process_wasi.c +++ b/src/native/libs/System.Native/pal_process_wasi.c @@ -129,7 +129,7 @@ char* SystemNative_GetProcessPath(void) int32_t SystemNative_SpawnProcess(const char* path, char* const argv[], char* const envp[], int32_t stdin_fd, int32_t stdout_fd, int32_t stderr_fd, const char* working_dir, int32_t* out_pid, int32_t* out_pidfd, int32_t kill_on_parent_death, int32_t create_suspended, - int32_t create_new_process_group, int32_t detached, const int32_t* inherited_handles, + int32_t create_new_process_group, const int32_t* inherited_handles, int32_t inherited_handles_count) { errno = ENOTSUP; From cdd13b0ad172044087dedc8ac89ef6e731e64e4c Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 27 Feb 2026 12:37:24 +0000 Subject: [PATCH 06/21] Address unresolved feedback: simplify OpenProcess, remove SIGSTOP mapping, fix pwd test Co-authored-by: adamsitnik <6011991+adamsitnik@users.noreply.github.com> --- .../tests/SafeProcessHandleTests.cs | 4 +++- src/native/libs/System.Native/pal_process.c | 13 +------------ 2 files changed, 4 insertions(+), 13 deletions(-) diff --git a/src/libraries/System.Diagnostics.Process/tests/SafeProcessHandleTests.cs b/src/libraries/System.Diagnostics.Process/tests/SafeProcessHandleTests.cs index c585cc969abc96..66692036a38274 100644 --- a/src/libraries/System.Diagnostics.Process/tests/SafeProcessHandleTests.cs +++ b/src/libraries/System.Diagnostics.Process/tests/SafeProcessHandleTests.cs @@ -29,7 +29,9 @@ private static ProcessStartOptions CreateTenSecondSleep() [Fact] public static void Start_WithNoArguments_Succeeds() { - ProcessStartOptions options = new("pwd"); + ProcessStartOptions options = OperatingSystem.IsWindows() + ? new("hostname") + : new("pwd"); using SafeProcessHandle processHandle = SafeProcessHandle.Start(options, input: null, output: null, error: null); diff --git a/src/native/libs/System.Native/pal_process.c b/src/native/libs/System.Native/pal_process.c index ac2b24c5d8c2ad..a442bb2ef7509e 100644 --- a/src/native/libs/System.Native/pal_process.c +++ b/src/native/libs/System.Native/pal_process.c @@ -928,7 +928,6 @@ static int map_managed_signal_to_native(int managed_signal) case -9: return SIGTTOU; case -10: return SIGTSTP; case -11: return SIGKILL; - case -12: return SIGSTOP; default: return 0; } } @@ -948,7 +947,6 @@ static int map_native_signal_to_managed(int native_signal) case SIGTTOU: return -9; case SIGTSTP: return -10; case SIGKILL: return -11; - case SIGSTOP: return -12; default: return 0; } } @@ -1382,14 +1380,5 @@ int32_t SystemNative_OpenProcess(int32_t pid, int32_t* out_pidfd) { *out_pidfd = -1; - siginfo_t info; - memset(&info, 0, sizeof(info)); - int waitid_ret = waitid(P_PID, (id_t)pid, &info, WNOHANG | WNOWAIT | WEXITED | WSTOPPED | WCONTINUED); - - if (waitid_ret != 0) - { - return -1; - } - - return 0; + return kill(pid, 0); } From 4b02d2ea31c5c3ec8ec71ad2d4102ed6ee7ecc8f Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 27 Feb 2026 15:00:02 +0000 Subject: [PATCH 07/21] Replace HAVE_POSIX_SPAWN* feature detection with __APPLE__ platform checks Co-authored-by: adamsitnik <6011991+adamsitnik@users.noreply.github.com> --- src/native/libs/Common/pal_config.h.in | 5 --- src/native/libs/System.Native/pal_process.c | 15 +++----- src/native/libs/configure.cmake | 40 --------------------- 3 files changed, 4 insertions(+), 56 deletions(-) diff --git a/src/native/libs/Common/pal_config.h.in b/src/native/libs/Common/pal_config.h.in index 832f325b826b4b..a60327d9318187 100644 --- a/src/native/libs/Common/pal_config.h.in +++ b/src/native/libs/Common/pal_config.h.in @@ -145,11 +145,6 @@ #cmakedefine01 HAVE_GETGRGID_R #cmakedefine01 HAVE_TERMIOS2 #cmakedefine01 HAVE_SYS_EVENT_H -#cmakedefine01 HAVE_POSIX_SPAWN -#cmakedefine01 HAVE_POSIX_SPAWN_CLOEXEC_DEFAULT -#cmakedefine01 HAVE_POSIX_SPAWN_FILE_ACTIONS_ADDCHDIR_NP -#cmakedefine01 HAVE_POSIX_SPAWN_FILE_ACTIONS_ADDINHERIT_NP -#cmakedefine01 HAVE_POSIX_SPAWN_START_SUSPENDED // Mac OS X has stat64, but it is deprecated since plain stat now // provides the same 64-bit aware struct when targeting OS X > 10.5 diff --git a/src/native/libs/System.Native/pal_process.c b/src/native/libs/System.Native/pal_process.c index a442bb2ef7509e..4c8aeeada9dcaa 100644 --- a/src/native/libs/System.Native/pal_process.c +++ b/src/native/libs/System.Native/pal_process.c @@ -44,7 +44,7 @@ #include #endif -#if HAVE_POSIX_SPAWN +#ifdef __APPLE__ #include #endif @@ -967,10 +967,10 @@ int32_t SystemNative_SpawnProcess( const int32_t* inherited_handles, int32_t inherited_handles_count) { -#if HAVE_POSIX_SPAWN && HAVE_POSIX_SPAWN_CLOEXEC_DEFAULT && HAVE_POSIX_SPAWN_FILE_ACTIONS_ADDINHERIT_NP +#ifdef __APPLE__ // ========== POSIX_SPAWN PATH (macOS) ========== -#if !HAVE_POSIX_SPAWN_START_SUSPENDED +#ifndef POSIX_SPAWN_START_SUSPENDED if (create_suspended) { errno = ENOTSUP; @@ -990,7 +990,7 @@ int32_t SystemNative_SpawnProcess( } short flags = POSIX_SPAWN_CLOEXEC_DEFAULT | POSIX_SPAWN_SETSIGDEF; -#if HAVE_POSIX_SPAWN_START_SUSPENDED +#ifdef POSIX_SPAWN_START_SUSPENDED if (create_suspended) { flags |= POSIX_SPAWN_START_SUSPENDED; @@ -1073,7 +1073,6 @@ int32_t SystemNative_SpawnProcess( // Change working directory if specified if (working_dir != NULL) { -#if HAVE_POSIX_SPAWN_FILE_ACTIONS_ADDCHDIR_NP if ((result = posix_spawn_file_actions_addchdir_np(&file_actions, working_dir)) != 0) { int saved_errno = result; @@ -1082,12 +1081,6 @@ int32_t SystemNative_SpawnProcess( errno = saved_errno; return -1; } -#else - posix_spawn_file_actions_destroy(&file_actions); - posix_spawnattr_destroy(&attr); - errno = ENOTSUP; - return -1; -#endif } // Spawn the process diff --git a/src/native/libs/configure.cmake b/src/native/libs/configure.cmake index f39f2a86d7d417..bb1f3c8c81d7ee 100644 --- a/src/native/libs/configure.cmake +++ b/src/native/libs/configure.cmake @@ -500,46 +500,6 @@ set(CMAKE_REQUIRED_LIBRARIES ${PREVIOUS_CMAKE_REQUIRED_LIBRARIES}) check_include_files("sys/event.h" HAVE_SYS_EVENT_H) -# Check for posix_spawn features (macOS) -if(CLR_CMAKE_TARGET_APPLE) - check_symbol_exists(posix_spawn "spawn.h" HAVE_POSIX_SPAWN) - - check_c_source_compiles(" - #include - int main(void) { - #ifdef POSIX_SPAWN_CLOEXEC_DEFAULT - return 0; - #else - #error POSIX_SPAWN_CLOEXEC_DEFAULT not defined - #endif - } - " HAVE_POSIX_SPAWN_CLOEXEC_DEFAULT) - - check_symbol_exists(posix_spawn_file_actions_addchdir_np "spawn.h" HAVE_POSIX_SPAWN_FILE_ACTIONS_ADDCHDIR_NP) - - check_c_source_compiles(" - #include - int main(void) { - posix_spawn_file_actions_t actions; - posix_spawn_file_actions_init(&actions); - posix_spawn_file_actions_addinherit_np(&actions, 3); - posix_spawn_file_actions_destroy(&actions); - return 0; - } - " HAVE_POSIX_SPAWN_FILE_ACTIONS_ADDINHERIT_NP) - - check_c_source_compiles(" - #include - int main(void) { - #ifdef POSIX_SPAWN_START_SUSPENDED - return 0; - #else - #error POSIX_SPAWN_START_SUSPENDED not defined - #endif - } - " HAVE_POSIX_SPAWN_START_SUSPENDED) -endif() - check_symbol_exists( disconnectx "sys/socket.h" From a88393c094ca0747058826e7e65e78a14dc913d5 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 27 Feb 2026 15:01:18 +0000 Subject: [PATCH 08/21] Remove POSIX_SPAWN_START_SUSPENDED guards - macOS SDK always provides it Co-authored-by: adamsitnik <6011991+adamsitnik@users.noreply.github.com> --- src/native/libs/System.Native/pal_process.c | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/src/native/libs/System.Native/pal_process.c b/src/native/libs/System.Native/pal_process.c index 4c8aeeada9dcaa..88f79d0f6e38ef 100644 --- a/src/native/libs/System.Native/pal_process.c +++ b/src/native/libs/System.Native/pal_process.c @@ -970,14 +970,6 @@ int32_t SystemNative_SpawnProcess( #ifdef __APPLE__ // ========== POSIX_SPAWN PATH (macOS) ========== -#ifndef POSIX_SPAWN_START_SUSPENDED - if (create_suspended) - { - errno = ENOTSUP; - return -1; - } -#endif - pid_t child_pid; posix_spawn_file_actions_t file_actions; posix_spawnattr_t attr; @@ -990,12 +982,10 @@ int32_t SystemNative_SpawnProcess( } short flags = POSIX_SPAWN_CLOEXEC_DEFAULT | POSIX_SPAWN_SETSIGDEF; -#ifdef POSIX_SPAWN_START_SUSPENDED if (create_suspended) { flags |= POSIX_SPAWN_START_SUSPENDED; } -#endif if (create_new_process_group) { flags |= POSIX_SPAWN_SETPGROUP; From 300ba39f486cb299597fbd6bd6bf32dacf019a7c Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 27 Feb 2026 17:05:54 +0000 Subject: [PATCH 09/21] Address feedback: reuse pal_signal.c functions, use HAVE_KQUEUE, reorder args, add GetEnvVars helper, restore HAVE_POSIX_SPAWN_FILE_ACTIONS_ADDCHDIR_NP, fix test Co-authored-by: adamsitnik <6011991+adamsitnik@users.noreply.github.com> --- .../System.Native/Interop.SpawnProcess.cs | 10 +- .../SafeHandles/SafeProcessHandle.Unix.cs | 10 +- .../tests/SafeProcessHandleTests.cs | 1 + src/native/libs/Common/pal_config.h.in | 2 +- src/native/libs/System.Native/pal_process.c | 98 +++++++------------ src/native/libs/System.Native/pal_process.h | 10 +- .../libs/System.Native/pal_process_wasi.c | 8 +- src/native/libs/System.Native/pal_signal.c | 2 +- src/native/libs/System.Native/pal_signal.h | 6 ++ src/native/libs/configure.cmake | 3 +- 10 files changed, 67 insertions(+), 83 deletions(-) diff --git a/src/libraries/Common/src/Interop/Unix/System.Native/Interop.SpawnProcess.cs b/src/libraries/Common/src/Interop/Unix/System.Native/Interop.SpawnProcess.cs index 8deac9d2ad22a2..b7e02b923e6826 100644 --- a/src/libraries/Common/src/Interop/Unix/System.Native/Interop.SpawnProcess.cs +++ b/src/libraries/Common/src/Interop/Unix/System.Native/Interop.SpawnProcess.cs @@ -14,17 +14,17 @@ internal static unsafe partial int SpawnProcess( string path, byte** argv, byte** envp, + string? workingDir, + int* inheritedHandles, + int inheritedHandlesCount, int stdinFd, int stdoutFd, int stderrFd, - string? workingDir, - out int pid, - out int pidfd, int killOnParentDeath, int createSuspended, int createNewProcessGroup, - int* inheritedHandles, - int inheritedHandlesCount); + out int pid, + out int pidfd); [LibraryImport(Libraries.SystemNative, EntryPoint = "SystemNative_SendSignal", SetLastError = true)] internal static partial int SendSignal(int pidfd, int pid, PosixSignal managedSignal); 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 8965a593929da2..453f4021bd3bb9 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 @@ -121,17 +121,17 @@ private static unsafe SafeProcessHandle StartProcessInternal(string resolvedPath resolvedPath, argvPtr, envpPtr, + options.WorkingDirectory, + inheritedHandlesPtr, + inheritedHandlesCount, stdinFd, stdoutFd, stderrFd, - options.WorkingDirectory, - out int pid, - out int pidfd, options.KillOnParentExit ? 1 : 0, createSuspended ? 1 : 0, options.CreateNewProcessGroup ? 1 : 0, - inheritedHandlesPtr, - inheritedHandlesCount); + out int pid, + out int pidfd); if (result == -1) { diff --git a/src/libraries/System.Diagnostics.Process/tests/SafeProcessHandleTests.cs b/src/libraries/System.Diagnostics.Process/tests/SafeProcessHandleTests.cs index 66692036a38274..836da2cf989fb0 100644 --- a/src/libraries/System.Diagnostics.Process/tests/SafeProcessHandleTests.cs +++ b/src/libraries/System.Diagnostics.Process/tests/SafeProcessHandleTests.cs @@ -344,6 +344,7 @@ public static void Open_InvalidProcessId_Throws() } [Fact] + [PlatformSpecific(TestPlatforms.Windows)] public static void Open_CurrentProcess_Succeeds() { int currentPid = Environment.ProcessId; diff --git a/src/native/libs/Common/pal_config.h.in b/src/native/libs/Common/pal_config.h.in index a60327d9318187..d27fc095157d0a 100644 --- a/src/native/libs/Common/pal_config.h.in +++ b/src/native/libs/Common/pal_config.h.in @@ -144,7 +144,7 @@ #cmakedefine01 HAVE_MAKEDEV_SYSMACROSH #cmakedefine01 HAVE_GETGRGID_R #cmakedefine01 HAVE_TERMIOS2 -#cmakedefine01 HAVE_SYS_EVENT_H +#cmakedefine01 HAVE_POSIX_SPAWN_FILE_ACTIONS_ADDCHDIR_NP // Mac OS X has stat64, but it is deprecated since plain stat now // provides the same 64-bit aware struct when targeting OS X > 10.5 diff --git a/src/native/libs/System.Native/pal_process.c b/src/native/libs/System.Native/pal_process.c index 88f79d0f6e38ef..f25ec72bd2f157 100644 --- a/src/native/libs/System.Native/pal_process.c +++ b/src/native/libs/System.Native/pal_process.c @@ -4,6 +4,7 @@ #include "pal_config.h" #include "pal_process.h" #include "pal_io.h" +#include "pal_signal.h" #include "pal_utilities.h" #include @@ -32,6 +33,7 @@ #ifdef __APPLE__ #include +#include #endif #ifdef __FreeBSD__ @@ -40,14 +42,10 @@ #include #endif -#if HAVE_SYS_EVENT_H +#if HAVE_KQUEUE #include #endif -#ifdef __APPLE__ -#include -#endif - #include // Validate that our SysLogPriority values are correct for the platform @@ -912,60 +910,37 @@ char* SystemNative_GetProcessPath(void) return minipal_getexepath(); } -// Map managed PosixSignal enum values to native signal numbers -static int map_managed_signal_to_native(int managed_signal) +static char* const* GetEnvVars(char* const envp[]) { - switch (managed_signal) + if (envp != NULL) { - case -1: return SIGHUP; - case -2: return SIGINT; - case -3: return SIGQUIT; - case -4: return SIGTERM; - case -5: return SIGCHLD; - case -6: return SIGCONT; - case -7: return SIGWINCH; - case -8: return SIGTTIN; - case -9: return SIGTTOU; - case -10: return SIGTSTP; - case -11: return SIGKILL; - default: return 0; + return envp; } -} -static int map_native_signal_to_managed(int native_signal) -{ - switch (native_signal) - { - case SIGHUP: return -1; - case SIGINT: return -2; - case SIGQUIT: return -3; - case SIGTERM: return -4; - case SIGCHLD: return -5; - case SIGCONT: return -6; - case SIGWINCH: return -7; - case SIGTTIN: return -8; - case SIGTTOU: return -9; - case SIGTSTP: return -10; - case SIGKILL: return -11; - default: return 0; - } +#if HAVE_CRT_EXTERNS_H + return *_NSGetEnviron(); +#else + extern char **environ; + return environ; +#endif } int32_t SystemNative_SpawnProcess( const char* path, char* const argv[], char* const envp[], + const char* working_dir, + const int32_t* inherited_handles, + int32_t inherited_handles_count, int32_t stdin_fd, int32_t stdout_fd, int32_t stderr_fd, - const char* working_dir, - int32_t* out_pid, - int32_t* out_pidfd, int32_t kill_on_parent_death, int32_t create_suspended, int32_t create_new_process_group, - const int32_t* inherited_handles, - int32_t inherited_handles_count) + int32_t* out_pid, + int32_t* out_pidfd +) { #ifdef __APPLE__ // ========== POSIX_SPAWN PATH (macOS) ========== @@ -981,6 +956,8 @@ int32_t SystemNative_SpawnProcess( return -1; } + // POSIX_SPAWN_CLOEXEC_DEFAULT to close all FDs except stdin/stdout/stderr + // POSIX_SPAWN_SETSIGDEF to reset signal handlers short flags = POSIX_SPAWN_CLOEXEC_DEFAULT | POSIX_SPAWN_SETSIGDEF; if (create_suspended) { @@ -998,8 +975,11 @@ int32_t SystemNative_SpawnProcess( return -1; } + // If create_new_process_group is set, configure the process group ID to 0 + // which means the child will become the leader of a new process group if (create_new_process_group) { + // posix_spawnattr_setpgroup with pgid=0 makes the child the leader of a new process group if ((result = posix_spawnattr_setpgroup(&attr, 0)) != 0) { int saved_errno = result; @@ -1063,6 +1043,8 @@ int32_t SystemNative_SpawnProcess( // Change working directory if specified if (working_dir != NULL) { + // posix_spawn_file_actions_addchdir_np is not available on tvOS +#if HAVE_POSIX_SPAWN_FILE_ACTIONS_ADDCHDIR_NP if ((result = posix_spawn_file_actions_addchdir_np(&file_actions, working_dir)) != 0) { int saved_errno = result; @@ -1071,24 +1053,16 @@ int32_t SystemNative_SpawnProcess( errno = saved_errno; return -1; } - } - - // Spawn the process - // If envp is NULL, use the current environment - char* const* env; - if (envp != NULL) - { - env = envp; - } - else - { -#if HAVE_CRT_EXTERNS_H - env = *_NSGetEnviron(); #else - extern char **environ; - env = environ; + posix_spawn_file_actions_destroy(&file_actions); + posix_spawnattr_destroy(&attr); + errno = ENOTSUP; + return -1; #endif } + + // Spawn the process + char* const* env = GetEnvVars(envp); result = posix_spawn(&child_pid, path, &file_actions, &attr, argv, env); posix_spawn_file_actions_destroy(&file_actions); @@ -1135,7 +1109,7 @@ int32_t SystemNative_SpawnProcess( int32_t SystemNative_SendSignal(int32_t pidfd, int32_t pid, int32_t managed_signal) { - int native_signal = map_managed_signal_to_native(managed_signal); + int native_signal = SystemNative_GetPlatformSignalNumber((PosixSignal)managed_signal); if (native_signal == 0) { errno = EINVAL; @@ -1158,7 +1132,9 @@ static int map_wait_status(int status, int32_t* out_exitCode, int32_t* out_signa { int sig = WTERMSIG(status); *out_exitCode = 128 + sig; - *out_signal = map_native_signal_to_managed(sig); + PosixSignal posixSignal; + TryConvertSignalCodeToPosixSignal(sig, &posixSignal); + *out_signal = (int32_t)posixSignal; return 0; } return -1; @@ -1342,7 +1318,7 @@ int32_t SystemNative_WaitForExitOrKillOnTimeout(int32_t pidfd, int32_t pid, int3 } *out_timeout = 1; - ret = SystemNative_SendSignal(pidfd, pid, map_native_signal_to_managed(SIGKILL)); + ret = SystemNative_SendSignal(pidfd, pid, (int32_t)PosixSignalSIGKILL); if (ret == -1) { diff --git a/src/native/libs/System.Native/pal_process.h b/src/native/libs/System.Native/pal_process.h index e00716852693c5..bd07f261e16467 100644 --- a/src/native/libs/System.Native/pal_process.h +++ b/src/native/libs/System.Native/pal_process.h @@ -254,17 +254,17 @@ PALEXPORT int32_t SystemNative_SpawnProcess( const char* path, char* const argv[], char* const envp[], + const char* working_dir, + const int32_t* inherited_handles, + int32_t inherited_handles_count, int32_t stdin_fd, int32_t stdout_fd, int32_t stderr_fd, - const char* working_dir, - int32_t* out_pid, - int32_t* out_pidfd, int32_t kill_on_parent_death, int32_t create_suspended, int32_t create_new_process_group, - const int32_t* inherited_handles, - int32_t inherited_handles_count); + int32_t* out_pid, + int32_t* out_pidfd); /** * Sends a signal to a process. diff --git a/src/native/libs/System.Native/pal_process_wasi.c b/src/native/libs/System.Native/pal_process_wasi.c index 94f2761d90e425..eb053ec349d168 100644 --- a/src/native/libs/System.Native/pal_process_wasi.c +++ b/src/native/libs/System.Native/pal_process_wasi.c @@ -127,10 +127,10 @@ char* SystemNative_GetProcessPath(void) } int32_t SystemNative_SpawnProcess(const char* path, char* const argv[], char* const envp[], - int32_t stdin_fd, int32_t stdout_fd, int32_t stderr_fd, const char* working_dir, - int32_t* out_pid, int32_t* out_pidfd, int32_t kill_on_parent_death, int32_t create_suspended, - int32_t create_new_process_group, const int32_t* inherited_handles, - int32_t inherited_handles_count) + const char* working_dir, const int32_t* inherited_handles, int32_t inherited_handles_count, + int32_t stdin_fd, int32_t stdout_fd, int32_t stderr_fd, int32_t kill_on_parent_death, + int32_t create_suspended, int32_t create_new_process_group, int32_t* out_pid, + int32_t* out_pidfd) { errno = ENOTSUP; return -1; diff --git a/src/native/libs/System.Native/pal_signal.c b/src/native/libs/System.Native/pal_signal.c index 33be19b7faf37d..d2f636b3986d70 100644 --- a/src/native/libs/System.Native/pal_signal.c +++ b/src/native/libs/System.Native/pal_signal.c @@ -83,7 +83,7 @@ static bool IsSigIgn(struct sigaction* action) action->sa_handler == SIG_IGN; } -static bool TryConvertSignalCodeToPosixSignal(int signalCode, PosixSignal* posixSignal) +bool TryConvertSignalCodeToPosixSignal(int signalCode, PosixSignal* posixSignal) { assert(posixSignal != NULL); diff --git a/src/native/libs/System.Native/pal_signal.h b/src/native/libs/System.Native/pal_signal.h index d462b611ccb5de..e49a954db606d6 100644 --- a/src/native/libs/System.Native/pal_signal.h +++ b/src/native/libs/System.Native/pal_signal.h @@ -63,6 +63,12 @@ PALEXPORT void SystemNative_SetPosixSignalHandler(PosixSignalHandler signalHandl */ PALEXPORT int32_t SystemNative_GetPlatformSignalNumber(PosixSignal signal); +/** + * Converts a native signal code to its PosixSignal equivalent. + * Returns true if the signal was converted, false otherwise. + */ +bool TryConvertSignalCodeToPosixSignal(int signalCode, PosixSignal* posixSignal); + /** * Enables calling the PosixSignalHandler for the specified signal. */ diff --git a/src/native/libs/configure.cmake b/src/native/libs/configure.cmake index bb1f3c8c81d7ee..39a75e1a0fc77f 100644 --- a/src/native/libs/configure.cmake +++ b/src/native/libs/configure.cmake @@ -498,7 +498,8 @@ check_symbol_exists( HAVE_KQUEUE) set(CMAKE_REQUIRED_LIBRARIES ${PREVIOUS_CMAKE_REQUIRED_LIBRARIES}) -check_include_files("sys/event.h" HAVE_SYS_EVENT_H) +# posix_spawn_file_actions_addchdir_np is not available on tvOS +check_symbol_exists(posix_spawn_file_actions_addchdir_np "spawn.h" HAVE_POSIX_SPAWN_FILE_ACTIONS_ADDCHDIR_NP) check_symbol_exists( disconnectx From 2240ff72328b590cd0290800ab565260987c53cd Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 27 Feb 2026 17:20:10 +0000 Subject: [PATCH 10/21] Refactor addchdir_np block to reduce code duplication Co-authored-by: adamsitnik <6011991+adamsitnik@users.noreply.github.com> --- src/native/libs/System.Native/pal_process.c | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/src/native/libs/System.Native/pal_process.c b/src/native/libs/System.Native/pal_process.c index f25ec72bd2f157..d8a8e1c0a037f8 100644 --- a/src/native/libs/System.Native/pal_process.c +++ b/src/native/libs/System.Native/pal_process.c @@ -1045,7 +1045,12 @@ int32_t SystemNative_SpawnProcess( { // posix_spawn_file_actions_addchdir_np is not available on tvOS #if HAVE_POSIX_SPAWN_FILE_ACTIONS_ADDCHDIR_NP - if ((result = posix_spawn_file_actions_addchdir_np(&file_actions, working_dir)) != 0) + result = posix_spawn_file_actions_addchdir_np(&file_actions, working_dir); +#else + result = ENOTSUP; +#endif + + if (result != 0) { int saved_errno = result; posix_spawn_file_actions_destroy(&file_actions); @@ -1053,12 +1058,6 @@ int32_t SystemNative_SpawnProcess( errno = saved_errno; return -1; } -#else - posix_spawn_file_actions_destroy(&file_actions); - posix_spawnattr_destroy(&attr); - errno = ENOTSUP; - return -1; -#endif } // Spawn the process From 5d762a876e76b81fdc51dad43635db927bfa5f33 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 27 Feb 2026 18:54:29 +0000 Subject: [PATCH 11/21] Exclude TARGET_MACCATALYST and TARGET_TVOS from SpawnProcess guard Co-authored-by: adamsitnik <6011991+adamsitnik@users.noreply.github.com> --- src/native/libs/System.Native/pal_process.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/native/libs/System.Native/pal_process.c b/src/native/libs/System.Native/pal_process.c index d8a8e1c0a037f8..74c1b23b7f5aee 100644 --- a/src/native/libs/System.Native/pal_process.c +++ b/src/native/libs/System.Native/pal_process.c @@ -942,7 +942,7 @@ int32_t SystemNative_SpawnProcess( int32_t* out_pidfd ) { -#ifdef __APPLE__ +#if defined(__APPLE__) && !defined(TARGET_MACCATALYST) && !defined(TARGET_TVOS) // ========== POSIX_SPAWN PATH (macOS) ========== pid_t child_pid; From 897d98832a4048dee47056a50001712aa09d1367 Mon Sep 17 00:00:00 2001 From: Adam Sitnik Date: Fri, 6 Mar 2026 18:29:45 +0100 Subject: [PATCH 12/21] address code review feedback: - move more Windows-specific tests to .Windows.cs file - disable some tests on macOS - handle hardcoded signal values and invalid values - add missing DangerousAddRef/DangerousRelease --- .../System.Native/Interop.SpawnProcess.cs | 6 +- .../SafeHandles/SafeProcessHandle.Unix.cs | 36 +++++----- .../tests/SafeProcessHandleTests.Unix.cs | 50 ++++++++++---- .../tests/SafeProcessHandleTests.Windows.cs | 21 +++++- .../tests/SafeProcessHandleTests.cs | 65 +++---------------- .../System.Private.CoreLib.Shared.projitems | 2 +- 6 files changed, 87 insertions(+), 93 deletions(-) diff --git a/src/libraries/Common/src/Interop/Unix/System.Native/Interop.SpawnProcess.cs b/src/libraries/Common/src/Interop/Unix/System.Native/Interop.SpawnProcess.cs index b7e02b923e6826..41e39da0920a47 100644 --- a/src/libraries/Common/src/Interop/Unix/System.Native/Interop.SpawnProcess.cs +++ b/src/libraries/Common/src/Interop/Unix/System.Native/Interop.SpawnProcess.cs @@ -17,9 +17,9 @@ internal static unsafe partial int SpawnProcess( string? workingDir, int* inheritedHandles, int inheritedHandlesCount, - int stdinFd, - int stdoutFd, - int stderrFd, + SafeHandle stdinFd, + SafeHandle stdoutFd, + SafeHandle stderrFd, int killOnParentDeath, int createSuspended, int createNewProcessGroup, 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 499fafa0b9857c..3525677a2476df 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 @@ -69,7 +69,7 @@ private static SafeProcessHandle OpenCore(int processId) return new SafeProcessHandle(pidfd, processId); } - private static SafeProcessHandle StartCore(ProcessStartOptions options, SafeFileHandle inputHandle, SafeFileHandle outputHandle, SafeFileHandle errorHandle, bool createSuspended) + private static unsafe SafeProcessHandle StartCore(ProcessStartOptions options, SafeFileHandle inputHandle, SafeFileHandle outputHandle, SafeFileHandle errorHandle, bool createSuspended) { // Prepare arguments array (argv) string[] argv = [options.FileName, .. options.Arguments]; @@ -78,21 +78,11 @@ private static SafeProcessHandle StartCore(ProcessStartOptions options, SafeFile // If not accessed, pass null to use the current environment (environ) string[]? envp = options.HasEnvironmentBeenAccessed ? ProcessUtils.CreateEnvp(options.Environment) : null; - // Get file descriptors for stdin/stdout/stderr - int stdInFd = (int)inputHandle.DangerousGetHandle(); - int stdOutFd = (int)outputHandle.DangerousGetHandle(); - int stdErrFd = (int)errorHandle.DangerousGetHandle(); - - return StartProcessInternal(options.FileName, argv, envp, options, stdInFd, stdOutFd, stdErrFd, createSuspended); - } - - private static unsafe SafeProcessHandle StartProcessInternal(string resolvedPath, string[] argv, string[]? envp, - ProcessStartOptions options, int stdinFd, int stdoutFd, int stderrFd, bool createSuspended) - { byte** argvPtr = null; byte** envpPtr = null; int* inheritedHandlesPtr = null; int inheritedHandlesCount = 0; + SafeHandle[]? handlesToRelease = null; try { @@ -108,25 +98,29 @@ private static unsafe SafeProcessHandle StartProcessInternal(string resolvedPath if (options.HasInheritedHandles) { inheritedHandlesCount = options.InheritedHandles.Count; + handlesToRelease = new SafeHandle[inheritedHandlesCount]; inheritedHandlesPtr = (int*)NativeMemory.Alloc((nuint)inheritedHandlesCount, (nuint)sizeof(int)); + bool ignore = false; for (int i = 0; i < inheritedHandlesCount; i++) { + options.InheritedHandles[i].DangerousAddRef(ref ignore); inheritedHandlesPtr[i] = (int)options.InheritedHandles[i].DangerousGetHandle(); + handlesToRelease[i] = options.InheritedHandles[i]; } } // Call native library to spawn process int result = Interop.Sys.SpawnProcess( - resolvedPath, + options.FileName, argvPtr, envpPtr, options.WorkingDirectory, inheritedHandlesPtr, inheritedHandlesCount, - stdinFd, - stdoutFd, - stderrFd, + inputHandle, + outputHandle, + errorHandle, options.KillOnParentExit ? 1 : 0, createSuspended ? 1 : 0, options.CreateNewProcessGroup ? 1 : 0, @@ -145,6 +139,14 @@ private static unsafe SafeProcessHandle StartProcessInternal(string resolvedPath ProcessUtils.FreeArray(envpPtr, envp?.Length ?? 0); ProcessUtils.FreeArray(argvPtr, argv.Length); NativeMemory.Free(inheritedHandlesPtr); + + if (handlesToRelease is not null) + { + foreach (SafeHandle safeHandle in handlesToRelease) + { + safeHandle.DangerousRelease(); + } + } } } @@ -174,7 +176,7 @@ private bool TryWaitForExitCore(int milliseconds, [NotNullWhen(true)] out Proces } } - private int GetProcessIdCore() => throw new NotImplementedException(); + private static int GetProcessIdCore() => throw new PlatformNotSupportedException(); private ProcessExitStatus WaitForExitOrKillOnTimeoutCore(int milliseconds) { diff --git a/src/libraries/System.Diagnostics.Process/tests/SafeProcessHandleTests.Unix.cs b/src/libraries/System.Diagnostics.Process/tests/SafeProcessHandleTests.Unix.cs index 8a605aa8c1705a..eb7d9cb678c2b5 100644 --- a/src/libraries/System.Diagnostics.Process/tests/SafeProcessHandleTests.Unix.cs +++ b/src/libraries/System.Diagnostics.Process/tests/SafeProcessHandleTests.Unix.cs @@ -2,11 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.ComponentModel; -using System.Diagnostics; -using System.IO; using System.Runtime.InteropServices; -using System.Threading; -using System.Threading.Tasks; using Microsoft.Win32.SafeHandles; using Xunit; @@ -22,7 +18,7 @@ public static void SendSignal_SIGTERM_TerminatesProcess() processHandle.Signal(PosixSignal.SIGTERM); - ProcessExitStatus exitStatus = processHandle.WaitForExitOrKillOnTimeout(TimeSpan.FromSeconds(5)); + ProcessExitStatus exitStatus = processHandle.WaitForExitOrKillOnTimeout(TimeSpan.FromSeconds(1)); Assert.Equal(PosixSignal.SIGTERM, exitStatus.Signal); Assert.True(exitStatus.ExitCode > 128, $"Exit code {exitStatus.ExitCode} should indicate signal termination (>128)"); @@ -36,7 +32,7 @@ public static void SendSignal_SIGINT_TerminatesProcess() processHandle.Signal(PosixSignal.SIGINT); - ProcessExitStatus exitStatus = processHandle.WaitForExitOrKillOnTimeout(TimeSpan.FromSeconds(5)); + ProcessExitStatus exitStatus = processHandle.WaitForExitOrKillOnTimeout(TimeSpan.FromSeconds(1)); Assert.Equal(PosixSignal.SIGINT, exitStatus.Signal); Assert.True(exitStatus.ExitCode > 128, $"Exit code {exitStatus.ExitCode} should indicate signal termination (>128)"); @@ -44,13 +40,30 @@ public static void SendSignal_SIGINT_TerminatesProcess() [Fact] [PlatformSpecific(TestPlatforms.OSX)] - public static void Signal_InvalidSignal_ThrowsArgumentOutOfRangeException() + public static void SendSignal_HardcodedSigkillValue_TerminatesProcess() + { + using SafeProcessHandle processHandle = SafeProcessHandle.Start(CreateTenSecondSleep(), input: null, output: null, error: null); + + processHandle.Signal((PosixSignal)9); + + ProcessExitStatus exitStatus = processHandle.WaitForExitOrKillOnTimeout(TimeSpan.FromSeconds(1)); + + Assert.Equal(PosixSignal.SIGKILL, exitStatus.Signal); + Assert.True(exitStatus.ExitCode > 128, $"Exit code {exitStatus.ExitCode} should indicate signal termination (>128)"); + } + + [Fact] + [PlatformSpecific(TestPlatforms.OSX)] + public static void Signal_InvalidSignal_ThrowsWin32Exception() { using SafeProcessHandle processHandle = SafeProcessHandle.Start(CreateTenSecondSleep(), input: null, output: null, error: null); PosixSignal invalidSignal = (PosixSignal)100; - Assert.Throws(() => processHandle.Signal(invalidSignal)); + Win32Exception exception = Assert.Throws(() => processHandle.Signal(invalidSignal)); + + // EINVAL error code is 22 on Unix systems + Assert.Equal(22, exception.NativeErrorCode); processHandle.Kill(); processHandle.WaitForExit(); @@ -62,12 +75,7 @@ public static void SendSignal_ToExitedProcess_ThrowsWin32Exception() { ProcessStartOptions options = new("echo") { Arguments = { "test" } }; - using SafeFileHandle nullHandle = File.OpenNullHandle(); - using SafeProcessHandle processHandle = SafeProcessHandle.Start( - options, - input: null, - output: nullHandle, - error: nullHandle); + using SafeProcessHandle processHandle = SafeProcessHandle.Start(options, input: null, output: null, error: null); processHandle.WaitForExit(); @@ -76,5 +84,19 @@ public static void SendSignal_ToExitedProcess_ThrowsWin32Exception() // ESRCH error code is 3 on Unix systems Assert.Equal(3, exception.NativeErrorCode); } + + [Fact] + [PlatformSpecific(TestPlatforms.OSX)] + public static void ProcessId_WhenNotProvided_ThrowsPlatformNotSupportedException_OnPlatformsThatDontSupportProcessDesrcriptors() + { + ProcessStartOptions options = CreateTenSecondSleep(); + using SafeProcessHandle started = SafeProcessHandle.Start(options, input: null, output: null, error: null); + + using SafeProcessHandle copy = new(started.DangerousGetHandle(), ownsHandle: false); + Assert.Throws(() => copy.ProcessId); + + copy.Kill(); + Assert.True(started.TryWaitForExit(TimeSpan.FromMilliseconds(300), out _)); + } } } diff --git a/src/libraries/System.Diagnostics.Process/tests/SafeProcessHandleTests.Windows.cs b/src/libraries/System.Diagnostics.Process/tests/SafeProcessHandleTests.Windows.cs index c4f60641617f11..29325bf1961f60 100644 --- a/src/libraries/System.Diagnostics.Process/tests/SafeProcessHandleTests.Windows.cs +++ b/src/libraries/System.Diagnostics.Process/tests/SafeProcessHandleTests.Windows.cs @@ -3,7 +3,6 @@ using System.ComponentModel; using System.IO; -using System.IO.Pipes; using System.Runtime.InteropServices; using System.Threading; using System.Threading.Tasks; @@ -61,5 +60,25 @@ public async Task Signal_TerminatesProcessInNewProcessGroup(PosixSignal signal) Assert.True(exited, "Process should have exited after signal is sent"); Assert.NotEqual(0, exitStatus?.ExitCode); } + + [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsNotWindowsNanoServer))] + [PlatformSpecific(TestPlatforms.Windows)] + public void Signal_UnsupportedSignal_ThrowsPlatformNotSupportedException() + { + ProcessStartOptions options = CreateTenSecondSleep(); + options.CreateNewProcessGroup = true; + + using SafeProcessHandle processHandle = SafeProcessHandle.Start(options, input: null, output: null, error: null); + + try + { + Assert.Throws(() => processHandle.Signal(PosixSignal.SIGTERM)); + } + finally + { + processHandle.Kill(); + processHandle.WaitForExit(); + } + } } } diff --git a/src/libraries/System.Diagnostics.Process/tests/SafeProcessHandleTests.cs b/src/libraries/System.Diagnostics.Process/tests/SafeProcessHandleTests.cs index 9520fa0c32e9ce..7831c907b53b9a 100644 --- a/src/libraries/System.Diagnostics.Process/tests/SafeProcessHandleTests.cs +++ b/src/libraries/System.Diagnostics.Process/tests/SafeProcessHandleTests.cs @@ -22,7 +22,7 @@ public partial class SafeProcessHandleTests // - powershell is not available on Nano. We can't always use it. // - ping seems to be a workaround, but it's simple and work everywhere. The arguments are set to make it sleep for approximately 10 seconds. private static ProcessStartOptions CreateTenSecondSleep() => OperatingSystem.IsWindows() - ? new("ping") { Arguments = { "127.0.0.1", "-n", "11" } } + ? new("ping") { Arguments = { "127.0.0.1", "-n", "11" } } : new("sleep") { Arguments = { "10" } }; [Fact] @@ -309,23 +309,6 @@ public static async Task WaitForExitOrKillOnCancellationAsync_CompletesNormallyW Assert.Null(exitStatus.Signal); } - [Fact] - public static void KillOnParentExit_CanBeSetToTrue() - { - ProcessStartOptions options = OperatingSystem.IsWindows() - ? new("cmd.exe") { Arguments = { "/c", "echo test" } } - : new("echo") { Arguments = { "test" } }; - - options.KillOnParentExit = true; - - Assert.True(options.KillOnParentExit); - - using SafeProcessHandle processHandle = SafeProcessHandle.Start(options, input: null, output: null, error: null); - - ProcessExitStatus exitStatus = processHandle.WaitForExitOrKillOnTimeout(TimeSpan.FromSeconds(5)); - Assert.Equal(0, exitStatus.ExitCode); - } - [Fact] public static void KillOnParentExit_DefaultsToFalse() { @@ -355,6 +338,7 @@ public void Open_CanWaitForExitOnOpenedProcess() } [Fact] + [PlatformSpecific(TestPlatforms.Windows | TestPlatforms.Linux)] // we don't have handles on macOS public static void ProcessId_IsFetched_WhenNotProvided() { ProcessStartOptions options = CreateTenSecondSleep(); @@ -367,42 +351,6 @@ public static void ProcessId_IsFetched_WhenNotProvided() Assert.True(started.TryWaitForExit(TimeSpan.FromMilliseconds(300), out _)); } - [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsNotWindowsNanoServer))] - [PlatformSpecific(TestPlatforms.Windows)] - public void Signal_UnsupportedSignal_ThrowsPlatformNotSupportedException() - { - ProcessStartOptions options = CreateTenSecondSleep(); - options.CreateNewProcessGroup = true; - - using SafeProcessHandle processHandle = SafeProcessHandle.Start(options, input: null, output: null, error: null); - - try - { - Assert.Throws(() => processHandle.Signal(PosixSignal.SIGTERM)); - } - finally - { - processHandle.Kill(); - processHandle.WaitForExit(); - } - } - - [Fact] - public void CreateNewProcessGroup_CanBeSetToTrue() - { - ProcessStartOptions options = new("cmd.exe") - { - Arguments = { "/c", "echo test" }, - CreateNewProcessGroup = true - }; - - Assert.True(options.CreateNewProcessGroup); - - using SafeProcessHandle processHandle = SafeProcessHandle.Start(options, input: null, output: null, error: null); - ProcessExitStatus exitStatus = processHandle.WaitForExitOrKillOnTimeout(TimeSpan.FromSeconds(5)); - Assert.Equal(0, exitStatus.ExitCode); - } - [ConditionalTheory] [InlineData(true, false)] [InlineData(true, true)] @@ -570,7 +518,8 @@ public void KillOnParentExit_KillsTheChild_WhenParentExits(bool enabled) remoteHandle.Process.WaitForExit(); - VerifyProcessIsRunning(enabled, remoteHandle.ExitCode); + // It's currently not implemented on macOS. + VerifyProcessIsRunning(shouldExited: enabled && !OperatingSystem.IsMacOS(), remoteHandle.ExitCode); } [ConditionalTheory(typeof(RemoteExecutor), nameof(RemoteExecutor.IsSupported))] @@ -602,7 +551,8 @@ public void KillOnParentExit_KillsTheChild_WhenParentIsKilled(bool enabled) remoteHandle.Process.Kill(); remoteHandle.Process.WaitForExit(); - VerifyProcessIsRunning(enabled, grandChildPid); + // It's currently not implemented on macOS. + VerifyProcessIsRunning(shouldExited: enabled && !OperatingSystem.IsMacOS(), grandChildPid); } [ConditionalTheory(typeof(RemoteExecutor), nameof(RemoteExecutor.IsSupported))] @@ -638,7 +588,8 @@ public void KillOnParentExit_KillsTheChild_WhenParentCrashes(bool enabled) remoteHandle.Process.StandardInput.WriteLine("One AccessViolationException please."); remoteHandle.Process.WaitForExit(); - VerifyProcessIsRunning(enabled, grandChildPid); + // It's currently not implemented on macOS. + VerifyProcessIsRunning(shouldExited: enabled && !OperatingSystem.IsMacOS(), grandChildPid); } private static void VerifyProcessIsRunning(bool shouldExited, int processId) diff --git a/src/libraries/System.Private.CoreLib/src/System.Private.CoreLib.Shared.projitems b/src/libraries/System.Private.CoreLib/src/System.Private.CoreLib.Shared.projitems index 9b2f3707a4180a..d6447cbb0484eb 100644 --- a/src/libraries/System.Private.CoreLib/src/System.Private.CoreLib.Shared.projitems +++ b/src/libraries/System.Private.CoreLib/src/System.Private.CoreLib.Shared.projitems @@ -2951,4 +2951,4 @@ - + \ No newline at end of file From 1fce62357cc096517d341f44c1877149ece9dadc Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 7 Mar 2026 19:06:44 +0000 Subject: [PATCH 13/21] Fix failing tests: use started.Kill() instead of copy, use INT_MIN for out_pidfd Co-authored-by: adamsitnik <6011991+adamsitnik@users.noreply.github.com> --- .../tests/SafeProcessHandleTests.Unix.cs | 2 +- src/native/libs/System.Native/pal_process.c | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/libraries/System.Diagnostics.Process/tests/SafeProcessHandleTests.Unix.cs b/src/libraries/System.Diagnostics.Process/tests/SafeProcessHandleTests.Unix.cs index eb7d9cb678c2b5..28d2e4aeb61336 100644 --- a/src/libraries/System.Diagnostics.Process/tests/SafeProcessHandleTests.Unix.cs +++ b/src/libraries/System.Diagnostics.Process/tests/SafeProcessHandleTests.Unix.cs @@ -95,7 +95,7 @@ public static void ProcessId_WhenNotProvided_ThrowsPlatformNotSupportedException using SafeProcessHandle copy = new(started.DangerousGetHandle(), ownsHandle: false); Assert.Throws(() => copy.ProcessId); - copy.Kill(); + started.Kill(); Assert.True(started.TryWaitForExit(TimeSpan.FromMilliseconds(300), out _)); } } diff --git a/src/native/libs/System.Native/pal_process.c b/src/native/libs/System.Native/pal_process.c index 74c1b23b7f5aee..1c3d11d16c0c4b 100644 --- a/src/native/libs/System.Native/pal_process.c +++ b/src/native/libs/System.Native/pal_process.c @@ -1082,7 +1082,7 @@ int32_t SystemNative_SpawnProcess( } if (out_pidfd != NULL) { - *out_pidfd = -1; // pidfd not supported on macOS + *out_pidfd = INT_MIN; // pidfd not supported on macOS (-1 is treated as invalid handle) } return 0; #else @@ -1336,7 +1336,7 @@ int32_t SystemNative_WaitForExitOrKillOnTimeout(int32_t pidfd, int32_t pid, int3 int32_t SystemNative_OpenProcess(int32_t pid, int32_t* out_pidfd) { - *out_pidfd = -1; + *out_pidfd = INT_MIN; return kill(pid, 0); } From f2e54dd455e4cb5bd1327a2be4dab88cc60b8cda Mon Sep 17 00:00:00 2001 From: Adam Sitnik Date: Sat, 7 Mar 2026 22:01:46 +0100 Subject: [PATCH 14/21] fix test failures: - don't wait for non-child process to exit on Unix, as it's not supported (we can get process exit notification, but not exit status) - when process is a group leader, the WaitForExitOrKill should kill entire process group --- .../Unix/System.Native/Interop.SpawnProcess.cs | 2 +- .../Win32/SafeHandles/SafeProcessHandle.Unix.cs | 12 +++++++----- .../Microsoft/Win32/SafeHandles/SafeProcessHandle.cs | 8 ++++---- .../tests/SafeProcessHandleTests.cs | 1 - src/native/libs/System.Native/pal_process.c | 12 ++++++++++-- src/native/libs/System.Native/pal_process.h | 2 +- 6 files changed, 23 insertions(+), 14 deletions(-) diff --git a/src/libraries/Common/src/Interop/Unix/System.Native/Interop.SpawnProcess.cs b/src/libraries/Common/src/Interop/Unix/System.Native/Interop.SpawnProcess.cs index 41e39da0920a47..d094cbdfcc501e 100644 --- a/src/libraries/Common/src/Interop/Unix/System.Native/Interop.SpawnProcess.cs +++ b/src/libraries/Common/src/Interop/Unix/System.Native/Interop.SpawnProcess.cs @@ -39,7 +39,7 @@ internal static unsafe partial int SpawnProcess( internal static partial int TryWaitForExitCancellable(SafeProcessHandle pidfd, int pid, int cancelPipeFd, out int exitCode, out int signal); [LibraryImport(Libraries.SystemNative, EntryPoint = "SystemNative_WaitForExitOrKillOnTimeout", SetLastError = true)] - internal static partial int WaitForExitOrKillOnTimeout(SafeProcessHandle pidfd, int pid, int timeoutMs, out int exitCode, out int signal, out int hasTimedout); + internal static partial int WaitForExitOrKillOnTimeout(SafeProcessHandle pidfd, int pid, bool isGroupLeader, int timeoutMs, out int exitCode, out int signal, out int hasTimedout); [LibraryImport(Libraries.SystemNative, EntryPoint = "SystemNative_TryGetExitCode", SetLastError = true)] internal static partial int TryGetExitCode(SafeProcessHandle pidfd, int pid, out int exitCode, out int signal); 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 3525677a2476df..e0a85556a4d6d9 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 @@ -26,6 +26,7 @@ public sealed partial class SafeProcessHandle : SafeHandleZeroOrMinusOneIsInvali private readonly SafeWaitHandle? _handle; private readonly bool _releaseRef; + private readonly bool _isGroupLeader; internal SafeProcessHandle(int processId, SafeWaitHandle handle) : this(handle.DangerousGetHandle(), ownsHandle: true) @@ -35,10 +36,11 @@ internal SafeProcessHandle(int processId, SafeWaitHandle handle) : handle.DangerousAddRef(ref _releaseRef); } - private SafeProcessHandle(int pidfd, int pid) + private SafeProcessHandle(int pidfd, int pid, bool isGroupLeader) : this(existingHandle: (IntPtr)pidfd, ownsHandle: true) { ProcessId = pid; + _isGroupLeader = isGroupLeader; } protected override bool ReleaseHandle() @@ -66,7 +68,7 @@ private static SafeProcessHandle OpenCore(int processId) throw new Win32Exception(); } - return new SafeProcessHandle(pidfd, processId); + return new SafeProcessHandle(pidfd, processId, isGroupLeader: false); } private static unsafe SafeProcessHandle StartCore(ProcessStartOptions options, SafeFileHandle inputHandle, SafeFileHandle outputHandle, SafeFileHandle errorHandle, bool createSuspended) @@ -132,7 +134,7 @@ private static unsafe SafeProcessHandle StartCore(ProcessStartOptions options, S throw new Win32Exception(); } - return new SafeProcessHandle(pidfd == -1 ? NoPidFd : pidfd, pid); + return new SafeProcessHandle(pidfd, pid, options.CreateNewProcessGroup); } finally { @@ -180,7 +182,7 @@ private bool TryWaitForExitCore(int milliseconds, [NotNullWhen(true)] out Proces private ProcessExitStatus WaitForExitOrKillOnTimeoutCore(int milliseconds) { - switch (Interop.Sys.WaitForExitOrKillOnTimeout(this, ProcessId, milliseconds, out int exitCode, out int rawSignal, out int hasTimedout)) + switch (Interop.Sys.WaitForExitOrKillOnTimeout(this, ProcessId, _isGroupLeader, milliseconds, out int exitCode, out int rawSignal, out int hasTimedout)) { case -1: throw new Win32Exception(); @@ -245,7 +247,7 @@ private async Task WaitForExitOrKillOnCancellationAsyncCore(C case -1: throw new Win32Exception(); case 1: // canceled - bool wasKilled = KillCore(throwOnError: false); + bool wasKilled = KillCore(throwOnError: false, entireProcessGroup: _isGroupLeader); ProcessExitStatus status = WaitForExitCore(); return new ProcessExitStatus(status.ExitCode, wasKilled, status.Signal); default: diff --git a/src/libraries/System.Diagnostics.Process/src/Microsoft/Win32/SafeHandles/SafeProcessHandle.cs b/src/libraries/System.Diagnostics.Process/src/Microsoft/Win32/SafeHandles/SafeProcessHandle.cs index 2526ebef919147..076e41774de24d 100644 --- a/src/libraries/System.Diagnostics.Process/src/Microsoft/Win32/SafeHandles/SafeProcessHandle.cs +++ b/src/libraries/System.Diagnostics.Process/src/Microsoft/Win32/SafeHandles/SafeProcessHandle.cs @@ -244,10 +244,10 @@ public bool Kill() /// /// true if the process group was terminated; false if the process had already exited. /// - /// Thrown when the handle is invalid. On Windows, thrown when the process was not started with . + /// Thrown when the handle is invalid. On Windows, thrown when the process was not started with . /// Thrown when the kill operation fails for reasons other than the process having already exited. /// - /// On Windows, this API provides best-effort simulation of Unix process groups using an anonymous Windows job object. The process must be started with `` to enable it. All processes that inherited the Windows job object are killed. + /// On Windows, this API provides best-effort simulation of Unix process groups using an anonymous Windows job object. The process must be started with `` to enable it. All processes that inherited the Windows job object are killed. /// internal bool KillProcessGroup() { @@ -292,7 +292,7 @@ internal void Resume() /// /// /// is mapped to GenerateConsoleCtrlEvent(CTRL_C_EVENT). - /// The root process of the process group must re-enable the handler in order to receive this signal. + /// The root process of the process group must re-enable the handler in order to receive this signal. /// /// is mapped to GenerateConsoleCtrlEvent(CTRL_BREAK_EVENT). /// is mapped to . @@ -326,7 +326,7 @@ public void Signal(PosixSignal signal) /// /// /// is mapped to GenerateConsoleCtrlEvent(CTRL_C_EVENT). - /// The root process of the process group must re-enable the handler in order to receive this signal. + /// The root process of the process group must re-enable the handler in order to receive this signal. /// /// is mapped to GenerateConsoleCtrlEvent(CTRL_BREAK_EVENT). /// is mapped to . diff --git a/src/libraries/System.Diagnostics.Process/tests/SafeProcessHandleTests.cs b/src/libraries/System.Diagnostics.Process/tests/SafeProcessHandleTests.cs index db4022e3dcef6d..f73363527677f3 100644 --- a/src/libraries/System.Diagnostics.Process/tests/SafeProcessHandleTests.cs +++ b/src/libraries/System.Diagnostics.Process/tests/SafeProcessHandleTests.cs @@ -598,7 +598,6 @@ private static void VerifyProcessIsRunning(bool shouldExited, int processId) { using SafeProcessHandle grandChild = SafeProcessHandle.Open(processId); grandChild.Kill(); - grandChild.WaitForExit(); } catch (Win32Exception) when (shouldExited) { diff --git a/src/native/libs/System.Native/pal_process.c b/src/native/libs/System.Native/pal_process.c index 1c3d11d16c0c4b..f7432d247d17c5 100644 --- a/src/native/libs/System.Native/pal_process.c +++ b/src/native/libs/System.Native/pal_process.c @@ -1307,7 +1307,7 @@ int32_t SystemNative_TryWaitForExit(int32_t pidfd, int32_t pid, int32_t timeout_ #endif } -int32_t SystemNative_WaitForExitOrKillOnTimeout(int32_t pidfd, int32_t pid, int32_t timeout_ms, int32_t* out_exitCode, int32_t* out_signal, int32_t* out_timeout) +int32_t SystemNative_WaitForExitOrKillOnTimeout(int32_t pidfd, int32_t pid, int32_t isGroupLeader, int32_t timeout_ms, int32_t* out_exitCode, int32_t* out_signal, int32_t* out_timeout) { *out_timeout = 0; int ret = SystemNative_TryWaitForExit(pidfd, pid, timeout_ms, out_exitCode, out_signal); @@ -1317,7 +1317,15 @@ int32_t SystemNative_WaitForExitOrKillOnTimeout(int32_t pidfd, int32_t pid, int3 } *out_timeout = 1; - ret = SystemNative_SendSignal(pidfd, pid, (int32_t)PosixSignalSIGKILL); + if (isGroupLeader) + { + // Negative pid means to signal the process group + ret = SystemNative_SendSignal(-1, -pid, (int32_t)PosixSignalSIGKILL); + } + else + { + ret = SystemNative_SendSignal(pidfd, pid, (int32_t)PosixSignalSIGKILL); + } if (ret == -1) { diff --git a/src/native/libs/System.Native/pal_process.h b/src/native/libs/System.Native/pal_process.h index bd07f261e16467..ee8967e619d2ce 100644 --- a/src/native/libs/System.Native/pal_process.h +++ b/src/native/libs/System.Native/pal_process.h @@ -299,7 +299,7 @@ PALEXPORT int32_t SystemNative_TryWaitForExitCancellable(int32_t pidfd, int32_t * * Returns 0 on success, -1 on error (errno is set). */ -PALEXPORT int32_t SystemNative_WaitForExitOrKillOnTimeout(int32_t pidfd, int32_t pid, int32_t timeout_ms, int32_t* out_exitCode, int32_t* out_signal, int32_t* out_timeout); +PALEXPORT int32_t SystemNative_WaitForExitOrKillOnTimeout(int32_t pidfd, int32_t pid, int32_t isGroupLeader, int32_t timeout_ms, int32_t* out_exitCode, int32_t* out_signal, int32_t* out_timeout); /** * Tries to get the exit code of a process without blocking. From f91125c6d7101e39965410d01c49ab7c7ddfaa10 Mon Sep 17 00:00:00 2001 From: Adam Sitnik Date: Sat, 7 Mar 2026 23:33:05 +0100 Subject: [PATCH 15/21] Apply suggestion from @adamsitnik --- src/native/libs/System.Native/pal_process_wasi.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/native/libs/System.Native/pal_process_wasi.c b/src/native/libs/System.Native/pal_process_wasi.c index eb053ec349d168..644f75478e64e9 100644 --- a/src/native/libs/System.Native/pal_process_wasi.c +++ b/src/native/libs/System.Native/pal_process_wasi.c @@ -160,7 +160,7 @@ int32_t SystemNative_TryWaitForExitCancellable(int32_t pidfd, int32_t pid, int32 return -1; } -int32_t SystemNative_WaitForExitOrKillOnTimeout(int32_t pidfd, int32_t pid, int32_t timeout_ms, int32_t* out_exitCode, int32_t* out_signal, int32_t* out_timeout) +int32_t SystemNative_WaitForExitOrKillOnTimeout(int32_t pidfd, int32_t pid, int32_t isGroupLeader, int32_t timeout_ms, int32_t* out_exitCode, int32_t* out_signal, int32_t* out_timeout) { errno = ENOTSUP; return -1; From 726017d22c3238c69e5aa563eceb940e7fcc813f Mon Sep 17 00:00:00 2001 From: Adam Sitnik Date: Mon, 9 Mar 2026 13:23:37 +0100 Subject: [PATCH 16/21] fix KillOnParentExit_KillsTheChild_WhenParentExits test failure: exit codes on Unix can't be > 255, so we can't return PID as exit code --- .../tests/SafeProcessHandleTests.cs | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/libraries/System.Diagnostics.Process/tests/SafeProcessHandleTests.cs b/src/libraries/System.Diagnostics.Process/tests/SafeProcessHandleTests.cs index f73363527677f3..eddca710d5bebd 100644 --- a/src/libraries/System.Diagnostics.Process/tests/SafeProcessHandleTests.cs +++ b/src/libraries/System.Diagnostics.Process/tests/SafeProcessHandleTests.cs @@ -503,6 +503,7 @@ public async Task KillProcessGroup_Kills_EntireProcessTree_OnWindows(bool create public void KillOnParentExit_KillsTheChild_WhenParentExits(bool enabled) { RemoteInvokeOptions remoteInvokeOptions = new() { CheckExitCode = false }; + remoteInvokeOptions.StartInfo.RedirectStandardOutput = true; using RemoteInvokeHandle remoteHandle = RemoteExecutor.Invoke( (enabledStr) => @@ -511,15 +512,19 @@ public void KillOnParentExit_KillsTheChild_WhenParentExits(bool enabled) processStartOptions.KillOnParentExit = bool.Parse(enabledStr); using SafeProcessHandle started = SafeProcessHandle.Start(processStartOptions, input: null, output: null, error: null); - return started.ProcessId; // return grand child pid as exit code + Console.WriteLine(started.ProcessId); + + return 0; }, arg: enabled.ToString(), remoteInvokeOptions); + string firstLine = remoteHandle.Process.StandardOutput.ReadLine(); + int grandChildPid = int.Parse(firstLine); remoteHandle.Process.WaitForExit(); // It's currently not implemented on macOS. - VerifyProcessIsRunning(shouldExited: enabled && !OperatingSystem.IsMacOS(), remoteHandle.ExitCode); + VerifyProcessIsRunning(shouldExited: enabled && !OperatingSystem.IsMacOS(), grandChildPid); } [ConditionalTheory(typeof(RemoteExecutor), nameof(RemoteExecutor.IsSupported))] From 33db32ffe1bae2bc751e31f7677e35d788ef47c4 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 9 Mar 2026 12:41:48 +0000 Subject: [PATCH 17/21] Fix _isGroupLeader correctness: detect group leader status in OpenCore via getpgid Co-authored-by: adamsitnik <6011991+adamsitnik@users.noreply.github.com> --- .../src/Interop/Unix/System.Native/Interop.SpawnProcess.cs | 2 +- .../src/Microsoft/Win32/SafeHandles/SafeProcessHandle.Unix.cs | 4 ++-- src/native/libs/System.Native/pal_process.c | 3 ++- src/native/libs/System.Native/pal_process.h | 2 +- src/native/libs/System.Native/pal_process_wasi.c | 2 +- 5 files changed, 7 insertions(+), 6 deletions(-) diff --git a/src/libraries/Common/src/Interop/Unix/System.Native/Interop.SpawnProcess.cs b/src/libraries/Common/src/Interop/Unix/System.Native/Interop.SpawnProcess.cs index d094cbdfcc501e..2dee43ba05dfec 100644 --- a/src/libraries/Common/src/Interop/Unix/System.Native/Interop.SpawnProcess.cs +++ b/src/libraries/Common/src/Interop/Unix/System.Native/Interop.SpawnProcess.cs @@ -45,6 +45,6 @@ internal static unsafe partial int SpawnProcess( internal static partial int TryGetExitCode(SafeProcessHandle pidfd, int pid, out int exitCode, out int signal); [LibraryImport(Libraries.SystemNative, EntryPoint = "SystemNative_OpenProcess", SetLastError = true)] - internal static partial int OpenProcess(int pid, out int outPidfd); + internal static partial int OpenProcess(int pid, out int outPidfd, out int outIsGroupLeader); } } 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 e0a85556a4d6d9..f1573cddcd476f 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 @@ -61,14 +61,14 @@ protected override bool ReleaseHandle() private static SafeProcessHandle OpenCore(int processId) { - int result = Interop.Sys.OpenProcess(processId, out int pidfd); + int result = Interop.Sys.OpenProcess(processId, out int pidfd, out int isGroupLeader); if (result == -1) { throw new Win32Exception(); } - return new SafeProcessHandle(pidfd, processId, isGroupLeader: false); + return new SafeProcessHandle(pidfd, processId, isGroupLeader: isGroupLeader != 0); } private static unsafe SafeProcessHandle StartCore(ProcessStartOptions options, SafeFileHandle inputHandle, SafeFileHandle outputHandle, SafeFileHandle errorHandle, bool createSuspended) diff --git a/src/native/libs/System.Native/pal_process.c b/src/native/libs/System.Native/pal_process.c index f7432d247d17c5..be8d0555842004 100644 --- a/src/native/libs/System.Native/pal_process.c +++ b/src/native/libs/System.Native/pal_process.c @@ -1342,9 +1342,10 @@ int32_t SystemNative_WaitForExitOrKillOnTimeout(int32_t pidfd, int32_t pid, int3 return SystemNative_WaitForExitAndReap(pidfd, pid, out_exitCode, out_signal); } -int32_t SystemNative_OpenProcess(int32_t pid, int32_t* out_pidfd) +int32_t SystemNative_OpenProcess(int32_t pid, int32_t* out_pidfd, int32_t* out_isGroupLeader) { *out_pidfd = INT_MIN; + *out_isGroupLeader = (getpgid(pid) == pid) ? 1 : 0; return kill(pid, 0); } diff --git a/src/native/libs/System.Native/pal_process.h b/src/native/libs/System.Native/pal_process.h index ee8967e619d2ce..cbf1051e54b039 100644 --- a/src/native/libs/System.Native/pal_process.h +++ b/src/native/libs/System.Native/pal_process.h @@ -313,4 +313,4 @@ PALEXPORT int32_t SystemNative_TryGetExitCode(int32_t pidfd, int32_t pid, int32_ * * Returns 0 on success, -1 on error (errno is set). */ -PALEXPORT int32_t SystemNative_OpenProcess(int32_t pid, int32_t* out_pidfd); +PALEXPORT int32_t SystemNative_OpenProcess(int32_t pid, int32_t* out_pidfd, int32_t* out_isGroupLeader); diff --git a/src/native/libs/System.Native/pal_process_wasi.c b/src/native/libs/System.Native/pal_process_wasi.c index 644f75478e64e9..fb002fb520a23c 100644 --- a/src/native/libs/System.Native/pal_process_wasi.c +++ b/src/native/libs/System.Native/pal_process_wasi.c @@ -172,7 +172,7 @@ int32_t SystemNative_TryGetExitCode(int32_t pidfd, int32_t pid, int32_t* out_exi return -1; } -int32_t SystemNative_OpenProcess(int32_t pid, int32_t* out_pidfd) +int32_t SystemNative_OpenProcess(int32_t pid, int32_t* out_pidfd, int32_t* out_isGroupLeader) { errno = ENOTSUP; return -1; From af33fd5ffa22ba1d642b01fb3f0e6e17b10de604 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 9 Mar 2026 12:54:57 +0000 Subject: [PATCH 18/21] Remove 'out' prefix from OpenProcess parameter names since out keyword is already used Co-authored-by: adamsitnik <6011991+adamsitnik@users.noreply.github.com> --- .../src/Interop/Unix/System.Native/Interop.SpawnProcess.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libraries/Common/src/Interop/Unix/System.Native/Interop.SpawnProcess.cs b/src/libraries/Common/src/Interop/Unix/System.Native/Interop.SpawnProcess.cs index 2dee43ba05dfec..86ffbb4b77c63c 100644 --- a/src/libraries/Common/src/Interop/Unix/System.Native/Interop.SpawnProcess.cs +++ b/src/libraries/Common/src/Interop/Unix/System.Native/Interop.SpawnProcess.cs @@ -45,6 +45,6 @@ internal static unsafe partial int SpawnProcess( internal static partial int TryGetExitCode(SafeProcessHandle pidfd, int pid, out int exitCode, out int signal); [LibraryImport(Libraries.SystemNative, EntryPoint = "SystemNative_OpenProcess", SetLastError = true)] - internal static partial int OpenProcess(int pid, out int outPidfd, out int outIsGroupLeader); + internal static partial int OpenProcess(int pid, out int pidfd, out int isGroupLeader); } } From bf28925e121610a0bd5b603acb533b8e564e08e6 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 9 Mar 2026 13:11:48 +0000 Subject: [PATCH 19/21] Implement terminal configuration for SafeProcessHandle and move logic to ProcessUtils - Add IsATty(SafeFileHandle) overload to Interop.IsATty.cs - Move ConfigureTerminalForChildProcesses logic from Process.*.cs to ProcessUtils.*.cs - Update Process.*.cs to delegate to ProcessUtils - Add _usesTerminal field to SafeProcessHandle.Unix.cs - Detect terminal usage in StartCore with IsATty - Add OnProcessExited helper, called from all WaitForExit* methods - Update csproj with new files and IsATty reference Co-authored-by: adamsitnik <6011991+adamsitnik@users.noreply.github.com> --- .../Unix/System.Native/Interop.IsATty.cs | 5 + .../SafeHandles/SafeProcessHandle.Unix.cs | 104 +++++++++++++----- .../src/System.Diagnostics.Process.csproj | 4 + ...ConfigureTerminalForChildProcesses.Unix.cs | 56 +--------- ...ConfigureTerminalForChildProcesses.Unix.cs | 64 +++++++++++ ....ConfigureTerminalForChildProcesses.iOS.cs | 22 ++++ 6 files changed, 177 insertions(+), 78 deletions(-) create mode 100644 src/libraries/System.Diagnostics.Process/src/System/Diagnostics/ProcessUtils.ConfigureTerminalForChildProcesses.Unix.cs create mode 100644 src/libraries/System.Diagnostics.Process/src/System/Diagnostics/ProcessUtils.ConfigureTerminalForChildProcesses.iOS.cs diff --git a/src/libraries/Common/src/Interop/Unix/System.Native/Interop.IsATty.cs b/src/libraries/Common/src/Interop/Unix/System.Native/Interop.IsATty.cs index beb79d77d5165b..ae905f656dc549 100644 --- a/src/libraries/Common/src/Interop/Unix/System.Native/Interop.IsATty.cs +++ b/src/libraries/Common/src/Interop/Unix/System.Native/Interop.IsATty.cs @@ -3,6 +3,7 @@ using System; using System.Runtime.InteropServices; +using Microsoft.Win32.SafeHandles; internal static partial class Interop { @@ -11,5 +12,9 @@ internal static partial class Sys [LibraryImport(Libraries.SystemNative, EntryPoint = "SystemNative_IsATty")] [return: MarshalAs(UnmanagedType.Bool)] internal static partial bool IsATty(IntPtr fd); + + [LibraryImport(Libraries.SystemNative, EntryPoint = "SystemNative_IsATty")] + [return: MarshalAs(UnmanagedType.Bool)] + internal static partial bool IsATty(SafeFileHandle fd); } } 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 f1573cddcd476f..e9c2d8ae12068a 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 @@ -27,6 +27,7 @@ public sealed partial class SafeProcessHandle : SafeHandleZeroOrMinusOneIsInvali private readonly SafeWaitHandle? _handle; private readonly bool _releaseRef; private readonly bool _isGroupLeader; + private readonly bool _usesTerminal; internal SafeProcessHandle(int processId, SafeWaitHandle handle) : this(handle.DangerousGetHandle(), ownsHandle: true) @@ -36,11 +37,12 @@ internal SafeProcessHandle(int processId, SafeWaitHandle handle) : handle.DangerousAddRef(ref _releaseRef); } - private SafeProcessHandle(int pidfd, int pid, bool isGroupLeader) + private SafeProcessHandle(int pidfd, int pid, bool isGroupLeader, bool usesTerminal) : this(existingHandle: (IntPtr)pidfd, ownsHandle: true) { ProcessId = pid; _isGroupLeader = isGroupLeader; + _usesTerminal = usesTerminal; } protected override bool ReleaseHandle() @@ -68,7 +70,7 @@ private static SafeProcessHandle OpenCore(int processId) throw new Win32Exception(); } - return new SafeProcessHandle(pidfd, processId, isGroupLeader: isGroupLeader != 0); + return new SafeProcessHandle(pidfd, processId, isGroupLeader: isGroupLeader != 0, usesTerminal: false); } private static unsafe SafeProcessHandle StartCore(ProcessStartOptions options, SafeFileHandle inputHandle, SafeFileHandle outputHandle, SafeFileHandle errorHandle, bool createSuspended) @@ -80,6 +82,12 @@ private static unsafe SafeProcessHandle StartCore(ProcessStartOptions options, S // If not accessed, pass null to use the current environment (environ) string[]? envp = options.HasEnvironmentBeenAccessed ? ProcessUtils.CreateEnvp(options.Environment) : null; + // .NET applications don't echo characters unless there is a Console.Read operation. + // Unix applications expect the terminal to be in an echoing state by default. + // To support processes that interact with the terminal (e.g. 'vi'), we need to configure the + // terminal to echo. We keep this configuration as long as there are children possibly using the terminal. + bool usesTerminal = Interop.Sys.IsATty(inputHandle) || Interop.Sys.IsATty(outputHandle) || Interop.Sys.IsATty(errorHandle); + byte** argvPtr = null; byte** envpPtr = null; int* inheritedHandlesPtr = null; @@ -112,29 +120,54 @@ private static unsafe SafeProcessHandle StartCore(ProcessStartOptions options, S } } - // Call native library to spawn process - int result = Interop.Sys.SpawnProcess( - options.FileName, - argvPtr, - envpPtr, - options.WorkingDirectory, - inheritedHandlesPtr, - inheritedHandlesCount, - inputHandle, - outputHandle, - errorHandle, - options.KillOnParentExit ? 1 : 0, - createSuspended ? 1 : 0, - options.CreateNewProcessGroup ? 1 : 0, - out int pid, - out int pidfd); - - if (result == -1) + // Lock to avoid races with OnSigChild + // By using a ReaderWriterLock we allow multiple processes to start concurrently. + bool spawnSucceeded = false; + ProcessUtils.s_processStartLock.EnterReadLock(); + try { - throw new Win32Exception(); + if (usesTerminal) + { + ProcessUtils.ConfigureTerminalForChildProcesses(1); + } + + // Call native library to spawn process + int result = Interop.Sys.SpawnProcess( + options.FileName, + argvPtr, + envpPtr, + options.WorkingDirectory, + inheritedHandlesPtr, + inheritedHandlesCount, + inputHandle, + outputHandle, + errorHandle, + options.KillOnParentExit ? 1 : 0, + createSuspended ? 1 : 0, + options.CreateNewProcessGroup ? 1 : 0, + out int pid, + out int pidfd); + + if (result == -1) + { + throw new Win32Exception(); + } + + spawnSucceeded = true; + return new SafeProcessHandle(pidfd, pid, options.CreateNewProcessGroup, usesTerminal); } + finally + { + ProcessUtils.s_processStartLock.ExitReadLock(); - return new SafeProcessHandle(pidfd, pid, options.CreateNewProcessGroup); + if (!spawnSucceeded && usesTerminal) + { + // We failed to launch a child that could use the terminal. + ProcessUtils.s_processStartLock.EnterWriteLock(); + ProcessUtils.ConfigureTerminalForChildProcesses(-1); + ProcessUtils.s_processStartLock.ExitWriteLock(); + } + } } finally { @@ -159,7 +192,9 @@ private ProcessExitStatus WaitForExitCore() case -1: throw new Win32Exception(); default: - return new(exitCode, false, rawSignal != 0 ? (PosixSignal)rawSignal : null); + ProcessExitStatus status = new(exitCode, false, rawSignal != 0 ? (PosixSignal)rawSignal : null); + OnProcessExited(); + return status; } } @@ -174,6 +209,7 @@ private bool TryWaitForExitCore(int milliseconds, [NotNullWhen(true)] out Proces return false; default: exitStatus = new(exitCode, false, rawSignal != 0 ? (PosixSignal)rawSignal : null); + OnProcessExited(); return true; } } @@ -187,7 +223,9 @@ private ProcessExitStatus WaitForExitOrKillOnTimeoutCore(int milliseconds) case -1: throw new Win32Exception(); default: - return new(exitCode, hasTimedout == 1, rawSignal != 0 ? (PosixSignal)rawSignal : null); + ProcessExitStatus status = new(exitCode, hasTimedout == 1, rawSignal != 0 ? (PosixSignal)rawSignal : null); + OnProcessExited(); + return status; } } @@ -217,7 +255,9 @@ private async Task WaitForExitAsyncCore(CancellationToken can case 1: // canceled throw new OperationCanceledException(cancellationToken); default: - return new ProcessExitStatus(exitCode, false, rawSignal != 0 ? (PosixSignal)rawSignal : null); + ProcessExitStatus status = new(exitCode, false, rawSignal != 0 ? (PosixSignal)rawSignal : null); + OnProcessExited(); + return status; } }, cancellationToken).ConfigureAwait(false); } @@ -251,7 +291,9 @@ private async Task WaitForExitOrKillOnCancellationAsyncCore(C ProcessExitStatus status = WaitForExitCore(); return new ProcessExitStatus(status.ExitCode, wasKilled, status.Signal); default: - return new ProcessExitStatus(exitCode, false, rawSignal != 0 ? (PosixSignal)rawSignal : null); + ProcessExitStatus exitStatus = new(exitCode, false, rawSignal != 0 ? (PosixSignal)rawSignal : null); + OnProcessExited(); + return exitStatus; } }, cancellationToken).ConfigureAwait(false); } @@ -269,6 +311,16 @@ private static unsafe void CreatePipe(out SafeFileHandle readHandle, out SafeFil writeHandle = new SafeFileHandle((IntPtr)fds[Interop.Sys.WriteEndOfPipe], ownsHandle: true); } + private void OnProcessExited() + { + if (_usesTerminal) + { + ProcessUtils.s_processStartLock.EnterWriteLock(); + ProcessUtils.ConfigureTerminalForChildProcesses(-1); + ProcessUtils.s_processStartLock.ExitWriteLock(); + } + } + internal bool KillCore(bool throwOnError, bool entireProcessGroup = false) { // If entireProcessGroup is true, send to -pid (negative pid), don't use pidfd. 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 304697a27bf1ba..f77832b6323315 100644 --- a/src/libraries/System.Diagnostics.Process/src/System.Diagnostics.Process.csproj +++ b/src/libraries/System.Diagnostics.Process/src/System.Diagnostics.Process.csproj @@ -308,16 +308,20 @@ Link="Common\Interop\Unix\Interop.GetEUid.cs" /> + + + diff --git a/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/Process.ConfigureTerminalForChildProcesses.Unix.cs b/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/Process.ConfigureTerminalForChildProcesses.Unix.cs index da7c8948cd0432..43baf4f9910330 100644 --- a/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/Process.ConfigureTerminalForChildProcesses.Unix.cs +++ b/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/Process.ConfigureTerminalForChildProcesses.Unix.cs @@ -1,64 +1,16 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -using System.Runtime.InteropServices; -using System.Threading; - namespace System.Diagnostics { public partial class Process { - private static int s_childrenUsingTerminalCount; - internal static void ConfigureTerminalForChildProcesses(int increment, bool configureConsole = true) - { - Debug.Assert(increment != 0); - - int childrenUsingTerminalRemaining = Interlocked.Add(ref s_childrenUsingTerminalCount, increment); - if (increment > 0) - { - Debug.Assert(ProcessUtils.s_processStartLock.IsReadLockHeld); - Debug.Assert(configureConsole); - - // At least one child is using the terminal. - Interop.Sys.ConfigureTerminalForChildProcess(childUsesTerminal: true); - } - else - { - Debug.Assert(ProcessUtils.s_processStartLock.IsWriteLockHeld); - - if (childrenUsingTerminalRemaining == 0 && configureConsole) - { - // No more children are using the terminal. - Interop.Sys.ConfigureTerminalForChildProcess(childUsesTerminal: false); - } - } - } - - private static unsafe void SetDelayedSigChildConsoleConfigurationHandler() - { - Interop.Sys.SetDelayedSigChildConsoleConfigurationHandler(&DelayedSigChildConsoleConfiguration); - } + => ProcessUtils.ConfigureTerminalForChildProcesses(increment, configureConsole); - [UnmanagedCallersOnly] - private static void DelayedSigChildConsoleConfiguration() - { - // Lock to avoid races with Process.Start - ProcessUtils.s_processStartLock.EnterWriteLock(); - try - { - if (s_childrenUsingTerminalCount == 0) - { - // No more children are using the terminal. - Interop.Sys.ConfigureTerminalForChildProcess(childUsesTerminal: false); - } - } - finally - { - ProcessUtils.s_processStartLock.ExitWriteLock(); - } - } + private static void SetDelayedSigChildConsoleConfigurationHandler() + => ProcessUtils.SetDelayedSigChildConsoleConfigurationHandler(); - private static bool AreChildrenUsingTerminal => s_childrenUsingTerminalCount > 0; + private static bool AreChildrenUsingTerminal => ProcessUtils.AreChildrenUsingTerminal; } } diff --git a/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/ProcessUtils.ConfigureTerminalForChildProcesses.Unix.cs b/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/ProcessUtils.ConfigureTerminalForChildProcesses.Unix.cs new file mode 100644 index 00000000000000..c5adac1723c4f1 --- /dev/null +++ b/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/ProcessUtils.ConfigureTerminalForChildProcesses.Unix.cs @@ -0,0 +1,64 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Runtime.InteropServices; +using System.Threading; + +namespace System.Diagnostics +{ + internal static partial class ProcessUtils + { + private static int s_childrenUsingTerminalCount; + + internal static void ConfigureTerminalForChildProcesses(int increment, bool configureConsole = true) + { + Debug.Assert(increment != 0); + + int childrenUsingTerminalRemaining = Interlocked.Add(ref s_childrenUsingTerminalCount, increment); + if (increment > 0) + { + Debug.Assert(ProcessUtils.s_processStartLock.IsReadLockHeld); + Debug.Assert(configureConsole); + + // At least one child is using the terminal. + Interop.Sys.ConfigureTerminalForChildProcess(childUsesTerminal: true); + } + else + { + Debug.Assert(ProcessUtils.s_processStartLock.IsWriteLockHeld); + + if (childrenUsingTerminalRemaining == 0 && configureConsole) + { + // No more children are using the terminal. + Interop.Sys.ConfigureTerminalForChildProcess(childUsesTerminal: false); + } + } + } + + internal static unsafe void SetDelayedSigChildConsoleConfigurationHandler() + { + Interop.Sys.SetDelayedSigChildConsoleConfigurationHandler(&DelayedSigChildConsoleConfiguration); + } + + [UnmanagedCallersOnly] + private static void DelayedSigChildConsoleConfiguration() + { + // Lock to avoid races with Process.Start + ProcessUtils.s_processStartLock.EnterWriteLock(); + try + { + if (s_childrenUsingTerminalCount == 0) + { + // No more children are using the terminal. + Interop.Sys.ConfigureTerminalForChildProcess(childUsesTerminal: false); + } + } + finally + { + ProcessUtils.s_processStartLock.ExitWriteLock(); + } + } + + internal static bool AreChildrenUsingTerminal => s_childrenUsingTerminalCount > 0; + } +} diff --git a/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/ProcessUtils.ConfigureTerminalForChildProcesses.iOS.cs b/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/ProcessUtils.ConfigureTerminalForChildProcesses.iOS.cs new file mode 100644 index 00000000000000..5f3fa16dfc9163 --- /dev/null +++ b/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/ProcessUtils.ConfigureTerminalForChildProcesses.iOS.cs @@ -0,0 +1,22 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace System.Diagnostics +{ + internal static partial class ProcessUtils + { + /// These methods are used on other Unix systems to track how many children use the terminal, + /// and update the terminal configuration when necessary. + + [Conditional("unnecessary")] + internal static void ConfigureTerminalForChildProcesses(int increment, bool configureConsole = true) + { + } + + internal static void SetDelayedSigChildConsoleConfigurationHandler() + { + } + + internal static bool AreChildrenUsingTerminal => false; + } +} From e88f121c120bdb4ae53b4038038bc4095cac0479 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 9 Mar 2026 13:36:10 +0000 Subject: [PATCH 20/21] Remove Process.ConfigureTerminalForChildProcesses wrapper files, update all callers to use ProcessUtils directly Co-authored-by: adamsitnik <6011991+adamsitnik@users.noreply.github.com> --- .../src/System.Diagnostics.Process.csproj | 2 -- ...ConfigureTerminalForChildProcesses.Unix.cs | 16 --------------- ....ConfigureTerminalForChildProcesses.iOS.cs | 20 ------------------- .../src/System/Diagnostics/Process.Unix.cs | 10 +++++----- .../Diagnostics/ProcessWaitState.Unix.cs | 2 +- 5 files changed, 6 insertions(+), 44 deletions(-) delete mode 100644 src/libraries/System.Diagnostics.Process/src/System/Diagnostics/Process.ConfigureTerminalForChildProcesses.Unix.cs delete mode 100644 src/libraries/System.Diagnostics.Process/src/System/Diagnostics/Process.ConfigureTerminalForChildProcesses.iOS.cs 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 f77832b6323315..71694cf65c3170 100644 --- a/src/libraries/System.Diagnostics.Process/src/System.Diagnostics.Process.csproj +++ b/src/libraries/System.Diagnostics.Process/src/System.Diagnostics.Process.csproj @@ -315,12 +315,10 @@ - - diff --git a/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/Process.ConfigureTerminalForChildProcesses.Unix.cs b/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/Process.ConfigureTerminalForChildProcesses.Unix.cs deleted file mode 100644 index 43baf4f9910330..00000000000000 --- a/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/Process.ConfigureTerminalForChildProcesses.Unix.cs +++ /dev/null @@ -1,16 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -namespace System.Diagnostics -{ - public partial class Process - { - internal static void ConfigureTerminalForChildProcesses(int increment, bool configureConsole = true) - => ProcessUtils.ConfigureTerminalForChildProcesses(increment, configureConsole); - - private static void SetDelayedSigChildConsoleConfigurationHandler() - => ProcessUtils.SetDelayedSigChildConsoleConfigurationHandler(); - - private static bool AreChildrenUsingTerminal => ProcessUtils.AreChildrenUsingTerminal; - } -} diff --git a/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/Process.ConfigureTerminalForChildProcesses.iOS.cs b/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/Process.ConfigureTerminalForChildProcesses.iOS.cs deleted file mode 100644 index 44821b211d4923..00000000000000 --- a/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/Process.ConfigureTerminalForChildProcesses.iOS.cs +++ /dev/null @@ -1,20 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -namespace System.Diagnostics -{ - public partial class Process - { - /// These methods are used on other Unix systems to track how many children use the terminal, - /// and update the terminal configuration when necessary. - - [Conditional("unnecessary")] - internal static void ConfigureTerminalForChildProcesses(int increment, bool configureConsole = true) - { - } - - static partial void SetDelayedSigChildConsoleConfigurationHandler(); - - private static bool AreChildrenUsingTerminal => false; - } -} 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 ea80e0180cfc73..ac277e98f24afa 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 @@ -505,7 +505,7 @@ private bool ForkAndExecProcess( { if (usesTerminal) { - ConfigureTerminalForChildProcesses(1); + ProcessUtils.ConfigureTerminalForChildProcesses(1); } int childPid; @@ -553,7 +553,7 @@ private bool ForkAndExecProcess( { // We failed to launch a child that could use the terminal. ProcessUtils.s_processStartLock.EnterWriteLock(); - ConfigureTerminalForChildProcesses(-1); + ProcessUtils.ConfigureTerminalForChildProcesses(-1); ProcessUtils.s_processStartLock.ExitWriteLock(); } } @@ -979,7 +979,7 @@ private static unsafe void EnsureInitialized() // Register our callback. Interop.Sys.RegisterForSigChld(&OnSigChild); - SetDelayedSigChildConsoleConfigurationHandler(); + ProcessUtils.SetDelayedSigChildConsoleConfigurationHandler(); s_initialized = true; } @@ -999,9 +999,9 @@ private static int OnSigChild(int reapAll, int configureConsole) ProcessUtils.s_processStartLock.EnterWriteLock(); try { - bool childrenUsingTerminalPre = AreChildrenUsingTerminal; + bool childrenUsingTerminalPre = ProcessUtils.AreChildrenUsingTerminal; ProcessWaitState.CheckChildren(reapAll != 0, configureConsole != 0); - bool childrenUsingTerminalPost = AreChildrenUsingTerminal; + bool childrenUsingTerminalPost = ProcessUtils.AreChildrenUsingTerminal; // return whether console configuration was skipped. return childrenUsingTerminalPre && !childrenUsingTerminalPost && configureConsole == 0 ? 1 : 0; diff --git a/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/ProcessWaitState.Unix.cs b/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/ProcessWaitState.Unix.cs index 042a95b1950c64..94c1355558dfc2 100644 --- a/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/ProcessWaitState.Unix.cs +++ b/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/ProcessWaitState.Unix.cs @@ -550,7 +550,7 @@ private void ChildReaped(int exitCode, bool configureConsole) if (_usesTerminal) { // Update terminal settings before calling SetExited. - Process.ConfigureTerminalForChildProcesses(-1, configureConsole); + ProcessUtils.ConfigureTerminalForChildProcesses(-1, configureConsole); } SetExited(); From a247583f60aecf5583a747fd6deb2371cad24470 Mon Sep 17 00:00:00 2001 From: Adam Sitnik Date: Tue, 10 Mar 2026 14:23:49 +0100 Subject: [PATCH 21/21] Does AV in child process causes the test runner to stop due to SIGSEGV ? --- .../System.Diagnostics.Process/tests/SafeProcessHandleTests.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/libraries/System.Diagnostics.Process/tests/SafeProcessHandleTests.cs b/src/libraries/System.Diagnostics.Process/tests/SafeProcessHandleTests.cs index eddca710d5bebd..0aa9fbfaef29db 100644 --- a/src/libraries/System.Diagnostics.Process/tests/SafeProcessHandleTests.cs +++ b/src/libraries/System.Diagnostics.Process/tests/SafeProcessHandleTests.cs @@ -563,6 +563,7 @@ public void KillOnParentExit_KillsTheChild_WhenParentIsKilled(bool enabled) [ConditionalTheory(typeof(RemoteExecutor), nameof(RemoteExecutor.IsSupported))] [InlineData(true)] [InlineData(false)] + [PlatformSpecific(TestPlatforms.Windows)] public void KillOnParentExit_KillsTheChild_WhenParentCrashes(bool enabled) { RemoteInvokeOptions remoteInvokeOptions = new() { CheckExitCode = false };