diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 49e27e29a..d5a6778a7 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -41,6 +41,8 @@ Fixed * Fixed waiting for notification start/stop to complete in CoreBluetooth backend. * Fixed write without response on BlueZ < 5.51. * Fixed error propagation for CoreBluetooth events +* Fixed deadlock in CoreBluetooth backend when device disconnects while + callbacks are pending. Fixes #535. `0.11.0`_ (2021-03-17) diff --git a/bleak/backends/corebluetooth/CentralManagerDelegate.py b/bleak/backends/corebluetooth/CentralManagerDelegate.py index 371147869..4004a2797 100644 --- a/bleak/backends/corebluetooth/CentralManagerDelegate.py +++ b/bleak/backends/corebluetooth/CentralManagerDelegate.py @@ -7,11 +7,12 @@ """ import asyncio +import itertools import logging import platform import threading from enum import Enum -from typing import List +from typing import List, Optional import objc from CoreBluetooth import ( @@ -68,7 +69,7 @@ def init(self): return None self.event_loop = asyncio.get_event_loop() - self.connected_peripheral_delegate = None + self.connected_peripheral_delegate: Optional[PeripheralDelegate] = None self.connected_peripheral = None self._connection_state = CMDConnectionState.DISCONNECTED @@ -269,8 +270,8 @@ def did_connect_peripheral(self, central, peripheral): ) ) if self._connection_state != CMDConnectionState.CONNECTED: - peripheralDelegate = PeripheralDelegate.alloc().initWithPeripheral_( - peripheral + peripheralDelegate: PeripheralDelegate = ( + PeripheralDelegate.alloc().initWithPeripheral_(peripheral) ) self.connected_peripheral_delegate = peripheralDelegate self._connection_state = CMDConnectionState.CONNECTED @@ -312,6 +313,27 @@ def did_disconnect_peripheral( self, central: CBCentralManager, peripheral: CBPeripheral, error: NSError ): logger.debug("Peripheral Device disconnected!") + + # If there are any pending futures waiting for delegate callbacks, we + # need to raise an exception since the callback will no longer be + # called because the device is disconnected. + delegate = self.connected_peripheral_delegate + for future in itertools.chain( + (delegate._services_discovered_future,), + delegate._service_characteristic_discovered_futures.values(), + delegate._characteristic_descriptor_discover_futures.values(), + delegate._characteristic_read_futures.values(), + delegate._characteristic_write_futures.values(), + delegate._characteristic_notify_change_futures.values(), + delegate._descriptor_read_futures.values(), + delegate._descriptor_write_futures.values(), + ): + try: + future.set_exception(BleakError("disconnected")) + except asyncio.InvalidStateError: + # the future was already done + pass + self.connected_peripheral_delegate = None self.connected_peripheral = None self._connection_state = CMDConnectionState.DISCONNECTED