Skip to content

Commit 297ec80

Browse files
committed
termination handler
motivation: allow clients of SwiftPM to terminate background activities changes: * introduce new terminator class that is a registry for termination handlers * use the terminator instad of ProcessSet to manage the termination of active processes (eg git, tests) and build system * adjust call sites TODO: * add more tests * integrate into dependency resolition rdar://64900054 rdar://63723896
1 parent cb7755b commit 297ec80

File tree

12 files changed

+483
-328
lines changed

12 files changed

+483
-328
lines changed

Sources/Basics/Terminator.swift

Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
/*
2+
This source file is part of the Swift.org open source project
3+
4+
Copyright (c) 2022 Apple Inc. and the Swift project authors
5+
Licensed under Apache License v2.0 with Runtime Library Exception
6+
7+
See http://swift.org/LICENSE.txt for license information
8+
See http://swift.org/CONTRIBUTORS.txt for Swift project authors
9+
*/
10+
11+
import Dispatch
12+
import Foundation
13+
import TSCBasic
14+
15+
public typealias TerminationHandler = () throws -> Void
16+
17+
#warning("make observabilityScope non optional")
18+
public class Terminator {
19+
public typealias RegistrationKey = String
20+
21+
private let killTimeout: DispatchTimeInterval
22+
private let observabilityScope: ObservabilityScope?
23+
24+
private let registry = ThreadSafeKeyValueStore<String, (name: String, handler: TerminationHandler)>()
25+
private let terminationQueue = DispatchQueue(label: "org.swift.swiftpm.terminator")
26+
27+
private let cancelled = ThreadSafeBox<Bool>(false)
28+
29+
30+
/// Create a process set.
31+
public init(
32+
killTimeout: DispatchTimeInterval = .seconds(30),
33+
observabilityScope: ObservabilityScope?
34+
) {
35+
self.killTimeout = killTimeout
36+
self.observabilityScope = observabilityScope
37+
}
38+
39+
@discardableResult
40+
public func register(name: String, handler: @escaping TerminationHandler) -> RegistrationKey {
41+
if self.cancelled.get() ?? false {
42+
self.observabilityScope?.emit(debug: "not registering '\(name)' with terminator as its already cancelled")
43+
}
44+
let key = UUID().uuidString
45+
self.observabilityScope?.emit(debug: "registering '\(name)' with terminator")
46+
self.registry[key] = (name: name, handler: handler)
47+
return key
48+
}
49+
50+
public func register(_ process: TSCBasic.Process) -> RegistrationKey {
51+
self.register(name: "\(process.arguments.joined(separator: " "))", handler: {
52+
process.terminate(timeout: .now() + self.killTimeout)
53+
})
54+
}
55+
56+
public func deregister(_ key: RegistrationKey) {
57+
self.registry[key] = nil
58+
}
59+
60+
public func terminate() {
61+
self.observabilityScope?.emit(info: "starting termination cycle with \(self.registry.count) termination handlers registered")
62+
63+
self.cancelled.put(true)
64+
65+
let success = ThreadSafeArrayStore<String>()
66+
let group = DispatchGroup()
67+
for (_, (name, handler)) in self.registry.get() {
68+
group.enter()
69+
self.terminationQueue.async {
70+
defer { group.leave() }
71+
do {
72+
self.observabilityScope?.emit(debug: "terminating '\(name)'")
73+
try handler()
74+
success.append(name)
75+
} catch {
76+
self.observabilityScope?.emit(warning: "failed terminating '\(name)': \(error)")
77+
}
78+
}
79+
}
80+
81+
if case .timedOut = group.wait(timeout: .now() + self.killTimeout) {
82+
self.observabilityScope?.emit(warning: "timeout waiting for termination with \(self.registry.count - success.count) termination handlers remaining")
83+
} else {
84+
self.observabilityScope?.emit(info: "termination cycle completed successfully")
85+
}
86+
}
87+
}
88+
89+
extension TSCBasic.Process {
90+
fileprivate func terminate(timeout: DispatchTime) {
91+
// send graceful shutdown signal
92+
self.signal(SIGINT)
93+
94+
// start a thread to see if we need to terminate more forcibly
95+
let forceKillSemaphore = DispatchSemaphore(value: 1)
96+
let forceKillThread = TSCBasic.Thread {
97+
if case .timedOut = forceKillSemaphore.wait(timeout: timeout) {
98+
// send a force-kill signal
99+
#if os(Windows)
100+
self.signal(SIGTERM)
101+
#else
102+
self.signal(SIGKILL)
103+
#endif
104+
}
105+
}
106+
forceKillThread.start()
107+
_ = try? self.waitUntilExit()
108+
forceKillSemaphore.signal() // let the force-kill thread know we do not need it any more
109+
// join the force-kill thread thread so we don't exit before everything terminates
110+
forceKillThread.join()
111+
}
112+
}

Sources/Commands/APIDigester.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,7 @@ struct APIDigesterBaselineDumper {
9090
}
9191

9292
// Clone the current package in a sandbox and checkout the baseline revision.
93-
let repositoryProvider = GitRepositoryProvider()
93+
let repositoryProvider = GitRepositoryProvider(terminator: swiftTool.terminator)
9494
let specifier = RepositorySpecifier(path: baselinePackageRoot)
9595
let workingCopy = try repositoryProvider.createWorkingCopy(
9696
repository: specifier,

Sources/Commands/SwiftPackageTool.swift

Lines changed: 12 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -394,7 +394,7 @@ extension SwiftPackageTool {
394394
let apiDigesterTool = SwiftAPIDigester(fileSystem: swiftTool.fileSystem, tool: apiDigesterPath)
395395

396396
let packageRoot = try swiftOptions.packagePath ?? swiftTool.getPackageRoot()
397-
let repository = GitRepository(path: packageRoot)
397+
let repository = GitRepository(path: packageRoot, terminator: swiftTool.terminator)
398398
let baselineRevision = try repository.resolveRevision(identifier: treeish)
399399

400400
// We turn build manifest caching off because we need the build plan.
@@ -883,7 +883,7 @@ extension SwiftPackageTool {
883883

884884
func run(_ swiftTool: SwiftTool) throws {
885885
let packageRoot = try swiftOptions.packagePath ?? swiftTool.getPackageRoot()
886-
let repository = GitRepository(path: packageRoot)
886+
let repository = GitRepository(path: packageRoot, terminator: swiftTool.terminator)
887887

888888
let destination: AbsolutePath
889889
if let output = output {
@@ -1170,22 +1170,14 @@ final class PluginDelegate: PluginInvocationDelegate {
11701170

11711171
// Create a build operation. We have to disable the cache in order to get a build plan created.
11721172
let outputStream = BufferedOutputByteStream()
1173-
let buildOperation = BuildOperation(
1174-
buildParameters: buildParameters,
1173+
let buildOperation = try swiftTool.createBuildOperation(
1174+
explicitProduct: explicitProduct,
11751175
cacheBuildManifest: false,
1176-
packageGraphLoader: { try self.swiftTool.loadPackageGraph(explicitProduct: explicitProduct) },
1177-
pluginScriptRunner: try self.swiftTool.getPluginScriptRunner(),
1178-
pluginWorkDirectory: try self.swiftTool.getActiveWorkspace().location.pluginWorkingDirectory,
1179-
outputStream: outputStream,
1180-
logLevel: logLevel,
1181-
fileSystem: swiftTool.fileSystem,
1182-
observabilityScope: self.swiftTool.observabilityScope
1176+
customBuildParameters: buildParameters,
1177+
customOutputStream: outputStream,
1178+
customLogLevel: logLevel
11831179
)
1184-
1185-
// Save the instance so it can be canceled from the interrupt handler.
1186-
self.swiftTool.buildSystemRef.buildSystem = buildOperation
1187-
1188-
// Get or create the build description and plan the build.
1180+
11891181
let _ = try buildOperation.getBuildDescription()
11901182
let buildPlan = buildOperation.buildPlan!
11911183

@@ -1280,7 +1272,7 @@ final class PluginDelegate: PluginInvocationDelegate {
12801272
let testRunner = TestRunner(
12811273
bundlePaths: [testProduct.bundlePath],
12821274
xctestArg: testSpecifier,
1283-
processSet: swiftTool.processSet,
1275+
terminator: swiftTool.terminator,
12841276
toolchain: toolchain,
12851277
testEnv: testEnvironment,
12861278
observabilityScope: swiftTool.observabilityScope)
@@ -1555,15 +1547,16 @@ extension SwiftPackageTool {
15551547
dstdir = try swiftTool.getPackageRoot()
15561548
projectName = graph.rootPackages[0].manifest.displayName // TODO: use identity instead?
15571549
}
1558-
let xcodeprojPath = Xcodeproj.buildXcodeprojPath(outputDir: dstdir, projectName: projectName)
1550+
let xcodeprojPath = XcodeProject.makePath(outputDir: dstdir, projectName: projectName)
15591551

15601552
var genOptions = xcodeprojOptions()
15611553
genOptions.manifestLoader = try swiftTool.getManifestLoader()
15621554

1563-
try Xcodeproj.generate(
1555+
try XcodeProject.generate(
15641556
projectName: projectName,
15651557
xcodeprojPath: xcodeprojPath,
15661558
graph: graph,
1559+
repositoryProvider: GitRepositoryProvider(terminator: swiftTool.terminator),
15671560
options: genOptions,
15681561
fileSystem: swiftTool.fileSystem,
15691562
observabilityScope: swiftTool.observabilityScope

Sources/Commands/SwiftTestTool.swift

Lines changed: 11 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -283,7 +283,7 @@ public struct SwiftTestTool: SwiftCommand {
283283
let runner = TestRunner(
284284
bundlePaths: testProducts.map { $0.bundlePath },
285285
xctestArg: xctestArg,
286-
processSet: swiftTool.processSet,
286+
terminator: swiftTool.terminator,
287287
toolchain: toolchain,
288288
testEnv: testEnv,
289289
observabilityScope: swiftTool.observabilityScope
@@ -327,7 +327,7 @@ public struct SwiftTestTool: SwiftCommand {
327327
// Run the tests using the parallel runner.
328328
let runner = ParallelTestRunner(
329329
bundlePaths: testProducts.map { $0.bundlePath },
330-
processSet: swiftTool.processSet,
330+
terminator: swiftTool.terminator,
331331
toolchain: toolchain,
332332
numJobs: options.numberOfWorkers ?? ProcessInfo.processInfo.activeProcessorCount,
333333
options: swiftOptions,
@@ -514,7 +514,7 @@ final class TestRunner {
514514
/// Arguments to pass to XCTest if any.
515515
private let xctestArg: String?
516516

517-
private let processSet: ProcessSet
517+
private let terminator: Terminator
518518

519519
// The toolchain to use.
520520
private let toolchain: UserToolchain
@@ -532,14 +532,14 @@ final class TestRunner {
532532
init(
533533
bundlePaths: [AbsolutePath],
534534
xctestArg: String? = nil,
535-
processSet: ProcessSet,
535+
terminator: Terminator,
536536
toolchain: UserToolchain,
537537
testEnv: [String: String],
538538
observabilityScope: ObservabilityScope
539539
) {
540540
self.bundlePaths = bundlePaths
541541
self.xctestArg = xctestArg
542-
self.processSet = processSet
542+
self.terminator = terminator
543543
self.toolchain = toolchain
544544
self.testEnv = testEnv
545545
self.observabilityScope = observabilityScope.makeChildScope(description: "Test Runner")
@@ -591,7 +591,8 @@ final class TestRunner {
591591
stderr: outputHandler
592592
)
593593
let process = Process(arguments: try args(forTestAt: path), environment: self.testEnv, outputRedirection: outputRedirection)
594-
try self.processSet.add(process)
594+
let terminationKey = self.terminator.register(process)
595+
defer { self.terminator.deregister(terminationKey) }
595596
try process.launch()
596597
let result = try process.waitUntilExit()
597598
switch result.exitStatus {
@@ -605,8 +606,6 @@ final class TestRunner {
605606
default:
606607
return false
607608
}
608-
} catch ProcessSetError.cancelled {
609-
return false
610609
} catch {
611610
testObservabilityScope.emit(error)
612611
return false
@@ -644,7 +643,7 @@ final class ParallelTestRunner {
644643
/// True if all tests executed successfully.
645644
private(set) var ranSuccessfully = true
646645

647-
private let processSet: ProcessSet
646+
private let terminator: Terminator
648647

649648
private let toolchain: UserToolchain
650649

@@ -662,7 +661,7 @@ final class ParallelTestRunner {
662661

663662
init(
664663
bundlePaths: [AbsolutePath],
665-
processSet: ProcessSet,
664+
terminator: Terminator,
666665
toolchain: UserToolchain,
667666
numJobs: Int,
668667
options: SwiftToolOptions,
@@ -671,7 +670,7 @@ final class ParallelTestRunner {
671670
observabilityScope: ObservabilityScope
672671
) {
673672
self.bundlePaths = bundlePaths
674-
self.processSet = processSet
673+
self.terminator = terminator
675674
self.toolchain = toolchain
676675
self.numJobs = numJobs
677676
self.shouldOutputSuccess = shouldOutputSuccess
@@ -727,7 +726,7 @@ final class ParallelTestRunner {
727726
let testRunner = TestRunner(
728727
bundlePaths: [test.productPath],
729728
xctestArg: test.specifier,
730-
processSet: self.processSet,
729+
terminator: self.terminator,
731730
toolchain: self.toolchain,
732731
testEnv: testEnv,
733732
observabilityScope: self.observabilityScope

0 commit comments

Comments
 (0)