12
12
internal import TestingInternals
13
13
14
14
#if SWT_TARGET_OS_APPLE || os(Linux)
15
- /// Wait for a given PID to exit and report its status.
16
- ///
17
- /// - Parameters:
18
- /// - pid: The PID to wait for.
19
- ///
20
- /// - Returns: The exit condition of `pid`.
21
- ///
22
- /// - Throws: Any error encountered calling `waitpid()` except for `EINTR`,
23
- /// which is ignored.
15
+ extension ExitCondition {
16
+ /// Initialize an instance of this type from an instance of the POSIX
17
+ /// `siginfo_t` type.
18
+ ///
19
+ /// - Parameters:
20
+ /// - siginfo: The instance of `siginfo_t` to initialize from.
21
+ ///
22
+ /// - Throws: If `siginfo.si_code` does not equal either `CLD_EXITED`,
23
+ /// `CLD_KILLED`, or `CLD_DUMPED` (i.e. it does not represent an exit
24
+ /// condition.)
25
+ fileprivate init ( _ siginfo: siginfo_t ) throws {
26
+ switch siginfo. si_code {
27
+ case . init( CLD_EXITED) :
28
+ self = . exitCode( siginfo. si_status)
29
+ case . init( CLD_KILLED) , . init( CLD_DUMPED) :
30
+ self = . signal( siginfo. si_status)
31
+ default :
32
+ throw SystemError ( description: " Unexpected siginfo_t value. Please file a bug report at https://github.com/apple/swift-testing/issues/new and include this information: \( String ( reflecting: siginfo) ) " )
33
+ }
34
+ }
35
+ }
36
+
37
+ #if !(SWT_TARGET_OS_APPLE && !SWT_NO_LIBDISPATCH)
38
+ /// A mapping of awaited child PIDs to their corresponding Swift continuations.
39
+ private let _childProcessContinuations = Locked < [ pid_t : CheckedContinuation < ExitCondition , any Error > ] > ( )
40
+
41
+ /// A condition variable used to suspend the waiter thread created by
42
+ /// `_createWaitThread()` when there are no child processes to await.
43
+ private let _waitThreadNoChildrenCondition = {
44
+ let result = UnsafeMutablePointer< pthread_cond_t> . allocate( capacity: 1 )
45
+ _ = pthread_cond_init ( result, nil )
46
+ return UncheckedSendable ( rawValue: result)
47
+ } ( )
48
+
49
+ #if os(Linux)
50
+ /// Set the name of the current thread.
24
51
///
25
- /// This function blocks the calling thread on `waitpid()`. External callers
26
- /// should use ``wait(for:)`` instead to avoid deadlocks.
27
- private func _blockAndWait( for pid: pid_t ) throws -> ExitCondition {
28
- while true {
29
- var status : CInt = 0
30
- if waitpid ( pid, & status, 0 ) >= 0 {
31
- if swt_WIFSIGNALED ( status) {
32
- return . signal( swt_WTERMSIG ( status) )
33
- } else if swt_WIFEXITED ( status) {
34
- return . exitCode( swt_WEXITSTATUS ( status) )
35
- } else {
36
- // Unreachable: neither signalled nor exited, but waitpid()
37
- // and libdispatch indicate that the process has died.
38
- throw SystemError ( description: " Unexpected waitpid() result \( status) . Please file a bug report at https://github.com/apple/swift-testing/issues/new " )
52
+ /// This function declaration is provided because `pthread_setname_np()` is
53
+ /// only declared if `_GNU_SOURCE` is set, but setting it causes build errors
54
+ /// due to conflicts with Swift's Glibc module.
55
+ @_extern ( c) func pthread_setname_np( _: pthread_t , _: UnsafePointer < CChar > ) -> CInt
56
+ #endif
57
+
58
+ /// The implementation of `_createWaitThread()`, run only once.
59
+ private let _createWaitThreadImpl: Void = {
60
+ // The body of the thread's run loop.
61
+ func waitForAnyChild( ) {
62
+ // Listen for child process exit events. WNOWAIT means we don't perturb the
63
+ // state of a terminated (zombie) child process, allowing us to fetch the
64
+ // continuation (if available) before reaping.
65
+ var siginfo = siginfo_t ( )
66
+ if 0 == waitid ( P_ALL, 0 , & siginfo, WEXITED | WNOWAIT) {
67
+ if case let pid = siginfo. si_pid, pid != 0 {
68
+ let continuation = _childProcessContinuations. withLock { childProcessContinuations in
69
+ childProcessContinuations. removeValue ( forKey: pid)
70
+ }
71
+
72
+ // If we had a continuation for this PID, allow the process to be reaped
73
+ // and pass the resulting exit condition back to the calling task. If
74
+ // there is no continuation, then either it hasn't been stored yet or
75
+ // this child process is not tracked by the waiter thread.
76
+ if let continuation, 0 == waitid ( P_PID, id_t ( pid) , & siginfo, WEXITED) {
77
+ let result = Result {
78
+ try ExitCondition ( siginfo)
79
+ }
80
+ continuation. resume ( with: result)
81
+ }
82
+ }
83
+ } else if case let errorCode = swt_errno ( ) , errorCode == ECHILD {
84
+ // We got ECHILD. If there are no continuations added right now, we should
85
+ // suspend this thread on the no-children condition until it's awoken by a
86
+ // newly-scheduled waiter process. (If this condition is spuriously
87
+ // woken, we'll just loop again, which is fine.) Note that we read errno
88
+ // outside the lock in case acquiring the lock perturbs it.
89
+ _childProcessContinuations. withUnsafeUnderlyingLock { lock, childProcessContinuations in
90
+ if childProcessContinuations. isEmpty {
91
+ _ = pthread_cond_wait ( _waitThreadNoChildrenCondition. rawValue, lock)
92
+ }
39
93
}
40
- } else if swt_errno ( ) != EINTR {
41
- throw CError ( rawValue: swt_errno ( ) )
42
94
}
43
95
}
96
+
97
+ // Create the thread. We immediately detach it upon success to allow the
98
+ // system to reclaim its resources when done.
99
+ #if SWT_TARGET_OS_APPLE
100
+ var thread : pthread_t ?
101
+ #else
102
+ var thread = pthread_t ( )
103
+ #endif
104
+ _ = pthread_create (
105
+ & thread,
106
+ nil ,
107
+ { _ in
108
+ // Set the thread name to help with diagnostics.
109
+ let threadName = " swift-testing exit test monitor "
110
+ #if SWT_TARGET_OS_APPLE
111
+ _ = pthread_setname_np ( threadName)
112
+ #else
113
+ _ = pthread_setname_np ( pthread_self ( ) , threadName)
114
+ #endif
115
+
116
+ // Run an infinite loop that waits for child processes to terminate and
117
+ // captures their exit statuses.
118
+ while true {
119
+ waitForAnyChild ( )
120
+ }
121
+ } ,
122
+ nil
123
+ )
124
+ } ( )
125
+
126
+ /// Create a waiter thread that is responsible for waiting for child processes
127
+ /// to exit.
128
+ private func _createWaitThread( ) {
129
+ _createWaitThreadImpl
44
130
}
131
+ #endif
45
132
46
133
/// Wait for a given PID to exit and report its status.
47
134
///
@@ -53,7 +140,7 @@ private func _blockAndWait(for pid: pid_t) throws -> ExitCondition {
53
140
/// - Throws: Any error encountered calling `waitpid()` except for `EINTR`,
54
141
/// which is ignored.
55
142
func wait( for pid: pid_t ) async throws -> ExitCondition {
56
- #if SWT_TARGET_OS_APPLE
143
+ #if SWT_TARGET_OS_APPLE && !SWT_NO_LIBDISPATCH
57
144
let source = DispatchSource . makeProcessSource ( identifier: pid, eventMask: . exit)
58
145
defer {
59
146
source. cancel ( )
@@ -65,37 +152,32 @@ func wait(for pid: pid_t) async throws -> ExitCondition {
65
152
source. resume ( )
66
153
}
67
154
withExtendedLifetime ( source) { }
68
- return try _blockAndWait ( for: pid)
155
+
156
+ // Get the exit status of the process or throw an error (other than EINTR.)
157
+ while true {
158
+ var siginfo = siginfo_t ( )
159
+ if 0 == waitid ( P_PID, id_t ( pid) , & siginfo, WEXITED) {
160
+ return try ExitCondition ( siginfo)
161
+ } else if case let errorCode = swt_errno ( ) , errorCode != EINTR {
162
+ throw CError ( rawValue: errorCode)
163
+ }
164
+ }
69
165
#else
70
- // On Linux, spin up a background thread and waitpid() there.
166
+ // Ensure the waiter thread is running.
167
+ _createWaitThread ( )
168
+
71
169
return try await withCheckedThrowingContinuation { continuation in
72
- // Create a structure to hold the state needed by the thread, and box it
73
- // as a refcounted pointer that we can pass to libpthread.
74
- struct Context {
75
- var pid : pid_t
76
- var continuation : CheckedContinuation < ExitCondition , any Error >
77
- }
78
- let context = Unmanaged . passRetained (
79
- Context ( pid: pid, continuation: continuation) as AnyObject
80
- ) . toOpaque ( )
81
-
82
- // The body of the thread: unwrap and take ownership of the context we
83
- // created above, then call waitpid() and report the result/error.
84
- let body : @convention ( c) ( UnsafeMutableRawPointer ? ) -> UnsafeMutableRawPointer ? = { contextp in
85
- let context = Unmanaged < AnyObject > . fromOpaque ( contextp!) . takeRetainedValue ( ) as! Context
86
- let result = Result { try _blockAndWait ( for: context. pid) }
87
- context. continuation. resume ( with: result)
88
- return nil
89
- }
170
+ _childProcessContinuations. withLock { childProcessContinuations in
171
+ // We don't need to worry about a race condition here because waitid()
172
+ // does not clear the wait/zombie state of the child process. If it sees
173
+ // the child process has terminated and manages to acquire the lock before
174
+ // we add this continuation to the dictionary, then it will simply loop
175
+ // and report the status again.
176
+ let oldContinuation = childProcessContinuations. updateValue ( continuation, forKey: pid)
177
+ assert ( oldContinuation == nil , " Unexpected continuation found for PID \( pid) . Please file a bug report at https://github.com/apple/swift-testing/issues/new " )
90
178
91
- // Create the thread. We immediately detach it upon success to allow the
92
- // system to reclaim its resources when done.
93
- var thread = pthread_t ( )
94
- switch pthread_create ( & thread, nil , body, context) {
95
- case 0 :
96
- _ = pthread_detach ( thread)
97
- case let errorCode:
98
- continuation. resume ( throwing: CError ( rawValue: errorCode) )
179
+ // Wake up the waiter thread if it is waiting for more child processes.
180
+ _ = pthread_cond_signal ( _waitThreadNoChildrenCondition. rawValue)
99
181
}
100
182
}
101
183
#endif
@@ -110,13 +192,34 @@ func wait(for pid: pid_t) async throws -> ExitCondition {
110
192
///
111
193
/// - Throws: Any error encountered calling `WaitForSingleObject()` or
112
194
/// `GetExitCodeProcess()`.
113
- ///
114
- /// This function blocks the calling thread on `WaitForSingleObject()`. External
115
- /// callers should use ``wait(for:)`` instead to avoid deadlocks.
116
- private func _blockAndWait( for processHandle: HANDLE ) throws -> ExitCondition {
117
- if WAIT_FAILED == WaitForSingleObject ( processHandle, INFINITE) {
118
- throw Win32Error ( rawValue: GetLastError ( ) )
195
+ func wait( for processHandle: HANDLE ) async throws -> ExitCondition {
196
+ // Once the continuation resumes, it will need to unregister the wait, so
197
+ // yield the wait handle back to the calling scope.
198
+ var waitHandle : HANDLE ?
199
+ defer {
200
+ if let waitHandle {
201
+ _ = UnregisterWait ( waitHandle)
202
+ }
203
+ }
204
+
205
+ try await withCheckedThrowingContinuation { ( continuation: CheckedContinuation < Void , any Error > ) in
206
+ // Set up a callback that immediately resumes the continuation and does no
207
+ // other work.
208
+ let context = Unmanaged . passRetained ( continuation as AnyObject ) . toOpaque ( )
209
+ let callback : WAITORTIMERCALLBACK = { context, _ in
210
+ let continuation = Unmanaged < AnyObject > . fromOpaque ( context!) . takeRetainedValue ( ) as! CheckedContinuation < Void , any Error >
211
+ continuation. resume ( )
212
+ }
213
+
214
+ // We only want the callback to fire once (and not be rescheduled.) Waiting
215
+ // may take an arbitrarily long time, so let the thread pool know that too.
216
+ let flags = ULONG ( WT_EXECUTEONLYONCE | WT_EXECUTELONGFUNCTION)
217
+ guard RegisterWaitForSingleObject ( & waitHandle, processHandle, callback, context, INFINITE, flags) else {
218
+ continuation. resume ( throwing: Win32Error ( rawValue: GetLastError ( ) ) )
219
+ return
220
+ }
119
221
}
222
+
120
223
var status : DWORD = 0
121
224
guard GetExitCodeProcess ( processHandle, & status) else {
122
225
// The child process terminated but we couldn't get its status back.
@@ -127,35 +230,5 @@ private func _blockAndWait(for processHandle: HANDLE) throws -> ExitCondition {
127
230
// FIXME: handle SEH/VEH uncaught exceptions.
128
231
return . exitCode( CInt ( bitPattern: status) )
129
232
}
130
-
131
- /// Wait for a given process handle to exit and report its status.
132
- ///
133
- /// - Parameters:
134
- /// - processHandle: The handle to wait for.
135
- ///
136
- /// - Returns: The exit condition of `processHandle`.
137
- ///
138
- /// - Throws: Any error encountered calling `WaitForSingleObject()` or
139
- /// `GetExitCodeProcess()`.
140
- func wait( for processHandle: HANDLE ) async throws -> ExitCondition {
141
- try await withCheckedThrowingContinuation { continuation in
142
- // Create a structure to hold the state needed by the thread, and box it
143
- // as a refcounted pointer that we can pass to libpthread.
144
- struct Context {
145
- var processHandle : HANDLE
146
- var continuation : CheckedContinuation < ExitCondition , any Error >
147
- }
148
- let context = Unmanaged . passRetained (
149
- Context ( processHandle: processHandle, continuation: continuation) as AnyObject
150
- ) . toOpaque ( )
151
-
152
- let body : _beginthread_proc_type = { contextp in
153
- let context = Unmanaged < AnyObject > . fromOpaque ( contextp!) . takeRetainedValue ( ) as! Context
154
- let result = Result { try _blockAndWait ( for: context. processHandle) }
155
- context. continuation. resume ( with: result)
156
- }
157
- _ = _beginthread ( body, 0 , context)
158
- }
159
- }
160
233
#endif
161
234
#endif
0 commit comments