From 4daf75a46922c474ec74e91d94dee5c1ec83e7f0 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Sat, 27 Dec 2025 00:02:41 +0100 Subject: [PATCH] fix(macos): enforce node bridge timeouts --- .../NodeMode/MacNodeBridgePairingClient.swift | 20 ++++++++++--------- .../NodeMode/MacNodeBridgeSession.swift | 20 ++++++++++--------- .../NodeMode/MacNodeModeCoordinator.swift | 20 ++++++++++--------- 3 files changed, 33 insertions(+), 27 deletions(-) diff --git a/apps/macos/Sources/Clawdis/NodeMode/MacNodeBridgePairingClient.swift b/apps/macos/Sources/Clawdis/NodeMode/MacNodeBridgePairingClient.swift index f41d64b65..fb53961b6 100644 --- a/apps/macos/Sources/Clawdis/NodeMode/MacNodeBridgePairingClient.swift +++ b/apps/macos/Sources/Clawdis/NodeMode/MacNodeBridgePairingClient.swift @@ -177,18 +177,20 @@ actor MacNodeBridgePairingClient { purpose: String, operation: @escaping @Sendable () async throws -> T) async throws -> T { - let task = Task { try await operation() } - let timeout = Task { - try await Task.sleep(nanoseconds: UInt64(seconds * 1_000_000_000)) + try await withThrowingTaskGroup(of: T.self) { group in + group.addTask { try await operation() } + group.addTask { + try await Task.sleep(nanoseconds: UInt64(seconds * 1_000_000_000)) + throw NSError(domain: "Bridge", code: 0, userInfo: [ + NSLocalizedDescriptionKey: "\(purpose) timed out", + ]) + } + let result = try await group.next() + group.cancelAll() + if let result { return result } throw NSError(domain: "Bridge", code: 0, userInfo: [ NSLocalizedDescriptionKey: "\(purpose) timed out", ]) } - defer { timeout.cancel() } - return try await withTaskCancellationHandler(operation: { - try await task.value - }, onCancel: { - timeout.cancel() - }) } } diff --git a/apps/macos/Sources/Clawdis/NodeMode/MacNodeBridgeSession.swift b/apps/macos/Sources/Clawdis/NodeMode/MacNodeBridgeSession.swift index 1e119dc6b..023598252 100644 --- a/apps/macos/Sources/Clawdis/NodeMode/MacNodeBridgeSession.swift +++ b/apps/macos/Sources/Clawdis/NodeMode/MacNodeBridgeSession.swift @@ -315,16 +315,18 @@ actor MacNodeBridgeSession { seconds: Double, operation: @escaping @Sendable () async throws -> T) async throws -> T { - let task = Task { try await operation() } - let timeout = Task { - try await Task.sleep(nanoseconds: UInt64(seconds * 1_000_000_000)) + try await withThrowingTaskGroup(of: T.self) { group in + group.addTask { + try await operation() + } + group.addTask { + try await Task.sleep(nanoseconds: UInt64(seconds * 1_000_000_000)) + throw TimeoutError(message: "operation timed out") + } + let result = try await group.next() + group.cancelAll() + if let result { return result } throw TimeoutError(message: "operation timed out") } - defer { timeout.cancel() } - return try await withTaskCancellationHandler(operation: { - try await task.value - }, onCancel: { - timeout.cancel() - }) } } diff --git a/apps/macos/Sources/Clawdis/NodeMode/MacNodeModeCoordinator.swift b/apps/macos/Sources/Clawdis/NodeMode/MacNodeModeCoordinator.swift index 2610d42c4..b946a6d84 100644 --- a/apps/macos/Sources/Clawdis/NodeMode/MacNodeModeCoordinator.swift +++ b/apps/macos/Sources/Clawdis/NodeMode/MacNodeModeCoordinator.swift @@ -253,19 +253,21 @@ final class MacNodeModeCoordinator { seconds: Double, operation: @escaping @Sendable () async throws -> T) async throws -> T { - let task = Task { try await operation() } - let timeout = Task { - try await Task.sleep(nanoseconds: UInt64(seconds * 1_000_000_000)) + try await withThrowingTaskGroup(of: T.self) { group in + group.addTask { try await operation() } + group.addTask { + try await Task.sleep(nanoseconds: UInt64(seconds * 1_000_000_000)) + throw NSError(domain: "Bridge", code: 22, userInfo: [ + NSLocalizedDescriptionKey: "operation timed out", + ]) + } + let result = try await group.next() + group.cancelAll() + if let result { return result } throw NSError(domain: "Bridge", code: 22, userInfo: [ NSLocalizedDescriptionKey: "operation timed out", ]) } - defer { timeout.cancel() } - return try await withTaskCancellationHandler(operation: { - try await task.value - }, onCancel: { - timeout.cancel() - }) } private func resolveBridgeEndpoint(timeoutSeconds: Double) async -> NWEndpoint? {