Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
a6f9c67
Add Process.ReadAllLines synchronous API with platform-specific imple…
Copilot Apr 17, 2026
ab4eca9
Move buffer renting into iterator for proper cleanup, remove unnecess…
Copilot Apr 17, 2026
b0ed0c6
Fix encoding: use Decoder-based char-level line scanning for correct …
Copilot Apr 19, 2026
1665be6
Remove unused using System.Runtime.InteropServices from Process.Multi…
Copilot Apr 19, 2026
711986f
Add BOM-stripping for sync ReadAllLines and encoding tests for UTF-8/…
Copilot Apr 19, 2026
0dcc7f8
Add XML param docs to SkipBomIfPresent
Copilot Apr 19, 2026
7fdd0ea
Optimize DecodeAndAppendChars to compact before growing when startInd…
Copilot Apr 19, 2026
421cb5d
Only compact char buffer when it frees enough space to avoid wasteful…
Copilot Apr 19, 2026
6436c51
Address review feedback: handle \r line endings, make RentLargerBuffe…
Copilot Apr 24, 2026
856cd0e
Extract PollForPipeActivity helper to deduplicate poll logic in Unix
Copilot Apr 24, 2026
9c517c8
Merge remote-tracking branch 'origin/main' into copilot/add-readallli…
Copilot Apr 24, 2026
33b5d6a
Fix build: use ProcessUtils.ToTimeoutMilliseconds, remove redundant E…
Copilot Apr 24, 2026
14a9071
Address feedback: inline PollPipes, remove DangerousAddRef on Windows…
Copilot Apr 24, 2026
9b51e98
Use single triggered variable in PollForPipeActivity
Copilot Apr 24, 2026
658e832
Always compact before growing in DecodeAndAppendChars; fix RentLarger…
Copilot Apr 24, 2026
1f1f41a
Restore stackalloc in ReadPipes; accept Span in PollForPipeActivity/P…
Copilot Apr 24, 2026
62d57a0
Merge branch 'main' into copilot/add-readalllines-to-process
adamsitnik Apr 24, 2026
e4e05db
reduce code duplication:
adamsitnik Apr 25, 2026
7c196cc
Fix DecodeAndAppendChars early return, use single byte buffer on Unix…
Copilot Apr 27, 2026
f583fca
Address review feedback: byte-level preamble/encoding detection, remo…
Copilot Apr 28, 2026
8936fe6
Add missing using System.IO to ProcessStreamingTests.cs to fix build
Copilot Apr 28, 2026
d19dd3e
Fix multi-byte split test to use raw bytes; add mixed line endings test
Copilot Apr 28, 2026
c8bda3e
Fix partial UTF-32 BOM across reads; restore non-blocking mode in fin…
Copilot Apr 28, 2026
5f740c5
Revert "Fix partial UTF-32 BOM across reads; restore non-blocking mod…
adamsitnik Apr 29, 2026
32043d7
keep reading until we have enough bytes to recognize the encoding, em…
adamsitnik Apr 29, 2026
804f781
Apply suggestions from code review
adamsitnik Apr 29, 2026
6340d76
address code review feedback: handle preamble/encoding for very short…
adamsitnik Apr 29, 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 @@ -170,6 +170,7 @@ 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 System.Collections.Generic.IEnumerable<System.Diagnostics.ProcessOutputLine> ReadAllLines(System.TimeSpan? timeout = default(System.TimeSpan?)) { throw null; }
public System.Collections.Generic.IAsyncEnumerable<System.Diagnostics.ProcessOutputLine> ReadAllLinesAsync(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; }
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
// 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.Collections.Generic;
using System.ComponentModel;
using System.IO;
using System.IO.Pipes;
using System.Runtime.InteropServices;
using System.Text;
using Microsoft.Win32.SafeHandles;

namespace System.Diagnostics
Expand All @@ -13,6 +15,248 @@ public partial class Process
{
private static SafePipeHandle GetSafeHandleFromStreamReader(StreamReader reader) => ((AnonymousPipeClientStream)reader.BaseStream).SafePipeHandle;

/// <summary>
/// Reads from both standard output and standard error pipes as lines of text using Unix
/// poll-based multiplexing with non-blocking reads.
/// Buffers are rented from the pool and returned when enumeration completes.
/// </summary>
private IEnumerable<ProcessOutputLine> ReadPipesToLines(
int timeoutMs,
Encoding outputEncoding,
Encoding errorEncoding)
{
SafePipeHandle outputHandle = GetSafeHandleFromStreamReader(_standardOutput!);
SafePipeHandle errorHandle = GetSafeHandleFromStreamReader(_standardError!);

byte[] outputByteBuffer = ArrayPool<byte>.Shared.Rent(InitialReadAllBufferSize);
byte[] errorByteBuffer = ArrayPool<byte>.Shared.Rent(InitialReadAllBufferSize);
char[] outputCharBuffer = ArrayPool<char>.Shared.Rent(InitialReadAllBufferSize);
char[] errorCharBuffer = ArrayPool<char>.Shared.Rent(InitialReadAllBufferSize);
Comment thread
adamsitnik marked this conversation as resolved.
bool outputRefAdded = false, errorRefAdded = false;

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

int outputFd = outputHandle.DangerousGetHandle().ToInt32();
int errorFd = errorHandle.DangerousGetHandle().ToInt32();

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.
Comment thread
adamsitnik marked this conversation as resolved.

// Cannot use stackalloc in an iterator method; use a regular array.
Interop.PollEvent[] pollFds = new Interop.PollEvent[2];
Comment thread
adamsitnik marked this conversation as resolved.

long deadline = timeoutMs >= 0 ? Environment.TickCount64 + timeoutMs : long.MaxValue;

Decoder outputDecoder = outputEncoding.GetDecoder();
Decoder errorDecoder = errorEncoding.GetDecoder();
int outputCharStart = 0, outputCharEnd = 0;
int errorCharStart = 0, errorCharEnd = 0;
int unconsumedOutputBytesCount = 0, unconsumedErrorBytesCount = 0;
bool outputDone = false, errorDone = false;
bool outputPreambleChecked = false, errorPreambleChecked = false;

List<ProcessOutputLine> lines = new();

while (!outputDone || !errorDone)
{
int numFds = PollForPipeActivity(pollFds, errorFd, outputFd, errorDone, outputDone, deadline, timeoutMs, out int errorIndex, out int outputIndex);

// Process error pipe first (lower index) when both have data available.
for (int i = 0; i < numFds; i++)
{
if (pollFds[i].TriggeredEvents == Interop.PollEvents.POLLNONE)
{
continue;
}

bool isError = i == errorIndex;
SafePipeHandle currentHandle = isError ? errorHandle : outputHandle;

// Use explicit branching to avoid ref locals across yield points.
if (isError)
{
HandlePipeLineRead(currentHandle, ref errorDecoder, ref errorEncoding,
errorByteBuffer, ref unconsumedErrorBytesCount,
ref errorCharBuffer, ref errorCharStart, ref errorCharEnd,
ref errorPreambleChecked, ref errorDone, isError, lines);
}
else
{
HandlePipeLineRead(currentHandle, ref outputDecoder, ref outputEncoding,
outputByteBuffer, ref unconsumedOutputBytesCount,
ref outputCharBuffer, ref outputCharStart, ref outputCharEnd,
ref outputPreambleChecked, ref outputDone, isError, lines);
}
}
Comment thread
adamsitnik marked this conversation as resolved.

// Yield parsed lines outside of any ref-local scope.
foreach (ProcessOutputLine line in lines)
{
yield return line;
}

lines.Clear();
}
}
finally
{
if (outputRefAdded)
{
outputHandle.DangerousRelease();
}

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

Comment thread
adamsitnik marked this conversation as resolved.
ArrayPool<byte>.Shared.Return(outputByteBuffer);
ArrayPool<byte>.Shared.Return(errorByteBuffer);
ArrayPool<char>.Shared.Return(outputCharBuffer);
ArrayPool<char>.Shared.Return(errorCharBuffer);
}
}

/// <summary>
/// Populates the poll fd array with the active pipe file descriptors.
/// Error is added first so it gets serviced first when both have data.
/// Returns the number of active file descriptors.
/// </summary>
private static int PreparePollFds(
Span<Interop.PollEvent> pollFds,
int errorFd, int outputFd,
bool errorDone, bool outputDone,
out int errorIndex, out int outputIndex)
{
int numFds = 0;
errorIndex = -1;
outputIndex = -1;

if (!errorDone)
{
errorIndex = numFds;
pollFds[numFds].FileDescriptor = errorFd;
pollFds[numFds].Events = Interop.PollEvents.POLLIN;
pollFds[numFds].TriggeredEvents = Interop.PollEvents.POLLNONE;
numFds++;
}

if (!outputDone)
{
outputIndex = numFds;
pollFds[numFds].FileDescriptor = outputFd;
pollFds[numFds].Events = Interop.PollEvents.POLLIN;
pollFds[numFds].TriggeredEvents = Interop.PollEvents.POLLNONE;
numFds++;
}

return numFds;
}

/// <summary>
/// Prepares the poll fd array, checks the remaining timeout, calls poll(2), and handles
/// errors. Returns the number of polled fds, or 0 if poll was interrupted (EINTR) and
/// the caller should retry.
/// </summary>
private static int PollForPipeActivity(
Span<Interop.PollEvent> pollFds,
int errorFd, int outputFd,
bool errorDone, bool outputDone,
long deadline, int timeoutMs,
out int errorIndex, out int outputIndex)
{
int numFds = PreparePollFds(pollFds, errorFd, outputFd, errorDone, outputDone, out errorIndex, out outputIndex);

if (!TryGetRemainingTimeout(deadline, timeoutMs, out int pollTimeout))
{
throw new TimeoutException();
}

uint triggered = 0;
Interop.Error pollError;
unsafe
{
fixed (Interop.PollEvent* pPollFds = pollFds)
{
pollError = Interop.Sys.Poll(pPollFds, (uint)numFds, pollTimeout, &triggered);
}
}

if (pollError != Interop.Error.SUCCESS)
{
if (pollError == Interop.Error.EINTR)
{
return 0;
}

throw new Win32Exception(Interop.Sys.ConvertErrorPalToPlatform(pollError));
}

if (triggered == 0)
{
throw new TimeoutException();
}

return numFds;
}

/// <summary>
/// Handles a poll notification for a single pipe: reads bytes, decodes to chars,
/// strips BOM on first decode, parses lines, compacts the char buffer, and sets
/// <paramref name="done"/> to <see langword="true"/> on EOF.
/// </summary>
private static void HandlePipeLineRead(
SafePipeHandle handle,
ref Decoder decoder,
ref Encoding encoding,
byte[] byteBuffer,
ref int unconsumedBytesCount,
ref char[] charBuffer,
ref int charStart,
ref int charEnd,
ref bool preambleChecked,
ref bool done,
bool standardError,
List<ProcessOutputLine> lines)
{
int bytesRead = ReadNonBlocking(handle, byteBuffer, offset: unconsumedBytesCount);
if (bytesRead > 0)
{
ReadOnlySpan<byte> bytes = byteBuffer.AsSpan(0, unconsumedBytesCount + bytesRead);

if (!preambleChecked)
{
if (bytes.Length >= MaxEncodingBytesLength)
{
bytes = bytes.Slice(SkipPreambleOrDetectEncoding(bytes, ref encoding, ref decoder));
preambleChecked = true;
unconsumedBytesCount = 0;
}
else
{
unconsumedBytesCount += bytesRead;
}
}

if (preambleChecked)
{
DecodeBytesAndParseLines(decoder, bytes, ref charBuffer, ref charStart, ref charEnd, standardError, lines);
}
}
else if (bytesRead == 0)
{
done = FlushDecoderAndEmitRemainingChars(preambleChecked, encoding, decoder, byteBuffer.AsSpan(0, unconsumedBytesCount),
ref charBuffer, ref charStart, ref charEnd, standardError, lines);
}
// bytesRead < 0 means EAGAIN — nothing available yet, let poll retry.
}

/// <summary>
/// Reads from both standard output and standard error pipes using Unix poll-based multiplexing
/// with non-blocking reads.
Expand Down Expand Up @@ -43,59 +287,7 @@ private static void ReadPipes(
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);
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();
}
}
}
int numFds = PollForPipeActivity(pollFds, errorFd, outputFd, errorDone, outputDone, deadline, timeoutMs, out int errorIndex, out int outputIndex);

for (int i = 0; i < numFds; i++)
{
Expand Down
Loading
Loading