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
+