Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<OutputType>Library</OutputType>
</PropertyGroup>

<ItemGroup>
Expand Down
246 changes: 246 additions & 0 deletions src/Cli/Abstractions/Command/Command.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,246 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the MIT License. See License.txt in the project root for license information.

// Copied from: https://github.com/dotnet/sdk/blob/4a81a96a9f1bd661592975c8269e078f6e3f18c9/src/Cli/Microsoft.DotNet.Cli.Utils/Command.cs

// Also note that CommandResult Execute(Func<Process, Task>? processStarted, StreamWriter? fileWriter) is different for how
// the processStartedHandler is implemented and called. This difference will have be accounted for when we migrate over to
// the dotnet cli utils package.

using Azure.Functions.Cli.Abstractions.Extensions;
using Azure.Functions.Cli.Abstractions.Logging;
using Azure.Functions.Cli.Abstractions.Streams;
using System.Diagnostics;
using System.Diagnostics.Metrics;
using System.Runtime.CompilerServices;
using System.Windows.Input;

namespace Azure.Functions.Cli.Abstractions.Command
{
public class Command(Process? process, bool trimTrailingNewlines = false) : ICommand
{
private readonly Process _process = process ?? throw new ArgumentNullException(nameof(process));

private StreamForwarder? _stdOut;

private StreamForwarder? _stdErr;

private bool _running = false;

private bool _trimTrailingNewlines = trimTrailingNewlines;

public CommandResult Execute()
{
return Execute(null, null);
}
public CommandResult Execute(Func<Process, Task>? processStarted, StreamWriter? fileWriter)
{
Reporter.Verbose.WriteLine(string.Format(
"Running {0} {1}",
_process.StartInfo.FileName,
_process.StartInfo.Arguments));

ThrowIfRunning();

_running = true;

_process.EnableRaisingEvents = true;

Stopwatch? sw = null;
if (CommandLoggingContext.IsVerbose)
{
sw = Stopwatch.StartNew();
Reporter.Verbose.WriteLine($"> {FormatProcessInfo(_process.StartInfo)}".White());
}

Task? processTask = null;

using (var reaper = new ProcessReaper(_process))
{
_process.Start();
if (processStarted != null)
{
processTask = Task.Run(async () =>
{
try
{
await processStarted(_process);
}
catch (Exception ex)
{
Reporter.Verbose.WriteLine(string.Format(
"Error in process started handler: ",
ex.Message));
}
});
}
reaper.NotifyProcessStarted();

Reporter.Verbose.WriteLine(string.Format(
"Process ID: {0}",
_process.Id));

var taskOut = _stdOut?.BeginRead(_process.StandardOutput);
var taskErr = _stdErr?.BeginRead(_process.StandardError);
_process.WaitForExit();

taskOut?.Wait();
taskErr?.Wait();

processTask?.Wait();
}

var exitCode = _process.ExitCode;

if (CommandLoggingContext.IsVerbose)
{
Debug.Assert(sw is not null);
var message = string.Format(
"{0} exited with {1} in {2} ms.",
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;
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 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($"Unable to invoke {memberName} after the command has been run");
}
}
}
}
79 changes: 79 additions & 0 deletions src/Cli/Abstractions/Command/CommandLoggingContext.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the MIT License. See License.txt in the project root for license information.

// Copied from: https://github.com/dotnet/sdk/blob/4a81a96a9f1bd661592975c8269e078f6e3f18c9/src/Cli/Microsoft.DotNet.Cli.Utils/CommandLoggingContext.cs
using Azure.Functions.Cli.Abstractions.Environment;
using Azure.Functions.Cli.Abstractions.Logging;

namespace Azure.Functions.Cli.Abstractions.Command
{
/// <summary>
/// Defines settings for logging.
/// </summary>
public static class CommandLoggingContext
{
private static readonly Lazy<bool> _ansiPassThru = new(() => Env.GetEnvironmentVariableAsBool(Variables.AnsiPassThru));
private static Lazy<bool> _verbose = new(() => Env.GetEnvironmentVariableAsBool(Variables.Verbose));
private static Lazy<bool> _output = new(() => Env.GetEnvironmentVariableAsBool(Variables.Output, true));
private static Lazy<bool> _error = new(() => Env.GetEnvironmentVariableAsBool(Variables.Error, true));

/// <summary>
/// Gets a value indicating whether true if the verbose output is enabled.
/// </summary>
public static bool IsVerbose => _verbose.Value;

public static bool ShouldPassAnsiCodesThrough => _ansiPassThru.Value;

/// <summary>
/// Gets a value indicating whether true if normal output is enabled.
/// </summary>
internal static bool OutputEnabled => _output.Value;

/// <summary>
/// Gets a value indicating whether true if error output is enabled.
/// </summary>
internal static bool ErrorEnabled => _error.Value;

/// <summary>
/// Sets or resets the verbose output.
/// </summary>
/// <remarks>
/// After calling, consider calling <see cref="Reporter.Reset()"/> to apply change to reporter.
/// </remarks>
public static void SetVerbose(bool value)
{
_verbose = new Lazy<bool>(() => value);
}

/// <summary>
/// Sets or resets the normal output.
/// </summary>
/// <remarks>
/// After calling, consider calling <see cref="Reporter.Reset()"/> to apply change to reporter.
/// </remarks>
public static void SetOutput(bool value)
{
_output = new Lazy<bool>(() => value);
}

/// <summary>
/// Sets or resets the error output.
/// </summary>
/// <remarks>
/// After calling, consider calling <see cref="Reporter.Reset()"/> to apply change to reporter.
/// </remarks>
public static void SetError(bool value)
{
_error = new Lazy<bool>(() => value);
}

public static class Variables
{
private const string Prefix = "DOTNET_CLI_CONTEXT_";
public static readonly string Verbose = Prefix + "VERBOSE";
internal static readonly string Output = Prefix + "OUTPUT";
internal static readonly string Error = Prefix + "ERROR";
internal static readonly string AnsiPassThru = Prefix + "ANSI_PASS_THRU";
}
}
}
21 changes: 21 additions & 0 deletions src/Cli/Abstractions/Command/CommandResult.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the MIT License. See License.txt in the project root for license information.

// Copied from: https://github.com/dotnet/sdk/blob/4a81a96a9f1bd661592975c8269e078f6e3f18c9/src/Cli/Microsoft.DotNet.Cli.Utils/CommandResult.cs
using System.Diagnostics;

namespace Azure.Functions.Cli.Abstractions.Command
{
public readonly struct CommandResult(ProcessStartInfo startInfo, int exitCode, string? stdOut, string? stdErr)
{
public static readonly CommandResult Empty = default(CommandResult);

public ProcessStartInfo StartInfo { get; } = startInfo;

public int ExitCode { get; } = exitCode;

public string? StdOut { get; } = stdOut;

public string? StdErr { get; } = stdErr;
}
}
33 changes: 33 additions & 0 deletions src/Cli/Abstractions/Command/ICommand.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the MIT License. See License.txt in the project root for license information.

// Copied from: https://github.com/dotnet/sdk/blob/4a81a96a9f1bd661592975c8269e078f6e3f18c9/src/Cli/Microsoft.DotNet.Cli.Utils/ICommand.cs
namespace Azure.Functions.Cli.Abstractions.Command
{
public interface ICommand
{
string CommandName { get; }

string CommandArgs { get; }

CommandResult Execute();

ICommand WorkingDirectory(string projectDirectory);

ICommand EnvironmentVariable(string name, string? value);

ICommand CaptureStdOut();

ICommand CaptureStdErr();

ICommand ForwardStdOut(TextWriter? to = null, bool onlyIfVerbose = false, bool ansiPassThrough = true);

ICommand ForwardStdErr(TextWriter? to = null, bool onlyIfVerbose = false, bool ansiPassThrough = true);

ICommand OnOutputLine(Action<string> handler);

ICommand OnErrorLine(Action<string> handler);

ICommand SetCommandArgs(string commandArgs);
}
}
Loading
Loading