Merge pull request #1240 from dotnet/brthor/stream_forwarding_changes

Stream Forwarding Changes.
This commit is contained in:
Bryan Thornbury 2016-02-09 10:45:35 -08:00
commit 0b27dba299
14 changed files with 231 additions and 159 deletions

View file

@ -65,3 +65,6 @@ Each command's project root should contain a manpage-style Readme.md that descri
#### Add command to packages
- Update the `symlinks` property of `packaging/debian/debian_config.json` to include the new command
- Update the `$Projects` property in `packaging/osx/scripts/postinstall`
#### Things to Know
- Any added commands are usually invoked through `dotnet {command}`. As a result of this, stdout and stderr are redirected through the driver (`dotnet`) and buffered by line. As a result of this, child commands should use Console.WriteLine in any cases where they expect output to be written immediately. Any uses of Console.Write should be followed by Console.WriteLine to ensure the output is written.

View file

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

View file

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

View file

@ -2,50 +2,46 @@ using System;
using System.IO;
using System.Text;
using System.Threading;
using System.Linq;
namespace Microsoft.DotNet.Cli.Utils
{
public sealed class StreamForwarder
{
private const int DefaultBufferSize = 256;
private static readonly char[] s_ignoreCharacters = new char[] { '\r' };
private static readonly char s_flushBuilderCharacter = '\n';
private readonly int _bufferSize;
private StringBuilder _builder;
private StringWriter _capture;
private Action<string> _write;
private Action<string> _writeLine;
public StreamForwarder(int bufferSize = DefaultBufferSize)
public string CapturedOutput
{
_bufferSize = bufferSize;
}
public void Capture()
{
if (_capture != null)
{
throw new InvalidOperationException("Already capturing stream!");
}
_capture = new StringWriter();
}
public string GetCapturedOutput()
get
{
return _capture?.GetStringBuilder()?.ToString();
}
}
public void ForwardTo(Action<string> write, Action<string> writeLine)
public StreamForwarder Capture()
{
if (writeLine == null)
{
throw new ArgumentNullException(nameof(writeLine));
ThrowIfCaptureSet();
_capture = new StringWriter();
return this;
}
if (_writeLine != null)
public StreamForwarder ForwardTo(Action<string> writeLine)
{
throw new InvalidOperationException("Already handling stream!");
}
_write = write;
ThrowIfNull(writeLine);
ThrowIfForwarderSet();
_writeLine = writeLine;
return this;
}
public Thread BeginRead(TextReader reader)
@ -57,64 +53,43 @@ namespace Microsoft.DotNet.Cli.Utils
public void Read(TextReader reader)
{
var bufferSize = 1;
int readCharacterCount;
char currentCharacter;
var buffer = new char[bufferSize];
_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
while ((readCharacterCount = reader.Read(buffer, 0, bufferSize)) > 0)
{
int n = _builder.Length;
if (n == 0)
{
return;
}
currentCharacter = buffer[0];
int offset = 0;
bool sawReturn = false;
for (int i = 0; i < n; i++)
if (currentCharacter == s_flushBuilderCharacter)
{
char c = _builder[i];
switch (c)
{
case '\r':
sawReturn = true;
continue;
case '\n':
WriteLine(_builder.ToString(offset, i - offset - (sawReturn ? 1 : 0)));
offset = i + 1;
break;
WriteBuilder();
}
sawReturn = false;
}
// If the buffer contains no line breaks and _write is
// supported, send the buffer content.
if (!sawReturn &&
(offset == 0) &&
((_write != null) || (_writeLine == null)))
else if (! s_ignoreCharacters.Contains(currentCharacter))
{
WriteRemainder();
}
else
{
_builder.Remove(0, offset);
_builder.Append(currentCharacter);
}
}
private void WriteRemainder()
// Flush anything else when the stream is closed
// Which should only happen if someone used console.Write
WriteBuilder();
}
private void WriteBuilder()
{
if (_builder.Length == 0)
{
return;
}
Write(_builder.ToString());
WriteLine(_builder.ToString());
_builder.Clear();
}
@ -124,26 +99,34 @@ namespace Microsoft.DotNet.Cli.Utils
{
_capture.WriteLine(str);
}
// If _write is supported, so is _writeLine.
if (_writeLine != null)
{
_writeLine(str);
}
}
private void Write(string str)
private void ThrowIfNull(object obj)
{
if (obj == null)
{
throw new ArgumentNullException(nameof(obj));
}
}
private void ThrowIfForwarderSet()
{
if (_writeLine != null)
{
throw new InvalidOperationException("WriteLine forwarder set previously");
}
}
private void ThrowIfCaptureSet()
{
if (_capture != null)
{
_capture.Write(str);
}
if (_write != null)
{
_write(str);
}
else if (_writeLine != null)
{
_writeLine(str);
throw new InvalidOperationException("Already capturing stream!");
}
}
}

View file

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

View file

@ -206,7 +206,7 @@ namespace Microsoft.DotNet.Tests.ArgumentForwarding
/// <returns></returns>
private string[] ParseReflectorOutput(string reflectorOutput)
{
return reflectorOutput.Split(',');
return reflectorOutput.TrimEnd('\r', '\n').Split(',');
}
/// <summary>

View file

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

View file

@ -10,109 +10,138 @@ using Xunit;
using Microsoft.DotNet.Cli.Utils;
using Microsoft.DotNet.ProjectModel;
using Microsoft.DotNet.Tools.Test.Utilities;
using Microsoft.Extensions.PlatformAbstractions;
using System.Threading;
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()
{
Console.WriteLine("Dummy Entrypoint");
}
[Fact]
public void Unbuffered()
public StreamForwarderTests()
{
Forward(4, true, "");
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");
_root = Temp.CreateDirectory();
}
[Fact]
public void LineBuffered()
public static IEnumerable<object[]> ForwardingTheoryVariations
{
Forward(4, false, "");
Forward(4, false, "123", "123\n");
Forward(4, false, "1234", "1234\n");
Forward(3, false, "123456789", "123456789\n");
Forward(4, false, "\r\n", "\n");
Forward(4, false, "\r\n34", "\n", "34\n");
Forward(4, false, "1\r\n4", "1\n", "4\n");
Forward(4, false, "12\r\n", "12\n");
Forward(4, false, "123\r\n", "123\n");
Forward(4, false, "1234\r\n", "1234\n");
Forward(3, false, "\r\n3456\r\n9", "\n", "3456\n", "9\n");
Forward(4, false, "\n", "\n");
Forward(4, false, "\n234", "\n", "234\n");
Forward(4, false, "1\n34", "1\n", "34\n");
Forward(4, false, "12\n4", "12\n", "4\n");
Forward(4, false, "123\n", "123\n");
Forward(4, false, "1234\n", "1234\n");
Forward(3, false, "\n23456\n89", "\n", "23456\n", "89\n");
get
{
return new[]
{
new object[] { "123", new string[]{"123"} },
new object[] { "123\n", new string[] {"123"} },
new object[] { "123\r\n", new string[] {"123"} },
new object[] { "1234\n5678", new string[] {"1234", "5678"} },
new object[] { "1234\r\n5678", new string[] {"1234", "5678"} },
new object[] { "1234\n5678\n", new string[] {"1234", "5678"} },
new object[] { "1234\r\n5678\r\n", new string[] {"1234", "5678"} },
new object[] { "1234\n5678\nabcdefghijklmnopqrstuvwxyz", new string[] {"1234", "5678", "abcdefghijklmnopqrstuvwxyz"} },
new object[] { "1234\r\n5678\r\nabcdefghijklmnopqrstuvwxyz", new string[] {"1234", "5678", "abcdefghijklmnopqrstuvwxyz"} },
new object[] { "1234\n5678\nabcdefghijklmnopqrstuvwxyz\n", new string[] {"1234", "5678", "abcdefghijklmnopqrstuvwxyz"} },
new object[] { "1234\r\n5678\r\nabcdefghijklmnopqrstuvwxyz\r\n", new string[] {"1234", "5678", "abcdefghijklmnopqrstuvwxyz"} }
};
}
}
private static void Forward(int bufferSize, bool unbuffered, string str, params string[] expectedWrites)
[Theory]
[InlineData("123")]
[InlineData("123\n")]
public void TestNoForwardingNoCapture(string inputStr)
{
var expectedCaptured = str.Replace("\r", "").Replace("\n", Environment.NewLine);
TestCapturingAndForwardingHelper(ForwardOptions.None, inputStr, null, new string[0]);
}
// No forwarding.
Forward(bufferSize, ForwardOptions.None, str, null, new string[0]);
[Theory]
[MemberData("ForwardingTheoryVariations")]
public void TestForwardingOnly(string inputStr, string[] expectedWrites)
{
for(int i = 0; i < expectedWrites.Length; ++i)
{
expectedWrites[i] += Environment.NewLine;
}
// Capture only.
Forward(bufferSize, ForwardOptions.Capture, str, expectedCaptured, new string[0]);
TestCapturingAndForwardingHelper(ForwardOptions.WriteLine, inputStr, null, expectedWrites);
}
var writeOptions = unbuffered ?
ForwardOptions.Write | ForwardOptions.WriteLine :
ForwardOptions.WriteLine;
[Theory]
[MemberData("ForwardingTheoryVariations")]
public void TestCaptureOnly(string inputStr, string[] expectedWrites)
{
for(int i = 0; i < expectedWrites.Length; ++i)
{
expectedWrites[i] += Environment.NewLine;
}
// Forward.
Forward(bufferSize, writeOptions, str, null, expectedWrites);
var expectedCaptured = string.Join("", expectedWrites);
// Forward and capture.
Forward(bufferSize, writeOptions | ForwardOptions.Capture, str, expectedCaptured, expectedWrites);
TestCapturingAndForwardingHelper(ForwardOptions.Capture, inputStr, expectedCaptured, new string[0]);
}
[Theory]
[MemberData("ForwardingTheoryVariations")]
public void TestCaptureAndForwardingTogether(string inputStr, string[] expectedWrites)
{
for(int i = 0; i < expectedWrites.Length; ++i)
{
expectedWrites[i] += Environment.NewLine;
}
var expectedCaptured = string.Join("", expectedWrites);
TestCapturingAndForwardingHelper(ForwardOptions.WriteLine | ForwardOptions.Capture, inputStr, expectedCaptured, expectedWrites);
}
private enum ForwardOptions
{
None = 0x0,
Capture = 0x1,
Write = 0x02,
WriteLine = 0x04,
WriteLine = 0x02,
}
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>();
if ((options & ForwardOptions.WriteLine) != 0)
{
forwarder.ForwardTo(
write: (options & ForwardOptions.Write) == 0 ? (Action<string>)null : writes.Add,
writeLine: s => writes.Add(s + "\n"));
forwarder.ForwardTo(writeLine: s => writes.Add(s + Environment.NewLine));
}
if ((options & ForwardOptions.Capture) != 0)
{
forwarder.Capture();
}
forwarder.Read(new StringReader(str));
Assert.Equal(expectedWrites, writes);
var captured = forwarder.GetCapturedOutput();
var captured = forwarder.CapturedOutput;
Assert.Equal(expectedCaptured, captured);
}
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;
}
}
}

View file

@ -22,5 +22,9 @@
}
},
"content": [
"../TestProjects/OutputStandardOutputAndError/*"
],
"testRunner": "xunit"
}

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": { }
}
}

View file

@ -62,7 +62,7 @@ namespace Microsoft.DotNet.Tools.Builder.Tests
Assert.False(File.Exists(lockFile));
buildResult = BuildProject(expectBuildFailure : true);
Assert.Contains("does not have a lock file", buildResult.StdOut);
Assert.Contains("does not have a lock file", buildResult.StdErr);
}
[Fact]

View file

@ -58,7 +58,8 @@ namespace Microsoft.DotNet.Tools.Compiler.Tests
var buildCmd = new BuildCommand(testProject, output: outputDir, framework: DefaultFramework);
var result = buildCmd.ExecuteWithCapturedOutput();
result.Should().Pass();
Assert.Contains("CA1018", result.StdOut);
Assert.Contains("CA1018", result.StdErr);
}
private void CopyProjectToTempDir(string projectDir, TempDirectory tempDir)