Skip to content

Commit ea9e277

Browse files
committed
Basics: allow reusing cancellator handlers from SwiftTool (#6396)
These signal handlers are useful to other tools that don't have access to `SwiftTool`. For example, `DestinationCommand` would benefit from this as it needs to set up its own cancellator for handling archiver processes. Moved signal handler setup to a new `installSignalHandlers` function on `Cancellator`. Made captures of `Cancellator` in handler closures weak so that it's not referenced indefinitely and is released when no longer needed.
1 parent 34253fa commit ea9e277

File tree

3 files changed

+81
-52
lines changed

3 files changed

+81
-52
lines changed

Sources/Basics/Cancellator.swift

Lines changed: 75 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,20 +14,94 @@ import Dispatch
1414
import Foundation
1515
import TSCBasic
1616

17+
#if canImport(WinSDK)
18+
import WinSDK
19+
#endif
20+
1721
public typealias CancellationHandler = (DispatchTime) throws -> Void
1822

19-
public class Cancellator: Cancellable {
23+
public final class Cancellator: Cancellable {
2024
public typealias RegistrationKey = String
2125

2226
private let observabilityScope: ObservabilityScope?
2327
private let registry = ThreadSafeKeyValueStore<String, (name: String, handler: CancellationHandler)>()
2428
private let cancelationQueue = DispatchQueue(label: "org.swift.swiftpm.cancellator", qos: .userInteractive, attributes: .concurrent)
2529
private let cancelling = ThreadSafeBox<Bool>(false)
2630

31+
private static let signalHandlerLock = NSLock()
32+
private static var isSignalHandlerInstalled = false
33+
2734
public init(observabilityScope: ObservabilityScope?) {
2835
self.observabilityScope = observabilityScope
2936
}
3037

38+
#if os(Windows)
39+
// unfortunately this is needed for C callback handlers used by Windows shutdown handler
40+
static var shared: Cancellator?
41+
#endif
42+
43+
/// Installs signal handlers to terminate sub-processes on cancellation.
44+
public func installSignalHandlers() {
45+
Self.signalHandlerLock.withLock {
46+
precondition(!Self.isSignalHandlerInstalled)
47+
48+
#if os(Windows)
49+
// Closures passed to `SetConsoleCtrlHandler` can't capture context, working around that with a global.
50+
Self.shared = self
51+
52+
// set shutdown handler to terminate sub-processes, etc
53+
_ = SetConsoleCtrlHandler({ _ in
54+
// Terminate all processes on receiving an interrupt signal.
55+
try? Cancellator.shared?.cancel(deadline: .now() + .seconds(30))
56+
57+
// Reset the handler.
58+
_ = SetConsoleCtrlHandler(nil, false)
59+
60+
// Exit as if by signal()
61+
TerminateProcess(GetCurrentProcess(), 3)
62+
63+
return true
64+
}, true)
65+
#else
66+
// trap SIGINT to terminate sub-processes, etc
67+
signal(SIGINT, SIG_IGN)
68+
let interruptSignalSource = DispatchSource.makeSignalSource(signal: SIGINT)
69+
interruptSignalSource.setEventHandler { [weak self] in
70+
// cancel the trap?
71+
interruptSignalSource.cancel()
72+
73+
// Terminate all processes on receiving an interrupt signal.
74+
try? self?.cancel(deadline: .now() + .seconds(30))
75+
76+
#if os(macOS) || os(OpenBSD)
77+
// Install the default signal handler.
78+
var action = sigaction()
79+
action.__sigaction_u.__sa_handler = SIG_DFL
80+
sigaction(SIGINT, &action, nil)
81+
kill(getpid(), SIGINT)
82+
#elseif os(Android)
83+
// Install the default signal handler.
84+
var action = sigaction()
85+
action.sa_handler = SIG_DFL
86+
sigaction(SIGINT, &action, nil)
87+
kill(getpid(), SIGINT)
88+
#else
89+
var action = sigaction()
90+
action.__sigaction_handler = unsafeBitCast(
91+
SIG_DFL,
92+
to: sigaction.__Unnamed_union___sigaction_handler.self
93+
)
94+
sigaction(SIGINT, &action, nil)
95+
kill(getpid(), SIGINT)
96+
#endif
97+
}
98+
interruptSignalSource.resume()
99+
#endif
100+
101+
Self.isSignalHandlerInstalled = true
102+
}
103+
}
104+
31105
@discardableResult
32106
public func register(name: String, handler: @escaping CancellationHandler) -> RegistrationKey? {
33107
if self.cancelling.get(default: false) {

Sources/CoreCommands/SwiftTool.swift

Lines changed: 5 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -50,13 +50,16 @@ import var TSCUtility.verbosity
5050
typealias Diagnostic = Basics.Diagnostic
5151

5252
public struct ToolWorkspaceConfiguration {
53+
let shouldInstallSignalHandlers: Bool
5354
let wantsMultipleTestProducts: Bool
5455
let wantsREPLProduct: Bool
5556

5657
public init(
58+
shouldInstallSignalHandlers: Bool = true,
5759
wantsMultipleTestProducts: Bool = false,
5860
wantsREPLProduct: Bool = false
5961
) {
62+
self.shouldInstallSignalHandlers = shouldInstallSignalHandlers
6063
self.wantsMultipleTestProducts = wantsMultipleTestProducts
6164
self.wantsREPLProduct = wantsREPLProduct
6265
}
@@ -308,57 +311,9 @@ public final class SwiftTool {
308311
try ProcessEnv.chdir(packagePath)
309312
}
310313

311-
#if os(Windows)
312-
// set shutdown handler to terminate sub-processes, etc
313-
SwiftTool.cancellator = cancellator
314-
_ = SetConsoleCtrlHandler({ _ in
315-
// Terminate all processes on receiving an interrupt signal.
316-
try? SwiftTool.cancellator?.cancel(deadline: .now() + .seconds(30))
317-
318-
// Reset the handler.
319-
_ = SetConsoleCtrlHandler(nil, false)
320-
321-
// Exit as if by signal()
322-
TerminateProcess(GetCurrentProcess(), 3)
323-
324-
return true
325-
}, true)
326-
#else
327-
// trap SIGINT to terminate sub-processes, etc
328-
signal(SIGINT, SIG_IGN)
329-
let interruptSignalSource = DispatchSource.makeSignalSource(signal: SIGINT)
330-
interruptSignalSource.setEventHandler {
331-
// cancel the trap?
332-
interruptSignalSource.cancel()
333-
334-
// Terminate all processes on receiving an interrupt signal.
335-
try? cancellator.cancel(deadline: .now() + .seconds(30))
336-
337-
#if os(macOS) || os(OpenBSD)
338-
// Install the default signal handler.
339-
var action = sigaction()
340-
action.__sigaction_u.__sa_handler = SIG_DFL
341-
sigaction(SIGINT, &action, nil)
342-
kill(getpid(), SIGINT)
343-
#elseif os(Android)
344-
// Install the default signal handler.
345-
var action = sigaction()
346-
action.sa_handler = SIG_DFL
347-
sigaction(SIGINT, &action, nil)
348-
kill(getpid(), SIGINT)
349-
#else
350-
var action = sigaction()
351-
action.__sigaction_handler = unsafeBitCast(
352-
SIG_DFL,
353-
to: sigaction.__Unnamed_union___sigaction_handler.self
354-
)
355-
sigaction(SIGINT, &action, nil)
356-
kill(getpid(), SIGINT)
357-
#endif
314+
if toolWorkspaceConfiguration.shouldInstallSignalHandlers {
315+
cancellator.installSignalHandlers()
358316
}
359-
interruptSignalSource.resume()
360-
#endif
361-
362317
self.cancellator = cancellator
363318
} catch {
364319
self.observabilityScope.emit(error)

Tests/CommandsTests/SwiftToolTests.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -237,7 +237,7 @@ extension SwiftTool {
237237
return try SwiftTool(
238238
outputStream: outputStream,
239239
options: options,
240-
toolWorkspaceConfiguration: .init(),
240+
toolWorkspaceConfiguration: .init(shouldInstallSignalHandlers: false),
241241
workspaceDelegateProvider: {
242242
ToolWorkspaceDelegate(
243243
observabilityScope: $0,

0 commit comments

Comments
 (0)