Skip to content

Commit

Permalink
Merge branch 'main' into IncludeReferencesInAssemblyDepResolve
Browse files Browse the repository at this point in the history
  • Loading branch information
Herrmel authored Jan 27, 2025
2 parents 7e94ee7 + dccdb2e commit 728a0d9
Show file tree
Hide file tree
Showing 24 changed files with 208 additions and 57 deletions.
11 changes: 11 additions & 0 deletions .github/codecov.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
coverage:
status:
project:
default:
informational: true
patch:
default:
informational: true
comment: false
github_checks:
annotations: false
3 changes: 2 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,8 @@ jobs:
- uses: codecov/codecov-action@v5
if: matrix.os == 'ubuntu-latest'
with:
fail_ci_if_error: true
fail_ci_if_error: false
token: ${{ secrets.CODECOV_TOKEN }}

- run: echo "DOTNET_DbgEnableMiniDump=1" >> $GITHUB_ENV
if: matrix.os == 'ubuntu-latest'
Expand Down
59 changes: 46 additions & 13 deletions src/Docfx.App/PdfBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -51,17 +51,17 @@ class Outline
public string? pdfFooterTemplate { get; init; }
}

public static Task Run(BuildJsonConfig config, string configDirectory, string? outputDirectory = null)
public static Task Run(BuildJsonConfig config, string configDirectory, string? outputDirectory = null, CancellationToken cancellationToken = default)
{
var outputFolder = Path.GetFullPath(Path.Combine(
string.IsNullOrEmpty(outputDirectory) ? Path.Combine(configDirectory, config.Output ?? "") : outputDirectory,
config.Dest ?? ""));

Logger.LogInfo($"Searching for manifest in {outputFolder}");
return CreatePdf(outputFolder);
return CreatePdf(outputFolder, cancellationToken);
}

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

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

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

await AnsiConsole.Progress().StartAsync(async progress =>
var pdfBuildTask = AnsiConsole.Progress().StartAsync(async progress =>
{
await Parallel.ForEachAsync(pdfTocs, 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"));
var task = progress.AddTask(outputName);
var outputPath = Path.Combine(outputFolder, outputName);
var pdfOutputPath = Path.Combine(outputFolder, outputName);

await CreatePdf(
PrintPdf, PrintHeaderFooter, task, new(baseUrl, url), toc, outputFolder, outputPath,
pageNumbers => pdfPageNumbers[url] = pageNumbers);
PrintPdf, PrintHeaderFooter, task, new(baseUrl, url), toc, outputFolder, pdfOutputPath,
pageNumbers => pdfPageNumbers[url] = pageNumbers,
cancellationToken);

task.Value = task.MaxValue;
task.StopTask();
});
});

try
{
await pdfBuildTask.WaitAsync(cancellationToken);
}
catch (OperationCanceledException)
{
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; // Wait AnsiConsole.Progress operation completed to output logs.
}
catch
{
Logger.LogError($"PDF file generation is canceled by user interaction.");
return;
}
}
}

Logger.LogVerbose($"PDF done in {stopwatch.Elapsed}");
return;

IEnumerable<(string url, Outline toc)> GetPdfTocs()
{
Expand Down Expand Up @@ -150,7 +176,7 @@ IResult TocPage(string url)

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

try
Expand Down Expand Up @@ -273,7 +299,7 @@ static string ExpandTemplate(string? pdfTemplate, int pageNumber, int totalPages

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

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

foreach (var (url, node) in pages)
{
cancellationToken.ThrowIfCancellationRequested();

if (!pageBytes.TryGetValue(node, out var bytes))
continue;

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

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

using var output = File.Create(outputPath);
using var output = File.Create(pdfOutputPath);
using var builder = new PdfDocumentBuilder(output);

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

await MergePdf();
return;

IEnumerable<(Uri url, Outline node)> GetPages(Outline outline)
{
Expand Down Expand Up @@ -368,6 +397,8 @@ async Task MergePdf()

foreach (var (url, node) in pages)
{
cancellationToken.ThrowIfCancellationRequested();

if (!pageBytes.TryGetValue(node, out var bytes))
continue;

Expand All @@ -387,6 +418,8 @@ async Task MergePdf()
using var document = PdfDocument.Open(bytes);
for (var i = 1; i <= document.NumberOfPages; i++)
{
cancellationToken.ThrowIfCancellationRequested();

pageNumber++;

var pageBuilder = builder.AddPage(document, i, x => CopyLink(node, x));
Expand Down
42 changes: 42 additions & 0 deletions src/Docfx.App/RunServe.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,19 @@
// The .NET Foundation licenses this file to you under the MIT license.

using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Runtime.InteropServices;
using Docfx.Common;
using Docfx.Plugins;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.FileProviders;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;

#nullable enable

namespace Docfx;

/// <summary>
Expand Down Expand Up @@ -44,6 +48,7 @@ public static void Exec(string folder, string host, int? port, bool openBrowser,
Console.WriteLine($"Serving \"{folder}\" on {url}");
Console.WriteLine("Press Ctrl+C to shut down");
using var app = builder.Build();
app.UseExtensionlessHtmlUrl();
app.UseServe(folder);

if (openBrowser || !string.IsNullOrEmpty(openFile))
Expand Down Expand Up @@ -161,4 +166,41 @@ private static void LaunchBrowser(string url)
Logger.LogError($"Could not launch the browser process. with error - {ex.Message}");
}
}

/// <summary>
/// Enable HTML content access with extensionless URL.
/// This extension method must be called before `UseFileServer` or `UseStaticFiles`.
/// </summary>
private static IApplicationBuilder UseExtensionlessHtmlUrl(this WebApplication app)
{
// Configure middleware that rewrite extensionless url to physical HTML file path.
return app.Use(async (context, next) =>
{
if (IsGetOrHeadMethod(context.Request.Method)
&& TryResolveHtmlFilePath(context.Request.Path, out var htmlFilePath))
{
context.Request.Path = htmlFilePath;
}

await next();
});

static bool IsGetOrHeadMethod(string method) => HttpMethods.IsGet(method) || HttpMethods.IsHead(method);

// Try to resolve HTML file path.
bool TryResolveHtmlFilePath(PathString pathString, [NotNullWhen(true)] out string? htmlPath)
{
var path = pathString.Value;
if (!string.IsNullOrEmpty(path) && !Path.HasExtension(path) && !path.EndsWith('/'))
{
htmlPath = $"{path}.html";
var fileInfo = app.Environment.WebRootFileProvider.GetFileInfo(htmlPath);
if (fileInfo != null)
return true;
}

htmlPath = null;
return false;
}
}
}
33 changes: 33 additions & 0 deletions src/docfx/Models/CancellableCommandBase.cs
Original file line number Diff line number Diff line change
@@ -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 System.Runtime.InteropServices;
using Spectre.Console.Cli;

namespace Docfx;

public abstract class CancellableCommandBase<TSettings> : Command<TSettings>
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();
}
}
}
8 changes: 4 additions & 4 deletions src/docfx/Models/DefaultCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@

namespace Docfx;

class DefaultCommand : Command<DefaultCommand.Options>
class DefaultCommand : CancellableCommandBase<DefaultCommand.Options>
{
[Description("Runs metadata, build and pdf commands")]
internal class Options : BuildCommandOptions
Expand All @@ -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)
{
Expand Down Expand Up @@ -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)
Expand Down
9 changes: 5 additions & 4 deletions src/docfx/Models/PdfCommand.cs
Original file line number Diff line number Diff line change
@@ -1,22 +1,23 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.Diagnostics.CodeAnalysis;
using Docfx.Pdf;
using Spectre.Console.Cli;

#nullable enable

namespace Docfx;

internal class PdfCommand : Command<PdfCommandOptions>
internal class PdfCommand : CancellableCommandBase<PdfCommandOptions>
{
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();
});
}
}
10 changes: 10 additions & 0 deletions test/Directory.Build.props
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,16 @@
<VSTestLogger>$(VSTestLogger);html%3BLogFileName=TestResults-$(MSBuildProjectName)-$(TargetFramework)-$(RUNNER_OS).html</VSTestLogger>
</PropertyGroup>

<ItemGroup Condition="'$(IsTestProject)' == 'true'">
<ProjectReference Include="$(MSBuildThisFileDirectory)Docfx.Tests.Common/Docfx.Tests.Common.csproj" />
</ItemGroup>

<!-- Set additional PackagesReferences to suppress warning MSB3277 (Assembly version conflict) -->
<ItemGroup>
<PackageReference Include="System.Collections.Immutable" />
<PackageReference Include="System.Text.Json" />
</ItemGroup>

<ItemGroup>
<PackageReference Include="FluentAssertions" />
<PackageReference Include="Microsoft.NET.Test.Sdk" />
Expand Down
2 changes: 1 addition & 1 deletion test/Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
<Import Project="$([MSBuild]::GetPathOfFileAbove('$(MSBuildThisFile)', '$(MSBuildThisFileDirectory)../'))" />

<ItemGroup>
<PackageVersion Include="FluentAssertions" Version="7.0.0" />
<PackageVersion Include="FluentAssertions" Version="[7.1.0]" />
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="17.12.0" />
<PackageVersion Include="Verify.DiffPlex" Version="3.1.2" />
<PackageVersion Include="Verify.Xunit" Version="28.4.0" />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,5 @@
<ItemGroup>
<ProjectReference Include="..\..\src\Docfx.Build\Docfx.Build.csproj" />
<ProjectReference Include="..\..\src\Docfx.Build.ManagedReference\Docfx.Build.ManagedReference.csproj" />
<ProjectReference Include="..\Docfx.Tests.Common\Docfx.Tests.Common.csproj" />
</ItemGroup>
</Project>
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,5 @@
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\src\Docfx.Build.OverwriteDocuments\Docfx.Build.OverwriteDocuments.csproj" />
<ProjectReference Include="..\Docfx.Tests.Common\Docfx.Tests.Common.csproj" />
</ItemGroup>
</Project>
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,5 @@
<ProjectReference Include="..\..\src\Docfx.Build\Docfx.Build.csproj" />
<ProjectReference Include="..\..\src\Docfx.Build.ManagedReference\Docfx.Build.ManagedReference.csproj" />
<ProjectReference Include="..\..\src\Docfx.Common\Docfx.Common.csproj" />
<ProjectReference Include="..\Docfx.Tests.Common\Docfx.Tests.Common.csproj" />
</ItemGroup>
</Project>
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,5 @@
<ProjectReference Include="..\..\src\Docfx.Build.RestApi\Docfx.Build.RestApi.csproj" />
<ProjectReference Include="..\..\src\Docfx.DataContracts.Common\Docfx.DataContracts.Common.csproj" />
<ProjectReference Include="..\..\src\Docfx.DataContracts.RestApi\Docfx.DataContracts.RestApi.csproj" />
<ProjectReference Include="..\Docfx.Tests.Common\Docfx.Tests.Common.csproj" />
</ItemGroup>
</Project>
Loading

0 comments on commit 728a0d9

Please sign in to comment.