-
Notifications
You must be signed in to change notification settings - Fork 102
/
Copy pathWaitFor.swift
270 lines (250 loc) · 10.7 KB
/
WaitFor.swift
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
//
// This source file is part of the Swift.org open source project
//
// Copyright (c) 2024 Apple Inc. and the Swift project authors
// Licensed under Apache License v2.0 with Runtime Library Exception
//
// See https://swift.org/LICENSE.txt for license information
// See https://swift.org/CONTRIBUTORS.txt for Swift project authors
//
#if !SWT_NO_PROCESS_SPAWNING
internal import _TestingInternals
#if SWT_TARGET_OS_APPLE || os(Linux) || os(FreeBSD)
/// Block the calling thread, wait for the target process to exit, and return
/// a value describing the conditions under which it exited.
///
/// - Parameters:
/// - pid: The ID of the process to wait for.
///
/// - Throws: If the exit status of the process with ID `pid` cannot be
/// determined (i.e. it does not represent an exit condition.)
private func _blockAndWait(for pid: consuming pid_t) throws -> ExitCondition {
let pid = consume pid
// Get the exit status of the process or throw an error (other than EINTR.)
while true {
var siginfo = siginfo_t()
if 0 == waitid(P_PID, id_t(pid), &siginfo, WEXITED) {
switch siginfo.si_code {
case .init(CLD_EXITED):
return .exitCode(siginfo.si_status)
case .init(CLD_KILLED), .init(CLD_DUMPED):
return .signal(siginfo.si_status)
default:
throw SystemError(description: "Unexpected siginfo_t value. Please file a bug report at https://github.com/swiftlang/swift-testing/issues/new and include this information: \(String(reflecting: siginfo))")
}
} else if case let errorCode = swt_errno(), errorCode != EINTR {
throw CError(rawValue: errorCode)
}
}
}
#endif
#if SWT_TARGET_OS_APPLE && !SWT_NO_LIBDISPATCH
/// Asynchronously wait for a process to terminate using a dispatch source.
///
/// - Parameters:
/// - processID: The ID of the process to wait for.
///
/// - Returns: The exit condition of `processID`.
///
/// - Throws: If the exit status of the process with ID `processID` cannot be
/// determined (i.e. it does not represent an exit condition.)
///
/// This implementation of `wait(for:)` suspends the calling task until
/// libdispatch reports that `processID` has terminated, then synchronously
/// calls `_blockAndWait(for:)` (which should not block because `processID` will
/// have already terminated by that point.)
///
/// - Note: The open-source implementation of libdispatch available on Linux
/// and other platforms does not support `DispatchSourceProcess`. Those
/// platforms use an alternate implementation below.
func wait(for pid: consuming pid_t) async throws -> ExitCondition {
let pid = consume pid
let source = DispatchSource.makeProcessSource(identifier: pid, eventMask: .exit)
defer {
source.cancel()
}
await withCheckedContinuation { continuation in
source.setEventHandler {
continuation.resume()
}
source.resume()
}
withExtendedLifetime(source) {}
return try _blockAndWait(for: pid)
}
#elseif SWT_TARGET_OS_APPLE || os(Linux) || os(FreeBSD)
/// A mapping of awaited child PIDs to their corresponding Swift continuations.
private let _childProcessContinuations = Locked<[pid_t: CheckedContinuation<ExitCondition, any Error>]>()
/// A condition variable used to suspend the waiter thread created by
/// `_createWaitThread()` when there are no child processes to await.
private nonisolated(unsafe) let _waitThreadNoChildrenCondition = {
let result = UnsafeMutablePointer<pthread_cond_t>.allocate(capacity: 1)
_ = pthread_cond_init(result, nil)
return result
}()
/// Create a waiter thread that is responsible for waiting for child processes
/// to exit.
private let _createWaitThread: Void = {
// The body of the thread's run loop.
func waitForAnyChild() {
// Listen for child process exit events. WNOWAIT means we don't perturb the
// state of a terminated (zombie) child process, allowing us to fetch the
// continuation (if available) before reaping.
var siginfo = siginfo_t()
if 0 == waitid(P_ALL, 0, &siginfo, WEXITED | WNOWAIT) {
if case let pid = siginfo.si_pid, pid != 0 {
let continuation = _childProcessContinuations.withLock { childProcessContinuations in
childProcessContinuations.removeValue(forKey: pid)
}
// If we had a continuation for this PID, allow the process to be reaped
// and pass the resulting exit condition back to the calling task. If
// there is no continuation, then either it hasn't been stored yet or
// this child process is not tracked by the waiter thread.
if let continuation {
let result = Result {
try _blockAndWait(for: pid)
}
continuation.resume(with: result)
}
}
} else if case let errorCode = swt_errno(), errorCode == ECHILD {
// We got ECHILD. If there are no continuations added right now, we should
// suspend this thread on the no-children condition until it's awoken by a
// newly-scheduled waiter process. (If this condition is spuriously
// woken, we'll just loop again, which is fine.) Note that we read errno
// outside the lock in case acquiring the lock perturbs it.
_childProcessContinuations.withUnsafeUnderlyingLock { lock, childProcessContinuations in
if childProcessContinuations.isEmpty {
_ = pthread_cond_wait(_waitThreadNoChildrenCondition, lock)
}
}
}
}
// Create the thread. It will run immediately; because it runs in an infinite
// loop, we aren't worried about detaching or joining it.
#if SWT_TARGET_OS_APPLE
var thread: pthread_t?
#else
var thread = pthread_t()
#endif
_ = pthread_create(
&thread,
nil,
{ _ in
// Set the thread name to help with diagnostics. Note that different
// platforms support different thread name lengths. See MAXTHREADNAMESIZE
// on Darwin, TASK_COMM_LEN on Linux, and MAXCOMLEN on FreeBSD. We try to
// maximize legibility in the available space.
#if SWT_TARGET_OS_APPLE
_ = pthread_setname_np("Swift Testing exit test monitor")
#elseif os(Linux)
_ = swt_pthread_setname_np(pthread_self(), "SWT ExT monitor")
#elseif os(FreeBSD)
_ = pthread_set_name_np(pthread_self(), "SWT ex test monitor")
#else
#warning("Platform-specific implementation missing: thread naming unavailable")
#endif
// Run an infinite loop that waits for child processes to terminate and
// captures their exit statuses.
while true {
waitForAnyChild()
}
},
nil
)
}()
/// Asynchronously wait for a process to terminate using a background thread
/// that calls `waitid()` in a loop.
///
/// - Parameters:
/// - processID: The ID of the process to wait for.
///
/// - Returns: The exit condition of `processID`.
///
/// - Throws: If the exit status of the process with ID `processID` cannot be
/// determined (i.e. it does not represent an exit condition.)
///
/// This implementation of `wait(for:)` suspends the calling task until
/// `waitid()`, called on a shared background thread, reports that `processID`
/// has terminated, then calls `_blockAndWait(for:)` (which should not block
/// because `processID` will have already terminated by that point.)
///
/// On Apple platforms, the libdispatch-based implementation above is more
/// efficient because it does not need to permanently reserve a thread.
func wait(for pid: consuming pid_t) async throws -> ExitCondition {
let pid = consume pid
// Ensure the waiter thread is running.
_createWaitThread
return try await withCheckedThrowingContinuation { continuation in
_childProcessContinuations.withLock { childProcessContinuations in
// We don't need to worry about a race condition here because waitid()
// does not clear the wait/zombie state of the child process. If it sees
// the child process has terminated and manages to acquire the lock before
// we add this continuation to the dictionary, then it will simply loop
// and report the status again.
let oldContinuation = childProcessContinuations.updateValue(continuation, forKey: pid)
assert(oldContinuation == nil, "Unexpected continuation found for PID \(pid). Please file a bug report at https://github.com/swiftlang/swift-testing/issues/new")
// Wake up the waiter thread if it is waiting for more child processes.
_ = pthread_cond_signal(_waitThreadNoChildrenCondition)
}
}
}
#elseif os(Windows)
/// Asynchronously wait for a process to terminate using the Windows thread
/// pool.
///
/// - Parameters:
/// - processHandle: A Windows handle representing the process to wait for.
/// This handle is closed before the function returns.
///
/// - Returns: The exit condition of `processHandle`.
///
/// - Throws: Any error encountered calling `RegisterWaitForSingleObject()` or
/// `GetExitCodeProcess()`.
///
/// This implementation of `wait(for:)` calls `RegisterWaitForSingleObject()` to
/// wait for `processHandle`, suspends the calling task until the waiter's
/// callback is called, then calls `GetExitCodeProcess()`.
func wait(for processHandle: consuming HANDLE) async throws -> ExitCondition {
let processHandle = consume processHandle
defer {
_ = CloseHandle(processHandle)
}
// Once the continuation resumes, it will need to unregister the wait, so
// yield the wait handle back to the calling scope.
var waitHandle: HANDLE?
defer {
if let waitHandle {
_ = UnregisterWait(waitHandle)
}
}
try await withCheckedThrowingContinuation { (continuation: CheckedContinuation<Void, any Error>) in
// Set up a callback that immediately resumes the continuation and does no
// other work.
let context = Unmanaged.passRetained(continuation as AnyObject).toOpaque()
let callback: WAITORTIMERCALLBACK = { context, _ in
let continuation = Unmanaged<AnyObject>.fromOpaque(context!).takeRetainedValue() as! CheckedContinuation<Void, any Error>
continuation.resume()
}
// We only want the callback to fire once (and not be rescheduled.) Waiting
// may take an arbitrarily long time, so let the thread pool know that too.
let flags = ULONG(WT_EXECUTEONLYONCE | WT_EXECUTELONGFUNCTION)
guard RegisterWaitForSingleObject(&waitHandle, processHandle, callback, context, INFINITE, flags) else {
continuation.resume(throwing: Win32Error(rawValue: GetLastError()))
return
}
}
var status: DWORD = 0
guard GetExitCodeProcess(processHandle, &status) else {
// The child process terminated but we couldn't get its status back.
// Assume generic failure.
return .failure
}
// FIXME: handle SEH/VEH uncaught exceptions.
return .exitCode(CInt(bitPattern: .init(status)))
}
#else
#warning("Platform-specific implementation missing: cannot wait for child processes to exit")
func wait(for processID: consuming Never) async throws -> ExitCondition {}
#endif
#endif