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 0a6e75ee3cba55..7cd0b3015d5f69 100644
--- a/src/libraries/System.Diagnostics.Process/ref/System.Diagnostics.Process.cs
+++ b/src/libraries/System.Diagnostics.Process/ref/System.Diagnostics.Process.cs
@@ -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")]
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
index f23ad6631fa89e..27393f1d50a8f2 100644
--- 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
@@ -2,6 +2,8 @@
// 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;
@@ -9,13 +11,15 @@ namespace System.Diagnostics
{
public partial class Process
{
+ private static SafePipeHandle GetSafeHandleFromStreamReader(StreamReader reader) => ((AnonymousPipeClientStream)reader.BaseStream).SafePipeHandle;
+
///
/// 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,
+ SafePipeHandle outputHandle,
+ SafePipeHandle errorHandle,
int timeoutMs,
ref byte[] outputBuffer,
ref int outputBytesRead,
@@ -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();
}
@@ -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);
@@ -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).
///
- 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)
{
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
index 6d28e278a28bac..fb30cc07fb254c 100644
--- 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
@@ -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;
@@ -10,6 +11,8 @@ namespace System.Diagnostics
{
public partial class Process
{
+ private static SafeFileHandle GetSafeHandleFromStreamReader(StreamReader reader) => ((FileStream)reader.BaseStream).SafeFileHandle;
+
///
/// Reads from both standard output and standard error pipes using Windows overlapped IO
/// with wait handles for single-threaded synchronous multiplexing.
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
index 8da1afc4b1a6ad..71ec5a984a1096 100644
--- a/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/Process.Multiplexing.cs
+++ b/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/Process.Multiplexing.cs
@@ -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
@@ -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));
+ string standardError = errorEncoding.GetString(errorBuffer.AsSpan(0, errorBytesRead));
return (standardOutput, standardError);
}
@@ -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[] errorResult = errorBytesRead > 0
- ? errorBuffer.AsSpan(0, errorBytesRead).ToArray()
- : Array.Empty();
+ byte[] outputResult = outputBuffer.AsSpan(0, outputBytesRead).ToArray();
+ byte[] errorResult = errorBuffer.AsSpan(0, errorBytesRead).ToArray();
return (outputResult, errorResult);
}
@@ -120,6 +111,151 @@ public partial class Process
}
}
+ ///
+ /// Asynchronously reads all standard output and standard error of the process as text.
+ ///
+ ///
+ /// A token to cancel the asynchronous operation.
+ ///
+ ///
+ /// A task that represents the asynchronous read operation. The value of the task contains
+ /// a tuple with 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 was canceled.
+ ///
+ ///
+ /// The process has been disposed.
+ ///
+ public async Task<(string StandardOutput, string StandardError)> ReadAllTextAsync(CancellationToken cancellationToken = default)
+ {
+ (ArraySegment standardOutput, ArraySegment 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.Shared.Return(standardOutput.Array!);
+ ArrayPool.Shared.Return(standardError.Array!);
+ }
+ }
+
+ ///
+ /// Asynchronously reads all standard output and standard error of the process as byte arrays.
+ ///
+ ///
+ /// A token to cancel the asynchronous operation.
+ ///
+ ///
+ /// A task that represents the asynchronous read operation. The value of the task contains
+ /// a tuple with 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 was canceled.
+ ///
+ ///
+ /// The process has been disposed.
+ ///
+ public async Task<(byte[] StandardOutput, byte[] StandardError)> ReadAllBytesAsync(CancellationToken cancellationToken = default)
+ {
+ (ArraySegment standardOutput, ArraySegment standardError) = await ReadAllBytesIntoRentedArraysAsync(cancellationToken).ConfigureAwait(false);
+
+ try
+ {
+ return (standardOutput.AsSpan().ToArray(), standardError.AsSpan().ToArray());
+ }
+ finally
+ {
+ ArrayPool.Shared.Return(standardOutput.Array!);
+ ArrayPool.Shared.Return(standardError.Array!);
+ }
+ }
+
+ private async Task<(ArraySegment StandardOutput, ArraySegment StandardError)> ReadAllBytesIntoRentedArraysAsync(CancellationToken cancellationToken)
+ {
+ ValidateReadAllState();
+
+ Task> outputTask = ReadPipeToBufferAsync(_standardOutput!.BaseStream, cancellationToken);
+ Task> 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.Shared.Return(outputTask.Result.Array!);
+ }
+
+ if (errorTask.IsCompletedSuccessfully)
+ {
+ ArrayPool.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);
+ }
+
+ ///
+ /// 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.
+ ///
+ private static async Task> ReadPipeToBufferAsync(Stream stream, CancellationToken cancellationToken)
+ {
+ int bytesRead = 0;
+ byte[] buffer = ArrayPool.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(buffer, 0, bytesRead);
+ }
+ catch
+ {
+ ArrayPool.Shared.Return(buffer);
+ throw;
+ }
+ }
+
///
/// 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.
@@ -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,
@@ -184,40 +320,16 @@ private void ReadPipesToBuffers(
{
if (outputRefAdded)
{
- outputOwner.DangerousRelease();
+ outputHandle.DangerousRelease();
}
if (errorRefAdded)
{
- errorOwner.DangerousRelease();
+ errorHandle.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.
///
diff --git a/src/libraries/System.Diagnostics.Process/tests/ProcessMultiplexingTests.cs b/src/libraries/System.Diagnostics.Process/tests/ProcessMultiplexingTests.cs
index 906cbbe4c47202..a392a3b9a16016 100644
--- a/src/libraries/System.Diagnostics.Process/tests/ProcessMultiplexingTests.cs
+++ b/src/libraries/System.Diagnostics.Process/tests/ProcessMultiplexingTests.cs
@@ -2,9 +2,11 @@
// The .NET Foundation licenses this file to you under the MIT license.
using System.IO;
+using System.Reflection;
using System.Text;
+using System.Threading;
+using System.Threading.Tasks;
using Microsoft.DotNet.RemoteExecutor;
-using Microsoft.Win32.SafeHandles;
using Xunit;
namespace System.Diagnostics.Tests
@@ -14,9 +16,11 @@ 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)
+ [InlineData(true, false)]
+ [InlineData(false, false)]
+ [InlineData(true, true)]
+ [InlineData(false, true)]
+ public async Task ReadAll_ThrowsAfterDispose(bool bytes, bool useAsync)
{
Process process = CreateProcess(RemotelyInvokable.Dummy);
process.Start();
@@ -26,40 +30,74 @@ public void ReadAll_ThrowsAfterDispose(bool bytes)
if (bytes)
{
- Assert.Throws(() => process.ReadAllBytes());
+ if (useAsync)
+ {
+ await Assert.ThrowsAsync(() => process.ReadAllBytesAsync());
+ }
+ else
+ {
+ Assert.Throws(() => process.ReadAllBytes());
+ }
}
else
{
- Assert.Throws(() => process.ReadAllText());
+ if (useAsync)
+ {
+ await Assert.ThrowsAsync(() => process.ReadAllTextAsync());
+ }
+ else
+ {
+ Assert.Throws(() => process.ReadAllText());
+ }
}
}
[ConditionalTheory(typeof(RemoteExecutor), nameof(RemoteExecutor.IsSupported))]
- [InlineData(true)]
- [InlineData(false)]
- public void ReadAll_ThrowsWhenNoStreamsRedirected(bool bytes)
+ [InlineData(true, false)]
+ [InlineData(false, false)]
+ [InlineData(true, true)]
+ [InlineData(false, true)]
+ public async Task ReadAll_ThrowsWhenNoStreamsRedirected(bool bytes, bool useAsync)
{
Process process = CreateProcess(RemotelyInvokable.Dummy);
process.Start();
if (bytes)
{
- Assert.Throws(() => process.ReadAllBytes());
+ if (useAsync)
+ {
+ await Assert.ThrowsAsync(() => process.ReadAllBytesAsync());
+ }
+ else
+ {
+ Assert.Throws(() => process.ReadAllBytes());
+ }
}
else
{
- Assert.Throws(() => process.ReadAllText());
+ if (useAsync)
+ {
+ await Assert.ThrowsAsync(() => process.ReadAllTextAsync());
+ }
+ 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)
+ [InlineData(true, true, false)]
+ [InlineData(true, false, false)]
+ [InlineData(false, true, false)]
+ [InlineData(false, false, false)]
+ [InlineData(true, true, true)]
+ [InlineData(true, false, true)]
+ [InlineData(false, true, true)]
+ [InlineData(false, false, true)]
+ public async Task ReadAll_ThrowsWhenOnlyOutputOrErrorIsRedirected(bool bytes, bool standardOutput, bool useAsync)
{
Process process = CreateProcess(RemotelyInvokable.Dummy);
process.StartInfo.RedirectStandardOutput = standardOutput;
@@ -68,22 +106,40 @@ public void ReadAll_ThrowsWhenOnlyOutputOrErrorIsRedirected(bool bytes, bool sta
if (bytes)
{
- Assert.Throws(() => process.ReadAllBytes());
+ if (useAsync)
+ {
+ await Assert.ThrowsAsync(() => process.ReadAllBytesAsync());
+ }
+ else
+ {
+ Assert.Throws(() => process.ReadAllBytes());
+ }
}
else
{
- Assert.Throws(() => process.ReadAllText());
+ if (useAsync)
+ {
+ await Assert.ThrowsAsync(() => process.ReadAllTextAsync());
+ }
+ 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)
+ [InlineData(true, true, false)]
+ [InlineData(true, false, false)]
+ [InlineData(false, true, false)]
+ [InlineData(false, false, false)]
+ [InlineData(true, true, true)]
+ [InlineData(true, false, true)]
+ [InlineData(false, true, true)]
+ [InlineData(false, false, true)]
+ public async Task ReadAll_ThrowsWhenOutputOrErrorIsInSyncMode(bool bytes, bool standardOutput, bool useAsync)
{
Process process = CreateProcess(RemotelyInvokable.Dummy);
process.StartInfo.RedirectStandardOutput = true;
@@ -95,22 +151,40 @@ public void ReadAll_ThrowsWhenOutputOrErrorIsInSyncMode(bool bytes, bool standar
if (bytes)
{
- Assert.Throws(() => process.ReadAllBytes());
+ if (useAsync)
+ {
+ await Assert.ThrowsAsync(() => process.ReadAllBytesAsync());
+ }
+ else
+ {
+ Assert.Throws(() => process.ReadAllBytes());
+ }
}
else
{
- Assert.Throws(() => process.ReadAllText());
+ if (useAsync)
+ {
+ await Assert.ThrowsAsync(() => process.ReadAllTextAsync());
+ }
+ 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)
+ [InlineData(true, true, false)]
+ [InlineData(true, false, false)]
+ [InlineData(false, true, false)]
+ [InlineData(false, false, false)]
+ [InlineData(true, true, true)]
+ [InlineData(true, false, true)]
+ [InlineData(false, true, true)]
+ [InlineData(false, false, true)]
+ public async Task ReadAll_ThrowsWhenOutputOrErrorIsInAsyncMode(bool bytes, bool standardOutput, bool useAsync)
{
Process process = CreateProcess(RemotelyInvokable.StreamBody);
process.StartInfo.RedirectStandardOutput = true;
@@ -128,11 +202,25 @@ public void ReadAll_ThrowsWhenOutputOrErrorIsInAsyncMode(bool bytes, bool standa
if (bytes)
{
- Assert.Throws(() => process.ReadAllBytes());
+ if (useAsync)
+ {
+ await Assert.ThrowsAsync(() => process.ReadAllBytesAsync());
+ }
+ else
+ {
+ Assert.Throws(() => process.ReadAllBytes());
+ }
}
else
{
- Assert.Throws(() => process.ReadAllText());
+ if (useAsync)
+ {
+ await Assert.ThrowsAsync(() => process.ReadAllTextAsync());
+ }
+ else
+ {
+ Assert.Throws(() => process.ReadAllText());
+ }
}
if (standardOutput)
@@ -178,15 +266,23 @@ public void ReadAll_ThrowsTimeoutExceptionOnTimeout(bool bytes)
}
[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)
+ [InlineData("hello", "world", true, false)]
+ [InlineData("hello", "world", false, false)]
+ [InlineData("just output", "", true, false)]
+ [InlineData("just output", "", false, false)]
+ [InlineData("", "just error", true, false)]
+ [InlineData("", "just error", false, false)]
+ [InlineData("", "", true, false)]
+ [InlineData("", "", false, false)]
+ [InlineData("hello", "world", true, true)]
+ [InlineData("hello", "world", false, true)]
+ [InlineData("just output", "", true, true)]
+ [InlineData("just output", "", false, true)]
+ [InlineData("", "just error", true, true)]
+ [InlineData("", "just error", false, true)]
+ [InlineData("", "", true, true)]
+ [InlineData("", "", false, true)]
+ public async Task ReadAll_ReadsBothOutputAndError(string standardOutput, string standardError, bool bytes, bool useAsync)
{
using Process process = StartPrintingProcess(
string.IsNullOrEmpty(standardOutput) ? DontPrintAnything : standardOutput,
@@ -194,14 +290,18 @@ public void ReadAll_ReadsBothOutputAndError(string standardOutput, string standa
if (bytes)
{
- (byte[] capturedOutput, byte[] capturedError) = process.ReadAllBytes();
+ (byte[] capturedOutput, byte[] capturedError) = useAsync
+ ? await process.ReadAllBytesAsync()
+ : process.ReadAllBytes();
Assert.Equal(Encoding.Default.GetBytes(standardOutput), capturedOutput);
Assert.Equal(Encoding.Default.GetBytes(standardError), capturedError);
}
else
{
- (string capturedOutput, string capturedError) = process.ReadAllText();
+ (string capturedOutput, string capturedError) = useAsync
+ ? await process.ReadAllTextAsync()
+ : process.ReadAllText();
Assert.Equal(standardOutput, capturedOutput);
Assert.Equal(standardError, capturedError);
@@ -210,8 +310,10 @@ public void ReadAll_ReadsBothOutputAndError(string standardOutput, string standa
Assert.True(process.WaitForExit(WaitInMS));
}
- [ConditionalFact(typeof(RemoteExecutor), nameof(RemoteExecutor.IsSupported))]
- public void ReadAllText_ReadsInterleavedOutput()
+ [ConditionalTheory(typeof(RemoteExecutor), nameof(RemoteExecutor.IsSupported))]
+ [InlineData(false)]
+ [InlineData(true)]
+ public async Task ReadAllText_ReadsInterleavedOutput(bool useAsync)
{
const int iterations = 100;
using Process process = CreateProcess(() =>
@@ -231,7 +333,9 @@ public void ReadAllText_ReadsInterleavedOutput()
process.StartInfo.RedirectStandardError = true;
process.Start();
- (string standardOutput, string standardError) = process.ReadAllText();
+ (string standardOutput, string standardError) = useAsync
+ ? await process.ReadAllTextAsync()
+ : process.ReadAllText();
StringBuilder expectedOutput = new();
StringBuilder expectedError = new();
@@ -247,8 +351,10 @@ public void ReadAllText_ReadsInterleavedOutput()
Assert.True(process.WaitForExit(WaitInMS));
}
- [ConditionalFact(typeof(RemoteExecutor), nameof(RemoteExecutor.IsSupported))]
- public void ReadAllBytes_ReadsBinaryDataWithNullBytes()
+ [ConditionalTheory(typeof(RemoteExecutor), nameof(RemoteExecutor.IsSupported))]
+ [InlineData(false)]
+ [InlineData(true)]
+ public async Task ReadAllBytes_ReadsBinaryDataWithNullBytes(bool useAsync)
{
string testFilePath = GetTestFilePath();
byte[] binaryData = new byte[1024];
@@ -273,15 +379,19 @@ public void ReadAllBytes_ReadsBinaryDataWithNullBytes()
process.StartInfo.RedirectStandardError = true;
process.Start();
- (byte[] standardOutput, byte[] standardError) = process.ReadAllBytes();
+ (byte[] standardOutput, byte[] standardError) = useAsync
+ ? await process.ReadAllBytesAsync()
+ : process.ReadAllBytes();
Assert.Equal(binaryData, standardOutput);
Assert.Empty(standardError);
Assert.True(process.WaitForExit(WaitInMS));
}
- [ConditionalFact(typeof(RemoteExecutor), nameof(RemoteExecutor.IsSupported))]
- public void ReadAllText_ReadsLargeOutput()
+ [ConditionalTheory(typeof(RemoteExecutor), nameof(RemoteExecutor.IsSupported))]
+ [InlineData(false)]
+ [InlineData(true)]
+ public async Task ReadAllText_ReadsLargeOutput(bool useAsync)
{
string testFilePath = GetTestFilePath();
string largeText = new string('A', 100_000);
@@ -299,15 +409,19 @@ public void ReadAllText_ReadsLargeOutput()
process.StartInfo.RedirectStandardError = true;
process.Start();
- (string standardOutput, string standardError) = process.ReadAllText();
+ (string standardOutput, string standardError) = useAsync
+ ? await process.ReadAllTextAsync()
+ : 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()
+ [ConditionalTheory(typeof(RemoteExecutor), nameof(RemoteExecutor.IsSupported))]
+ [InlineData(false)]
+ [InlineData(true)]
+ public async Task ReadAllBytes_ReadsLargeOutput(bool useAsync)
{
string testFilePath = GetTestFilePath();
byte[] largeByteArray = new byte[100_000];
@@ -326,13 +440,92 @@ public void ReadAllBytes_ReadsLargeOutput()
process.StartInfo.RedirectStandardError = true;
process.Start();
- (byte[] standardOutput, byte[] standardError) = process.ReadAllBytes();
+ (byte[] standardOutput, byte[] standardError) = useAsync
+ ? await process.ReadAllBytesAsync()
+ : process.ReadAllBytes();
Assert.Equal(largeByteArray, standardOutput);
Assert.Empty(standardError);
Assert.True(process.WaitForExit(WaitInMS));
}
+ [ConditionalTheory(typeof(RemoteExecutor), nameof(RemoteExecutor.IsSupported))]
+ [InlineData(true, false)]
+ [InlineData(false, true)]
+ [InlineData(true, true)]
+ [InlineData(false, false)]
+ public async Task ReadAllAsync_ThrowsAllAvailableExceptions(bool multiple, bool bytes)
+ {
+ using Process process = CreateProcess(RemotelyInvokable.Dummy);
+ process.StartInfo.RedirectStandardOutput = true;
+ process.StartInfo.RedirectStandardError = true;
+ process.Start();
+
+ try
+ {
+ // Close the underlying pipe streams to force both async reads to fail.
+ // We access the internal fields directly via reflection to avoid going through
+ // the StandardOutput/StandardError properties, which would set the StreamReadMode
+ // and prevent ReadAllBytesAsync from being called.
+ FieldInfo stdoutField = typeof(Process).GetField("_standardOutput", BindingFlags.NonPublic | BindingFlags.Instance)!;
+ FieldInfo stderrField = typeof(Process).GetField("_standardError", BindingFlags.NonPublic | BindingFlags.Instance)!;
+
+ StreamReader stdoutReader = (StreamReader)stdoutField.GetValue(process)!;
+ StreamReader stderrReader = (StreamReader)stderrField.GetValue(process)!;
+
+ stdoutReader.BaseStream.Dispose();
+
+ if (multiple)
+ {
+ stderrReader.BaseStream.Dispose();
+
+ AggregateException aggregate = await Assert.ThrowsAsync(() => bytes ? process.ReadAllBytesAsync() : process.ReadAllTextAsync());
+ Assert.Equal(2, aggregate.InnerExceptions.Count);
+ Assert.All(aggregate.InnerExceptions, ex => Assert.IsType(ex));
+ }
+ else
+ {
+ await Assert.ThrowsAsync(() => bytes ? process.ReadAllBytesAsync() : process.ReadAllTextAsync());
+ }
+ }
+ finally
+ {
+ process.Kill();
+ }
+ }
+
+ [ConditionalTheory(typeof(RemoteExecutor), nameof(RemoteExecutor.IsSupported))]
+ [InlineData(true)]
+ [InlineData(false)]
+ public async Task ReadAllAsync_ThrowsOperationCanceledOnCancellation(bool bytes)
+ {
+ Process process = CreateProcess(RemotelyInvokable.ReadLine);
+ process.StartInfo.RedirectStandardOutput = true;
+ process.StartInfo.RedirectStandardError = true;
+ process.StartInfo.RedirectStandardInput = true;
+ process.Start();
+
+ try
+ {
+ using CancellationTokenSource cts = new(TimeSpan.FromMilliseconds(100));
+
+ if (bytes)
+ {
+ await Assert.ThrowsAnyAsync(() => process.ReadAllBytesAsync(cts.Token));
+ }
+ else
+ {
+ await Assert.ThrowsAnyAsync(() => process.ReadAllTextAsync(cts.Token));
+ }
+ }
+ finally
+ {
+ process.Kill();
+ }
+
+ Assert.True(process.WaitForExit(WaitInMS));
+ }
+
private Process StartPrintingProcess(string stdOutText, string stdErrText)
{
Process process = CreateProcess((stdOut, stdErr) =>