-
Notifications
You must be signed in to change notification settings - Fork 5.4k
Implement Process.ReadAllText and ReadAllBytes with platform-specific multiplexing #126807
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
adamsitnik
merged 20 commits into
main
from
copilot/implement-readalltext-and-readallbytes
Apr 15, 2026
Merged
Changes from 6 commits
Commits
Show all changes
20 commits
Select commit
Hold shift + click to select a range
6946997
Add Process.ReadAllText and ReadAllBytes with platform-specific multi…
Copilot c2dceaf
Fix fileOffset values and refactor to unified ReadPipes (WIP - nullab…
Copilot d518461
Rewrite Windows ReadPipes to use overlapped IO with wait handles inst…
Copilot 4eb1e88
Fix code review: make RentLargerBuffer private, fix fully-qualified W…
Copilot 0986690
Address review feedback: require both stdout/stderr redirected, add h…
Copilot df0e65a
Address round 2 feedback: extract ValidateReadAllState helper, make W…
Copilot 5d6e9c0
address my own feedback:
adamsitnik 99ef166
Apply suggestions from code review
adamsitnik 70df3b1
fix the build
adamsitnik 474a484
Fix test missing else, use ToTimeoutMilliseconds for validation, fix …
Copilot 9775b5d
Require StreamReadMode.Undefined for ReadAll*, rename test, add sync …
Copilot 1d30ed2
Address feedback: Win32Exception fix, ref assembly order, AllocateOve…
Copilot 77dceb0
Add using to Process in ReadAllBytes_ReadsBinaryDataWithNullBytes test
Copilot 9896de0
Fix RentLargerBuffer infinite loop at Array.MaxLength, use Assert.Equ…
Copilot c3bd6bc
address my own feedback
adamsitnik ce6b594
Merge branch 'main' into copilot/implement-readalltext-and-readallbytes
adamsitnik e8cbe82
Address jkotas and tmds feedback: PinnedGCHandle, RentLargerBuffer im…
Copilot 6ecc0b9
Flip WaitHandle order to prioritize stderr, add Raymond Chen blog links
Copilot 8f7c3c8
Remove Raymond Chen blog links from CancelPendingIOIfNeeded — keep on…
Copilot c92f673
Use non-blocking IO on Unix: DangerousSetIsNonBlocking + Interop.Sys.…
Copilot File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Some comments aren't visible on the classic Files Changed page.
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
131 changes: 131 additions & 0 deletions
131
src/libraries/System.Diagnostics.Process/src/System/Diagnostics/Process.Multiplexing.Unix.cs
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,131 @@ | ||
| // Licensed to the .NET Foundation under one or more agreements. | ||
| // The .NET Foundation licenses this file to you under the MIT license. | ||
|
|
||
| using System.ComponentModel; | ||
| using System.IO; | ||
| using System.Runtime.InteropServices; | ||
| using Microsoft.Win32.SafeHandles; | ||
|
|
||
| namespace System.Diagnostics | ||
| { | ||
| public partial class Process | ||
| { | ||
| /// <summary> | ||
| /// Reads from both standard output and standard error pipes using Unix poll-based multiplexing. | ||
| /// </summary> | ||
| private static void ReadPipes( | ||
| SafeFileHandle outputHandle, | ||
| SafeFileHandle errorHandle, | ||
| int timeoutMs, | ||
| ref byte[] outputBuffer, | ||
| ref int outputBytesRead, | ||
| ref byte[] errorBuffer, | ||
| ref int errorBytesRead) | ||
| { | ||
| int outputFd = outputHandle.DangerousGetHandle().ToInt32(); | ||
| int errorFd = errorHandle.DangerousGetHandle().ToInt32(); | ||
| bool outputDone = false; | ||
| bool errorDone = false; | ||
|
|
||
| Interop.PollEvent[] pollFds = new Interop.PollEvent[2]; | ||
|
|
||
| long deadline = timeoutMs >= 0 | ||
| ? Environment.TickCount64 + timeoutMs | ||
| : long.MaxValue; | ||
|
|
||
| while (!outputDone || !errorDone) | ||
| { | ||
| int numFds = 0; | ||
|
|
||
| int outputIndex = -1; | ||
| int errorIndex = -1; | ||
|
|
||
| if (!outputDone) | ||
| { | ||
| outputIndex = numFds; | ||
| pollFds[numFds].FileDescriptor = outputFd; | ||
| pollFds[numFds].Events = Interop.PollEvents.POLLIN; | ||
| pollFds[numFds].TriggeredEvents = Interop.PollEvents.POLLNONE; | ||
| numFds++; | ||
| } | ||
|
|
||
| if (!errorDone) | ||
| { | ||
| errorIndex = numFds; | ||
| pollFds[numFds].FileDescriptor = errorFd; | ||
| pollFds[numFds].Events = Interop.PollEvents.POLLIN; | ||
| pollFds[numFds].TriggeredEvents = Interop.PollEvents.POLLNONE; | ||
| numFds++; | ||
| } | ||
|
|
||
| int pollTimeout; | ||
| if (timeoutMs >= 0) | ||
| { | ||
| long remaining = deadline - Environment.TickCount64; | ||
| if (remaining <= 0) | ||
| { | ||
| throw new TimeoutException(); | ||
| } | ||
|
|
||
| pollTimeout = (int)Math.Min(remaining, int.MaxValue); | ||
| } | ||
| else | ||
| { | ||
| pollTimeout = -1; // Infinite | ||
| } | ||
|
|
||
| unsafe | ||
| { | ||
| uint triggered; | ||
| fixed (Interop.PollEvent* pPollFds = pollFds) | ||
| { | ||
| Interop.Error error = Interop.Sys.Poll(pPollFds, (uint)numFds, pollTimeout, &triggered); | ||
|
adamsitnik marked this conversation as resolved.
|
||
| if (error != Interop.Error.SUCCESS) | ||
| { | ||
| if (error == Interop.Error.EINTR) | ||
| { | ||
| continue; | ||
| } | ||
|
|
||
| throw new Win32Exception(Marshal.GetLastPInvokeError()); | ||
| } | ||
|
|
||
| if (triggered == 0) | ||
| { | ||
| throw new TimeoutException(); | ||
| } | ||
| } | ||
| } | ||
|
|
||
| for (int i = 0; i < numFds; i++) | ||
| { | ||
| if ((pollFds[i].TriggeredEvents & (Interop.PollEvents.POLLIN | Interop.PollEvents.POLLHUP | Interop.PollEvents.POLLERR)) == Interop.PollEvents.POLLNONE) | ||
| { | ||
| continue; | ||
| } | ||
|
adamsitnik marked this conversation as resolved.
|
||
|
|
||
| bool isError = i == errorIndex; | ||
| SafeFileHandle currentHandle = isError ? errorHandle : outputHandle; | ||
| ref byte[] currentBuffer = ref (isError ? ref errorBuffer : ref outputBuffer); | ||
| ref int currentBytesRead = ref (isError ? ref errorBytesRead : ref outputBytesRead); | ||
| ref bool currentDone = ref (isError ? ref errorDone : ref outputDone); | ||
|
|
||
| int bytesRead = RandomAccess.Read(currentHandle, currentBuffer.AsSpan(currentBytesRead), fileOffset: 0); | ||
|
adamsitnik marked this conversation as resolved.
Outdated
adamsitnik marked this conversation as resolved.
Outdated
|
||
| if (bytesRead > 0) | ||
| { | ||
| currentBytesRead += bytesRead; | ||
|
|
||
| if (currentBytesRead == currentBuffer.Length) | ||
| { | ||
| RentLargerBuffer(ref currentBuffer, currentBytesRead); | ||
| } | ||
| } | ||
| else | ||
| { | ||
| currentDone = true; | ||
| } | ||
| } | ||
| } | ||
| } | ||
| } | ||
| } | ||
209 changes: 209 additions & 0 deletions
209
...braries/System.Diagnostics.Process/src/System/Diagnostics/Process.Multiplexing.Windows.cs
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,209 @@ | ||
| // Licensed to the .NET Foundation under one or more agreements. | ||
| // The .NET Foundation licenses this file to you under the MIT license. | ||
|
|
||
| using System.Buffers; | ||
| using System.ComponentModel; | ||
| using System.Runtime.InteropServices; | ||
| using System.Threading; | ||
| using Microsoft.Win32.SafeHandles; | ||
|
|
||
| namespace System.Diagnostics | ||
| { | ||
| public partial class Process | ||
| { | ||
| /// <summary> | ||
| /// Reads from both standard output and standard error pipes using Windows overlapped IO | ||
| /// with wait handles for single-threaded synchronous multiplexing. | ||
| /// </summary> | ||
| private static unsafe void ReadPipes( | ||
| SafeFileHandle outputHandle, | ||
| SafeFileHandle errorHandle, | ||
| int timeoutMs, | ||
| ref byte[] outputBuffer, | ||
| ref int outputBytesRead, | ||
| ref byte[] errorBuffer, | ||
| ref int errorBytesRead) | ||
| { | ||
| MemoryHandle outputPin = default; | ||
| MemoryHandle errorPin = default; | ||
| NativeOverlapped* outputOverlapped = null; | ||
| NativeOverlapped* errorOverlapped = null; | ||
| EventWaitHandle? outputEvent = null; | ||
| EventWaitHandle? errorEvent = null; | ||
|
|
||
| try | ||
| { | ||
| outputPin = outputBuffer.AsMemory().Pin(); | ||
|
jkotas marked this conversation as resolved.
Outdated
|
||
| errorPin = errorBuffer.AsMemory().Pin(); | ||
|
|
||
| outputEvent = new EventWaitHandle(initialState: false, EventResetMode.ManualReset); | ||
| errorEvent = new EventWaitHandle(initialState: false, EventResetMode.ManualReset); | ||
|
|
||
| outputOverlapped = AllocateOverlapped(outputEvent); | ||
| errorOverlapped = AllocateOverlapped(errorEvent); | ||
|
adamsitnik marked this conversation as resolved.
|
||
|
|
||
| bool outputDone = false; | ||
| bool errorDone = false; | ||
|
|
||
| WaitHandle[] waitHandles = [outputEvent, errorEvent]; | ||
|
adamsitnik marked this conversation as resolved.
Outdated
|
||
|
|
||
| // Issue initial reads. | ||
| Interop.Kernel32.ReadFile(outputHandle, (byte*)outputPin.Pointer + outputBytesRead, | ||
| outputBuffer.Length - outputBytesRead, IntPtr.Zero, outputOverlapped); | ||
|
|
||
| Interop.Kernel32.ReadFile(errorHandle, (byte*)errorPin.Pointer + errorBytesRead, | ||
| errorBuffer.Length - errorBytesRead, IntPtr.Zero, errorOverlapped); | ||
|
|
||
|
adamsitnik marked this conversation as resolved.
|
||
| long deadline = timeoutMs >= 0 | ||
| ? Environment.TickCount64 + timeoutMs | ||
| : long.MaxValue; | ||
|
|
||
| while (!outputDone || !errorDone) | ||
| { | ||
| int waitTimeout; | ||
| if (timeoutMs >= 0) | ||
| { | ||
| long remaining = deadline - Environment.TickCount64; | ||
| if (remaining <= 0) | ||
| { | ||
| CancelPendingIOIfNeeded(outputHandle, outputDone, outputOverlapped); | ||
| CancelPendingIOIfNeeded(errorHandle, errorDone, errorOverlapped); | ||
| throw new TimeoutException(); | ||
| } | ||
|
|
||
| waitTimeout = (int)Math.Min(remaining, int.MaxValue); | ||
| } | ||
| else | ||
| { | ||
| waitTimeout = Timeout.Infinite; | ||
| } | ||
|
|
||
| int waitResult = WaitHandle.WaitAny(waitHandles, waitTimeout); | ||
|
|
||
| if (waitResult == WaitHandle.WaitTimeout) | ||
| { | ||
| CancelPendingIOIfNeeded(outputHandle, outputDone, outputOverlapped); | ||
| CancelPendingIOIfNeeded(errorHandle, errorDone, errorOverlapped); | ||
| throw new TimeoutException(); | ||
| } | ||
|
|
||
| bool isError = waitResult == 1; | ||
| NativeOverlapped* currentOverlapped = isError ? errorOverlapped : outputOverlapped; | ||
| SafeFileHandle currentHandle = isError ? errorHandle : outputHandle; | ||
| ref int totalBytesRead = ref (isError ? ref errorBytesRead : ref outputBytesRead); | ||
| ref byte[] currentBuffer = ref (isError ? ref errorBuffer : ref outputBuffer); | ||
| EventWaitHandle currentEvent = isError ? errorEvent : outputEvent; | ||
|
|
||
| int bytesRead = GetOverlappedResultForPipe(currentHandle, currentOverlapped); | ||
|
|
||
| if (bytesRead > 0) | ||
| { | ||
| totalBytesRead += bytesRead; | ||
|
|
||
| if (totalBytesRead == currentBuffer.Length) | ||
| { | ||
| ref MemoryHandle currentPin = ref (isError ? ref errorPin : ref outputPin); | ||
| currentPin.Dispose(); | ||
|
|
||
| RentLargerBuffer(ref currentBuffer, totalBytesRead); | ||
|
|
||
| currentPin = currentBuffer.AsMemory().Pin(); | ||
| } | ||
|
|
||
| // Reset the event and overlapped for next read. | ||
| ResetOverlapped(currentEvent, currentOverlapped); | ||
|
|
||
| byte* pinPointer = isError ? (byte*)errorPin.Pointer : (byte*)outputPin.Pointer; | ||
| Interop.Kernel32.ReadFile(currentHandle, pinPointer + totalBytesRead, | ||
| currentBuffer.Length - totalBytesRead, IntPtr.Zero, currentOverlapped); | ||
| } | ||
|
adamsitnik marked this conversation as resolved.
Outdated
|
||
| else | ||
| { | ||
| // EOF: pipe write end was closed. | ||
| if (isError) | ||
| { | ||
| errorDone = true; | ||
| } | ||
| else | ||
| { | ||
| outputDone = true; | ||
| } | ||
|
|
||
| // Reset the event so WaitAny won't trigger on this stale handle. | ||
| currentEvent.Reset(); | ||
| } | ||
| } | ||
| } | ||
| finally | ||
| { | ||
| if (outputOverlapped is not null) | ||
| { | ||
| NativeMemory.Free(outputOverlapped); | ||
| } | ||
|
|
||
| if (errorOverlapped is not null) | ||
| { | ||
| NativeMemory.Free(errorOverlapped); | ||
| } | ||
|
|
||
| outputEvent?.Dispose(); | ||
| errorEvent?.Dispose(); | ||
| outputPin.Dispose(); | ||
| errorPin.Dispose(); | ||
| } | ||
| } | ||
|
|
||
| private static unsafe NativeOverlapped* AllocateOverlapped(EventWaitHandle waitHandle) | ||
| { | ||
| NativeOverlapped* overlapped = (NativeOverlapped*)NativeMemory.AllocZeroed((nuint)sizeof(NativeOverlapped)); | ||
| overlapped->EventHandle = waitHandle.SafeWaitHandle.DangerousGetHandle(); | ||
| return overlapped; | ||
| } | ||
|
|
||
| private static unsafe void ResetOverlapped(EventWaitHandle waitHandle, NativeOverlapped* overlapped) | ||
| { | ||
| waitHandle.Reset(); | ||
|
|
||
| overlapped->InternalHigh = IntPtr.Zero; | ||
| overlapped->InternalLow = IntPtr.Zero; | ||
| overlapped->OffsetHigh = 0; | ||
| overlapped->OffsetLow = 0; | ||
| overlapped->EventHandle = waitHandle.SafeWaitHandle.DangerousGetHandle(); | ||
| } | ||
|
|
||
| private static unsafe int GetOverlappedResultForPipe(SafeFileHandle handle, NativeOverlapped* overlapped) | ||
| { | ||
| int bytesRead = 0; | ||
| if (!Interop.Kernel32.GetOverlappedResult(handle, overlapped, ref bytesRead, bWait: false)) | ||
| { | ||
| int errorCode = Marshal.GetLastPInvokeError(); | ||
| switch (errorCode) | ||
| { | ||
| case Interop.Errors.ERROR_HANDLE_EOF: | ||
| case Interop.Errors.ERROR_BROKEN_PIPE: | ||
| case Interop.Errors.ERROR_PIPE_NOT_CONNECTED: | ||
| return 0; | ||
| default: | ||
| throw new Win32Exception(errorCode); | ||
| } | ||
| } | ||
|
|
||
| return bytesRead; | ||
| } | ||
|
|
||
| private static unsafe void CancelPendingIOIfNeeded(SafeFileHandle handle, bool done, NativeOverlapped* overlapped) | ||
| { | ||
| if (done) | ||
| { | ||
| return; | ||
| } | ||
|
|
||
| // CancelIoEx marks matching outstanding I/O requests for cancellation. | ||
| Interop.Kernel32.CancelIoEx(handle, overlapped); | ||
|
|
||
| // We must observe completion before freeing the OVERLAPPED. | ||
| int bytesRead = 0; | ||
| Interop.Kernel32.GetOverlappedResult(handle, overlapped, ref bytesRead, bWait: true); | ||
| } | ||
| } | ||
| } | ||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.