diff --git a/RELEASE.md b/RELEASE.md index c98e00ae9..c66092747 100644 --- a/RELEASE.md +++ b/RELEASE.md @@ -1,6 +1,9 @@ # 1.0.0-alpha.9 -- Removed `CustomBuildCommand` and replaced with fluent `IBootstrapper.AddCommand()` overloads. +- Added `IBootstrapper.AddDelegateCommand()` fluent methods to configure delegate-based commands. +- Added `IBootstrapper.AddBuildCommand()` fluent methods to configure simple commands that build specified pipelines. +- Refactored the base commands to allow consumers to derive from `EngineCommand`. +- Added a new `IEngineManager` interface to expose the engine manager to commands that derive from `EngineCommand`. - Refactored `IEngine.Settings` and `IExecutionContext.Settings` to use a `IConfiguration` as the backing store and present it as metadata. - Lazily creates type-based pipelines using the DI container so they can have injected services (#59). - Adds `INamedPipeline` to allow pipeline instances to provide names. diff --git a/azure-pipelines.yml b/azure-pipelines.yml index aa8fbf77d..4b8796bee 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -8,7 +8,7 @@ jobs: version: '2.x' - task: UseDotNet@2 inputs: - version: '3.x' + version: '3.0.x' - script: build -target BuildServer env: SYSTEM_ACCESSTOKEN: $(System.AccessToken) @@ -23,7 +23,7 @@ jobs: version: '2.x' - task: UseDotNet@2 inputs: - version: '3.x' + version: '3.0.x' includePreviewVersions: true - script: pwsh ./build.ps1 -target BuildServer env: @@ -37,7 +37,7 @@ jobs: version: '2.x' - task: UseDotNet@2 inputs: - version: '3.x' + version: '3.0.x' includePreviewVersions: true - script: pwsh ./build.ps1 -target BuildServer env: diff --git a/src/core/Statiq.App/Bootstrapper/Bootstrapper.cs b/src/core/Statiq.App/Bootstrapper/Bootstrapper.cs index cd7ed6617..caf2facae 100644 --- a/src/core/Statiq.App/Bootstrapper/Bootstrapper.cs +++ b/src/core/Statiq.App/Bootstrapper/Bootstrapper.cs @@ -58,7 +58,7 @@ public async Task RunAsync() Configurators.Configure(this); // Get our initial settings for configuration - SettingsConfigurationProvider settingsProvider = new SettingsConfigurationProvider(); + EngineSettingsConfigurationProvider settingsProvider = new EngineSettingsConfigurationProvider(); ConfigurableSettings configurableSettings = new ConfigurableSettings(settingsProvider); Configurators.Configure(configurableSettings); @@ -85,7 +85,7 @@ public async Task RunAsync() // Create the stand-alone command line service container and register a few types needed for the CLI CommandServiceTypeRegistrar registrar = new CommandServiceTypeRegistrar(); - registrar.RegisterInstance(typeof(SettingsConfigurationProvider), settingsProvider); + registrar.RegisterInstance(typeof(IEngineSettingsDictionary), settingsProvider); registrar.RegisterInstance(typeof(IConfigurationRoot), configurationRoot); registrar.RegisterInstance(typeof(IServiceCollection), serviceCollection); registrar.RegisterInstance(typeof(IBootstrapper), this); diff --git a/src/core/Statiq.App/Bootstrapper/IBootstrapper.CommandDefaults.cs b/src/core/Statiq.App/Bootstrapper/IBootstrapper.CommandDefaults.cs index 88d24f004..e01598da6 100644 --- a/src/core/Statiq.App/Bootstrapper/IBootstrapper.CommandDefaults.cs +++ b/src/core/Statiq.App/Bootstrapper/IBootstrapper.CommandDefaults.cs @@ -38,28 +38,61 @@ public IBootstrapper AddCommand(Type commandType, string name) return this; } - public IBootstrapper AddCommand(string name, EngineCommandSettings commandSettings) - { - _ = name ?? throw new ArgumentNullException(nameof(name)); - _ = commandSettings ?? throw new ArgumentNullException(nameof(commandSettings)); - return ConfigureCommands(x => x.AddCommand(name).WithData(commandSettings)); - } + public IBootstrapper AddBuildCommand( + string name, + params string[] pipelines) => + AddBuildCommand(name, null, false, pipelines); - public IBootstrapper AddCommand( + public IBootstrapper AddBuildCommand( string name, + string description, + params string[] pipelines) => + AddBuildCommand(name, description, false, pipelines); + + public IBootstrapper AddBuildCommand( + string name, + bool defaultPipelines, params string[] pipelines) => - AddCommand(name, false, pipelines); + AddBuildCommand(name, null, defaultPipelines, pipelines); - public IBootstrapper AddCommand( + public IBootstrapper AddBuildCommand( string name, + string description, bool defaultPipelines, params string[] pipelines) => - AddCommand(name, new EngineCommandSettings + AddBuildCommand(name, description, new EngineCommandSettings { Pipelines = pipelines, DefaultPipelines = defaultPipelines }); + public IBootstrapper AddBuildCommand(string name, EngineCommandSettings commandSettings) => + AddBuildCommand(name, null, commandSettings); + + public IBootstrapper AddBuildCommand(string name, string description, EngineCommandSettings commandSettings) + { + _ = name ?? throw new ArgumentNullException(nameof(name)); + _ = commandSettings ?? throw new ArgumentNullException(nameof(commandSettings)); + return ConfigureCommands(x => x + .AddCommand>(name) + .WithData(commandSettings) + .WithDescription(description)); + } + + public IBootstrapper AddDelegateCommand(string name, Func func) => + AddDelegateCommand(name, null, (c, _) => func(c)); + + public IBootstrapper AddDelegateCommand(string name, string description, Func func) => + AddDelegateCommand(name, description, (c, _) => func(c)); + + public IBootstrapper AddDelegateCommand(string name, Func func) + where TSettings : CommandSettings => + AddDelegateCommand(name, null, func); + + public IBootstrapper AddDelegateCommand(string name, string description, Func func) + where TSettings : CommandSettings => + ConfigureCommands(x => x.AddDelegate(name, func).WithDescription(description)); + public IBootstrapper AddCommands(Assembly assembly) { _ = assembly ?? throw new ArgumentNullException(nameof(assembly)); diff --git a/src/core/Statiq.App/Bootstrapper/IBootstrapper.DefaultDefaults.cs b/src/core/Statiq.App/Bootstrapper/IBootstrapper.DefaultDefaults.cs index 6bf0a037d..e37f9931b 100644 --- a/src/core/Statiq.App/Bootstrapper/IBootstrapper.DefaultDefaults.cs +++ b/src/core/Statiq.App/Bootstrapper/IBootstrapper.DefaultDefaults.cs @@ -109,8 +109,8 @@ public IBootstrapper AddDefaultConfiguration() => public IBootstrapper AddDefaultCommands() { - SetDefaultCommand(); - AddCommand(); + SetDefaultCommand>(); + AddCommand>(); AddCommand(); AddCommands(); return this; diff --git a/src/core/Statiq.App/Bootstrapper/IBootstrapper.PipelinesDefaults.cs b/src/core/Statiq.App/Bootstrapper/IBootstrapper.PipelinesDefaults.cs index 83d8ed633..5a522b604 100644 --- a/src/core/Statiq.App/Bootstrapper/IBootstrapper.PipelinesDefaults.cs +++ b/src/core/Statiq.App/Bootstrapper/IBootstrapper.PipelinesDefaults.cs @@ -16,7 +16,7 @@ public IBootstrapper AddPipelines( ConfigureEngine(x => action(x.Pipelines)); public IBootstrapper AddPipelines( - Action action) => + Action action) => ConfigureEngine(x => action(x.Settings, x.Pipelines)); // By type @@ -27,10 +27,10 @@ public IBootstrapper AddPipeline(string name, IPipeline pipeline) => public IBootstrapper AddPipeline(IPipeline pipeline) => ConfigureEngine(x => x.Pipelines.Add(pipeline)); - public IBootstrapper AddPipeline(string name, Func pipelineFunc) => + public IBootstrapper AddPipeline(string name, Func pipelineFunc) => ConfigureEngine(x => x.Pipelines.Add(name, pipelineFunc(x.Settings))); - public IBootstrapper AddPipeline(Func pipelineFunc) => + public IBootstrapper AddPipeline(Func pipelineFunc) => ConfigureEngine(x => x.Pipelines.Add(pipelineFunc(x.Settings))); public IBootstrapper AddPipeline(Type pipelineType) diff --git a/src/core/Statiq.App/Commands/BuildCommand.cs b/src/core/Statiq.App/Commands/BuildCommand.cs index f1666e1fa..20992852f 100644 --- a/src/core/Statiq.App/Commands/BuildCommand.cs +++ b/src/core/Statiq.App/Commands/BuildCommand.cs @@ -8,24 +8,29 @@ namespace Statiq.App { - [Description("Runs the pipelines.")] - internal class BuildCommand : EngineCommand + [Description("Executes the pipelines.")] + internal class BuildCommand : EngineCommand + where TSettings : BaseCommandSettings { - public BuildCommand(SettingsConfigurationProvider settingsProvider, IConfigurationRoot configurationRoot, IServiceCollection serviceCollection, IBootstrapper bootstrapper) - : base(settingsProvider, configurationRoot, serviceCollection, bootstrapper) + public BuildCommand( + IEngineSettingsDictionary engineSettings, + IConfigurationRoot configurationRoot, + IServiceCollection serviceCollection, + IBootstrapper bootstrapper) + : base(engineSettings, configurationRoot, serviceCollection, bootstrapper) { } - public override async Task ExecuteCommandAsync(CommandContext context, EngineCommandSettings settings) + protected override async Task ExecuteEngineAsync( + CommandContext commandContext, + TSettings commandSettings, + IEngineManager engineManager) { using (CancellationTokenSource cancellationTokenSource = new CancellationTokenSource()) { - using (EngineManager engineManager = new EngineManager(context, this, settings)) - { - return await engineManager.ExecuteAsync(cancellationTokenSource) - ? (int)ExitCode.Normal - : (int)ExitCode.ExecutionError; - } + return await engineManager.ExecuteAsync(cancellationTokenSource) + ? (int)ExitCode.Normal + : (int)ExitCode.ExecutionError; } } } diff --git a/src/core/Statiq.App/Commands/CustomBuildCommand.cs b/src/core/Statiq.App/Commands/CustomBuildCommand.cs deleted file mode 100644 index a9309a6f2..000000000 --- a/src/core/Statiq.App/Commands/CustomBuildCommand.cs +++ /dev/null @@ -1,39 +0,0 @@ -using System.ComponentModel; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.DependencyInjection; -using Spectre.Cli; -using Statiq.Common; - -namespace Statiq.App -{ - [Description("Runs the pipelines.")] - internal class CustomBuildCommand : EngineCommand - { - public CustomBuildCommand(SettingsConfigurationProvider settingsProvider, IConfigurationRoot configurationRoot, IServiceCollection serviceCollection, IBootstrapper bootstrapper) - : base(settingsProvider, configurationRoot, serviceCollection, bootstrapper) - { - } - - public override async Task ExecuteCommandAsync(CommandContext context, BaseCommandSettings settings) - { - EngineCommandSettings engineSettings = context.Data as EngineCommandSettings ?? new EngineCommandSettings(); - - // Copy over the base settings - engineSettings.LogLevel = settings.LogLevel; - engineSettings.Attach = settings.Attach; - engineSettings.LogFile = settings.LogFile; - - using (CancellationTokenSource cancellationTokenSource = new CancellationTokenSource()) - { - using (EngineManager engineManager = new EngineManager(context, this, engineSettings)) - { - return await engineManager.ExecuteAsync(cancellationTokenSource) - ? (int)ExitCode.Normal - : (int)ExitCode.ExecutionError; - } - } - } - } -} diff --git a/src/core/Statiq.App/Commands/EngineCommandData.cs b/src/core/Statiq.App/Commands/EngineCommandData.cs deleted file mode 100644 index 184d41323..000000000 --- a/src/core/Statiq.App/Commands/EngineCommandData.cs +++ /dev/null @@ -1,20 +0,0 @@ -using System.Collections.Generic; - -namespace Statiq.App -{ - internal class EngineCommandData - { - public EngineCommandData(string[] pipelines, bool defaultPipelines, IEnumerable> settings) - { - Pipelines = pipelines; - DefaultPipelines = defaultPipelines; - Settings = settings; - } - - public string[] Pipelines { get; } - - public bool DefaultPipelines { get; } - - public IEnumerable> Settings { get; } - } -} diff --git a/src/core/Statiq.App/Commands/EngineCommand{TSettings}.cs b/src/core/Statiq.App/Commands/EngineCommand{TSettings}.cs index 7c23d1da8..88d121459 100644 --- a/src/core/Statiq.App/Commands/EngineCommand{TSettings}.cs +++ b/src/core/Statiq.App/Commands/EngineCommand{TSettings}.cs @@ -1,37 +1,61 @@ -using System; -using System.Diagnostics; -using System.IO; -using System.Threading; -using System.Threading.Tasks; +using System.Threading.Tasks; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Options; -using NetEscapades.Extensions.Logging.RollingFile; using Spectre.Cli; -using Statiq.Common; -using Statiq.Core; namespace Statiq.App { - internal abstract class EngineCommand : BaseCommand, IEngineCommand + public abstract class EngineCommand : BaseCommand where TSettings : BaseCommandSettings { - protected EngineCommand(SettingsConfigurationProvider settingsProvider, IConfigurationRoot configurationRoot, IServiceCollection serviceCollection, IBootstrapper bootstrapper) + protected EngineCommand( + IEngineSettingsDictionary engineSettings, + IConfigurationRoot configurationRoot, + IServiceCollection serviceCollection, + IBootstrapper bootstrapper) : base(serviceCollection) { - SettingsProvider = settingsProvider; + EngineSettings = engineSettings; ConfigurationRoot = configurationRoot; ServiceCollection = serviceCollection; Bootstrapper = bootstrapper; } - public SettingsConfigurationProvider SettingsProvider { get; } + public IEngineSettingsDictionary EngineSettings { get; } public IConfigurationRoot ConfigurationRoot { get; } public IServiceCollection ServiceCollection { get; } public IBootstrapper Bootstrapper { get; } + + public override sealed async Task ExecuteCommandAsync(CommandContext commandContext, TSettings commandSettings) + { + // We need to get the engine command settings to pass to the engine manager + // First try the actual command settings + if (!(commandSettings is EngineCommandSettings engineCommandSettings)) + { + // Then try the command data or create one and either way copy over the base command settings + engineCommandSettings = commandContext.Data as EngineCommandSettings ?? new EngineCommandSettings(); + engineCommandSettings.LogLevel = commandSettings.LogLevel; + engineCommandSettings.Attach = commandSettings.Attach; + engineCommandSettings.LogFile = commandSettings.LogFile; + } + + // Execute the engine manager and dispose it when done + using (EngineManager engineManager = + new EngineManager( + commandContext, + engineCommandSettings, + EngineSettings, + ConfigurationRoot, + ServiceCollection, + Bootstrapper)) + { + return await ExecuteEngineAsync(commandContext, commandSettings, engineManager); + } + } + + protected abstract Task ExecuteEngineAsync(CommandContext commandContext, TSettings commandSettings, IEngineManager engineManager); } } diff --git a/src/core/Statiq.App/Commands/EngineManager.cs b/src/core/Statiq.App/Commands/EngineManager.cs index efe65bfd0..f84974fb1 100644 --- a/src/core/Statiq.App/Commands/EngineManager.cs +++ b/src/core/Statiq.App/Commands/EngineManager.cs @@ -16,17 +16,23 @@ namespace Statiq.App /// /// This class can be used from commands to wrap engine execution and apply settings, etc. /// - internal class EngineManager : IDisposable + internal class EngineManager : IEngineManager, IDisposable { private readonly ILogger _logger; private readonly string[] _pipelines; private readonly bool _defaultPipelines; - public EngineManager(CommandContext context, IEngineCommand command, EngineCommandSettings settings) + public EngineManager( + CommandContext commandContext, + EngineCommandSettings commandSettings, + IDictionary engineSettings, + IConfigurationRoot configurationRoot, + IServiceCollection serviceCollection, + IBootstrapper bootstrapper) { // Get the standard input stream string input = null; - if (settings?.StdIn == true) + if (commandSettings?.StdIn == true) { using (StreamReader reader = new StreamReader(Console.OpenStandardInput(), Console.InputEncoding)) { @@ -35,29 +41,29 @@ public EngineManager(CommandContext context, IEngineCommand command, EngineComma } // Create the application state - ApplicationState applicationState = new ApplicationState(command.Bootstrapper.Arguments, context.Name, input); + ApplicationState applicationState = new ApplicationState(bootstrapper.Arguments, commandContext.Name, input); // Create the engine and get a logger - Engine = new Engine(applicationState, command.ConfigurationRoot, command.ServiceCollection); + Engine = new Engine(applicationState, configurationRoot, serviceCollection); _logger = Engine.Services.GetRequiredService>(); // Apply command settings - if (settings != null) + if (commandSettings != null) { - ApplyCommandSettings(Engine, command.SettingsProvider, settings); + ApplyCommandSettings(Engine, engineSettings, commandSettings); } - _pipelines = settings?.Pipelines; - _defaultPipelines = settings == null || settings.Pipelines == null || settings.Pipelines.Length == 0 || settings.DefaultPipelines; + _pipelines = commandSettings?.Pipelines; + _defaultPipelines = commandSettings == null || commandSettings.Pipelines == null || commandSettings.Pipelines.Length == 0 || commandSettings.DefaultPipelines; // Run engine configurators after command line, settings, etc. have been applied - command.Bootstrapper.Configurators.Configure(Engine); + bootstrapper.Configurators.Configure(Engine); // Log the full environment _logger.LogInformation($"Root path:{Environment.NewLine} {Engine.FileSystem.RootPath}"); _logger.LogInformation($"Input path(s):{Environment.NewLine} {string.Join(Environment.NewLine + " ", Engine.FileSystem.InputPaths)}"); _logger.LogInformation($"Output path:{Environment.NewLine} {Engine.FileSystem.OutputPath}"); _logger.LogInformation($"Temp path:{Environment.NewLine} {Engine.FileSystem.TempPath}"); - _logger.LogDebug($"Configuration:{Environment.NewLine}{command.ConfigurationRoot.GetSafeDebugView()}"); + _logger.LogDebug($"Configuration:{Environment.NewLine}{configurationRoot.GetSafeDebugView()}"); } public Engine Engine { get; } diff --git a/src/core/Statiq.App/Commands/IEngineCommand.cs b/src/core/Statiq.App/Commands/IEngineCommand.cs deleted file mode 100644 index a15f72781..000000000 --- a/src/core/Statiq.App/Commands/IEngineCommand.cs +++ /dev/null @@ -1,16 +0,0 @@ -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.DependencyInjection; - -namespace Statiq.App -{ - internal interface IEngineCommand - { - SettingsConfigurationProvider SettingsProvider { get; } - - IConfigurationRoot ConfigurationRoot { get; } - - IServiceCollection ServiceCollection { get; } - - IBootstrapper Bootstrapper { get; } - } -} diff --git a/src/core/Statiq.App/Commands/IEngineManager.cs b/src/core/Statiq.App/Commands/IEngineManager.cs new file mode 100644 index 000000000..151a4f36a --- /dev/null +++ b/src/core/Statiq.App/Commands/IEngineManager.cs @@ -0,0 +1,13 @@ +using System.Threading; +using System.Threading.Tasks; +using Statiq.Core; + +namespace Statiq.App +{ + public interface IEngineManager + { + Engine Engine { get; } + + Task ExecuteAsync(CancellationTokenSource cancellationTokenSource); + } +} diff --git a/src/core/Statiq.App/Commands/PreviewCommand.cs b/src/core/Statiq.App/Commands/PreviewCommand.cs index 66127c6ff..8ef1bb6c2 100644 --- a/src/core/Statiq.App/Commands/PreviewCommand.cs +++ b/src/core/Statiq.App/Commands/PreviewCommand.cs @@ -21,158 +21,162 @@ internal class PreviewCommand : EngineCommand private readonly AutoResetEvent _messageEvent = new AutoResetEvent(false); private readonly InterlockedBool _exit = new InterlockedBool(false); - public PreviewCommand(SettingsConfigurationProvider settingsProvider, IConfigurationRoot configurationRoot, IServiceCollection serviceCollection, IBootstrapper bootstrapper) - : base(settingsProvider, configurationRoot, serviceCollection, bootstrapper) + public PreviewCommand( + IEngineSettingsDictionary engineSettings, + IConfigurationRoot configurationRoot, + IServiceCollection serviceCollection, + IBootstrapper bootstrapper) + : base(engineSettings, configurationRoot, serviceCollection, bootstrapper) { } - public override async Task ExecuteCommandAsync(CommandContext context, PreviewCommandSettings settings) + protected override async Task ExecuteEngineAsync( + CommandContext commandContext, + PreviewCommandSettings commandSettings, + IEngineManager engineManager) { ExitCode exitCode = ExitCode.Normal; - using (EngineManager engineManager = new EngineManager(context, this, settings)) - { - ILogger logger = engineManager.Engine.Services.GetRequiredService>(); + ILogger logger = engineManager.Engine.Services.GetRequiredService>(); - // Execute the engine for the first time - using (CancellationTokenSource cancellationTokenSource = new CancellationTokenSource()) + // Execute the engine for the first time + using (CancellationTokenSource cancellationTokenSource = new CancellationTokenSource()) + { + if (!await engineManager.ExecuteAsync(cancellationTokenSource)) { - if (!await engineManager.ExecuteAsync(cancellationTokenSource)) - { - return (int)ExitCode.ExecutionError; - } + return (int)ExitCode.ExecutionError; } + } - // Start the preview server - Dictionary contentTypes = settings.ContentTypes?.Length > 0 - ? GetContentTypes(settings.ContentTypes) - : new Dictionary(); - ILoggerProvider loggerProvider = engineManager.Engine.Services.GetRequiredService(); - Server previewServer = await StartPreviewServerAsync( + // Start the preview server + Dictionary contentTypes = commandSettings.ContentTypes?.Length > 0 + ? GetContentTypes(commandSettings.ContentTypes) + : new Dictionary(); + ILoggerProvider loggerProvider = engineManager.Engine.Services.GetRequiredService(); + Server previewServer = await StartPreviewServerAsync( + engineManager.Engine.FileSystem.GetOutputDirectory().Path, + commandSettings.Port, + commandSettings.ForceExt, + commandSettings.VirtualDirectory, + !commandSettings.NoReload, + contentTypes, + loggerProvider, + logger); + + // Start the watchers + ActionFileSystemWatcher inputFolderWatcher = null; + if (!commandSettings.NoWatch) + { + logger.LogInformation("Watching paths(s) {0}", string.Join(", ", engineManager.Engine.FileSystem.InputPaths)); + inputFolderWatcher = new ActionFileSystemWatcher( engineManager.Engine.FileSystem.GetOutputDirectory().Path, - settings.Port, - settings.ForceExt, - settings.VirtualDirectory, - !settings.NoReload, - contentTypes, - loggerProvider, - logger); - - // Start the watchers - ActionFileSystemWatcher inputFolderWatcher = null; - if (!settings.NoWatch) - { - logger.LogInformation("Watching paths(s) {0}", string.Join(", ", engineManager.Engine.FileSystem.InputPaths)); - inputFolderWatcher = new ActionFileSystemWatcher( - engineManager.Engine.FileSystem.GetOutputDirectory().Path, - engineManager.Engine.FileSystem.GetInputDirectories().Select(x => x.Path), - true, - "*.*", - path => - { - _changedFiles.Enqueue(path); - _messageEvent.Set(); - }); - } + engineManager.Engine.FileSystem.GetInputDirectories().Select(x => x.Path), + true, + "*.*", + path => + { + _changedFiles.Enqueue(path); + _messageEvent.Set(); + }); + } - // Start the message pump + // Start the message pump - // Only wait for a key if console input has not been redirected, otherwise it's on the caller to exit - if (!Console.IsInputRedirected) + // Only wait for a key if console input has not been redirected, otherwise it's on the caller to exit + if (!Console.IsInputRedirected) + { + // Start the key listening thread + Thread thread = new Thread(() => { - // Start the key listening thread - Thread thread = new Thread(() => + logger.LogInformation("Hit Ctrl-C to exit"); + Console.TreatControlCAsInput = true; + while (true) { - logger.LogInformation("Hit Ctrl-C to exit"); - Console.TreatControlCAsInput = true; - while (true) + // Would have preferred to use Console.CancelKeyPress, but that bubbles up to calling batch files + // The (ConsoleKey)3 check is to support a bug in VS Code: https://github.com/Microsoft/vscode/issues/9347 + ConsoleKeyInfo consoleKey = Console.ReadKey(true); + if (consoleKey.Key == (ConsoleKey)3 || (consoleKey.Key == ConsoleKey.C && (consoleKey.Modifiers & ConsoleModifiers.Control) != 0)) { - // Would have preferred to use Console.CancelKeyPress, but that bubbles up to calling batch files - // The (ConsoleKey)3 check is to support a bug in VS Code: https://github.com/Microsoft/vscode/issues/9347 - ConsoleKeyInfo consoleKey = Console.ReadKey(true); - if (consoleKey.Key == (ConsoleKey)3 || (consoleKey.Key == ConsoleKey.C && (consoleKey.Modifiers & ConsoleModifiers.Control) != 0)) - { - _exit.Set(); - _messageEvent.Set(); - break; - } + _exit.Set(); + _messageEvent.Set(); + break; } - }) - { - IsBackground = true - }; - thread.Start(); + } + }) + { + IsBackground = true + }; + thread.Start(); + } + + // Wait for activity + while (true) + { + _messageEvent.WaitOne(); // Blocks the current thread until a signal + if (_exit) + { + break; } - // Wait for activity - while (true) + // Execute if files have changed + HashSet changedFiles = new HashSet(); + while (_changedFiles.TryDequeue(out string changedFile)) { - _messageEvent.WaitOne(); // Blocks the current thread until a signal - if (_exit) + if (changedFiles.Add(changedFile)) { - break; + logger.LogDebug($"{changedFile} has changed"); } + } + if (changedFiles.Count > 0) + { + logger.LogInformation($"{changedFiles.Count} files have changed, re-executing"); - // Execute if files have changed - HashSet changedFiles = new HashSet(); - while (_changedFiles.TryDequeue(out string changedFile)) + // Reset caches when an error occurs during the previous preview + string existingResetCacheSetting = null; + bool setResetCacheSetting = false; + if (exitCode == ExitCode.ExecutionError) { - if (changedFiles.Add(changedFile)) - { - logger.LogDebug($"{changedFile} has changed"); - } + existingResetCacheSetting = engineManager.Engine.Settings.GetString(Keys.ResetCache); + setResetCacheSetting = true; + EngineSettings[Keys.ResetCache] = "true"; } - if (changedFiles.Count > 0) - { - logger.LogInformation($"{changedFiles.Count} files have changed, re-executing"); - // Reset caches when an error occurs during the previous preview - string existingResetCacheSetting = null; - bool setResetCacheSetting = false; - if (exitCode == ExitCode.ExecutionError) - { - existingResetCacheSetting = engineManager.Engine.Settings.GetString(Keys.ResetCache); - setResetCacheSetting = true; - SettingsProvider[Keys.ResetCache] = "true"; - } + // If there was an execution error due to reload, keep previewing but clear the cache + using (CancellationTokenSource cancellationTokenSource = new CancellationTokenSource()) + { + exitCode = await engineManager.ExecuteAsync(cancellationTokenSource) + ? ExitCode.Normal + : ExitCode.ExecutionError; + } - // If there was an execution error due to reload, keep previewing but clear the cache - using (CancellationTokenSource cancellationTokenSource = new CancellationTokenSource()) + // Reset the reset cache setting after removing it + if (setResetCacheSetting) + { + if (existingResetCacheSetting == null) { - exitCode = await engineManager.ExecuteAsync(cancellationTokenSource) - ? ExitCode.Normal - : ExitCode.ExecutionError; + EngineSettings.Remove(Keys.ResetCache); } - - // Reset the reset cache setting after removing it - if (setResetCacheSetting) { - if (existingResetCacheSetting == null) - { - SettingsProvider.Remove(Keys.ResetCache); - } - { - SettingsProvider[Keys.ResetCache] = existingResetCacheSetting; - } + EngineSettings[Keys.ResetCache] = existingResetCacheSetting; } - - await previewServer.TriggerReloadAsync(); } - // Check one more time for exit - if (_exit) - { - break; - } - logger.LogInformation("Hit Ctrl-C to exit"); - _messageEvent.Reset(); + await previewServer.TriggerReloadAsync(); } - // Shutdown - logger.LogInformation("Shutting down"); - inputFolderWatcher?.Dispose(); - previewServer.Dispose(); + // Check one more time for exit + if (_exit) + { + break; + } + logger.LogInformation("Hit Ctrl-C to exit"); + _messageEvent.Reset(); } + // Shutdown + logger.LogInformation("Shutting down"); + inputFolderWatcher?.Dispose(); + previewServer.Dispose(); + return (int)exitCode; } diff --git a/src/core/Statiq.App/Configuration/SettingsConfigurationProvider.cs b/src/core/Statiq.App/Configuration/EngineSettingsConfigurationProvider.cs similarity index 92% rename from src/core/Statiq.App/Configuration/SettingsConfigurationProvider.cs rename to src/core/Statiq.App/Configuration/EngineSettingsConfigurationProvider.cs index a7d4a0656..3f96c3dd9 100644 --- a/src/core/Statiq.App/Configuration/SettingsConfigurationProvider.cs +++ b/src/core/Statiq.App/Configuration/EngineSettingsConfigurationProvider.cs @@ -6,7 +6,7 @@ namespace Statiq.App { - internal class SettingsConfigurationProvider : ConfigurationProvider, IConfigurationSource, IDictionary + internal class EngineSettingsConfigurationProvider : ConfigurationProvider, IConfigurationSource, IEngineSettingsDictionary { public IConfigurationProvider Build(IConfigurationBuilder builder) => this; diff --git a/src/core/Statiq.App/Configuration/IEngineSettingsDictionary.cs b/src/core/Statiq.App/Configuration/IEngineSettingsDictionary.cs new file mode 100644 index 000000000..859437448 --- /dev/null +++ b/src/core/Statiq.App/Configuration/IEngineSettingsDictionary.cs @@ -0,0 +1,8 @@ +using System.Collections.Generic; + +namespace Statiq.App +{ + public interface IEngineSettingsDictionary : IDictionary + { + } +} diff --git a/src/core/Statiq.App/Configuration/PipelineBuilder.cs b/src/core/Statiq.App/Configuration/PipelineBuilder.cs index fe4cf01f4..d6a24aa72 100644 --- a/src/core/Statiq.App/Configuration/PipelineBuilder.cs +++ b/src/core/Statiq.App/Configuration/PipelineBuilder.cs @@ -14,13 +14,13 @@ public class PipelineBuilder private readonly IPipelineCollection _collection; - internal PipelineBuilder(IPipelineCollection collection, ISettings settings) + internal PipelineBuilder(IPipelineCollection collection, IEngineSettings settings) { _collection = collection; Settings = settings; } - public ISettings Settings { get; } + public IEngineSettings Settings { get; } internal IPipeline Build() { diff --git a/src/core/Statiq.App/Statiq.App.csproj b/src/core/Statiq.App/Statiq.App.csproj index b7de70abe..7052f972a 100644 --- a/src/core/Statiq.App/Statiq.App.csproj +++ b/src/core/Statiq.App/Statiq.App.csproj @@ -14,7 +14,7 @@ - + diff --git a/src/core/Statiq.Common/Config/Config.FromSetting.cs b/src/core/Statiq.Common/Config/Config.FromSetting.cs index 2b55c939c..a0d107b1e 100644 --- a/src/core/Statiq.Common/Config/Config.FromSetting.cs +++ b/src/core/Statiq.Common/Config/Config.FromSetting.cs @@ -40,7 +40,7 @@ public static Config FromSetting(string key, object defaultValue = null) /// The type of config value. /// The delegate that produces the config value. /// A config object. - public static Config FromSettings(Func func) + public static Config FromSettings(Func func) { _ = func ?? throw new ArgumentNullException(nameof(func)); return new Config((_, ctx) => Task.FromResult(func(ctx.Settings)), false); @@ -52,7 +52,7 @@ public static Config FromSettings(Func func) /// The type of config value. /// The delegate that produces the config value. /// A config object. - public static Config FromSettings(Func> func) + public static Config FromSettings(Func> func) { _ = func ?? throw new ArgumentNullException(nameof(func)); return new Config((_, ctx) => func(ctx.Settings), false); @@ -65,7 +65,7 @@ public static Config FromSettings(Func> /// The type of config value. /// A delegate action to evaluate. /// A config object. - public static Config FromSettings(Action action) => + public static Config FromSettings(Action action) => new Config((__, ctx) => { _ = action ?? throw new ArgumentNullException(nameof(action)); @@ -80,7 +80,7 @@ public static Config FromSettings(Action action) => /// The type of config value. /// A delegate action to evaluate. /// A config object. - public static Config FromSettings(Func action) => + public static Config FromSettings(Func action) => new Config(async (__, ctx) => { _ = action ?? throw new ArgumentNullException(nameof(action)); @@ -93,7 +93,7 @@ public static Config FromSettings(Func action) /// /// A delegate action to evaluate. /// A config object. - public static Config FromSettings(Action action) => + public static Config FromSettings(Action action) => FromSettings(action); /// @@ -101,7 +101,7 @@ public static Config FromSettings(Action action) => /// /// A delegate action to evaluate. /// A config object. - public static Config FromSettings(Func action) => + public static Config FromSettings(Func action) => FromSettings(action); } } diff --git a/src/core/Statiq.Common/Configuration/ConfigurationMetadata.cs b/src/core/Statiq.Common/Configuration/ConfigurationMetadata.cs index dcf66d3fa..362e2b020 100644 --- a/src/core/Statiq.Common/Configuration/ConfigurationMetadata.cs +++ b/src/core/Statiq.Common/Configuration/ConfigurationMetadata.cs @@ -18,20 +18,16 @@ internal ConfigurationMetadata(IConfiguration configuration) public IConfiguration Configuration { get; protected set; } - public bool ContainsKey(string key) - { - _ = key ?? throw new ArgumentNullException(nameof(key)); - IConfigurationSection section = Configuration.GetSection(key); - return section.Exists() && section.Value != null; - } + public bool ContainsKey(string key) => + Configuration.GetSection(key ?? throw new ArgumentNullException(nameof(key))).Exists(); public bool TryGetRaw(string key, out object value) { _ = key ?? throw new ArgumentNullException(nameof(key)); IConfigurationSection section = Configuration.GetSection(key); - if (section.Exists() && section.Value != null) + if (section.Exists()) { - value = section.Value; + value = section.Value ?? (object)new ConfigurationMetadata(section); return true; } value = default; diff --git a/src/core/Statiq.Common/Execution/IEngine.cs b/src/core/Statiq.Common/Execution/IEngine.cs index 79784efc4..8ba9c8db7 100644 --- a/src/core/Statiq.Common/Execution/IEngine.cs +++ b/src/core/Statiq.Common/Execution/IEngine.cs @@ -19,7 +19,7 @@ public interface IEngine : IConfigurable, IDocumentFactory /// /// The application configuration as metadata. /// - ISettings Settings { get; } + IEngineSettings Settings { get; } /// /// Gets global events and event handlers. diff --git a/src/core/Statiq.Common/Execution/ISettings.cs b/src/core/Statiq.Common/Execution/IEngineSettings.cs similarity index 80% rename from src/core/Statiq.Common/Execution/ISettings.cs rename to src/core/Statiq.Common/Execution/IEngineSettings.cs index 9e7866cba..6a8d39c0d 100644 --- a/src/core/Statiq.Common/Execution/ISettings.cs +++ b/src/core/Statiq.Common/Execution/IEngineSettings.cs @@ -4,7 +4,7 @@ namespace Statiq.Common { - public interface ISettings : IMetadata + public interface IEngineSettings : IMetadata { IConfiguration Configuration { get; } } diff --git a/src/core/Statiq.Common/Execution/IExecutionContext.cs b/src/core/Statiq.Common/Execution/IExecutionContext.cs index 81562fd1c..ca16fb07d 100644 --- a/src/core/Statiq.Common/Execution/IExecutionContext.cs +++ b/src/core/Statiq.Common/Execution/IExecutionContext.cs @@ -61,7 +61,7 @@ public partial interface IExecutionContext : IDocumentFactory, IServiceProvider, /// /// The application configuration as metadata. /// - ISettings Settings { get; } + IEngineSettings Settings { get; } /// /// Gets the available shortcodes. diff --git a/src/core/Statiq.Core/Execution/Engine.cs b/src/core/Statiq.Core/Execution/Engine.cs index 5522b3193..97b693bda 100644 --- a/src/core/Statiq.Core/Execution/Engine.cs +++ b/src/core/Statiq.Core/Execution/Engine.cs @@ -64,7 +64,7 @@ public Engine(ApplicationState applicationState, IConfiguration configuration, I { _pipelines = new PipelineCollection(this); ApplicationState = applicationState ?? new ApplicationState(null, null, null); - Settings = new Settings(configuration ?? new ConfigurationRoot(Array.Empty())); + Settings = new EngineSettings(configuration ?? new ConfigurationRoot(Array.Empty())); _serviceScope = GetServiceScope(serviceCollection); _logger = Services.GetRequiredService>(); DocumentFactory = new DocumentFactory(Settings); @@ -92,7 +92,7 @@ private IServiceScope GetServiceScope(IServiceCollection serviceCollection) serviceCollection.AddSingleton(ApplicationState); serviceCollection.AddSingleton(Events); serviceCollection.AddSingleton(FileSystem); - serviceCollection.AddSingleton(Settings); + serviceCollection.AddSingleton(Settings); serviceCollection.AddSingleton(Settings.Configuration); serviceCollection.AddSingleton(Shortcodes); serviceCollection.AddSingleton(MemoryStreamFactory); @@ -110,7 +110,7 @@ private IServiceScope GetServiceScope(IServiceCollection serviceCollection) public ApplicationState ApplicationState { get; } /// - public ISettings Settings { get; } + public IEngineSettings Settings { get; } /// public IEventCollection Events { get; } = new EventCollection(); diff --git a/src/core/Statiq.Core/Execution/Settings.cs b/src/core/Statiq.Core/Execution/EngineSettings.cs similarity index 71% rename from src/core/Statiq.Core/Execution/Settings.cs rename to src/core/Statiq.Core/Execution/EngineSettings.cs index 358fe5886..494c44c9c 100644 --- a/src/core/Statiq.Core/Execution/Settings.cs +++ b/src/core/Statiq.Core/Execution/EngineSettings.cs @@ -10,9 +10,9 @@ namespace Statiq.Core { - internal class Settings : ConfigurationMetadata, ISettings + internal class EngineSettings : ConfigurationMetadata, IEngineSettings { - public Settings(IConfiguration configuration) + public EngineSettings(IConfiguration configuration) : base(configuration) { } diff --git a/src/core/Statiq.Core/Execution/ExecutionContext.cs b/src/core/Statiq.Core/Execution/ExecutionContext.cs index 77b0be675..85c2834b4 100644 --- a/src/core/Statiq.Core/Execution/ExecutionContext.cs +++ b/src/core/Statiq.Core/Execution/ExecutionContext.cs @@ -50,7 +50,7 @@ internal class ExecutionContext : IExecutionContext public IReadOnlyFileSystem FileSystem => _contextData.Engine.FileSystem; /// - public ISettings Settings => _contextData.Engine.Settings; + public IEngineSettings Settings => _contextData.Engine.Settings; /// public IReadOnlyShortcodeCollection Shortcodes => _contextData.Engine.Shortcodes; diff --git a/src/core/Statiq.Testing/Execution/TestEngine.cs b/src/core/Statiq.Testing/Execution/TestEngine.cs index a70916c87..83828d4b6 100644 --- a/src/core/Statiq.Testing/Execution/TestEngine.cs +++ b/src/core/Statiq.Testing/Execution/TestEngine.cs @@ -24,7 +24,7 @@ public TestEngine() public ApplicationState ApplicationState { get; set; } /// - ISettings IEngine.Settings => Settings; + IEngineSettings IEngine.Settings => Settings; /// public IEventCollection Events { get; set; } = new TestEventCollection(); diff --git a/src/core/Statiq.Testing/Execution/TestExecutionContext.cs b/src/core/Statiq.Testing/Execution/TestExecutionContext.cs index e64ccca60..b107b6588 100644 --- a/src/core/Statiq.Testing/Execution/TestExecutionContext.cs +++ b/src/core/Statiq.Testing/Execution/TestExecutionContext.cs @@ -74,7 +74,7 @@ public TestExecutionContext(IEnumerable inputs) public Guid ExecutionId { get; set; } = Guid.NewGuid(); /// - ISettings IExecutionContext.Settings => Settings; + IEngineSettings IExecutionContext.Settings => Settings; /// public IRawAssemblyCollection DynamicAssemblies { get; set; } = new TestRawAssemblyCollection(); diff --git a/src/core/Statiq.Testing/Execution/TestSettings.cs b/src/core/Statiq.Testing/Execution/TestSettings.cs index ede61fe9d..826ff70df 100644 --- a/src/core/Statiq.Testing/Execution/TestSettings.cs +++ b/src/core/Statiq.Testing/Execution/TestSettings.cs @@ -15,7 +15,7 @@ namespace Statiq.Testing { - public class TestSettings : ConfigurationMetadata, IConfigurationProvider, IConfigurationSource, IDictionary, ISettings + public class TestSettings : ConfigurationMetadata, IConfigurationProvider, IConfigurationSource, IDictionary, IEngineSettings { private readonly Dictionary _settings = new Dictionary(StringComparer.OrdinalIgnoreCase); diff --git a/tests/core/Statiq.App.Tests/Bootstrapper/BootstrapperFixture.cs b/tests/core/Statiq.App.Tests/Bootstrapper/BootstrapperFixture.cs index 9ea0cdfd9..f0ef12dc2 100644 --- a/tests/core/Statiq.App.Tests/Bootstrapper/BootstrapperFixture.cs +++ b/tests/core/Statiq.App.Tests/Bootstrapper/BootstrapperFixture.cs @@ -26,7 +26,7 @@ public async Task LogsVersion() string[] args = new[] { "build" }; TestLoggerProvider provider = new TestLoggerProvider(); IBootstrapper bootstrapper = App.Bootstrapper.Create(args); - bootstrapper.AddCommand("build"); + bootstrapper.AddCommand>("build"); bootstrapper.ConfigureServices(services => services.AddSingleton(provider)); bootstrapper.AddPipeline("Foo"); @@ -48,7 +48,7 @@ public async Task NoPipelinesWarning() ThrowLogLevel = LogLevel.None }; IBootstrapper bootstrapper = App.Bootstrapper.Create(args); - bootstrapper.AddCommand("build"); + bootstrapper.AddCommand>("build"); bootstrapper.ConfigureServices(services => services.AddSingleton(provider)); // When @@ -76,7 +76,7 @@ public async Task SetsLogLevel(string logLevel, int expected) ThrowLogLevel = LogLevel.None }; IBootstrapper bootstrapper = App.Bootstrapper.Create(args); - bootstrapper.AddCommand("build"); + bootstrapper.AddCommand>("build"); bootstrapper.ConfigureServices(services => services.AddSingleton(provider)); bootstrapper.AddPipeline( "Foo", @@ -102,7 +102,7 @@ public async Task CatalogsType() string[] args = new[] { "build", "-l", "Debug" }; TestLoggerProvider provider = new TestLoggerProvider(); IBootstrapper bootstrapper = App.Bootstrapper.Create(args); - bootstrapper.AddCommand("build"); + bootstrapper.AddCommand>("build"); bootstrapper.ConfigureServices(services => services.AddSingleton(provider)); bootstrapper.AddPipeline("Foo"); @@ -127,7 +127,7 @@ public async Task LogsEvironmentVariablesAsMasked() }; IBootstrapper bootstrapper = App.Bootstrapper.Create(args); bootstrapper.AddEnvironmentVariables(); - bootstrapper.AddCommand("build"); + bootstrapper.AddCommand>("build"); bootstrapper.ConfigureServices(services => services.AddSingleton(provider)); // When diff --git a/tests/core/Statiq.Common.Tests/Configuration/ConfigurationMetadataFixture.cs b/tests/core/Statiq.Common.Tests/Configuration/ConfigurationMetadataFixture.cs index 13d05c07c..d5998609c 100644 --- a/tests/core/Statiq.Common.Tests/Configuration/ConfigurationMetadataFixture.cs +++ b/tests/core/Statiq.Common.Tests/Configuration/ConfigurationMetadataFixture.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.IO; using System.Text; using Microsoft.Extensions.Configuration; using NUnit.Framework; @@ -11,26 +12,112 @@ namespace Statiq.Common.Tests.Configuration [TestFixture] public class ConfigurationMetadataFixture : BaseFixture { + public class ContainsKeyTests : ConfigurationMetadataFixture + { + [TestCase("key0", true)] + [TestCase("section0", true)] + [TestCase("section0:key1", true)] + [TestCase("sectiona", false)] + [TestCase("section2:subsection0", true)] + [TestCase("section2:subsection0:key5", true)] + public void ContainsKey(string key, bool expected) + { + // Given + IConfiguration configuration = GetConfiguration(); + ConfigurationMetadata metadata = new ConfigurationMetadata(configuration); + + // When + bool result = metadata.ContainsKey(key); + + // Then + result.ShouldBe(expected); + } + } + + public class TryGetRawTests : ConfigurationMetadataFixture + { + [Test] + public void GetsSimpleValue() + { + // Given + IConfiguration configuration = GetConfiguration(); + ConfigurationMetadata metadata = new ConfigurationMetadata(configuration); + + // When + bool result = metadata.TryGetRaw("key0", out object value); + + // Then + result.ShouldBeTrue(); + value.ShouldBe("value0"); + } + + [Test] + public void GetsSectionValue() + { + // Given + IConfiguration configuration = GetConfiguration(); + ConfigurationMetadata metadata = new ConfigurationMetadata(configuration); + + // When + bool result = metadata.TryGetRaw("section0", out object value); + object value2 = null; + bool result2 = (value as ConfigurationMetadata)?.TryGetRaw("key1", out value2) ?? false; + + // Then + result.ShouldBeTrue(); + value.ShouldBeOfType(); + result2.ShouldBeTrue(); + value2.ShouldBe("value1"); + } + } + public class CountTests : ConfigurationMetadataFixture { [Test] public void EnsureCountIsNotRecursive() { // Given - IConfiguration configuration = new ConfigurationBuilder() - .AddInMemoryCollection(new Dictionary - { - { "Foo", "1" }, - { "Bar", "2" } - }).Build(); + IConfiguration configuration = GetConfiguration(); ConfigurationMetadata metadata = new ConfigurationMetadata(configuration); // When int count = metadata.Count; // Then - count.ShouldBe(2); + count.ShouldBe(14); } } + + private static IConfiguration GetConfiguration() + { + string json = @" +{ + ""key0"": ""value0"", + ""section0"": { + ""key1"": ""value1"", + ""key2"": ""value2"" + }, + ""section1"": { + ""key3"": ""3"", + ""key4"": ""value4"" + }, + ""section2"": { + ""subsection0"" : { + ""key5"": ""value5"", + ""key6"": ""value6"" + }, + ""subsection1"" : { + ""key7"": ""value7"", + ""key8"": ""value8"" + } + } +}"; + MemoryStream stream = new MemoryStream(); + StreamWriter writer = new StreamWriter(stream); + writer.Write(json); + writer.Flush(); + stream.Position = 0; + return new ConfigurationBuilder().AddJsonStream(stream).Build(); + } } } diff --git a/tests/core/Statiq.Common.Tests/Statiq.Common.Tests.csproj b/tests/core/Statiq.Common.Tests/Statiq.Common.Tests.csproj index 1e45ba31d..549ca28cd 100644 --- a/tests/core/Statiq.Common.Tests/Statiq.Common.Tests.csproj +++ b/tests/core/Statiq.Common.Tests/Statiq.Common.Tests.csproj @@ -7,6 +7,7 @@ +