Skip to content
Merged
Show file tree
Hide file tree
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 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
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 @@ -185,6 +185,8 @@ public void Refresh() { }
[System.Runtime.Versioning.SupportedOSPlatformAttribute("windows")]
public static System.Diagnostics.Process? Start(string fileName, string arguments, string userName, System.Security.SecureString password, string domain) { throw null; }
public override string ToString() { throw null; }
public (string StandardOutput, string StandardError) ReadAllText(System.TimeSpan? timeout = default(System.TimeSpan?)) { throw null; }
public (byte[] StandardOutput, byte[] StandardError) ReadAllBytes(System.TimeSpan? timeout = default(System.TimeSpan?)) { throw null; }
public void WaitForExit() { }
public bool WaitForExit(int milliseconds) { throw null; }
public bool WaitForExit(System.TimeSpan timeout) { throw null; }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
<Compile Include="System\Diagnostics\AsyncStreamReader.cs" />
<Compile Include="System\Diagnostics\DataReceivedEventArgs.cs" />
<Compile Include="System\Diagnostics\Process.cs" />
<Compile Include="System\Diagnostics\Process.Multiplexing.cs" />
<Compile Include="System\Diagnostics\ProcessExitStatus.cs" />
<Compile Include="System\Diagnostics\ProcessInfo.cs" />
<Compile Include="System\Diagnostics\ProcessModule.cs" />
Expand Down Expand Up @@ -224,6 +225,13 @@
<Compile Include="Microsoft\Win32\SafeHandles\SafeProcessHandle.Windows.cs" />
<Compile Include="System\Diagnostics\PerformanceCounterLib.cs" />
<Compile Include="System\Diagnostics\Process.Windows.cs" />
<Compile Include="System\Diagnostics\Process.Multiplexing.Windows.cs" />
<Compile Include="$(CommonPath)Interop\Windows\Kernel32\Interop.ReadFile_SafeHandle_NativeOverlapped.cs"
Link="Common\Interop\Windows\Kernel32\Interop.ReadFile_SafeHandle_NativeOverlapped.cs" />
<Compile Include="$(CommonPath)Interop\Windows\Kernel32\Interop.GetOverlappedResult.cs"
Link="Common\Interop\Windows\Kernel32\Interop.GetOverlappedResult.cs" />
<Compile Include="$(CommonPath)Interop\Windows\Kernel32\Interop.CancelIoEx.cs"
Link="Common\Interop\Windows\Kernel32\Interop.CancelIoEx.cs" />
<Compile Include="System\Diagnostics\ProcessManager.Windows.cs" />
<Compile Include="System\Diagnostics\ProcessStartInfo.Windows.cs" />
<Compile Include="System\Diagnostics\ProcessThread.Windows.cs" />
Expand All @@ -236,6 +244,7 @@
<ItemGroup Condition="'$(TargetPlatformIdentifier)' != '' and '$(TargetPlatformIdentifier)' != 'windows'">
<Compile Include="Microsoft\Win32\SafeHandles\SafeProcessHandle.Unix.cs" />
<Compile Include="System\Diagnostics\Process.Unix.cs" />
<Compile Include="System\Diagnostics\Process.Multiplexing.Unix.cs" />
<Compile Include="System\Diagnostics\ProcessManager.Unix.cs" />
<Compile Include="System\Diagnostics\ProcessThread.Unix.cs" />
<Compile Include="System\Diagnostics\ProcessStartInfo.Unix.cs" />
Expand All @@ -246,6 +255,10 @@
Link="Common\Interop\Unix\Interop.Libraries.cs" />
<Compile Include="$(CommonPath)Interop\Unix\Interop.Errors.cs"
Link="Common\Interop\Unix\Interop.Errors.cs" />
<Compile Include="$(CommonPath)Interop\Unix\Interop.Poll.Structs.cs"
Link="Common\Interop\Unix\Interop.Poll.Structs.cs" />
<Compile Include="$(CommonPath)Interop\Unix\System.Native\Interop.Poll.cs"
Link="Common\Interop\Unix\System.Native\Interop.Poll.cs" />
<Compile Include="$(CommonPath)Interop\Unix\System.Native\Interop.Close.cs"
Link="Common\Interop\Unix\Interop.Close.cs" />
<Compile Include="$(CommonPath)Interop\Unix\Interop.DefaultPathBufferSize.cs"
Expand Down Expand Up @@ -406,6 +419,7 @@
<ProjectReference Include="$(LibrariesProjectRoot)System.Runtime.InteropServices\src\System.Runtime.InteropServices.csproj" />
<ProjectReference Include="$(LibrariesProjectRoot)System.Text.Encoding.Extensions\src\System.Text.Encoding.Extensions.csproj" />
<ProjectReference Include="$(LibrariesProjectRoot)System.Threading\src\System.Threading.csproj" />
<ProjectReference Include="$(LibrariesProjectRoot)System.Threading.Overlapped\src\System.Threading.Overlapped.csproj" />
<ProjectReference Include="$(LibrariesProjectRoot)System.Threading.Thread\src\System.Threading.Thread.csproj" />
<ProjectReference Include="$(LibrariesProjectRoot)System.Threading.ThreadPool\src\System.Threading.ThreadPool.csproj" />
</ItemGroup>
Expand Down
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();
Comment thread
adamsitnik marked this conversation as resolved.
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);
Comment thread
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;
}
Comment thread
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);
Comment thread
adamsitnik marked this conversation as resolved.
Outdated
Comment thread
adamsitnik marked this conversation as resolved.
Outdated
if (bytesRead > 0)
{
currentBytesRead += bytesRead;

if (currentBytesRead == currentBuffer.Length)
{
RentLargerBuffer(ref currentBuffer, currentBytesRead);
}
}
else
{
currentDone = true;
}
}
}
}
}
}
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();
Comment thread
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);
Comment thread
adamsitnik marked this conversation as resolved.

bool outputDone = false;
bool errorDone = false;

WaitHandle[] waitHandles = [outputEvent, errorEvent];
Comment thread
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);

Comment thread
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);
}
Comment thread
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);
}
}
}
Loading
Loading