diff --git a/src/Docfx.App/Helpers/DocumentBuilderWrapper.cs b/src/Docfx.App/Helpers/DocumentBuilderWrapper.cs index b7eb555a8ce..bc542a514fb 100644 --- a/src/Docfx.App/Helpers/DocumentBuilderWrapper.cs +++ b/src/Docfx.App/Helpers/DocumentBuilderWrapper.cs @@ -15,7 +15,7 @@ internal static class DocumentBuilderWrapper { private static readonly Assembly[] s_pluginAssemblies = LoadPluginAssemblies(AppContext.BaseDirectory).ToArray(); - public static void BuildDocument(BuildJsonConfig config, BuildOptions options, TemplateManager templateManager, string baseDirectory, string outputDirectory, string templateDirectory) + public static void BuildDocument(BuildJsonConfig config, BuildOptions options, TemplateManager templateManager, string baseDirectory, string outputDirectory, string templateDirectory, CancellationToken cancellationToken) { var postProcessorNames = config.PostProcessors.ToImmutableArray(); var metadata = config.GlobalMetadata?.ToImmutableDictionary(); @@ -39,7 +39,7 @@ public static void BuildDocument(BuildJsonConfig config, BuildOptions options, T using var builder = new DocumentBuilder(s_pluginAssemblies.Concat(pluginAssemblies), postProcessorNames); var parameters = ConfigToParameter(config, options, templateManager, baseDirectory, outputDirectory, templateDirectory); - builder.Build(parameters, outputDirectory); + builder.Build(parameters, outputDirectory, cancellationToken); } private static IEnumerable LoadPluginAssemblies(string pluginDirectory) diff --git a/src/Docfx.App/RunBuild.cs b/src/Docfx.App/RunBuild.cs index 33706794ffd..5501ae9e856 100644 --- a/src/Docfx.App/RunBuild.cs +++ b/src/Docfx.App/RunBuild.cs @@ -16,7 +16,7 @@ internal static class RunBuild /// /// Build document with specified settings. /// - public static string Exec(BuildJsonConfig config, BuildOptions options, string configDirectory, string outputDirectory = null) + public static string Exec(BuildJsonConfig config, BuildOptions options, string configDirectory, string outputDirectory = null, CancellationToken cancellationToken = default) { var stopwatch = Stopwatch.StartNew(); if (config.Template == null || config.Template.Count == 0) @@ -36,7 +36,7 @@ public static string Exec(BuildJsonConfig config, BuildOptions options, string c { var templateManager = new TemplateManager(config.Template, config.Theme, configDirectory); - DocumentBuilderWrapper.BuildDocument(config, options, templateManager, baseDirectory, outputFolder, null); + DocumentBuilderWrapper.BuildDocument(config, options, templateManager, baseDirectory, outputFolder, null, cancellationToken); templateManager.ProcessTheme(outputFolder, true); } diff --git a/src/Docfx.Build/CompilePhaseHandler.cs b/src/Docfx.Build/CompilePhaseHandler.cs index e6ad1537014..71da01dbaf2 100644 --- a/src/Docfx.Build/CompilePhaseHandler.cs +++ b/src/Docfx.Build/CompilePhaseHandler.cs @@ -39,7 +39,7 @@ public void Handle(List hostServices, int maxParallelism) _restructions.AddRange(hostService.TableOfContentRestructions); } } - }, maxParallelism); + }, maxParallelism, Context.CancellationToken); DistributeTocRestructions(hostServices); @@ -49,7 +49,7 @@ public void Handle(List hostServices, int maxParallelism) Build(hostService, maxParallelism); } - hostServices.RunAll(Postbuild, maxParallelism); + hostServices.RunAll(Postbuild, maxParallelism, Context.CancellationToken); } private void Prepare(List hostServices, int maxParallelism) @@ -66,7 +66,8 @@ private void Prepare(List hostServices, int maxParallelism) { m.LocalPathFromRoot ??= StringExtension.ToDisplayPath(Path.Combine(m.BaseDir, m.File)); }, - maxParallelism); + maxParallelism, + Context.CancellationToken); } } @@ -83,7 +84,7 @@ private void DistributeTocRestructions(List hostServices) } } - private static void Prebuild(HostService hostService) + private void Prebuild(HostService hostService) { RunBuildSteps( hostService.Processor.BuildSteps, @@ -99,7 +100,7 @@ private static void Prebuild(HostService hostService) }); } - private static void Build(HostService hostService, int maxParallelism) + private void Build(HostService hostService, int maxParallelism) { hostService.Models.RunAll( m => @@ -124,10 +125,11 @@ private static void Build(HostService hostService, int maxParallelism) }); } }, - maxParallelism); + maxParallelism, + Context.CancellationToken); } - private static void Postbuild(HostService hostService) + private void Postbuild(HostService hostService) { hostService.Reload(hostService.Models); @@ -140,12 +142,14 @@ private static void Postbuild(HostService hostService) }); } - private static void RunBuildSteps(IEnumerable buildSteps, Action action) + private void RunBuildSteps(IEnumerable buildSteps, Action action) { if (buildSteps != null) { foreach (var buildStep in buildSteps.OrderBy(static step => step.BuildOrder)) { + Context.CancellationToken.ThrowIfCancellationRequested(); + action(buildStep); } } diff --git a/src/Docfx.Build/DocumentBuildContext.cs b/src/Docfx.Build/DocumentBuildContext.cs index b3da3dfb121..ba2d30408d6 100644 --- a/src/Docfx.Build/DocumentBuildContext.cs +++ b/src/Docfx.Build/DocumentBuildContext.cs @@ -16,13 +16,7 @@ public sealed class DocumentBuildContext : IDocumentBuildContext private readonly ConcurrentDictionary _tableOfContents = new(FilePathComparer.OSPlatformSensitiveStringComparer); private readonly Task _reader; - public DocumentBuildContext(string buildOutputFolder) - : this(buildOutputFolder, Enumerable.Empty(), ImmutableArray.Empty, ImmutableArray.Empty, 1, Directory.GetCurrentDirectory(), string.Empty, null, null) { } - - public DocumentBuildContext(string buildOutputFolder, IEnumerable allSourceFiles, ImmutableArray externalReferencePackages, ImmutableArray xrefMaps, int maxParallelism, string baseFolder, string versionName, ApplyTemplateSettings applyTemplateSetting, string rootTocPath) - : this(buildOutputFolder, allSourceFiles, externalReferencePackages, xrefMaps, maxParallelism, baseFolder, versionName, applyTemplateSetting, rootTocPath, null, null) { } - - public DocumentBuildContext(DocumentBuildParameters parameters) + public DocumentBuildContext(DocumentBuildParameters parameters, CancellationToken cancellationToken) { BuildOutputFolder = Path.Combine(Path.GetFullPath(EnvironmentContext.BaseDirectory), parameters.OutputBaseDir); VersionName = parameters.VersionName; @@ -34,9 +28,10 @@ public DocumentBuildContext(DocumentBuildParameters parameters) if (parameters.XRefMaps.Length > 0) { + // Note: `_reader` task is processed asyncronously and await is called later. So OperationCancellationException is not thrown by this lines. _reader = new XRefCollection( from u in parameters.XRefMaps - select new Uri(u, UriKind.RelativeOrAbsolute)).GetReaderAsync(parameters.Files.DefaultBaseDir, parameters.MarkdownEngineParameters?.FallbackFolders); + select new Uri(u, UriKind.RelativeOrAbsolute)).GetReaderAsync(parameters.Files.DefaultBaseDir, parameters.MarkdownEngineParameters?.FallbackFolders, cancellationToken); } RootTocPath = parameters.RootTocPath; @@ -54,9 +49,18 @@ from u in parameters.XRefMaps } } VersionFolder = versionDir; + CancellationToken = cancellationToken; } - public DocumentBuildContext( + #region Constructors that used by test code. + + internal DocumentBuildContext(string buildOutputFolder) + : this(buildOutputFolder, Enumerable.Empty(), ImmutableArray.Empty, ImmutableArray.Empty, 1, Directory.GetCurrentDirectory(), string.Empty, null, null) { } + + private DocumentBuildContext(string buildOutputFolder, IEnumerable allSourceFiles, ImmutableArray externalReferencePackages, ImmutableArray xrefMaps, int maxParallelism, string baseFolder, string versionName, ApplyTemplateSettings applyTemplateSetting, string rootTocPath) + : this(buildOutputFolder, allSourceFiles, externalReferencePackages, xrefMaps, maxParallelism, baseFolder, versionName, applyTemplateSetting, rootTocPath, null, null) { } + + private DocumentBuildContext( string buildOutputFolder, IEnumerable allSourceFiles, ImmutableArray externalReferencePackages, @@ -98,6 +102,7 @@ from u in xrefMaps } VersionFolder = versionFolder; } + #endregion public string BuildOutputFolder { get; } @@ -129,6 +134,8 @@ from u in xrefMaps public ICustomHrefGenerator HrefGenerator { get; } + public CancellationToken CancellationToken { get; } = CancellationToken.None; + internal ConcurrentBag ManifestItems { get; } = new(); private ConcurrentDictionary ExternalXRefSpec { get; } = new(); @@ -150,9 +157,10 @@ public void ReportExternalXRefSpec(XRefSpec spec) public void ResolveExternalXRefSpec() { - Task.WaitAll( + Task.WaitAll([ Task.Run(ResolveExternalXRefSpecForSpecs), - Task.Run(ResolveExternalXRefSpecForNoneSpecsAsync)); + Task.Run(ResolveExternalXRefSpecForNoneSpecsAsync) + ], CancellationToken); } private void ResolveExternalXRefSpecForSpecs() @@ -205,7 +213,7 @@ private List ResolveByExternalReferencePackages(List uidList, Co var oldSpecCount = externalXRefSpec.Count; var list = new List(); - using (var externalReferences = new ExternalReferencePackageCollection(ExternalReferencePackages, MaxParallelism)) + using (var externalReferences = new ExternalReferencePackageCollection(ExternalReferencePackages, MaxParallelism, CancellationToken)) { foreach (var uid in uidList) { @@ -401,7 +409,7 @@ public XRefSpec GetXrefSpec(string uid) if (ExternalReferencePackages.Length > 0) { - using (var externalReferences = new ExternalReferencePackageCollection(ExternalReferencePackages, MaxParallelism)) + using (var externalReferences = new ExternalReferencePackageCollection(ExternalReferencePackages, MaxParallelism, CancellationToken)) { xref = GetExternalReference(externalReferences, uid); } diff --git a/src/Docfx.Build/DocumentBuilder.cs b/src/Docfx.Build/DocumentBuilder.cs index ede13a86dd3..cf61e4cec8e 100644 --- a/src/Docfx.Build/DocumentBuilder.cs +++ b/src/Docfx.Build/DocumentBuilder.cs @@ -39,10 +39,12 @@ public void Build(DocumentBuildParameters parameter) Build(new DocumentBuildParameters[] { parameter }, parameter.OutputBaseDir); } - public void Build(IList parameters, string outputDirectory) + public void Build(IList parameters, string outputDirectory, CancellationToken cancellationToken = default) { ArgumentNullException.ThrowIfNull(parameters); + cancellationToken.ThrowIfCancellationRequested(); + if (parameters.Count == 0) { throw new ArgumentException("Parameters are empty.", nameof(parameters)); @@ -119,7 +121,7 @@ public void Build(IList parameters, string outputDirect MetadataValidators = MetadataValidators.ToList(), Processors = Processors, }; - manifests.Add(builder.Build(parameter, markdownService)); + manifests.Add(builder.Build(parameter, markdownService, cancellationToken)); } } if (noContentFound) diff --git a/src/Docfx.Build/HostServiceCreator.cs b/src/Docfx.Build/HostServiceCreator.cs index fb6aea1c646..076b190d1cf 100644 --- a/src/Docfx.Build/HostServiceCreator.cs +++ b/src/Docfx.Build/HostServiceCreator.cs @@ -97,7 +97,9 @@ bool NeedApplyMetadata() { invalidFiles.Add(file.File); } - }, _context.MaxParallelism); + }, + _context.MaxParallelism, + _context.CancellationToken); return (models.OrderBy(m => m.File, StringComparer.Ordinal).ToArray(), invalidFiles); } diff --git a/src/Docfx.Build/LinkPhaseHandler.cs b/src/Docfx.Build/LinkPhaseHandler.cs index 2419be3382e..3914c232c4e 100644 --- a/src/Docfx.Build/LinkPhaseHandler.cs +++ b/src/Docfx.Build/LinkPhaseHandler.cs @@ -77,7 +77,7 @@ private IEnumerable ExportManifest(HostService hostServ } } } - }); + }, Context.CancellationToken); return manifestItems; } @@ -136,7 +136,7 @@ private void CheckFileLink(FileModel model, HostService hostService, SaveResult Logger.LogWarning($"Invalid file link:({fileLink}).", code: WarningCodes.Build.InvalidFileLink); } } - }); + }, Context.CancellationToken); } private void HandleUids(SaveResult result) diff --git a/src/Docfx.Build/ManifestProcessor.cs b/src/Docfx.Build/ManifestProcessor.cs index b27302e681a..e135b74a317 100644 --- a/src/Docfx.Build/ManifestProcessor.cs +++ b/src/Docfx.Build/ManifestProcessor.cs @@ -87,7 +87,8 @@ private void NormalizeToObject() } } }, - _context.MaxParallelism); + _context.MaxParallelism, + _context.CancellationToken); } private void FeedOptions() @@ -113,7 +114,8 @@ private void FeedOptions() } } }, - _context.MaxParallelism); + _context.MaxParallelism, + _context.CancellationToken); } private void UpdateHref() @@ -130,7 +132,8 @@ private void UpdateHref() m.Item.Content = m.FileModel.Content; } }, - _context.MaxParallelism); + _context.MaxParallelism, + _context.CancellationToken); } private void ApplySystemMetadata() @@ -171,7 +174,8 @@ private void ApplySystemMetadata() } } }, - _context.MaxParallelism); + _context.MaxParallelism, + _context.CancellationToken); _globalMetadata["_shared"] = sharedObjects; } diff --git a/src/Docfx.Build/PostProcessors/ExtractSearchIndex.cs b/src/Docfx.Build/PostProcessors/ExtractSearchIndex.cs index eb96e045435..1a996d4267c 100644 --- a/src/Docfx.Build/PostProcessors/ExtractSearchIndex.cs +++ b/src/Docfx.Build/PostProcessors/ExtractSearchIndex.cs @@ -37,7 +37,7 @@ public ImmutableDictionary PrepareMetadata(ImmutableDictionary PrepareMetadata(ImmutableDictionary PrepareMetadata(ImmutableDictionary PrepareMetadata(ImmutableDictionary Processors { get; set; } public IEnumerable MetadataValidators { get; set; } - public static ImmutableList Build(IDocumentProcessor processor, DocumentBuildParameters parameters, IMarkdownService markdownService) + public static ImmutableList Build( + IDocumentProcessor processor, + DocumentBuildParameters parameters, + IMarkdownService markdownService) { var hostServiceCreator = new HostServiceCreator(null); var hostService = hostServiceCreator.CreateHostService( @@ -32,7 +36,7 @@ public static ImmutableList Build(IDocumentProcessor processor, Docum return hostService.Models; } - public Manifest Build(DocumentBuildParameters parameters, IMarkdownService markdownService) + public Manifest Build(DocumentBuildParameters parameters, IMarkdownService markdownService, CancellationToken cancellationToken) { ArgumentNullException.ThrowIfNull(parameters); @@ -52,14 +56,14 @@ public Manifest Build(DocumentBuildParameters parameters, IMarkdownService markd Directory.CreateDirectory(parameters.OutputBaseDir); - var context = new DocumentBuildContext(parameters); + var context = new DocumentBuildContext(parameters, cancellationToken); // Start building document... var templateProcessor = parameters.TemplateManager?.GetTemplateProcessor(context, parameters.MaxParallelism) ?? new TemplateProcessor(new EmptyResourceReader(), context, 16); var hostServiceCreator = new HostServiceCreator(context); - var hostServices = GetInnerContexts(parameters, Processors, templateProcessor, hostServiceCreator, markdownService); + var hostServices = GetInnerContexts(parameters, Processors, templateProcessor, hostServiceCreator, markdownService, cancellationToken); templateProcessor.CopyTemplateResources(context.ApplyTemplateSettings); @@ -86,9 +90,14 @@ private List GetInnerContexts( IEnumerable processors, TemplateProcessor templateProcessor, HostServiceCreator creator, - IMarkdownService markdownService) + IMarkdownService markdownService, + CancellationToken cancellationToken) { - var files = (from file in parameters.Files.EnumerateFiles().AsParallel().WithDegreeOfParallelism(parameters.MaxParallelism) + cancellationToken.ThrowIfCancellationRequested(); + + var files = (from file in parameters.Files.EnumerateFiles().AsParallel() + .WithDegreeOfParallelism(parameters.MaxParallelism) + .WithCancellation(cancellationToken) from p in (from processor in processors let priority = processor.GetProcessingPriority(file) where priority != ProcessingPriority.NotSupported @@ -114,7 +123,9 @@ orderby ps.Key descending try { - return (from processor in processors.AsParallel().WithDegreeOfParallelism(parameters.MaxParallelism) + return (from processor in processors.AsParallel() + .WithDegreeOfParallelism(parameters.MaxParallelism) + .WithCancellation(cancellationToken) join item in toHandleItems.AsParallel() on processor equals item.Key into g from item in g.DefaultIfEmpty() where item != null && item.Any(s => s.Type != DocumentType.Overwrite) // when normal file exists then processing is needed @@ -138,9 +149,14 @@ select creator.CreateHostService( private static string ExportXRefMap(DocumentBuildParameters parameters, DocumentBuildContext context) { Logger.LogVerbose("Exporting xref map..."); + + context.CancellationToken.ThrowIfCancellationRequested(); + var xrefMap = new XRefMap { - References = (from xref in context.XRefSpecMap.Values.AsParallel().WithDegreeOfParallelism(parameters.MaxParallelism) + References = (from xref in context.XRefSpecMap.Values.AsParallel() + .WithDegreeOfParallelism(parameters.MaxParallelism) + .WithCancellation(context.CancellationToken) select new XRefSpec(xref) { Href = context.UpdateHref(xref.Href, RelativePath.WorkingFolder) diff --git a/src/Docfx.Build/TemplateProcessors/TemplateProcessor.cs b/src/Docfx.Build/TemplateProcessors/TemplateProcessor.cs index 6e2f4680c11..3ec5c86c140 100644 --- a/src/Docfx.Build/TemplateProcessors/TemplateProcessor.cs +++ b/src/Docfx.Build/TemplateProcessors/TemplateProcessor.cs @@ -104,6 +104,8 @@ private void CopyTemplateResources(string outputDirectory, IEnumerable s.Resources).Distinct()) { + _context.CancellationToken.ThrowIfCancellationRequested(); + var resourceKey = resourceInfo.ResourceKey; try @@ -194,7 +196,8 @@ private List ProcessCore(List items, ApplyTe manifest.Add(transformer.Transform(item)); } }, - _maxParallelism); + _maxParallelism, + _context.CancellationToken); return manifest.ToList(); } diff --git a/src/Docfx.Build/XRefMaps/XRefArchiveBuilder.cs b/src/Docfx.Build/XRefMaps/XRefArchiveBuilder.cs index 515cec49607..0a43a29fa30 100644 --- a/src/Docfx.Build/XRefMaps/XRefArchiveBuilder.cs +++ b/src/Docfx.Build/XRefMaps/XRefArchiveBuilder.cs @@ -1,6 +1,7 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System.Threading; using Docfx.Common; namespace Docfx.Build.Engine; @@ -10,7 +11,7 @@ public class XRefArchiveBuilder private readonly object _syncRoot = new(); private readonly XRefMapDownloader _downloader = new(); - public async Task DownloadAsync(Uri uri, string outputFile) + public async Task DownloadAsync(Uri uri, string outputFile, CancellationToken cancellationToken = default) { ArgumentNullException.ThrowIfNull(uri); @@ -24,7 +25,7 @@ public async Task DownloadAsync(Uri uri, string outputFile) try { using var xa = XRefArchive.Open(outputFile, XRefArchiveMode.Create); - await DownloadCoreAsync(uri, xa); + await DownloadCoreAsync(uri, xa, cancellationToken); } catch (Exception ex) { @@ -36,10 +37,10 @@ public async Task DownloadAsync(Uri uri, string outputFile) } } - private async Task DownloadCoreAsync(Uri uri, XRefArchive xa) + private async Task DownloadCoreAsync(Uri uri, XRefArchive xa, CancellationToken cancellationToken) { IXRefContainer container; - container = await _downloader.DownloadAsync(uri); + container = await _downloader.DownloadAsync(uri, cancellationToken); if (container is not XRefMap map) { // XRefArchive is not supported by `docfx download`. diff --git a/src/Docfx.Build/XRefMaps/XRefCollection.cs b/src/Docfx.Build/XRefMaps/XRefCollection.cs index dbcc34e0499..357a313bb99 100644 --- a/src/Docfx.Build/XRefMaps/XRefCollection.cs +++ b/src/Docfx.Build/XRefMaps/XRefCollection.cs @@ -20,10 +20,10 @@ public XRefCollection(IEnumerable uris) public ImmutableList Uris { get; set; } - public Task GetReaderAsync(string baseFolder, IReadOnlyList fallbackFolders = null) + public Task GetReaderAsync(string baseFolder, IReadOnlyList fallbackFolders = null, CancellationToken cancellationToken = default) { var creator = new ReaderCreator(Uris, MaxParallelism, baseFolder, fallbackFolders); - return creator.CreateAsync(); + return creator.CreateAsync(cancellationToken); } private sealed class ReaderCreator @@ -39,13 +39,18 @@ public ReaderCreator(ImmutableList uris, int maxParallelism, string baseFol _downloader = new XRefMapDownloader(baseFolder, fallbackFolders, maxParallelism); } - public async Task CreateAsync() + public async Task CreateAsync(CancellationToken cancellationToken) { - AddToDownloadList(_uris); + AddToDownloadList(_uris, cancellationToken); var dict = new Dictionary(); + while (_processing.Any()) { - Task task = await Task.WhenAny(_processing.Keys); + Task task = await Task.WhenAny(_processing.Keys) + .WaitAsync(cancellationToken); + + cancellationToken.ThrowIfCancellationRequested(); + Uri uri = _processing[task]; _processing.Remove(task); try @@ -58,7 +63,8 @@ from r in container.GetRedirections() where r != null select GetUri(uri, r.Href) into u where u != null - select u); + select u, + cancellationToken); } dict[uri.IsAbsoluteUri ? uri.AbsoluteUri : uri.OriginalString] = container; } @@ -97,7 +103,7 @@ private static Uri GetUri(Uri baseUri, string href) return new Uri(baseUri, uri); } - private void AddToDownloadList(IEnumerable uris) + private void AddToDownloadList(IEnumerable uris, CancellationToken cancellationToken) { foreach (var uri in uris) { @@ -105,13 +111,13 @@ private void AddToDownloadList(IEnumerable uris) { if (_set.Add(uri.AbsoluteUri)) { - var task = _downloader.DownloadAsync(uri); + var task = _downloader.DownloadAsync(uri, cancellationToken); _processing[task] = uri; } } else if (_set.Add(uri.OriginalString)) { - var task = _downloader.DownloadAsync(uri); + var task = _downloader.DownloadAsync(uri, cancellationToken); _processing[task] = uri; } } diff --git a/src/Docfx.DataContracts.Common/ExternalReferences/ExternalReferencePackageCollection.cs b/src/Docfx.DataContracts.Common/ExternalReferences/ExternalReferencePackageCollection.cs index 75f907339f8..a3dfce97f36 100644 --- a/src/Docfx.DataContracts.Common/ExternalReferences/ExternalReferencePackageCollection.cs +++ b/src/Docfx.DataContracts.Common/ExternalReferences/ExternalReferencePackageCollection.cs @@ -8,11 +8,14 @@ public class ExternalReferencePackageCollection : IDisposable { private readonly LruList _cache = LruList.Create(0x100); - public ExternalReferencePackageCollection(IEnumerable packageFiles, int maxParallelism) + public ExternalReferencePackageCollection(IEnumerable packageFiles, int maxParallelism, CancellationToken cancellationToken) { ArgumentNullException.ThrowIfNull(packageFiles); - Readers = (from file in packageFiles.AsParallel().WithDegreeOfParallelism(maxParallelism).AsOrdered() + Readers = (from file in packageFiles.AsParallel() + .WithDegreeOfParallelism(maxParallelism) + .WithCancellation(cancellationToken) + .AsOrdered() let reader = ExternalReferencePackageReader.CreateNoThrow(file) where reader != null select reader).ToImmutableList(); diff --git a/src/Docfx.Plugins/DocumentExceptionExtensions.cs b/src/Docfx.Plugins/DocumentExceptionExtensions.cs index 9f4059d5c0a..6b2866a3b40 100644 --- a/src/Docfx.Plugins/DocumentExceptionExtensions.cs +++ b/src/Docfx.Plugins/DocumentExceptionExtensions.cs @@ -1,8 +1,13 @@ -namespace Docfx.Plugins; +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Runtime.ExceptionServices; + +namespace Docfx.Plugins; public static class DocumentExceptionExtensions { - public static TResult[] RunAll(this IReadOnlyList elements, Func func) + public static TResult[] RunAll(this IReadOnlyList elements, Func func, CancellationToken cancellationToken = default) { ArgumentNullException.ThrowIfNull(elements); ArgumentNullException.ThrowIfNull(func); @@ -11,6 +16,8 @@ public static TResult[] RunAll(this IReadOnlyList e DocumentException firstException = null; for (int i = 0; i < elements.Count; i++) { + cancellationToken.ThrowIfCancellationRequested(); + try { results[i] = func(elements[i]); @@ -27,12 +34,12 @@ public static TResult[] RunAll(this IReadOnlyList e return results; } - public static void RunAll(this IReadOnlyList elements, Action action) + public static void RunAll(this IReadOnlyList elements, Action action, CancellationToken cancellationToken = default) { - RunAll((IEnumerable)elements, action); + RunAll((IEnumerable)elements, action, cancellationToken); } - public static void RunAll(this IEnumerable elements, Action action) + public static void RunAll(this IEnumerable elements, Action action, CancellationToken cancellationToken = default) { ArgumentNullException.ThrowIfNull(elements); ArgumentNullException.ThrowIfNull(action); @@ -40,6 +47,7 @@ public static void RunAll(this IEnumerable elements, Action< DocumentException firstException = null; foreach (var element in elements) { + cancellationToken.ThrowIfCancellationRequested(); try { action(element); @@ -55,12 +63,12 @@ public static void RunAll(this IEnumerable elements, Action< } } - public static void RunAll(this IReadOnlyList elements, Action action, int parallelism) + public static void RunAll(this IReadOnlyList elements, Action action, int parallelism, CancellationToken cancellationToken = default) { - RunAll((IEnumerable)elements, action, parallelism); + RunAll((IEnumerable)elements, action, parallelism, cancellationToken); } - public static void RunAll(this IEnumerable elements, Action action, int parallelism) + public static void RunAll(this IEnumerable elements, Action action, int parallelism, CancellationToken cancellationToken = default) { ArgumentNullException.ThrowIfNull(elements); ArgumentNullException.ThrowIfNull(action); @@ -69,24 +77,43 @@ public static void RunAll(this IEnumerable elements, Action< { throw new ArgumentOutOfRangeException(nameof(parallelism)); } - DocumentException firstException = null; - Parallel.ForEach( - elements, - new ParallelOptions { MaxDegreeOfParallelism = parallelism }, - s => - { - try + + try + { + DocumentException firstException = null; + Parallel.ForEach( + elements, + new ParallelOptions { - action(s); - } - catch (DocumentException ex) + MaxDegreeOfParallelism = parallelism, + CancellationToken = cancellationToken, + }, + s => { - Interlocked.CompareExchange(ref firstException, ex, null); - } - }); - if (firstException != null) + try + { + action(s); + } + catch (DocumentException ex) + { + Interlocked.CompareExchange(ref firstException, ex, null); + } + }); + + if (firstException != null) + { + throw new DocumentException(firstException.Message, firstException); + } + } + catch (AggregateException ex) { - throw new DocumentException(firstException.Message, firstException); + // If exceptions are OperationCanceledException. throw only first exception. + var innerExceptions = ex.Flatten().InnerExceptions.ToArray(); + if (innerExceptions.All(x => x is OperationCanceledException)) + ExceptionDispatchInfo.Throw(innerExceptions[0]); + + throw; } + } } diff --git a/src/Docfx.Plugins/IDocumentBuildContext.cs b/src/Docfx.Plugins/IDocumentBuildContext.cs index dd59d17d555..be3aef4ea4f 100644 --- a/src/Docfx.Plugins/IDocumentBuildContext.cs +++ b/src/Docfx.Plugins/IDocumentBuildContext.cs @@ -94,4 +94,9 @@ public interface IDocumentBuildContext /// Custom href generator /// ICustomHrefGenerator HrefGenerator { get; } + + /// + /// The token to cancel build operation. + /// + CancellationToken CancellationToken { get; } } diff --git a/src/Docfx.Plugins/IPostProcessor.cs b/src/Docfx.Plugins/IPostProcessor.cs index d881ee761a7..1b07a79e839 100644 --- a/src/Docfx.Plugins/IPostProcessor.cs +++ b/src/Docfx.Plugins/IPostProcessor.cs @@ -19,6 +19,7 @@ public interface IPostProcessor /// /// /// The output folder where our static website will be placed + /// The token to cancel operation. /// - Manifest Process(Manifest manifest, string outputFolder); + Manifest Process(Manifest manifest, string outputFolder, CancellationToken cancellationToken); } diff --git a/test/docfx.Tests/Api.verified.cs b/test/docfx.Tests/Api.verified.cs index 74ea367b49b..8e47673f8ac 100644 --- a/test/docfx.Tests/Api.verified.cs +++ b/test/docfx.Tests/Api.verified.cs @@ -58,13 +58,11 @@ public override System.IO.Stream GetResourceStream(string name) { } } public sealed class DocumentBuildContext : Docfx.Plugins.IDocumentBuildContext { - public DocumentBuildContext(Docfx.Build.Engine.DocumentBuildParameters parameters) { } - public DocumentBuildContext(string buildOutputFolder) { } - public DocumentBuildContext(string buildOutputFolder, System.Collections.Generic.IEnumerable allSourceFiles, System.Collections.Immutable.ImmutableArray externalReferencePackages, System.Collections.Immutable.ImmutableArray xrefMaps, int maxParallelism, string baseFolder, string versionName, Docfx.Build.Engine.ApplyTemplateSettings applyTemplateSetting, string rootTocPath) { } - public DocumentBuildContext(string buildOutputFolder, System.Collections.Generic.IEnumerable allSourceFiles, System.Collections.Immutable.ImmutableArray externalReferencePackages, System.Collections.Immutable.ImmutableArray xrefMaps, int maxParallelism, string baseFolder, string versionName, Docfx.Build.Engine.ApplyTemplateSettings applyTemplateSetting, string rootTocPath, string versionFolder, Docfx.Plugins.GroupInfo groupInfo) { } + public DocumentBuildContext(Docfx.Build.Engine.DocumentBuildParameters parameters, System.Threading.CancellationToken cancellationToken) { } public System.Collections.Immutable.ImmutableDictionary AllSourceFiles { get; } public Docfx.Build.Engine.ApplyTemplateSettings ApplyTemplateSettings { get; set; } public string BuildOutputFolder { get; } + public System.Threading.CancellationToken CancellationToken { get; } public System.Collections.Immutable.ImmutableArray ExternalReferencePackages { get; } public System.Collections.Concurrent.ConcurrentDictionary FileMap { get; } public Docfx.Plugins.GroupInfo GroupInfo { get; } @@ -120,7 +118,7 @@ public class DocumentBuilder : System.IDisposable { public DocumentBuilder(System.Collections.Generic.IEnumerable assemblies, System.Collections.Immutable.ImmutableArray postProcessorNames) { } public void Build(Docfx.Build.Engine.DocumentBuildParameters parameter) { } - public void Build(System.Collections.Generic.IList parameters, string outputDirectory) { } + public void Build(System.Collections.Generic.IList parameters, string outputDirectory, System.Threading.CancellationToken cancellationToken = default) { } public void Dispose() { } } public sealed class EmptyResourceReader : Docfx.Build.Engine.ResourceFileReader @@ -391,7 +389,7 @@ public static Docfx.Build.Engine.XRefArchive Open(string file, Docfx.Build.Engin public class XRefArchiveBuilder { public XRefArchiveBuilder() { } - public System.Threading.Tasks.Task DownloadAsync(System.Uri uri, string outputFile) { } + public System.Threading.Tasks.Task DownloadAsync(System.Uri uri, string outputFile, System.Threading.CancellationToken cancellationToken = default) { } } public enum XRefArchiveMode { @@ -2452,7 +2450,7 @@ public static class TableOfContents } public class ExternalReferencePackageCollection : System.IDisposable { - public ExternalReferencePackageCollection(System.Collections.Generic.IEnumerable packageFiles, int maxParallelism) { } + public ExternalReferencePackageCollection(System.Collections.Generic.IEnumerable packageFiles, int maxParallelism, System.Threading.CancellationToken cancellationToken) { } public System.Collections.Immutable.ImmutableList Readers { get; } public void Dispose() { } protected virtual void Dispose(bool disposing) { } @@ -3954,11 +3952,11 @@ public DocumentException(string message, System.Exception inner) { } } public static class DocumentExceptionExtensions { - public static void RunAll(this System.Collections.Generic.IEnumerable elements, System.Action action) { } - public static void RunAll(this System.Collections.Generic.IReadOnlyList elements, System.Action action) { } - public static void RunAll(this System.Collections.Generic.IEnumerable elements, System.Action action, int parallelism) { } - public static void RunAll(this System.Collections.Generic.IReadOnlyList elements, System.Action action, int parallelism) { } - public static TResult[] RunAll(this System.Collections.Generic.IReadOnlyList elements, System.Func func) { } + public static void RunAll(this System.Collections.Generic.IEnumerable elements, System.Action action, System.Threading.CancellationToken cancellationToken = default) { } + public static void RunAll(this System.Collections.Generic.IReadOnlyList elements, System.Action action, System.Threading.CancellationToken cancellationToken = default) { } + public static void RunAll(this System.Collections.Generic.IEnumerable elements, System.Action action, int parallelism, System.Threading.CancellationToken cancellationToken = default) { } + public static void RunAll(this System.Collections.Generic.IReadOnlyList elements, System.Action action, int parallelism, System.Threading.CancellationToken cancellationToken = default) { } + public static TResult[] RunAll(this System.Collections.Generic.IReadOnlyList elements, System.Func func, System.Threading.CancellationToken cancellationToken = default) { } } public enum DocumentType { @@ -4064,6 +4062,7 @@ public interface ICustomHrefGenerator } public interface IDocumentBuildContext { + System.Threading.CancellationToken CancellationToken { get; } Docfx.Plugins.GroupInfo GroupInfo { get; } Docfx.Plugins.ICustomHrefGenerator HrefGenerator { get; } string RootTocPath { get; } @@ -4154,7 +4153,7 @@ public interface IMarkdownService public interface IPostProcessor { System.Collections.Immutable.ImmutableDictionary PrepareMetadata(System.Collections.Immutable.ImmutableDictionary metadata); - Docfx.Plugins.Manifest Process(Docfx.Plugins.Manifest manifest, string outputFolder); + Docfx.Plugins.Manifest Process(Docfx.Plugins.Manifest manifest, string outputFolder, System.Threading.CancellationToken cancellationToken); } public readonly struct LinkSourceInfo { diff --git a/test/docfx.Tests/Assets/template/plugins/CustomPostProcessor.cs b/test/docfx.Tests/Assets/template/plugins/CustomPostProcessor.cs index cf7bb040ee1..124e1b09c43 100644 --- a/test/docfx.Tests/Assets/template/plugins/CustomPostProcessor.cs +++ b/test/docfx.Tests/Assets/template/plugins/CustomPostProcessor.cs @@ -9,7 +9,7 @@ public class CustomPostProcessor : IPostProcessor { public ImmutableDictionary PrepareMetadata(ImmutableDictionary metadata) => metadata; - public Manifest Process(Manifest manifest, string outputFolder) + public Manifest Process(Manifest manifest, string outputFolder, CancellationToken cancellationToken = default) { File.WriteAllText(Path.Combine(outputFolder, "customPostProcessor.txt"), "customPostProcessor"); return manifest;