From 944f1ec5eccca656dec4bcbb42dbf79efe4f3d29 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 18 Mar 2026 13:44:35 +0000 Subject: [PATCH 1/8] Initial plan From 3c2fc3dcee91185d3633eaa5adbc0c6322d4a2de Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 18 Mar 2026 14:24:06 +0000 Subject: [PATCH 2/8] Fix Process.WaitForExit hang when grandchild process keeps pipe open Add CancelDueToProcessExit and CancelDueToProcessExitAsync methods to AsyncStreamReader that wait a timeout for the read task to finish and cancel it if it doesn't. Replace all EOF.GetAwaiter().GetResult() and EOF.WaitAsync() calls in WaitForExitCore (Unix/Windows) and WaitForExitAsync with the new timeout-based methods. Use 300ms default when no timeout is provided. Add Theory test reproducing the hang scenario with a grandchild process that sleeps for 8 hours. Co-authored-by: adamsitnik <6011991+adamsitnik@users.noreply.github.com> --- .../System/Diagnostics/AsyncStreamReader.cs | 24 ++++++++ .../src/System/Diagnostics/Process.Unix.cs | 7 ++- .../src/System/Diagnostics/Process.Windows.cs | 8 +-- .../src/System/Diagnostics/Process.cs | 12 ++-- .../tests/ProcessWaitingTests.cs | 60 +++++++++++++++++++ 5 files changed, 98 insertions(+), 13 deletions(-) diff --git a/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/AsyncStreamReader.cs b/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/AsyncStreamReader.cs index fbfd3a6baaae8a..bc8524864c112b 100644 --- a/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/AsyncStreamReader.cs +++ b/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/AsyncStreamReader.cs @@ -255,6 +255,30 @@ private bool FlushMessageQueue(bool rethrowInNewThread) internal Task EOF => _readToBufferTask ?? Task.CompletedTask; + internal void CancelDueToProcessExit(int milliseconds) + { + Task? task = _readToBufferTask; + if (task is not null && !task.Wait(milliseconds)) + { + _cts.Cancel(); + task.GetAwaiter().GetResult(); + } + } + + internal async Task CancelDueToProcessExitAsync(int milliseconds) + { + Task? task = _readToBufferTask; + if (task is not null) + { + Task completed = await Task.WhenAny(task, Task.Delay(milliseconds)).ConfigureAwait(false); + if (completed != task) + { + _cts.Cancel(); + await task.ConfigureAwait(false); + } + } + } + public void Dispose() { _cts.Cancel(); 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 f26a8f70ffba74..a789be74236ec1 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 @@ -205,10 +205,11 @@ private bool WaitForExitCore(int milliseconds) bool exited = GetWaitState().WaitForExit(milliseconds); Debug.Assert(exited || milliseconds != Timeout.Infinite); - if (exited && milliseconds == Timeout.Infinite) // if we have a hard timeout, we cannot wait for the streams + if (exited) { - _output?.EOF.GetAwaiter().GetResult(); - _error?.EOF.GetAwaiter().GetResult(); + int streamTimeout = milliseconds == Timeout.Infinite ? 300 : milliseconds; + _output?.CancelDueToProcessExit(streamTimeout); + _error?.CancelDueToProcessExit(streamTimeout); } return exited; 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 8a7e2b914c4850..43a3954b3b67f2 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 @@ -176,11 +176,11 @@ private bool WaitForExitCore(int milliseconds) } finally { - // If we have a hard timeout, we cannot wait for the streams - if (milliseconds == Timeout.Infinite) + if (_signaled) { - _output?.EOF.GetAwaiter().GetResult(); - _error?.EOF.GetAwaiter().GetResult(); + int streamTimeout = milliseconds == Timeout.Infinite ? 300 : milliseconds; + _output?.CancelDueToProcessExit(streamTimeout); + _error?.CancelDueToProcessExit(streamTimeout); } handle?.Dispose(); diff --git a/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/Process.cs b/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/Process.cs index 4218596c129644..8b73d0135027df 100644 --- a/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/Process.cs +++ b/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/Process.cs @@ -1527,7 +1527,7 @@ public async Task WaitForExitAsync(CancellationToken cancellationToken = default // exception up to the user if (HasExited) { - await WaitUntilOutputEOF(cancellationToken).ConfigureAwait(false); + await CancelStreamReadersDueToProcessExitAsync().ConfigureAwait(false); return; } @@ -1554,24 +1554,24 @@ public async Task WaitForExitAsync(CancellationToken cancellationToken = default } } - // Wait until output streams have been drained - await WaitUntilOutputEOF(cancellationToken).ConfigureAwait(false); + // Cancel stream readers after process exit with a default timeout + await CancelStreamReadersDueToProcessExitAsync().ConfigureAwait(false); } finally { Exited -= handler; } - async Task WaitUntilOutputEOF(CancellationToken cancellationToken) + async Task CancelStreamReadersDueToProcessExitAsync() { if (_output is not null) { - await _output.EOF.WaitAsync(cancellationToken).ConfigureAwait(false); + await _output.CancelDueToProcessExitAsync(300).ConfigureAwait(false); } if (_error is not null) { - await _error.EOF.WaitAsync(cancellationToken).ConfigureAwait(false); + await _error.CancelDueToProcessExitAsync(300).ConfigureAwait(false); } } } diff --git a/src/libraries/System.Diagnostics.Process/tests/ProcessWaitingTests.cs b/src/libraries/System.Diagnostics.Process/tests/ProcessWaitingTests.cs index 8ef602113be272..a04505f209cc10 100644 --- a/src/libraries/System.Diagnostics.Process/tests/ProcessWaitingTests.cs +++ b/src/libraries/System.Diagnostics.Process/tests/ProcessWaitingTests.cs @@ -671,5 +671,65 @@ public async Task WaitForExitAsync_NotDirected_ThrowsInvalidOperationException() var process = new Process(); await Assert.ThrowsAsync(() => process.WaitForExitAsync()); } + + [ConditionalTheory(typeof(RemoteExecutor), nameof(RemoteExecutor.IsSupported))] + [InlineData("WaitForExit")] + [InlineData("WaitForExitInt")] + [InlineData("WaitForExitTimeSpan")] + [InlineData("WaitForExitAsync")] + public async Task WaitForExit_WithGrandChildProcess_DoesNotHang(string waitMethod) + { + Process child = CreateProcess(StartGrandChildProcessAndExit); + child.StartInfo.RedirectStandardOutput = true; + child.Start(); + + string grandChildPidStr = await child.StandardOutput.ReadLineAsync(); + int grandChildPid = int.Parse(grandChildPidStr); + + var stopwatch = Stopwatch.StartNew(); + + switch (waitMethod) + { + case "WaitForExit": + child.WaitForExit(); + break; + case "WaitForExitInt": + Assert.True(child.WaitForExit(WaitInMS)); + break; + case "WaitForExitTimeSpan": + Assert.True(child.WaitForExit(TimeSpan.FromMilliseconds(WaitInMS))); + break; + case "WaitForExitAsync": + await child.WaitForExitAsync(); + break; + } + + stopwatch.Stop(); + Assert.True(stopwatch.Elapsed.TotalSeconds < 2, $"WaitForExit took {stopwatch.Elapsed.TotalSeconds:F1}s, expected < 2s"); + + try + { + Process.GetProcessById(grandChildPid).Kill(); + } + catch (Exception) { } + } + + private static int StartGrandChildProcessAndExit() + { + using Process grandChild = new Process(); + grandChild.StartInfo.FileName = Environment.ProcessPath; + grandChild.StartInfo.Arguments = $"exec \"{typeof(ProcessWaitingTests).Assembly.Location}\" {nameof(SleepForEightHours)}"; + grandChild.Start(); + Console.WriteLine(grandChild.Id); + + return RemoteExecutor.SuccessExitCode; + } + + public static int SleepForEightHours() + { + Thread.Sleep(TimeSpan.FromHours(8)); + + return RemoteExecutor.SuccessExitCode; + } } } From 7c5fa6bb2358e53ec8e004ca15b76e7a1057fa26 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 18 Mar 2026 14:37:21 +0000 Subject: [PATCH 3/8] Fix test to use RemoteExecutor.HostRunner and RemoteExecutor.Path for grandchild process dispatch Co-authored-by: adamsitnik <6011991+adamsitnik@users.noreply.github.com> --- .../System.Diagnostics.Process/tests/ProcessWaitingTests.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/libraries/System.Diagnostics.Process/tests/ProcessWaitingTests.cs b/src/libraries/System.Diagnostics.Process/tests/ProcessWaitingTests.cs index a04505f209cc10..4039529a0c754d 100644 --- a/src/libraries/System.Diagnostics.Process/tests/ProcessWaitingTests.cs +++ b/src/libraries/System.Diagnostics.Process/tests/ProcessWaitingTests.cs @@ -717,8 +717,8 @@ public async Task WaitForExit_WithGrandChildProcess_DoesNotHang(string waitMetho private static int StartGrandChildProcessAndExit() { using Process grandChild = new Process(); - grandChild.StartInfo.FileName = Environment.ProcessPath; - grandChild.StartInfo.Arguments = $"exec \"{typeof(ProcessWaitingTests).Assembly.Location}\" {nameof(SleepForEightHours)}"; + grandChild.StartInfo.FileName = RemoteExecutor.HostRunner; + grandChild.StartInfo.Arguments = $"exec \"{RemoteExecutor.Path}\" {typeof(ProcessWaitingTests).Assembly.Location} {nameof(SleepForEightHours)}"; grandChild.Start(); Console.WriteLine(grandChild.Id); From a0f49f563b062001afcf5643b4c04889d62cf8c5 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 18 Mar 2026 16:03:20 +0000 Subject: [PATCH 4/8] Use CreateProcess for grandchild process in test per reviewer feedback Co-authored-by: adamsitnik <6011991+adamsitnik@users.noreply.github.com> --- .../tests/ProcessWaitingTests.cs | 22 ++++++++----------- 1 file changed, 9 insertions(+), 13 deletions(-) diff --git a/src/libraries/System.Diagnostics.Process/tests/ProcessWaitingTests.cs b/src/libraries/System.Diagnostics.Process/tests/ProcessWaitingTests.cs index 4039529a0c754d..e1d48347386651 100644 --- a/src/libraries/System.Diagnostics.Process/tests/ProcessWaitingTests.cs +++ b/src/libraries/System.Diagnostics.Process/tests/ProcessWaitingTests.cs @@ -679,7 +679,14 @@ public async Task WaitForExitAsync_NotDirected_ThrowsInvalidOperationException() [InlineData("WaitForExitAsync")] public async Task WaitForExit_WithGrandChildProcess_DoesNotHang(string waitMethod) { - Process child = CreateProcess(StartGrandChildProcessAndExit); + Process child = CreateProcess(() => + { + using Process grandChild = CreateProcess(SleepForEightHours); + grandChild.Start(); + Console.WriteLine(grandChild.Id); + + return RemoteExecutor.SuccessExitCode; + }); child.StartInfo.RedirectStandardOutput = true; child.Start(); @@ -714,18 +721,7 @@ public async Task WaitForExit_WithGrandChildProcess_DoesNotHang(string waitMetho catch (Exception) { } } - private static int StartGrandChildProcessAndExit() - { - using Process grandChild = new Process(); - grandChild.StartInfo.FileName = RemoteExecutor.HostRunner; - grandChild.StartInfo.Arguments = $"exec \"{RemoteExecutor.Path}\" {typeof(ProcessWaitingTests).Assembly.Location} {nameof(SleepForEightHours)}"; - grandChild.Start(); - Console.WriteLine(grandChild.Id); - - return RemoteExecutor.SuccessExitCode; - } - - public static int SleepForEightHours() + private static int SleepForEightHours() { Thread.Sleep(TimeSpan.FromHours(8)); From 679d12bfd27b96064da6b045837966e9d5eaf711 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 18 Mar 2026 16:29:47 +0000 Subject: [PATCH 5/8] Replace magic number 300 with StreamDrainDefaultTimeoutMs const Co-authored-by: adamsitnik <6011991+adamsitnik@users.noreply.github.com> --- .../src/System/Diagnostics/Process.Unix.cs | 2 +- .../src/System/Diagnostics/Process.Windows.cs | 2 +- .../src/System/Diagnostics/Process.cs | 9 +++++++-- 3 files changed, 9 insertions(+), 4 deletions(-) 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 a789be74236ec1..a39e963e7918f6 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 @@ -207,7 +207,7 @@ private bool WaitForExitCore(int milliseconds) if (exited) { - int streamTimeout = milliseconds == Timeout.Infinite ? 300 : milliseconds; + int streamTimeout = milliseconds == Timeout.Infinite ? StreamDrainDefaultTimeoutMs : milliseconds; _output?.CancelDueToProcessExit(streamTimeout); _error?.CancelDueToProcessExit(streamTimeout); } 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 43a3954b3b67f2..dda889c557b80d 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 @@ -178,7 +178,7 @@ private bool WaitForExitCore(int milliseconds) { if (_signaled) { - int streamTimeout = milliseconds == Timeout.Infinite ? 300 : milliseconds; + int streamTimeout = milliseconds == Timeout.Infinite ? StreamDrainDefaultTimeoutMs : milliseconds; _output?.CancelDueToProcessExit(streamTimeout); _error?.CancelDueToProcessExit(streamTimeout); } diff --git a/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/Process.cs b/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/Process.cs index 8b73d0135027df..ae96ff10eeff98 100644 --- a/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/Process.cs +++ b/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/Process.cs @@ -83,6 +83,11 @@ public partial class Process : Component internal bool _pendingOutputRead; internal bool _pendingErrorRead; + /// + /// Default timeout in milliseconds to wait for redirected streams to complete after the process exits. + /// + private const int StreamDrainDefaultTimeoutMs = 300; + private static int s_cachedSerializationSwitch; /// @@ -1566,12 +1571,12 @@ async Task CancelStreamReadersDueToProcessExitAsync() { if (_output is not null) { - await _output.CancelDueToProcessExitAsync(300).ConfigureAwait(false); + await _output.CancelDueToProcessExitAsync(StreamDrainDefaultTimeoutMs).ConfigureAwait(false); } if (_error is not null) { - await _error.CancelDueToProcessExitAsync(300).ConfigureAwait(false); + await _error.CancelDueToProcessExitAsync(StreamDrainDefaultTimeoutMs).ConfigureAwait(false); } } } From dae1fe04cd172e4fa70ac5a32d19c95164c414e4 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 18 Mar 2026 22:07:06 +0000 Subject: [PATCH 6/8] Move StreamDrainDefaultTimeoutMs to AsyncStreamReader, make Cancel methods parameterless, remove EOF Co-authored-by: adamsitnik <6011991+adamsitnik@users.noreply.github.com> --- .../src/System/Diagnostics/AsyncStreamReader.cs | 15 +++++++++------ .../src/System/Diagnostics/Process.Unix.cs | 5 ++--- .../src/System/Diagnostics/Process.Windows.cs | 5 ++--- .../src/System/Diagnostics/Process.cs | 9 ++------- 4 files changed, 15 insertions(+), 19 deletions(-) diff --git a/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/AsyncStreamReader.cs b/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/AsyncStreamReader.cs index bc8524864c112b..e7e58decd7833f 100644 --- a/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/AsyncStreamReader.cs +++ b/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/AsyncStreamReader.cs @@ -24,6 +24,11 @@ internal sealed class AsyncStreamReader : IDisposable { private const int DefaultBufferSize = 1024; // Byte buffer size + /// + /// Default timeout in milliseconds to wait for redirected streams to complete after the process exits. + /// + private const int StreamDrainDefaultTimeoutMs = 300; + private readonly Stream _stream; private readonly Decoder _decoder; private readonly byte[] _byteBuffer; @@ -253,24 +258,22 @@ private bool FlushMessageQueue(bool rethrowInNewThread) } } - internal Task EOF => _readToBufferTask ?? Task.CompletedTask; - - internal void CancelDueToProcessExit(int milliseconds) + internal void CancelDueToProcessExit() { Task? task = _readToBufferTask; - if (task is not null && !task.Wait(milliseconds)) + if (task is not null && !task.Wait(StreamDrainDefaultTimeoutMs)) { _cts.Cancel(); task.GetAwaiter().GetResult(); } } - internal async Task CancelDueToProcessExitAsync(int milliseconds) + internal async Task CancelDueToProcessExitAsync() { Task? task = _readToBufferTask; if (task is not null) { - Task completed = await Task.WhenAny(task, Task.Delay(milliseconds)).ConfigureAwait(false); + Task completed = await Task.WhenAny(task, Task.Delay(StreamDrainDefaultTimeoutMs)).ConfigureAwait(false); if (completed != task) { _cts.Cancel(); 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 a39e963e7918f6..61d89baf60c8b5 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 @@ -207,9 +207,8 @@ private bool WaitForExitCore(int milliseconds) if (exited) { - int streamTimeout = milliseconds == Timeout.Infinite ? StreamDrainDefaultTimeoutMs : milliseconds; - _output?.CancelDueToProcessExit(streamTimeout); - _error?.CancelDueToProcessExit(streamTimeout); + _output?.CancelDueToProcessExit(); + _error?.CancelDueToProcessExit(); } return exited; 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 dda889c557b80d..2980e5a0b42f44 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 @@ -178,9 +178,8 @@ private bool WaitForExitCore(int milliseconds) { if (_signaled) { - int streamTimeout = milliseconds == Timeout.Infinite ? StreamDrainDefaultTimeoutMs : milliseconds; - _output?.CancelDueToProcessExit(streamTimeout); - _error?.CancelDueToProcessExit(streamTimeout); + _output?.CancelDueToProcessExit(); + _error?.CancelDueToProcessExit(); } handle?.Dispose(); diff --git a/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/Process.cs b/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/Process.cs index ae96ff10eeff98..b2497a0eb565eb 100644 --- a/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/Process.cs +++ b/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/Process.cs @@ -83,11 +83,6 @@ public partial class Process : Component internal bool _pendingOutputRead; internal bool _pendingErrorRead; - /// - /// Default timeout in milliseconds to wait for redirected streams to complete after the process exits. - /// - private const int StreamDrainDefaultTimeoutMs = 300; - private static int s_cachedSerializationSwitch; /// @@ -1571,12 +1566,12 @@ async Task CancelStreamReadersDueToProcessExitAsync() { if (_output is not null) { - await _output.CancelDueToProcessExitAsync(StreamDrainDefaultTimeoutMs).ConfigureAwait(false); + await _output.CancelDueToProcessExitAsync().ConfigureAwait(false); } if (_error is not null) { - await _error.CancelDueToProcessExitAsync(StreamDrainDefaultTimeoutMs).ConfigureAwait(false); + await _error.CancelDueToProcessExitAsync().ConfigureAwait(false); } } } From 2d9fec379502c53e400ed9a5a51d494dab954940 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 19 Mar 2026 13:41:02 +0000 Subject: [PATCH 7/8] Add CancellationToken parameter to CancelDueToProcessExitAsync and pass from callers Co-authored-by: adamsitnik <6011991+adamsitnik@users.noreply.github.com> --- .../src/System/Diagnostics/AsyncStreamReader.cs | 4 ++-- .../src/System/Diagnostics/Process.cs | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/AsyncStreamReader.cs b/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/AsyncStreamReader.cs index e7e58decd7833f..a4b1640a2f743b 100644 --- a/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/AsyncStreamReader.cs +++ b/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/AsyncStreamReader.cs @@ -268,12 +268,12 @@ internal void CancelDueToProcessExit() } } - internal async Task CancelDueToProcessExitAsync() + internal async Task CancelDueToProcessExitAsync(CancellationToken cancellationToken) { Task? task = _readToBufferTask; if (task is not null) { - Task completed = await Task.WhenAny(task, Task.Delay(StreamDrainDefaultTimeoutMs)).ConfigureAwait(false); + Task completed = await Task.WhenAny(task, Task.Delay(StreamDrainDefaultTimeoutMs, cancellationToken)).ConfigureAwait(false); if (completed != task) { _cts.Cancel(); diff --git a/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/Process.cs b/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/Process.cs index b2497a0eb565eb..5e68e5ba046daf 100644 --- a/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/Process.cs +++ b/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/Process.cs @@ -1566,12 +1566,12 @@ async Task CancelStreamReadersDueToProcessExitAsync() { if (_output is not null) { - await _output.CancelDueToProcessExitAsync().ConfigureAwait(false); + await _output.CancelDueToProcessExitAsync(cancellationToken).ConfigureAwait(false); } if (_error is not null) { - await _error.CancelDueToProcessExitAsync().ConfigureAwait(false); + await _error.CancelDueToProcessExitAsync(cancellationToken).ConfigureAwait(false); } } } From 1991b3d96bf98210c8f097719982c5d8fc32ea8d Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 19 Mar 2026 14:09:03 +0000 Subject: [PATCH 8/8] Pass CancellationToken explicitly to CancelStreamReadersDueToProcessExitAsync local function Co-authored-by: adamsitnik <6011991+adamsitnik@users.noreply.github.com> --- .../src/System/Diagnostics/Process.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/Process.cs b/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/Process.cs index 5e68e5ba046daf..4f3d3abccf01f6 100644 --- a/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/Process.cs +++ b/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/Process.cs @@ -1527,7 +1527,7 @@ public async Task WaitForExitAsync(CancellationToken cancellationToken = default // exception up to the user if (HasExited) { - await CancelStreamReadersDueToProcessExitAsync().ConfigureAwait(false); + await CancelStreamReadersDueToProcessExitAsync(cancellationToken).ConfigureAwait(false); return; } @@ -1555,14 +1555,14 @@ public async Task WaitForExitAsync(CancellationToken cancellationToken = default } // Cancel stream readers after process exit with a default timeout - await CancelStreamReadersDueToProcessExitAsync().ConfigureAwait(false); + await CancelStreamReadersDueToProcessExitAsync(cancellationToken).ConfigureAwait(false); } finally { Exited -= handler; } - async Task CancelStreamReadersDueToProcessExitAsync() + async Task CancelStreamReadersDueToProcessExitAsync(CancellationToken cancellationToken) { if (_output is not null) {