Skip to content

Commit 46c5cd2

Browse files
authored
chore: Add cancellation support for PdfBuilder operation (#10460)
* chore: add cancellation support for PdfBuilder * chore: add CancellableCommand
1 parent cd2fb6b commit 46c5cd2

File tree

4 files changed

+88
-21
lines changed

4 files changed

+88
-21
lines changed

src/Docfx.App/PdfBuilder.cs

Lines changed: 46 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -51,17 +51,17 @@ class Outline
5151
public string? pdfFooterTemplate { get; init; }
5252
}
5353

54-
public static Task Run(BuildJsonConfig config, string configDirectory, string? outputDirectory = null)
54+
public static Task Run(BuildJsonConfig config, string configDirectory, string? outputDirectory = null, CancellationToken cancellationToken = default)
5555
{
5656
var outputFolder = Path.GetFullPath(Path.Combine(
5757
string.IsNullOrEmpty(outputDirectory) ? Path.Combine(configDirectory, config.Output ?? "") : outputDirectory,
5858
config.Dest ?? ""));
5959

6060
Logger.LogInfo($"Searching for manifest in {outputFolder}");
61-
return CreatePdf(outputFolder);
61+
return CreatePdf(outputFolder, cancellationToken);
6262
}
6363

64-
public static async Task CreatePdf(string outputFolder)
64+
public static async Task CreatePdf(string outputFolder, CancellationToken cancellationToken = default)
6565
{
6666
var stopwatch = Stopwatch.StartNew();
6767
var pdfTocs = GetPdfTocs().ToDictionary(p => p.url, p => p.toc);
@@ -82,7 +82,7 @@ public static async Task CreatePdf(string outputFolder)
8282
using var app = builder.Build();
8383
app.UseServe(outputFolder);
8484
app.MapGet("/_pdftoc/{*url}", TocPage);
85-
await app.StartAsync();
85+
await app.StartAsync(cancellationToken);
8686

8787
baseUrl = new Uri(app.Urls.First());
8888

@@ -100,25 +100,51 @@ public static async Task CreatePdf(string outputFolder)
100100
var headerFooterTemplateCache = new ConcurrentDictionary<string, string>();
101101
var headerFooterPageCache = new ConcurrentDictionary<(string, string), Task<byte[]>>();
102102

103-
await AnsiConsole.Progress().StartAsync(async progress =>
103+
var pdfBuildTask = AnsiConsole.Progress().StartAsync(async progress =>
104104
{
105-
await Parallel.ForEachAsync(pdfTocs, async (item, _) =>
105+
await Parallel.ForEachAsync(pdfTocs, new ParallelOptions { CancellationToken = cancellationToken }, async (item, _) =>
106106
{
107107
var (url, toc) = item;
108108
var outputName = Path.Combine(Path.GetDirectoryName(url) ?? "", toc.pdfFileName ?? Path.ChangeExtension(Path.GetFileName(url), ".pdf"));
109109
var task = progress.AddTask(outputName);
110-
var outputPath = Path.Combine(outputFolder, outputName);
110+
var pdfOutputPath = Path.Combine(outputFolder, outputName);
111111

112112
await CreatePdf(
113-
PrintPdf, PrintHeaderFooter, task, new(baseUrl, url), toc, outputFolder, outputPath,
114-
pageNumbers => pdfPageNumbers[url] = pageNumbers);
113+
PrintPdf, PrintHeaderFooter, task, new(baseUrl, url), toc, outputFolder, pdfOutputPath,
114+
pageNumbers => pdfPageNumbers[url] = pageNumbers,
115+
cancellationToken);
115116

116117
task.Value = task.MaxValue;
117118
task.StopTask();
118119
});
119120
});
120121

122+
try
123+
{
124+
await pdfBuildTask.WaitAsync(cancellationToken);
125+
}
126+
catch (OperationCanceledException)
127+
{
128+
if (!pdfBuildTask.IsCompleted)
129+
{
130+
// If pdf generation task is not completed.
131+
// Manually close playwright context/browser to immediately shutdown remaining tasks.
132+
await context.CloseAsync();
133+
await browser.CloseAsync();
134+
try
135+
{
136+
await pdfBuildTask; // Wait AnsiConsole.Progress operation completed to output logs.
137+
}
138+
catch
139+
{
140+
Logger.LogError($"PDF file generation is canceled by user interaction.");
141+
return;
142+
}
143+
}
144+
}
145+
121146
Logger.LogVerbose($"PDF done in {stopwatch.Elapsed}");
147+
return;
122148

123149
IEnumerable<(string url, Outline toc)> GetPdfTocs()
124150
{
@@ -150,7 +176,7 @@ IResult TocPage(string url)
150176

151177
async Task<byte[]?> PrintPdf(Outline outline, Uri url)
152178
{
153-
await pageLimiter.WaitAsync();
179+
await pageLimiter.WaitAsync(cancellationToken);
154180
var page = pagePool.TryTake(out var pooled) ? pooled : await context.NewPageAsync();
155181

156182
try
@@ -273,7 +299,7 @@ static string ExpandTemplate(string? pdfTemplate, int pageNumber, int totalPages
273299

274300
static async Task CreatePdf(
275301
Func<Outline, Uri, Task<byte[]?>> printPdf, Func<Outline, int, int, Page, Task<byte[]>> printHeaderFooter, ProgressTask task,
276-
Uri outlineUrl, Outline outline, string outputFolder, string outputPath, Action<Dictionary<Outline, int>> updatePageNumbers)
302+
Uri outlineUrl, Outline outline, string outputFolder, string pdfOutputPath, Action<Dictionary<Outline, int>> updatePageNumbers, CancellationToken cancellationToken)
277303
{
278304
var pages = GetPages(outline).ToArray();
279305
if (pages.Length == 0)
@@ -284,7 +310,7 @@ static async Task CreatePdf(
284310
// Make progress at 99% before merge PDF
285311
task.MaxValue = pages.Length + (pages.Length / 99.0);
286312

287-
await Parallel.ForEachAsync(pages, async (item, _) =>
313+
await Parallel.ForEachAsync(pages, new ParallelOptions { CancellationToken = cancellationToken }, async (item, _) =>
288314
{
289315
var (url, node) = item;
290316
if (await printPdf(outline, url) is { } bytes)
@@ -302,6 +328,8 @@ await Parallel.ForEachAsync(pages, async (item, _) =>
302328

303329
foreach (var (url, node) in pages)
304330
{
331+
cancellationToken.ThrowIfCancellationRequested();
332+
305333
if (!pageBytes.TryGetValue(node, out var bytes))
306334
continue;
307335

@@ -324,13 +352,14 @@ await Parallel.ForEachAsync(pages, async (item, _) =>
324352

325353
var producer = $"docfx ({typeof(PdfBuilder).Assembly.GetCustomAttribute<AssemblyFileVersionAttribute>()?.Version})";
326354

327-
using var output = File.Create(outputPath);
355+
using var output = File.Create(pdfOutputPath);
328356
using var builder = new PdfDocumentBuilder(output);
329357

330358
builder.DocumentInformation = new() { Producer = producer };
331359
builder.Bookmarks = CreateBookmarks(outline.items);
332360

333361
await MergePdf();
362+
return;
334363

335364
IEnumerable<(Uri url, Outline node)> GetPages(Outline outline)
336365
{
@@ -368,6 +397,8 @@ async Task MergePdf()
368397

369398
foreach (var (url, node) in pages)
370399
{
400+
cancellationToken.ThrowIfCancellationRequested();
401+
371402
if (!pageBytes.TryGetValue(node, out var bytes))
372403
continue;
373404

@@ -387,6 +418,8 @@ async Task MergePdf()
387418
using var document = PdfDocument.Open(bytes);
388419
for (var i = 1; i <= document.NumberOfPages; i++)
389420
{
421+
cancellationToken.ThrowIfCancellationRequested();
422+
390423
pageNumber++;
391424

392425
var pageBuilder = builder.AddPage(document, i, x => CopyLink(node, x));
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
#nullable enable
5+
6+
using System.Runtime.InteropServices;
7+
using Spectre.Console.Cli;
8+
9+
namespace Docfx;
10+
11+
public abstract class CancellableCommandBase<TSettings> : Command<TSettings>
12+
where TSettings : CommandSettings
13+
{
14+
public abstract int Execute(CommandContext context, TSettings settings, CancellationToken cancellation);
15+
16+
public sealed override int Execute(CommandContext context, TSettings settings)
17+
{
18+
using var cancellationSource = new CancellationTokenSource();
19+
20+
using var sigInt = PosixSignalRegistration.Create(PosixSignal.SIGINT, onSignal);
21+
using var sigQuit = PosixSignalRegistration.Create(PosixSignal.SIGQUIT, onSignal);
22+
using var sigTerm = PosixSignalRegistration.Create(PosixSignal.SIGTERM, onSignal);
23+
24+
var exitCode = Execute(context, settings, cancellationSource.Token);
25+
return exitCode;
26+
27+
void onSignal(PosixSignalContext context)
28+
{
29+
context.Cancel = true;
30+
cancellationSource.Cancel();
31+
}
32+
}
33+
}

src/docfx/Models/DefaultCommand.cs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010

1111
namespace Docfx;
1212

13-
class DefaultCommand : Command<DefaultCommand.Options>
13+
class DefaultCommand : CancellableCommandBase<DefaultCommand.Options>
1414
{
1515
[Description("Runs metadata, build and pdf commands")]
1616
internal class Options : BuildCommandOptions
@@ -20,7 +20,7 @@ internal class Options : BuildCommandOptions
2020
public bool Version { get; set; }
2121
}
2222

23-
public override int Execute(CommandContext context, Options options)
23+
public override int Execute(CommandContext context, Options options, CancellationToken cancellationToken)
2424
{
2525
if (options.Version)
2626
{
@@ -48,9 +48,9 @@ public override int Execute(CommandContext context, Options options)
4848
if (config.build is not null)
4949
{
5050
BuildCommand.MergeOptionsToConfig(options, config.build, configDirectory);
51-
serveDirectory = RunBuild.Exec(config.build, new(), configDirectory, outputFolder);
51+
serveDirectory = RunBuild.Exec(config.build, new(), configDirectory, outputFolder, cancellationToken);
5252

53-
PdfBuilder.CreatePdf(serveDirectory).GetAwaiter().GetResult();
53+
PdfBuilder.CreatePdf(serveDirectory, cancellationToken).GetAwaiter().GetResult();
5454
}
5555

5656
if (options.Serve && serveDirectory is not null)

src/docfx/Models/PdfCommand.cs

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,23 @@
11
// Licensed to the .NET Foundation under one or more agreements.
22
// The .NET Foundation licenses this file to you under the MIT license.
33

4-
using System.Diagnostics.CodeAnalysis;
54
using Docfx.Pdf;
65
using Spectre.Console.Cli;
76

7+
#nullable enable
8+
89
namespace Docfx;
910

10-
internal class PdfCommand : Command<PdfCommandOptions>
11+
internal class PdfCommand : CancellableCommandBase<PdfCommandOptions>
1112
{
12-
public override int Execute([NotNull] CommandContext context, [NotNull] PdfCommandOptions options)
13+
public override int Execute(CommandContext context, PdfCommandOptions options, CancellationToken cancellationToken)
1314
{
1415
return CommandHelper.Run(options, () =>
1516
{
1617
var (config, configDirectory) = Docset.GetConfig(options.ConfigFile);
1718

1819
if (config.build is not null)
19-
PdfBuilder.Run(config.build, configDirectory, options.OutputFolder).GetAwaiter().GetResult();
20+
PdfBuilder.Run(config.build, configDirectory, options.OutputFolder, cancellationToken).GetAwaiter().GetResult();
2021
});
2122
}
2223
}

0 commit comments

Comments
 (0)