dotnet-installer/src/Microsoft.DotNet.Cli.Utils/Command.cs

353 lines
11 KiB
C#
Raw Normal View History

// 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;
using Microsoft.DotNet.Tools.Common;
using Microsoft.DotNet.ProjectModel;
using NuGet.Frameworks;
namespace Microsoft.DotNet.Cli.Utils
{
public class Command
{
private Process _process;
private StringWriter _stdOutCapture;
private StringWriter _stdErrCapture;
2015-11-01 16:21:10 -08:00
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,
RedirectStandardError = true,
RedirectStandardOutput = true
};
_process = new Process()
{
StartInfo = psi
};
}
public static Command Create(string executable, IEnumerable<string> args, NuGetFramework framework = null)
{
return Create(executable, string.Join(" ", args), framework);
}
public static Command Create(string executable, string args, NuGetFramework framework = null)
{
ResolveExecutablePath(ref executable, ref args, framework);
return new Command(executable, args);
}
private static void ResolveExecutablePath(ref string executable, ref string args, NuGetFramework framework = null)
{
executable =
ResolveExecutablePathFromProject(executable, framework) ??
ResolveExecutableFromPath(executable, ref args);
}
private static string ResolveExecutableFromPath(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;
}
}
2015-10-15 12:18:45 -07:00
// 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)
{
2015-10-15 12:18:45 -07:00
var comSpec = 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))
{
2015-10-15 12:56:07 -07:00
executable = (executable + " " + args).Replace("\"", "\\\"");
}
2015-10-15 12:56:07 -07:00
args = $"/C \"{executable}\"";
executable = comSpec;
}
return executable;
}
private static string ResolveExecutablePathFromProject(string executable, NuGetFramework framework)
{
if (framework == null) return null;
var projectRootPath = PathUtility.GetProjectRootPath();
if (projectRootPath == null) return null;
var commandName = Path.GetFileNameWithoutExtension(executable);
var projectContext = ProjectContext.Create(projectRootPath, framework);
var commandPackage = projectContext.LibraryManager.GetLibraries()
.Where(l => l.GetType() == typeof (PackageDescription))
.Select(l => l as PackageDescription)
.FirstOrDefault(p =>
{
var fileNames = p.Library.Files
.Select(Path.GetFileName)
.Where(n => Path.GetFileNameWithoutExtension(n) == commandName)
.ToList();
return fileNames.Contains(commandName + FileNameSuffixes.ExeSuffix) &&
fileNames.Contains(commandName + FileNameSuffixes.DynamicLib) &&
fileNames.Contains(commandName + FileNameSuffixes.Deps);
});
if (commandPackage == null) return null;
var commandPath = commandPackage.Library.Files
.First(f => Path.GetFileName(f) == commandName + FileNameSuffixes.ExeSuffix);
return Path.Combine(projectContext.PackagesDirectory, commandPackage.Path, commandPath);
2015-10-15 12:18:45 -07:00
}
private static bool ShouldUseCmd(string executable)
{
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{
2015-10-15 12:18:45 -07:00
var extension = Path.GetExtension(executable);
if (!string.IsNullOrEmpty(extension))
2015-10-15 12:18:45 -07:00
{
return !string.Equals(extension, ".exe", StringComparison.Ordinal);
2015-10-15 12:18:45 -07:00
}
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 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;
}
2015-10-15 12:18:45 -07:00
// Non-windows never uses cmd
return false;
}
public CommandResult Execute()
{
Reporter.Verbose.WriteLine($"Running {_process.StartInfo.FileName} {_process.StartInfo.Arguments}");
ThrowIfRunning();
_running = true;
_process.OutputDataReceived += (sender, args) =>
{
ProcessData(args.Data, _stdOutCapture, _stdOutForward, _stdOutHandler);
};
_process.ErrorDataReceived += (sender, args) =>
{
ProcessData(args.Data, _stdErrCapture, _stdErrForward, _stdErrHandler);
};
_process.EnableRaisingEvents = true;
#if DEBUG
2015-10-18 21:24:26 -07:00
var sw = Stopwatch.StartNew();
2015-11-01 16:21:10 -08:00
Reporter.Verbose.WriteLine($"> {FormatProcessInfo(_process.StartInfo)}".White());
#endif
_process.Start();
_process.BeginOutputReadLine();
_process.BeginErrorReadLine();
_process.WaitForExit();
var exitCode = _process.ExitCode;
#if DEBUG
2015-10-22 05:31:24 -07:00
var message = $"< {FormatProcessInfo(_process.StartInfo)} exited with {exitCode} in {sw.ElapsedMilliseconds} ms.";
2015-10-20 02:42:29 -07:00
if (exitCode == 0)
{
2015-11-01 16:21:10 -08:00
Reporter.Verbose.WriteLine(message.Green());
2015-10-20 02:42:29 -07:00
}
else
{
2015-11-01 16:21:10 -08:00
Reporter.Verbose.WriteLine(message.Red().Bold());
2015-10-20 02:42:29 -07:00
}
#endif
return new CommandResult(
exitCode,
_stdOutCapture?.GetStringBuilder()?.ToString(),
_stdErrCapture?.GetStringBuilder()?.ToString());
}
public Command WorkingDirectory(string projectDirectory)
{
_process.StartInfo.WorkingDirectory = projectDirectory;
return this;
}
2015-10-30 15:40:13 -07:00
public Command EnvironmentVariable(string name, string value)
{
2015-11-01 06:24:31 -08:00
_process.StartInfo.Environment[name] = value;
2015-10-30 15:40:13 -07:00
return this;
}
public Command CaptureStdOut()
{
ThrowIfRunning();
_stdOutCapture = new StringWriter();
return this;
}
public Command CaptureStdErr()
{
ThrowIfRunning();
_stdErrCapture = new StringWriter();
return this;
}
2015-11-01 16:21:10 -08:00
public Command ForwardStdOut(TextWriter to = null, bool onlyIfVerbose = false)
{
ThrowIfRunning();
2015-11-01 16:21:10 -08:00
if (!onlyIfVerbose || CommandContext.IsVerbose())
{
if (to == null)
{
_stdOutForward = Reporter.Output.WriteLine;
}
else
{
_stdOutForward = to.WriteLine;
}
}
return this;
}
2015-11-01 16:21:10 -08:00
public Command ForwardStdErr(TextWriter to = null, bool onlyIfVerbose = false)
{
ThrowIfRunning();
2015-11-01 16:21:10 -08:00
if (!onlyIfVerbose || CommandContext.IsVerbose())
{
if (to == null)
{
_stdErrForward = Reporter.Error.WriteLine;
}
else
{
_stdErrForward = to.WriteLine;
}
}
return this;
}
public Command OnOutputLine(Action<string> handler)
{
ThrowIfRunning();
if (_stdOutHandler != null)
{
throw new InvalidOperationException("Already handling stdout!");
}
_stdOutHandler = handler;
return this;
}
public Command OnErrorLine(Action<string> handler)
{
ThrowIfRunning();
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");
}
}
2015-11-01 16:21:10 -08:00
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)
{
2015-11-01 16:21:10 -08:00
forward(data);
}
if (handler != null)
{
handler(data);
}
}
}
}