Skip to content

Commit

Permalink
bleak: fix leaking of ensure_future()
Browse files Browse the repository at this point in the history
As noted in the Python docs, a reference to the return value of
`asyncio.ensure_future()` must be held in order to prevent it from
being garbage collected before the task completes.

This applies the recommended fix from the docs of holding the reference
in a global set and then discarding the reference when the task
completes.

Also change `asyncio.ensure_future()` to `asyncio.create_task()` while
we are touching this since the minimum supported Python version is now
3.7.

Fixes: #1258
  • Loading branch information
dlech committed Mar 20, 2023
1 parent 1b93859 commit e01ae59
Show file tree
Hide file tree
Showing 4 changed files with 27 additions and 3 deletions.
6 changes: 6 additions & 0 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,12 @@ and this project adheres to `Semantic Versioning <https://semver.org/spec/v2.0.0
`Unreleased`_
=============

Fixed
-----
* Fixed possible garbage collection of running async callback from ``BleakClient.start_notify()``.
* Fixed possible garbage collection of running async callback from ``BleakScanner(detection_callback=)``.
* Fixed possible garbage collection of disconnect monitor in BlueZ backend. Fixed #1258.

`0.20.0`_ (2023-03-17)
======================

Expand Down
9 changes: 8 additions & 1 deletion bleak/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
Iterable,
List,
Optional,
Set,
Tuple,
Type,
Union,
Expand Down Expand Up @@ -70,6 +71,10 @@
_logger.setLevel(logging.DEBUG)


# prevent tasks from being garbage collected
_background_tasks: Set[asyncio.Task] = set()


class BleakScanner:
"""
Interface for Bleak Bluetooth LE Scanners.
Expand Down Expand Up @@ -702,7 +707,9 @@ def callback(sender: BleakGATTCharacteristic, data: bytearray):
if inspect.iscoroutinefunction(callback):

def wrapped_callback(data):
asyncio.ensure_future(callback(characteristic, data))
task = asyncio.create_task(callback(characteristic, data))
_background_tasks.add(task)
task.add_done_callback(_background_tasks.discard)

else:
wrapped_callback = functools.partial(callback, characteristic)
Expand Down
7 changes: 6 additions & 1 deletion bleak/backends/bluezdbus/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,9 @@

logger = logging.getLogger(__name__)

# prevent tasks from being garbage collected
_background_tasks: Set[asyncio.Task] = set()


class BleakClientBlueZDBus(BaseBleakClient):
"""A native Linux Bleak Client
Expand Down Expand Up @@ -243,13 +246,15 @@ def on_value_changed(char_path: str, value: bytes) -> None:
self._is_connected = True

# Create a task that runs until the device is disconnected.
asyncio.ensure_future(
task = asyncio.create_task(
self._disconnect_monitor(
self._bus,
self._device_path,
local_disconnect_monitor_event,
)
)
_background_tasks.add(task)
task.add_done_callback(_background_tasks.discard)

#
# We will try to use the cache if it exists and `dangerous_use_bleak_cache`
Expand Down
8 changes: 7 additions & 1 deletion bleak/backends/scanner.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,17 @@
List,
NamedTuple,
Optional,
Set,
Tuple,
Type,
)

from ..exc import BleakError
from .device import BLEDevice

# prevent tasks from being garbage collected
_background_tasks: Set[asyncio.Task] = set()


class AdvertisementData(NamedTuple):
"""
Expand Down Expand Up @@ -165,7 +169,9 @@ def register_detection_callback(
if inspect.iscoroutinefunction(callback):

def detection_callback(s, d):
asyncio.ensure_future(callback(s, d))
task = asyncio.create_task(callback(s, d))
_background_tasks.add(task)
task.add_done_callback(_background_tasks.discard)

else:
detection_callback = callback
Expand Down

0 comments on commit e01ae59

Please sign in to comment.