Skip to content
Merged
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,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.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; }
public void Refresh() { }
Expand Down Expand Up @@ -225,6 +226,14 @@ public sealed partial class ProcessExitStatus
public int ExitCode { get { throw null; } }
public System.Runtime.InteropServices.PosixSignal? Signal { get { throw null; } }
}
public readonly partial struct ProcessOutputLine
{
Comment thread
adamsitnik marked this conversation as resolved.
private readonly object _dummy;
private readonly int _dummyPrimitive;
public ProcessOutputLine(string content, bool standardError) { throw null; }
public string Content { get { throw null; } }
public bool StandardError { get { throw null; } }
}
public enum ProcessPriorityClass
{
Normal = 32,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
<Compile Include="System\Diagnostics\Process.cs" />
<Compile Include="System\Diagnostics\Process.Multiplexing.cs" />
<Compile Include="System\Diagnostics\ProcessExitStatus.cs" />
<Compile Include="System\Diagnostics\ProcessOutputLine.cs" />
<Compile Include="System\Diagnostics\ProcessInfo.cs" />
<Compile Include="System\Diagnostics\ProcessModule.cs" />
<Compile Include="System\Diagnostics\ProcessModuleCollection.cs" />
Expand Down Expand Up @@ -423,6 +424,7 @@
<ProjectReference Include="$(LibrariesProjectRoot)System.Runtime.InteropServices\src\System.Runtime.InteropServices.csproj" />
<ProjectReference Include="$(LibrariesProjectRoot)System.Text.Encoding.Extensions\src\System.Text.Encoding.Extensions.csproj" />
<ProjectReference Include="$(LibrariesProjectRoot)System.Threading\src\System.Threading.csproj" />
<ProjectReference Include="$(LibrariesProjectRoot)System.Threading.Channels\src\System.Threading.Channels.csproj" />
<ProjectReference Include="$(LibrariesProjectRoot)System.Threading.Overlapped\src\System.Threading.Overlapped.csproj" />
<ProjectReference Include="$(LibrariesProjectRoot)System.Threading.Thread\src\System.Threading.Thread.csproj" />
<ProjectReference Include="$(LibrariesProjectRoot)System.Threading.ThreadPool\src\System.Threading.ThreadPool.csproj" />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,12 @@
// The .NET Foundation licenses this file to you under the MIT license.

using System.Buffers;
using System.Collections.Generic;
using System.IO;
using System.Runtime.InteropServices;
using System.Runtime.CompilerServices;
using System.Text;
using System.Threading;
using System.Threading.Channels;
using System.Threading.Tasks;
using Microsoft.Win32.SafeHandles;

Expand Down Expand Up @@ -256,6 +258,92 @@ private static async Task<ArraySegment<byte>> ReadPipeToBufferAsync(Stream strea
}
}

/// <summary>
/// Asynchronously reads all standard output and standard error of the process as lines of text,
/// interleaving them as they become available.
/// </summary>
/// <param name="cancellationToken">
/// A token to cancel the asynchronous operation.
/// </param>
/// <returns>
/// An async enumerable of <see cref="ProcessOutputLine"/> instances representing the lines
/// read from standard output and standard error.
/// </returns>
/// <remarks>
/// Lines from standard output and standard error are yielded as they become available.
/// When the consumer stops enumerating early (for example, by breaking out of
/// <see langword="await foreach" />), any pending read operations are canceled.
/// </remarks>
/// <exception cref="InvalidOperationException">
/// Standard output or standard error has not been redirected.
/// -or-
/// A redirected stream has already been used for synchronous or asynchronous reading.
/// </exception>
/// <exception cref="OperationCanceledException">
/// The <paramref name="cancellationToken" /> was canceled.
/// </exception>
/// <exception cref="ObjectDisposedException">
/// The process has been disposed.
/// </exception>
public async IAsyncEnumerable<ProcessOutputLine> ReadAllLinesAsync([EnumeratorCancellation] CancellationToken cancellationToken = default)
{
ValidateReadAllState();

StreamReader outputReader = _standardOutput!;
StreamReader errorReader = _standardError!;

Channel<ProcessOutputLine> channel = Channel.CreateBounded<ProcessOutputLine>(0);
bool firstCompleted = false;

CancellationTokenSource linkedCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);

Task outputTask = ReadToChannelAsync(outputReader, standardError: false, linkedCts.Token);
Task errorTask = ReadToChannelAsync(errorReader, standardError: true, linkedCts.Token);
Comment thread
MihaZupan marked this conversation as resolved.

try
{
while (await channel.Reader.WaitToReadAsync(cancellationToken).ConfigureAwait(false))
{
while (channel.Reader.TryRead(out ProcessOutputLine line))
{
yield return line;
}
}
}
finally
{
linkedCts.Cancel();

// Ensure both tasks complete before disposing the CancellationTokenSource.
// The tasks handle all exceptions internally, so they always run to completion.
await outputTask.ConfigureAwait(false);
await errorTask.ConfigureAwait(false);

linkedCts.Dispose();
}

async Task ReadToChannelAsync(StreamReader reader, bool standardError, CancellationToken ct)
{
try
{
while (await reader.ReadLineAsync(ct).ConfigureAwait(false) is string line)
{
await channel.Writer.WriteAsync(new ProcessOutputLine(line, standardError), ct).ConfigureAwait(false);
}
}
catch (Exception ex)
{
channel.Writer.TryComplete(ex);
return;
}

if (Interlocked.Exchange(ref firstCompleted, true))
{
channel.Writer.TryComplete();
}
}
}

/// <summary>
/// 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.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

namespace System.Diagnostics
{
/// <summary>
/// Represents a single line of text read from a process's standard output or standard error stream.
/// </summary>
public readonly struct ProcessOutputLine
{
/// <summary>
/// Initializes a new instance of the <see cref="ProcessOutputLine"/> struct.
/// </summary>
/// <param name="content">The text content of the output line.</param>
/// <param name="standardError">
/// <see langword="true" /> if the line was read from standard error;
/// otherwise, <see langword="false" />.
/// </param>
public ProcessOutputLine(string content, bool standardError)
{
Content = content ?? throw new ArgumentNullException(nameof(content));
StandardError = standardError;
}

/// <summary>
/// Gets the text content of the output line.
/// </summary>
public string Content { get; }

/// <summary>
/// Gets a value that indicates whether the line was read from standard error.
/// </summary>
/// <value>
/// <see langword="true" /> if the line was read from standard error;
/// otherwise, <see langword="false" />.
/// </value>
public bool StandardError { get; }
}
}
Loading
Loading