From dec1c3fa848e9e2dcb8876d6057e1fc63f58a622 Mon Sep 17 00:00:00 2001 From: Andreas Bauer Date: Tue, 23 Jul 2024 14:01:26 +0200 Subject: [PATCH] Adjust implementation for async connect method --- Package.swift | 2 +- Sources/SpeziDevices/PairedDevices.swift | 60 ++++++++++++++----- .../PairedDevicesTests.swift | 4 +- .../UITests/TestApp/BluetoothViewsTest.swift | 2 +- Tests/UITests/TestApp/DevicesTestView.swift | 6 +- 5 files changed, 52 insertions(+), 22 deletions(-) diff --git a/Package.swift b/Package.swift index 20fe7e2..07cc74f 100644 --- a/Package.swift +++ b/Package.swift @@ -36,7 +36,7 @@ let package = Package( .package(url: "https://github.com/StanfordSpezi/SpeziFoundation", from: "1.1.1"), .package(url: "https://github.com/StanfordSpezi/Spezi.git", from: "1.4.0"), .package(url: "https://github.com/StanfordSpezi/SpeziViews.git", from: "1.5.0"), - .package(url: "https://github.com/StanfordSpezi/SpeziBluetooth", exact: "3.0.0-beta.1"), + .package(url: "https://github.com/StanfordSpezi/SpeziBluetooth", exact: "3.0.0-beta.2"), .package(url: "https://github.com/StanfordSpezi/SpeziNetworking", from: "2.1.1"), .package(url: "https://github.com/StanfordBDHG/XCTestExtensions.git", .upToNextMinor(from: "0.4.12")) ] + swiftLintPackage(), diff --git a/Sources/SpeziDevices/PairedDevices.swift b/Sources/SpeziDevices/PairedDevices.swift index 9ad8a90..26e5efb 100644 --- a/Sources/SpeziDevices/PairedDevices.swift +++ b/Sources/SpeziDevices/PairedDevices.swift @@ -336,7 +336,15 @@ public final class PairedDevices: @unchecked Sendable { pendingConnectionAttempts[device.id] = Task { await previousTask?.value // make sure its ordered - await device.connect() + do { + try await device.connect() + } catch { + guard !Task.isCancelled else { + return + } + logger.warning("Failed connection attempt for device \(device.label). Retrying ...") + connectionAttempt(for: device) + } } } @@ -394,27 +402,49 @@ extension PairedDevices { throw DevicePairingError.invalidState } - await device.connect() - let id = device.id - let timeoutHandler = { @Sendable @MainActor in - _ = self.ongoingPairings.removeValue(forKey: id)?.signalTimeout() - } - async let _ = withTimeout(of: timeout, perform: timeoutHandler) + try await withThrowingDiscardingTaskGroup { group in + // timeout task + group.addTask { + await withTimeout(of: timeout) { @MainActor in + _ = self.ongoingPairings.removeValue(forKey: id)?.signalTimeout() + } + } + + // connect task + group.addTask { @MainActor in + do { + try await device.connect() + } catch { + if error is CancellationError { + self.ongoingPairings.removeValue(forKey: id)?.signalCancellation() + } - try await withTaskCancellationHandler { - try await withCheckedThrowingContinuation { continuation in - ongoingPairings[id] = PairingContinuation(continuation) + throw error + } } - } onCancel: { - Task { @MainActor [weak device] in - ongoingPairings.removeValue(forKey: id)?.signalCancellation() - await device?.disconnect() + + // pairing task + group.addTask { @MainActor in + try await withTaskCancellationHandler { + try await withCheckedThrowingContinuation { continuation in + self.ongoingPairings[id] = PairingContinuation(continuation) + } + } onCancel: { + Task { @MainActor [weak device] in + self.ongoingPairings.removeValue(forKey: id)?.signalCancellation() + await device?.disconnect() + } + } } } - // if cancelled the continuation throws an CancellationError + // the task group above should exit with a CancellationError anyways, but safe to double check here + guard !Task.isCancelled else { + throw CancellationError() + } + await registerPairedDevice(device) } diff --git a/Tests/SpeziDevicesTests/PairedDevicesTests.swift b/Tests/SpeziDevicesTests/PairedDevicesTests.swift index 57cc2e5..3e1840c 100644 --- a/Tests/SpeziDevicesTests/PairedDevicesTests.swift +++ b/Tests/SpeziDevicesTests/PairedDevicesTests.swift @@ -83,7 +83,7 @@ final class PairedDevicesTests: XCTestCase { }() - await device.connect() + try await device.connect() try await Task.sleep(for: .seconds(1.1)) XCTAssertEqual(device.state, .connected) @@ -113,7 +113,7 @@ final class PairedDevicesTests: XCTestCase { } device.$nearby.inject(true) - await device.connect() + try await device.connect() try await XCTAssertThrowsErrorAsync(await devices.pair(with: device)) { error in XCTAssertEqual(try XCTUnwrap(error as? DevicePairingError), .invalidState) } diff --git a/Tests/UITests/TestApp/BluetoothViewsTest.swift b/Tests/UITests/TestApp/BluetoothViewsTest.swift index 284b26f..b570fb0 100644 --- a/Tests/UITests/TestApp/BluetoothViewsTest.swift +++ b/Tests/UITests/TestApp/BluetoothViewsTest.swift @@ -41,7 +41,7 @@ struct BluetoothViewsTest: View { Task { switch device.state { case .disconnected, .disconnecting: - await device.connect() + try await device.connect() case .connecting, .connected: await device.disconnect() } diff --git a/Tests/UITests/TestApp/DevicesTestView.swift b/Tests/UITests/TestApp/DevicesTestView.swift index f30828c..29a1a68 100644 --- a/Tests/UITests/TestApp/DevicesTestView.swift +++ b/Tests/UITests/TestApp/DevicesTestView.swift @@ -48,9 +48,9 @@ struct DevicesTestView: View { device.$advertisementData.inject(AdvertisementData()) // trigger onChange advertisement } AsyncButton { - await device.connect() - await weightScale.connect() - await bloodPressureCuff.connect() + try await device.connect() + try await weightScale.connect() + try await bloodPressureCuff.connect() } label: { Label("Connect", systemImage: "cable.connector") }