Skip to content

Commit b66032f

Browse files
committed
Fix: Replace Process.waitUntilExit() with DispatchGroup to prevent hangs
The waitUntilExit() call was causing hangs when invoked from Swift concurrency's cooperative thread pool. This is because waitUntilExit() runs the current thread's runloop in NSDefaultRunLoopMode while waiting, which can cause unexpected behavior when called from arbitrary threads or thread pool contexts. The issue occurs because: 1. waitUntilExit() runs the runloop in default mode, allowing timers and callbacks to fire unexpectedly (see Mike Ash's article on dangerous Cocoa calls) 2. Swift concurrency's cooperative thread pool doesn't guarantee which thread executes a given task, making runloop-dependent APIs unreliable 3. The runloop may not be properly configured on thread pool threads The fix uses a DispatchGroup that is entered before starting the process and left in the process's terminationHandler. The close() method then calls wait() on the group, which is a simple blocking call without runloop involvement. This approach is safe because: - It doesn't depend on runloop infrastructure - It works correctly regardless of which thread it's called from - It provides clean synchronization with process termination References: - https://stackoverflow.com/questions/34996937/how-to-safely-use-nstask-waituntilexit-off-the-main-thread - https://mikeash.com/pyblog/friday-qa-2009-11-13-dangerous-cocoa-calls.html
1 parent 41a3cc0 commit b66032f

File tree

1 file changed

+7
-1
lines changed

1 file changed

+7
-1
lines changed

Sources/PklSwift/MessageTransport.swift

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,8 @@ public class ServerMessageTransport: BaseMessageTransport, @unchecked Sendable {
127127
var process: Process?
128128
let pklCommand: [String]?
129129

130+
private let processTerminationGroup = DispatchGroup()
131+
130132
override var running: Bool { self.process?.isRunning == true }
131133

132134
override convenience init() {
@@ -153,6 +155,10 @@ public class ServerMessageTransport: BaseMessageTransport, @unchecked Sendable {
153155
self.process!.standardInput = self.writer
154156
let debugArguments = arguments
155157
debug("Spawning command \(pklCommand[0]) with arguments \(debugArguments)")
158+
self.processTerminationGroup.enter()
159+
self.process?.terminationHandler = { [processTerminationGroup] _ in
160+
processTerminationGroup.leave()
161+
}
156162
try self.process!.run()
157163
}
158164

@@ -186,7 +192,7 @@ public class ServerMessageTransport: BaseMessageTransport, @unchecked Sendable {
186192
#else
187193
self.process?.terminate()
188194
#endif
189-
self.process!.waitUntilExit()
195+
self.processTerminationGroup.wait()
190196
self.process = nil
191197
}
192198

0 commit comments

Comments
 (0)