diff --git a/src/libraries/System.Diagnostics.Process/ref/System.Diagnostics.Process.cs b/src/libraries/System.Diagnostics.Process/ref/System.Diagnostics.Process.cs
index 7257744b6de56c..0a6e75ee3cba55 100644
--- a/src/libraries/System.Diagnostics.Process/ref/System.Diagnostics.Process.cs
+++ b/src/libraries/System.Diagnostics.Process/ref/System.Diagnostics.Process.cs
@@ -157,6 +157,8 @@ public void Kill() { }
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 (string StandardOutput, string StandardError) ReadAllText(System.TimeSpan? timeout = default(System.TimeSpan?)) { throw null; }
public void Refresh() { }
[System.Runtime.Versioning.UnsupportedOSPlatformAttribute("ios")]
[System.Runtime.Versioning.UnsupportedOSPlatformAttribute("tvos")]
diff --git a/src/libraries/System.Diagnostics.Process/src/System.Diagnostics.Process.csproj b/src/libraries/System.Diagnostics.Process/src/System.Diagnostics.Process.csproj
index 2c1c9e0be2eb23..6db2a72aeb08fa 100644
--- a/src/libraries/System.Diagnostics.Process/src/System.Diagnostics.Process.csproj
+++ b/src/libraries/System.Diagnostics.Process/src/System.Diagnostics.Process.csproj
@@ -20,6 +20,7 @@
+
@@ -224,6 +225,13 @@
+
+
+
+
@@ -236,6 +244,7 @@
+
@@ -246,6 +255,14 @@
Link="Common\Interop\Unix\Interop.Libraries.cs" />
+
+
+
+
+
diff --git a/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/Process.Multiplexing.Unix.cs b/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/Process.Multiplexing.Unix.cs
new file mode 100644
index 00000000000000..f23ad6631fa89e
--- /dev/null
+++ b/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/Process.Multiplexing.Unix.cs
@@ -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
+ {
+ ///
+ /// Reads from both standard output and standard error pipes using Unix poll-based multiplexing
+ /// with non-blocking reads.
+ ///
+ 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 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);
+ 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;
+ }
+
+ 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.
+ }
+ }
+ }
+
+ ///
+ /// 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).
+ ///
+ 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;
+ }
+ }
+ }
+}
diff --git a/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/Process.Multiplexing.Windows.cs b/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/Process.Multiplexing.Windows.cs
new file mode 100644
index 00000000000000..6d28e278a28bac
--- /dev/null
+++ b/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/Process.Multiplexing.Windows.cs
@@ -0,0 +1,257 @@
+// 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 System.Threading;
+using Microsoft.Win32.SafeHandles;
+
+namespace System.Diagnostics
+{
+ public partial class Process
+ {
+ ///
+ /// Reads from both standard output and standard error pipes using Windows overlapped IO
+ /// with wait handles for single-threaded synchronous multiplexing.
+ ///
+ private static unsafe void ReadPipes(
+ SafeFileHandle outputHandle,
+ SafeFileHandle errorHandle,
+ int timeoutMs,
+ ref byte[] outputBuffer,
+ ref int outputBytesRead,
+ ref byte[] errorBuffer,
+ ref int errorBytesRead)
+ {
+ PinnedGCHandle outputPin = default, errorPin = default;
+ NativeOverlapped* outputOverlapped = null, errorOverlapped = null;
+ EventWaitHandle? outputEvent = null, errorEvent = null;
+
+ try
+ {
+ outputPin = new PinnedGCHandle(outputBuffer);
+ errorPin = new PinnedGCHandle(errorBuffer);
+
+ outputEvent = new EventWaitHandle(initialState: false, EventResetMode.ManualReset);
+ errorEvent = new EventWaitHandle(initialState: false, EventResetMode.ManualReset);
+
+ outputOverlapped = AllocateOverlapped(outputEvent);
+ errorOverlapped = AllocateOverlapped(errorEvent);
+
+ // Error output gets index 0 so WaitAny services it first when both are signaled.
+ WaitHandle[] waitHandles = [errorEvent, outputEvent];
+
+ // Issue initial reads.
+ bool outputDone = !QueueRead(outputHandle, outputPin.GetAddressOfArrayData(), outputBuffer.Length, outputOverlapped, outputEvent);
+ bool errorDone = !QueueRead(errorHandle, errorPin.GetAddressOfArrayData(), errorBuffer.Length, errorOverlapped, errorEvent);
+
+ long deadline = timeoutMs >= 0
+ ? Environment.TickCount64 + timeoutMs
+ : long.MaxValue;
+
+ while (!outputDone || !errorDone)
+ {
+
+ int waitResult = TryGetRemainingTimeout(deadline, timeoutMs, out int remainingMilliseconds)
+ ? WaitHandle.WaitAny(waitHandles, remainingMilliseconds)
+ : WaitHandle.WaitTimeout;
+
+ if (waitResult == WaitHandle.WaitTimeout)
+ {
+ CancelPendingIOIfNeeded(outputHandle, outputDone, outputOverlapped);
+ CancelPendingIOIfNeeded(errorHandle, errorDone, errorOverlapped);
+
+ throw new TimeoutException();
+ }
+
+ bool isError = waitResult == 0;
+ 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 PinnedGCHandle currentPin = ref (isError ? ref errorPin : ref outputPin);
+
+ RentLargerBuffer(ref currentBuffer, totalBytesRead);
+
+ currentPin.Target = currentBuffer;
+ }
+
+ // Reset the event and overlapped for next read.
+ ResetOverlapped(currentEvent, currentOverlapped);
+
+ byte* pinPointer = isError ? errorPin.GetAddressOfArrayData() : outputPin.GetAddressOfArrayData();
+ if (!QueueRead(currentHandle, pinPointer + totalBytesRead,
+ currentBuffer.Length - totalBytesRead, currentOverlapped, currentEvent))
+ {
+ if (isError)
+ {
+ errorDone = true;
+ }
+ else
+ {
+ outputDone = true;
+ }
+
+ // Ensure WaitAny won't trigger on this stale handle.
+ currentEvent.Reset();
+ }
+ }
+ 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 = SetLowOrderBit(waitHandle);
+
+ 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 = SetLowOrderBit(waitHandle);
+ }
+
+ 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: // logically success with 0 bytes read (read at end of file)
+ case Interop.Errors.ERROR_BROKEN_PIPE: // For pipes, ERROR_BROKEN_PIPE is the normal end of the pipe.
+ case Interop.Errors.ERROR_PIPE_NOT_CONNECTED: // Named pipe server has disconnected, return 0 to match NamedPipeClientStream behaviour
+ return 0; // EOF!
+ default:
+ throw new Win32Exception(errorCode);
+ }
+ }
+
+ return bytesRead;
+ }
+
+ ///
+ /// Cancels a pending overlapped I/O and waits for completion before returning.
+ ///
+ private static unsafe void CancelPendingIOIfNeeded(SafeFileHandle handle, bool done, NativeOverlapped* overlapped)
+ {
+ if (done)
+ {
+ return;
+ }
+
+ // CancelIoEx marks matching outstanding I/O requests for cancellation.
+ // It does not wait for all canceled operations to complete.
+ // When CancelIoEx returns true, it means that the cancel request was successfully queued.
+ if (!Interop.Kernel32.CancelIoEx(handle, overlapped))
+ {
+ // Failure has two common meanings:
+ // ERROR_NOT_FOUND (extremely common). It means:
+ // - The I/O already completed.
+ // - Or it never existed.
+ // - Or it completed between your decision and the call.
+ // Other errors indicate real failures (invalid handle, driver limitation, etc.).
+ int errorCode = Marshal.GetLastPInvokeError();
+ Debug.Assert(errorCode == Interop.Errors.ERROR_NOT_FOUND, $"CancelIoEx failed with {errorCode}.");
+ }
+
+ // We must observe completion before freeing the OVERLAPPED in all the above scenarios.
+ // Use bWait: true to ensure the I/O operation completes before we free the OVERLAPPED structure.
+ // Per MSDN: "Do not reuse or free the OVERLAPPED structure until GetOverlappedResult returns."
+ int bytesRead = 0;
+ if (!Interop.Kernel32.GetOverlappedResult(handle, overlapped, ref bytesRead, bWait: true))
+ {
+ int errorCode = Marshal.GetLastPInvokeError();
+ Debug.Assert(errorCode is Interop.Errors.ERROR_OPERATION_ABORTED or Interop.Errors.ERROR_BROKEN_PIPE, $"GetOverlappedResult failed with {errorCode}.");
+ }
+ }
+
+ ///
+ /// Returns the event handle with the low-order bit set.
+ /// Per https://learn.microsoft.com/windows/win32/api/ioapiset/nf-ioapiset-getqueuedcompletionstatus,
+ /// setting the low-order bit of hEvent in the OVERLAPPED structure prevents the I/O completion
+ /// from being queued to a completion port bound to the same file object. The kernel masks off
+ /// the bit when signaling, so the event still works normally.
+ ///
+ private static nint SetLowOrderBit(EventWaitHandle waitHandle)
+ => waitHandle.SafeWaitHandle.DangerousGetHandle() | 1;
+
+ private static unsafe bool QueueRead(
+ SafeFileHandle handle,
+ byte* buffer,
+ int bufferLength,
+ NativeOverlapped* overlapped,
+ EventWaitHandle waitHandle)
+ {
+ if (Interop.Kernel32.ReadFile(handle, buffer, bufferLength, IntPtr.Zero, overlapped) != 0)
+ {
+ waitHandle.Set();
+ return true;
+ }
+
+ int error = Marshal.GetLastPInvokeError();
+ if (error == Interop.Errors.ERROR_IO_PENDING)
+ {
+ return true;
+ }
+
+ if (error == Interop.Errors.ERROR_BROKEN_PIPE || error == Interop.Errors.ERROR_HANDLE_EOF)
+ {
+ return false;
+ }
+
+ throw new Win32Exception(error);
+ }
+ }
+}
diff --git a/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/Process.Multiplexing.cs b/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/Process.Multiplexing.cs
new file mode 100644
index 00000000000000..8da1afc4b1a6ad
--- /dev/null
+++ b/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/Process.Multiplexing.cs
@@ -0,0 +1,254 @@
+// 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.IO;
+using System.Runtime.InteropServices;
+using System.Text;
+using System.Threading;
+using Microsoft.Win32.SafeHandles;
+
+namespace System.Diagnostics
+{
+ public partial class Process
+ {
+ /// Initial buffer size for reading process output.
+ private const int InitialReadAllBufferSize = 4096;
+
+ ///
+ /// Reads all standard output and standard error of the process as text.
+ ///
+ ///
+ /// The maximum amount of time to wait for the streams to be fully read.
+ /// When , waits indefinitely.
+ ///
+ ///
+ /// A tuple containing the standard output and standard error text.
+ ///
+ ///
+ /// Standard output or standard error has not been redirected.
+ /// -or-
+ /// A redirected stream has already been used for synchronous or asynchronous reading.
+ ///
+ ///
+ /// The operation did not complete within the specified .
+ ///
+ ///
+ /// The process has been disposed.
+ ///
+ public (string StandardOutput, string StandardError) ReadAllText(TimeSpan? timeout = default)
+ {
+ ValidateReadAllState();
+
+ byte[] outputBuffer = ArrayPool.Shared.Rent(InitialReadAllBufferSize);
+ byte[] errorBuffer = ArrayPool.Shared.Rent(InitialReadAllBufferSize);
+ int outputBytesRead = 0;
+ int errorBytesRead = 0;
+
+ try
+ {
+ ReadPipesToBuffers(timeout, ref outputBuffer, ref outputBytesRead, ref errorBuffer, ref errorBytesRead);
+
+ 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;
+
+ return (standardOutput, standardError);
+ }
+ finally
+ {
+ ArrayPool.Shared.Return(outputBuffer);
+ ArrayPool.Shared.Return(errorBuffer);
+ }
+ }
+
+ ///
+ /// Reads all standard output and standard error of the process as byte arrays.
+ ///
+ ///
+ /// The maximum amount of time to wait for the streams to be fully read.
+ /// When , waits indefinitely.
+ ///
+ ///
+ /// A tuple containing the standard output and standard error bytes.
+ ///
+ ///
+ /// Standard output or standard error has not been redirected.
+ /// -or-
+ /// A redirected stream has already been used for synchronous or asynchronous reading.
+ ///
+ ///
+ /// The operation did not complete within the specified .
+ ///
+ ///
+ /// The process has been disposed.
+ ///
+ public (byte[] StandardOutput, byte[] StandardError) ReadAllBytes(TimeSpan? timeout = default)
+ {
+ ValidateReadAllState();
+
+ byte[] outputBuffer = ArrayPool.Shared.Rent(InitialReadAllBufferSize);
+ byte[] errorBuffer = ArrayPool.Shared.Rent(InitialReadAllBufferSize);
+ int outputBytesRead = 0;
+ int errorBytesRead = 0;
+
+ try
+ {
+ ReadPipesToBuffers(timeout, ref outputBuffer, ref outputBytesRead, ref errorBuffer, ref errorBytesRead);
+
+ byte[] outputResult = outputBytesRead > 0
+ ? outputBuffer.AsSpan(0, outputBytesRead).ToArray()
+ : Array.Empty();
+
+ byte[] errorResult = errorBytesRead > 0
+ ? errorBuffer.AsSpan(0, errorBytesRead).ToArray()
+ : Array.Empty();
+
+ return (outputResult, errorResult);
+ }
+ finally
+ {
+ ArrayPool.Shared.Return(outputBuffer);
+ ArrayPool.Shared.Return(errorBuffer);
+ }
+ }
+
+ ///
+ /// 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.
+ ///
+ private void ValidateReadAllState()
+ {
+ CheckDisposed();
+
+ if (_standardOutput is null)
+ {
+ throw new InvalidOperationException(SR.CantGetStandardOut);
+ }
+ else if (_standardError is null)
+ {
+ throw new InvalidOperationException(SR.CantGetStandardError);
+ }
+ else if (_outputStreamReadMode != StreamReadMode.Undefined)
+ {
+ throw new InvalidOperationException(SR.CantMixSyncAsyncOperation);
+ }
+ else if (_errorStreamReadMode != StreamReadMode.Undefined)
+ {
+ throw new InvalidOperationException(SR.CantMixSyncAsyncOperation);
+ }
+
+ _outputStreamReadMode = StreamReadMode.SyncMode;
+ _errorStreamReadMode = StreamReadMode.SyncMode;
+ }
+
+ ///
+ /// Obtains handles and reads both stdout and stderr pipes into the provided buffers.
+ /// The caller is responsible for calling before renting buffers,
+ /// and for renting and returning the buffers.
+ ///
+ private void ReadPipesToBuffers(
+ TimeSpan? timeout,
+ ref byte[] outputBuffer,
+ ref int outputBytesRead,
+ ref byte[] errorBuffer,
+ ref int errorBytesRead)
+ {
+ int timeoutMs = timeout.HasValue
+ ? ToTimeoutMilliseconds(timeout.Value)
+ : Timeout.Infinite;
+
+ SafeFileHandle outputHandle = GetSafeFileHandleFromStreamReader(_standardOutput!, out SafeHandle outputOwner);
+ SafeFileHandle errorHandle = GetSafeFileHandleFromStreamReader(_standardError!, out SafeHandle errorOwner);
+
+ bool outputRefAdded = false;
+ bool errorRefAdded = false;
+
+ try
+ {
+ outputOwner.DangerousAddRef(ref outputRefAdded);
+ errorOwner.DangerousAddRef(ref errorRefAdded);
+
+ ReadPipes(outputHandle, errorHandle, timeoutMs,
+ ref outputBuffer, ref outputBytesRead,
+ ref errorBuffer, ref errorBytesRead);
+ }
+ finally
+ {
+ if (outputRefAdded)
+ {
+ outputOwner.DangerousRelease();
+ }
+
+ if (errorRefAdded)
+ {
+ errorOwner.DangerousRelease();
+ }
+ }
+ }
+
+ ///
+ /// Obtains the from the underlying stream of a .
+ /// On Unix, the stream is an and the handle is obtained via the pipe handle.
+ /// On Windows, the stream is a opened for async IO.
+ ///
+ 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();
+ }
+
+ ///
+ /// Rents a larger buffer from the array pool and copies the existing data to it.
+ ///
+ private static void RentLargerBuffer(ref byte[] buffer, int bytesRead)
+ {
+ int newSize = (int)Math.Min((long)buffer.Length * 2, Array.MaxLength);
+ newSize = Math.Max(buffer.Length + 1, newSize);
+ byte[] newBuffer = ArrayPool.Shared.Rent(newSize);
+ Buffer.BlockCopy(buffer, 0, newBuffer, 0, bytesRead);
+ byte[] oldBuffer = buffer;
+ buffer = newBuffer;
+ ArrayPool.Shared.Return(oldBuffer);
+ }
+
+ private static bool TryGetRemainingTimeout(long deadline, int originalTimeout, out int remainingTimeoutMs)
+ {
+ if (originalTimeout < 0)
+ {
+ remainingTimeoutMs = Timeout.Infinite;
+ return true;
+ }
+
+ long remaining = deadline - Environment.TickCount64;
+ if (remaining <= 0)
+ {
+ remainingTimeoutMs = 0;
+ return false;
+ }
+
+ remainingTimeoutMs = (int)Math.Min(remaining, int.MaxValue);
+ return true;
+ }
+ }
+}
diff --git a/src/libraries/System.Diagnostics.Process/tests/ProcessMultiplexingTests.cs b/src/libraries/System.Diagnostics.Process/tests/ProcessMultiplexingTests.cs
new file mode 100644
index 00000000000000..906cbbe4c47202
--- /dev/null
+++ b/src/libraries/System.Diagnostics.Process/tests/ProcessMultiplexingTests.cs
@@ -0,0 +1,360 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System.IO;
+using System.Text;
+using Microsoft.DotNet.RemoteExecutor;
+using Microsoft.Win32.SafeHandles;
+using Xunit;
+
+namespace System.Diagnostics.Tests
+{
+ public class ProcessMultiplexingTests : ProcessTestBase
+ {
+ private const string DontPrintAnything = "DO_NOT_PRINT";
+
+ [ConditionalTheory(typeof(RemoteExecutor), nameof(RemoteExecutor.IsSupported))]
+ [InlineData(true)]
+ [InlineData(false)]
+ public void ReadAll_ThrowsAfterDispose(bool bytes)
+ {
+ Process process = CreateProcess(RemotelyInvokable.Dummy);
+ process.Start();
+ Assert.True(process.WaitForExit(WaitInMS));
+
+ process.Dispose();
+
+ if (bytes)
+ {
+ Assert.Throws(() => process.ReadAllBytes());
+ }
+ else
+ {
+ Assert.Throws(() => process.ReadAllText());
+ }
+ }
+
+ [ConditionalTheory(typeof(RemoteExecutor), nameof(RemoteExecutor.IsSupported))]
+ [InlineData(true)]
+ [InlineData(false)]
+ public void ReadAll_ThrowsWhenNoStreamsRedirected(bool bytes)
+ {
+ Process process = CreateProcess(RemotelyInvokable.Dummy);
+ process.Start();
+
+ if (bytes)
+ {
+ Assert.Throws(() => process.ReadAllBytes());
+ }
+ else
+ {
+ Assert.Throws(() => process.ReadAllText());
+ }
+
+ Assert.True(process.WaitForExit(WaitInMS));
+ }
+
+ [ConditionalTheory(typeof(RemoteExecutor), nameof(RemoteExecutor.IsSupported))]
+ [InlineData(true, true)]
+ [InlineData(true, false)]
+ [InlineData(false, true)]
+ [InlineData(false, false)]
+ public void ReadAll_ThrowsWhenOnlyOutputOrErrorIsRedirected(bool bytes, bool standardOutput)
+ {
+ Process process = CreateProcess(RemotelyInvokable.Dummy);
+ process.StartInfo.RedirectStandardOutput = standardOutput;
+ process.StartInfo.RedirectStandardError = !standardOutput;
+ process.Start();
+
+ if (bytes)
+ {
+ Assert.Throws(() => process.ReadAllBytes());
+ }
+ else
+ {
+ Assert.Throws(() => process.ReadAllText());
+ }
+
+ Assert.True(process.WaitForExit(WaitInMS));
+ }
+
+ [ConditionalTheory(typeof(RemoteExecutor), nameof(RemoteExecutor.IsSupported))]
+ [InlineData(true, true)]
+ [InlineData(true, false)]
+ [InlineData(false, true)]
+ [InlineData(false, false)]
+ public void ReadAll_ThrowsWhenOutputOrErrorIsInSyncMode(bool bytes, bool standardOutput)
+ {
+ Process process = CreateProcess(RemotelyInvokable.Dummy);
+ process.StartInfo.RedirectStandardOutput = true;
+ process.StartInfo.RedirectStandardError = true;
+ process.Start();
+
+ // Access the StreamReader property to set the stream to sync mode
+ _ = standardOutput ? process.StandardOutput : process.StandardError;
+
+ if (bytes)
+ {
+ Assert.Throws(() => process.ReadAllBytes());
+ }
+ else
+ {
+ Assert.Throws(() => process.ReadAllText());
+ }
+
+ Assert.True(process.WaitForExit(WaitInMS));
+ }
+
+ [ConditionalTheory(typeof(RemoteExecutor), nameof(RemoteExecutor.IsSupported))]
+ [InlineData(true, true)]
+ [InlineData(true, false)]
+ [InlineData(false, true)]
+ [InlineData(false, false)]
+ public void ReadAll_ThrowsWhenOutputOrErrorIsInAsyncMode(bool bytes, bool standardOutput)
+ {
+ Process process = CreateProcess(RemotelyInvokable.StreamBody);
+ process.StartInfo.RedirectStandardOutput = true;
+ process.StartInfo.RedirectStandardError = true;
+ process.Start();
+
+ if (standardOutput)
+ {
+ process.BeginOutputReadLine();
+ }
+ else
+ {
+ process.BeginErrorReadLine();
+ }
+
+ if (bytes)
+ {
+ Assert.Throws(() => process.ReadAllBytes());
+ }
+ else
+ {
+ Assert.Throws(() => process.ReadAllText());
+ }
+
+ if (standardOutput)
+ {
+ process.CancelOutputRead();
+ }
+ else
+ {
+ process.CancelErrorRead();
+ }
+
+ Assert.True(process.WaitForExit(WaitInMS));
+ }
+
+ [ConditionalTheory(typeof(RemoteExecutor), nameof(RemoteExecutor.IsSupported))]
+ [InlineData(true)]
+ [InlineData(false)]
+ public void ReadAll_ThrowsTimeoutExceptionOnTimeout(bool bytes)
+ {
+ Process process = CreateProcess(RemotelyInvokable.ReadLine);
+ process.StartInfo.RedirectStandardOutput = true;
+ process.StartInfo.RedirectStandardError = true;
+ process.StartInfo.RedirectStandardInput = true;
+ process.Start();
+
+ try
+ {
+ if (bytes)
+ {
+ Assert.Throws(() => process.ReadAllBytes(TimeSpan.FromMilliseconds(100)));
+ }
+ else
+ {
+ Assert.Throws(() => process.ReadAllText(TimeSpan.FromMilliseconds(100)));
+ }
+ }
+ finally
+ {
+ process.Kill();
+ }
+
+ Assert.True(process.WaitForExit(WaitInMS));
+ }
+
+ [ConditionalTheory(typeof(RemoteExecutor), nameof(RemoteExecutor.IsSupported))]
+ [InlineData("hello", "world", true)]
+ [InlineData("hello", "world", false)]
+ [InlineData("just output", "", true)]
+ [InlineData("just output", "", false)]
+ [InlineData("", "just error", true)]
+ [InlineData("", "just error", false)]
+ [InlineData("", "", true)]
+ [InlineData("", "", false)]
+ public void ReadAll_ReadsBothOutputAndError(string standardOutput, string standardError, bool bytes)
+ {
+ using Process process = StartPrintingProcess(
+ string.IsNullOrEmpty(standardOutput) ? DontPrintAnything : standardOutput,
+ string.IsNullOrEmpty(standardError) ? DontPrintAnything : standardError);
+
+ if (bytes)
+ {
+ (byte[] capturedOutput, byte[] capturedError) = process.ReadAllBytes();
+
+ Assert.Equal(Encoding.Default.GetBytes(standardOutput), capturedOutput);
+ Assert.Equal(Encoding.Default.GetBytes(standardError), capturedError);
+ }
+ else
+ {
+ (string capturedOutput, string capturedError) = process.ReadAllText();
+
+ Assert.Equal(standardOutput, capturedOutput);
+ Assert.Equal(standardError, capturedError);
+ }
+
+ Assert.True(process.WaitForExit(WaitInMS));
+ }
+
+ [ConditionalFact(typeof(RemoteExecutor), nameof(RemoteExecutor.IsSupported))]
+ public void ReadAllText_ReadsInterleavedOutput()
+ {
+ const int iterations = 100;
+ using Process process = CreateProcess(() =>
+ {
+ for (int i = 0; i < iterations; i++)
+ {
+ Console.Out.Write($"out{i} ");
+ Console.Out.Flush();
+ Console.Error.Write($"err{i} ");
+ Console.Error.Flush();
+ }
+
+ return RemoteExecutor.SuccessExitCode;
+ });
+
+ process.StartInfo.RedirectStandardOutput = true;
+ process.StartInfo.RedirectStandardError = true;
+ process.Start();
+
+ (string standardOutput, string standardError) = process.ReadAllText();
+
+ StringBuilder expectedOutput = new();
+ StringBuilder expectedError = new();
+ for (int i = 0; i < iterations; i++)
+ {
+ expectedOutput.Append($"out{i} ");
+ expectedError.Append($"err{i} ");
+ }
+
+ Assert.Equal(expectedOutput.ToString(), standardOutput);
+ Assert.Equal(expectedError.ToString(), standardError);
+
+ Assert.True(process.WaitForExit(WaitInMS));
+ }
+
+ [ConditionalFact(typeof(RemoteExecutor), nameof(RemoteExecutor.IsSupported))]
+ public void ReadAllBytes_ReadsBinaryDataWithNullBytes()
+ {
+ string testFilePath = GetTestFilePath();
+ byte[] binaryData = new byte[1024];
+ Random.Shared.NextBytes(binaryData);
+ // Ensure there are null bytes throughout the data.
+ for (int i = 0; i < binaryData.Length; i += 10)
+ {
+ binaryData[i] = 0;
+ }
+
+ File.WriteAllBytes(testFilePath, binaryData);
+
+ using Process process = CreateProcess(static path =>
+ {
+ using FileStream source = File.OpenRead(path);
+ source.CopyTo(Console.OpenStandardOutput());
+
+ return RemoteExecutor.SuccessExitCode;
+ }, testFilePath);
+
+ process.StartInfo.RedirectStandardOutput = true;
+ process.StartInfo.RedirectStandardError = true;
+ process.Start();
+
+ (byte[] standardOutput, byte[] standardError) = process.ReadAllBytes();
+
+ Assert.Equal(binaryData, standardOutput);
+ Assert.Empty(standardError);
+ Assert.True(process.WaitForExit(WaitInMS));
+ }
+
+ [ConditionalFact(typeof(RemoteExecutor), nameof(RemoteExecutor.IsSupported))]
+ public void ReadAllText_ReadsLargeOutput()
+ {
+ string testFilePath = GetTestFilePath();
+ string largeText = new string('A', 100_000);
+ File.WriteAllText(testFilePath, largeText);
+
+ using Process process = CreateProcess(static path =>
+ {
+ using FileStream source = File.OpenRead(path);
+ source.CopyTo(Console.OpenStandardOutput());
+
+ return RemoteExecutor.SuccessExitCode;
+ }, testFilePath);
+
+ process.StartInfo.RedirectStandardOutput = true;
+ process.StartInfo.RedirectStandardError = true;
+ process.Start();
+
+ (string standardOutput, string standardError) = process.ReadAllText();
+
+ Assert.Equal(largeText, standardOutput);
+ Assert.Equal(string.Empty, standardError);
+ Assert.True(process.WaitForExit(WaitInMS));
+ }
+
+ [ConditionalFact(typeof(RemoteExecutor), nameof(RemoteExecutor.IsSupported))]
+ public void ReadAllBytes_ReadsLargeOutput()
+ {
+ string testFilePath = GetTestFilePath();
+ byte[] largeByteArray = new byte[100_000];
+ Random.Shared.NextBytes(largeByteArray);
+ File.WriteAllBytes(testFilePath, largeByteArray);
+
+ using Process process = CreateProcess(static path =>
+ {
+ using FileStream source = File.OpenRead(path);
+ source.CopyTo(Console.OpenStandardOutput());
+
+ return RemoteExecutor.SuccessExitCode;
+ }, testFilePath);
+
+ process.StartInfo.RedirectStandardOutput = true;
+ process.StartInfo.RedirectStandardError = true;
+ process.Start();
+
+ (byte[] standardOutput, byte[] standardError) = process.ReadAllBytes();
+
+ Assert.Equal(largeByteArray, standardOutput);
+ Assert.Empty(standardError);
+ Assert.True(process.WaitForExit(WaitInMS));
+ }
+
+ private Process StartPrintingProcess(string stdOutText, string stdErrText)
+ {
+ Process process = CreateProcess((stdOut, stdErr) =>
+ {
+ if (stdOut != DontPrintAnything)
+ {
+ Console.Out.Write(stdOut);
+ }
+
+ if (stdErr != DontPrintAnything)
+ {
+ Console.Error.Write(stdErr);
+ }
+
+ return RemoteExecutor.SuccessExitCode;
+ }, stdOutText, stdErrText);
+
+ process.StartInfo.RedirectStandardOutput = true;
+ process.StartInfo.RedirectStandardError = true;
+ process.Start();
+
+ return process;
+ }
+ }
+}
diff --git a/src/libraries/System.Diagnostics.Process/tests/System.Diagnostics.Process.Tests.csproj b/src/libraries/System.Diagnostics.Process/tests/System.Diagnostics.Process.Tests.csproj
index 5c345d299a1ac8..dd73a7baa7f533 100644
--- a/src/libraries/System.Diagnostics.Process/tests/System.Diagnostics.Process.Tests.csproj
+++ b/src/libraries/System.Diagnostics.Process/tests/System.Diagnostics.Process.Tests.csproj
@@ -31,6 +31,7 @@
+