From e502ad13f99c61ec83c152e37d7e61c8d0f4b8db Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Fri, 12 Dec 2025 23:06:17 +0000 Subject: [PATCH] fix(node): prevent iOS VoiceWake crash --- apps/ios/Sources/Bridge/BridgeClient.swift | 4 ++-- .../Sources/Bridge/BridgeDiscoveryModel.swift | 3 ++- apps/ios/Sources/Bridge/BridgeSession.swift | 4 ++-- .../ios/Sources/Screen/ScreenController.swift | 2 +- apps/ios/Sources/Voice/VoiceWakeManager.swift | 21 +++++++------------ .../Bridge/BridgeConnectionHandler.swift | 2 +- .../Sources/Clawdis/Bridge/BridgeServer.swift | 3 ++- 7 files changed, 18 insertions(+), 21 deletions(-) diff --git a/apps/ios/Sources/Bridge/BridgeClient.swift b/apps/ios/Sources/Bridge/BridgeClient.swift index c23aa0d42..9e003bdb0 100644 --- a/apps/ios/Sources/Bridge/BridgeClient.swift +++ b/apps/ios/Sources/Bridge/BridgeClient.swift @@ -82,7 +82,7 @@ actor BridgeClient { var line = Data() line.append(data) line.append(0x0A) - try await withCheckedThrowingContinuation { (cont: CheckedContinuation) in + try await withCheckedThrowingContinuation(isolation: nil) { (cont: CheckedContinuation) in connection.send(content: line, completion: .contentProcessed { err in if let err { cont.resume(throwing: err) } else { cont.resume(returning: ()) } }) @@ -104,7 +104,7 @@ actor BridgeClient { } private func receiveChunk(over connection: NWConnection) async throws -> Data { - try await withCheckedThrowingContinuation { (cont: CheckedContinuation) in + try await withCheckedThrowingContinuation(isolation: nil) { (cont: CheckedContinuation) in connection.receive(minimumIncompleteLength: 1, maximumLength: 64 * 1024) { data, _, isComplete, error in if let error { cont.resume(throwing: error) diff --git a/apps/ios/Sources/Bridge/BridgeDiscoveryModel.swift b/apps/ios/Sources/Bridge/BridgeDiscoveryModel.swift index a81068876..73a2f1646 100644 --- a/apps/ios/Sources/Bridge/BridgeDiscoveryModel.swift +++ b/apps/ios/Sources/Bridge/BridgeDiscoveryModel.swift @@ -19,8 +19,9 @@ final class BridgeDiscoveryModel: ObservableObject { func start() { if self.browser != nil { return } let params = NWParameters.tcp + params.includePeerToPeer = true let browser = NWBrowser( - for: .bonjour(type: ClawdisBonjour.bridgeServiceType, domain: nil), + for: .bonjour(type: ClawdisBonjour.bridgeServiceType, domain: ClawdisBonjour.bridgeServiceDomain), using: params) browser.stateUpdateHandler = { [weak self] state in diff --git a/apps/ios/Sources/Bridge/BridgeSession.swift b/apps/ios/Sources/Bridge/BridgeSession.swift index a5267a197..38ac1581d 100644 --- a/apps/ios/Sources/Bridge/BridgeSession.swift +++ b/apps/ios/Sources/Bridge/BridgeSession.swift @@ -111,7 +111,7 @@ actor BridgeSession { var line = Data() line.append(data) line.append(0x0A) - try await withCheckedThrowingContinuation { (cont: CheckedContinuation) in + try await withCheckedThrowingContinuation(isolation: nil) { (cont: CheckedContinuation) in connection.send(content: line, completion: .contentProcessed { err in if let err { cont.resume(throwing: err) } else { cont.resume(returning: ()) } }) @@ -134,7 +134,7 @@ actor BridgeSession { private func receiveChunk() async throws -> Data { guard let connection = self.connection else { return Data() } - return try await withCheckedThrowingContinuation { (cont: CheckedContinuation) in + return try await withCheckedThrowingContinuation(isolation: nil) { (cont: CheckedContinuation) in connection.receive(minimumIncompleteLength: 1, maximumLength: 64 * 1024) { data, _, isComplete, error in if let error { cont.resume(throwing: error) diff --git a/apps/ios/Sources/Screen/ScreenController.swift b/apps/ios/Sources/Screen/ScreenController.swift index 33143cd87..bdcd209be 100644 --- a/apps/ios/Sources/Screen/ScreenController.swift +++ b/apps/ios/Sources/Screen/ScreenController.swift @@ -57,7 +57,7 @@ final class ScreenController: ObservableObject { if let maxWidth { config.snapshotWidth = NSNumber(value: Double(maxWidth)) } - let image: UIImage = try await withCheckedThrowingContinuation { (cont: CheckedContinuation) in + let image: UIImage = try await withCheckedThrowingContinuation { cont in self.webView.takeSnapshot(with: config) { image, error in if let error { cont.resume(throwing: error) diff --git a/apps/ios/Sources/Voice/VoiceWakeManager.swift b/apps/ios/Sources/Voice/VoiceWakeManager.swift index f9cc8bc3b..a8833b4a4 100644 --- a/apps/ios/Sources/Voice/VoiceWakeManager.swift +++ b/apps/ios/Sources/Voice/VoiceWakeManager.swift @@ -96,9 +96,8 @@ final class VoiceWakeManager: NSObject, ObservableObject { inputNode.removeTap(onBus: 0) let recordingFormat = inputNode.outputFormat(forBus: 0) - inputNode.installTap(onBus: 0, bufferSize: 1024, format: recordingFormat) { [weak self] buffer, _ in - guard let self else { return } - self.recognitionRequest?.append(buffer) + inputNode.installTap(onBus: 0, bufferSize: 1024, format: recordingFormat) { [request] buffer, _ in + request.append(buffer) } self.audioEngine.prepare() @@ -169,22 +168,18 @@ final class VoiceWakeManager: NSObject, ObservableObject { try session.setActive(true, options: []) } - private static func requestMicrophonePermission() async -> Bool { - await withCheckedContinuation { cont in + private nonisolated static func requestMicrophonePermission() async -> Bool { + await withCheckedContinuation(isolation: nil) { cont in AVAudioApplication.requestRecordPermission { ok in - Task { @MainActor in - cont.resume(returning: ok) - } + cont.resume(returning: ok) } } } - private static func requestSpeechPermission() async -> Bool { - await withCheckedContinuation { cont in + private nonisolated static func requestSpeechPermission() async -> Bool { + await withCheckedContinuation(isolation: nil) { cont in SFSpeechRecognizer.requestAuthorization { status in - Task { @MainActor in - cont.resume(returning: status == .authorized) - } + cont.resume(returning: status == .authorized) } } } diff --git a/apps/macos/Sources/Clawdis/Bridge/BridgeConnectionHandler.swift b/apps/macos/Sources/Clawdis/Bridge/BridgeConnectionHandler.swift index 97218fb27..bbe2b8700 100644 --- a/apps/macos/Sources/Clawdis/Bridge/BridgeConnectionHandler.swift +++ b/apps/macos/Sources/Clawdis/Bridge/BridgeConnectionHandler.swift @@ -210,7 +210,7 @@ actor BridgeConnectionHandler { var line = Data() line.append(data) line.append(0x0A) // \n - let _: Void = try await withCheckedThrowingContinuation { (cont: CheckedContinuation) in + let _: Void = try await withCheckedThrowingContinuation { cont in self.connection.send(content: line, completion: .contentProcessed { err in if let err { cont.resume(throwing: err) diff --git a/apps/macos/Sources/Clawdis/Bridge/BridgeServer.swift b/apps/macos/Sources/Clawdis/Bridge/BridgeServer.swift index 5e0f7f2f0..adb609b14 100644 --- a/apps/macos/Sources/Clawdis/Bridge/BridgeServer.swift +++ b/apps/macos/Sources/Clawdis/Bridge/BridgeServer.swift @@ -24,13 +24,14 @@ actor BridgeServer { self.store = store let params = NWParameters.tcp + params.includePeerToPeer = true let listener = try NWListener(using: params, on: .any) let name = Host.current().localizedName ?? ProcessInfo.processInfo.hostName listener.service = NWListener.Service( name: "\(name) (Clawdis)", type: ClawdisBonjour.bridgeServiceType, - domain: nil, + domain: ClawdisBonjour.bridgeServiceDomain, txtRecord: nil) listener.newConnectionHandler = { [weak self] connection in