| File: Command.cs | Web Access |
| Project: src\sdk\src\Cli\Microsoft.DotNet.Cli.Utils\Microsoft.DotNet.Cli.Utils.csproj (Microsoft.DotNet.Cli.Utils) |
// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using Microsoft.DotNet.Cli.Utils.Extensions; namespace Microsoft.DotNet.Cli.Utils; public class Command(Process? process, bool trimTrailingNewlines = false, IDictionary<string, string?>? customEnvironmentVariables = null) : ICommand { private readonly Process _process = process ?? throw new ArgumentNullException(nameof(process)); private readonly Dictionary<string, string?>? _customEnvironmentVariables = // copy the dictionary to avoid mutating the original customEnvironmentVariables == null ? null : new(customEnvironmentVariables); private StreamForwarder? _stdOut; private StreamForwarder? _stdErr; private bool _running = false; private readonly bool _trimTrailingNewlines = trimTrailingNewlines; public CommandResult Execute() { return Execute(null); } public CommandResult Execute(Action<Process>? processStarted) { Reporter.Verbose.WriteLine(string.Format( LocalizableStrings.RunningFileNameArguments, _process.StartInfo.FileName, _process.StartInfo.Arguments)); ThrowIfRunning(); _running = true; _process.EnableRaisingEvents = true; Stopwatch? sw = null; if (CommandLoggingContext.IsVerbose) { sw = Stopwatch.StartNew(); Reporter.Verbose.WriteLine($"> {Command.FormatProcessInfo(_process.StartInfo)}".White()); } using (var reaper = ProcessReaper.Create(_process)) { _process.Start(); processStarted?.Invoke(_process); reaper.NotifyProcessStarted(); Reporter.Verbose.WriteLine(string.Format( LocalizableStrings.ProcessId, _process.Id)); var taskOut = _stdOut?.BeginRead(_process.StandardOutput); var taskErr = _stdErr?.BeginRead(_process.StandardError); _process.WaitForExit(); taskOut?.Wait(); taskErr?.Wait(); } var exitCode = _process.ExitCode; if (CommandLoggingContext.IsVerbose) { Debug.Assert(sw is not null); var message = string.Format( LocalizableStrings.ProcessExitedWithCode, Command.FormatProcessInfo(_process.StartInfo), exitCode, sw.ElapsedMilliseconds); if (exitCode == 0) { Reporter.Verbose.WriteLine(message.Green()); } else { Reporter.Verbose.WriteLine(message.Red().Bold()); } } return new CommandResult( _process.StartInfo, exitCode, _stdOut?.CapturedOutput, _stdErr?.CapturedOutput); } public ICommand WorkingDirectory(string? projectDirectory) { _process.StartInfo.WorkingDirectory = projectDirectory; return this; } public ICommand EnvironmentVariable(string name, string? value) { _process.StartInfo.Environment[name] = value; _customEnvironmentVariables?[name] = value; return this; } public ICommand StandardOutputEncoding(Encoding encoding) { _process.StartInfo.StandardOutputEncoding = encoding; return this; } public ICommand CaptureStdOut() { ThrowIfRunning(); EnsureStdOut(); _stdOut?.Capture(_trimTrailingNewlines); return this; } public ICommand CaptureStdErr() { ThrowIfRunning(); EnsureStdErr(); _stdErr?.Capture(_trimTrailingNewlines); return this; } public ICommand ForwardStdOut(TextWriter? to = null, bool onlyIfVerbose = false, bool ansiPassThrough = true) { ThrowIfRunning(); if (!onlyIfVerbose || CommandLoggingContext.IsVerbose) { EnsureStdOut(); if (to == null) { _stdOut?.ForwardTo(writeLine: Reporter.Output.WriteLine); EnvironmentVariable(CommandLoggingContext.Variables.AnsiPassThru, ansiPassThrough.ToString()); } else { _stdOut?.ForwardTo(writeLine: to.WriteLine); } } return this; } public ICommand ForwardStdErr(TextWriter? to = null, bool onlyIfVerbose = false, bool ansiPassThrough = true) { ThrowIfRunning(); if (!onlyIfVerbose || CommandLoggingContext.IsVerbose) { EnsureStdErr(); if (to == null) { _stdErr?.ForwardTo(writeLine: Reporter.Error.WriteLine); EnvironmentVariable(CommandLoggingContext.Variables.AnsiPassThru, ansiPassThrough.ToString()); } else { _stdErr?.ForwardTo(writeLine: to.WriteLine); } } return this; } public ICommand OnOutputLine(Action<string> handler) { ThrowIfRunning(); EnsureStdOut(); _stdOut?.ForwardTo(writeLine: handler); return this; } public ICommand OnErrorLine(Action<string> handler) { ThrowIfRunning(); EnsureStdErr(); _stdErr?.ForwardTo(writeLine: handler); return this; } public string CommandName => _process.StartInfo.FileName; public string CommandArgs => _process.StartInfo.Arguments; public ProcessStartInfo StartInfo => _process.StartInfo; /// <summary> /// If set in the constructor, it's used to keep track of environment variables modified via <see cref="EnvironmentVariable"/> /// unlike <see cref="ProcessStartInfo.Environment"/> which includes all environment variables of the current process. /// </summary> public IReadOnlyDictionary<string, string?>? CustomEnvironmentVariables => _customEnvironmentVariables; public ICommand SetCommandArgs(string commandArgs) { _process.StartInfo.Arguments = commandArgs; return this; } private static string FormatProcessInfo(ProcessStartInfo info) { if (string.IsNullOrWhiteSpace(info.Arguments)) { return info.FileName; } return $"{info.FileName} {info.Arguments}"; } private void EnsureStdOut() { _stdOut ??= new StreamForwarder(); _process.StartInfo.RedirectStandardOutput = true; } private void EnsureStdErr() { _stdErr ??= new StreamForwarder(); _process.StartInfo.RedirectStandardError = true; } private void ThrowIfRunning([CallerMemberName] string? memberName = null) { if (_running) { throw new InvalidOperationException(string.Format( LocalizableStrings.UnableToInvokeMemberNameAfterCommand, memberName)); } } }