Skip to content

Commit

Permalink
fix(macos): Break the corebluetooth loop when manager turned off
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
dmtrKovalenko committed May 24, 2024
1 parent 1937ff5 commit 60d00ca
Show file tree
Hide file tree
Showing 3 changed files with 69 additions and 11 deletions.
18 changes: 15 additions & 3 deletions src/corebluetooth/adapter.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};
Expand Down Expand Up @@ -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(),
Expand All @@ -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 {
Expand Down Expand Up @@ -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)
}
}
}
}
});
Expand Down
19 changes: 18 additions & 1 deletion src/corebluetooth/framework.rs
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,22 @@ 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,
}

pub fn centeralmanger_state(cbcentralmanager: id) -> CBManagerState {
unsafe { msg_send![cbcentralmanager, state] }
}

pub fn centralmanager_stopscan(cbcentralmanager: id) {
unsafe { msg_send![cbcentralmanager, stopScan] }
}
Expand Down Expand Up @@ -207,10 +223,11 @@ pub mod cb {
unsafe { msg_send![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,
Expand Down
43 changes: 36 additions & 7 deletions src/corebluetooth/internal.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
use super::{
central_delegate::{CentralDelegate, CentralDelegateEvent},
framework::{
cb::{self, CBManagerAuthorization, CBPeripheralState},
cb::{self, CBManagerAuthorization, CBManagerState, CBPeripheralState},
ns,
},
future::{BtlePlugFuture, BtlePlugFutureStateShared},
Expand All @@ -21,10 +21,7 @@ use super::{
nsuuid_to_uuid,
},
};
use crate::api::{
bleuuid::uuid_from_u16, CharPropFlags, Characteristic, Descriptor, ScanFilter, Service,
WriteType,
};
use crate::api::{CharPropFlags, Characteristic, Descriptor, ScanFilter, Service, WriteType};
use crate::Error;
use cocoa::{
base::{id, nil},
Expand Down Expand Up @@ -148,6 +145,7 @@ pub enum CoreBluetoothReply {
ReadResult(Vec<u8>),
Connected(BTreeSet<Service>),
State(CBPeripheralState),
ManagerState(CBManagerState),
Ok,
Err(String),
}
Expand Down Expand Up @@ -408,11 +406,14 @@ pub enum CoreBluetoothMessage {
data: Vec<u8>,
future: CoreBluetoothReplyStateShared,
},
FetchManagerState {
future: CoreBluetoothReplyStateShared,
},
}

#[derive(Debug)]
pub enum CoreBluetoothEvent {
AdapterConnected,
DidUpdateState,
DeviceDiscovered {
uuid: Uuid,
name: Option<String>,
Expand Down Expand Up @@ -795,6 +796,18 @@ impl CoreBluetoothInternal {
}
}

fn get_manager_state_sync(&mut self) -> CBManagerState {
cb::centeralmanger_state(*self.manager)
}

fn get_manager_state(&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,
Expand Down Expand Up @@ -837,6 +850,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)
Expand Down Expand Up @@ -1010,7 +1028,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
Expand Down Expand Up @@ -1109,6 +1127,9 @@ impl CoreBluetoothInternal {
CoreBluetoothMessage::IsConnected{peripheral_uuid, future} => {
self.is_connected(peripheral_uuid, future);
},
CoreBluetoothMessage::FetchManagerState {future} =>{
self.get_manager_state(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)
}
Expand Down Expand Up @@ -1189,6 +1210,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_sync() == CBManagerState::PoweredOff {
trace!("Breaking out of the corebluetooth loop. Manager is off.");
break;
}
cbi.wait_for_message().await;
}
})
Expand Down

0 comments on commit 60d00ca

Please sign in to comment.