// 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 { /// /// Interface for commands /// public interface ICommand { /// /// Configure this object with the given command line arguments /// /// Command line arguments /// Logger for output void Configure(CommandLineArguments arguments, ILogger logger); /// /// Gets all command line parameters to show in help for this command /// /// The command line arguments /// List of name/description pairs List> GetParameters(CommandLineArguments arguments); /// /// Execute this command /// /// The logger to use for this command /// Exit code Task ExecuteAsync(ILogger logger); } /// /// Interface describing a command that can be exectued /// public interface ICommandFactory { /// /// Names for this command /// public IReadOnlyList Names { get; } /// /// Short description for the mode. Will be displayed in the help text. /// public string Description { get; } /// /// Whether to include this command in help text /// public bool Advertise { get; } /// /// Whether the command accepts positional arguments /// public bool AcceptsPositionalArguments { get; } /// /// Create a command instance /// public ICommand CreateInstance(IServiceProvider serviceProvider); } #pragma warning disable CA1019 // Define accessors for attribute arguments /// /// Attribute used to specify names of program modes, and help text /// [AttributeUsage(AttributeTargets.Class)] public sealed class CommandAttribute : Attribute { /// /// Names for this command /// public string[] Names { get; } /// /// Short description for the mode. Will be displayed in the help text. /// public string Description { get; } /// /// Whether to include the command in help listings /// public bool Advertise { get; set; } = true; /// /// Whether the command takes positional arguments /// public bool AcceptsPositionalArguments { get; set; } = false; /// /// Constructor /// /// Name of the mode /// Short description for display in the help text public CommandAttribute(string name, string description) { Names = [name]; Description = description; } /// /// Constructor /// /// Category for this command /// Name of the mode /// Short description for display in the help text public CommandAttribute(string category, string name, string description) { Names = [category, name]; Description = description; } } #pragma warning restore CA1019 // Define accessors for attribute arguments /// /// Base class for all commands that can be executed by HordeAgent /// public abstract class Command : ICommand { /// public virtual void Configure(CommandLineArguments arguments, ILogger logger) { arguments.ApplyTo(this, logger); } /// public virtual List> GetParameters(CommandLineArguments arguments) { return CommandLineArguments.GetParameters(GetType()); } /// public abstract Task ExecuteAsync(ILogger logger); } /// /// Default implementation of a command factory /// class CommandFactory : ICommandFactory { public IReadOnlyList 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); } /// /// Entry point for dispatching commands /// public static class CommandHost { /// /// Adds services for executing the /// /// /// 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(); if (attribute != null) { services.AddTransient(type); services.AddTransient(typeof(ICommandFactory), sp => new CommandFactory(attribute.Names, attribute.Description, attribute.Advertise, attribute.AcceptsPositionalArguments, type)); } } } } /// /// Entry point for executing registered command types in a particular assembly /// /// Command line arguments /// The service provider for the application /// The default command type /// Description of the tool, to print at the top of help text. /// Return code from the command public static async Task RunAsync(CommandLineArguments args, IServiceProvider serviceProvider, Type? defaultCommandType, string? toolDescription = null) { // Find all the command types List commandFactories = serviceProvider.GetServices().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 \" -Help\" for command-specific help"); return 0; } else { command = (ICommand)serviceProvider.GetService(defaultCommandType)!; } } else { foreach (ICommandFactory factory in commandFactories) { IEnumerable 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>(); 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; } } /// /// Print a formatted list of all the available commands /// /// List of command attributes static void PrintCommands(IEnumerable attributes) { List> commands = []; foreach (ICommandFactory attribute in attributes) { if (attribute.Advertise) { commands.Add(new KeyValuePair(String.Join(" ", attribute.Names), attribute.Description)); } } HelpUtils.PrintTable([.. commands.OrderBy(x => x.Key)], 4, 20); } } }