Stream Forwarding changes to not wait on buffer full before writing. Instead input streams will be read character by character as they Console.Write or Console.WriteLine. Upon finding a newline character, the line will be printed to the parent process's console.

This commit is contained in:
Bryan Thornbury 2016-02-04 18:18:08 -08:00
parent 129bd03098
commit ccaaebf6e5
9 changed files with 256 additions and 153 deletions

View file

@ -111,8 +111,8 @@ namespace Microsoft.DotNet.Cli.Utils
return new CommandResult( return new CommandResult(
this._process.StartInfo, this._process.StartInfo,
exitCode, exitCode,
_stdOut.GetCapturedOutput(), _stdOut.CapturedOutput,
_stdErr.GetCapturedOutput()); _stdErr.CapturedOutput);
} }
public Command WorkingDirectory(string projectDirectory) public Command WorkingDirectory(string projectDirectory)
@ -148,11 +148,11 @@ namespace Microsoft.DotNet.Cli.Utils
{ {
if (to == null) if (to == null)
{ {
_stdOut.ForwardTo(write: Reporter.Output.Write, writeLine: Reporter.Output.WriteLine); _stdOut.ForwardTo(writeLine: Reporter.Output.WriteLine);
} }
else else
{ {
_stdOut.ForwardTo(write: to.Write, writeLine: to.WriteLine); _stdOut.ForwardTo(writeLine: to.WriteLine);
} }
} }
return this; return this;
@ -165,11 +165,11 @@ namespace Microsoft.DotNet.Cli.Utils
{ {
if (to == null) if (to == null)
{ {
_stdErr.ForwardTo(write: Reporter.Error.Write, writeLine: Reporter.Error.WriteLine); _stdErr.ForwardTo(writeLine: Reporter.Error.WriteLine);
} }
else else
{ {
_stdErr.ForwardTo(write: to.Write, writeLine: to.WriteLine); _stdErr.ForwardTo(writeLine: to.WriteLine);
} }
} }
return this; return this;
@ -178,14 +178,14 @@ namespace Microsoft.DotNet.Cli.Utils
public Command OnOutputLine(Action<string> handler) public Command OnOutputLine(Action<string> handler)
{ {
ThrowIfRunning(); ThrowIfRunning();
_stdOut.ForwardTo(write: null, writeLine: handler); _stdOut.ForwardTo(writeLine: handler);
return this; return this;
} }
public Command OnErrorLine(Action<string> handler) public Command OnErrorLine(Action<string> handler)
{ {
ThrowIfRunning(); ThrowIfRunning();
_stdErr.ForwardTo(write: null, writeLine: handler); _stdErr.ForwardTo(writeLine: handler);
return this; return this;
} }

View file

@ -9,7 +9,7 @@ namespace Microsoft.DotNet.Cli.Utils
// Stupid-simple console manager // Stupid-simple console manager
public class Reporter public class Reporter
{ {
private static readonly Reporter Null = new Reporter(console: null); private static readonly Reporter NullReporter = new Reporter(console: null);
private static object _lock = new object(); private static object _lock = new object();
private readonly AnsiConsole _console; private readonly AnsiConsole _console;
@ -20,8 +20,8 @@ namespace Microsoft.DotNet.Cli.Utils
} }
public static Reporter Output { get; } = Create(AnsiConsole.GetOutput); public static Reporter Output { get; } = Create(AnsiConsole.GetOutput);
public static Reporter Error { get; } = Create(AnsiConsole.GetOutput); public static Reporter Error { get; } = Create(AnsiConsole.GetError);
public static Reporter Verbose { get; } = CommandContext.IsVerbose() ? Create(AnsiConsole.GetOutput) : Null; public static Reporter Verbose { get; } = CommandContext.IsVerbose() ? Create(AnsiConsole.GetOutput) : NullReporter;
public static Reporter Create(Func<bool, AnsiConsole> getter) public static Reporter Create(Func<bool, AnsiConsole> getter)
{ {

View file

@ -7,45 +7,45 @@ namespace Microsoft.DotNet.Cli.Utils
{ {
public sealed class StreamForwarder public sealed class StreamForwarder
{ {
private const int DefaultBufferSize = 256;
private readonly int _bufferSize;
private StringBuilder _builder; private StringBuilder _builder;
private StringWriter _capture; private StringWriter _capture;
private Action<string> _write; private Action<string> _write;
private Action<string> _writeLine; private Action<string> _writeLine;
public StreamForwarder(int bufferSize = DefaultBufferSize) public string CapturedOutput
{ {
_bufferSize = bufferSize; get
{
return _capture?.GetStringBuilder()?.ToString();
}
} }
public void Capture() public StreamForwarder Capture()
{ {
if (_capture != null) if (_capture != null)
{ {
throw new InvalidOperationException("Already capturing stream!"); throw new InvalidOperationException("Already capturing stream!");
} }
_capture = new StringWriter(); _capture = new StringWriter();
return this;
} }
public string GetCapturedOutput() public StreamForwarder ForwardTo(Action<string> writeLine)
{
return _capture?.GetStringBuilder()?.ToString();
}
public void ForwardTo(Action<string> write, Action<string> writeLine)
{ {
if (writeLine == null) if (writeLine == null)
{ {
throw new ArgumentNullException(nameof(writeLine)); throw new ArgumentNullException(nameof(writeLine));
} }
if (_writeLine != null) if (_writeLine != null)
{ {
throw new InvalidOperationException("Already handling stream!"); throw new InvalidOperationException("WriteLine forwarder set previously");
} }
_write = write;
_writeLine = writeLine; _writeLine = writeLine;
return this;
} }
public Thread BeginRead(TextReader reader) public Thread BeginRead(TextReader reader)
@ -57,91 +57,59 @@ namespace Microsoft.DotNet.Cli.Utils
public void Read(TextReader reader) public void Read(TextReader reader)
{ {
var bufferSize = 1;
int readCharacterCount;
char currentCharacter;
var buffer = new char[bufferSize];
_builder = new StringBuilder(); _builder = new StringBuilder();
var buffer = new char[_bufferSize];
int n;
while ((n = reader.Read(buffer, 0, _bufferSize)) > 0)
{
_builder.Append(buffer, 0, n);
WriteBlocks();
}
WriteRemainder();
}
private void WriteBlocks() // Using Read with buffer size 1 to prevent looping endlessly
{ // like we would when using Read() with no buffer
int n = _builder.Length; while ((readCharacterCount = reader.Read(buffer, 0, bufferSize)) > 0)
if (n == 0)
{ {
return; currentCharacter = buffer[0];
}
int offset = 0; // Flush per line
bool sawReturn = false; if (currentCharacter == '\n')
for (int i = 0; i < n; i++)
{
char c = _builder[i];
switch (c)
{ {
case '\r': WriteBuilder();
sawReturn = true; }
continue; else
case '\n': {
WriteLine(_builder.ToString(offset, i - offset - (sawReturn ? 1 : 0))); // Ignore \r
offset = i + 1; if (currentCharacter != '\r')
break; {
_builder.Append(currentCharacter);
}
} }
sawReturn = false;
} }
// If the buffer contains no line breaks and _write is // Flush anything else when the stream is closed
// supported, send the buffer content. // Which should only happen if someone used console.Write
if (!sawReturn && WriteBuilder();
(offset == 0) &&
((_write != null) || (_writeLine == null)))
{
WriteRemainder();
}
else
{
_builder.Remove(0, offset);
}
} }
private void WriteRemainder() private void WriteBuilder()
{ {
if (_builder.Length == 0) if (_builder.Length == 0)
{ {
return; return;
} }
Write(_builder.ToString());
WriteLine(_builder.ToString());
_builder.Clear(); _builder.Clear();
} }
private void WriteLine(string str) private void WriteLine(string str)
{ {
if (_capture != null) if (_capture != null)
{ {
_capture.WriteLine(str); _capture.WriteLine(str);
} }
// If _write is supported, so is _writeLine.
if (_writeLine != null)
{
_writeLine(str);
}
}
private void Write(string str) if (_writeLine != null)
{
if (_capture != null)
{
_capture.Write(str);
}
if (_write != null)
{
_write(str);
}
else if (_writeLine != null)
{ {
_writeLine(str); _writeLine(str);
} }

View file

@ -21,7 +21,6 @@ namespace Microsoft.DotNet.Tools.Restore
{ {
private static readonly string DefaultRid = PlatformServices.Default.Runtime.GetLegacyRestoreRuntimeIdentifier(); private static readonly string DefaultRid = PlatformServices.Default.Runtime.GetLegacyRestoreRuntimeIdentifier();
public static int Run(string[] args) public static int Run(string[] args)
{ {
DebugHelper.HandleDebugSwitch(ref args); DebugHelper.HandleDebugSwitch(ref args);

View file

@ -32,8 +32,8 @@ namespace Microsoft.DotNet.Tools.Test.Utilities
var stdOut = new StreamForwarder(); var stdOut = new StreamForwarder();
var stdErr = new StreamForwarder(); var stdErr = new StreamForwarder();
stdOut.ForwardTo(write: Reporter.Output.Write, writeLine: Reporter.Output.WriteLine); stdOut.ForwardTo(writeLine: Reporter.Output.WriteLine);
stdErr.ForwardTo(write: Reporter.Error.Write, writeLine: Reporter.Output.WriteLine); stdErr.ForwardTo(writeLine: Reporter.Output.WriteLine);
return RunProcess(commandPath, args, stdOut, stdErr); return RunProcess(commandPath, args, stdOut, stdErr);
} }
@ -82,8 +82,8 @@ namespace Microsoft.DotNet.Tools.Test.Utilities
var result = new CommandResult( var result = new CommandResult(
process.StartInfo, process.StartInfo,
process.ExitCode, process.ExitCode,
stdOut.GetCapturedOutput(), stdOut.CapturedOutput,
stdErr.GetCapturedOutput()); stdErr.CapturedOutput);
return result; return result;
} }

View file

@ -10,109 +10,192 @@ using Xunit;
using Microsoft.DotNet.Cli.Utils; using Microsoft.DotNet.Cli.Utils;
using Microsoft.DotNet.ProjectModel; using Microsoft.DotNet.ProjectModel;
using Microsoft.DotNet.Tools.Test.Utilities; using Microsoft.DotNet.Tools.Test.Utilities;
using Microsoft.Extensions.PlatformAbstractions;
using System.Threading;
namespace StreamForwarderTests namespace StreamForwarderTests
{ {
public class StreamForwarderTests public class StreamForwarderTests : TestBase
{ {
private static readonly string s_rid = PlatformServices.Default.Runtime.GetLegacyRestoreRuntimeIdentifier();
private static readonly string s_testProjectRoot = Path.Combine(AppContext.BaseDirectory, "TestProjects");
private TempDirectory _root;
public static void Main() public static void Main()
{ {
Console.WriteLine("Dummy Entrypoint"); Console.WriteLine("Dummy Entrypoint");
} }
[Fact] public StreamForwarderTests()
public void Unbuffered()
{ {
Forward(4, true, ""); _root = Temp.CreateDirectory();
Forward(4, true, "123", "123");
Forward(4, true, "1234", "1234");
Forward(3, true, "123456789", "123", "456", "789");
Forward(4, true, "\r\n", "\n");
Forward(4, true, "\r\n34", "\n", "34");
Forward(4, true, "1\r\n4", "1\n", "4");
Forward(4, true, "12\r\n", "12\n");
Forward(4, true, "123\r\n", "123\n");
Forward(4, true, "1234\r\n", "1234", "\n");
Forward(3, true, "\r\n3456\r\n9", "\n", "3456", "\n", "9");
Forward(4, true, "\n", "\n");
Forward(4, true, "\n234", "\n", "234");
Forward(4, true, "1\n34", "1\n", "34");
Forward(4, true, "12\n4", "12\n", "4");
Forward(4, true, "123\n", "123\n");
Forward(4, true, "1234\n", "1234", "\n");
Forward(3, true, "\n23456\n89", "\n", "23456", "\n", "89");
} }
[Fact] public static IEnumerable<object[]> ForwardingTheoryVariations
public void LineBuffered()
{ {
Forward(4, false, ""); get
Forward(4, false, "123", "123\n"); {
Forward(4, false, "1234", "1234\n"); return new[]
Forward(3, false, "123456789", "123456789\n"); {
Forward(4, false, "\r\n", "\n"); new object[] { "123", new string[]{"123"} },
Forward(4, false, "\r\n34", "\n", "34\n"); new object[] { "123\n", new string[] {"123"} },
Forward(4, false, "1\r\n4", "1\n", "4\n"); new object[] { "123\r\n", new string[] {"123"} },
Forward(4, false, "12\r\n", "12\n"); new object[] { "1234\n5678", new string[] {"1234", "5678"} },
Forward(4, false, "123\r\n", "123\n"); new object[] { "1234\r\n5678", new string[] {"1234", "5678"} },
Forward(4, false, "1234\r\n", "1234\n"); new object[] { "1234\n5678\n", new string[] {"1234", "5678"} },
Forward(3, false, "\r\n3456\r\n9", "\n", "3456\n", "9\n"); new object[] { "1234\r\n5678\r\n", new string[] {"1234", "5678"} },
Forward(4, false, "\n", "\n"); new object[] { "1234\n5678\nabcdefghijklmnopqrstuvwxyz", new string[] {"1234", "5678", "abcdefghijklmnopqrstuvwxyz"} },
Forward(4, false, "\n234", "\n", "234\n"); new object[] { "1234\r\n5678\r\nabcdefghijklmnopqrstuvwxyz", new string[] {"1234", "5678", "abcdefghijklmnopqrstuvwxyz"} },
Forward(4, false, "1\n34", "1\n", "34\n"); new object[] { "1234\n5678\nabcdefghijklmnopqrstuvwxyz\n", new string[] {"1234", "5678", "abcdefghijklmnopqrstuvwxyz"} },
Forward(4, false, "12\n4", "12\n", "4\n"); new object[] { "1234\r\n5678\r\nabcdefghijklmnopqrstuvwxyz\r\n", new string[] {"1234", "5678", "abcdefghijklmnopqrstuvwxyz"} }
Forward(4, false, "123\n", "123\n"); };
Forward(4, false, "1234\n", "1234\n"); }
Forward(3, false, "\n23456\n89", "\n", "23456\n", "89\n");
} }
private static void Forward(int bufferSize, bool unbuffered, string str, params string[] expectedWrites) [Theory]
[InlineData("123")]
[InlineData("123\n")]
public void TestNoForwardingNoCapture(string inputStr)
{
TestCapturingAndForwardingHelper(ForwardOptions.None, inputStr, null, new string[0]);
}
[Theory]
[MemberData("ForwardingTheoryVariations")]
public void TestForwardingOnly(string inputStr, string[] expectedWrites)
{ {
var expectedCaptured = str.Replace("\r", "").Replace("\n", Environment.NewLine); for(int i = 0; i < expectedWrites.Length; ++i)
{
expectedWrites[i] += Environment.NewLine;
}
TestCapturingAndForwardingHelper(ForwardOptions.WriteLine, inputStr, null, expectedWrites);
}
// No forwarding. [Theory]
Forward(bufferSize, ForwardOptions.None, str, null, new string[0]); [MemberData("ForwardingTheoryVariations")]
public void TestCaptureOnly(string inputStr, string[] expectedWrites)
{
for(int i = 0; i < expectedWrites.Length; ++i)
{
expectedWrites[i] += Environment.NewLine;
}
// Capture only. var expectedCaptured = string.Join("", expectedWrites);
Forward(bufferSize, ForwardOptions.Capture, str, expectedCaptured, new string[0]);
TestCapturingAndForwardingHelper(ForwardOptions.Capture, inputStr, expectedCaptured, new string[0]);
}
var writeOptions = unbuffered ? [Theory]
ForwardOptions.Write | ForwardOptions.WriteLine : [MemberData("ForwardingTheoryVariations")]
ForwardOptions.WriteLine; public void TestCaptureAndForwardingTogether(string inputStr, string[] expectedWrites)
{
for(int i = 0; i < expectedWrites.Length; ++i)
{
expectedWrites[i] += Environment.NewLine;
}
// Forward. var expectedCaptured = string.Join("", expectedWrites);
Forward(bufferSize, writeOptions, str, null, expectedWrites);
// Forward and capture. TestCapturingAndForwardingHelper(ForwardOptions.WriteLine | ForwardOptions.Capture, inputStr, expectedCaptured, expectedWrites);
Forward(bufferSize, writeOptions | ForwardOptions.Capture, str, expectedCaptured, expectedWrites);
} }
private enum ForwardOptions private enum ForwardOptions
{ {
None = 0x0, None = 0x0,
Capture = 0x1, Capture = 0x1,
Write = 0x02, WriteLine = 0x02,
WriteLine = 0x04,
} }
private static void Forward(int bufferSize, ForwardOptions options, string str, string expectedCaptured, string[] expectedWrites) private void TestCapturingAndForwardingHelper(ForwardOptions options, string str, string expectedCaptured, string[] expectedWrites)
{ {
var forwarder = new StreamForwarder(bufferSize); var forwarder = new StreamForwarder();
var writes = new List<string>(); var writes = new List<string>();
if ((options & ForwardOptions.WriteLine) != 0) if ((options & ForwardOptions.WriteLine) != 0)
{ {
forwarder.ForwardTo( forwarder.ForwardTo(writeLine: s => writes.Add(s + Environment.NewLine));
write: (options & ForwardOptions.Write) == 0 ? (Action<string>)null : writes.Add,
writeLine: s => writes.Add(s + "\n"));
} }
if ((options & ForwardOptions.Capture) != 0) if ((options & ForwardOptions.Capture) != 0)
{ {
forwarder.Capture(); forwarder.Capture();
} }
forwarder.Read(new StringReader(str)); forwarder.Read(new StringReader(str));
Assert.Equal(expectedWrites, writes); Assert.Equal(expectedWrites, writes);
var captured = forwarder.GetCapturedOutput();
var captured = forwarder.CapturedOutput;
Assert.Equal(expectedCaptured, captured); Assert.Equal(expectedCaptured, captured);
} }
[Fact]
public void TestAsyncOrdering()
{
var expectedOutputLines = new string[]
{
"** Standard Out 1 **",
"** Standard Error 1 **",
"** Standard Out 2 **",
"** Standard Error 2 **"
};
var expectedOutput = string.Join(Environment.NewLine, expectedOutputLines) + Environment.NewLine;
var testProjectDllPath = SetupTestProject();
var testReporter = new TestReporter();
var testCommand = Command.Create(testProjectDllPath, new string[0])
.OnOutputLine(testReporter.WriteLine)
.OnErrorLine(testReporter.WriteLine);
testCommand.Execute();
var resultString = testReporter.InternalStringWriter.GetStringBuilder().ToString();
Console.WriteLine(expectedOutput);
Console.WriteLine(resultString);
Assert.Equal(expectedOutput, resultString);
}
private string SetupTestProject()
{
var sourceTestProjectPath = Path.Combine(s_testProjectRoot, "OutputStandardOutputAndError");
var binTestProjectPath = _root.CopyDirectory(sourceTestProjectPath).Path;
var buildCommand = new BuildCommand(Path.Combine(binTestProjectPath, "project.json"));
buildCommand.Execute();
var buildOutputExe = "OutputStandardOutputAndError" + Constants.ExeSuffix;
var buildOutputPath = Path.Combine(binTestProjectPath, "bin/Debug/dnxcore50", buildOutputExe);
return buildOutputPath;
}
private class TestReporter
{
private static object _lock = new object();
private StringWriter _stringWriter;
public StringWriter InternalStringWriter
{
get { return _stringWriter; }
}
public TestReporter()
{
_stringWriter = new StringWriter();
}
public void WriteLine(string message)
{
lock(_lock)
{
_stringWriter.WriteLine(message);
}
}
}
} }
} }

View file

@ -0,0 +1,20 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="14.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup>
<VisualStudioVersion Condition="'$(VisualStudioVersion)' == ''">14.0</VisualStudioVersion>
<VSToolsPath Condition="'$(VSToolsPath)' == ''">$(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)</VSToolsPath>
</PropertyGroup>
<Import Project="$(VSToolsPath)\DNX\Microsoft.DNX.Props" Condition="'$(VSToolsPath)' != ''" />
<PropertyGroup Label="Globals">
<ProjectGuid>58808bbc-371e-47d6-a3d0-4902145eda4e</ProjectGuid>
<RootNamespace>OutputStandardOutputAndError</RootNamespace>
<BaseIntermediateOutputPath Condition="'$(BaseIntermediateOutputPath)'=='' ">..\..\artifacts\obj\$(MSBuildProjectName)</BaseIntermediateOutputPath>
<OutputPath Condition="'$(OutputPath)'=='' ">..\..\artifacts\bin\$(MSBuildProjectName)\</OutputPath>
</PropertyGroup>
<PropertyGroup>
<SchemaVersion>2.0</SchemaVersion>
</PropertyGroup>
<Import Project="$(VSToolsPath)\DNX\Microsoft.DNX.targets" Condition="'$(VSToolsPath)' != ''" />
</Project>

View file

@ -0,0 +1,19 @@
// Copyright (c) .NET Foundation and contributors. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
using System;
using System.Diagnostics;
namespace TestApp
{
public class Program
{
public static void Main(string[] args)
{
Console.Out.WriteLine("** Standard Out 1 **");
Console.Error.WriteLine("** Standard Error 1 **");
Console.Out.WriteLine("** Standard Out 2 **");
Console.Error.WriteLine("** Standard Error 2 **");
}
}
}

View file

@ -0,0 +1,14 @@
{
"version": "1.0.0-*",
"compilationOptions": {
"emitEntryPoint": true
},
"dependencies": {
"NETStandard.Library": "1.0.0-rc2-23728"
},
"frameworks": {
"dnxcore50": { }
}
}