diff --git a/eng/pipelines/runtime.yml b/eng/pipelines/runtime.yml index 2c8dc82291b3e9..e4690b62f54c69 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.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.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/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..86ffbb4b77c63c --- /dev/null +++ b/src/libraries/Common/src/Interop/Unix/System.Native/Interop.SpawnProcess.cs @@ -0,0 +1,50 @@ +// 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, + string? workingDir, + int* inheritedHandles, + int inheritedHandlesCount, + SafeHandle stdinFd, + SafeHandle stdoutFd, + SafeHandle stderrFd, + int killOnParentDeath, + int createSuspended, + int createNewProcessGroup, + 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); + + [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, 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); + + [LibraryImport(Libraries.SystemNative, EntryPoint = "SystemNative_OpenProcess", SetLastError = true)] + internal static partial int OpenProcess(int pid, out int pidfd, out int isGroupLeader); + } +} 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 2bb261112683c1..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 @@ -21,8 +21,13 @@ 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; + private readonly bool _isGroupLeader; + private readonly bool _usesTerminal; internal SafeProcessHandle(int processId, SafeWaitHandle handle) : this(handle.DangerousGetHandle(), ownsHandle: true) @@ -32,36 +37,341 @@ internal SafeProcessHandle(int processId, SafeWaitHandle handle) : handle.DangerousAddRef(ref _releaseRef); } + 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() { if (_releaseRef) { 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, out int isGroupLeader); + + if (result == -1) + { + throw new Win32Exception(); + } + + 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) + { + // Prepare arguments array (argv) + string[] argv = [options.FileName, .. options.Arguments]; + + // 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; + + // .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; + int inheritedHandlesCount = 0; + SafeHandle[]? handlesToRelease = null; + + try + { + ProcessUtils.AllocNullTerminatedArray(argv, ref argvPtr); + + // Only allocate envp if the user has accessed the environment + if (envp is not null) + { + ProcessUtils.AllocNullTerminatedArray(envp, ref envpPtr); + } + + // Allocate and copy inherited handles if provided + 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]; + } + } + + // 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 + { + 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(); + + 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 + { + 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(); + } + } + } + } + + private ProcessExitStatus WaitForExitCore() + { + switch (Interop.Sys.WaitForExitAndReap(this, ProcessId, out int exitCode, out int rawSignal)) + { + case -1: + throw new Win32Exception(); + default: + ProcessExitStatus status = new(exitCode, false, rawSignal != 0 ? (PosixSignal)rawSignal : null); + OnProcessExited(); + return status; + } + } + + 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: + throw new Win32Exception(); + case 1: // timeout + exitStatus = null; + return false; + default: + exitStatus = new(exitCode, false, rawSignal != 0 ? (PosixSignal)rawSignal : null); + OnProcessExited(); + return true; + } + } + + private static int GetProcessIdCore() => throw new PlatformNotSupportedException(); + + private ProcessExitStatus WaitForExitOrKillOnTimeoutCore(int milliseconds) + { + switch (Interop.Sys.WaitForExitOrKillOnTimeout(this, ProcessId, _isGroupLeader, milliseconds, out int exitCode, out int rawSignal, out int hasTimedout)) + { + case -1: + throw new Win32Exception(); + default: + ProcessExitStatus status = new(exitCode, hasTimedout == 1, rawSignal != 0 ? (PosixSignal)rawSignal : null); + OnProcessExited(); + return status; } - return true; } - private int GetProcessIdCore() => throw new NotImplementedException(); + private async Task WaitForExitAsyncCore(CancellationToken cancellationToken) + { + if (!cancellationToken.CanBeCanceled) + { + return await Task.Run(WaitForExitCore, cancellationToken).ConfigureAwait(false); + } + + CreatePipe(out SafeFileHandle readHandle, out SafeFileHandle writeHandle); - private static SafeProcessHandle OpenCore(int processId) => throw new NotImplementedException(); + 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); - private static SafeProcessHandle StartCore(ProcessStartOptions options, SafeFileHandle inputHandle, SafeFileHandle outputHandle, SafeFileHandle errorHandle, bool createSuspended) => throw new NotImplementedException(); + return await Task.Run(() => + { + switch (Interop.Sys.TryWaitForExitCancellable(this, ProcessId, (int)readHandle.DangerousGetHandle(), out int exitCode, out int rawSignal)) + { + case -1: + throw new Win32Exception(); + case 1: // canceled + throw new OperationCanceledException(cancellationToken); + default: + ProcessExitStatus status = new(exitCode, false, rawSignal != 0 ? (PosixSignal)rawSignal : null); + OnProcessExited(); + return status; + } + }, cancellationToken).ConfigureAwait(false); + } + } - private ProcessExitStatus WaitForExitCore() => throw new NotImplementedException(); + private async Task WaitForExitOrKillOnCancellationAsyncCore(CancellationToken cancellationToken) + { + if (!cancellationToken.CanBeCanceled) + { + return await Task.Run(WaitForExitCore, cancellationToken).ConfigureAwait(false); + } - private bool TryWaitForExitCore(int milliseconds, [NotNullWhen(true)] out ProcessExitStatus? exitStatus) => throw new NotImplementedException(); + CreatePipe(out SafeFileHandle readHandle, out SafeFileHandle writeHandle); - private ProcessExitStatus WaitForExitOrKillOnTimeoutCore(int milliseconds) => throw new NotImplementedException(); + 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); - private Task WaitForExitAsyncCore(CancellationToken cancellationToken) => throw new NotImplementedException(); + return await Task.Run(() => + { + switch (Interop.Sys.TryWaitForExitCancellable(this, ProcessId, (int)readHandle.DangerousGetHandle(), out int exitCode, out int rawSignal)) + { + case -1: + throw new Win32Exception(); + case 1: // canceled + bool wasKilled = KillCore(throwOnError: false, entireProcessGroup: _isGroupLeader); + ProcessExitStatus status = WaitForExitCore(); + return new ProcessExitStatus(status.ExitCode, wasKilled, status.Signal); + default: + ProcessExitStatus exitStatus = new(exitCode, false, rawSignal != 0 ? (PosixSignal)rawSignal : null); + OnProcessExited(); + return exitStatus; + } + }, cancellationToken).ConfigureAwait(false); + } + } - private Task WaitForExitOrKillOnCancellationAsyncCore(CancellationToken cancellationToken) => throw new NotImplementedException(); + 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(); + } - internal bool KillCore(bool throwOnError, bool entireProcessGroup = false) => throw new NotImplementedException(); + readHandle = new SafeFileHandle((IntPtr)fds[Interop.Sys.ReadEndOfPipe], ownsHandle: true); + writeHandle = new SafeFileHandle((IntPtr)fds[Interop.Sys.WriteEndOfPipe], ownsHandle: true); + } - private void ResumeCore() => throw new NotImplementedException(); + private void OnProcessExited() + { + if (_usesTerminal) + { + ProcessUtils.s_processStartLock.EnterWriteLock(); + ProcessUtils.ConfigureTerminalForChildProcesses(-1); + ProcessUtils.s_processStartLock.ExitWriteLock(); + } + } - private void SendSignalCore(PosixSignal signal, bool entireProcessGroup) => throw new NotImplementedException(); + 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; + } + + throw new Win32Exception(); + } + + 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; + } + + throw new Win32Exception(); + } } } 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/src/System.Diagnostics.Process.csproj b/src/libraries/System.Diagnostics.Process/src/System.Diagnostics.Process.csproj index 3a12c726a7f49e..71694cf65c3170 100644 --- a/src/libraries/System.Diagnostics.Process/src/System.Diagnostics.Process.csproj +++ b/src/libraries/System.Diagnostics.Process/src/System.Diagnostics.Process.csproj @@ -268,6 +268,10 @@ Link="Common\Interop\Unix\Interop.SysConf.cs" /> + + + - + - + 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 943d88b53f51c3..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 @@ -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); @@ -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(); } } @@ -604,26 +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) - { - 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? ResolveExecutableForShellExecute(string filename, string? workingDirectory) { // Determine if filename points to an executable file. @@ -999,7 +979,7 @@ private static unsafe void EnsureInitialized() // Register our callback. Interop.Sys.RegisterForSigChld(&OnSigChild); - SetDelayedSigChildConsoleConfigurationHandler(); + ProcessUtils.SetDelayedSigChildConsoleConfigurationHandler(); s_initialized = true; } @@ -1019,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/Process.ConfigureTerminalForChildProcesses.Unix.cs b/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/ProcessUtils.ConfigureTerminalForChildProcesses.Unix.cs similarity index 90% rename from src/libraries/System.Diagnostics.Process/src/System/Diagnostics/Process.ConfigureTerminalForChildProcesses.Unix.cs rename to src/libraries/System.Diagnostics.Process/src/System/Diagnostics/ProcessUtils.ConfigureTerminalForChildProcesses.Unix.cs index da7c8948cd0432..c5adac1723c4f1 100644 --- a/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/Process.ConfigureTerminalForChildProcesses.Unix.cs +++ b/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/ProcessUtils.ConfigureTerminalForChildProcesses.Unix.cs @@ -6,7 +6,7 @@ namespace System.Diagnostics { - public partial class Process + internal static partial class ProcessUtils { private static int s_childrenUsingTerminalCount; @@ -35,7 +35,7 @@ internal static void ConfigureTerminalForChildProcesses(int increment, bool conf } } - private static unsafe void SetDelayedSigChildConsoleConfigurationHandler() + internal static unsafe void SetDelayedSigChildConsoleConfigurationHandler() { Interop.Sys.SetDelayedSigChildConsoleConfigurationHandler(&DelayedSigChildConsoleConfiguration); } @@ -59,6 +59,6 @@ private static void DelayedSigChildConsoleConfiguration() } } - private static bool AreChildrenUsingTerminal => s_childrenUsingTerminalCount > 0; + internal static bool AreChildrenUsingTerminal => s_childrenUsingTerminalCount > 0; } } diff --git a/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/Process.ConfigureTerminalForChildProcesses.iOS.cs b/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/ProcessUtils.ConfigureTerminalForChildProcesses.iOS.cs similarity index 71% rename from src/libraries/System.Diagnostics.Process/src/System/Diagnostics/Process.ConfigureTerminalForChildProcesses.iOS.cs rename to src/libraries/System.Diagnostics.Process/src/System/Diagnostics/ProcessUtils.ConfigureTerminalForChildProcesses.iOS.cs index 44821b211d4923..5f3fa16dfc9163 100644 --- a/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/Process.ConfigureTerminalForChildProcesses.iOS.cs +++ b/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/ProcessUtils.ConfigureTerminalForChildProcesses.iOS.cs @@ -3,7 +3,7 @@ namespace System.Diagnostics { - public partial class Process + 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. @@ -13,8 +13,10 @@ internal static void ConfigureTerminalForChildProcesses(int increment, bool conf { } - static partial void SetDelayedSigChildConsoleConfigurationHandler(); + internal static void SetDelayedSigChildConsoleConfigurationHandler() + { + } - private static bool AreChildrenUsingTerminal => false; + internal static bool AreChildrenUsingTerminal => false; } } 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/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(); 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..28d2e4aeb61336 --- /dev/null +++ b/src/libraries/System.Diagnostics.Process/tests/SafeProcessHandleTests.Unix.cs @@ -0,0 +1,102 @@ +// 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.Runtime.InteropServices; +using Microsoft.Win32.SafeHandles; +using Xunit; + +namespace System.Diagnostics.Tests +{ + public partial class SafeProcessHandleTests + { + [Fact] + [PlatformSpecific(TestPlatforms.OSX)] + public static void SendSignal_SIGTERM_TerminatesProcess() + { + using SafeProcessHandle processHandle = SafeProcessHandle.Start(CreateTenSecondSleep(), input: null, output: null, error: null); + + processHandle.Signal(PosixSignal.SIGTERM); + + 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)"); + } + + [Fact] + [PlatformSpecific(TestPlatforms.OSX)] + public static void SendSignal_SIGINT_TerminatesProcess() + { + using SafeProcessHandle processHandle = SafeProcessHandle.Start(CreateTenSecondSleep(), input: null, output: null, error: null); + + processHandle.Signal(PosixSignal.SIGINT); + + 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)"); + } + + [Fact] + [PlatformSpecific(TestPlatforms.OSX)] + 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; + + Win32Exception exception = Assert.Throws(() => processHandle.Signal(invalidSignal)); + + // EINVAL error code is 22 on Unix systems + Assert.Equal(22, exception.NativeErrorCode); + + processHandle.Kill(); + processHandle.WaitForExit(); + } + + [Fact] + [PlatformSpecific(TestPlatforms.OSX)] + public static void SendSignal_ToExitedProcess_ThrowsWin32Exception() + { + ProcessStartOptions options = new("echo") { Arguments = { "test" } }; + + using SafeProcessHandle processHandle = SafeProcessHandle.Start(options, input: null, output: null, error: null); + + processHandle.WaitForExit(); + + Win32Exception exception = Assert.Throws(() => processHandle.Signal(PosixSignal.SIGTERM)); + + // 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); + + started.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 298b3ea1d8c556..0aa9fbfaef29db 100644 --- a/src/libraries/System.Diagnostics.Process/tests/SafeProcessHandleTests.cs +++ b/src/libraries/System.Diagnostics.Process/tests/SafeProcessHandleTests.cs @@ -14,7 +14,7 @@ namespace System.Diagnostics.Tests { - [PlatformSpecific(TestPlatforms.Windows)] + [PlatformSpecific(TestPlatforms.OSX | TestPlatforms.Windows)] public partial class SafeProcessHandleTests { // On Windows: @@ -28,7 +28,9 @@ private static ProcessStartOptions CreateTenSecondSleep() => OperatingSystem.IsW [Fact] public static void Start_WithNoArguments_Succeeds() { - ProcessStartOptions options = new("hostname"); + ProcessStartOptions options = OperatingSystem.IsWindows() + ? new("hostname") + : new("pwd"); using SafeProcessHandle processHandle = SafeProcessHandle.Start(options, input: null, output: null, error: null); @@ -48,7 +50,16 @@ 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.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] @@ -84,7 +95,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); @@ -111,7 +124,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); @@ -125,7 +140,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); @@ -163,7 +180,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); @@ -186,7 +205,17 @@ 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); + } + 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] @@ -233,7 +262,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); @@ -248,7 +279,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); @@ -262,7 +295,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); @@ -274,23 +309,12 @@ public static async Task WaitForExitOrKillOnCancellationAsync_CompletesNormallyW Assert.Null(exitStatus.Signal); } - [Fact] - public static void KillOnParentExit_CanBeSetToTrue() - { - ProcessStartOptions options = new("cmd.exe") { Arguments = { "/c", "echo test" }, 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() { - 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); } @@ -314,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(); @@ -326,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)] @@ -514,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) => @@ -522,14 +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(); - VerifyProcessIsRunning(enabled, remoteHandle.ExitCode); + // It's currently not implemented on macOS. + VerifyProcessIsRunning(shouldExited: enabled && !OperatingSystem.IsMacOS(), grandChildPid); } [ConditionalTheory(typeof(RemoteExecutor), nameof(RemoteExecutor.IsSupported))] @@ -561,12 +556,14 @@ 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))] [InlineData(true)] [InlineData(false)] + [PlatformSpecific(TestPlatforms.Windows)] public void KillOnParentExit_KillsTheChild_WhenParentCrashes(bool enabled) { RemoteInvokeOptions remoteInvokeOptions = new() { CheckExitCode = false }; @@ -597,7 +594,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) @@ -606,7 +604,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/libraries/System.Diagnostics.Process/tests/System.Diagnostics.Process.Tests.csproj b/src/libraries/System.Diagnostics.Process/tests/System.Diagnostics.Process.Tests.csproj index 7e4bcea4519680..b4f391bd7cf28a 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 @@ + - + \ No newline at end of file diff --git a/src/native/libs/Common/pal_config.h.in b/src/native/libs/Common/pal_config.h.in index e0d211c087c1ab..a85d4b184b45a5 100644 --- a/src/native/libs/Common/pal_config.h.in +++ b/src/native/libs/Common/pal_config.h.in @@ -143,6 +143,7 @@ #cmakedefine01 HAVE_MAKEDEV_SYSMACROSH #cmakedefine01 HAVE_GETGRGID_R #cmakedefine01 HAVE_TERMIOS2 +#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/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..be8d0555842004 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 @@ -20,10 +21,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 @@ -31,6 +33,7 @@ #ifdef __APPLE__ #include +#include #endif #ifdef __FreeBSD__ @@ -39,6 +42,10 @@ #include #endif +#if HAVE_KQUEUE +#include +#endif + #include // Validate that our SysLogPriority values are correct for the platform @@ -902,3 +909,443 @@ char* SystemNative_GetProcessPath(void) { return minipal_getexepath(); } + +static char* const* GetEnvVars(char* const envp[]) +{ + if (envp != NULL) + { + return envp; + } + +#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, + int32_t kill_on_parent_death, + int32_t create_suspended, + int32_t create_new_process_group, + int32_t* out_pid, + int32_t* out_pidfd +) +{ +#if defined(__APPLE__) && !defined(TARGET_MACCATALYST) && !defined(TARGET_TVOS) + // ========== POSIX_SPAWN PATH (macOS) ========== + + 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; + } + + // 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) + { + flags |= POSIX_SPAWN_START_SUSPENDED; + } + 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 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; + 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) + { + // posix_spawn_file_actions_addchdir_np is not available on tvOS +#if HAVE_POSIX_SPAWN_FILE_ACTIONS_ADDCHDIR_NP + 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); + posix_spawnattr_destroy(&attr); + errno = saved_errno; + return -1; + } + } + + // 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); + 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 = INT_MIN; // pidfd not supported on macOS (-1 is treated as invalid handle) + } + 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)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 = SystemNative_GetPlatformSignalNumber((PosixSignal)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; + PosixSignal posixSignal; + TryConvertSignalCodeToPosixSignal(sig, &posixSignal); + *out_signal = (int32_t)posixSignal; + 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 + } + + return SystemNative_WaitForExitAndReap(pidfd, pid, out_exitCode, out_signal); +#else + (void)ret; + (void)pidfd; + (void)pid; + (void)cancelPipeFd; + (void)out_exitCode; + (void)out_signal; + errno = ENOTSUP; + return -1; +#endif +} + +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); + + 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; + (void)out_exitCode; + (void)out_signal; + errno = ENOTSUP; + return -1; +#endif +} + +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); + if (ret != 1) + { + return ret; // Either process exited (0) or error occurred (-1) + } + + *out_timeout = 1; + 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) + { + 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, 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 6ef66da7acf84f..cbf1051e54b039 100644 --- a/src/native/libs/System.Native/pal_process.h +++ b/src/native/libs/System.Native/pal_process.h @@ -244,3 +244,73 @@ 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[], + 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); + +/** + * 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 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. + * + * 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, 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 569a0066eec69e..fb002fb520a23c 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[], + 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; +} + +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 isGroupLeader, 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, int32_t* out_isGroupLeader) +{ + 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 3e02cd323e7191..d58ec9aee2b40f 100644 --- a/src/native/libs/configure.cmake +++ b/src/native/libs/configure.cmake @@ -494,6 +494,9 @@ check_symbol_exists( HAVE_KQUEUE) set(CMAKE_REQUIRED_LIBRARIES ${PREVIOUS_CMAKE_REQUIRED_LIBRARIES}) +# 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 "sys/socket.h"