diff --git a/Directory.Packages.props b/Directory.Packages.props index a525dd4e3c4..c82835df8bd 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -15,8 +15,8 @@ - - + + diff --git a/src/Docfx.App/PdfBuilder.cs b/src/Docfx.App/PdfBuilder.cs index bd3c7d28f14..1357cd8236c 100644 --- a/src/Docfx.App/PdfBuilder.cs +++ b/src/Docfx.App/PdfBuilder.cs @@ -5,6 +5,7 @@ using System.Collections.Concurrent; using System.Diagnostics; using System.Reflection; +using System.Runtime.InteropServices; using System.Text; using System.Text.Json; using Docfx.Build; @@ -73,6 +74,19 @@ public static async Task CreatePdf(string outputFolder, CancellationToken cancel Program.Main(["install", "chromium", "--only-shell"]); + // Create linked CancellationToken with PosixSignalRegistration handler. + // It's required because default `Ctrl+C` interruption is canceled when using WebApplication inside Spectre.Console command. + using var cancellationTokenSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); + void onSignal(PosixSignalContext context) + { + context.Cancel = true; + cancellationTokenSource.Cancel(); + } + using var sigInt = PosixSignalRegistration.Create(PosixSignal.SIGINT, onSignal); + using var sigQuit = PosixSignalRegistration.Create(PosixSignal.SIGQUIT, onSignal); + using var sigTerm = PosixSignalRegistration.Create(PosixSignal.SIGTERM, onSignal); + cancellationToken = cancellationTokenSource.Token; + var builder = WebApplication.CreateBuilder(); builder.Logging.ClearProviders(); builder.WebHost.UseUrls("http://127.0.0.1:0"); diff --git a/src/Docfx.Dotnet/DotnetApiCatalog.cs b/src/Docfx.Dotnet/DotnetApiCatalog.cs index 5d2fb237e8b..44724f112b4 100644 --- a/src/Docfx.Dotnet/DotnetApiCatalog.cs +++ b/src/Docfx.Dotnet/DotnetApiCatalog.cs @@ -55,7 +55,7 @@ public static async Task GenerateManagedReferenceYamlFiles(string configPath, Do } } - internal static async Task Exec(MetadataJsonConfig config, DotnetApiOptions options, string configDirectory, string outputDirectory = null) + internal static async Task Exec(MetadataJsonConfig config, DotnetApiOptions options, string configDirectory, string outputDirectory = null, CancellationToken cancellationToken = default) { var stopwatch = Stopwatch.StartNew(); diff --git a/src/docfx/Models/BuildCommand.cs b/src/docfx/Models/BuildCommand.cs index 4ac804afbc7..6f779889d9f 100644 --- a/src/docfx/Models/BuildCommand.cs +++ b/src/docfx/Models/BuildCommand.cs @@ -8,11 +8,13 @@ namespace Docfx; -internal class BuildCommand : Command +# pragma warning disable 1998 // CS1998: This async method lacks 'await' operators and will run synchronously. Consider using the 'await' operator to await non-blocking API calls + +internal class BuildCommand : AsyncCommand { - public override int Execute(CommandContext context, BuildCommandOptions settings) + public override Task ExecuteAsync(CommandContext context, BuildCommandOptions settings, CancellationToken cancellationToken) { - return CommandHelper.Run(settings, () => + return CommandHelper.RunAsync(settings, async () => { if (settings.Serve && CommandHelper.IsTcpPortAlreadyUsed(settings.Host, settings.Port)) { diff --git a/src/docfx/Models/CancellableCommandBase.cs b/src/docfx/Models/CancellableCommandBase.cs deleted file mode 100644 index 471bb9556e7..00000000000 --- a/src/docfx/Models/CancellableCommandBase.cs +++ /dev/null @@ -1,33 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -#nullable enable - -using System.Runtime.InteropServices; -using Spectre.Console.Cli; - -namespace Docfx; - -public abstract class CancellableCommandBase : Command - where TSettings : CommandSettings -{ - public abstract int Execute(CommandContext context, TSettings settings, CancellationToken cancellation); - - public sealed override int Execute(CommandContext context, TSettings settings) - { - using var cancellationSource = new CancellationTokenSource(); - - using var sigInt = PosixSignalRegistration.Create(PosixSignal.SIGINT, onSignal); - using var sigQuit = PosixSignalRegistration.Create(PosixSignal.SIGQUIT, onSignal); - using var sigTerm = PosixSignalRegistration.Create(PosixSignal.SIGTERM, onSignal); - - var exitCode = Execute(context, settings, cancellationSource.Token); - return exitCode; - - void onSignal(PosixSignalContext context) - { - context.Cancel = true; - cancellationSource.Cancel(); - } - } -} diff --git a/src/docfx/Models/CommandHelper.cs b/src/docfx/Models/CommandHelper.cs index dc8b45717f0..5f3ecde729c 100644 --- a/src/docfx/Models/CommandHelper.cs +++ b/src/docfx/Models/CommandHelper.cs @@ -20,37 +20,28 @@ public static int Run(Action run) Logger.Flush(); Logger.UnregisterAllListeners(); + // Logger.PrintSummary() method is not called when Run without LogOptions.(DownloadCommand/ServeCommand/TemplateCommand) return 0; } public static int Run(LogOptions options, Action run) { - var consoleLogListener = new ConsoleLogListener(); - Logger.RegisterListener(consoleLogListener); - - if (!string.IsNullOrWhiteSpace(options.LogFilePath)) - { - Logger.RegisterListener(new ReportLogListener(options.LogFilePath)); - } + SetupLogger(options); - if (options.LogLevel.HasValue) - { - Logger.LogLevelThreshold = options.LogLevel.Value; - } - else if (options.Verbose) - { - Logger.LogLevelThreshold = LogLevel.Verbose; - } + run(); - Logger.WarningsAsErrors = options.WarningsAsErrors; + CleanupLogger(); + return Logger.HasError ? -1 : 0; + } - run(); + public static async Task RunAsync(LogOptions options, Func run) + { + SetupLogger(options); - Logger.Flush(); - Logger.UnregisterAllListeners(); - Logger.PrintSummary(); + await run(); + CleanupLogger(); return Logger.HasError ? -1 : 0; } @@ -82,4 +73,33 @@ public static bool IsTcpPortAlreadyUsed(string? host, int? port) } } } + + private static void SetupLogger(LogOptions options) + { + var consoleLogListener = new ConsoleLogListener(); + Logger.RegisterListener(consoleLogListener); + + if (!string.IsNullOrWhiteSpace(options.LogFilePath)) + { + Logger.RegisterListener(new ReportLogListener(options.LogFilePath)); + } + + if (options.LogLevel.HasValue) + { + Logger.LogLevelThreshold = options.LogLevel.Value; + } + else if (options.Verbose) + { + Logger.LogLevelThreshold = LogLevel.Verbose; + } + + Logger.WarningsAsErrors = options.WarningsAsErrors; + } + + private static void CleanupLogger() + { + Logger.Flush(); + Logger.UnregisterAllListeners(); + Logger.PrintSummary(); + } } diff --git a/src/docfx/Models/DefaultCommand.cs b/src/docfx/Models/DefaultCommand.cs index d026f949f5b..47ee79c027f 100644 --- a/src/docfx/Models/DefaultCommand.cs +++ b/src/docfx/Models/DefaultCommand.cs @@ -10,7 +10,7 @@ namespace Docfx; -class DefaultCommand : CancellableCommandBase +class DefaultCommand : Command { [Description("Runs metadata, build and pdf commands")] internal class Options : BuildCommandOptions diff --git a/src/docfx/Models/DownloadCommand.cs b/src/docfx/Models/DownloadCommand.cs index 138394a2742..bb2739eb12a 100644 --- a/src/docfx/Models/DownloadCommand.cs +++ b/src/docfx/Models/DownloadCommand.cs @@ -10,7 +10,7 @@ namespace Docfx; internal class DownloadCommand : Command { - public override int Execute([NotNull] CommandContext context, [NotNull] DownloadCommandOptions options) + public override int Execute([NotNull] CommandContext context, [NotNull] DownloadCommandOptions options, CancellationToken cancellationToken) { return CommandHelper.Run(() => { diff --git a/src/docfx/Models/InitCommand.cs b/src/docfx/Models/InitCommand.cs index 3cb1e707637..c8a245074ec 100644 --- a/src/docfx/Models/InitCommand.cs +++ b/src/docfx/Models/InitCommand.cs @@ -13,7 +13,7 @@ namespace Docfx; class InitCommand : Command { - public override int Execute([NotNull] CommandContext context, [NotNull] InitCommandOptions options) + public override int Execute([NotNull] CommandContext context, [NotNull] InitCommandOptions options, CancellationToken cancellationToken) { WriteLine( """ diff --git a/src/docfx/Models/MergeCommand.cs b/src/docfx/Models/MergeCommand.cs index fa5c9624041..52315ec2eb8 100644 --- a/src/docfx/Models/MergeCommand.cs +++ b/src/docfx/Models/MergeCommand.cs @@ -8,7 +8,7 @@ namespace Docfx; internal class MergeCommand : Command { - public override int Execute([NotNull] CommandContext context, [NotNull] MergeCommandOptions options) + public override int Execute([NotNull] CommandContext context, [NotNull] MergeCommandOptions options, CancellationToken cancellationToken) { return CommandHelper.Run(options, () => { diff --git a/src/docfx/Models/MetadataCommand.cs b/src/docfx/Models/MetadataCommand.cs index 520e27e413d..8bd0b98823c 100644 --- a/src/docfx/Models/MetadataCommand.cs +++ b/src/docfx/Models/MetadataCommand.cs @@ -7,15 +7,15 @@ namespace Docfx; -internal class MetadataCommand : Command +internal class MetadataCommand : AsyncCommand { - public override int Execute([NotNull] CommandContext context, [NotNull] MetadataCommandOptions options) + public override Task ExecuteAsync([NotNull] CommandContext context, [NotNull] MetadataCommandOptions options, CancellationToken cancellationToken) { - return CommandHelper.Run(options, () => + return CommandHelper.RunAsync(options, async () => { var (config, baseDirectory) = Docset.GetConfig(options.Config); MergeOptionsToConfig(options, config); - DotnetApiCatalog.Exec(config.metadata, new(), baseDirectory, options.OutputFolder).GetAwaiter().GetResult(); + await DotnetApiCatalog.Exec(config.metadata, new(), baseDirectory, options.OutputFolder, cancellationToken); }); } diff --git a/src/docfx/Models/PdfCommand.cs b/src/docfx/Models/PdfCommand.cs index 7a20db92cb8..ba0829596fb 100644 --- a/src/docfx/Models/PdfCommand.cs +++ b/src/docfx/Models/PdfCommand.cs @@ -8,16 +8,16 @@ namespace Docfx; -internal class PdfCommand : CancellableCommandBase +internal class PdfCommand : AsyncCommand { - public override int Execute(CommandContext context, PdfCommandOptions options, CancellationToken cancellationToken) + public override async Task ExecuteAsync(CommandContext context, PdfCommandOptions options, CancellationToken cancellationToken) { - return CommandHelper.Run(options, () => + return await CommandHelper.RunAsync(options, async () => { var (config, configDirectory) = Docset.GetConfig(options.ConfigFile); if (config.build is not null) - PdfBuilder.Run(config.build, configDirectory, options.OutputFolder, cancellationToken).GetAwaiter().GetResult(); + await PdfBuilder.Run(config.build, configDirectory, options.OutputFolder, cancellationToken); }); } } diff --git a/src/docfx/Models/ServeCommand.cs b/src/docfx/Models/ServeCommand.cs index c3c0a3b3b3d..83d52a6f06a 100644 --- a/src/docfx/Models/ServeCommand.cs +++ b/src/docfx/Models/ServeCommand.cs @@ -33,7 +33,7 @@ internal class Settings : CommandSettings public string OpenFile { get; set; } } - public override int Execute([NotNull] CommandContext context, [NotNull] Settings options) + public override int Execute([NotNull] CommandContext context, [NotNull] Settings options, CancellationToken cancellationToken) { return CommandHelper.Run(() => RunServe.Exec(options.Folder, options.Host, options.Port, options.OpenBrowser, options.OpenFile)); } diff --git a/src/docfx/Models/TemplateCommand.cs b/src/docfx/Models/TemplateCommand.cs index b30ce1d4566..fff7fb9c48b 100644 --- a/src/docfx/Models/TemplateCommand.cs +++ b/src/docfx/Models/TemplateCommand.cs @@ -12,7 +12,7 @@ internal class TemplateCommand { public class ListCommand : Command { - public override int Execute(CommandContext context) + public override int Execute(CommandContext context, CancellationToken cancellationToken) { foreach (var path in Directory.GetDirectories(GetTemplateBaseDirectory())) Console.WriteLine(Path.GetFileName(path)); @@ -38,7 +38,7 @@ internal class Options : CommandSettings public string OutputFolder { get; set; } } - public override int Execute(CommandContext context, Options options) + public override int Execute(CommandContext context, Options options, CancellationToken cancellationToken) { return CommandHelper.Run(() => {