From 9f883419b587d154b40cf4276b6e8d6ce97890ef Mon Sep 17 00:00:00 2001 From: filzrev <103790468+filzrev@users.noreply.github.com> Date: Thu, 26 Dec 2024 06:55:33 +0900 Subject: [PATCH] chore: add CancellableCommand --- src/Docfx.App/PdfBuilder.cs | 50 ++++++++++---------------- src/docfx/Models/CancellableCommand.cs | 33 +++++++++++++++++ src/docfx/Models/DefaultCommand.cs | 8 ++--- src/docfx/Models/PdfCommand.cs | 8 +++-- 4 files changed, 60 insertions(+), 39 deletions(-) create mode 100644 src/docfx/Models/CancellableCommand.cs 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/CancellableCommand.cs b/src/docfx/Models/CancellableCommand.cs new file mode 100644 index 00000000000..233da2d74ca --- /dev/null +++ b/src/docfx/Models/CancellableCommand.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 CancellableCommand : 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..af072ff626a 100644 --- a/src/docfx/Models/DefaultCommand.cs +++ b/src/docfx/Models/DefaultCommand.cs @@ -10,7 +10,7 @@ namespace Docfx; -class DefaultCommand : Command +class DefaultCommand : CancellableCommand { [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..23d7c9430cc 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 : CancellableCommand { - 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(); }); } }