Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

bluez: Verify that Advertisement Monitor has been registered #1140

Open
wants to merge 3 commits into
base: develop
Choose a base branch
from

Conversation

bojanpotocnik
Copy link
Contributor

Solves #1136

Comment on lines 493 to 505
reply = await self._bus.call(
Message(
destination=defs.BLUEZ_SERVICE,
path=adapter_path,
interface=defs.PROPERTIES_INTERFACE,
member="Get",
signature="ss",
body=[
defs.ADVERTISEMENT_MONITOR_MANAGER_INTERFACE,
"SupportedFeatures",
],
)
)
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Another option would be to execute

                reply = await self._bus.call(
                    Message(
                        destination=defs.BLUEZ_SERVICE,
                        path=monitor_path,
                        interface=defs.PROPERTIES_INTERFACE,
                        member="Get",
                        signature="ss",
                        body=[defs.ADVERTISEMENT_MONITOR_INTERFACE, "Type"],
                    )
                )

however this fails with ['Method "Get" with signature "ss" on interface "org.freedesktop.DBus.Properties" doesn\'t exist\n']. Considering that even await asyncio.sleep(0.1) was sufficient here, I left this call which always succeeds.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think asyncio.sleep(0) is idiomatic if we just need to yield from the coroutine momentarily.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Tested, and it is not enough. I think that await asyncio.sleep(0) does yield, but it does not assure that the D-Bus event/operation queue is completely processed, or processed at all. Just for curiosity, asyncio.sleep(0.001) "always" does the job, 0.0001 does the job in about half of the tries, 0.00001 is never enough.

Copy link
Collaborator

@dlech dlech left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is an interesting idea. Since Release being called could be for other reasons besides this one particular case, maybe it is better to just stick with the os.uname() check and fail early?

Comment on lines 493 to 505
reply = await self._bus.call(
Message(
destination=defs.BLUEZ_SERVICE,
path=adapter_path,
interface=defs.PROPERTIES_INTERFACE,
member="Get",
signature="ss",
body=[
defs.ADVERTISEMENT_MONITOR_MANAGER_INTERFACE,
"SupportedFeatures",
],
)
)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think asyncio.sleep(0) is idiomatic if we just need to yield from the coroutine momentarily.

bleak/backends/bluezdbus/advertisement_monitor.py Outdated Show resolved Hide resolved
bleak/backends/bluezdbus/advertisement_monitor.py Outdated Show resolved Hide resolved
@bojanpotocnik
Copy link
Contributor Author

Since Release being called could be for other reasons besides this one particular case ...

My first thought - before further analysing the BlueZ source code - was that even though the Release is called for other reasons, it shall for sure not be called before this function returns.

Checking the source https://git.kernel.org/pub/scm/bluetooth/bluez.git/tree/src/adv_monitor.c#n520, I see that D-Bus Release method is invoked only from monitor_release(), which is called only from monitor_destroy(), which is called from 5 functions, all of them related to removal of the monitor as a result of deliberate action, error, or invalid parameters.

... maybe it is better to just stick with the os.uname() check and fail early?

I like the os.uname() idea, but would prefer to solve this as BlueZ-ish as possible - maybe someone can use a custom kernel or kernel patched to have or have not support for this feature... Most probably not, but still - this was a fun/frustrating challenge for today.

@bojanpotocnik bojanpotocnik force-pushed the 1136_verify_adding_adv_monitor branch 2 times, most recently from 2c4800e to 57a68f9 Compare November 24, 2022 11:33
@bojanpotocnik
Copy link
Contributor Author

bojanpotocnik commented Nov 24, 2022

Done some more testing, using kernel <5.10 and >5.10.

In case of kernel <5.10, Release() is invoked during the call of SupportedFeatures(), as expected.

In case of kernel >5.10, Activate() is called some time after this function already returns - e.g. if using:

async with BleakScanner(..., scanning_mode="passive"):
    await asyncio.sleep(0.0001)

no AdvertisementMonitor method is invoked, but when this delay is increased, Activate() is invoked. Interestingly, Release() is not invoked at all, even if I add some sleep after the context manager.

I added example file: On one hand, I like to show how one can use async iterator to print detected devices, but on the other hand, it gets complicated. I can remove or simplify it if you prefer, I'll do PR for BleakScanner supporting async iteration anyway. it tries to use the passive scanning first and falls back to active scanning if not possible.

@bojanpotocnik bojanpotocnik force-pushed the 1136_verify_adding_adv_monitor branch 2 times, most recently from d952af5 to a74f1d9 Compare November 24, 2022 17:51
@dlech
Copy link
Collaborator

dlech commented Nov 25, 2022

The scope of this PR is creeping. Can we please stick to just one change at a time?

Regarding checking if Release has been called or not, I've thought about it a bit more and it seems like we should make this cancel any async task in the async context manager and raise an exception similar to how asyncio.timeout works. This would avoid any race condition of when Release is called and we wouldn't have to rely on the hack of getting a property to get the right timing and would also cover any other reason for Release being called at any time.

but would prefer to solve this as BlueZ-ish as possible

This seems like it would require a change to BlueZ, such as exposing a list of supported management APIs. Since Release can be called for multiple reasons, it doesn't seem like a reliable solution for detecting support of passive scanning (but it still does seem worth handling the call to Release since we have seen that not handling can lead to waiting for advertisements that will never come).

@bojanpotocnik
Copy link
Contributor Author

I'll wait for #1146 and #1147 to be merged and then rebase using features from #1146.

self._bus.export(monitor_path, monitor)

# During export, MGMT command "Add Advertisement Patterns Monitor (0x0052)" is executed.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would it work to detect "org.bluez.AdvertisementMonitorManager1" in the adapter details for this case?

https://github.com/Bluetooth-Devices/bluetooth-adapters/blob/bcadadd67150bd4648a0002dc151adc46d302732/src/bluetooth_adapters/systems/linux.py#L104

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you for the suggestion, but unfortunately this would not solve this particular issue.

The issue here is that not only that org.bluez.AdvertisementMonitorManager1 is present in managed objects, even calling org.bluez.AdvertisementMonitorManager1.RegisterMonitor() succeeds. Only "later" kernel responds with Unknown Command (0x01) to underlying MGMT command Add Advertisement Patterns Monitor (0x0052) - this response is processed by the BlueZ code and registered AdvertisementMonitor1 is destroyed, but the error is not propagated to the D-Bus caller (bleak) in any way.

@bojanpotocnik bojanpotocnik force-pushed the 1136_verify_adding_adv_monitor branch 2 times, most recently from 932d1f5 to c798a47 Compare November 30, 2022 15:31
@bojanpotocnik
Copy link
Contributor Author

Rebased on #1146.

# kernel does not support passive scanning and error was returned when trying to execute
# MGMT command "Add Adv Patterns Monitor" (see https://github.com/hbldh/bleak/issues/1136).
def advertisement_monitor_released() -> None:
# FIXME: This is not actually raised
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've again hit a roadblock and would be happy for some ideas on how to solve it. So, there are many reasons why active discovery could be (prematurely) stopped (Discovering set to False), but only a specific case when Advertising Monitor is released.
I want to use this fact to detect that the passive scanning is not supported, instead of just informing the user that something stopped the discovery (e.g. using HCI injection or mgmt commands) and it should be restarted (which would not help in this case).
Trying to achieve that BleakNoPassiveScanError would be raised (preferably in the start() method), I noticed that I came really close to the previous solution, where I "processed the D-Bus events" after exporting the Advertising Manager 🤔

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@bojanpotocnik bojanpotocnik changed the base branch from develop to scanner-early-stop-hook December 1, 2022 08:22
@bojanpotocnik bojanpotocnik changed the base branch from scanner-early-stop-hook to develop December 1, 2022 08:23
@bojanpotocnik
Copy link
Contributor Author

Based on the BlueZ tests, it seems like Release() being invoked for failed registration of the monitor is expected behaviour.

https://git.kernel.org/pub/scm/bluetooth/bluez.git/tree/test/example-adv-monitor#n328

    # Register the app root path to expose advertisement monitors.
    # Release() should get called on monitor0 - incorrect monitor type.
    # Activate() should get called on monitor1.
    ret = app.register_app()
    if not ret:
        print('RegisterMonitor failed.')
        mainloop.quit()
        exit(-1)

@bojanpotocnik
Copy link
Contributor Author

After further checking BlueZ sources and tests, I used the fact that either Activate or Release will be invoked when the advertisement monitor is processed and came up with the current implementation.
One additional benefit of this implementation is that when BleakScanner.start() returns, we are sure that the passive scanning has actually been started (or failed).

@bojanpotocnik bojanpotocnik marked this pull request as ready for review December 1, 2022 13:23
Comment on lines 488 to 495
# 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 https://github.com/hbldh/bleak/issues/1136).
# Otherwise, monitor will be activated and start to receive advertisement packets.
monitor_processed = asyncio.Queue()

try:
monitor = AdvertisementMonitor(filters, discovery_stopped_callback)
monitor = AdvertisementMonitor(filters, monitor_processed.put_nowait)
Copy link
Contributor Author

@bojanpotocnik bojanpotocnik Dec 1, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can also keep the discovery_stopped_callback functionality, if required, by adding an intermediate callback which will call put_nowait and then discovery_stopped_callback.

@bojanpotocnik bojanpotocnik force-pushed the 1136_verify_adding_adv_monitor branch 2 times, most recently from cbdfdc8 to db0be8c Compare December 5, 2022 12:17
@bojanpotocnik
Copy link
Contributor Author

As the latest solution does not really use features from #1146, except adding a comment explaining why callback is not added to _discovery_stopped_callbacks when passive scanning

BlueZManager -> async passive_scan() -> async with self._bus_lock:
            self._device_removed_callbacks.append(device_removed_callback_and_state)

            # Once a monitoring job is activated by BlueZ, the client can expect to get notified
            # on the targeted advertisements no matter if there is an ongoing discovery session
            # (started/stopped with org.bluez.Adapter1.StartDiscovery/StopDiscovery, as done when
            # using active scanning).
            # That is why discovery_stopped_callback is not added to self._discovery_stopped_callbacks
            # at this point, as org.bluez.Adapter1.Discovering status is not relevant for passive
            # scanning.

            # If advertisement monitor is released before the scanning is stopped, it means that the

I've moved it to bojanpotocnik:1136_verify_adding_adv_monitor_rebased_1146 and rebased this one on develop.

This exception can be used to e.g. retry scanning with scan_mode
adjusted to "active" on systems not supporting the "passive" scan.
In case of BlueZ, using passive scanning mode is not simply passing
"passive" as a ``scan_mode`` parameter, but requires more arguments.
This example showcases that.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants