Invoke compile-csc in-proc

Since processes are expensive, suppress spawning a new process when dotnet-compile is invoking dotnet-compile-csc.
This commit is contained in:
Eric Erhardt 2016-04-14 23:33:41 -05:00
parent 37f00f24e9
commit 6afc2ca813
9 changed files with 508 additions and 53 deletions

View file

@ -0,0 +1,81 @@
// 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.Collections.Concurrent;
using System.IO;
using System.Threading;
namespace Microsoft.DotNet.Cli.Utils
{
/// <summary>
/// An in-memory stream that will block any read calls until something was written to it.
/// </summary>
public sealed class BlockingMemoryStream : Stream
{
private readonly BlockingCollection<byte[]> _buffers = new BlockingCollection<byte[]>();
private ArraySegment<byte> _remaining;
public override void Write(byte[] buffer, int offset, int count)
{
byte[] tmp = new byte[count];
Buffer.BlockCopy(buffer, offset, tmp, 0, count);
_buffers.Add(tmp);
}
public override int Read(byte[] buffer, int offset, int count)
{
if (count == 0)
{
return 0;
}
if (_remaining.Count == 0)
{
byte[] tmp;
if (!_buffers.TryTake(out tmp, Timeout.Infinite) || tmp.Length == 0)
{
return 0;
}
_remaining = new ArraySegment<byte>(tmp, 0, tmp.Length);
}
if (_remaining.Count <= count)
{
count = _remaining.Count;
Buffer.BlockCopy(_remaining.Array, _remaining.Offset, buffer, offset, count);
_remaining = default(ArraySegment<byte>);
}
else
{
Buffer.BlockCopy(_remaining.Array, _remaining.Offset, buffer, offset, count);
_remaining = new ArraySegment<byte>(_remaining.Array, _remaining.Offset + count, _remaining.Count - count);
}
return count;
}
public void DoneWriting()
{
_buffers.CompleteAdding();
}
protected override void Dispose(bool disposing)
{
if (disposing)
{
_buffers.Dispose();
}
base.Dispose(disposing);
}
public override bool CanRead => true;
public override bool CanSeek => false;
public override bool CanWrite => true;
public override long Length { get { throw new NotImplementedException(); } }
public override long Position { get { throw new NotImplementedException(); } set { throw new NotImplementedException(); } }
public override void Flush() { }
public override long Seek(long offset, SeekOrigin origin) { throw new NotImplementedException(); }
public override void SetLength(long value) { throw new NotImplementedException(); }
}
}

View file

@ -0,0 +1,141 @@
// 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.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Threading;
namespace Microsoft.DotNet.Cli.Utils
{
/// <summary>
/// A Command that is capable of running in the current process.
/// </summary>
public class BuiltInCommand : ICommand
{
private readonly IEnumerable<string> _commandArgs;
private readonly Func<string[], int> _builtInCommand;
private readonly StreamForwarder _stdOut;
private readonly StreamForwarder _stdErr;
public string CommandName { get; }
public string CommandArgs => string.Join(" ", _commandArgs);
public BuiltInCommand(string commandName, IEnumerable<string> commandArgs, Func<string[], int> builtInCommand)
{
CommandName = commandName;
_commandArgs = commandArgs;
_builtInCommand = builtInCommand;
_stdOut = new StreamForwarder();
_stdErr = new StreamForwarder();
}
public CommandResult Execute()
{
TextWriter originalConsoleOut = Console.Out;
TextWriter originalConsoleError = Console.Error;
try
{
// redirecting the standard out and error so we can forward
// the output to the caller
using (BlockingMemoryStream outStream = new BlockingMemoryStream())
using (BlockingMemoryStream errorStream = new BlockingMemoryStream())
{
Console.SetOut(new StreamWriter(outStream) { AutoFlush = true });
Console.SetError(new StreamWriter(errorStream) { AutoFlush = true });
// Reset the Reporters to the new Console Out and Error.
Reporter.Reset();
Thread threadOut = _stdOut.BeginRead(new StreamReader(outStream));
Thread threadErr = _stdErr.BeginRead(new StreamReader(errorStream));
int exitCode = _builtInCommand(_commandArgs.ToArray());
outStream.DoneWriting();
errorStream.DoneWriting();
threadOut.Join();
threadErr.Join();
// fake out a ProcessStartInfo using the Muxer command name, since this is a built-in command
ProcessStartInfo startInfo = new ProcessStartInfo(new Muxer().MuxerPath, $"{CommandName} {CommandArgs}");
return new CommandResult(startInfo, exitCode, null, null);
}
}
finally
{
Console.SetOut(originalConsoleOut);
Console.SetError(originalConsoleError);
Reporter.Reset();
}
}
public ICommand OnOutputLine(Action<string> handler)
{
if (handler == null)
{
throw new ArgumentNullException(nameof(handler));
}
_stdOut.ForwardTo(writeLine: handler);
return this;
}
public ICommand OnErrorLine(Action<string> handler)
{
if (handler == null)
{
throw new ArgumentNullException(nameof(handler));
}
_stdErr.ForwardTo(writeLine: handler);
return this;
}
public CommandResolutionStrategy ResolutionStrategy
{
get
{
throw new NotImplementedException();
}
}
public ICommand CaptureStdErr()
{
throw new NotImplementedException();
}
public ICommand CaptureStdOut()
{
throw new NotImplementedException();
}
public ICommand EnvironmentVariable(string name, string value)
{
throw new NotImplementedException();
}
public ICommand ForwardStdErr(TextWriter to = null, bool onlyIfVerbose = false, bool ansiPassThrough = true)
{
throw new NotImplementedException();
}
public ICommand ForwardStdOut(TextWriter to = null, bool onlyIfVerbose = false, bool ansiPassThrough = true)
{
throw new NotImplementedException();
}
public ICommand WorkingDirectory(string projectDirectory)
{
throw new NotImplementedException();
}
}
}

View file

@ -14,16 +14,34 @@ namespace Microsoft.DotNet.Cli.Utils
private readonly AnsiConsole _console;
static Reporter()
{
Reset();
}
private Reporter(AnsiConsole console)
{
_console = console;
}
public static Reporter Output { get; } = new Reporter(AnsiConsole.GetOutput());
public static Reporter Error { get; } = new Reporter(AnsiConsole.GetError());
public static Reporter Verbose { get; } = CommandContext.IsVerbose() ?
new Reporter(AnsiConsole.GetOutput()) :
NullReporter;
public static Reporter Output { get; private set; }
public static Reporter Error { get; private set; }
public static Reporter Verbose { get; private set; }
/// <summary>
/// Resets the Reporters to write to the current Console Out/Error.
/// </summary>
public static void Reset()
{
lock (_lock)
{
Output = new Reporter(AnsiConsole.GetOutput());
Error = new Reporter(AnsiConsole.GetError());
Verbose = CommandContext.IsVerbose() ?
new Reporter(AnsiConsole.GetOutput()) :
NullReporter;
}
}
public void WriteLine(string message)
{

View file

@ -1,10 +1,13 @@
// 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.Collections.Generic;
using System.Diagnostics;
using Microsoft.DotNet.Cli.Utils;
using NuGet.Frameworks;
namespace Microsoft.DotNet.Cli.Utils
namespace Microsoft.DotNet.Cli
{
public class DotNetCommandFactory : ICommandFactory
{
@ -14,6 +17,15 @@ namespace Microsoft.DotNet.Cli.Utils
NuGetFramework framework = null,
string configuration = Constants.DefaultConfiguration)
{
Func<string[], int> builtInCommand;
if (Program.TryGetBuiltInCommand(commandName, out builtInCommand))
{
Debug.Assert(framework == null, "BuiltInCommand doesn't support the 'framework' argument.");
Debug.Assert(configuration == Constants.DefaultConfiguration, "BuiltInCommand doesn't support the 'configuration' argument.");
return new BuiltInCommand(commandName, args, builtInCommand);
}
return Command.CreateDotNet(commandName, args, framework, configuration);
}
}

View file

@ -6,26 +6,37 @@ using System.Collections.Generic;
using System.IO;
using System.Linq;
using Microsoft.DotNet.Cli.Utils;
using Microsoft.DotNet.Tools.Test;
using Microsoft.Extensions.PlatformAbstractions;
using NuGet.Frameworks;
using Microsoft.DotNet.ProjectModel.Server;
using Microsoft.DotNet.Tools.Build;
using Microsoft.DotNet.Tools.Compiler;
using Microsoft.DotNet.Tools.Compiler.Csc;
using Microsoft.DotNet.Tools.Compiler.Native;
using Microsoft.DotNet.Tools.Help;
using Microsoft.DotNet.Tools.New;
using Microsoft.DotNet.Tools.Publish;
using Microsoft.DotNet.Tools.Repl;
using Microsoft.DotNet.Tools.Resgen;
using Microsoft.DotNet.Tools.Restore;
using Microsoft.DotNet.Tools.Run;
using Microsoft.DotNet.Tools.Test;
using Microsoft.Extensions.PlatformAbstractions;
using NuGet.Frameworks;
namespace Microsoft.DotNet.Cli
{
public class Program
{
private static Dictionary<string, Func<string[], int>> s_builtIns = new Dictionary<string, Func<string[], int>>
{
["build"] = BuildCommand.Run,
["compile-csc"] = CompileCscCommand.Run,
["help"] = HelpCommand.Run,
["new"] = NewCommand.Run,
["pack"] = PackCommand.Run,
["projectmodel-server"] = ProjectModelServerCommand.Run,
["publish"] = PublishCommand.Run,
["restore"] = RestoreCommand.Run,
["run"] = RunCommand.Run,
["test"] = TestCommand.Run
};
public static int Main(string[] args)
{
DebugHelper.HandleDebugSwitch(ref args);
@ -100,23 +111,9 @@ namespace Microsoft.DotNet.Cli
command = "help";
}
var builtIns = new Dictionary<string, Func<string[], int>>
{
["build"] = BuildCommand.Run,
["compile-csc"] = CompileCscCommand.Run,
["help"] = HelpCommand.Run,
["new"] = NewCommand.Run,
["pack"] = PackCommand.Run,
["projectmodel-server"] = ProjectModelServerCommand.Run,
["publish"] = PublishCommand.Run,
["restore"] = RestoreCommand.Run,
["run"] = RunCommand.Run,
["test"] = TestCommand.Run
};
int exitCode;
Func<string[], int> builtIn;
if (builtIns.TryGetValue(command, out builtIn))
if (s_builtIns.TryGetValue(command, out builtIn))
{
exitCode = builtIn(appArgs.ToArray());
}
@ -141,6 +138,11 @@ namespace Microsoft.DotNet.Cli
}
internal static bool TryGetBuiltInCommand(string commandName, out Func<string[], int> builtInCommand)
{
return s_builtIns.TryGetValue(commandName, out builtInCommand);
}
private static void PrintVersion()
{
Reporter.Output.WriteLine(Product.Version);

View file

@ -1,11 +1,14 @@
// 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.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Runtime.InteropServices;
using System.Text;
using Microsoft.DotNet.Cli;
using Microsoft.DotNet.Cli.Compiler.Common;
using Microsoft.DotNet.Cli.Utils;
using Microsoft.DotNet.ProjectModel;
@ -81,7 +84,7 @@ namespace Microsoft.DotNet.Tools.Compiler
var compilationOptions = context.ResolveCompilationOptions(args.ConfigValue);
// Set default platform if it isn't already set and we're on desktop
if(compilationOptions.EmitEntryPoint == true && string.IsNullOrEmpty(compilationOptions.Platform) && context.TargetFramework.IsDesktop())
if (compilationOptions.EmitEntryPoint == true && string.IsNullOrEmpty(compilationOptions.Platform) && context.TargetFramework.IsDesktop())
{
// See https://github.com/dotnet/cli/issues/2428 for more details.
compilationOptions.Platform = RuntimeInformation.ProcessArchitecture == Architecture.X64 ?
@ -181,31 +184,15 @@ namespace Microsoft.DotNet.Tools.Compiler
_scriptRunner.RunScripts(context, ScriptNames.PreCompile, contextVariables);
var result = _commandFactory.Create($"compile-{compilerName}", new[] { "@" + $"{rsp}" })
.OnErrorLine(line =>
{
var diagnostic = ParseDiagnostic(context.ProjectDirectory, line);
if (diagnostic != null)
{
diagnostics.Add(diagnostic);
}
else
{
Reporter.Error.WriteLine(line);
}
})
.OnOutputLine(line =>
{
var diagnostic = ParseDiagnostic(context.ProjectDirectory, line);
if (diagnostic != null)
{
diagnostics.Add(diagnostic);
}
else
{
Reporter.Output.WriteLine(line);
}
}).Execute();
// Cache the reporters before invoking the command in case it is a built-in command, which replaces
// the static Reporter instances.
Reporter errorReporter = Reporter.Error;
Reporter outputReporter = Reporter.Output;
CommandResult result = _commandFactory.Create($"compile-{compilerName}", new[] { $"@{rsp}" })
.OnErrorLine(line => HandleCompilerOutputLine(line, context, diagnostics, errorReporter))
.OnOutputLine(line => HandleCompilerOutputLine(line, context, diagnostics, outputReporter))
.Execute();
// Run post-compile event
contextVariables["compile:CompilerExitCode"] = result.ExitCode.ToString();
@ -225,5 +212,18 @@ namespace Microsoft.DotNet.Tools.Compiler
return PrintSummary(diagnostics, sw, success);
}
private static void HandleCompilerOutputLine(string line, ProjectContext context, List<DiagnosticMessage> diagnostics, Reporter reporter)
{
var diagnostic = ParseDiagnostic(context.ProjectDirectory, line);
if (diagnostic != null)
{
diagnostics.Add(diagnostic);
}
else
{
reporter.WriteLine(line);
}
}
}
}

View file

@ -2,6 +2,7 @@
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
using System;
using Microsoft.DotNet.Cli;
using Microsoft.DotNet.Cli.Utils;
namespace Microsoft.DotNet.Tools.Compiler

View file

@ -0,0 +1,111 @@
// 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.Threading;
using Microsoft.DotNet.Tools.Test.Utilities;
using Xunit;
namespace Microsoft.DotNet.Cli.Utils
{
public class BlockingMemoryStreamTests : TestBase
{
/// <summary>
/// Tests reading a bigger buffer than what is available.
/// </summary>
[Fact]
public void ReadBiggerBuffer()
{
using (var stream = new BlockingMemoryStream())
{
stream.Write(new byte[] { 1, 2, 3 }, 0, 3);
byte[] buffer = new byte[10];
int count = stream.Read(buffer, 0, buffer.Length);
Assert.Equal(3, count);
Assert.Equal(1, buffer[0]);
Assert.Equal(2, buffer[1]);
Assert.Equal(3, buffer[2]);
}
}
/// <summary>
/// Tests reading smaller buffers than what is available.
/// </summary>
[Fact]
public void ReadSmallerBuffers()
{
using (var stream = new BlockingMemoryStream())
{
stream.Write(new byte[] { 1, 2, 3, 4 }, 0, 4);
stream.Write(new byte[] { 5, 6, 7, 8, 9 }, 0, 5);
byte[] buffer = new byte[3];
int count = stream.Read(buffer, 0, buffer.Length);
Assert.Equal(3, count);
Assert.Equal(1, buffer[0]);
Assert.Equal(2, buffer[1]);
Assert.Equal(3, buffer[2]);
count = stream.Read(buffer, 0, buffer.Length);
Assert.Equal(1, count);
Assert.Equal(4, buffer[0]);
count = stream.Read(buffer, 0, buffer.Length);
Assert.Equal(3, count);
Assert.Equal(5, buffer[0]);
Assert.Equal(6, buffer[1]);
Assert.Equal(7, buffer[2]);
count = stream.Read(buffer, 0, buffer.Length);
Assert.Equal(2, count);
Assert.Equal(8, buffer[0]);
Assert.Equal(9, buffer[1]);
}
}
/// <summary>
/// Tests reading will block until the stream is written to.
/// </summary>
[Fact]
public void TestReadBlocksUntilWrite()
{
using (var stream = new BlockingMemoryStream())
{
ManualResetEvent readerThreadExecuting = new ManualResetEvent(false);
bool readerThreadSuccessful = false;
Thread readerThread = new Thread(() =>
{
byte[] buffer = new byte[10];
readerThreadExecuting.Set();
int count = stream.Read(buffer, 0, buffer.Length);
Assert.Equal(3, count);
Assert.Equal(1, buffer[0]);
Assert.Equal(2, buffer[1]);
Assert.Equal(3, buffer[2]);
readerThreadSuccessful = true;
});
readerThread.IsBackground = true;
readerThread.Start();
// ensure the thread is executing
readerThreadExecuting.WaitOne();
Assert.True(readerThread.IsAlive);
// give it a little while to ensure it is blocking
Thread.Sleep(10);
Assert.True(readerThread.IsAlive);
stream.Write(new byte[] { 1, 2, 3 }, 0, 3);
Assert.True(readerThread.Join(1000));
Assert.True(readerThreadSuccessful);
}
}
}
}

View file

@ -0,0 +1,89 @@
// 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.Linq;
using Microsoft.DotNet.Tools.Test.Utilities;
using Xunit;
namespace Microsoft.DotNet.Cli.Utils
{
public class BuiltInCommandTests : TestBase
{
/// <summary>
/// Tests that BuiltInCommand.Execute returns the correct exit code and a
/// valid StartInfo FileName and Arguments.
/// </summary>
[Fact]
public void TestExecute()
{
Func<string[], int> testCommand = args => args.Length;
string[] testCommandArgs = new[] { "1", "2" };
var builtInCommand = new BuiltInCommand("fakeCommand", testCommandArgs, testCommand);
CommandResult result = builtInCommand.Execute();
Assert.Equal(testCommandArgs.Length, result.ExitCode);
Assert.Equal(new Muxer().MuxerPath, result.StartInfo.FileName);
Assert.Equal("fakeCommand 1 2", result.StartInfo.Arguments);
}
/// <summary>
/// Tests that BuiltInCommand.Execute raises the OnOutputLine and OnErrorLine
/// the correct number of times and with the correct content.
/// </summary>
[Fact]
public void TestOnOutputLines()
{
int exitCode = 29;
Func<string[], int> testCommand = args =>
{
Console.Out.Write("first");
Console.Out.WriteLine("second");
Console.Out.WriteLine("third");
Console.Error.WriteLine("fourth");
Console.Error.WriteLine("fifth");
return exitCode;
};
int onOutputLineCallCount = 0;
int onErrorLineCallCount = 0;
CommandResult result = new BuiltInCommand("fakeCommand", Enumerable.Empty<string>(), testCommand)
.OnOutputLine(line =>
{
onOutputLineCallCount++;
if (onOutputLineCallCount == 1)
{
Assert.Equal($"firstsecond", line);
}
else
{
Assert.Equal($"third", line);
}
})
.OnErrorLine(line =>
{
onErrorLineCallCount++;
if (onErrorLineCallCount == 1)
{
Assert.Equal($"fourth", line);
}
else
{
Assert.Equal($"fifth", line);
}
})
.Execute();
Assert.Equal(exitCode, result.ExitCode);
Assert.Equal(2, onOutputLineCallCount);
Assert.Equal(2, onErrorLineCallCount);
}
}
}