diff --git a/README.md b/README.md index f94878b..2b53ebb 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,27 @@ -Nitride CIL -=========== +# MfGames.ToolBuilder -A static site generator based on the [Gallium ECS](https://gitlab.com/mfgames-cil/gallium-cil/). +_An opinionated library for easily creating command-line tools in C#._ + +ToolBuilder is a library to encapsulate the setup and running of tools, CLI components arranged with verbs (like `git`). It includes some opinionated decisions on default setup. + +## Commands + +This library is built on top of [System.CommandLine](https://github.com/dotnet/command-line-api) and .NET 6 generic hosting. The commands are combined together using dependency injection (see below) which allows for them to be included as parameters for the constructor or dynamically discovered. + +All commands are assumed to be asynchronous and include a cancellation token in their calls. + +`System.CommandLine` was chosen because it allows for composition of features instead on needing inheritance to give a tool the ability to render a table or handle additional features. + +## Autofac + +While most libraries should only use the included service provider infrastructure with the base library, this tool sets up [Autofac](https://autofac.org/) to handle services. This allows for both service provider usage (via `ConfigureServices`) and Autofac (via `ConfigureContainer`). + +Autofac was chosen because of a number of quality of life features and personal preferences of the developer. A tool can easily ignore that functionality in favor of using only `IServiceProvider`. + +## Serilog + +Likewise, tools are set up to use [Serilog](https://serilog.net/) for logging instead of the built-in logging code. However, both are served by Serilog so `ILoggerFactory` can be used. + +Like Autofac, Serilog was given because of the variety of sinks, certain quality of life, but also the ability to log more detailed objects. It also produces some coloring to log details based on data types. + +To use the Serilog functionality with `ILoggerFactory`, objects needed to be included as parameters instead of inlined with string interpolation. diff --git a/TASKS.md b/TASKS.md new file mode 100644 index 0000000..c312138 --- /dev/null +++ b/TASKS.md @@ -0,0 +1,3 @@ +- [ ] Split out tables into an assembly +- [ ] Switch to GitVersion for release +- [ ] Switch to scripts instead of Node for targets diff --git a/flake.lock b/flake.lock index a918885..2e87e5b 100644 --- a/flake.lock +++ b/flake.lock @@ -2,11 +2,11 @@ "nodes": { "flake-utils": { "locked": { - "lastModified": 1638122382, - "narHash": "sha256-sQzZzAbvKEqN9s0bzWuYmRaA03v40gaJ4+iL1LXjaeI=", + "lastModified": 1648297722, + "narHash": "sha256-W+qlPsiZd8F3XkzXOzAoR+mpFqzm3ekQkJNa+PIh1BQ=", "owner": "numtide", "repo": "flake-utils", - "rev": "74f7e4319258e287b0f9cb95426c9853b282730b", + "rev": "0f8662f1319ad6abf89b3380dd2722369fc51ade", "type": "github" }, "original": { @@ -17,11 +17,11 @@ }, "nixpkgs": { "locked": { - "lastModified": 1638198142, - "narHash": "sha256-plU9b8r4St6q4U7VHtG9V7oF8k9fIpfXl/KDaZLuY9k=", + "lastModified": 1648390671, + "narHash": "sha256-u69opCeHUx3CsdIerD0wVSR+DjfDQjnztObqfk9Trqc=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "8a308775674e178495767df90c419425474582a1", + "rev": "ce8cbe3c01fd8ee2de526ccd84bbf9b82397a510", "type": "github" }, "original": { diff --git a/flake.nix b/flake.nix index d6a1166..dabd502 100644 --- a/flake.nix +++ b/flake.nix @@ -11,7 +11,7 @@ let pkgs = nixpkgs.legacyPackages.${system}; in { devShell = pkgs.mkShell { - buildInputs = [ pkgs.dotnet-sdk_5 pkgs.nodejs-16_x pkgs.nixfmt ]; + buildInputs = [ pkgs.dotnet-sdk pkgs.nodejs-16_x pkgs.nixfmt ]; }; }); } diff --git a/src/MfGames.ToolBuilder/FakedRootCommand.cs b/src/MfGames.ToolBuilder/FakedRootCommand.cs deleted file mode 100644 index a51dc5c..0000000 --- a/src/MfGames.ToolBuilder/FakedRootCommand.cs +++ /dev/null @@ -1,28 +0,0 @@ -using System; -using System.CommandLine; -using System.Linq; - -namespace MfGames.ToolBuilder -{ - /// - /// Fakes the functionality of the RootCommand because of the given issue: - /// https://github.com/dotnet/command-line-api/issues/1471 - /// In short, RootCommand doesn't resolve properly when working in certain - /// situations, mainly having the command line parser in a library that is - /// called by another executable. - /// - public class FakedRootCommand : Command - { - public FakedRootCommand(ToolNames names) - : base(names.GetExecutableName(), string.Empty) - { - } - - public static string[] GetArguments() - { - string[] args = Environment.GetCommandLineArgs(); - - return args.Length == 0 ? args : args.Skip(1).ToArray(); - } - } -} diff --git a/src/MfGames.ToolBuilder/Globals/ConfigToolGlobalService.cs b/src/MfGames.ToolBuilder/Globals/ConfigToolGlobal.cs similarity index 80% rename from src/MfGames.ToolBuilder/Globals/ConfigToolGlobalService.cs rename to src/MfGames.ToolBuilder/Globals/ConfigToolGlobal.cs index 8eeb429..e9374ee 100644 --- a/src/MfGames.ToolBuilder/Globals/ConfigToolGlobalService.cs +++ b/src/MfGames.ToolBuilder/Globals/ConfigToolGlobal.cs @@ -8,15 +8,18 @@ using Microsoft.Extensions.Configuration; using Newtonsoft.Json; -namespace MfGames.ToolBuilder.Globals +namespace MfGames.ToolBuilder.Services { /// /// A utility class for handling `--config` options. /// - public class ConfigToolGlobalService + public class ConfigToolGlobal { - public ConfigToolGlobalService() + private readonly string configName; + + public ConfigToolGlobal(string configName) { + this.configName = configName; this.ConfigOption = new Option( "--config", "Configuration file to use for settings, otherwise a default will be used.") @@ -46,9 +49,7 @@ namespace MfGames.ToolBuilder.Globals get { // If we don't have an internal name, blow up. - string? configName = this.Names?.GetConfigName(); - - if (string.IsNullOrWhiteSpace(configName)) + if (string.IsNullOrWhiteSpace(this.configName)) { throw new ApplicationException( "Cannot determine the default configuration path unless the configuration name has been set."); @@ -61,24 +62,18 @@ namespace MfGames.ToolBuilder.Globals .GetFolderPath(Environment.SpecialFolder.ApplicationData); string appDirectory = Path.Combine( configDirectory, - configName); + this.configName); string configPath = Path.Combine(appDirectory, "Settings.json"); return configPath; } } - /// - /// Gets or sets the name of the application. This is used to figure out - /// the name of the configuration file and what is shown on the screen. - /// - public ToolNames? Names { get; set; } - /// /// Adds the common options to the command. /// /// - public void AddOptions(Command root) + public void Attach(Command root) { root.AddGlobalOption(this.ConfigOption); } @@ -107,7 +102,7 @@ namespace MfGames.ToolBuilder.Globals } // Otherwise, use the default files. - foreach (var config in configs) + foreach (string config in configs) { builder.AddJsonFile(config, true, true); } @@ -128,26 +123,12 @@ namespace MfGames.ToolBuilder.Globals return default; } - var json = file.ReadAllText(); + string json = file.ReadAllText(); TType result = JsonConvert.DeserializeObject(json)!; return result; } - /// - /// Sets the internal name of the application, used for the - /// configuration path. - /// - /// - /// The names object for the tool. - /// - /// The service for chaining operations. - public ConfigToolGlobalService WithNames(ToolNames? names) - { - this.Names = names; - return this; - } - /// /// Writes the given object to the default configuration file. /// diff --git a/src/MfGames.ToolBuilder/Globals/LoggingToolGlobalService.cs b/src/MfGames.ToolBuilder/Globals/LoggingToolGlobal.cs similarity index 87% rename from src/MfGames.ToolBuilder/Globals/LoggingToolGlobalService.cs rename to src/MfGames.ToolBuilder/Globals/LoggingToolGlobal.cs index c8b5809..b13bfd1 100644 --- a/src/MfGames.ToolBuilder/Globals/LoggingToolGlobalService.cs +++ b/src/MfGames.ToolBuilder/Globals/LoggingToolGlobal.cs @@ -8,20 +8,20 @@ using Serilog.Core; using Serilog.Events; using Serilog.Exceptions; -namespace MfGames.ToolBuilder.Globals +namespace MfGames.ToolBuilder.Services { /// /// A service for handling logging options. /// - public class LoggingToolGlobalService + public class LoggingToolGlobal { - public LoggingToolGlobalService() + public LoggingToolGlobal() { this.LogLevelOption = new Option( "--log-level", () => nameof(LogEventLevel.Information), string.Format( - "Controls the verbosity of the output: {0}. Not case-sensitive and prefixes allowed.", + "Controls the verbosity of the output: {0} (defaults to Warning). Not case-sensitive and prefixes allowed.", string.Join(", ", Enum.GetNames()))); } @@ -34,7 +34,7 @@ namespace MfGames.ToolBuilder.Globals /// Adds the common options to the command. /// /// - public void AddOptions(Command root) + public void Attach(Command root) { root.AddGlobalOption(this.LogLevelOption); } @@ -49,7 +49,7 @@ namespace MfGames.ToolBuilder.Globals string level = GlobalOptionHelper.GetArgumentValue( this.LogLevelOption, arguments, - "Information"); + "Warning"); LogEventLevel logLevel = level.GetEnumFuzzy( "log level"); diff --git a/src/MfGames.ToolBuilder/ITopCommand.cs b/src/MfGames.ToolBuilder/ITopCommand.cs deleted file mode 100644 index f7b3171..0000000 --- a/src/MfGames.ToolBuilder/ITopCommand.cs +++ /dev/null @@ -1,14 +0,0 @@ -using System.CommandLine; - -namespace MfGames.ToolBuilder -{ - /// - /// An interface that indicates that the given command is a top-level - /// command instead of one that is included as a sub-command inside another. - /// This is used to arrange the various sub-commands using dependency - /// injection and is purely a marker interface. - /// - public interface ITopCommand : ICommand - { - } -} diff --git a/src/MfGames.ToolBuilder/MfGames.ToolBuilder.csproj b/src/MfGames.ToolBuilder/MfGames.ToolBuilder.csproj index a139ebf..f20dbb9 100644 --- a/src/MfGames.ToolBuilder/MfGames.ToolBuilder.csproj +++ b/src/MfGames.ToolBuilder/MfGames.ToolBuilder.csproj @@ -1,43 +1,49 @@ - net5.0 + net6.0 enable A framework for easily creating command line tools using System.CommandLine, Autofac, and Serilog. + + + + $(NoWarn);NU5104 + - + - + - + - + all runtime; build; native; contentfiles; analyzers; buildtransitive - + all runtime; build; native; contentfiles; analyzers; buildtransitive - + all runtime; build; native; contentfiles; analyzers; buildtransitive - + - + + diff --git a/src/MfGames.ToolBuilder/ToolBox.cs b/src/MfGames.ToolBuilder/ToolBox.cs new file mode 100644 index 0000000..a92b8ad --- /dev/null +++ b/src/MfGames.ToolBuilder/ToolBox.cs @@ -0,0 +1,51 @@ +using System; +using System.CommandLine.Parsing; +using System.Threading.Tasks; + +using Serilog; + +namespace MfGames.ToolBuilder; + +/// +/// A collection of tools set up for running the command-line shell. +/// +public class ToolBox +{ + private readonly string[] arguments; + + private readonly Parser parser; + + public ToolBox(string[] arguments, Parser parser) + { + this.arguments = arguments; + this.parser = parser; + } + + /// + /// Finishes building the tool, parses the arguments, and runs the + /// command. + /// + /// An error code, 0 for successful, otherwise false. + public async Task RunAsync() + { + try + { + ParseResult parseResults = this.parser.Parse(this.arguments); + int exitCode = await parseResults.InvokeAsync(); + + return exitCode; + } + catch (Exception exception) + { + Log.Fatal( + exception, + "There was a problem running the command: {Arguments}", + this.arguments); + + return Environment.ExitCode == 0 ? 1 : Environment.ExitCode; + } + + // Get the exit code and return it. + // TODO return Environment.ExitCode; + } +} diff --git a/src/MfGames.ToolBuilder/ToolBoxBuilder.cs b/src/MfGames.ToolBuilder/ToolBoxBuilder.cs new file mode 100644 index 0000000..a346547 --- /dev/null +++ b/src/MfGames.ToolBuilder/ToolBoxBuilder.cs @@ -0,0 +1,186 @@ +using System; +using System.CommandLine; +using System.CommandLine.Builder; +using System.CommandLine.Hosting; +using System.CommandLine.Parsing; +using System.IO; + +using Autofac; +using Autofac.Extensions.DependencyInjection; + +using MfGames.ToolBuilder.Services; + +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; + +using Serilog; + +namespace MfGames.ToolBuilder +{ + /// + /// A builder pattern for creating a tool box, a nested collection of tools. + /// This wraps much of the hosting and command-line parsing infrastructure + /// with some opinionated decisions and reduces the amount of boilerplate + /// needed to create tools. + /// + public class ToolBoxBuilder + { + private readonly string[] arguments; + + private readonly ConfigToolGlobal config; + + private readonly IHostBuilder hostBuilder; + + private readonly LoggingToolGlobal logging; + + public ToolBoxBuilder(string configName, string[] arguments) + { + // Create our various services. + this.arguments = arguments; + this.config = new ConfigToolGlobal(configName); + this.logging = new LoggingToolGlobal(); + + // Set up logging first so we can report the loading process. This + // sets up the Serilog.Log.Logger which means we can use that for + // everything beyond this point. + this.logging.Configure(arguments); + + // Start up the basic configuration. + // + // Normally, we would use Host.CreateDefaultBuilder(args) to create + // this, but when run as a stand-alone application in someone's + // $HOME on Linux, this causes an inotify watcher to be registered + // on the entire directory tree (which takes a really long time). + // + // We also don't need most of the default features. + var serviceProviderFactory = new AutofacServiceProviderFactory(); + this.hostBuilder = new HostBuilder() + .UseDefaultServiceProvider(this.ConfigureDefaultServiceProvider) + .UseConsoleLifetime() + .ConfigureAppConfiguration(this.ConfigureAppConfiguration) + .UseSerilog() + .UseServiceProviderFactory(serviceProviderFactory) + .ConfigureServices(this.ConfigureServices) + .ConfigureContainer(this.ConfigureContainer); + } + + public static ToolBoxBuilder Create( + string configName, + string[] arguments) + { + return new ToolBoxBuilder(configName, arguments); + } + + /// + /// Constructs a tool box, a collection of tools. + /// + /// The constructed toolbox. + public ToolBox Build() + { + // There is a Catch-22 with how System.CommandLine is build. It + // requires the root command to be passed into the constructor + // for the CommandLineBuilder but we can't create the root command + // until we build our host. + // + // To handle this, we build our first host builder (which doesn't + // know about the command line parser), use it to get the command + // and then pass it into the command-line version to create a + // second `HostBuilder` that we don't use. + IHost host = this.hostBuilder.Build(); + ILifetimeScope container = host.Services.GetAutofacRoot()!; + + // Create the root command and attach our globals to that. + RootCommand rootCommand = container.Resolve(); + + this.config.Attach(rootCommand); + this.logging.Attach(rootCommand); + + // Finish creating the command line builder so we can make the parser. + CommandLineBuilder? builder = new CommandLineBuilder(rootCommand) + .UseDefaults() + .UseHost(); + + // Finish building the parser, wrap it in a tool box, and return it. + Parser parser = builder.Build(); + var toolBox = new ToolBox(this.arguments, parser); + + return toolBox; + } + + /// + /// Provides additional configuration for Autofac containers. + /// + /// The configuration method. + /// The builder to chain methods. + public ToolBoxBuilder ConfigureContainer( + Action configure) + { + this.hostBuilder.ConfigureContainer(configure); + return this; + } + + /// + /// Provides additional configuration for services. + /// + /// The configuration method. + /// The builder to chain methods. + public ToolBoxBuilder ConfigureServices( + Action configure) + { + this.hostBuilder.ConfigureServices(configure); + return this; + } + + private void ConfigureAppConfiguration( + HostBuilderContext context, + IConfigurationBuilder builder) + { + builder.SetBasePath(Directory.GetCurrentDirectory()); + this.config.Configure(builder, this.arguments); + } + + private void ConfigureContainer( + HostBuilderContext context, + ContainerBuilder builder) + { + // We want to get logging up and running as soon as possible. We + // also hook up the logging to the process exit in an attempt to + // make sure the logger is properly flushed before exiting. + builder.RegisterInstance(Log.Logger).As().SingleInstance(); + + AppDomain.CurrentDomain.ProcessExit += + (_, _) => Log.CloseAndFlush(); + + // Register the global services as singletons. + builder + .RegisterInstance(this.config) + .AsSelf() + .SingleInstance(); + builder + .RegisterInstance(this.logging) + .AsSelf() + .SingleInstance(); + + // Register the components required to make the CLI work. + builder.RegisterModule(); + } + + private void ConfigureDefaultServiceProvider( + HostBuilderContext context, + ServiceProviderOptions options) + { + bool isDevelopment = context.HostingEnvironment.IsDevelopment(); + + options.ValidateScopes = isDevelopment; + options.ValidateOnBuild = isDevelopment; + } + + private void ConfigureServices( + HostBuilderContext context, + IServiceCollection services) + { + services.AddAutofac(); + } + } +} diff --git a/src/MfGames.ToolBuilder/ToolBuilder.cs b/src/MfGames.ToolBuilder/ToolBuilder.cs deleted file mode 100644 index b78167d..0000000 --- a/src/MfGames.ToolBuilder/ToolBuilder.cs +++ /dev/null @@ -1,174 +0,0 @@ -using System; -using System.IO; -using System.Threading.Tasks; - -using Autofac; -using Autofac.Extensions.DependencyInjection; - -using MfGames.ToolBuilder.Globals; - -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Hosting; - -using Serilog; - -namespace MfGames.ToolBuilder -{ - /// - /// A builder pattern for creating the tool. This wraps much of the hosting - /// infrastructure with some opinionated decisions and reduces the amount of - /// boilerplate needed to configure the tool. - /// - public class ToolBuilder - { - private readonly string[] arguments; - - private readonly ConfigToolGlobalService configService; - - private readonly IHostBuilder hostBuilder; - - private readonly LoggingToolGlobalService loggingService; - - private readonly ToolNames names; - - public ToolBuilder( - ToolNames names, - string[] arguments) - { - // Create our various services. - this.names = names; - this.arguments = arguments; - this.configService = new ConfigToolGlobalService() - .WithNames(this.names); - this.loggingService = new LoggingToolGlobalService(); - - // Set up logging first so we can report the loading process. This - // sets up the Serilog.Log.Logger which means we can use that for - // everything beyond this point. - this.loggingService.Configure(arguments); - - // Start up the basic configuration. - // - // Normally, we would use Host.CreateDefaultBuilder(args) to create - // this, but when run as a stand-alone application in someone's - // $HOME on Linux, this causes an inotify watcher to be registered - // on the entire directory tree (which takes a really long time). - // - // We also don't need most of the default features. - this.hostBuilder = - new HostBuilder() - .UseDefaultServiceProvider( - (context, options) => - { - bool isDevelopment = - context.HostingEnvironment.IsDevelopment(); - options.ValidateScopes = isDevelopment; - options.ValidateOnBuild = isDevelopment; - }) - .UseConsoleLifetime() - .ConfigureAppConfiguration(this.ConfigureAppConfiguration) - .UseSerilog() - .UseServiceProviderFactory( - new AutofacServiceProviderFactory()) - .ConfigureServices(this.ConfigureServices) - .ConfigureContainer( - this.ConfigureContainer); - } - - public static ToolBuilder Create( - ToolNames names, - string[] arguments) - { - return new ToolBuilder(names, arguments); - } - - public ToolBuilder ConfigureContainer( - Action configure) - { - this.hostBuilder.ConfigureContainer(configure); - return this; - } - - public ToolBuilder ConfigureServices( - Action configure) - { - this.hostBuilder.ConfigureServices(configure); - return this; - } - - /// - /// Finishes building the tool, parses the arguments, and runs the - /// command. - /// - /// An error code, 0 for successful, otherwise false. - public async Task RunAsync() - { - try - { - await this.hostBuilder - .RunConsoleAsync() - .ConfigureAwait(false); - } - catch (Exception exception) - { - Log.Fatal( - exception, - "There was a problem running the command: {Arguments}", - this.arguments); - - return Environment.ExitCode == 0 ? 1 : Environment.ExitCode; - } - - // Get the exit code and return it. - return Environment.ExitCode; - } - - private void ConfigureAppConfiguration( - HostBuilderContext context, - IConfigurationBuilder builder) - { - builder.SetBasePath(Directory.GetCurrentDirectory()); - this.configService.Configure(builder, this.arguments); - } - - private void ConfigureContainer( - HostBuilderContext context, - ContainerBuilder builder) - { - // We want to get logging up and running as soon as possible. We - // also hook up the logging to the process exit in an attempt to - // make sure the logger is properly flushed before exiting. - builder.RegisterInstance(Log.Logger).As().SingleInstance(); - - AppDomain.CurrentDomain.ProcessExit += - (_, _) => Log.CloseAndFlush(); - - // Register the names as a singleton instance. - builder.RegisterInstance(this.names) - .As() - .SingleInstance(); - - // Register the global services as singletons. - builder - .RegisterInstance(this.configService) - .AsSelf() - .SingleInstance(); - builder - .RegisterInstance(this.loggingService) - .AsSelf() - .SingleInstance(); - - // Register the components required to make the CLI work. - builder.RegisterModule(); - } - - private void ConfigureServices( - HostBuilderContext context, - IServiceCollection services) - { - services.AddAutofac(); - services.AddHostedService(); - } - } -} diff --git a/src/MfGames.ToolBuilder/ToolBuilderModule.cs b/src/MfGames.ToolBuilder/ToolBuilderModule.cs index 0ac3f7a..5965dda 100644 --- a/src/MfGames.ToolBuilder/ToolBuilderModule.cs +++ b/src/MfGames.ToolBuilder/ToolBuilderModule.cs @@ -1,6 +1,6 @@ using Autofac; -using MfGames.ToolBuilder.Globals; +using MfGames.ToolBuilder.Services; namespace MfGames.ToolBuilder { @@ -14,10 +14,8 @@ namespace MfGames.ToolBuilder { builder .RegisterAssemblyTypes(this.GetType().Assembly) - .Except() - .Except() - .Except() - .Except() + .Except() + .Except() .AsSelf() .AsImplementedInterfaces(); } diff --git a/src/MfGames.ToolBuilder/ToolNames.cs b/src/MfGames.ToolBuilder/ToolNames.cs deleted file mode 100644 index 14787fc..0000000 --- a/src/MfGames.ToolBuilder/ToolNames.cs +++ /dev/null @@ -1,74 +0,0 @@ -using System; -using System.IO; - -namespace MfGames.ToolBuilder -{ - /// - /// Provides the name of the tool in its various formats. This is used to - /// identify the executable name and other elements used throughout the - /// system. - /// - public class ToolNames - { - public ToolNames(string name) - { - this.Name = name ?? throw new ArgumentNullException(nameof(name)); - - // The name of the executing program comes in as the first parameter. - string[] args = Environment.GetCommandLineArgs(); - - if (args.Length > 0) - { - this.ExecutableName = Path.GetFileNameWithoutExtension(args[0]); - } - } - - /// - /// Gets the name of the configuration file. This will fall back to Name - /// if not provided. - /// - public string? ConfigName { get; set; } - - /// - /// Gets the human-readable name of the application, falling back to Name - /// if not provided. - /// - public string? DisplayName { get; set; } - - /// - /// Gets the name of the executable. This will fall back to ConfigName - /// if this propery is null. - /// - public string? ExecutableName { get; set; } - - /// - /// Gets the required name of the application. This can be human-readable - /// or not, depending on the developer. - /// - public string Name { get; set; } - - /// - /// Gets the configuration name using any fallbacks. - /// - public string GetConfigName() - { - return this.ConfigName ?? this.Name; - } - - /// - /// Gets the display name using any fallbacks. - /// - public string GetDisplayName() - { - return this.DisplayName ?? this.Name; - } - - /// - /// Gets the executable name using any fallbacks. - /// - public string GetExecutableName() - { - return this.ExecutableName ?? this.GetConfigName(); - } - } -} diff --git a/src/MfGames.ToolBuilder/ToolService.cs b/src/MfGames.ToolBuilder/ToolService.cs deleted file mode 100644 index 44cb8dd..0000000 --- a/src/MfGames.ToolBuilder/ToolService.cs +++ /dev/null @@ -1,158 +0,0 @@ -using System; -using System.Collections.Generic; -using System.CommandLine; -using System.CommandLine.Builder; -using System.CommandLine.Invocation; -using System.CommandLine.Parsing; -using System.Threading; -using System.Threading.Tasks; - -using MfGames.ToolBuilder.Globals; - -using Microsoft.Extensions.Hosting; - -using Serilog; - -#pragma warning disable Serilog004 - -// ReSharper disable TemplateIsNotCompileTimeConstantProblem -// ReSharper disable SuspiciousTypeConversion.Global - -namespace MfGames.ToolBuilder -{ - /// - /// Implements the command-line shell for generating the static site. - /// - public class ToolService : IHostedService - { - private readonly IList commands; - - private readonly ConfigToolGlobalService configService; - - private readonly IHostApplicationLifetime lifetime; - - private readonly ILogger logger; - - private readonly LoggingToolGlobalService loggingService; - - private readonly ToolNames names; - - public ToolService( - ToolNames names, - ILogger logger, - IHostApplicationLifetime lifetime, - IList commands, - ConfigToolGlobalService configService, - LoggingToolGlobalService loggingService) - { - this.names = names; - this.lifetime = lifetime; - this.commands = commands; - this.configService = configService; - this.loggingService = loggingService; - this.logger = logger.ForContext(); - } - - /// - public Task StartAsync(CancellationToken cancellationToken) - { - this.lifetime.ApplicationStarted - .Register( - () => - { - Task.Run( - async () => - await this.RunAsync().ConfigureAwait(false), - cancellationToken); - }); - - return Task.CompletedTask; - } - - /// - public Task StopAsync(CancellationToken cancellationToken) - { - return Task.CompletedTask; - } - - private FakedRootCommand CreateRootCommand() - { - // Create the root command and add in the top-level commands - // underneath it. We can't use the "real" `RootCommand` here because - // it doesn't work in stripped executables (because this is a - // library) so we fake it with a "normal" command. - var root = new FakedRootCommand(this.names); - - foreach (var command in this.commands) - { - root.AddCommand((Command)command); - } - - // Add the universal options. - this.loggingService.AddOptions(root); - this.configService.AddOptions(root); - - // Return the resulting container. - return root; - } - - private void OnException(Exception exception, InvocationContext context) - { - if (exception is ToolException toolException) - { - this.logger.Fatal(toolException.Message); - - foreach (var message in toolException.Messages) - { - this.logger.Fatal(message); - } - - Environment.ExitCode = toolException.ExitCode; - } - else - { - this.logger.Fatal( - exception, - "Unhandled exception!"); - - Environment.ExitCode = Environment.ExitCode == 0 - ? 1 - : Environment.ExitCode; - } - } - - private async Task RunAsync() - { - try - { - // Build the command tree. - FakedRootCommand root = this.CreateRootCommand(); - string[] args = FakedRootCommand.GetArguments(); - - // Execute the command. - this.logger.Verbose( - "Running the command-line arguments: {Arguments}", - args); - - CommandLineBuilder builder = new CommandLineBuilder(root) - .UseDefaults() - .UseExceptionHandler(this.OnException); - - Parser cli = builder.Build(); - int exitCode = await cli - .InvokeAsync(args) - .ConfigureAwait(false); - - if (exitCode != 0) - { - Environment.ExitCode = exitCode; - } - } - finally - { - // Stop the application once the work is done. - this.lifetime.StopApplication(); - } - } - } -} diff --git a/tests/MfGames.ToolBuilder.Tests/MfGames.ToolBuilder.Tests.csproj b/tests/MfGames.ToolBuilder.Tests/MfGames.ToolBuilder.Tests.csproj index 7dc4253..a9c4af3 100644 --- a/tests/MfGames.ToolBuilder.Tests/MfGames.ToolBuilder.Tests.csproj +++ b/tests/MfGames.ToolBuilder.Tests/MfGames.ToolBuilder.Tests.csproj @@ -1,29 +1,29 @@ - net5.0 + net6.0 enable - - + + all runtime; build; native; contentfiles; analyzers; buildtransitive - - + + all runtime; build; native; contentfiles; analyzers; buildtransitive - + all runtime; build; native; contentfiles; analyzers; buildtransitive - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/tests/SampleTool/CrashTopCommand.cs b/tests/SampleTool/CrashCommand.cs similarity index 89% rename from tests/SampleTool/CrashTopCommand.cs rename to tests/SampleTool/CrashCommand.cs index 9a0c4e1..cdd11ca 100644 --- a/tests/SampleTool/CrashTopCommand.cs +++ b/tests/SampleTool/CrashCommand.cs @@ -7,12 +7,12 @@ using MfGames.ToolBuilder; namespace SampleTool { - public class CrashTopCommand : Command, ITopCommand, ICommandHandler + public class CrashCommand : Command, ICommandHandler { private readonly Option messyOption; /// - public CrashTopCommand() + public CrashCommand() : base("crash", "Crash the application with an exception.") { this.Handler = this; diff --git a/tests/SampleTool/LogCommand.cs b/tests/SampleTool/LogCommand.cs new file mode 100644 index 0000000..45bda7a --- /dev/null +++ b/tests/SampleTool/LogCommand.cs @@ -0,0 +1,67 @@ +using System.CommandLine; +using System.CommandLine.Invocation; +using System.Threading.Tasks; + +using Microsoft.Extensions.Logging; + +using ILogger = Serilog.ILogger; + +namespace SampleTool; + +public class LogCommand : Command, ICommandHandler +{ + private readonly ILogger extensionLogger; + + private readonly ILogger serilogLogger; + + /// + public LogCommand( + ILoggerFactory loggerFactory, + ILogger serilogLogger) + : base("log", "Shows various logging messages.") + { + this.serilogLogger = serilogLogger; + this.extensionLogger = loggerFactory.CreateLogger(); + this.Handler = this; + } + + /// + public Task InvokeAsync(InvocationContext context) + { + // Show the serilog logging. + this.serilogLogger.Debug("Serilog Debug"); + this.serilogLogger.Error("Serilog Error"); + this.serilogLogger.Fatal("Serilog Fatal"); + this.serilogLogger.Information("Serilog Information"); + this.serilogLogger.Verbose("Serilog Verbose"); + this.serilogLogger.Warning("Serilog Warning"); + + // Show the extension logging. + this.extensionLogger.LogCritical( + "System.Extension.Logging LogCritical"); + this.extensionLogger.LogDebug( + "System.Extension.Logging LogDebug"); + this.extensionLogger.LogError( + "System.Extension.Logging LogError"); + this.extensionLogger.LogInformation( + "System.Extension.Logging LogInformation"); + this.extensionLogger.LogTrace( + "System.Extension.Logging LogTrace"); + this.extensionLogger.LogWarning( + "System.Extension.Logging LogWarning"); + + // Show Serilog working through logging extensions. + var hash = new { Number = 1, String = "String" }; + + this.extensionLogger.LogInformation( + "Contextual information via {Name} and {Quotes:l}", + "extension logger", + "without quotes"); + this.extensionLogger.LogInformation( + "Contextual information via {@Nested}", + hash); + + // We're good. + return Task.FromResult(0); + } +} diff --git a/tests/SampleTool/Program.cs b/tests/SampleTool/Program.cs index 9ef9de8..924962b 100644 --- a/tests/SampleTool/Program.cs +++ b/tests/SampleTool/Program.cs @@ -10,11 +10,10 @@ namespace SampleTool { public static async Task Main(string[] args) { - return await ToolBuilder - .Create( - new ToolNames("SampleApplication"), - args) + return await ToolBoxBuilder + .Create("SampleTool", args) .ConfigureContainer(ConfigureContainer) + .Build() .RunAsync(); } diff --git a/tests/SampleTool/SampleTool.csproj b/tests/SampleTool/SampleTool.csproj index d9b0e68..de830d0 100644 --- a/tests/SampleTool/SampleTool.csproj +++ b/tests/SampleTool/SampleTool.csproj @@ -2,11 +2,11 @@ Exe - net5.0 + net6.0 - + diff --git a/tests/SampleTool/SampleToolModule.cs b/tests/SampleTool/SampleToolModule.cs index 1ce0a71..6aa17b3 100644 --- a/tests/SampleTool/SampleToolModule.cs +++ b/tests/SampleTool/SampleToolModule.cs @@ -1,4 +1,8 @@ -using Autofac; +using System.CommandLine; + +using Autofac; + +using SampleTool; namespace MfGames.ToolBuilder { @@ -14,6 +18,25 @@ namespace MfGames.ToolBuilder .RegisterAssemblyTypes(this.GetType().Assembly) .AsSelf() .AsImplementedInterfaces(); + + builder + .Register( + c => + { + var root = new RootCommand + { + Name = "sample-tool", + Description = + "A sample tool that demonstrates functionality", + }; + + root.AddCommand(c.Resolve()); + root.AddCommand(c.Resolve()); + root.AddCommand(c.Resolve()); + + return root; + }) + .AsSelf(); } } } diff --git a/tests/SampleTool/TableTopCommand.cs b/tests/SampleTool/TableCommand.cs similarity index 87% rename from tests/SampleTool/TableTopCommand.cs rename to tests/SampleTool/TableCommand.cs index cce31a9..193bdea 100644 --- a/tests/SampleTool/TableTopCommand.cs +++ b/tests/SampleTool/TableCommand.cs @@ -2,23 +2,21 @@ using System; using System.Collections.Generic; using System.CommandLine; using System.CommandLine.Invocation; -using System.CommandLine.IO; using System.Data; using System.Threading.Tasks; -using MfGames.ToolBuilder; using MfGames.ToolBuilder.Tables; namespace SampleTool { - public class TableTopCommand : Command, ITopCommand, ICommandHandler + public class TableCommand : Command, ICommandHandler { private readonly DataTable table; private readonly TableToolService tableService; /// - public TableTopCommand(TableToolService.Factory tableService) + public TableCommand(TableToolService.Factory tableService) : base("table", "Display a table.") { // Create the table structure. @@ -36,7 +34,7 @@ namespace SampleTool "DefaultString", "DefaultInt32", }); - + // This class handles the command. this.Handler = this; }