From c7ce9dd0a177d4b1e9c5625be22a253b10cbfff7 Mon Sep 17 00:00:00 2001 From: Bruno Massa Date: Fri, 8 Mar 2024 17:15:32 -0500 Subject: [PATCH] fix: capture program exceptions and print in user-friendly way --- source/Helpers/SiteHelper.cs | 26 ++- .../Models/CommandLineOptions/BuildOptions.cs | 17 +- .../CommandLineOptions/GenerateOptions.cs | 12 +- .../CommandLineOptions/IGenerateOptions.cs | 5 + .../Models/CommandLineOptions/ServeOptions.cs | 8 +- source/Program.cs | 149 +++++++----------- source/SuCoS.csproj | 2 +- 7 files changed, 96 insertions(+), 123 deletions(-) diff --git a/source/Helpers/SiteHelper.cs b/source/Helpers/SiteHelper.cs index 64d2d10..a6a6d37 100644 --- a/source/Helpers/SiteHelper.cs +++ b/source/Helpers/SiteHelper.cs @@ -36,7 +36,7 @@ public static class SiteHelper } catch { - throw new FormatException($"Error reading app config {configFile}"); + throw; } var site = new Site(options, siteSettings, frontMatterParser, logger, null); @@ -97,22 +97,16 @@ public static class SiteHelper ArgumentNullException.ThrowIfNull(options); ArgumentNullException.ThrowIfNull(frontMatterParser); - try - { - // Read the main configation - var filePath = Path.Combine(options.Source, configFile); - if (!File.Exists(filePath)) - { - throw new FileNotFoundException($"The {configFile} file was not found in the specified source directory: {options.Source}"); - } - - var fileContent = File.ReadAllText(filePath); - var siteSettings = frontMatterParser.ParseSiteSettings(fileContent) ?? throw new FormatException("Error reading app config"); - return siteSettings; - } - catch + // Read the main configation + var filePath = Path.Combine(options.Source, configFile); + if (!File.Exists(filePath)) { - throw new FormatException("Error reading app config"); + throw new FileNotFoundException($"The {configFile} file was not found in the specified source directory: {options.Source}"); } + + var fileContent = File.ReadAllText(filePath); + var siteSettings = frontMatterParser.ParseSiteSettings(fileContent) + ?? throw new FormatException($"Error reading app config {configFile}"); + return siteSettings; } } diff --git a/source/Models/CommandLineOptions/BuildOptions.cs b/source/Models/CommandLineOptions/BuildOptions.cs index fa14596..9c51c14 100644 --- a/source/Models/CommandLineOptions/BuildOptions.cs +++ b/source/Models/CommandLineOptions/BuildOptions.cs @@ -1,23 +1,16 @@ +using CommandLine; + namespace SuCoS.Models.CommandLineOptions; /// /// Command line options for the build command. /// +[Verb("build", HelpText = "Builds the site")] public class BuildOptions : GenerateOptions { /// /// The path of the output files. /// - public string Output { get; } - - /// - /// Constructor - /// - /// - /// - public BuildOptions(string source, string output) - { - Source = source; - Output = string.IsNullOrEmpty(output) ? Path.Combine(source, "public") : output; - } + [Option('o', "output", Required = false, HelpText = "Output directory path")] + public required string Output { get; set; } } diff --git a/source/Models/CommandLineOptions/GenerateOptions.cs b/source/Models/CommandLineOptions/GenerateOptions.cs index 6122843..704c3ac 100644 --- a/source/Models/CommandLineOptions/GenerateOptions.cs +++ b/source/Models/CommandLineOptions/GenerateOptions.cs @@ -1,3 +1,5 @@ +using CommandLine; + namespace SuCoS.Models.CommandLineOptions; /// @@ -6,14 +8,22 @@ namespace SuCoS.Models.CommandLineOptions; public class GenerateOptions : IGenerateOptions { /// - public string Source { get; init; } = "."; + [Option('v', "verbose", Required = false, HelpText = "How verbose it must be")] + public bool Verbose { get; init; } + + /// + [Option('s', "source", Required = false, HelpText = "Source directory path")] + public required string Source { get; init; } = "."; /// + [Option('d', "draft", Required = false, HelpText = "Include draft content")] public bool Draft { get; init; } /// + [Option('f', "future", Required = false, HelpText = "Include content with dates in the future")] public bool Future { get; init; } /// + [Option('e', "expired", Required = false, HelpText = "Include content with ExpiredDate dates from the past")] public bool Expired { get; init; } } diff --git a/source/Models/CommandLineOptions/IGenerateOptions.cs b/source/Models/CommandLineOptions/IGenerateOptions.cs index 72a9114..6aa93e0 100644 --- a/source/Models/CommandLineOptions/IGenerateOptions.cs +++ b/source/Models/CommandLineOptions/IGenerateOptions.cs @@ -5,6 +5,11 @@ namespace SuCoS.Models.CommandLineOptions; /// public interface IGenerateOptions { + /// + /// How verbose it must be. + /// + bool Verbose { get; } + /// /// The path of the source files. /// diff --git a/source/Models/CommandLineOptions/ServeOptions.cs b/source/Models/CommandLineOptions/ServeOptions.cs index 9e2b5c9..a91c92a 100644 --- a/source/Models/CommandLineOptions/ServeOptions.cs +++ b/source/Models/CommandLineOptions/ServeOptions.cs @@ -1,8 +1,10 @@ +using CommandLine; + namespace SuCoS.Models.CommandLineOptions; /// /// Command line options for the serve command. /// -public class ServeOptions : GenerateOptions -{ -} +[Verb("serve", HelpText = "Starts the server")] +public class ServeOptions : GenerateOptions; + diff --git a/source/Program.cs b/source/Program.cs index 231a56f..7464da1 100644 --- a/source/Program.cs +++ b/source/Program.cs @@ -2,15 +2,18 @@ using Serilog.Events; using SuCoS.Helpers; using SuCoS.Models.CommandLineOptions; -using System.CommandLine; using System.Reflection; +using CommandLine; namespace SuCoS; /// /// The main entry point of the program. /// -public class Program +/// +/// Constructor +/// +public class Program(ILogger logger) { /// /// Basic logo of the program, for fun @@ -25,7 +28,7 @@ public class Program \/_____/\/___/ \/___/ \/___/ \/_____/ "; - private ILogger logger; + private ILogger Logger { get; set; } = logger; private static readonly string[] aliases = ["--source", "-s"]; private static readonly string[] aliasesArray = ["--draft", "-d"]; private static readonly string[] aliasesArray0 = ["--future", "-f"]; @@ -38,19 +41,10 @@ public class Program /// /// /// - public static int Main(string[] args) + public static async Task Main(string[] args) { - var logger = CreateLogger(); - var program = new Program(logger); - return program.Run(args); - } - - /// - /// Constructor - /// - public Program(ILogger logger) - { - this.logger = logger; + var program = new Program(CreateLogger()); + return await program.RunCommandLine(args); } /// @@ -58,81 +52,55 @@ public class Program /// /// /// - public int Run(string[] args) + public async Task RunCommandLine(string[] args) { - // Print the logo of the program. - OutputLogo(); - OutputWelcome(); - - // Shared options between the commands - var sourceOption = new Option(aliases, () => ".", "Source directory path"); - var draftOption = new Option(aliasesArray, "Include draft content"); - var futureOption = new Option(aliasesArray0, "Include content with dates in the future"); - var expiredOption = new Option(aliasesArray1, "Include content with ExpiredDate dates from the past"); - var verboseOption = new Option(aliasesArray2, "Verbose output"); - - // BuildCommand setup - var buildOutputOption = new Option(aliasesArray3, "Output directory path"); - - Command buildCommandHandler = new("build", "Builds the site") - { - sourceOption, - draftOption, - buildOutputOption, - futureOption, - expiredOption, - verboseOption - }; - buildCommandHandler.SetHandler((source, output, draft, future, expired, verbose) => - { - logger = CreateLogger(verbose); - - BuildOptions buildOptions = new( - source: source, - output: output) + return await CommandLine.Parser.Default.ParseArguments(args) + .WithParsed(options => { - Draft = draft, - Future = future, - Expired = expired - }; - _ = new BuildCommand(buildOptions, logger); - }, - sourceOption, buildOutputOption, draftOption, futureOption, expiredOption, verboseOption); - - // ServerCommand setup - Command serveCommandHandler = new("serve", "Starts the server") - { - sourceOption, - draftOption, - futureOption, - expiredOption, - verboseOption - }; - serveCommandHandler.SetHandler(async (source, draft, future, expired, verbose) => - { - logger = CreateLogger(verbose); - - ServeOptions serverOptions = new() + Logger = CreateLogger(options.Verbose); + }) + .WithParsed(options => { - Source = source, - Draft = draft, - Future = future, - Expired = expired - }; - - var serveCommand = new ServeCommand(serverOptions, logger, new SourceFileWatcher()); - serveCommand.StartServer(); - await Task.Delay(-1).ConfigureAwait(false); // Wait forever. - }, - sourceOption, draftOption, futureOption, expiredOption, verboseOption); - - RootCommand rootCommand = new("SuCoS commands") - { - buildCommandHandler, - serveCommandHandler - }; - - return rootCommand.Invoke(args); + options.Output = string.IsNullOrEmpty(options.Output) ? Path.Combine(options.Source, "public") : options.Output; + }) + .MapResult( + (BuildOptions options) => + { + try + { + _ = new BuildCommand(options, Logger); + } + catch (Exception ex) + { + Logger.Error($"Build failed: {ex.Message}"); + return Task.FromResult(1); + } + return Task.FromResult(0); + }, + async (ServeOptions options) => + { + try + { + var serveCommand = new ServeCommand(options, Logger, new SourceFileWatcher()); + serveCommand.StartServer(); + await Task.Delay(-1).ConfigureAwait(false); // Wait forever. + } + catch (Exception ex) + { + if (options.Verbose) + { + Logger.Error(ex, "Serving failed"); + } + else + { + Logger.Error($"Serving failed: {ex.Message}"); + } + return 1; + } + return 0; + } + , errs => Task.FromResult(1) + ); } /// @@ -144,7 +112,8 @@ public class Program { return new LoggerConfiguration() .MinimumLevel.Is(verbose ? LogEventLevel.Debug : LogEventLevel.Information) - .WriteTo.Async(a => a.Console(formatProvider: System.Globalization.CultureInfo.CurrentCulture)) + // .WriteTo.Async(a => a.Console(formatProvider: System.Globalization.CultureInfo.CurrentCulture)) + .WriteTo.Console(formatProvider: System.Globalization.CultureInfo.CurrentCulture) .CreateLogger(); } @@ -157,7 +126,7 @@ public class Program var assemblyName = assembly?.GetName(); var appName = assemblyName?.Name; var appVersion = assemblyName?.Version; - logger.Information("{name} v{version}", appName, appVersion); + Logger.Information("{name} v{version}", appName, appVersion); } /// @@ -165,6 +134,6 @@ public class Program /// public void OutputLogo() { - logger.Information(helloWorld); + Logger.Information(helloWorld); } } diff --git a/source/SuCoS.csproj b/source/SuCoS.csproj index 6b90744..bf151db 100644 --- a/source/SuCoS.csproj +++ b/source/SuCoS.csproj @@ -13,6 +13,7 @@ + @@ -22,7 +23,6 @@ - -- GitLab