328 lines
9.9 KiB
C#
328 lines
9.9 KiB
C#
|
// 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.CompilerServices;
|
|||
|
using System.Runtime.InteropServices;
|
|||
|
|
|||
|
namespace Microsoft.DotNet.Cli.Build.Framework
|
|||
|
{
|
|||
|
public class Command
|
|||
|
{
|
|||
|
private Process _process;
|
|||
|
|
|||
|
private StringWriter _stdOutCapture;
|
|||
|
private StringWriter _stdErrCapture;
|
|||
|
|
|||
|
private Action<string> _stdOutForward;
|
|||
|
private Action<string> _stdErrForward;
|
|||
|
|
|||
|
private Action<string> _stdOutHandler;
|
|||
|
private Action<string> _stdErrHandler;
|
|||
|
|
|||
|
private bool _running = false;
|
|||
|
|
|||
|
private Command(string executable, string args)
|
|||
|
{
|
|||
|
// Set the things we need
|
|||
|
var psi = new ProcessStartInfo()
|
|||
|
{
|
|||
|
FileName = executable,
|
|||
|
Arguments = args
|
|||
|
};
|
|||
|
|
|||
|
_process = new Process()
|
|||
|
{
|
|||
|
StartInfo = psi
|
|||
|
};
|
|||
|
}
|
|||
|
|
|||
|
public static Command Create(string executable, params string[] args)
|
|||
|
{
|
|||
|
return Create(executable, ArgumentEscaper.EscapeAndConcatenateArgArrayForProcessStart(args));
|
|||
|
}
|
|||
|
|
|||
|
public static Command Create(string executable, IEnumerable<string> args)
|
|||
|
{
|
|||
|
return Create(executable, ArgumentEscaper.EscapeAndConcatenateArgArrayForProcessStart(args));
|
|||
|
}
|
|||
|
|
|||
|
public static Command Create(string executable, string args)
|
|||
|
{
|
|||
|
ResolveExecutablePath(ref executable, ref args);
|
|||
|
|
|||
|
return new Command(executable, args);
|
|||
|
}
|
|||
|
|
|||
|
private static void ResolveExecutablePath(ref string executable, ref string args)
|
|||
|
{
|
|||
|
foreach (string suffix in Constants.RunnableSuffixes)
|
|||
|
{
|
|||
|
var fullExecutable = Path.GetFullPath(Path.Combine(
|
|||
|
AppContext.BaseDirectory, executable + suffix));
|
|||
|
|
|||
|
if (File.Exists(fullExecutable))
|
|||
|
{
|
|||
|
executable = fullExecutable;
|
|||
|
|
|||
|
// In priority order we've found the best runnable extension, so break.
|
|||
|
break;
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
// On Windows, we want to avoid using "cmd" if possible (it mangles the colors, and a bunch of other things)
|
|||
|
// So, do a quick path search to see if we can just directly invoke it
|
|||
|
var useCmd = ShouldUseCmd(executable);
|
|||
|
|
|||
|
if (useCmd)
|
|||
|
{
|
|||
|
var comSpec = System.Environment.GetEnvironmentVariable("ComSpec");
|
|||
|
|
|||
|
// cmd doesn't like "foo.exe ", so we need to ensure that if
|
|||
|
// args is empty, we just run "foo.exe"
|
|||
|
if (!string.IsNullOrEmpty(args))
|
|||
|
{
|
|||
|
executable = (executable + " " + args).Replace("\"", "\\\"");
|
|||
|
}
|
|||
|
args = $"/C \"{executable}\"";
|
|||
|
executable = comSpec;
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
private static bool ShouldUseCmd(string executable)
|
|||
|
{
|
|||
|
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
|
|||
|
{
|
|||
|
var extension = Path.GetExtension(executable);
|
|||
|
if (!string.IsNullOrEmpty(extension))
|
|||
|
{
|
|||
|
return !string.Equals(extension, ".exe", StringComparison.Ordinal);
|
|||
|
}
|
|||
|
else if (executable.Contains(Path.DirectorySeparatorChar))
|
|||
|
{
|
|||
|
// It's a relative path without an extension
|
|||
|
if (File.Exists(executable + ".exe"))
|
|||
|
{
|
|||
|
// It refers to an exe!
|
|||
|
return false;
|
|||
|
}
|
|||
|
}
|
|||
|
else
|
|||
|
{
|
|||
|
// Search the path to see if we can find it
|
|||
|
foreach (var path in System.Environment.GetEnvironmentVariable("PATH").Split(Path.PathSeparator))
|
|||
|
{
|
|||
|
var candidate = Path.Combine(path, executable + ".exe");
|
|||
|
if (File.Exists(candidate))
|
|||
|
{
|
|||
|
// We found an exe!
|
|||
|
return false;
|
|||
|
}
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
// It's a non-exe :(
|
|||
|
return true;
|
|||
|
}
|
|||
|
|
|||
|
// Non-windows never uses cmd
|
|||
|
return false;
|
|||
|
}
|
|||
|
|
|||
|
public Command Environment(IDictionary<string, string> env)
|
|||
|
{
|
|||
|
foreach(var item in env)
|
|||
|
{
|
|||
|
_process.StartInfo.Environment[item.Key] = item.Value;
|
|||
|
}
|
|||
|
return this;
|
|||
|
}
|
|||
|
|
|||
|
public CommandResult Execute()
|
|||
|
{
|
|||
|
ThrowIfRunning();
|
|||
|
_running = true;
|
|||
|
|
|||
|
if (_process.StartInfo.RedirectStandardOutput)
|
|||
|
{
|
|||
|
_process.OutputDataReceived += (sender, args) =>
|
|||
|
{
|
|||
|
ProcessData(args.Data, _stdOutCapture, _stdOutForward, _stdOutHandler);
|
|||
|
};
|
|||
|
}
|
|||
|
|
|||
|
if (_process.StartInfo.RedirectStandardError)
|
|||
|
{
|
|||
|
_process.ErrorDataReceived += (sender, args) =>
|
|||
|
{
|
|||
|
ProcessData(args.Data, _stdErrCapture, _stdErrForward, _stdErrHandler);
|
|||
|
};
|
|||
|
}
|
|||
|
|
|||
|
_process.EnableRaisingEvents = true;
|
|||
|
|
|||
|
var sw = Stopwatch.StartNew();
|
|||
|
BuildReporter.BeginSection("EXEC", FormatProcessInfo(_process.StartInfo));
|
|||
|
|
|||
|
_process.Start();
|
|||
|
|
|||
|
if (_process.StartInfo.RedirectStandardOutput)
|
|||
|
{
|
|||
|
_process.BeginOutputReadLine();
|
|||
|
}
|
|||
|
|
|||
|
if (_process.StartInfo.RedirectStandardError)
|
|||
|
{
|
|||
|
_process.BeginErrorReadLine();
|
|||
|
}
|
|||
|
|
|||
|
_process.WaitForExit();
|
|||
|
|
|||
|
var exitCode = _process.ExitCode;
|
|||
|
|
|||
|
var message = $"{FormatProcessInfo(_process.StartInfo)} exited with {exitCode}";
|
|||
|
if (exitCode == 0)
|
|||
|
{
|
|||
|
BuildReporter.EndSection("EXEC", message.Green(), success: true);
|
|||
|
}
|
|||
|
else
|
|||
|
{
|
|||
|
BuildReporter.EndSection("EXEC", message.Red().Bold(), success: false);
|
|||
|
}
|
|||
|
|
|||
|
return new CommandResult(
|
|||
|
_process.StartInfo,
|
|||
|
exitCode,
|
|||
|
_stdOutCapture?.GetStringBuilder()?.ToString(),
|
|||
|
_stdErrCapture?.GetStringBuilder()?.ToString());
|
|||
|
}
|
|||
|
|
|||
|
public Command WorkingDirectory(string projectDirectory)
|
|||
|
{
|
|||
|
_process.StartInfo.WorkingDirectory = projectDirectory;
|
|||
|
return this;
|
|||
|
}
|
|||
|
|
|||
|
public Command EnvironmentVariable(string name, string value)
|
|||
|
{
|
|||
|
_process.StartInfo.Environment[name] = value;
|
|||
|
return this;
|
|||
|
}
|
|||
|
|
|||
|
public Command CaptureStdOut()
|
|||
|
{
|
|||
|
ThrowIfRunning();
|
|||
|
_process.StartInfo.RedirectStandardOutput = true;
|
|||
|
_stdOutCapture = new StringWriter();
|
|||
|
return this;
|
|||
|
}
|
|||
|
|
|||
|
public Command CaptureStdErr()
|
|||
|
{
|
|||
|
ThrowIfRunning();
|
|||
|
_process.StartInfo.RedirectStandardError = true;
|
|||
|
_stdErrCapture = new StringWriter();
|
|||
|
return this;
|
|||
|
}
|
|||
|
|
|||
|
public Command ForwardStdOut(TextWriter to = null)
|
|||
|
{
|
|||
|
ThrowIfRunning();
|
|||
|
_process.StartInfo.RedirectStandardOutput = true;
|
|||
|
if (to == null)
|
|||
|
{
|
|||
|
_stdOutForward = Reporter.Output.WriteLine;
|
|||
|
}
|
|||
|
else
|
|||
|
{
|
|||
|
_stdOutForward = to.WriteLine;
|
|||
|
}
|
|||
|
return this;
|
|||
|
}
|
|||
|
|
|||
|
public Command ForwardStdErr(TextWriter to = null)
|
|||
|
{
|
|||
|
ThrowIfRunning();
|
|||
|
_process.StartInfo.RedirectStandardError = true;
|
|||
|
if (to == null)
|
|||
|
{
|
|||
|
_stdErrForward = Reporter.Error.WriteLine;
|
|||
|
}
|
|||
|
else
|
|||
|
{
|
|||
|
_stdErrForward = to.WriteLine;
|
|||
|
}
|
|||
|
return this;
|
|||
|
}
|
|||
|
|
|||
|
public Command OnOutputLine(Action<string> handler)
|
|||
|
{
|
|||
|
ThrowIfRunning();
|
|||
|
_process.StartInfo.RedirectStandardOutput = true;
|
|||
|
if (_stdOutHandler != null)
|
|||
|
{
|
|||
|
throw new InvalidOperationException("Already handling stdout!");
|
|||
|
}
|
|||
|
_stdOutHandler = handler;
|
|||
|
return this;
|
|||
|
}
|
|||
|
|
|||
|
public Command OnErrorLine(Action<string> handler)
|
|||
|
{
|
|||
|
ThrowIfRunning();
|
|||
|
_process.StartInfo.RedirectStandardError = true;
|
|||
|
if (_stdErrHandler != null)
|
|||
|
{
|
|||
|
throw new InvalidOperationException("Already handling stderr!");
|
|||
|
}
|
|||
|
_stdErrHandler = handler;
|
|||
|
return this;
|
|||
|
}
|
|||
|
|
|||
|
private string FormatProcessInfo(ProcessStartInfo info)
|
|||
|
{
|
|||
|
if (string.IsNullOrWhiteSpace(info.Arguments))
|
|||
|
{
|
|||
|
return info.FileName;
|
|||
|
}
|
|||
|
|
|||
|
return info.FileName + " " + info.Arguments;
|
|||
|
}
|
|||
|
|
|||
|
private void ThrowIfRunning([CallerMemberName] string memberName = null)
|
|||
|
{
|
|||
|
if (_running)
|
|||
|
{
|
|||
|
throw new InvalidOperationException($"Unable to invoke {memberName} after the command has been run");
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
private void ProcessData(string data, StringWriter capture, Action<string> forward, Action<string> handler)
|
|||
|
{
|
|||
|
if (data == null)
|
|||
|
{
|
|||
|
return;
|
|||
|
}
|
|||
|
|
|||
|
if (capture != null)
|
|||
|
{
|
|||
|
capture.WriteLine(data);
|
|||
|
}
|
|||
|
|
|||
|
if (forward != null)
|
|||
|
{
|
|||
|
forward(data);
|
|||
|
}
|
|||
|
|
|||
|
if (handler != null)
|
|||
|
{
|
|||
|
handler(data);
|
|||
|
}
|
|||
|
}
|
|||
|
}
|
|||
|
}
|