File: Command.cs
Web Access
Project: Microsoft.Arcade.Common.csproj (Microsoft.Arcade.Common)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System;
using System.Diagnostics;
using System.IO;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;

namespace Microsoft.Arcade.Common;

public class Command : ICommand
{
    public static readonly string[] RunnableSuffixes = RuntimeInformation.IsOSPlatform(OSPlatform.Windows)
        ? new string[] { ".exe", ".cmd", ".bat" }
        : new string[] { string.Empty };

    private readonly Process _process;

    private Action<string> _statusForward;

    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 bool _quietBuildReporter = false;

    internal Command(string executable, string args)
    {
        // Set the things we need
        var psi = new ProcessStartInfo()
        {
            FileName = executable,
            Arguments = args
        };

        _process = new Process()
        {
            StartInfo = psi
        };
    }

    public ICommand QuietBuildReporter()
    {
        _quietBuildReporter = true;
        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;

        if (_process.StartInfo.RedirectStandardOutput ||
            _process.StartInfo.RedirectStandardInput ||
            _process.StartInfo.RedirectStandardError)
        {
            _process.StartInfo.UseShellExecute = false;
        }

        var sw = Stopwatch.StartNew();
        ReportExecBegin();

        _process.Start();

        if (_process.StartInfo.RedirectStandardOutput)
        {
            _process.BeginOutputReadLine();
        }

        if (_process.StartInfo.RedirectStandardError)
        {
            _process.BeginErrorReadLine();
        }

        _process.WaitForExit();

        var exitCode = _process.ExitCode;

        ReportExecEnd(exitCode);

        return new CommandResult(
            _process.StartInfo,
            exitCode,
            _stdOutCapture?.GetStringBuilder()?.ToString(),
            _stdErrCapture?.GetStringBuilder()?.ToString());
    }

    public ICommand WorkingDirectory(string projectDirectory)
    {
        _process.StartInfo.WorkingDirectory = projectDirectory;
        return this;
    }

    public ICommand EnvironmentVariable(string name, string value)
    {
        _process.StartInfo.Environment[name] = value;
        _process.StartInfo.UseShellExecute = false;
        return this;
    }

    public ICommand ForwardStatus(TextWriter to = null)
    {
        ThrowIfRunning();
        if (to == null)
        {
            _statusForward = Console.WriteLine;
        }
        else
        {
            _statusForward = to.WriteLine;
        }
        return this;
    }

    public ICommand CaptureStdOut()
    {
        ThrowIfRunning();
        _process.StartInfo.RedirectStandardOutput = true;
        _stdOutCapture = new StringWriter();
        return this;
    }

    public ICommand CaptureStdErr()
    {
        ThrowIfRunning();
        _process.StartInfo.RedirectStandardError = true;
        _stdErrCapture = new StringWriter();
        return this;
    }

    public ICommand ForwardStdOut(TextWriter to = null)
    {
        ThrowIfRunning();
        _process.StartInfo.RedirectStandardOutput = true;
        if (to == null)
        {
            _stdOutForward = Console.WriteLine;
        }
        else
        {
            _stdOutForward = to.WriteLine;
        }
        return this;
    }

    public ICommand ForwardStdErr(TextWriter to = null)
    {
        ThrowIfRunning();
        _process.StartInfo.RedirectStandardError = true;
        if (to == null)
        {
            _stdErrForward = Console.WriteLine;
        }
        else
        {
            _stdErrForward = to.WriteLine;
        }
        return this;
    }

    public ICommand OnOutputLine(Action<string> handler)
    {
        ThrowIfRunning();
        _process.StartInfo.RedirectStandardOutput = true;
        if (_stdOutHandler != null)
        {
            throw new InvalidOperationException("Already handling stdout!");
        }
        _stdOutHandler = handler;
        return this;
    }

    public ICommand 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, bool includeWorkingDirectory)
    {
        string prefix = includeWorkingDirectory ?
            $"{info.WorkingDirectory}> {info.FileName}" :
            info.FileName;

        if (string.IsNullOrWhiteSpace(info.Arguments))
        {
            return prefix;
        }

        return prefix + " " + info.Arguments;
    }

    private void ReportExecBegin()
    {
        if (!_quietBuildReporter && _statusForward != null)
        {
            _statusForward($"[EXEC Begin] {FormatProcessInfo(_process.StartInfo, includeWorkingDirectory: false)}");
        }
    }

    private void ReportExecEnd(int exitCode)
    {
        if (!_quietBuildReporter && _statusForward != null)
        {
            bool success = exitCode == 0;

            var message = $"{FormatProcessInfo(_process.StartInfo, includeWorkingDirectory: !success)} exited with {exitCode}";

            _statusForward($"[EXEC End] {message}");
        }
    }

    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);
        }
    }
}