Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
6946997
Add Process.ReadAllText and ReadAllBytes with platform-specific multi…
Copilot Apr 12, 2026
c2dceaf
Fix fileOffset values and refactor to unified ReadPipes (WIP - nullab…
Copilot Apr 12, 2026
d518461
Rewrite Windows ReadPipes to use overlapped IO with wait handles inst…
Copilot Apr 12, 2026
4eb1e88
Fix code review: make RentLargerBuffer private, fix fully-qualified W…
Copilot Apr 12, 2026
0986690
Address review feedback: require both stdout/stderr redirected, add h…
Copilot Apr 12, 2026
df0e65a
Address round 2 feedback: extract ValidateReadAllState helper, make W…
Copilot Apr 12, 2026
5d6e9c0
address my own feedback:
adamsitnik Apr 13, 2026
99ef166
Apply suggestions from code review
adamsitnik Apr 13, 2026
70df3b1
fix the build
adamsitnik Apr 13, 2026
474a484
Fix test missing else, use ToTimeoutMilliseconds for validation, fix …
Copilot Apr 13, 2026
9775b5d
Require StreamReadMode.Undefined for ReadAll*, rename test, add sync …
Copilot Apr 13, 2026
1d30ed2
Address feedback: Win32Exception fix, ref assembly order, AllocateOve…
Copilot Apr 13, 2026
77dceb0
Add using to Process in ReadAllBytes_ReadsBinaryDataWithNullBytes test
Copilot Apr 13, 2026
9896de0
Fix RentLargerBuffer infinite loop at Array.MaxLength, use Assert.Equ…
Copilot Apr 13, 2026
c3bd6bc
address my own feedback
adamsitnik Apr 13, 2026
ce6b594
Merge branch 'main' into copilot/implement-readalltext-and-readallbytes
adamsitnik Apr 13, 2026
e8cbe82
Address jkotas and tmds feedback: PinnedGCHandle, RentLargerBuffer im…
Copilot Apr 14, 2026
6ecc0b9
Flip WaitHandle order to prioritize stderr, add Raymond Chen blog links
Copilot Apr 14, 2026
8f7c3c8
Remove Raymond Chen blog links from CancelPendingIOIfNeeded — keep on…
Copilot Apr 14, 2026
c92f673
Use non-blocking IO on Unix: DangerousSetIsNonBlocking + Interop.Sys.…
Copilot Apr 14, 2026
cdd3da9
Add ReadAllTextAsync and ReadAllBytesAsync to Process
Copilot Apr 15, 2026
057bb80
Simplify WhenAny multiplexing to avoid redundant await
Copilot Apr 15, 2026
d5b93cc
Add ReadAllTextAsync and ReadAllBytesAsync to Process
Copilot Apr 15, 2026
06bb8da
Merge remote-tracking branch 'origin/copilot/add-readalltextbytes-asy…
Copilot Apr 15, 2026
24b0960
Fix double-return-to-pool bug: ReadPipesToBuffersAsync takes buffer o…
Copilot Apr 15, 2026
36d802c
Improve comment clarity in ReadPipesToBuffersAsync catch block
Copilot Apr 15, 2026
28d0ee9
Revert "Improve comment clarity in ReadPipesToBuffersAsync catch block"
adamsitnik Apr 15, 2026
4f2873b
Revert "Fix double-return-to-pool bug: ReadPipesToBuffersAsync takes …
adamsitnik Apr 15, 2026
3223fa1
address feedback:
adamsitnik Apr 15, 2026
8875214
Merge branch 'main' into copilot/add-readalltextbytes-async-methods
adamsitnik Apr 15, 2026
86e745a
Replace WhenAny multiplexing with parallel per-stream ReadPipeToBuffe…
Copilot Apr 15, 2026
442fb0f
return the pooled array even when one has failed and other succeeded
adamsitnik Apr 15, 2026
c0c1d8b
address code review feedback
adamsitnik Apr 15, 2026
378715b
fix typos/grammar
adamsitnik Apr 15, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -158,7 +158,9 @@ public void Kill(bool entireProcessTree) { }
public static void LeaveDebugMode() { }
protected void OnExited() { }
public (byte[] StandardOutput, byte[] StandardError) ReadAllBytes(System.TimeSpan? timeout = default(System.TimeSpan?)) { throw null; }
public System.Threading.Tasks.Task<(byte[] StandardOutput, byte[] StandardError)> ReadAllBytesAsync(System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; }
public (string StandardOutput, string StandardError) ReadAllText(System.TimeSpan? timeout = default(System.TimeSpan?)) { throw null; }
public System.Threading.Tasks.Task<(string StandardOutput, string StandardError)> ReadAllTextAsync(System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; }
public void Refresh() { }
[System.Runtime.Versioning.UnsupportedOSPlatformAttribute("ios")]
[System.Runtime.Versioning.UnsupportedOSPlatformAttribute("tvos")]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,20 +2,24 @@
// The .NET Foundation licenses this file to you under the MIT license.

using System.ComponentModel;
using System.IO;
using System.IO.Pipes;
using System.Runtime.InteropServices;
using Microsoft.Win32.SafeHandles;

namespace System.Diagnostics
{
public partial class Process
{
private static SafePipeHandle GetSafeHandleFromStreamReader(StreamReader reader) => ((AnonymousPipeClientStream)reader.BaseStream).SafePipeHandle;

/// <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,
SafePipeHandle outputHandle,
SafePipeHandle errorHandle,
int timeoutMs,
ref byte[] outputBuffer,
ref int outputBytesRead,
Expand All @@ -25,7 +29,7 @@ private static void ReadPipes(
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)
if (Interop.Sys.Fcntl.DangerousSetIsNonBlocking(outputFd, 1) != 0 || Interop.Sys.Fcntl.DangerousSetIsNonBlocking(errorFd, 1) != 0)
{
throw new Win32Exception();
Comment thread
adamsitnik marked this conversation as resolved.
}
Expand Down Expand Up @@ -101,7 +105,7 @@ private static void ReadPipes(
}

bool isError = i == errorIndex;
SafeFileHandle currentHandle = isError ? errorHandle : outputHandle;
SafePipeHandle 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);
Expand Down Expand Up @@ -130,7 +134,7 @@ private static void ReadPipes(
/// 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)
private static unsafe int ReadNonBlocking(SafePipeHandle handle, byte[] buffer, int offset)
{
fixed (byte* pBuffer = buffer)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
// The .NET Foundation licenses this file to you under the MIT license.

using System.ComponentModel;
using System.IO;
using System.Runtime.InteropServices;
using System.Threading;
using Microsoft.Win32.SafeHandles;
Expand All @@ -10,6 +11,8 @@ namespace System.Diagnostics
{
public partial class Process
{
private static SafeFileHandle GetSafeHandleFromStreamReader(StreamReader reader) => ((FileStream)reader.BaseStream).SafeFileHandle;

/// <summary>
/// Reads from both standard output and standard error pipes using Windows overlapped IO
/// with wait handles for single-threaded synchronous multiplexing.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
using System.Runtime.InteropServices;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Win32.SafeHandles;

namespace System.Diagnostics
Expand Down Expand Up @@ -52,13 +53,8 @@ public partial class Process
Encoding outputEncoding = _startInfo?.StandardOutputEncoding ?? GetStandardOutputEncoding();
Encoding errorEncoding = _startInfo?.StandardErrorEncoding ?? GetStandardOutputEncoding();

string standardOutput = outputBytesRead > 0
? outputEncoding.GetString(outputBuffer, 0, outputBytesRead)
: string.Empty;

string standardError = errorBytesRead > 0
? errorEncoding.GetString(errorBuffer, 0, errorBytesRead)
: string.Empty;
string standardOutput = outputEncoding.GetString(outputBuffer.AsSpan(0, outputBytesRead));
Comment thread
MihaZupan marked this conversation as resolved.
string standardError = errorEncoding.GetString(errorBuffer.AsSpan(0, errorBytesRead));

return (standardOutput, standardError);
}
Expand Down Expand Up @@ -103,13 +99,8 @@ public partial class Process
{
ReadPipesToBuffers(timeout, ref outputBuffer, ref outputBytesRead, ref errorBuffer, ref errorBytesRead);

byte[] outputResult = outputBytesRead > 0
? outputBuffer.AsSpan(0, outputBytesRead).ToArray()
: Array.Empty<byte>();

byte[] errorResult = errorBytesRead > 0
? errorBuffer.AsSpan(0, errorBytesRead).ToArray()
: Array.Empty<byte>();
byte[] outputResult = outputBuffer.AsSpan(0, outputBytesRead).ToArray();
Comment thread
MihaZupan marked this conversation as resolved.
byte[] errorResult = errorBuffer.AsSpan(0, errorBytesRead).ToArray();

return (outputResult, errorResult);
}
Expand All @@ -120,6 +111,151 @@ public partial class Process
}
}

/// <summary>
/// Asynchronously reads all standard output and standard error of the process as text.
/// </summary>
/// <param name="cancellationToken">
/// A token to cancel the asynchronous operation.
/// </param>
/// <returns>
/// A task that represents the asynchronous read operation. The value of the task contains
/// a tuple with the standard output and standard error text.
/// </returns>
/// <exception cref="InvalidOperationException">
/// Standard output or standard error has not been redirected.
/// -or-
/// A redirected stream has already been used for synchronous or asynchronous reading.
/// </exception>
/// <exception cref="OperationCanceledException">
/// The <paramref name="cancellationToken" /> was canceled.
/// </exception>
/// <exception cref="ObjectDisposedException">
/// The process has been disposed.
/// </exception>
public async Task<(string StandardOutput, string StandardError)> ReadAllTextAsync(CancellationToken cancellationToken = default)
{
(ArraySegment<byte> standardOutput, ArraySegment<byte> standardError) = await ReadAllBytesIntoRentedArraysAsync(cancellationToken).ConfigureAwait(false);

try
{
Encoding outputEncoding = _startInfo?.StandardOutputEncoding ?? GetStandardOutputEncoding();
Encoding errorEncoding = _startInfo?.StandardErrorEncoding ?? GetStandardOutputEncoding();

return (outputEncoding.GetString(standardOutput.AsSpan()), errorEncoding.GetString(standardError.AsSpan()));
}
finally
{
ArrayPool<byte>.Shared.Return(standardOutput.Array!);
ArrayPool<byte>.Shared.Return(standardError.Array!);
}
}

/// <summary>
/// Asynchronously reads all standard output and standard error of the process as byte arrays.
/// </summary>
/// <param name="cancellationToken">
/// A token to cancel the asynchronous operation.
/// </param>
/// <returns>
/// A task that represents the asynchronous read operation. The value of the task contains
/// a tuple with the standard output and standard error bytes.
/// </returns>
/// <exception cref="InvalidOperationException">
/// Standard output or standard error has not been redirected.
/// -or-
/// A redirected stream has already been used for synchronous or asynchronous reading.
/// </exception>
/// <exception cref="OperationCanceledException">
/// The <paramref name="cancellationToken" /> was canceled.
/// </exception>
/// <exception cref="ObjectDisposedException">
/// The process has been disposed.
/// </exception>
public async Task<(byte[] StandardOutput, byte[] StandardError)> ReadAllBytesAsync(CancellationToken cancellationToken = default)
{
(ArraySegment<byte> standardOutput, ArraySegment<byte> standardError) = await ReadAllBytesIntoRentedArraysAsync(cancellationToken).ConfigureAwait(false);

try
{
return (standardOutput.AsSpan().ToArray(), standardError.AsSpan().ToArray());
}
finally
{
ArrayPool<byte>.Shared.Return(standardOutput.Array!);
ArrayPool<byte>.Shared.Return(standardError.Array!);
}
}

private async Task<(ArraySegment<byte> StandardOutput, ArraySegment<byte> StandardError)> ReadAllBytesIntoRentedArraysAsync(CancellationToken cancellationToken)
{
ValidateReadAllState();

Task<ArraySegment<byte>> outputTask = ReadPipeToBufferAsync(_standardOutput!.BaseStream, cancellationToken);
Task<ArraySegment<byte>> errorTask = ReadPipeToBufferAsync(_standardError!.BaseStream, cancellationToken);

Task whenAll = Task.WhenAll(outputTask, errorTask);

try
{
await whenAll.ConfigureAwait(false);
}
catch
{
// It's possible that one of the tasks has failed and the other has succeeded.
// In such case, we need to return the array to the pool.
if (outputTask.IsCompletedSuccessfully)
{
ArrayPool<byte>.Shared.Return(outputTask.Result.Array!);
}

if (errorTask.IsCompletedSuccessfully)
{
ArrayPool<byte>.Shared.Return(errorTask.Result.Array!);
}

// If there is an AggregateException with multiple exceptions, throw it.
if (whenAll.Exception?.InnerExceptions.Count > 1)
{
throw whenAll.Exception;
}

throw;
}

// If we got here, Task.WhenAll has succeeded and both results are available.
return (outputTask.Result, errorTask.Result);
}

/// <summary>
/// Asynchronously reads the entire content of a stream into a pooled buffer.
/// The caller is responsible for returning the buffer to the pool after use.
/// </summary>
private static async Task<ArraySegment<byte>> ReadPipeToBufferAsync(Stream stream, CancellationToken cancellationToken)
{
int bytesRead = 0;
byte[] buffer = ArrayPool<byte>.Shared.Rent(InitialReadAllBufferSize);

try
{
int read;
while ((read = await stream.ReadAsync(buffer.AsMemory(bytesRead), cancellationToken).ConfigureAwait(false)) > 0)
{
bytesRead += read;
if (bytesRead == buffer.Length)
{
RentLargerBuffer(ref buffer, bytesRead);
}
}

return new ArraySegment<byte>(buffer, 0, bytesRead);
}
catch
{
ArrayPool<byte>.Shared.Return(buffer);
throw;
}
}

/// <summary>
/// Validates that the process is not disposed, both stdout and stderr are redirected,
/// and neither stream has been used (mode must be Undefined). Sets both streams to sync mode.
Expand Down Expand Up @@ -165,16 +301,16 @@ private void ReadPipesToBuffers(
? ToTimeoutMilliseconds(timeout.Value)
: Timeout.Infinite;

SafeFileHandle outputHandle = GetSafeFileHandleFromStreamReader(_standardOutput!, out SafeHandle outputOwner);
SafeFileHandle errorHandle = GetSafeFileHandleFromStreamReader(_standardError!, out SafeHandle errorOwner);
var outputHandle = GetSafeHandleFromStreamReader(_standardOutput!);
var errorHandle = GetSafeHandleFromStreamReader(_standardError!);

bool outputRefAdded = false;
bool errorRefAdded = false;

try
{
outputOwner.DangerousAddRef(ref outputRefAdded);
errorOwner.DangerousAddRef(ref errorRefAdded);
outputHandle.DangerousAddRef(ref outputRefAdded);
errorHandle.DangerousAddRef(ref errorRefAdded);

ReadPipes(outputHandle, errorHandle, timeoutMs,
ref outputBuffer, ref outputBytesRead,
Expand All @@ -184,40 +320,16 @@ private void ReadPipesToBuffers(
{
if (outputRefAdded)
{
outputOwner.DangerousRelease();
outputHandle.DangerousRelease();
}

if (errorRefAdded)
{
errorOwner.DangerousRelease();
errorHandle.DangerousRelease();
}
}
}

/// <summary>
/// Obtains the <see cref="SafeFileHandle"/> from the underlying stream of a <see cref="StreamReader"/>.
/// On Unix, the stream is an <see cref="System.IO.Pipes.AnonymousPipeClientStream"/> and the handle is obtained via the pipe handle.
/// On Windows, the stream is a <see cref="FileStream"/> opened for async IO.
/// </summary>
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);
}

throw new UnreachableException();
}

/// <summary>
/// Rents a larger buffer from the array pool and copies the existing data to it.
/// </summary>
Expand Down
Loading
Loading