Files
UnrealEngine/Engine/Source/Programs/Shared/EpicGames.Core/CommandHost.cs
2025-05-18 13:04:45 +08:00

338 lines
9.9 KiB
C#

// Copyright Epic Games, Inc. All Rights Reserved.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Threading.Tasks;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
namespace EpicGames.Core
{
/// <summary>
/// Interface for commands
/// </summary>
public interface ICommand
{
/// <summary>
/// Configure this object with the given command line arguments
/// </summary>
/// <param name="arguments">Command line arguments</param>
/// <param name="logger">Logger for output</param>
void Configure(CommandLineArguments arguments, ILogger logger);
/// <summary>
/// Gets all command line parameters to show in help for this command
/// </summary>
/// <param name="arguments">The command line arguments</param>
/// <returns>List of name/description pairs</returns>
List<KeyValuePair<string, string>> GetParameters(CommandLineArguments arguments);
/// <summary>
/// Execute this command
/// </summary>
/// <param name="logger">The logger to use for this command</param>
/// <returns>Exit code</returns>
Task<int> ExecuteAsync(ILogger logger);
}
/// <summary>
/// Interface describing a command that can be exectued
/// </summary>
public interface ICommandFactory
{
/// <summary>
/// Names for this command
/// </summary>
public IReadOnlyList<string> Names { get; }
/// <summary>
/// Short description for the mode. Will be displayed in the help text.
/// </summary>
public string Description { get; }
/// <summary>
/// Whether to include this command in help text
/// </summary>
public bool Advertise { get; }
/// <summary>
/// Whether the command accepts positional arguments
/// </summary>
public bool AcceptsPositionalArguments { get; }
/// <summary>
/// Create a command instance
/// </summary>
public ICommand CreateInstance(IServiceProvider serviceProvider);
}
#pragma warning disable CA1019 // Define accessors for attribute arguments
/// <summary>
/// Attribute used to specify names of program modes, and help text
/// </summary>
[AttributeUsage(AttributeTargets.Class)]
public sealed class CommandAttribute : Attribute
{
/// <summary>
/// Names for this command
/// </summary>
public string[] Names { get; }
/// <summary>
/// Short description for the mode. Will be displayed in the help text.
/// </summary>
public string Description { get; }
/// <summary>
/// Whether to include the command in help listings
/// </summary>
public bool Advertise { get; set; } = true;
/// <summary>
/// Whether the command takes positional arguments
/// </summary>
public bool AcceptsPositionalArguments { get; set; } = false;
/// <summary>
/// Constructor
/// </summary>
/// <param name="name">Name of the mode</param>
/// <param name="description">Short description for display in the help text</param>
public CommandAttribute(string name, string description)
{
Names = [name];
Description = description;
}
/// <summary>
/// Constructor
/// </summary>
/// <param name="category">Category for this command</param>
/// <param name="name">Name of the mode</param>
/// <param name="description">Short description for display in the help text</param>
public CommandAttribute(string category, string name, string description)
{
Names = [category, name];
Description = description;
}
}
#pragma warning restore CA1019 // Define accessors for attribute arguments
/// <summary>
/// Base class for all commands that can be executed by HordeAgent
/// </summary>
public abstract class Command : ICommand
{
/// <inheritdoc/>
public virtual void Configure(CommandLineArguments arguments, ILogger logger)
{
arguments.ApplyTo(this, logger);
}
/// <inheritdoc/>
public virtual List<KeyValuePair<string, string>> GetParameters(CommandLineArguments arguments)
{
return CommandLineArguments.GetParameters(GetType());
}
/// <inheritdoc/>
public abstract Task<int> ExecuteAsync(ILogger logger);
}
/// <summary>
/// Default implementation of a command factory
/// </summary>
class CommandFactory : ICommandFactory
{
public IReadOnlyList<string> Names { get; }
public string Description { get; }
public bool Advertise { get; }
public bool AcceptsPositionalArguments { get; }
public Type Type { get; }
public CommandFactory(string[] names, string description, bool advertise, bool acceptsPositionalArguments, Type type)
{
Names = names;
Description = description;
Advertise = advertise;
AcceptsPositionalArguments = acceptsPositionalArguments;
Type = type;
}
public ICommand CreateInstance(IServiceProvider serviceProvider) => (ICommand)serviceProvider.GetRequiredService(Type);
public override string ToString() => String.Join(" ", Names);
}
/// <summary>
/// Entry point for dispatching commands
/// </summary>
public static class CommandHost
{
/// <summary>
/// Adds services for executing the
/// </summary>
/// <param name="services"></param>
/// <param name="assembly"></param>
public static void AddCommandsFromAssembly(this IServiceCollection services, Assembly assembly)
{
List<(CommandAttribute, Type)> commands = [];
foreach (Type type in assembly.GetTypes())
{
if (typeof(ICommand).IsAssignableFrom(type) && !type.IsAbstract)
{
CommandAttribute? attribute = type.GetCustomAttribute<CommandAttribute>();
if (attribute != null)
{
services.AddTransient(type);
services.AddTransient(typeof(ICommandFactory), sp => new CommandFactory(attribute.Names, attribute.Description, attribute.Advertise, attribute.AcceptsPositionalArguments, type));
}
}
}
}
/// <summary>
/// Entry point for executing registered command types in a particular assembly
/// </summary>
/// <param name="args">Command line arguments</param>
/// <param name="serviceProvider">The service provider for the application</param>
/// <param name="defaultCommandType">The default command type</param>
/// <param name="toolDescription">Description of the tool, to print at the top of help text.</param>
/// <returns>Return code from the command</returns>
public static async Task<int> RunAsync(CommandLineArguments args, IServiceProvider serviceProvider, Type? defaultCommandType, string? toolDescription = null)
{
// Find all the command types
List<ICommandFactory> commandFactories = serviceProvider.GetServices<ICommandFactory>().ToList();
// Check if there's a matching command
ICommand? command = null;
ICommandFactory? commandFactory = null;
// Parse the positional arguments for the command name
string[] positionalArgs = args.GetPositionalArguments();
if (positionalArgs.Length == 0)
{
if (defaultCommandType == null || args.HasOption("-Help"))
{
if (toolDescription != null)
{
foreach (string line in toolDescription.Split('\n'))
{
Console.WriteLine(line);
}
Console.WriteLine("");
}
Console.WriteLine("Usage:");
Console.WriteLine(" [Command] [-Option1] [-Option2]...");
Console.WriteLine("");
Console.WriteLine("Commands:");
PrintCommands(commandFactories);
Console.WriteLine("");
Console.WriteLine("Specify \"<CommandName> -Help\" for command-specific help");
return 0;
}
else
{
command = (ICommand)serviceProvider.GetService(defaultCommandType)!;
}
}
else
{
foreach (ICommandFactory factory in commandFactories)
{
IEnumerable<string> positionalArgsEnum = positionalArgs;
if (factory.AcceptsPositionalArguments && factory.Names.Count < positionalArgs.Length)
{
positionalArgsEnum = positionalArgsEnum.Take(factory.Names.Count);
}
if (factory.Names.SequenceEqual(positionalArgsEnum, StringComparer.OrdinalIgnoreCase))
{
args.RemovePositionalArguments(factory.Names.Count);
command = factory.CreateInstance(serviceProvider);
commandFactory = factory;
break;
}
}
if (command == null)
{
ConsoleUtils.WriteError($"Invalid command '{String.Join(" ", positionalArgs)}'");
Console.WriteLine("");
Console.WriteLine("Available commands:");
PrintCommands(commandFactories);
return 1;
}
}
// If the help flag is specified, print the help info and exit immediately
if (args.HasOption("-Help"))
{
if (commandFactory == null)
{
HelpUtils.PrintHelp(null, null, command.GetParameters(args));
}
else
{
HelpUtils.PrintHelp($"Command: {String.Join(" ", commandFactory.Names)}", commandFactory.Description, command.GetParameters(args));
}
return 1;
}
// Configure the command
ILogger logger = serviceProvider.GetRequiredService<ILogger<Command>>();
try
{
command.Configure(args, logger);
args.CheckAllArgumentsUsed(logger);
}
catch (CommandLineArgumentException ex)
{
ConsoleUtils.WriteError(ex.Message);
Console.WriteLine("");
Console.WriteLine("Valid parameters:");
HelpUtils.PrintTable(command.GetParameters(args), 4, 24);
return 1;
}
// Execute all the commands
try
{
return await command.ExecuteAsync(logger);
}
catch (FatalErrorException ex)
{
logger.LogCritical(ex, "Fatal error: {Message}", ex.Message);
return ex.ExitCode;
}
catch (Exception ex)
{
logger.LogCritical(ex, "Fatal error: {ExceptionInfo}", ex.ToString());
return 1;
}
}
/// <summary>
/// Print a formatted list of all the available commands
/// </summary>
/// <param name="attributes">List of command attributes</param>
static void PrintCommands(IEnumerable<ICommandFactory> attributes)
{
List<KeyValuePair<string, string>> commands = [];
foreach (ICommandFactory attribute in attributes)
{
if (attribute.Advertise)
{
commands.Add(new KeyValuePair<string, string>(String.Join(" ", attribute.Names), attribute.Description));
}
}
HelpUtils.PrintTable([.. commands.OrderBy(x => x.Key)], 4, 20);
}
}
}