-
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 all 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
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
153 changes: 153 additions & 0 deletions
153
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,153 @@ | ||
| // 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.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 | ||
| /// with non-blocking reads. | ||
| /// </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(); | ||
|
|
||
| if (Interop.Sys.Fcntl.DangerousSetIsNonBlocking((IntPtr)outputFd, 1) != 0 || Interop.Sys.Fcntl.DangerousSetIsNonBlocking((IntPtr)errorFd, 1) != 0) | ||
| { | ||
| throw new Win32Exception(); | ||
| } | ||
|
|
||
| Span<Interop.PollEvent> pollFds = stackalloc Interop.PollEvent[2]; | ||
|
|
||
| long deadline = timeoutMs >= 0 | ||
| ? Environment.TickCount64 + timeoutMs | ||
| : long.MaxValue; | ||
|
|
||
| bool outputDone = false, errorDone = false; | ||
| 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 (!TryGetRemainingTimeout(deadline, timeoutMs, out pollTimeout)) | ||
| { | ||
| throw new TimeoutException(); | ||
| } | ||
|
|
||
| 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) | ||
| { | ||
| // We don't re-issue the poll immediately because we need to check | ||
| // if we've already exceeded the overall timeout. | ||
| continue; | ||
| } | ||
|
|
||
| throw new Win32Exception(Interop.Sys.ConvertErrorPalToPlatform(error)); | ||
| } | ||
|
|
||
| if (triggered == 0) | ||
| { | ||
| throw new TimeoutException(); | ||
| } | ||
| } | ||
| } | ||
|
|
||
| for (int i = 0; i < numFds; i++) | ||
| { | ||
| if (pollFds[i].TriggeredEvents == 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 = ReadNonBlocking(currentHandle, currentBuffer, currentBytesRead); | ||
| if (bytesRead > 0) | ||
| { | ||
| currentBytesRead += bytesRead; | ||
|
|
||
| if (currentBytesRead == currentBuffer.Length) | ||
| { | ||
| RentLargerBuffer(ref currentBuffer, currentBytesRead); | ||
| } | ||
| } | ||
| else if (bytesRead == 0) | ||
| { | ||
| // EOF: pipe write end was closed. | ||
| currentDone = true; | ||
| } | ||
| // bytesRead < 0 means EAGAIN — nothing available yet, let poll retry. | ||
| } | ||
| } | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// Performs a non-blocking read from the given handle into the buffer starting at the specified offset. | ||
| /// Returns the number of bytes read, 0 for EOF, or -1 for EAGAIN (nothing available yet). | ||
| /// </summary> | ||
| private static unsafe int ReadNonBlocking(SafeFileHandle handle, byte[] buffer, int offset) | ||
| { | ||
| fixed (byte* pBuffer = buffer) | ||
| { | ||
| int bytesRead = Interop.Sys.Read(handle, pBuffer + offset, buffer.Length - offset); | ||
| if (bytesRead < 0) | ||
| { | ||
| Interop.ErrorInfo errorInfo = Interop.Sys.GetLastErrorInfo(); | ||
| if (errorInfo.Error == Interop.Error.EAGAIN) | ||
| { | ||
| return -1; | ||
| } | ||
|
|
||
| throw new Win32Exception(errorInfo.RawErrno); | ||
| } | ||
|
|
||
| return bytesRead; | ||
| } | ||
| } | ||
| } | ||
| } | ||
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.