From a0635d7617f3633a0ca6b8c09b9c3e87d3f660e2 Mon Sep 17 00:00:00 2001 From: Dmitriy Kovalenko Date: Fri, 24 May 2024 13:14:04 +0200 Subject: [PATCH] fix(macos): Break the corebluetooth loop when manager turned off closes #387 In Core Bluetooth when the device is not applicable for background bluetooth manager will create an event for state change. And then change the manager state to power off. Currently, it is not tracked at all which leads to the forever stuck unresolved issues while the connection to peripheral is still held. An additional problem I faced that there is no way to manually kill the event loop of the corebluetooth from outside so the `CoreBluetoothInternal::drop` is never called because it is always living in the stalled thread. In this change, I added an API to access the manager state and exited the event loop when if the manager turned off. --- src/corebluetooth/adapter.rs | 18 +++++++++++++--- src/corebluetooth/framework.rs | 23 +++++++++++++++++++- src/corebluetooth/internal.rs | 38 +++++++++++++++++++++++++++++++--- 3 files changed, 72 insertions(+), 7 deletions(-) diff --git a/src/corebluetooth/adapter.rs b/src/corebluetooth/adapter.rs index c139c43d..fb3dc251 100644 --- a/src/corebluetooth/adapter.rs +++ b/src/corebluetooth/adapter.rs @@ -2,6 +2,7 @@ use super::internal::{run_corebluetooth_thread, CoreBluetoothEvent, CoreBluetoot use super::peripheral::{Peripheral, PeripheralId}; use crate::api::{Central, CentralEvent, ScanFilter}; use crate::common::adapter_manager::AdapterManager; +use crate::corebluetooth::internal::{CoreBluetoothReply, CoreBluetoothReplyFuture}; use crate::{Error, Result}; use async_trait::async_trait; use futures::channel::mpsc::{self, Sender}; @@ -29,7 +30,7 @@ impl Adapter { debug!("Waiting on adapter connect"); if !matches!( receiver.next().await, - Some(CoreBluetoothEvent::AdapterConnected) + Some(CoreBluetoothEvent::DidUpdateState) ) { return Err(Error::Other( "Adapter failed to connect.".to_string().into(), @@ -39,7 +40,7 @@ impl Adapter { let manager = Arc::new(AdapterManager::default()); let manager_clone = manager.clone(); - let adapter_sender_clone = adapter_sender.clone(); + let mut adapter_sender_clone = adapter_sender.clone(); task::spawn(async move { while let Some(msg) = receiver.next().await { match msg { @@ -67,7 +68,18 @@ impl Adapter { CoreBluetoothEvent::DeviceDisconnected { uuid } => { manager_clone.emit(CentralEvent::DeviceDisconnected(uuid.into())); } - _ => {} + CoreBluetoothEvent::DidUpdateState => { + let fut = CoreBluetoothReplyFuture::default(); + let _ = adapter_sender_clone + .send(CoreBluetoothMessage::FetchManagerState { + future: fut.get_state_clone(), + }) + .await; + + if let CoreBluetoothReply::ManagerState(state) = fut.await { + error!("Adapter state changed to {:?}. Aborting manager", state) + } + } } } }); diff --git a/src/corebluetooth/framework.rs b/src/corebluetooth/framework.rs index a9f40c4b..a6c61f20 100644 --- a/src/corebluetooth/framework.rs +++ b/src/corebluetooth/framework.rs @@ -82,6 +82,26 @@ pub mod cb { } } + #[derive(Copy, Clone, Debug, Eq, PartialEq)] + #[repr(i64)] + #[allow(dead_code)] + pub enum CBManagerState { + Unknown = 0, + Resetting = 1, + Unsupported = 2, + Unauthorized = 3, + PoweredOff = 4, + PoweredOn = 5, + } + + unsafe impl Encode for CBManagerState { + const ENCODING: Encoding = i64::ENCODING; + } + + pub fn centeralmanger_state(cbcentralmanager: id) -> CBManagerState { + unsafe { msg_send![cbcentralmanager, state] } + } + pub fn centralmanager_stopscan(cbcentralmanager: id) { unsafe { msg_send![cbcentralmanager, stopScan] } } @@ -130,10 +150,11 @@ pub mod cb { unsafe { msg_send_id![cbperipheral, name] } } + #[allow(dead_code)] #[derive(Copy, Clone, Debug, Eq, PartialEq)] #[repr(i64)] pub enum CBPeripheralState { - Disonnected = 0, + Disconnected = 0, Connecting = 1, Connected = 2, Disconnecting = 3, diff --git a/src/corebluetooth/internal.rs b/src/corebluetooth/internal.rs index 9ce8ffe7..2a1d109a 100644 --- a/src/corebluetooth/internal.rs +++ b/src/corebluetooth/internal.rs @@ -10,7 +10,7 @@ use super::{ central_delegate::{CentralDelegate, CentralDelegateEvent}, - framework::cb::{self, CBManagerAuthorization, CBPeripheralState}, + framework::cb::{self, CBManagerAuthorization, CBManagerState, CBPeripheralState}, future::{BtlePlugFuture, BtlePlugFutureStateShared}, utils::{ core_bluetooth::{cbuuid_to_uuid, uuid_to_cbuuid}, @@ -139,6 +139,7 @@ pub enum CoreBluetoothReply { ReadResult(Vec), Connected(BTreeSet), State(CBPeripheralState), + ManagerState(CBManagerState), Ok, Err(String), } @@ -399,11 +400,14 @@ pub enum CoreBluetoothMessage { data: Vec, future: CoreBluetoothReplyStateShared, }, + FetchManagerState { + future: CoreBluetoothReplyStateShared, + }, } #[derive(Debug)] pub enum CoreBluetoothEvent { - AdapterConnected, + DidUpdateState, DeviceDiscovered { uuid: Uuid, name: Option, @@ -789,6 +793,18 @@ impl CoreBluetoothInternal { } } + fn get_manager_state(&mut self) -> CBManagerState { + cb::centeralmanger_state(&*self.manager) + } + + fn get_manager_state_async(&mut self, fut: CoreBluetoothReplyStateShared) { + let state = cb::centeralmanger_state(&*self.manager); + trace!("Manager state {:?} ", state); + fut.lock() + .unwrap() + .set_reply(CoreBluetoothReply::ManagerState(state)); + } + fn write_value( &mut self, peripheral_uuid: Uuid, @@ -831,6 +847,11 @@ impl CoreBluetoothInternal { characteristic_uuid: Uuid, fut: CoreBluetoothReplyStateShared, ) { + trace!( + "Manager State {:?}", + cb::centeralmanger_state(&*self.manager) + ); + if let Some(peripheral) = self.peripherals.get_mut(&peripheral_uuid) { if let Some(service) = peripheral.services.get_mut(&service_uuid) { if let Some(characteristic) = service.characteristics.get_mut(&characteristic_uuid) @@ -1004,7 +1025,7 @@ impl CoreBluetoothInternal { // "ready" variable in our adapter that will cause scans/etc // to fail if this hasn't updated. CentralDelegateEvent::DidUpdateState => { - self.dispatch_event(CoreBluetoothEvent::AdapterConnected).await + self.dispatch_event(CoreBluetoothEvent::DidUpdateState).await } CentralDelegateEvent::DiscoveredPeripheral{cbperipheral} => { self.on_discovered_peripheral(cbperipheral).await @@ -1103,6 +1124,9 @@ impl CoreBluetoothInternal { CoreBluetoothMessage::IsConnected{peripheral_uuid, future} => { self.is_connected(peripheral_uuid, future); }, + CoreBluetoothMessage::FetchManagerState {future} =>{ + self.get_manager_state_async(future); + }, CoreBluetoothMessage::ReadDescriptorValue{peripheral_uuid, service_uuid, characteristic_uuid, descriptor_uuid, future} => { self.read_descriptor_value(peripheral_uuid, service_uuid, characteristic_uuid, descriptor_uuid, future) } @@ -1184,6 +1208,14 @@ pub fn run_corebluetooth_thread( runtime.block_on(async move { let mut cbi = CoreBluetoothInternal::new(receiver, event_sender); loop { + // When the IOS or MacOS device if powered off or locked + // the manager state will suddenly throw DidUpdateState event and turn off. + // If we are not exiting the main loop here the futures requested after + // power off will be stuck forever. + if cbi.get_manager_state() == CBManagerState::PoweredOff { + trace!("Breaking out of the corebluetooth loop. Manager is off."); + break; + } cbi.wait_for_message().await; } })