Skip to content

Commit 61365c6

Browse files
committed
fix(ios): fix thread-safety race in skipDescriptorDiscovery on reconnect (#809)
On reconnect, CoreBluetooth uses cached GATT data and fires didDiscoverCharacteristicsFor near-instantly on the main queue, before the bridge thread's write to skipDescriptorDiscovery is visible — causing the stale false value to be read and the connection to time out. Fix by dispatching setOnConnected synchronously to the main queue, matching the queue CoreBluetooth delegates run on. Apply the same fix to startScanning's scan filter properties for the same reason. Add comments on stateReceiver where the same issue exists but is too slow to manifest.
1 parent cf80aa7 commit 61365c6

File tree

2 files changed

+23
-11
lines changed

2 files changed

+23
-11
lines changed

ios/Sources/BluetoothLe/Device.swift

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -52,10 +52,14 @@ class Device: NSObject, CBPeripheralDelegate {
5252
_ skipDescriptorDiscovery: Bool,
5353
_ callback: @escaping Callback
5454
) {
55-
let key = "connect"
56-
self.skipDescriptorDiscovery = skipDescriptorDiscovery
57-
self.callbackMap[key] = callback
58-
self.setTimeout(key, "Connection timeout", connectionTimeout)
55+
// Delegates run on the main queue; this is called from the bridge thread.
56+
// Sync to main so writes are visible before any delegate fires.
57+
DispatchQueue.main.sync {
58+
let key = "connect"
59+
self.skipDescriptorDiscovery = skipDescriptorDiscovery
60+
self.callbackMap[key] = callback
61+
self.setTimeout(key, "Connection timeout", connectionTimeout)
62+
}
5963
}
6064

6165
func peripheral(

ios/Sources/BluetoothLe/DeviceManager.swift

Lines changed: 15 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,9 @@ class DeviceManager: NSObject, CBCentralManagerDelegate {
7373
return self.centralManager.state == CBManagerState.poweredOn
7474
}
7575

76+
// stateReceiver is written on the bridge thread and read on the main queue
77+
// (via centralManagerDidUpdateState). Same threading issue as startScanning
78+
// below; not fixed as BLE state transitions are too slow for the race to manifest.
7679
func registerStateReceiver( _ stateReceiver: @escaping StateReceiver) {
7780
self.stateReceiver = stateReceiver
7881
}
@@ -99,16 +102,21 @@ class DeviceManager: NSObject, CBCentralManagerDelegate {
99102
_ scanResultCallback: @escaping ScanResultCallback
100103
) {
101104
self.callbackMap["startScanning"] = callback
102-
self.scanResultCallback = scanResultCallback
103105

104106
if self.centralManager.isScanning == false {
105107
self.discoveredDevices.removeAll()
106-
self.deviceListMode = deviceListMode
107-
self.allowDuplicates = allowDuplicates
108-
self.deviceNameFilter = name
109-
self.deviceNamePrefixFilter = namePrefix
110-
self.manufacturerDataFilters = manufacturerDataFilters
111-
self.serviceDataFilters = serviceDataFilters
108+
// These are read from centralManager(_:didDiscover:) on the main queue.
109+
// Delegates run on the main queue; this is called from the bridge thread.
110+
// Sync to main so writes are visible before any delegate fires.
111+
DispatchQueue.main.sync {
112+
self.scanResultCallback = scanResultCallback
113+
self.deviceListMode = deviceListMode
114+
self.allowDuplicates = allowDuplicates
115+
self.deviceNameFilter = name
116+
self.deviceNamePrefixFilter = namePrefix
117+
self.manufacturerDataFilters = manufacturerDataFilters
118+
self.serviceDataFilters = serviceDataFilters
119+
}
112120

113121
if deviceListMode != .none {
114122
self.showDeviceList()

0 commit comments

Comments
 (0)