Skip to content

Commit f5bae92

Browse files
committed
Split SWT_NO_EXIT_TESTS into SWT_NO_EXIT_TESTS and SWT_NO_PROCESS_SPAWNING.
This PR separates out our process-spawning code to be guarded by `SWT_NO_PROCESS_SPAWNING` instead of `SWT_NO_EXIT_TESTS`. We do this so that we can potentially use process spawning on platforms where exit tests are not supported for some other reason (such as the iOS/Android sandboxes) but process spawning is still internally possible. There are a few use cases we have for spawning processes that don't involve exit tests: - Calling out to `tar` to compress attachments (see #714) - Running non-Swift scripts in their interpreters (see #478) - Multi-process parallelism (the XCTest model) I took the opportunity to clean up WaitFor.swift a bit and rearrange code so that the "new platform, dunno who lives here" case should compile (although not function) out-of-the-box.
1 parent d50f654 commit f5bae92

File tree

5 files changed

+91
-50
lines changed

5 files changed

+91
-50
lines changed

Package.swift

+2
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,7 @@ extension Array where Element == PackageDescription.SwiftSetting {
131131
.define("SWT_TARGET_OS_APPLE", .when(platforms: [.macOS, .iOS, .macCatalyst, .watchOS, .tvOS, .visionOS])),
132132

133133
.define("SWT_NO_EXIT_TESTS", .when(platforms: [.iOS, .watchOS, .tvOS, .visionOS, .wasi, .android])),
134+
.define("SWT_NO_PROCESS_SPAWNING", .when(platforms: [.iOS, .watchOS, .tvOS, .visionOS, .wasi, .android])),
134135
.define("SWT_NO_SNAPSHOT_TYPES", .when(platforms: [.linux, .windows, .wasi])),
135136
.define("SWT_NO_DYNAMIC_LINKING", .when(platforms: [.wasi])),
136137
.define("SWT_NO_PIPES", .when(platforms: [.wasi])),
@@ -164,6 +165,7 @@ extension Array where Element == PackageDescription.CXXSetting {
164165

165166
result += [
166167
.define("SWT_NO_EXIT_TESTS", .when(platforms: [.iOS, .watchOS, .tvOS, .visionOS, .wasi, .android])),
168+
.define("SWT_NO_PROCESS_SPAWNING", .when(platforms: [.iOS, .watchOS, .tvOS, .visionOS, .wasi, .android])),
167169
.define("SWT_NO_SNAPSHOT_TYPES", .when(platforms: [.linux, .windows, .wasi])),
168170
.define("SWT_NO_DYNAMIC_LINKING", .when(platforms: [.wasi])),
169171
.define("SWT_NO_PIPES", .when(platforms: [.wasi])),

Sources/Testing/ExitTests/ExitCondition.swift

+15-11
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,10 @@ private import _TestingInternals
1818
/// ``require(exitsWith:_:sourceLocation:performing:)`` to configure which exit
1919
/// statuses should be considered successful.
2020
@_spi(Experimental)
21-
#if SWT_NO_EXIT_TESTS
21+
#if SWT_NO_PROCESS_SPAWNING
2222
@available(*, unavailable, message: "Exit tests are not available on this platform.")
23+
#elseif SWT_NO_EXIT_TESTS
24+
@_spi(ForToolsIntegrationOnly)
2325
#endif
2426
public enum ExitCondition: Sendable {
2527
/// The process terminated successfully with status `EXIT_SUCCESS`.
@@ -78,8 +80,10 @@ public enum ExitCondition: Sendable {
7880

7981
// MARK: - Equatable
8082

81-
#if SWT_NO_EXIT_TESTS
83+
#if SWT_NO_PROCESS_SPAWNING
8284
@available(*, unavailable, message: "Exit tests are not available on this platform.")
85+
#elseif SWT_NO_EXIT_TESTS
86+
@_spi(ForToolsIntegrationOnly)
8387
#endif
8488
extension ExitCondition {
8589
/// Check whether or not two values of this type are equal.
@@ -108,9 +112,7 @@ extension ExitCondition {
108112
///
109113
/// For any values `a` and `b`, `a == b` implies that `a != b` is `false`.
110114
public static func ==(lhs: Self, rhs: Self) -> Bool {
111-
#if SWT_NO_EXIT_TESTS
112-
fatalError("Unsupported")
113-
#else
115+
#if !SWT_NO_PROCESS_SPAWNING
114116
return switch (lhs, rhs) {
115117
case let (.failure, .exitCode(exitCode)), let (.exitCode(exitCode), .failure):
116118
exitCode != EXIT_SUCCESS
@@ -122,6 +124,8 @@ extension ExitCondition {
122124
default:
123125
lhs === rhs
124126
}
127+
#else
128+
fatalError("Unsupported")
125129
#endif
126130
}
127131

@@ -152,10 +156,10 @@ extension ExitCondition {
152156
///
153157
/// For any values `a` and `b`, `a == b` implies that `a != b` is `false`.
154158
public static func !=(lhs: Self, rhs: Self) -> Bool {
155-
#if SWT_NO_EXIT_TESTS
156-
fatalError("Unsupported")
157-
#else
159+
#if !SWT_NO_PROCESS_SPAWNING
158160
!(lhs == rhs)
161+
#else
162+
fatalError("Unsupported")
159163
#endif
160164
}
161165

@@ -226,10 +230,10 @@ extension ExitCondition {
226230
///
227231
/// For any values `a` and `b`, `a === b` implies that `a !== b` is `false`.
228232
public static func !==(lhs: Self, rhs: Self) -> Bool {
229-
#if SWT_NO_EXIT_TESTS
230-
fatalError("Unsupported")
231-
#else
233+
#if !SWT_NO_PROCESS_SPAWNING
232234
!(lhs === rhs)
235+
#else
236+
fatalError("Unsupported")
233237
#endif
234238
}
235239
}

Sources/Testing/ExitTests/SpawnProcess.swift

+1-1
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010

1111
internal import _TestingInternals
1212

13-
#if !SWT_NO_EXIT_TESTS
13+
#if !SWT_NO_PROCESS_SPAWNING
1414
/// A platform-specific value identifying a process running on the current
1515
/// system.
1616
#if SWT_TARGET_OS_APPLE || os(Linux) || os(FreeBSD)

Sources/Testing/ExitTests/WaitFor.swift

+69-38
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,7 @@
88
// See https://swift.org/CONTRIBUTORS.txt for Swift project authors
99
//
1010

11-
#if !SWT_NO_EXIT_TESTS
12-
11+
#if !SWT_NO_PROCESS_SPAWNING
1312
internal import _TestingInternals
1413

1514
#if SWT_TARGET_OS_APPLE || os(Linux) || os(FreeBSD)
@@ -41,8 +40,45 @@ private func _blockAndWait(for pid: consuming pid_t) throws -> ExitCondition {
4140
}
4241
}
4342
}
43+
#endif
44+
45+
#if SWT_TARGET_OS_APPLE && !SWT_NO_LIBDISPATCH
46+
/// Asynchronously wait for a process to terminate using a dispatch source.
47+
///
48+
/// - Parameters:
49+
/// - processID: The ID of the process to wait for.
50+
///
51+
/// - Returns: The exit condition of `processID`.
52+
///
53+
/// - Throws: If the exit status of the process with ID `processID` cannot be
54+
/// determined (i.e. it does not represent an exit condition.)
55+
///
56+
/// This implementation of `wait(for:)` suspends the calling task until
57+
/// libdispatch reports that `processID` has terminated, then synchronously
58+
/// calls `_blockAndWait(for:)` (which should not block because `processID` will
59+
/// have already terminated by that point.)
60+
///
61+
/// - Note: The open-source implementation of libdispatch available on Linux
62+
/// and other platforms does not support `DispatchSourceProcess`. Those
63+
/// platforms use an alternate implementation below.
64+
func wait(for pid: consuming pid_t) async throws -> ExitCondition {
65+
let pid = consume pid
66+
67+
let source = DispatchSource.makeProcessSource(identifier: pid, eventMask: .exit)
68+
defer {
69+
source.cancel()
70+
}
71+
await withCheckedContinuation { continuation in
72+
source.setEventHandler {
73+
continuation.resume()
74+
}
75+
source.resume()
76+
}
77+
withExtendedLifetime(source) {}
4478

45-
#if !(SWT_TARGET_OS_APPLE && !SWT_NO_LIBDISPATCH)
79+
return try _blockAndWait(for: pid)
80+
}
81+
#elseif SWT_TARGET_OS_APPLE || os(Linux) || os(FreeBSD)
4682
/// A mapping of awaited child PIDs to their corresponding Swift continuations.
4783
private let _childProcessContinuations = Locked<[pid_t: CheckedContinuation<ExitCondition, any Error>]>()
4884

@@ -54,8 +90,9 @@ private nonisolated(unsafe) let _waitThreadNoChildrenCondition = {
5490
return result
5591
}()
5692

57-
/// The implementation of `_createWaitThread()`, run only once.
58-
private let _createWaitThreadImpl: Void = {
93+
/// Create a waiter thread that is responsible for waiting for child processes
94+
/// to exit.
95+
private let _createWaitThread: Void = {
5996
// The body of the thread's run loop.
6097
func waitForAnyChild() {
6198
// Listen for child process exit events. WNOWAIT means we don't perturb the
@@ -128,42 +165,29 @@ private let _createWaitThreadImpl: Void = {
128165
)
129166
}()
130167

131-
/// Create a waiter thread that is responsible for waiting for child processes
132-
/// to exit.
133-
private func _createWaitThread() {
134-
_createWaitThreadImpl
135-
}
136-
#endif
137-
138-
/// Wait for a given PID to exit and report its status.
168+
/// Asynchronously wait for a process to terminate using a background thread
169+
/// that calls `waitid()` in a loop.
139170
///
140171
/// - Parameters:
141-
/// - pid: The PID to wait for.
172+
/// - processID: The ID of the process to wait for.
173+
///
174+
/// - Returns: The exit condition of `processID`.
142175
///
143-
/// - Returns: The exit condition of `pid`.
176+
/// - Throws: If the exit status of the process with ID `processID` cannot be
177+
/// determined (i.e. it does not represent an exit condition.)
178+
///
179+
/// This implementation of `wait(for:)` suspends the calling task until
180+
/// `waitid()`, called on a shared background thread, reports that `processID`
181+
/// has terminated, then calls `_blockAndWait(for:)` (which should not block
182+
/// because `processID` will have already terminated by that point.)
144183
///
145-
/// - Throws: Any error encountered calling `waitpid()` except for `EINTR`,
146-
/// which is ignored.
184+
/// On Apple platforms, the libdispatch-based implementation above is more
185+
/// efficient because it does not need to permanently reserve a thread.
147186
func wait(for pid: consuming pid_t) async throws -> ExitCondition {
148187
let pid = consume pid
149188

150-
#if SWT_TARGET_OS_APPLE && !SWT_NO_LIBDISPATCH
151-
let source = DispatchSource.makeProcessSource(identifier: pid, eventMask: .exit)
152-
defer {
153-
source.cancel()
154-
}
155-
await withCheckedContinuation { continuation in
156-
source.setEventHandler {
157-
continuation.resume()
158-
}
159-
source.resume()
160-
}
161-
withExtendedLifetime(source) {}
162-
163-
return try _blockAndWait(for: pid)
164-
#else
165189
// Ensure the waiter thread is running.
166-
_createWaitThread()
190+
_createWaitThread
167191

168192
return try await withCheckedThrowingContinuation { continuation in
169193
_childProcessContinuations.withLock { childProcessContinuations in
@@ -179,19 +203,23 @@ func wait(for pid: consuming pid_t) async throws -> ExitCondition {
179203
_ = pthread_cond_signal(_waitThreadNoChildrenCondition)
180204
}
181205
}
182-
#endif
183206
}
184207
#elseif os(Windows)
185-
/// Wait for a given process handle to exit and report its status.
208+
/// Asynchronously wait for a process to terminate using the Windows thread
209+
/// pool.
186210
///
187211
/// - Parameters:
188-
/// - processHandle: The handle to wait for. This function takes ownership of
189-
/// this handle and closes it when done.
212+
/// - processHandle: A Windows handle representing the process to wait for.
213+
/// This handle is closed before the function returns.
190214
///
191215
/// - Returns: The exit condition of `processHandle`.
192216
///
193-
/// - Throws: Any error encountered calling `WaitForSingleObject()` or
217+
/// - Throws: Any error encountered calling `RegisterWaitForSingleObject()` or
194218
/// `GetExitCodeProcess()`.
219+
///
220+
/// This implementation of `wait(for:)` calls `RegisterWaitForSingleObject()` to
221+
/// wait for `processHandle`, suspends the calling task until the waiter's
222+
/// callback is called, then calls `GetExitCodeProcess()`.
195223
func wait(for processHandle: consuming HANDLE) async throws -> ExitCondition {
196224
let processHandle = consume processHandle
197225
defer {
@@ -235,5 +263,8 @@ func wait(for processHandle: consuming HANDLE) async throws -> ExitCondition {
235263
// FIXME: handle SEH/VEH uncaught exceptions.
236264
return .exitCode(CInt(bitPattern: .init(status)))
237265
}
266+
#else
267+
#warning("Platform-specific implementation missing: cannot wait for child processes to exit")
268+
func wait(for processID: consuming Never) async throws -> ExitCondition {}
238269
#endif
239270
#endif

cmake/modules/shared/CompilerSettings.cmake

+4
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,10 @@ set(SWT_NO_EXIT_TESTS_LIST "iOS" "watchOS" "tvOS" "visionOS" "WASI" "Android")
2424
if(CMAKE_SYSTEM_NAME IN_LIST SWT_NO_EXIT_TESTS_LIST)
2525
add_compile_definitions("SWT_NO_EXIT_TESTS")
2626
endif()
27+
set(SWT_NO_PROCESS_SPAWNING_LIST "iOS" "watchOS" "tvOS" "visionOS" "WASI" "Android")
28+
if(CMAKE_SYSTEM_NAME IN_LIST SWT_NO_PROCESS_SPAWNING_LIST)
29+
add_compile_definitions("SWT_NO_PROCESS_SPAWNING")
30+
endif()
2731
if(NOT APPLE)
2832
add_compile_definitions("SWT_NO_SNAPSHOT_TYPES")
2933
endif()

0 commit comments

Comments
 (0)