diff --git a/src/libraries/Common/src/Interop/Windows/Kernel32/Interop.CreateProcess.cs b/src/libraries/Common/src/Interop/Windows/Kernel32/Interop.CreateProcess.cs index ac2f7dca331b06..4a1fcb5ccee46d 100644 --- a/src/libraries/Common/src/Interop/Windows/Kernel32/Interop.CreateProcess.cs +++ b/src/libraries/Common/src/Interop/Windows/Kernel32/Interop.CreateProcess.cs @@ -58,6 +58,7 @@ internal struct STARTUPINFO } internal const int PROC_THREAD_ATTRIBUTE_HANDLE_LIST = 0x00020002; + internal const int CREATE_BREAKAWAY_FROM_JOB = 0x01000000; internal const int EXTENDED_STARTUPINFO_PRESENT = 0x00080000; [StructLayout(LayoutKind.Sequential)] diff --git a/src/libraries/Common/src/Interop/Windows/Kernel32/Interop.JobObjects.cs b/src/libraries/Common/src/Interop/Windows/Kernel32/Interop.JobObjects.cs index 3fd311578a3c92..b23709e05917b8 100644 --- a/src/libraries/Common/src/Interop/Windows/Kernel32/Interop.JobObjects.cs +++ b/src/libraries/Common/src/Interop/Windows/Kernel32/Interop.JobObjects.cs @@ -21,6 +21,7 @@ public SafeJobHandle() : base(true) { } internal static partial SafeJobHandle CreateJobObjectW(IntPtr lpJobAttributes, IntPtr lpName); internal const uint JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE = 0x00002000; + internal const uint JOB_OBJECT_LIMIT_BREAKAWAY_OK = 0x00000800; internal enum JOBOBJECTINFOCLASS { diff --git a/src/libraries/System.Diagnostics.Process/src/Microsoft/Win32/SafeHandles/SafeProcessHandle.Windows.cs b/src/libraries/System.Diagnostics.Process/src/Microsoft/Win32/SafeHandles/SafeProcessHandle.Windows.cs index c3f772c26c4a39..1d8525da6e2c41 100644 --- a/src/libraries/System.Diagnostics.Process/src/Microsoft/Win32/SafeHandles/SafeProcessHandle.Windows.cs +++ b/src/libraries/System.Diagnostics.Process/src/Microsoft/Win32/SafeHandles/SafeProcessHandle.Windows.cs @@ -98,7 +98,7 @@ private static bool TryOpenCore(int processId, [NotNullWhen(true)] out SafeProce private static unsafe Interop.Kernel32.SafeJobHandle CreateKillOnParentExitJob() { Interop.Kernel32.JOBOBJECT_EXTENDED_LIMIT_INFORMATION limitInfo = default; - limitInfo.BasicLimitInformation.LimitFlags = Interop.Kernel32.JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE; + limitInfo.BasicLimitInformation.LimitFlags = Interop.Kernel32.JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE | Interop.Kernel32.JOB_OBJECT_LIMIT_BREAKAWAY_OK; Interop.Kernel32.SafeJobHandle jobHandle = Interop.Kernel32.CreateJobObjectW(IntPtr.Zero, IntPtr.Zero); if (jobHandle.IsInvalid || !Interop.Kernel32.SetInformationJobObject( diff --git a/src/libraries/System.Diagnostics.Process/tests/ProcessHandlesTests.Windows.cs b/src/libraries/System.Diagnostics.Process/tests/ProcessHandlesTests.Windows.cs index d7188e0f63297a..6054c93e430e32 100644 --- a/src/libraries/System.Diagnostics.Process/tests/ProcessHandlesTests.Windows.cs +++ b/src/libraries/System.Diagnostics.Process/tests/ProcessHandlesTests.Windows.cs @@ -159,6 +159,85 @@ public unsafe void StartWithCallback_CreateProcess_CanRedirectOutput() Assert.Equal(0, process.ExitCode); } + [ConditionalTheory(typeof(RemoteExecutor), nameof(RemoteExecutor.IsSupported))] + [InlineData(true)] + [InlineData(false)] + public unsafe void ChildProcess_CanBreakAwayFromJob(bool killOnParentExit) + { + using Process process = CreateProcess(() => + { + using (Process grandChild = CreateProcess(() => + { + return RemoteExecutor.SuccessExitCode; + })) + { + ProcessStartInfo grandChildStartInfo = grandChild.StartInfo; + + using Process started = WindowsProcessStartArguments.Start(grandChildStartInfo, (WindowsProcessStartArguments args) => + { + Interop.Kernel32.STARTUPINFOEX startupInfoEx = default; + Interop.Kernel32.PROCESS_INFORMATION processInfo = default; + Interop.Kernel32.SECURITY_ATTRIBUTES unused_SecAttrs = default; + + startupInfoEx.StartupInfo.cb = sizeof(Interop.Kernel32.STARTUPINFOEX); + startupInfoEx.StartupInfo.hStdInput = args.StandardInput; + startupInfoEx.StartupInfo.hStdOutput = args.StandardOutput; + startupInfoEx.StartupInfo.hStdError = args.StandardError; + startupInfoEx.StartupInfo.dwFlags = Interop.Advapi32.StartupInfoOptions.STARTF_USESTDHANDLES; + + int creationFlags = Interop.Kernel32.CREATE_BREAKAWAY_FROM_JOB | Interop.Kernel32.EXTENDED_STARTUPINFO_PRESENT; + if (args.EnvironmentVariables != null) + { + creationFlags |= Interop.Advapi32.StartupInfoOptions.CREATE_UNICODE_ENVIRONMENT; + } + + if (!Interop.Kernel32.CreateProcess( + null, + args.Arguments, + ref unused_SecAttrs, + ref unused_SecAttrs, + bInheritHandles: true, + creationFlags, + args.EnvironmentVariables, + null, + &startupInfoEx, + &processInfo + )) + { + throw new Win32Exception(); + } + + Interop.Kernel32.CloseHandle(processInfo.hThread); + + return processInfo.hProcess; + }); + + try + { + Assert.True(started.WaitForExit(WaitInMS)); + return started.ExitCode; + } + finally + { + started.Kill(); + } + } + }); + + process.StartInfo.KillOnParentExit = killOnParentExit; + process.Start(); + + try + { + Assert.True(process.WaitForExit(WaitInMS)); + Assert.Equal(RemoteExecutor.SuccessExitCode, process.ExitCode); + } + finally + { + process.Kill(); + } + } + private unsafe int RunWithInvalidHandles(ProcessStartInfo startInfo) { const nint INVALID_HANDLE_VALUE = -1;