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 b2c8445a3b9006..9161b91dfaf0f3 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 @@ -622,18 +622,18 @@ internal static void CheckChildren(bool reapAll, bool configureConsole) pid = Interop.Sys.WaitIdAnyExitedNoHangNoWait(); if (pid > 0) { - if (s_childProcessWaitStates.TryGetValue(pid, out ProcessWaitState? pws)) + if (s_childProcessWaitStates.TryGetValue(pid, out ProcessWaitState? pws) && + pws.TryReapChild(configureConsole)) { - // Known Process. - if (pws.TryReapChild(configureConsole)) - { - pws.ReleaseRef(); - } + pws.ReleaseRef(); } else { - // unlikely: This is not a managed Process, so we are not responsible for reaping. - // Fall back to checking all Processes. + // The child may be unmanaged, or stopped rather than exited. On macOS, + // waitid can report stopped children despite omitting WSTOPPED; + // the SIGCHLD handler is also registered with SA_NOCLDSTOP. + // Scan all managed children instead of repeatedly observing + // the same child without reaping it. checkAll = true; break; } diff --git a/src/libraries/System.Diagnostics.Process/tests/AssemblyInfo.cs b/src/libraries/System.Diagnostics.Process/tests/AssemblyInfo.cs index f15a1cb605513a..41a9e3ed1e5ef3 100644 --- a/src/libraries/System.Diagnostics.Process/tests/AssemblyInfo.cs +++ b/src/libraries/System.Diagnostics.Process/tests/AssemblyInfo.cs @@ -3,10 +3,4 @@ using Xunit; -#if TARGET_OSX -// Retain serialization until process-management hangs under concurrent test classes on macOS are resolved. -// https://github.com/dotnet/runtime/issues/135294 -[assembly: CollectionBehavior(CollectionBehavior.CollectionPerAssembly)] -#endif - [assembly: SkipOnPlatform(TestPlatforms.Browser, "System.Diagnostics.Process is not supported on Browser.")] diff --git a/src/libraries/System.Diagnostics.Process/tests/ProcessTests.Unix.cs b/src/libraries/System.Diagnostics.Process/tests/ProcessTests.Unix.cs index f916cedb732608..161e4daaf1532d 100644 --- a/src/libraries/System.Diagnostics.Process/tests/ProcessTests.Unix.cs +++ b/src/libraries/System.Diagnostics.Process/tests/ProcessTests.Unix.cs @@ -769,6 +769,64 @@ public async Task TestChildProcessCleanupAfterDispose(bool shortProcess, bool en Assert.True(processReaped); } + [ConditionalTheory(typeof(RemoteExecutor), nameof(RemoteExecutor.IsSupported))] + [PlatformSpecific(TestPlatforms.OSX)] + [InlineData(false)] + [InlineData(true)] + public void WaitForExit_StoppedChild_DoesNotPreventReapingOtherChildren(bool useAsync) + { + RemoteExecutor.Invoke(async useAsyncString => + { + const uint StoppedProcessStatus = 4; // SSTOP in sys/proc.h. + using Process stopped = Process.Start("/bin/sleep", "300"); + using Process exiting = Process.Start("/bin/sleep", "300"); + int stoppedPid = stopped.Id; + int exitingPid = exiting.Id; + int killSignal = Interop.Sys.GetPlatformSignalNumber(PosixSignal.SIGKILL); + using var sigChildReceived = new ManualResetEventSlim(); + using PosixSignalRegistration registration = PosixSignalRegistration.Create(PosixSignal.SIGCHLD, context => + { + context.Cancel = true; + sigChildReceived.Set(); + }); + + try + { + Assert.Equal(0, Interop.Sys.Kill(stoppedPid, Interop.Sys.GetPlatformSIGSTOP())); + + Assert.True(SpinWait.SpinUntil( + () => Interop.libproc.GetProcessInfoById(stoppedPid) is { } info && + info.pbsd.pbi_status == StoppedProcessStatus, WaitInMS)); + + sigChildReceived.Reset(); + Assert.Equal(0, Interop.Sys.Kill(Environment.ProcessId, Interop.Sys.GetPlatformSignalNumber(PosixSignal.SIGCHLD))); + Assert.True(sigChildReceived.Wait(WaitInMS)); + + Assert.Equal(0, Interop.Sys.Kill(exitingPid, killSignal)); + if (bool.Parse(useAsyncString)) + { + using var cts = new CancellationTokenSource(WaitInMS); + await exiting.WaitForExitAsync(cts.Token); + } + else + { + Assert.True(exiting.WaitForExit(WaitInMS)); + } + Assert.True(exiting.HasExited); + } + finally + { + // Bypass managed process locks so cleanup also works if the reaper is stuck. + Assert.Equal(0, Interop.Sys.Kill(stoppedPid, killSignal)); + Assert.True(stopped.WaitForExit(WaitInMS)); + exiting.Kill(); + Assert.True(exiting.WaitForExit(WaitInMS)); + } + + return RemoteExecutor.SuccessExitCode; + }, useAsync.ToString()).Dispose(); + } + private static Process CreateShortProcess() { Process process = new Process(); 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 8a00172f4ddee7..446be53722769d 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 @@ -10,7 +10,6 @@ $([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) $(DefineConstants);TargetsWindows - $(DefineConstants);TARGET_OSX +