File: StreamForwarder.cs
Web Access
Project: ..\..\..\src\Cli\Microsoft.DotNet.Cli.Utils\Microsoft.DotNet.Cli.Utils.csproj (Microsoft.DotNet.Cli.Utils)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
namespace Microsoft.DotNet.Cli.Utils;
 
public sealed class StreamForwarder
{
    private static readonly char[] s_ignoreCharacters = ['\r'];
    private static readonly char s_flushBuilderCharacter = '\n';
 
    private StringBuilder? _builder;
    private StringWriter? _capture;
    private Action<string>? _writeLine;
    private bool _trimTrailingCapturedNewline;
 
    public string? CapturedOutput
    {
        get
        {
            var capture = _capture?.GetStringBuilder()?.ToString();
            if (_trimTrailingCapturedNewline)
            {
                capture = capture?.TrimEnd('\r', '\n');
            }
            return capture;
        }
    }
 
    public StreamForwarder Capture(bool trimTrailingNewline = false)
    {
        ThrowIfCaptureSet();
 
        _capture = new StringWriter();
        _trimTrailingCapturedNewline = trimTrailingNewline;
 
        return this;
    }
 
    public StreamForwarder ForwardTo(Action<string> writeLine)
    {
        ThrowIfNull(writeLine);
 
        ThrowIfForwarderSet();
 
        _writeLine = writeLine;
 
        return this;
    }
 
    public Task BeginRead(TextReader reader)
    {
        return Task.Run(() => Read(reader));
    }
 
    public void Read(TextReader reader)
    {
        var bufferSize = 1;
 
        int readCharacterCount;
        char currentCharacter;
 
        var buffer = new char[bufferSize];
        _builder = new StringBuilder();
 
        // Using Read with buffer size 1 to prevent looping endlessly
        // like we would when using Read() with no buffer
        while ((readCharacterCount = reader.Read(buffer, 0, bufferSize)) > 0)
        {
            currentCharacter = buffer[0];
 
            if (currentCharacter == s_flushBuilderCharacter)
            {
                WriteBuilder();
            }
            else if (!s_ignoreCharacters.Contains(currentCharacter))
            {
                _builder.Append(currentCharacter);
            }
        }
 
        // Flush anything else when the stream is closed
        // Which should only happen if someone used console.Write
        if (_builder.Length > 0)
        {
            WriteBuilder();
        }
    }
 
    private void WriteBuilder()
    {
        WriteLine(_builder?.ToString() ?? string.Empty);
        _builder?.Clear();
    }
 
    private void WriteLine(string str)
    {
        if (_capture != null)
        {
            _capture.WriteLine(str);
        }
 
        _writeLine?.Invoke(str);
    }
 
    private void ThrowIfNull(object obj)
    {
        if (obj == null)
        {
            throw new ArgumentNullException(nameof(obj));
        }
    }
 
    private void ThrowIfForwarderSet()
    {
        if (_writeLine != null)
        {
            throw new InvalidOperationException(LocalizableStrings.WriteLineForwarderSetPreviously);
        }
    }
 
    private void ThrowIfCaptureSet()
    {
        if (_capture != null)
        {
            throw new InvalidOperationException(LocalizableStrings.AlreadyCapturingStream);
        }
    }
}