Skip to content

Commit 61ce635

Browse files
authored
Revert "Define commands as asynchronous and use Task for preview cancellation…" (#1058)
This reverts commit 1c05ea6.
1 parent 6b4d5a3 commit 61ce635

36 files changed

+607
-460
lines changed

Sources/SwiftDocC/Infrastructure/Diagnostics/DiagnosticEngine.swift

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -29,9 +29,7 @@ public final class DiagnosticEngine {
2929
/// diagnostics with a severity up to and including `.information` will be printed.
3030
public var filterLevel: DiagnosticSeverity {
3131
didSet {
32-
self.filter = { [filterLevel] in
33-
$0.diagnostic.severity.rawValue <= filterLevel.rawValue
34-
}
32+
self.filter = { $0.diagnostic.severity.rawValue <= self.filterLevel.rawValue }
3533
}
3634
}
3735

Sources/SwiftDocC/Infrastructure/DocumentationContext.swift

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1331,7 +1331,6 @@ public class DocumentationContext: DocumentationContextDataProviderDelegate {
13311331
}
13321332

13331333
private func shouldContinueRegistration() throws {
1334-
try Task.checkCancellation()
13351334
guard isRegistrationEnabled.sync({ $0 }) else {
13361335
throw ContextError.registrationDisabled
13371336
}
@@ -1774,7 +1773,6 @@ public class DocumentationContext: DocumentationContextDataProviderDelegate {
17741773
///
17751774
/// When given `false` the context will try to cancel as quick as possible
17761775
/// any ongoing bundle registrations.
1777-
@available(*, deprecated, message: "This deprecated API will be removed after 6.2 is released")
17781776
public func setRegistrationEnabled(_ value: Bool) {
17791777
isRegistrationEnabled.sync({ $0 = value })
17801778
}

Sources/SwiftDocC/Infrastructure/DocumentationConverter.swift

Lines changed: 35 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,6 @@ public struct DocumentationConverter: DocumentationConverterProtocol {
5050
private var dataProvider: DocumentationWorkspaceDataProvider
5151

5252
/// An optional closure that sets up a context before the conversion begins.
53-
@available(*, deprecated, message: "This deprecated API will be removed after 6.2 is released")
5453
public var setupContext: ((inout DocumentationContext) -> Void)?
5554

5655
/// Conversion batches should be big enough to keep all cores busy but small enough not to keep
@@ -190,17 +189,49 @@ public struct DocumentationConverter: DocumentationConverterProtocol {
190189
if let dataProvider = self.currentDataProvider {
191190
try workspace.unregisterProvider(dataProvider)
192191
}
192+
193+
// Do additional context setup.
194+
setupContext?(&context)
193195

196+
/*
197+
Asynchronously cancel registration if necessary.
198+
We spawn a timer that periodically checks `isCancelled` and if necessary
199+
disables registration in `DocumentationContext` as registration being
200+
the largest part of a documentation conversion.
201+
*/
194202
let context = self.context
203+
let isCancelled = self.isCancelled
204+
205+
// `true` if the `isCancelled` flag is set.
206+
func isConversionCancelled() -> Bool {
207+
return isCancelled?.sync({ $0 }) == true
208+
}
209+
210+
// Run a timer that synchronizes the cancelled state between the converter and the context directly.
211+
// We need a timer on a separate dispatch queue because `workspace.registerProvider()` blocks
212+
// the current thread until it loads all symbol graphs, markdown files, and builds the topic graph
213+
// so in order to be able to update the context cancellation flag we need to run on a different thread.
214+
var cancelTimerQueue: DispatchQueue? = DispatchQueue(label: "org.swift.docc.ConvertActionCancelTimer", qos: .unspecified, attributes: .concurrent)
215+
let cancelTimer = DispatchSource.makeTimerSource(queue: cancelTimerQueue)
216+
cancelTimer.schedule(deadline: .now(), repeating: .milliseconds(500), leeway: .milliseconds(50))
217+
cancelTimer.setEventHandler {
218+
if isConversionCancelled() {
219+
cancelTimer.cancel()
220+
context.setRegistrationEnabled(false)
221+
}
222+
}
223+
cancelTimer.resume()
195224

196225
// Start bundle registration
197226
try workspace.registerProvider(dataProvider, options: bundleDiscoveryOptions)
198227
self.currentDataProvider = dataProvider
228+
229+
// Bundle registration is finished - stop the timer and reset the context cancellation state.
230+
cancelTimer.cancel()
231+
cancelTimerQueue = nil
232+
context.setRegistrationEnabled(true)
199233

200234
// If cancelled, return early before we emit diagnostics.
201-
func isConversionCancelled() -> Bool {
202-
Task.isCancelled || isCancelled?.sync({ $0 }) == true
203-
}
204235
guard !isConversionCancelled() else { return ([], []) }
205236

206237
processingDurationMetric = benchmark(begin: Benchmark.Duration(id: "documentation-processing"))

Sources/SwiftDocCUtilities/Action/Action.swift

Lines changed: 12 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
/*
22
This source file is part of the Swift.org open source project
33

4-
Copyright (c) 2021-2024 Apple Inc. and the Swift project authors
4+
Copyright (c) 2021 Apple Inc. and the Swift project authors
55
Licensed under Apache License v2.0 with Runtime Library Exception
66

77
See https://swift.org/LICENSE.txt for license information
@@ -13,16 +13,18 @@ import SwiftDocC
1313

1414
/// An independent unit of work in the command-line workflow.
1515
///
16-
/// An action represents a discrete documentation task; it takes options and inputs, performs its work, reports any problems it encounters, and outputs it generates.
17-
package protocol AsyncAction {
16+
/// An `Action` represents a discrete documentation task; it takes options and inputs,
17+
/// performs its work, reports any problems it encounters, and outputs it generates.
18+
public protocol Action {
1819
/// Performs the action and returns an ``ActionResult``.
19-
mutating func perform(logHandle: inout LogHandle) async throws -> ActionResult
20+
mutating func perform(logHandle: LogHandle) throws -> ActionResult
2021
}
2122

22-
package extension AsyncAction {
23-
mutating func perform(logHandle: LogHandle) async throws -> ActionResult {
24-
var logHandle = logHandle
25-
return try await perform(logHandle: &logHandle)
26-
}
23+
/// An action for which you can optionally customize the documentation context.
24+
public protocol RecreatingContext: Action {
25+
/// A closure that an action calls with the action's context for built documentation,
26+
/// before the action performs work.
27+
///
28+
/// Use this closure to set the action's context to a certain state before the action runs.
29+
var setupContext: ((inout DocumentationContext) -> Void)? { get set }
2730
}
28-

Sources/SwiftDocCUtilities/Action/Actions/Action+MoveOutput.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
import Foundation
1212
import SwiftDocC
1313

14-
extension AsyncAction {
14+
extension Action {
1515

1616
/// Creates a new unique directory, with an optional template, inside of specified container.
1717
/// - Parameters:

Sources/SwiftDocCUtilities/Action/Actions/Convert/ConvertAction.swift

Lines changed: 51 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ import Foundation
1414
import SwiftDocC
1515

1616
/// An action that converts a source bundle into compiled documentation.
17-
public struct ConvertAction: AsyncAction {
17+
public struct ConvertAction: Action, RecreatingContext {
1818
enum Error: DescribedError {
1919
case doesNotContainBundle(url: URL)
2020
case cancelPending
@@ -59,6 +59,12 @@ public struct ConvertAction: AsyncAction {
5959
private var fileManager: FileManagerProtocol
6060
private let temporaryDirectory: URL
6161

62+
public var setupContext: ((inout DocumentationContext) -> Void)? {
63+
didSet {
64+
converter.setupContext = setupContext
65+
}
66+
}
67+
6268
var converter: DocumentationConverter
6369

6470
private var durationMetric: Benchmark.Duration?
@@ -233,17 +239,52 @@ public struct ConvertAction: AsyncAction {
233239
dataProvider: dataProvider,
234240
bundleDiscoveryOptions: bundleDiscoveryOptions,
235241
sourceRepository: sourceRepository,
242+
isCancelled: isCancelled,
236243
diagnosticEngine: self.diagnosticEngine,
237244
experimentalModifyCatalogWithGeneratedCuration: experimentalModifyCatalogWithGeneratedCuration
238245
)
239246
}
247+
248+
/// `true` if the convert action is cancelled.
249+
private let isCancelled = Synchronized<Bool>(false)
240250

241-
/// A block of extra work that tests perform to affect the time it takes to convert documentation
242-
var _extraTestWork: (() async -> Void)?
251+
/// `true` if the convert action is currently running.
252+
let isPerforming = Synchronized<Bool>(false)
253+
254+
/// A block to execute when conversion has finished.
255+
/// It's used as a "future" for when the action is cancelled.
256+
var didPerformFuture: (()->Void)?
257+
258+
/// A block to execute when conversion has started.
259+
var willPerformFuture: (()->Void)?
260+
261+
/// Cancels the action.
262+
///
263+
/// The method blocks until the action has completed cancelling.
264+
mutating func cancel() throws {
265+
/// If the action is not running, there is nothing to cancel
266+
guard isPerforming.sync({ $0 }) == true else { return }
267+
268+
/// If the action is already cancelled throw `cancelPending`.
269+
if isCancelled.sync({ $0 }) == true {
270+
throw Error.cancelPending
271+
}
272+
273+
/// Set the cancelled flag.
274+
isCancelled.sync({ $0 = true })
275+
276+
/// Wait for the `perform(logHandle:)` method to call `didPerformFuture()`
277+
let waitGroup = DispatchGroup()
278+
waitGroup.enter()
279+
didPerformFuture = {
280+
waitGroup.leave()
281+
}
282+
waitGroup.wait()
283+
}
243284

244285
/// Converts each eligible file from the source documentation bundle,
245286
/// saves the results in the given output alongside the template files.
246-
public mutating func perform(logHandle: inout LogHandle) async throws -> ActionResult {
287+
mutating public func perform(logHandle: LogHandle) throws -> ActionResult {
247288
// Add the default diagnostic console writer now that we know what log handle it should write to.
248289
if !diagnosticEngine.hasConsumer(matching: { $0 is DiagnosticConsoleWriter }) {
249290
diagnosticEngine.add(
@@ -261,13 +302,15 @@ public struct ConvertAction: AsyncAction {
261302
var postConversionProblems: [Problem] = []
262303
let totalTimeMetric = benchmark(begin: Benchmark.Duration(id: "convert-total-time"))
263304

305+
// While running this method keep the `isPerforming` flag up.
306+
isPerforming.sync({ $0 = true })
307+
willPerformFuture?()
264308
defer {
309+
didPerformFuture?()
310+
isPerforming.sync({ $0 = false })
265311
diagnosticEngine.flush()
266312
}
267313

268-
// Run any extra work that the test may have injected
269-
await _extraTestWork?()
270-
271314
let temporaryFolder = try createTempFolder(with: htmlTemplateDirectory)
272315

273316
defer {
@@ -408,7 +451,7 @@ public struct ConvertAction: AsyncAction {
408451
benchmark(end: totalTimeMetric)
409452

410453
if !didEncounterError {
411-
let coverageResults = try await coverageAction.perform(logHandle: &logHandle)
454+
let coverageResults = try coverageAction.perform(logHandle: logHandle)
412455
postConversionProblems.append(contentsOf: coverageResults.problems)
413456
}
414457

Sources/SwiftDocCUtilities/Action/Actions/CoverageAction.swift

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -12,24 +12,24 @@ import Foundation
1212
import SwiftDocC
1313

1414
/// An action that creates documentation coverage info for a documentation bundle.
15-
public struct CoverageAction: AsyncAction {
16-
init(
15+
public struct CoverageAction: Action {
16+
internal init(
1717
documentationCoverageOptions: DocumentationCoverageOptions,
1818
workingDirectory: URL,
19-
fileManager: FileManagerProtocol
20-
) {
19+
fileManager: FileManagerProtocol) {
2120
self.documentationCoverageOptions = documentationCoverageOptions
2221
self.workingDirectory = workingDirectory
2322
self.fileManager = fileManager
2423
}
2524

2625
public let documentationCoverageOptions: DocumentationCoverageOptions
27-
let workingDirectory: URL
26+
internal let workingDirectory: URL
2827
private let fileManager: FileManagerProtocol
2928

30-
public mutating func perform(logHandle: inout LogHandle) async throws -> ActionResult {
29+
public mutating func perform(logHandle: LogHandle) throws -> ActionResult {
3130
switch documentationCoverageOptions.level {
3231
case .brief, .detailed:
32+
var logHandle = logHandle
3333
print(" --- Experimental coverage output enabled. ---", to: &logHandle)
3434

3535
let summaryString = try CoverageDataEntry.generateSummary(

Sources/SwiftDocCUtilities/Action/Actions/EmitGeneratedCurationAction.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import Foundation
1212
import SwiftDocC
1313

1414
/// An action that emits documentation extension files that reflect the auto-generated curation.
15-
struct EmitGeneratedCurationAction: AsyncAction {
15+
struct EmitGeneratedCurationAction: Action {
1616
let catalogURL: URL?
1717
let additionalSymbolGraphDirectory: URL?
1818
let outputURL: URL
@@ -41,7 +41,7 @@ struct EmitGeneratedCurationAction: AsyncAction {
4141
self.fileManager = fileManager
4242
}
4343

44-
mutating func perform(logHandle: inout LogHandle) async throws -> ActionResult {
44+
mutating func perform(logHandle: LogHandle) throws -> ActionResult {
4545
let workspace = DocumentationWorkspace()
4646
let context = try DocumentationContext(dataProvider: workspace)
4747

Sources/SwiftDocCUtilities/Action/Actions/IndexAction.swift

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import Foundation
1212
import SwiftDocC
1313

1414
/// An action that creates an index of a documentation bundle.
15-
public struct IndexAction: AsyncAction {
15+
public struct IndexAction: Action {
1616
let rootURL: URL
1717
let outputURL: URL
1818
let bundleIdentifier: String
@@ -22,7 +22,8 @@ public struct IndexAction: AsyncAction {
2222
private var dataProvider: LocalFileSystemDataProvider!
2323

2424
/// Initializes the action with the given validated options, creates or uses the given action workspace & context.
25-
public init(documentationBundleURL: URL, outputURL: URL, bundleIdentifier: String, diagnosticEngine: DiagnosticEngine = .init()) throws {
25+
public init(documentationBundleURL: URL, outputURL: URL, bundleIdentifier: String, diagnosticEngine: DiagnosticEngine = .init()) throws
26+
{
2627
// Initialize the action context.
2728
self.rootURL = documentationBundleURL
2829
self.outputURL = outputURL
@@ -34,7 +35,7 @@ public struct IndexAction: AsyncAction {
3435

3536
/// Converts each eligible file from the source documentation bundle,
3637
/// saves the results in the given output alongside the template files.
37-
mutating public func perform(logHandle: inout LogHandle) async throws -> ActionResult {
38+
mutating public func perform(logHandle: LogHandle) throws -> ActionResult {
3839
let problems = try buildIndex()
3940
diagnosticEngine.emit(problems)
4041

Sources/SwiftDocCUtilities/Action/Actions/Init/InitAction.swift

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,8 @@ import Foundation
1212
import SwiftDocC
1313

1414
/// An action that generates a documentation catalog from a template seed.
15-
public struct InitAction: AsyncAction {
16-
15+
public struct InitAction: Action {
16+
1717
enum Error: DescribedError {
1818
case catalogAlreadyExists
1919
var errorDescription: String {
@@ -72,7 +72,8 @@ public struct InitAction: AsyncAction {
7272
/// Generates a documentation catalog from a catalog template.
7373
///
7474
/// - Parameter logHandle: The file handle that the convert and preview actions will print debug messages to.
75-
public mutating func perform(logHandle: inout LogHandle) async throws -> ActionResult {
75+
public mutating func perform(logHandle: SwiftDocC.LogHandle) throws -> ActionResult {
76+
7677
let diagnosticEngine: DiagnosticEngine = DiagnosticEngine(treatWarningsAsErrors: false)
7778
diagnosticEngine.filterLevel = .warning
7879
diagnosticEngine.add(DiagnosticConsoleWriter(formattingOptions: []))

Sources/SwiftDocCUtilities/Action/Actions/Merge/MergeAction.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ import SwiftDocC
1313
import Markdown
1414

1515
/// An action that merges a list of documentation archives into a combined archive.
16-
struct MergeAction: AsyncAction {
16+
struct MergeAction: Action {
1717
var archives: [URL]
1818
var landingPageInfo: LandingPageInfo
1919
var outputURL: URL
@@ -33,7 +33,7 @@ struct MergeAction: AsyncAction {
3333
}
3434
}
3535

36-
mutating func perform(logHandle: inout LogHandle) async throws -> ActionResult {
36+
mutating func perform(logHandle: LogHandle) throws -> ActionResult {
3737
guard let firstArchive = archives.first else {
3838
// A validation warning should have already been raised in `Docc/Merge/InputAndOutputOptions/validate()`.
3939
return ActionResult(didEncounterError: true, outputs: [])

0 commit comments

Comments
 (0)