Repository: dotnet/runtime (C#)

Issue:
Add CreateNewProcessGroup to ProcessStartInfo
## Background and Motivation

<!--
We welcome API proposals! We have a process to evaluate the value and shape of new API. There is an overview of our process [here](https://github.com/dotnet/runtime/blob/master/docs/project/api-review-process.md). This template will help us gather the information we need to start the review process.
First, please describe the purpose and value of the new API here.
-->

_Edited by @jozkee_

Creating process groups is necessary to be able to send signals on Windows as they are sent to the whole group, you need to use `CREATE_NEW_PROCESS_GROUP` creation flag to isolate the signal from the parent process. POSIX systems have the same notion, however in unix-like systems one can use signals directly with processes.

This has been previously discussed in another issue in 2017 but it turned out that this change itself would not help the requesting team so it was dropped at that time. For the record, I am using proposals from that time.

https://github.com/dotnet/runtime/issues/20734

## Proposed API
Expose the creation flag via a Windows-only property.
```diff
namespace System.Diagnostics
{
     public sealed partial class ProcessStartInfo
     {
+          [SupportedOSPlatform("windows")]
+          public bool CreateNewProcessGroup { get; set; }
     }
}
```

## Usage Examples

<!--
Please provide code examples that highlight how the proposed API additions are meant to be consumed.
This will help suggest whether the API has the right shape to be functional, performant and useable.
You can use code blocks like this:
-->
``` C#
var startInfo = new ProcessStartInfo
{
    FileName = "my-program.exe",
    CreateNewProcessGroup = true,
};
var p = new Process.Start(startInfo);
// Do some work...

GenerateConsoleCtrlEvent(0 /* CTRL_C_EVENT */, (uint)process.Id)

[DllImport("kernel32.dll")]
[return: MarshalAs(UnmanagedType.Bool)]
public static extern bool GenerateConsoleCtrlEvent(uint dwCtrlEvent, uint dwProcessGroupId);
```
## Alternative Designs

1. https://github.com/dotnet/runtime/issues/71515 was considered but I've opted for this proposal instead as we do have concrete scenarios for it.  

2. https://github.com/dotnet/runtime/issues/109432, diffrent property naming: `EnableConsoleControlEvents`.  This name makes sense if CREATE_NEW_PROCESS_GROUP has no other reason to be used for, outside of signal isolation, I'm unaware of more scenarios where this is useful but that can change in the future. 

## Risks
None


Discussion:
Tagging subscribers to this area: @eiriktsarpalis, @jeffhandley
See info in area-owners.md if you want to be subscribed.
<details>
<summary>Issue Details</summary>
<hr />

## Background and Motivation

<!--
We welcome API proposals! We have a process to evaluate the value and shape of new API. There is an overview of our process [here](https://github.com/dotnet/runtime/blob/master/docs/project/api-review-process.md). This template will help us gather the information we need to start the review process.
First, please describe the purpose and value of the new API here.
-->

Creating process groups is necessary to be able to send signals on Windows as they are sent to the whole group and the calling process may not know the group ID itself. POSIX systems have the same notion, however in unix-like systems one can use signals directly with processes, which is impossible in Windows as I understand it.

This has been previously discussed in another issue in 2017 but it turned out that this change itself would not help the requesting team so it was dropped at that time. For the record, I am using proposals from that time.

https://github.com/dotnet/runtime/issues/20734

## Proposed API

```diff
namespace System.Diagnostics
{
     public sealed class ProcessStartInfo
     {
+          public bool CreateNewProcessGroup { get; set; }
     }
}
```

## Usage Examples

<!--
Please provide code examples that highlight how the proposed API additions are meant to be consumed.
This will help suggest whether the API has the right shape to be functional, performant and useable.
You can use code blocks like this:
``` C#
var startInfo = new System.Diagnostics.ProcessStartInfo
{
    FileName = "powershell",
    Arguments = "./script-waiting-for-interrupt.ps1",
    CreateNewProcessGroup = true,
};
var p = new System.Diagnostics.Process.Start(startInfo);
```
-->

## Alternative Designs

Alternative discussed in the linked issue was using creation flags such as:
``` C#
[Flags]
public enum CreationFlags
{
    CREATE_NEW_CONSOLE = 0x00000010,
    **CREATE_NEW_PROCESS_GROUP = 0x00000200**,
    CREATE_BREAKAWAY_FROM_JOB = 0x010000000,
    CREATE_DEFAULT_ERROR_MODE = 0x04000000,
    ...
};
```
But this turns somewhat Windows-centric which is not desired. Using a bool property instead is similar to how `CreateNoWindow` is exposed in ProcessStartInfo.

## Risks

I do not have deep knowledge on this subject but I do not see any risks. I would assume the default behavior would be kept the same so no breaking changes to existing code. I do not know of any circumstances where creating a process in new group would prevent the process from creation (such as clashing with CreateNoWindow flag), but that is potential risk. Stopping applications properly may be more challenging for developers using this as the spawned processes may not get signals generated by user interaction with terminal.

<table>
  <tr>
    <th align="left">Author:</th>
    <td>mladedav</td>
  </tr>
  <tr>
    <th align="left">Assignees:</th>
    <td>-</td>
  </tr>
  <tr>
    <th align="left">Labels:</th>
    <td>

`api-suggestion`, `area-System.Diagnostics.Process`, `untriaged`

</td>
  </tr>
  <tr>
    <th align="left">Milestone:</th>
    <td>-</td>
  </tr>
</table>
</details>
If I get this correctly, in order to obtain the same behaviour as `CREATE_NEW_PROCESS_GROUP` on Unix we would need to call `setpgid(0, 0);` between [fork and execve](https://github.com/dotnet/runtime/blob/2544c744d204c6ae0e20ba78c9cb8832a92091f3/src/libraries/Native/Unix/System.Native/pal_process.c#L438)?
I believe so. That would put the new process into a new process group with ID identical to that new process' ID.
My concern about this proposal is that it seems to only allow a very constrained use case for process groups. What if I wanted to create a process group that includes the current process?

It is probably telling that in the [original issue](https://github.com/dotnet/runtime/issues/20734#issuecomment-304395962) it was determined that ProcessStartInfo is probably not the appropriate API for making this type of configuration. So if we can't provide a general-purpose solution here, perhaps we shouldn't be adding it at all.
As I understand the comment, ProcessStartInfo wasn't appropriate for the advanced usage PaulHigin needed, not for the flag itself. I can't think of a better place for this since this needs to be set before the process is started, but I do not mind it being anywhere else.

The constraints are based on what both Windows and Linux can do - Windows only has a flag `CREATE_NEW_PROCESS_GROUP` for `CreaetProcess*` functions while Linux is able to change the group ID dynamically after fork.

If you want the new process to be part of the creating process' group, you simply don't specify the option, it is the default in both OSs.
Triage:

We should do research and find out if Windows allows for modifying process group after process creation as Unix does. If it does, we should consider adding APIs for all possibilities: 

- getting and setting process group of a given process instance
- specify it during creating

If it's not possible, the current proposal looks as good as it is.
Note: as @mladedav initially pointed out, the primary goal of this feature is to allow sending signals to the process on Windows.
If `Process` somehow exposed this functionality directly (e.g. with a `process.SendSignal(...)` method), then this switch would not be needed and may become an implementation detail (i.e. `Process.Start(...)` may still create a new process group, just not expose it to the caller).
 I am affected by this as well in a very similar way to the above mentioned issue (https://github.com/pulumi/pulumi-dotnet/issues/124)

Wanted to comment on this just to show there is a want for this, as the previous issue was closed due to lack of use by devs.
Any updates?
> We should do research and find out if Windows allows for modifying process group after process creation as Unix does. If it does, we should consider adding APIs for all possibilities:

>     * getting and setting process group of a given process instance

>     * specify it during creating

Please note the restriction on modifying the process group of an existing other process using `setpgid`: this can only be done if the target process has not called `execve` or a similar function yet. In fact, that form of the call is only intended to allow the parent process to ensure the process group has been set in the absence of other synchronization (such as the pipe .NET uses to obtain the return value from `execve`). The child process should always set its own process group ID (if the parent's process group ID is unsuitable). If the parent process encounters the error condition `[EACCES]` that the target process has already called `execve`, the process group has already been set, so the parent process needs to ignore the error.

Instead, the extra possibilities of `setpgid` are for two use cases:
* Create multiple child processes and have all of them join a single new process group. Most job control shells such as bash and zsh do this for pipelines. This allows all elements of a pipeline access to the terminal while also allowing the shell to wait for and check the exit status of all elements, and not creating any additional processes.
* In a job control shell, set the shell's process group ID to the shell's process ID, and attempt to undo that when disabling job control mode (such as when suspending the shell itself). If someone wants to do this from .NET, I'd say platform invoke is fine.

Documentation should warn that starting a process in a new process group may have unexpected effects if the process has a controlling terminal. In particular, the child process will not receive signals from key combinations such as Ctrl+C and will be suspended if it tries to read from the terminal (the kernel assuming that the parent process will deal with that).

If the process does not have a controlling terminal, creating new process groups is fine and allows sending signals to whole trees of processes except those that actively change their process group ID.

If Windows provides no way to create multiple child processes and have all of them join a single new process group, the `CreateNewProcessGroup` boolean is certainly sufficient.
@jilles-sg I updated the API proposal yesterday to reflect this will be a Windows-only API to unblock sending signals to an isolated process group.  There won't be any effects in Unix.
[Video](https://www.youtube.com/watch?v=52f8Bz8db04&t=0h0m0s)

Looks good as proposed

```c#
namespace System.Diagnostics
{
    public sealed partial class ProcessStartInfo
    {
        [SupportedOSPlatform("windows")]
        public bool CreateNewProcessGroup { get; set; }
    }
}
```




Patch:
diff --git a/src/libraries/Common/src/Interop/Windows/Advapi32/Interop.ProcessOptions.cs b/src/libraries/Common/src/Interop/Windows/Advapi32/Interop.ProcessOptions.cs
index 8ef5167d70edc6..893a0aaed9073c 100644
--- a/src/libraries/Common/src/Interop/Windows/Advapi32/Interop.ProcessOptions.cs
+++ b/src/libraries/Common/src/Interop/Windows/Advapi32/Interop.ProcessOptions.cs
@@ -43,6 +43,7 @@ internal static partial class StartupInfoOptions
             internal const int STARTF_USESTDHANDLES = 0x00000100;
             internal const int CREATE_UNICODE_ENVIRONMENT = 0x00000400;
             internal const int CREATE_NO_WINDOW = 0x08000000;
+            internal const int CREATE_NEW_PROCESS_GROUP = 0x00000200;
         }
     }
 }
diff --git a/src/libraries/System.Diagnostics.Process/ref/System.Diagnostics.Process.cs b/src/libraries/System.Diagnostics.Process/ref/System.Diagnostics.Process.cs
index d4cf2170b88b83..17366ae9811736 100644
--- a/src/libraries/System.Diagnostics.Process/ref/System.Diagnostics.Process.cs
+++ b/src/libraries/System.Diagnostics.Process/ref/System.Diagnostics.Process.cs
@@ -219,6 +219,8 @@ public ProcessStartInfo(string fileName, System.Collections.Generic.IEnumerable<
         public System.Collections.ObjectModel.Collection<string> ArgumentList { get { throw null; } }
         [System.Diagnostics.CodeAnalysis.AllowNullAttribute]
         public string Arguments { get { throw null; } set { } }
+        [System.Runtime.Versioning.SupportedOSPlatformAttribute("windows")]
+        public bool CreateNewProcessGroup { get { throw null; } set { } }
         public bool CreateNoWindow { get { throw null; } set { } }
         [System.Runtime.Versioning.SupportedOSPlatformAttribute("windows")]
         [System.Diagnostics.CodeAnalysis.AllowNullAttribute]
diff --git a/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/Process.Windows.cs b/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/Process.Windows.cs
index 06a2bd51d6d402..dc05e138a5aec6 100644
--- a/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/Process.Windows.cs
+++ b/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/Process.Windows.cs
@@ -505,6 +505,7 @@ private unsafe bool StartWithCreateProcess(ProcessStartInfo startInfo)
                     // set up the creation flags parameter
                     int creationFlags = 0;
                     if (startInfo.CreateNoWindow) creationFlags |= Interop.Advapi32.StartupInfoOptions.CREATE_NO_WINDOW;
+                    if (startInfo.CreateNewProcessGroup) creationFlags |= Interop.Advapi32.StartupInfoOptions.CREATE_NEW_PROCESS_GROUP;
 
                     // set up the environment block parameter
                     string? environmentBlock = null;
diff --git a/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/ProcessStartInfo.Unix.cs b/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/ProcessStartInfo.Unix.cs
index 9664815dcde794..196d57bf0b36b9 100644
--- a/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/ProcessStartInfo.Unix.cs
+++ b/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/ProcessStartInfo.Unix.cs
@@ -52,5 +52,12 @@ public SecureString? Password
             get { throw new PlatformNotSupportedException(SR.Format(SR.ProcessStartSingleFeatureNotSupported, nameof(Password))); }
             set { throw new PlatformNotSupportedException(SR.Format(SR.ProcessStartSingleFeatureNotSupported, nameof(Password))); }
         }
+
+        [SupportedOSPlatform("windows")]
+        public bool CreateNewProcessGroup
+        {
+            get { throw new PlatformNotSupportedException(SR.Format(SR.ProcessStartSingleFeatureNotSupported, nameof(CreateNewProcessGroup))); }
+            set { throw new PlatformNotSupportedException(SR.Format(SR.ProcessStartSingleFeatureNotSupported, nameof(CreateNewProcessGroup))); }
+        }
     }
 }
diff --git a/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/ProcessStartInfo.Windows.cs b/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/ProcessStartInfo.Windows.cs
index 8869c1809341f3..6217aeb9c5a5b3 100644
--- a/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/ProcessStartInfo.Windows.cs
+++ b/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/ProcessStartInfo.Windows.cs
@@ -46,5 +46,17 @@ public string Domain
         [CLSCompliant(false)]
         [SupportedOSPlatform("windows")]
         public SecureString? Password { get; set; }
+
+        /// <summary>
+        /// Gets or sets a value indicating whether to start the process in a new process group.
+        /// </summary>
+        /// <value><c>true</c> if the process should be started in a new process group; otherwise, <c>false</c>. The default is <c>false</c>.</value>
+        /// <remarks>
+        /// <para>When a process is created in a new process group, it becomes the root of a new process group.</para>
+        /// <para>An implicit call to <c>SetConsoleCtrlHandler(NULL,TRUE)</c> is made on behalf of the new process, this means that the new process has CTRL+C disabled.</para>
+        /// <para>This property is useful for preventing console control events sent to the child process from affecting the parent process.</para>
+        /// </remarks>
+        [SupportedOSPlatform("windows")]
+        public bool CreateNewProcessGroup { get; set; }
     }
 }


Test patch:
diff --git a/src/libraries/System.Diagnostics.Process/tests/Interop.cs b/src/libraries/System.Diagnostics.Process/tests/Interop.cs
index 969995e36d8a33..6bd2ddebbc8f6a 100644
--- a/src/libraries/System.Diagnostics.Process/tests/Interop.cs
+++ b/src/libraries/System.Diagnostics.Process/tests/Interop.cs
@@ -72,6 +72,11 @@ public struct SID_AND_ATTRIBUTES
         public int Attributes;
     }
 
+        
+    [DllImport("kernel32.dll", SetLastError = true)]
+    [return: MarshalAs(UnmanagedType.Bool)]
+    public static extern bool GenerateConsoleCtrlEvent(uint dwCtrlEvent, uint dwProcessGroupId);
+
     [DllImport("kernel32.dll")]
     public static extern bool GetProcessWorkingSetSizeEx(SafeProcessHandle hProcess, out IntPtr lpMinimumWorkingSetSize, out IntPtr lpMaximumWorkingSetSize, out uint flags);
 
diff --git a/src/libraries/System.Diagnostics.Process/tests/ProcessStartInfoTests.cs b/src/libraries/System.Diagnostics.Process/tests/ProcessStartInfoTests.cs
index 728e52f520208f..2e896862d638b5 100644
--- a/src/libraries/System.Diagnostics.Process/tests/ProcessStartInfoTests.cs
+++ b/src/libraries/System.Diagnostics.Process/tests/ProcessStartInfoTests.cs
@@ -905,6 +905,29 @@ public void TestEnvironmentVariablesPropertyUnix()
             });
         }
 
+        [Fact]
+        [PlatformSpecific(TestPlatforms.Windows)]
+        public void CreateNewProcessGroup_SetWindows_GetReturnsExpected()
+        {
+            ProcessStartInfo psi = new ProcessStartInfo();
+            Assert.False(psi.CreateNewProcessGroup);
+
+            psi.CreateNewProcessGroup = true;
+            Assert.True(psi.CreateNewProcessGroup);
+
+            psi.CreateNewProcessGroup = false;
+            Assert.False(psi.CreateNewProcessGroup);
+        }
+
+        [Fact]
+        [PlatformSpecific(TestPlatforms.AnyUnix)]
+        public void CreateNewProcessGroup_GetSetUnix_ThrowsPlatformNotSupportedException()
+        {
+            var info = new ProcessStartInfo();
+            Assert.Throws<PlatformNotSupportedException>(() => info.CreateNewProcessGroup);
+            Assert.Throws<PlatformNotSupportedException>(() => info.CreateNewProcessGroup = true);
+        }
+
         [Theory]
         [InlineData(null)]
         [InlineData("")]
diff --git a/src/libraries/System.Diagnostics.Process/tests/ProcessTests.Unix.cs b/src/libraries/System.Diagnostics.Process/tests/ProcessTests.Unix.cs
index 7f66696c6b2755..47c5ce05870287 100644
--- a/src/libraries/System.Diagnostics.Process/tests/ProcessTests.Unix.cs
+++ b/src/libraries/System.Diagnostics.Process/tests/ProcessTests.Unix.cs
@@ -1047,5 +1047,16 @@ private static string StartAndReadToEnd(string filename, string[] arguments)
                 return process.StandardOutput.ReadToEnd();
             }
         }
+
+        private static void SendSignal(PosixSignal signal, int processId)
+        {
+            int result = kill(processId, Interop.Sys.GetPlatformSignalNumber(signal));
+            if (result != 0)
+            {
+                throw new Win32Exception(Marshal.GetLastWin32Error(), $"Failed to send signal {signal} to process {processId}");
+            }
+        }
+
+        private static unsafe void ReEnableCtrlCHandlerIfNeeded(PosixSignal signal) { }
     }
 }
diff --git a/src/libraries/System.Diagnostics.Process/tests/ProcessTests.Windows.cs b/src/libraries/System.Diagnostics.Process/tests/ProcessTests.Windows.cs
index 6fb91c0f4e113e..7850ca8afcbd7d 100644
--- a/src/libraries/System.Diagnostics.Process/tests/ProcessTests.Windows.cs
+++ b/src/libraries/System.Diagnostics.Process/tests/ProcessTests.Windows.cs
@@ -2,7 +2,11 @@
 // The .NET Foundation licenses this file to you under the MIT license.
 
 using System;
+using System.ComponentModel;
 using System.IO;
+using System.Runtime.InteropServices;
+using Microsoft.DotNet.XUnitExtensions;
+using Xunit;
 
 namespace System.Diagnostics.Tests
 {
@@ -15,5 +19,41 @@ private string WriteScriptFile(string directory, string name, int returnValue)
             File.WriteAllText(filename, $"exit {returnValue}");
             return filename;
         }
+        
+        private static void SendSignal(PosixSignal signal, int processId)
+        {
+            uint dwCtrlEvent = signal switch
+            {
+                PosixSignal.SIGINT => Interop.Kernel32.CTRL_C_EVENT,
+                PosixSignal.SIGQUIT => Interop.Kernel32.CTRL_BREAK_EVENT,
+                _ => throw new ArgumentOutOfRangeException(nameof(signal))
+            };
+
+            if (!Interop.GenerateConsoleCtrlEvent(dwCtrlEvent, (uint)processId))
+            {
+                int error = Marshal.GetLastWin32Error();
+                if (error == Interop.Errors.ERROR_INVALID_FUNCTION && PlatformDetection.IsInContainer)
+                {
+                    // Docker in CI runs without a console attached.
+                    throw new SkipTestException($"GenerateConsoleCtrlEvent failed with ERROR_INVALID_FUNCTION. The process is not a console process or does not have a console.");
+                }
+
+                throw new Win32Exception(error);
+            }
+        }
+
+        // See https://learn.microsoft.com/windows/win32/api/processthreadsapi/nf-processthreadsapi-createprocessw#remarks:
+        // When a process is created with CREATE_NEW_PROCESS_GROUP specified, an implicit call to SetConsoleCtrlHandler(NULL,TRUE) 
+        // is made on behalf of the new process; this means that the new process has CTRL+C disabled.
+        private static unsafe void ReEnableCtrlCHandlerIfNeeded(PosixSignal signal)
+        {
+            if (signal is PosixSignal.SIGINT)
+            {
+                if (!Interop.Kernel32.SetConsoleCtrlHandler(null, false))
+                {
+                    throw new Win32Exception();
+                }
+            }
+        }
     }
 }
diff --git a/src/libraries/System.Diagnostics.Process/tests/ProcessTests.cs b/src/libraries/System.Diagnostics.Process/tests/ProcessTests.cs
index a3b2a7a97f0508..59f010f1941792 100644
--- a/src/libraries/System.Diagnostics.Process/tests/ProcessTests.cs
+++ b/src/libraries/System.Diagnostics.Process/tests/ProcessTests.cs
@@ -10,6 +10,7 @@
 using System.Linq;
 using System.Net;
 using System.Reflection;
+using System.Runtime.InteropServices;
 using System.Security;
 using System.Text;
 using System.Threading;
@@ -80,6 +81,82 @@ private void AssertNonZeroAllZeroDarwin(long value)
             }
         }
 
+        public static IEnumerable<object[]> SignalTestData()
+        {
+            if (OperatingSystem.IsWindows())
+            {
+                // GenerateConsoleCtrlEvent only supports sending CTRL_C_EVENT and CTRL_BREAK_EVENT
+                yield return new object[] { PosixSignal.SIGINT };
+                yield return new object[] { PosixSignal.SIGQUIT };
+            }
+            else
+            {
+                foreach (PosixSignal signal in Enum.GetValues<PosixSignal>())
+                {
+                    yield return new object[] { signal };
+                }
+                // Test a few raw signals.
+                yield return new object[] { (PosixSignal)3 }; // SIGQUIT
+                yield return new object[] { (PosixSignal)15 }; // SIGTERM
+            }
+        }
+
+        [ConditionalTheory(typeof(RemoteExecutor), nameof(RemoteExecutor.IsSupported))]
+        [MemberData(nameof(SignalTestData))]
+        public void TestCreateNewProcessGroup_HandlerReceivesExpectedSignal(PosixSignal signal)
+        {
+            const string PosixSignalRegistrationCreatedMessage = "PosixSignalRegistration created...";
+
+            var remoteInvokeOptions = new RemoteInvokeOptions { CheckExitCode = false };
+            remoteInvokeOptions.StartInfo.RedirectStandardOutput = true;
+            if (OperatingSystem.IsWindows())
+            {
+                remoteInvokeOptions.StartInfo.CreateNewProcessGroup = true;
+            }
+
+            using RemoteInvokeHandle remoteHandle = RemoteExecutor.Invoke(
+                (signalStr) =>
+                {
+                    PosixSignal expectedSignal = Enum.Parse<PosixSignal>(signalStr);
+                    using ManualResetEvent receivedSignalEvent = new ManualResetEvent(false);
+                    ReEnableCtrlCHandlerIfNeeded(expectedSignal);
+
+                    using PosixSignalRegistration p = PosixSignalRegistration.Create(expectedSignal, (ctx) =>
+                    {
+                        Assert.Equal(expectedSignal, ctx.Signal);
+                        receivedSignalEvent.Set();
+                        ctx.Cancel = true;
+                    });
+
+                    Console.WriteLine(PosixSignalRegistrationCreatedMessage);
+
+                    Assert.True(receivedSignalEvent.WaitOne(WaitInMS));
+
+                    return 0;
+                },
+                arg: $"{signal}",
+                remoteInvokeOptions);
+
+            while (!remoteHandle.Process.StandardOutput.ReadLine().EndsWith(PosixSignalRegistrationCreatedMessage))
+            {
+                Thread.Sleep(20);
+            }
+
+            try
+            {
+                SendSignal(signal, remoteHandle.Process.Id);
+
+                Assert.True(remoteHandle.Process.WaitForExit(WaitInMS));
+                Assert.Equal(0, remoteHandle.Process.ExitCode);
+            }
+            finally
+            {
+                // If sending the signal fails, we want to kill the process ASAP
+                // to prevent RemoteExecutor's timeout from hiding it.
+                remoteHandle.Process.Kill();
+            }
+        }
+
         [ConditionalTheory(typeof(RemoteExecutor), nameof(RemoteExecutor.IsSupported))]
         [InlineData(-2)]
         [InlineData((long)int.MaxValue + 1)]
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 a69d3eb2372059..718bddd017ea89 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
@@ -44,12 +44,18 @@
     <Compile Include="ProcessStartInfoTests.Windows.cs" />
     <Compile Include="ProcessTests.Windows.cs" />
     <Compile Include="ProcessThreadTests.Windows.cs" />
+    <Compile Include="$(CommonPath)Interop\Windows\Interop.BOOL.cs"
+             Link="Common\Interop\Windows\Interop.BOOL.cs" />
     <Compile Include="$(CommonPath)Interop\Windows\Interop.Libraries.cs"
-         Link="Common\Interop\Windows\Interop.Libraries.cs" />
+             Link="Common\Interop\Windows\Interop.Libraries.cs" />
     <Compile Include="$(CommonPath)Interop\Windows\Kernel32\Interop.LoadLibrary.cs"
-         Link="Common\Interop\Windows\Kernel32\Interop.LoadLibrary.cs" />
+             Link="Common\Interop\Windows\Kernel32\Interop.LoadLibrary.cs" />
     <Compile Include="$(CommonPath)Interop\Windows\Kernel32\Interop.FreeLibrary.cs"
              Link="Common\Interop\Windows\Kernel32\Interop.FreeLibrary.cs" />
+    <Compile Include="$(CommonPath)Interop\Windows\Kernel32\Interop.SetConsoleCtrlHandler.cs"
+             Link="Common\Interop\Windows\Kernel32\Interop.SetConsoleCtrlHandler.cs" />
+    <Compile Include="$(CommonPath)Interop\Windows\Interop.Errors.cs"
+             Link="Common\Interop\Windows\Interop.Errors.cs" />
     <!-- Helpers -->
     <Compile Include="$(CommonTestPath)TestUtilities\System\WindowsTestFileShare.cs" Link="Common\TestUtilities\System\WindowsTestFileShare.cs" />
   </ItemGroup>
@@ -63,6 +69,10 @@
              Link="Common\Interop\OSX\Interop.libproc.cs" />
     <Compile Include="$(CommonPath)Interop\OSX\Interop.libSystem.cs"
              Link="Common\Interop\OSX\Interop.libSystem.cs" />
+    <Compile Include="$(CommonPath)Interop\Unix\Interop.Libraries.cs" 
+             Link="Common\Interop\Unix\Interop.Libraries.cs" />
+    <Compile Include="$(CommonPath)Interop\Unix\System.Native\Interop.PosixSignal.cs" 
+             Link="Common\Interop\Unix\System.Native\Interop.PosixSignal.cs" />
   </ItemGroup>
   <ItemGroup>
     <ProjectReference Include="$(LibrariesProjectRoot)Microsoft.Win32.Registry\src\Microsoft.Win32.Registry.csproj" />