@@ -60,6 +60,7 @@ actor SSHTunnelManager {
6060 private let portRangeStart = 60_000
6161 private let portRangeEnd = 65_000
6262 private var healthCheckTask : Task < Void , Never > ?
63+ private static let processRegistry = OSAllocatedUnfairLock ( initialState: [ UUID: Process] ( ) )
6364
6465 private init ( ) {
6566 Task { [ weak self] in
@@ -201,6 +202,7 @@ actor SSHTunnelManager {
201202 createdAt: Date ( )
202203 )
203204 tunnels [ connectionId] = tunnel
205+ Self . processRegistry. withLock { $0 [ connectionId] = launch. process }
204206
205207 return localPort
206208 }
@@ -218,16 +220,43 @@ actor SSHTunnelManager {
218220 }
219221
220222 tunnels. removeValue ( forKey: connectionId)
223+ Self . processRegistry. withLock { $0. removeValue ( forKey: connectionId) }
221224 }
222225
223226 /// Close all SSH tunnels
224227 func closeAllTunnels( ) async {
225- for (_, tunnel) in tunnels {
226- if tunnel. process. isRunning {
227- tunnel. process. terminate ( )
228+ let currentTunnels = tunnels
229+ tunnels. removeAll ( )
230+ Self . processRegistry. withLock { $0. removeAll ( ) }
231+
232+ await withTaskGroup ( of: Void . self) { group in
233+ for (_, tunnel) in currentTunnels where tunnel. process. isRunning {
234+ group. addTask {
235+ tunnel. process. terminate ( )
236+ await self . waitForProcessExit ( tunnel. process, timeout: . seconds( 3 ) )
237+ }
238+ }
239+ }
240+ }
241+
242+ /// Synchronously terminate all SSH tunnel processes.
243+ /// Called from `applicationWillTerminate` where async is not available.
244+ nonisolated func terminateAllProcessesSync( ) {
245+ let processes = Self . processRegistry. withLock { dict -> [ Process ] in
246+ let procs = Array ( dict. values)
247+ dict. removeAll ( )
248+ return procs
249+ }
250+ for process in processes where process. isRunning {
251+ process. terminate ( )
252+ let deadline = Date ( ) . addingTimeInterval ( 1.0 )
253+ while process. isRunning, Date ( ) < deadline {
254+ Thread . sleep ( forTimeInterval: 0.05 )
255+ }
256+ if process. isRunning {
257+ kill ( process. processIdentifier, SIGKILL)
228258 }
229259 }
230- tunnels. removeAll ( )
231260 }
232261
233262 /// Check if a tunnel exists for a connection
@@ -389,12 +418,26 @@ actor SSHTunnelManager {
389418 return scriptPath
390419 }
391420
392- /// Wait for a Process to exit without blocking the current thread
393- private func waitForProcessExit( _ process: Process ) async {
394- await withCheckedContinuation { continuation in
395- process. terminationHandler = { _ in
396- continuation. resume ( )
421+ private func waitForProcessExit( _ process: Process , timeout: Duration = . seconds( 5 ) ) async {
422+ guard process. isRunning else { return }
423+
424+ await withTaskGroup ( of: Void . self) { group in
425+ group. addTask {
426+ await withCheckedContinuation { ( continuation: CheckedContinuation < Void , Never > ) in
427+ process. terminationHandler = { _ in
428+ continuation. resume ( )
429+ }
430+ }
431+ }
432+ group. addTask {
433+ try ? await Task . sleep ( for: timeout)
397434 }
435+ _ = await group. next ( )
436+ group. cancelAll ( )
437+ }
438+
439+ if process. isRunning {
440+ kill ( process. processIdentifier, SIGKILL)
398441 }
399442 }
400443
0 commit comments