Skip to content

Commit

Permalink
bluezdbus/scanner: Detect AdvertisementMonitor not registering
Browse files Browse the repository at this point in the history
  • Loading branch information
bojanpotocnik committed Dec 5, 2022
1 parent 6da7322 commit e56cc25
Show file tree
Hide file tree
Showing 3 changed files with 47 additions and 6 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ Added
* Added optional command line argument to use debug log level to all applicable examples.
* Make sure the disconnect monitor task is properly cancelled on the BlueZ client.
* Added ``BleakNoPassiveScanError`` exception.
* Make sure that BlueZ Advertisement Monitor is actually registered when passive scanning. Solves #1136.

Changed
-------
Expand Down
14 changes: 11 additions & 3 deletions bleak/backends/bluezdbus/advertisement_monitor.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
"""

import logging
from typing import Iterable, NamedTuple, Tuple, Union, no_type_check
from typing import Callable, Iterable, NamedTuple, Tuple, Union, no_type_check

from dbus_fast.service import ServiceInterface, dbus_property, method, PropertyAccess

Expand All @@ -34,6 +34,9 @@ class OrPattern(NamedTuple):
OrPatternLike = Union[OrPattern, Tuple[int, AdvertisementDataType, bytes]]


StatusCallback = Callable[[bool], None]


class AdvertisementMonitor(ServiceInterface):
"""
Implementation of the org.bluez.AdvertisementMonitor1 D-Bus interface.
Expand All @@ -49,25 +52,30 @@ class AdvertisementMonitor(ServiceInterface):
"""

def __init__(
self,
or_patterns: Iterable[OrPatternLike],
self, or_patterns: Iterable[OrPatternLike], status_callback: StatusCallback
):
"""
Args:
or_patterns:
List of or patterns that will be returned by the ``Patterns`` property.
status_callback:
A callback that is called with argument ``True`` when the D-bus "Activate"
method is called, or with ``False`` when "Release" is called.
"""
super().__init__(defs.ADVERTISEMENT_MONITOR_INTERFACE)
# dbus_fast marshaling requires list instead of tuple
self._or_patterns = [list(p) for p in or_patterns]
self._status_callback = status_callback

@method()
def Release(self):
logger.debug("Release")
self._status_callback(False)

@method()
def Activate(self):
logger.debug("Activate")
self._status_callback(True)

# REVISIT: mypy is broke, so we have to add redundant @no_type_check
# https://github.com/python/mypy/issues/6583
Expand Down
38 changes: 35 additions & 3 deletions bleak/backends/bluezdbus/manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
import asyncio
import logging
import os
import sys
from typing import (
Any,
Callable,
Expand All @@ -24,6 +25,11 @@
)
from weakref import WeakKeyDictionary

if sys.version_info < (3, 11):
from async_timeout import timeout as async_timeout
else:
from asyncio import timeout as async_timeout

from dbus_fast import BusType, Message, MessageType, Variant, unpack_variants
from dbus_fast.aio.message_bus import MessageBus

Expand Down Expand Up @@ -448,12 +454,20 @@ async def passive_scan(
)
self._device_removed_callbacks.append(device_removed_callback_and_state)

# If advertisement monitor is released before the scanning is stopped, it means that the
# kernel does not support passive scanning and error was returned when trying to execute
# MGMT command "Add Adv Patterns Monitor" (see #1136). Otherwise, monitor is activated
# and starts to receive advertisement packets.
monitor_activated = asyncio.Queue()

try:
monitor = AdvertisementMonitor(filters)
monitor = AdvertisementMonitor(filters, monitor_activated.put_nowait)

# this should be a unique path to allow multiple python interpreters
# running bleak and multiple scanners within a single interpreter
monitor_path = f"/org/bleak/{os.getpid()}/{id(monitor)}"
monitor_path = (
f"/org/bleak/{os.getpid()}/{type(monitor).__name__}_{id(monitor)}"
)

reply = await self._bus.call(
Message(
Expand Down Expand Up @@ -505,14 +519,32 @@ async def stop():
)
assert_reply(reply)

return stop
try:
# Advertising Monitor will be "immediately" activated or released
async with async_timeout(1):
if await monitor_activated.get():
# Advertising Monitor has been activated
return stop

except asyncio.TimeoutError:
pass

# Do not call await stop() here as the bus is already locked

except BaseException:
# if starting scanning failed, don't leak the callbacks
self._advertisement_callbacks.remove(callback_and_state)
self._device_removed_callbacks.remove(device_removed_callback_and_state)
raise

# Reaching here means that the Advertising Monitor has not been successfully activated
await stop()

raise BleakNoPassiveScanError(
"Advertising Monitor (required for passive scanning) is not supported by this kernel"
" (Linux kernel >= 5.10 is required)"
)

def add_device_watcher(
self,
device_path: str,
Expand Down

0 comments on commit e56cc25

Please sign in to comment.