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(() =>
{