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) =>