diff --git a/src/Docfx.App/PdfBuilder.cs b/src/Docfx.App/PdfBuilder.cs index 819d944c02c..b31e3704854 100644 --- a/src/Docfx.App/PdfBuilder.cs +++ b/src/Docfx.App/PdfBuilder.cs @@ -100,12 +100,9 @@ public static async Task CreatePdf(string outputFolder, CancellationToken cancel var headerFooterTemplateCache = new ConcurrentDictionary(); var headerFooterPageCache = new ConcurrentDictionary<(string, string), Task>(); - using var cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); - using var ctr = SubscribeCancelKeyPressEvent(cts); - var pdfBuildTask = AnsiConsole.Progress().StartAsync(async progress => { - await Parallel.ForEachAsync(pdfTocs, new ParallelOptions { CancellationToken = cts.Token }, async (item, _) => + await Parallel.ForEachAsync(pdfTocs, new ParallelOptions { CancellationToken = cancellationToken }, async (item, _) => { var (url, toc) = item; var outputName = Path.Combine(Path.GetDirectoryName(url) ?? "", toc.pdfFileName ?? Path.ChangeExtension(Path.GetFileName(url), ".pdf")); @@ -115,33 +112,34 @@ public static async Task CreatePdf(string outputFolder, CancellationToken cancel await CreatePdf( PrintPdf, PrintHeaderFooter, task, new(baseUrl, url), toc, outputFolder, pdfOutputPath, pageNumbers => pdfPageNumbers[url] = pageNumbers, - cts.Token); + cancellationToken); task.Value = task.MaxValue; task.StopTask(); }); }); - // Wait pdfBuildTask completed or cancelled. - await Task.WhenAny(pdfBuildTask, Task.Delay(Timeout.Infinite, cts.Token)); - - if (!pdfBuildTask.IsCompletedSuccessfully) + try + { + await pdfBuildTask.WaitAsync(cancellationToken); + } + catch (OperationCanceledException) { - // So manually close playwright context and browser to immeadiately shutdown running task. if (!pdfBuildTask.IsCompleted) { + // If pdf generation task is not completed. + // Manually close playwright context/browser to immediately shutdown remaining tasks. await context.CloseAsync(); await browser.CloseAsync(); - } - - try - { - await pdfBuildTask; - } - catch (OperationCanceledException) - { - Logger.LogError($"PDF file generation is canceled by user interaction."); - return; + try + { + await pdfBuildTask; // Wait AnsiConsole.Progress operation completed to output logs. + } + catch + { + Logger.LogError($"PDF file generation is canceled by user interaction."); + return; + } } } @@ -690,16 +688,4 @@ private static StringComparison GetStringComparison() ? StringComparison.OrdinalIgnoreCase : StringComparison.Ordinal; } - - private static CancellationTokenRegistration SubscribeCancelKeyPressEvent(CancellationTokenSource cts) - { - void onCancelKeyPress(object? sender, ConsoleCancelEventArgs e) - { - e.Cancel = true; - cts.Cancel(); - } - - Console.CancelKeyPress += onCancelKeyPress; - return cts.Token.Register(() => Console.CancelKeyPress -= onCancelKeyPress); - } } diff --git a/src/docfx/Models/CancellableCommandBase.cs b/src/docfx/Models/CancellableCommandBase.cs new file mode 100644 index 00000000000..6b9702e849b --- /dev/null +++ b/src/docfx/Models/CancellableCommandBase.cs @@ -0,0 +1,33 @@ +// 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 Spectre.Console.Cli; +using System.Runtime.InteropServices; + +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/DefaultCommand.cs b/src/docfx/Models/DefaultCommand.cs index fd21157e4be..d026f949f5b 100644 --- a/src/docfx/Models/DefaultCommand.cs +++ b/src/docfx/Models/DefaultCommand.cs @@ -10,7 +10,7 @@ namespace Docfx; -class DefaultCommand : Command +class DefaultCommand : CancellableCommandBase { [Description("Runs metadata, build and pdf commands")] internal class Options : BuildCommandOptions @@ -20,7 +20,7 @@ internal class Options : BuildCommandOptions public bool Version { get; set; } } - public override int Execute(CommandContext context, Options options) + public override int Execute(CommandContext context, Options options, CancellationToken cancellationToken) { if (options.Version) { @@ -48,9 +48,9 @@ public override int Execute(CommandContext context, Options options) if (config.build is not null) { BuildCommand.MergeOptionsToConfig(options, config.build, configDirectory); - serveDirectory = RunBuild.Exec(config.build, new(), configDirectory, outputFolder); + serveDirectory = RunBuild.Exec(config.build, new(), configDirectory, outputFolder, cancellationToken); - PdfBuilder.CreatePdf(serveDirectory).GetAwaiter().GetResult(); + PdfBuilder.CreatePdf(serveDirectory, cancellationToken).GetAwaiter().GetResult(); } if (options.Serve && serveDirectory is not null) diff --git a/src/docfx/Models/PdfCommand.cs b/src/docfx/Models/PdfCommand.cs index 067b11f52de..70948188ac5 100644 --- a/src/docfx/Models/PdfCommand.cs +++ b/src/docfx/Models/PdfCommand.cs @@ -5,18 +5,20 @@ using Docfx.Pdf; using Spectre.Console.Cli; +#nullable enable + namespace Docfx; -internal class PdfCommand : Command +internal class PdfCommand : CancellableCommandBase { - public override int Execute([NotNull] CommandContext context, [NotNull] PdfCommandOptions options) + public override int Execute(CommandContext context, PdfCommandOptions options, CancellationToken cancellationToken) { return CommandHelper.Run(options, () => { var (config, configDirectory) = Docset.GetConfig(options.ConfigFile); if (config.build is not null) - PdfBuilder.Run(config.build, configDirectory, options.OutputFolder).GetAwaiter().GetResult(); + PdfBuilder.Run(config.build, configDirectory, options.OutputFolder, cancellationToken).GetAwaiter().GetResult(); }); } }