Skip to content

Implement Process.ReadAllText and ReadAllBytes with platform-specific multiplexing#126807

Draft
Copilot wants to merge 6 commits intomainfrom
copilot/implement-readalltext-and-readallbytes
Draft

Implement Process.ReadAllText and ReadAllBytes with platform-specific multiplexing#126807
Copilot wants to merge 6 commits intomainfrom
copilot/implement-readalltext-and-readallbytes

Conversation

Copy link
Copy Markdown
Contributor

Copilot AI commented Apr 12, 2026

Description

Implements Process.ReadAllText and Process.ReadAllBytes — new APIs (approved in #125838) that drain both stdout and stderr without deadlocking, using single-threaded synchronous multiplexing. Both standard output and standard error must be redirected; the methods throw InvalidOperationException if either is missing.

using Process process = Process.Start(startInfo);
(string stdout, string stderr) = process.ReadAllText(timeout: TimeSpan.FromSeconds(30));
(byte[] stdoutBytes, byte[] stderrBytes) = process.ReadAllBytes();

Windows — Overlapped IO

  • ReadPipes is private static unsafe void with all resources (pins, overlapped, events) declared at method top and cleaned up in a single finally block — ensures no leaks if any allocation throws
  • NativeOverlapped* with ManualResetEvent per pipe
  • ReadFile (overlapped) → WaitHandle.WaitAnyGetOverlappedResult loop
  • CancelIoEx on timeout, GetOverlappedResult(bWait: true) to observe completion before freeing OVERLAPPED
  • Uses is not null pattern matching syntax throughout

Unix — poll(2)

  • Interop.Sys.Poll on stdout/stderr file descriptors
  • RandomAccess.Read once poll reports data ready
  • EINTR handling with deadline recalculation
  • ReadPipes is a regular private static void (non-nullable SafeFileHandle parameters)

Common (Process.Multiplexing.cs)

  • ValidateReadAllState: validates state (disposed, both streams redirected, async mode conflict) and sets sync mode — called from ReadAllText and ReadAllBytes before renting arrays from the pool
  • ReadPipesToBuffers: obtains handles with DangerousAddRef/DangerousRelease protection and delegates to platform ReadPipes
  • ReadAllBytes: calls ValidateReadAllState, rents from ArrayPool, calls ReadPipesToBuffers, copies results to owned arrays
  • ReadAllText: calls ValidateReadAllState, rents from ArrayPool, calls ReadPipesToBuffers, decodes directly from rented buffers via Encoding.GetString(byte[], int, int) (avoids extra buffer copy)
  • GetSafeFileHandleFromStreamReader: extracts handle from FileStream (Windows) or AnonymousPipeClientStream (Unix), returns out SafeHandle owner for reference counting
  • Buffer growth via RentLargerBuffer (2x, pool-backed)

Tests (17 cases)

  • Validation: no streams, only stdout redirected, only stderr redirected, async mode, disposed
  • Both streams, stdout-only output, stderr-only output
  • Large output (100KB, triggers buffer growth)
  • Empty output
  • Timeout on blocking process (single and both streams)

Copilot AI and others added 4 commits April 12, 2026 07:31
…le fixes needed)

Agent-Logs-Url: https://github.com/dotnet/runtime/sessions/45ece5ba-166f-4418-887f-3ecaba54615e

Co-authored-by: adamsitnik <6011991+adamsitnik@users.noreply.github.com>
…ead of GetAwaiter().GetResult()

Replace the async Task-based Windows implementation with proper Win32 overlapped IO:
- Allocate NativeOverlapped* with ManualResetEvent for each pipe
- Use Interop.Kernel32.ReadFile with overlapped structs
- Use WaitHandle.WaitAny for synchronous multiplexing
- Use GetOverlappedResult to retrieve bytes read
- Use CancelIoEx for timeout cancellation
- Add System.Threading.Overlapped project reference
- Add ReadFile, GetOverlappedResult, CancelIoEx interop references
- Fix nullable warning in Unix implementation

Agent-Logs-Url: https://github.com/dotnet/runtime/sessions/8af96aef-e52b-459d-a945-e1159e8be19b

Co-authored-by: adamsitnik <6011991+adamsitnik@users.noreply.github.com>
…in32Exception

Agent-Logs-Url: https://github.com/dotnet/runtime/sessions/8af96aef-e52b-459d-a945-e1159e8be19b

Co-authored-by: adamsitnik <6011991+adamsitnik@users.noreply.github.com>
Copilot AI review requested due to automatic review settings April 12, 2026 09:31
Copilot AI review requested due to automatic review settings April 12, 2026 09:31
@dotnet-policy-service
Copy link
Copy Markdown
Contributor

Tagging subscribers to this area: @dotnet/area-system-diagnostics-process
See info in area-owners.md if you want to be subscribed.

@adamsitnik adamsitnik added the NO-REVIEW Experimental/testing PR, do NOT review it label Apr 12, 2026
Copy link
Copy Markdown
Member

@adamsitnik adamsitnik left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot please address my feedback

Comment on lines +82 to +88
bool hasOutput = _standardOutput is not null;
bool hasError = _standardError is not null;

if (!hasOutput && !hasError)
{
throw new InvalidOperationException(SR.CantGetStandardOut);
}
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

After thinking about it for a while, I believe we should always require both std out and error to be redirected. Otherwise it could lead to a situation where the user forgets to redirect standard error, calls the new method, gets an empty error and thinks that nothing was written to standard error. While it could be written, but simply not read.

@copilot please make the verification more strict and update all called methods in a way they can assume that both std out and err will be always provided

Suggested change
bool hasOutput = _standardOutput is not null;
bool hasError = _standardError is not null;
if (!hasOutput && !hasError)
{
throw new InvalidOperationException(SR.CantGetStandardOut);
}
if (_standardOutput is null)
{
throw new InvalidOperationException(SR.CantGetStandardOut);
}
else if (_standardError is null)
{
throw new InvalidOperationException(SR.CantGetStandardError);
}

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in 0986690. Both _standardOutput and _standardError are now required — separate checks throw InvalidOperationException with the appropriate message if either is null. All downstream methods (ReadPipes on both platforms) now take non-nullable SafeFileHandle parameters. Added two new test cases (ReadAllBytes_ThrowsWhenOnlyOutputRedirected, ReadAllBytes_ThrowsWhenOnlyErrorRedirected) and updated all existing tests to always redirect both streams.

Comment on lines +128 to +134
byte[] outputResult = outputBytesRead > 0
? outputBuffer.AsSpan(0, outputBytesRead).ToArray()
: Array.Empty<byte>();

byte[] errorResult = errorBytesRead > 0
? errorBuffer.AsSpan(0, errorBytesRead).ToArray()
: Array.Empty<byte>();
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Create a new helper method that does everything that ReadAllBytes currently does except of creating a copy of the buffer. Then call this helper from ReadAllText in order to avoid creating a copy of the buffer when we just want to decode the text. And of course use this helper from ReadAllBytes as well.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in 0986690. Extracted ReadPipesToBuffers which handles validation, handle extraction, DangerousAddRef/DangerousRelease, and calls ReadPipes. Both ReadAllText and ReadAllBytes call it. ReadAllText now decodes directly from the rented buffers via Encoding.GetString(byte[], int, int) — no intermediate .ToArray() copy.

Comment on lines +150 to +162
private static SafeFileHandle GetSafeFileHandleFromStreamReader(StreamReader reader)
{
Stream baseStream = reader.BaseStream;

if (baseStream is FileStream fileStream)
{
return fileStream.SafeFileHandle;
}

if (baseStream is System.IO.Pipes.AnonymousPipeClientStream pipeStream)
{
return new SafeFileHandle(pipeStream.SafePipeHandle.DangerousGetHandle(), ownsHandle: false);
}
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Populate the owner and in the method that calls this helper call owner.DagnerousAddRef and accordingly owner.DangerousRelease before and after calling ReadPipes. This will allow us to avoid user disposing the handle while we are using it.

Suggested change
private static SafeFileHandle GetSafeFileHandleFromStreamReader(StreamReader reader)
{
Stream baseStream = reader.BaseStream;
if (baseStream is FileStream fileStream)
{
return fileStream.SafeFileHandle;
}
if (baseStream is System.IO.Pipes.AnonymousPipeClientStream pipeStream)
{
return new SafeFileHandle(pipeStream.SafePipeHandle.DangerousGetHandle(), ownsHandle: false);
}
private static SafeFileHandle GetSafeFileHandleFromStreamReader(StreamReader reader, out SafeHandle owner)
{
Stream baseStream = reader.BaseStream;
if (baseStream is FileStream fileStream)
{
owner = fileStream.SafeFileHandle;
return fileStream.SafeFileHandle;
}
if (baseStream is System.IO.Pipes.AnonymousPipeClientStream pipeStream)
{
owner = pipeStream.SafePipeHandle;
return new SafeFileHandle(pipeStream.SafePipeHandle.DangerousGetHandle(), ownsHandle: false);
}

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in 0986690. GetSafeFileHandleFromStreamReader now takes out SafeHandle owner. The caller in ReadPipesToBuffers calls DangerousAddRef before ReadPipes and DangerousRelease in the finally block.

Comment on lines +167 to +177
/// <summary>
/// Reads from one or both standard output and standard error pipes using platform-specific multiplexing.
/// </summary>
private static partial void ReadPipes(
SafeFileHandle? outputHandle,
SafeFileHandle? errorHandle,
int timeoutMs,
ref byte[] outputBuffer,
ref int outputBytesRead,
ref byte[] errorBuffer,
ref int errorBytesRead);
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As I wrote in the requirements, there is no need to use partial method here as we target only Windows and Unix platforms and the right method will be used anyway (form .Windows.cs or .Unix.cs file).

Suggested change
/// <summary>
/// Reads from one or both standard output and standard error pipes using platform-specific multiplexing.
/// </summary>
private static partial void ReadPipes(
SafeFileHandle? outputHandle,
SafeFileHandle? errorHandle,
int timeoutMs,
ref byte[] outputBuffer,
ref int outputBytesRead,
ref byte[] errorBuffer,
ref int errorBytesRead);

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in 0986690. Removed the partial method declaration. ReadPipes is now a regular private static void in each platform file.

…elper method, DangerousAddRef/Release, remove partial

Changes:
1. Require both stdout and stderr to be redirected (throw if either is missing)
2. Create ReadPipesToBuffers helper shared between ReadAllText and ReadAllBytes
   (ReadAllText avoids extra buffer copy by decoding from rented buffers directly)
3. GetSafeFileHandleFromStreamReader now returns an owner SafeHandle; caller uses
   DangerousAddRef/DangerousRelease to prevent handle disposal during reads
4. Remove partial method declaration; ReadPipes is a regular static method in each
   platform file (non-nullable parameters since both handles always provided)
5. Update tests: all tests redirect both stdout and stderr; add two new tests for
   missing-stream validation

Agent-Logs-Url: https://github.com/dotnet/runtime/sessions/cd917b13-f344-47c5-ac1e-cfb2c2da34f6

Co-authored-by: adamsitnik <6011991+adamsitnik@users.noreply.github.com>
Copilot AI requested review from Copilot and removed request for Copilot April 12, 2026 10:44
Copy link
Copy Markdown
Contributor Author

Copilot AI commented Apr 12, 2026

@copilot please address my feedback

All feedback addressed in 0986690. Summary of changes:

  1. Both stdout and stderr must be redirected (strict validation with separate error messages)
  2. ReadPipesToBuffers helper shared between ReadAllText and ReadAllBytes (avoids extra buffer copy in ReadAllText)
  3. GetSafeFileHandleFromStreamReader returns out SafeHandle owner; caller uses DangerousAddRef/DangerousRelease
  4. Removed partial method declaration — ReadPipes is a regular static method per platform with non-nullable parameters
  5. All tests updated to redirect both streams; added 2 new validation tests (17/17 pass)

Copilot AI requested a review from adamsitnik April 12, 2026 10:46
Copy link
Copy Markdown
Member

@adamsitnik adamsitnik left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot address my feedback

Comment on lines +129 to +149
CheckDisposed();

if (_standardOutput is null)
{
throw new InvalidOperationException(SR.CantGetStandardOut);
}

if (_standardError is null)
{
throw new InvalidOperationException(SR.CantGetStandardError);
}

if (_outputStreamReadMode == StreamReadMode.AsyncMode)
{
throw new InvalidOperationException(SR.CantMixSyncAsyncOperation);
}

if (_errorStreamReadMode == StreamReadMode.AsyncMode)
{
throw new InvalidOperationException(SR.CantMixSyncAsyncOperation);
}
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Move the validation logic to a helper method and call it from ReadAllBytes and ReadAllText before renting the arrays from the pool.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in df0e65a. Extracted ValidateReadAllState() which is called from both ReadAllText and ReadAllBytes before renting arrays from the pool. ReadPipesToBuffers no longer contains any validation logic.

}
finally
{
if (outputOverlapped != null)
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: prefer is null and is not null syntax

Suggested change
if (outputOverlapped != null)
if (outputOverlapped is not null)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in df0e65a.

NativeOverlapped* outputOverlapped = null;
NativeOverlapped* errorOverlapped = null;

EventWaitHandle outputEvent = new(initialState: false, EventResetMode.ManualReset);
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this can throw (for example OutOfMemoryException), in such case outputPin won't be diposed.

Make whole method unsafe, remove the unsafe block from here and make sure that the pins, overlapped and events always get cleaned up.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in df0e65a. The method is now private static unsafe void ReadPipes(...). All resources (pins, overlapped, events) are declared and initialized to defaults at the top, allocated inside a single try block, and cleaned up in the finally — so any exception at any allocation point will properly dispose previously allocated resources.

…indows ReadPipes fully unsafe with proper cleanup, use is not null

Agent-Logs-Url: https://github.com/dotnet/runtime/sessions/451cf437-2de0-409a-937a-0031c3fa69d4

Co-authored-by: adamsitnik <6011991+adamsitnik@users.noreply.github.com>
Copilot AI requested review from Copilot and removed request for Copilot April 12, 2026 11:36
Copy link
Copy Markdown
Contributor Author

Copilot AI commented Apr 12, 2026

@copilot address my feedback

All feedback addressed in df0e65aValidateReadAllState helper extracted, Windows ReadPipes made fully unsafe with proper cleanup ordering, is not null syntax. 17/17 tests pass.

Copilot AI requested a review from adamsitnik April 12, 2026 11:38
Comment on lines +42 to +43
outputOverlapped = AllocateOverlapped(outputEvent);
errorOverlapped = AllocateOverlapped(errorEvent);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can't the overlapped's be allocated on the stack?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area-System.Diagnostics.Process NO-REVIEW Experimental/testing PR, do NOT review it

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants