Skip to content

backends/bluezdbus: tolerate InProgress from StopDiscovery - #2022

Open
cgoudie wants to merge 2 commits into
hbldh:developfrom
TechBlueprints:fix/stopdiscovery-inprogress
Open

cgoudie wants to merge 2 commits into
hbldh:developfrom
TechBlueprints:fix/stopdiscovery-inprogress

Conversation

@cgoudie

@cgoudie cgoudie commented Sep 3, 2026 •

Copy link
Copy Markdown

Fixes #2021.

The change

In the BlueZ backend, BleakScanner.stop() no longer fails on org.bluez.Error.InProgress from StopDiscovery when the stop has in fact taken effect. On that reply it calls StartDiscovery and StopDiscovery once more on the same adapter, right then and under the same bus lock, without touching the discovery filter in between (our filter is still registered with bluetoothd, so the extra start is answered without any kernel traffic; clearing the filter first would start a real unfiltered discovery). If the second stop succeeds, bluetoothd's stale discovering flag has been reset, the original stop is treated as complete, logged at INFO, and the filters are cleared as usual. If the second stop is rejected as well, bluetoothd's discovery state is stuck (see point 6), a WARNING names the adapter and the remedy, and the original BleakDBusError is raised so the caller learns the adapter is not scanning. If the extra StartDiscovery itself fails, a WARNING is logged and stop() returns, since scanning is stopped as requested and the failure is from a call the user did not make. NotReady is still swallowed without probing, and every other error still raises.

The stop path lives in three small helpers (_stop_discovery, _probe_stuck_discovery, _call_adapter) with a match on the error name. No state is kept between calls.

Why swallowing is correct

InProgress on StopDiscovery is bluetoothd relaying MGMT_STATUS_REJECTED from the kernel, which the kernel returns whenever discovery is not in the FINDING or RESOLVING state (net/bluetooth/mgmt.c: stop_discovery, hci_core.c: hci_discovery_active). Before it replies, bluetoothd has already run discovery_remove(client) in src/adapter.c: stop_discovery_complete(), so our session is gone when the error arrives. The stop we asked for has happened in every sense that matters to the caller. Raising turns a completed scan into an exception, and the caller cannot retry because the scanner nulls _stop before awaiting it.

The points raised in the issue thread, one at a time.

1. "I would rather see an integration test that does it for real"

I tried, and I could not make one that fails without the fix, so I have not included one. The reason is in how the two halves interact. bluetoothd forwards a client's StopDiscovery to the kernel only while its own discovery_enable flag is set (discovery_stop, whose idle branch returns without any MGMT round trip otherwise), and it clears that flag when it processes the kernel's Discovering(false) event (discovering_callback). So the kernel's rejection needs a stop to be forwarded after the kernel has left FINDING (for an LE-only scan, at its own DISCOV_LE_TIMEOUT of 10.24 s) but before bluetoothd has processed that event. That is a scheduling race inside bluetoothd, not a time offset a test can aim at. Measured on BlueZ 5.72: stops at 10, 12, 15, 15.2, 15.4 and 16 s after start were accepted 18 of 18 times, and five find_device_by_address(..., timeout=15.0) teardowns stopped cleanly. I also tried inducing it from the vhci controller by holding the reply to bluetoothd's automatic restart so the kernel sits in STARTING while stop() runs; it does not work, because bluetoothd only sets discovery_enable in start_discovery_complete, so during its own restart it still reads idle and stops cleanly without asking the kernel. What is included instead is a unit test that fakes the bus reply (see Test below). It pins bleak's handling; it does not exercise BlueZ.

2. "We should just add some comments to it that link to the BlueZ bug or code that causes the problem"

Done. The comment above the condition names the bluetoothd path (stop_discovery_complete() calls discovery_remove() before checking the status, then relays MGMT_STATUS_REJECTED as InProgress) and links this issue and bluez/bluez#807.

3. "It sounds like we should make a patch to BlueZ to fix this"

Agreed, and I intend to send one to linux-bluetooth after this lands. The shape: in stop_discovery_complete(), treat MGMT_STATUS_REJECTED as "already stopped" rather than returning early, so discovery_enable, discovering and the Discovering property are reset the same way they are on success. bleak's change stands on its own regardless, since every 5.x user gets the error as things are.

4. "I guess this means we can't listen for a property change to tell us that the scanning stopped early"

Correct, twice over. When the kernel stops an LE scan on its own timer, discovering_callback only flips bluetoothd's internal discovery_enable; adapter->discovering stays true and no PropertiesChanged is emitted, so the early kernel stop is invisible over D-Bus. And after a failed StopDiscovery, the early return leaves Discovering true with no clients (bluez/bluez#807), so there is nothing to subscribe to for this case either.

5. "Also see #1146 which is related to catching that scanning stopped before we requested it"

Related but a different case. From bleak's side the scan never stopped early: bluetoothd kept our session and cycled the kernel scan internally, and the error arises at our own requested stop. An early-stop hook would not fire (no property change, per point 4) and is not needed for this fix. The two are complementary.

6. "It seems like it could affect starting the next scan since the discovering property is now wrong?"

Yes, in two different ways, and the probe tells them apart. In the harmless case the kernel's discovering = 0 event has reached bluetoothd before it relays the rejected stop, so discovery_enable is already 0: the next StartDiscovery with equal filters is accepted but skipped by update_discovery_filter() because adapter->discovering is still set, and that scan sees nothing once; its StopDiscovery then takes the idle branch of discovery_stop(), succeeds, and clears discovering. That same idle branch is what answers the probe's stop, so the probe succeeds. In the stuck case discovery_enable stays 1, every start queues a stop whose completion callback is NULL, every last-client stop is rejected, and the probe's stop is rejected too. The Discovering property cannot separate the two at the moment of the error: bluetoothd sends the reply before touching it and never clears it on the error path (bluez/bluez#807). The one empty scan in the harmless case is bluetoothd's bug; the patch in point 3 fixes it.

What we measured

24 occurrences on one Cerbo GX (BlueZ 5.72) in a single day, every one on the find_device_by_address(..., timeout=15.0) teardown for an absent device, none on a scan that found its device early. bluetoothd was crash-looping that day for an unrelated reason, which is consistent with the race widening under load; in the seven days since that was fixed there have been none. #958 and #1689 show other users hitting it. So the condition is real and provable from source, intermittent in practice, and not reproducible on demand.

Test

tests/backends/bluezdbus/test_issue_2021.py runs a real BleakScanner against the fake BlueZ service on a private dbus-daemon from the shared fixture in #2033 (that fixture commit is the first commit here). The fake adapter answers StopDiscovery and StartDiscovery from scripted reply lists. Five cases: one InProgress is recovered (asserts the exact call sequence including the final filter reset, an INFO record and no WARNING); the second stop is rejected too (the original InProgress error is raised, one WARNING, no filter reset); the extra StartDiscovery fails (returns, one WARNING); NotReady is swallowed without probing; Failed still raises. Also observed once on real hardware: on a Cerbo GX with BlueZ 5.72, a 10.3 s scan window hit the race, the second start and stop cleared it, and the scanner's stop returned normally with the INFO line.

🤖 Generated with Claude Code

@cgoudie

cgoudie commented Sep 3, 2026

Copy link
Copy Markdown
Author

Updated for review. The PR description now answers each of your points under its own heading, so I will only summarise here.

On the integration test: I tried to make one that fails without the fix and could not. bluetoothd only forwards a client's stop to the kernel while its own discovery_enable flag is set, and clears it when it processes the kernel's Discovering(false) event, so the rejection needs a stop to land in the gap between the two. That is a scheduling race, not a time offset: stops at 10, 12, 15, 15.2, 15.4 and 16 s after start were accepted 18 of 18 times on 5.72, as were five find_device_by_address timeouts. Holding the vhci controller's reply to bluetoothd's restart does not help either, because bluetoothd stops cleanly whenever it still reads idle. So the PR carries a unit test that fakes the InProgress reply and pins bleak's handling, plus the negative case that other errors still raise. If you see a way to reach the race from the harness, I will gladly add it.

The code comment now names the bluetoothd path as well as the bug. On your question about the next scan: yes, it is affected, and the description spells out how; that part is bluetoothd's and I will send a patch to linux-bluetooth after this. Marking ready for review.

@cgoudie
cgoudie marked this pull request as ready for review September 3, 2026 18:11
@codecov

codecov Bot commented Sep 3, 2026 •

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 65.33%. Comparing base (c7ca1b0) to head (7b53226).

Additional details and impacted files
@@             Coverage Diff             @@
##           develop    #2022      +/-   ##
===========================================
- Coverage    65.34%   65.33%   -0.01%     
===========================================
  Files           53       53              
  Lines         5026     5048      +22     
  Branches       608      610       +2     
===========================================
+ Hits          3284     3298      +14     
- Misses        1519     1522       +3     
- Partials       223      228       +5     
Flag Coverage Δ
android-integration-py3.13-api31 ?
android-integration-py3.13-api33 26.90% <0.00%> (-0.16%) ⬇️
bluez-integration-py310 34.44% <96.77%> (+0.42%) ⬆️
bluez-integration-py311 34.40% <100.00%> (+0.44%) ⬆️
bluez-integration-py312 34.40% <100.00%> (+0.44%) ⬆️
bluez-integration-py313 34.40% <100.00%> (+0.44%) ⬆️
bluez-integration-py314 33.09% <100.00%> (+0.46%) ⬆️
macos-latest-py310 16.40% <0.00%> (-0.08%) ⬇️
macos-latest-py311 16.40% <0.00%> (-0.08%) ⬇️
macos-latest-py312 16.40% <0.00%> (-0.08%) ⬇️
macos-latest-py313 16.40% <0.00%> (-0.08%) ⬇️
macos-latest-py314 16.17% <0.00%> (-0.08%) ⬇️
ubuntu-latest-py310 26.14% <96.77%> (+1.19%) ⬆️
ubuntu-latest-py311 26.16% <100.00%> (+1.21%) ⬆️
ubuntu-latest-py312 26.16% <100.00%> (+1.21%) ⬆️
ubuntu-latest-py313 26.16% <100.00%> (+1.21%) ⬆️
ubuntu-latest-py314 24.68% <100.00%> (+1.25%) ⬆️
windows-latest-py310 15.90% <0.00%> (-0.07%) ⬇️
windows-latest-py311 15.90% <0.00%> (-0.07%) ⬇️
windows-latest-py312 15.90% <0.00%> (-0.07%) ⬇️
windows-latest-py313 15.90% <0.00%> (-0.07%) ⬇️
windows-latest-py314 15.62% <0.00%> (-0.07%) ⬇️
winvhci-integration-py310 23.83% <0.00%> (-0.11%) ⬇️
winvhci-integration-py311 23.83% <0.00%> (-0.11%) ⬇️
winvhci-integration-py312 23.83% <0.00%> (-0.11%) ⬇️
winvhci-integration-py313 23.83% <0.00%> (-0.11%) ⬇️
winvhci-integration-py314 23.99% <0.00%> (-0.07%) ⬇️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

@dlech

dlech commented Sep 3, 2026

Copy link
Copy Markdown
Collaborator

On your question about the next scan: yes, it is affected

Hmm... then ignoring this error might will just lead to a silent scan not working problem later, correct? I'm tempted to say, let's keep the error just to let people know bluetoothd is broken and probably needs to be restarted.

@cgoudie

cgoudie commented Sep 5, 2026

Copy link
Copy Markdown
Author

Good question, and the answer is that there are two different situations here that produce the same InProgress, and this PR is about only the first one.

  1. The one-off case. The kernel stops an LE scan on its own after DISCOV_LE_TIMEOUT (10.24 s). If our stop() lands just after that, the kernel answers the redundant stop with REJECTED, and bluetoothd has already removed our client in stop_discovery_complete before it relays the error. So our discovery session is gone, the scan we asked to end has ended, and the very next StartDiscovery from the same connection starts a fresh scan normally. This is the common case, it is transient, and scanning is not broken afterward. Swallowing the error here is correct: the operation succeeded, and raising turns a successful stop into a spurious exception the caller cannot retry (the scanner nulls _stop before awaiting it).

  2. The stuck case. Separately, bluetoothd can get into a state where discovery_enable stays 1 while the kernel is DISCOVERY_STOPPED, and then it stays there: every last-client stop is rejected and never clears the belief, and every new filtered start takes the "queue a stop" branch whose completion callback is NULL, so no MGMT start is ever sent again. The card then scans nothing until it is reset. We have observed this on our own hardware (a card that ran 192 clean scan windows, then returned InProgress on every stop and moved zero RX bytes across nine accepted windows afterward). This is the case you are worried about, and you are right that it needs the adapter reset.

The point I would make is that raising the error does not help with case 2. bleak sees the identical InProgress in both, so raising cannot tell them apart: it would fail the common, successful, self-recovering case in order to surface the rare stuck one, and even then the generic InProgress does not tell a user "restart bluetoothd." The stuck state is a bluetoothd bug (it never clears discovering on a rejected stop, bluez/bluez#807), and the real fix is the BlueZ patch I plan to send: clear the belief on REJECTED, which both makes case 1 a non-event and stops case 2 from ever latching. bleak swallowing is the right interim behavior for case 1 today.

If a caller does want to detect the stuck state, the reliable signal is not a single InProgress but a persistent one paired with an empty scan, which is an application-level policy rather than something stop() should decide. Happy to add a logger.warning on the swallow so the condition is at least visible in logs without failing the call, if you would like that.

@dlech

dlech commented Sep 5, 2026

Copy link
Copy Markdown
Collaborator
  1. The kernel stops an LE scan on its own after DISCOV_LE_TIMEOUT (10.24 s).

Does this mean that any scan that a user requests in Bleak longer than this gets cut short and the rest of the time it is not actually scanning?

bleak sees the identical InProgress in both, so raising cannot tell them apart:

Can we look at the Discovering property? Even in cases where there is more than one client that has requested a scan, does this timeout stop them all? If that is so, then we might have an out here.

Happy to add a logger.warning on the swallow so the condition is at least visible in logs without failing the call, if you would like that.

This sounds like the best we can do if the "Discovering" property can't be used. So info is probably good enough. And we can add something to the docs about the known bug in BlueZ.

If the "Discovering" property is false at this point though, we could probably demote it from a warning to a debug since that likely means that the next scan will work. Would still want it to be warning in other case though.

cgoudie added a commit to TechBlueprints/bleak-connection-manager that referenced this pull request Sep 6, 2026
…overy

Prod hci7, 2026-09-04 18:00:42Z to 09-05 15:12Z: bluetoothd believed
discovery was enabled while the kernel was stopped. Every window was
accepted at StartDiscovery, heard nothing, and was rejected at stop; the
controller received zero bytes across nine measured windows; Discovering
read true with zero clients. No trigger saw it for 29 hours - the strike
counter climbed to ~99 with nothing reading it, because e8322ff (rightly)
took recovery off the InProgress path after 08-26 charged healthy cards
for overlapping discoveries.

Sourced in BlueZ 5.72 adapter.c: adapter->discovering is set true only by
client discovery (1873, 1925; passive scanning sets discovery_enable and
never discovering); stop_discovery_complete returns on non-success
without clearing it, and every later start takes update_discovery_filter's
"queue up a stop" branch whose completion is discarded, so no MGMT start
is ever sent again and the property stays true until the adapter object
is reset. On the healthy MGMT-stop path the reply is sent BEFORE the
property change is emitted (2153 vs 2165), so a reader needs a grace
period. Silent peers cannot produce this: after the kernel's own 10.24s
LE timeout, discovery_stop answers the last client from its idle phase
without touching the kernel (192 clean stops on hci7 with the same peers
off, then 99 rejected ones).

So the signal is the PROPERTY, not the exception. After our stop returns
- InProgress raised or swallowed, either way - if the session delivered
no advertisement and Discovering stays true through a 1.5s settle window,
that is the desync: one strike toward recovery under the existing gates
(three over a minute, daemon check, empty card, probation, kill switch).
Read from bleak's BlueZManager property cache (it mirrors every
PropertiesChanged under /org/bluez, Adapter1 included), no bus round
trip; None when there is no manager on the loop, never a guess. A bare
InProgress with Discovering cleared afterwards stays a ranking hint: that
is the one-off #2021 race. A session that heard an advertisement is not
charged whatever the property says: it WAS scanning; the next quiet one
carries it. Independent of hbldh/bleak#2022, which swallows the exception
(bleak-fix, 2026-09-06); BCM needs nothing from bleak for this.

Negative controls: with the check disabled the stuck and the
InProgress-plus-stuck tests fail; with the heard-nothing lock removed the
heard-something test fails. 348 tests.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@cgoudie

cgoudie commented Sep 10, 2026

Copy link
Copy Markdown
Author

Does this mean that any scan that a user requests in Bleak longer than this gets cut short and the rest of the time it is not actually scanning?

Not cut short, but not continuous either. When the kernel's DISCOV_LE_TIMEOUT ends the LE scan it sends MGMT_EV_DISCOVERING with discovering = 0. bluetoothd's discovering_callback handles that: if any discovery clients remain it re-arms the scan with trigger_start_discovery(adapter, IDLE_DISCOV_TIMEOUT), where IDLE_DISCOV_TIMEOUT is 5 seconds (src/adapter.c, unchanged between 5.72 and current master). So a 60 second BleakScanner session on BlueZ is really a train of roughly 10 second scan windows separated by roughly 5 second idle gaps, for as long as at least one client is registered. Advertisements that arrive during a gap are missed, and a device advertising at a slow interval can take a couple of windows to show up, but the session does run for the full duration the caller asked for.

The kernel timeout is per adapter, not per client. All clients share one kernel discovery, so it ends for all of them at once and bluetoothd restarts it for all of them together as long as any remain.

Can we look at the Discovering property?

I checked the path in adapter.c, and it does not separate the two cases at the moment we see the error. stop_discovery_complete sends our InProgress reply first and only clears adapter->discovering and emits the Discovering change on the success path; on the error path it returns before that. discovering_callback, which handles the kernel's discovering = 0 event, updates only the internal discovery_enable flag and never touches adapter->discovering or emits a property change. So after a rejected stop the Discovering property reads true in both the one-off case and the stuck case. That is exactly bluez/bluez#807. A rule of the form "demote to debug when Discovering is false" would therefore never fire.

What does separate them is what happens on the next stop, and that gives us a clean test that needs no property read.

In the one-off case the kernel's discovering = 0 event lands a moment after our rejected stop and discovery_enable drops to 0. The next StartDiscovery with the same filter is accepted but does not reach the kernel, because update_discovery_filter returns early when the filters are equal and adapter->discovering is still set. That scan sees nothing. Its StopDiscovery then takes the idle branch of discovery_stop (discovery_enable == 0), which succeeds, clears discovering and emits the property change, and the scan after that runs normally. So one rejection is always followed by a successful stop, and the cost is one empty scan.

In the stuck case discovery_enable stays 1, so every later start takes the "queue up a stop" branch in start_discovery_timeout whose completion callback is NULL, and every last-client stop is rejected again. No scan reaches the kernel until the adapter is reset.

So two InProgress rejections in a row on one adapter, with no successful stop between them, cannot come from the race and can only mean the stuck state. That is what the PR now does:

  • The first rejection is swallowed, logged at INFO, and remembered per adapter on the BlueZManager.
  • A successful StopDiscovery clears that memory.
  • A second rejection with the memory still set is logged at WARNING, naming the adapter and the remedy (power cycle or re-plug the adapter, or restart bluetoothd), and the original BleakDBusError is raised, so the caller learns the adapter is not scanning rather than getting silent empty scans.

That gives you the split you asked for, INFO for the harmless case and a raised error for the broken one, and keeps your original point that a broken bluetoothd should not be hidden. Two limits, both stated in the new troubleshooting section: the memory is per process, so a process that scans once never escalates; and if another scanner is active on the same adapter, bluetoothd answers our stop without asking the kernel, which counts as a success, so the detection only works while we are the last client. The tests cover the first rejection, the second consecutive one raising with the warning, and a clean stop between two rejections resetting the count.

The one empty scan in the harmless case is bluetoothd's bug and stays. Clearing discovering when the kernel rejects the stop is the fix on that side, and I will send it to linux-bluetooth as a separate patch once this lands.

@dlech dlech left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

This seems like the best we can do given what we know about the bug.

Comment thread bleak/backends/bluezdbus/manager.py Outdated
# then relays MGMT_STATUS_REJECTED as InProgress.
# See https://github.com/hbldh/bleak/issues/2021
# and https://github.com/bluez/bluez/issues/807.
if ex.dbus_error not in (

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

I would change this to a match statement.

Indent is getting pretty deep here too, so maybe put everything in the except in a function to help with readability?

Comment thread CHANGELOG.rst Outdated
Fixed
-----
* Fixed handling empty notification payloads in BlueZ backend when using "AcquireNotify". Fixes #1982.
* Fixed ``BleakScanner.stop()`` raising ``BleakDBusError`` with ``org.bluez.Error.InProgress`` in BlueZ backend when the kernel had already stopped scanning. A single rejection is now logged and tolerated; two in a row on the same adapter still raise, since that means bluetoothd's discovery state is stuck. Fixes #2021.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

I like to keep these short and let people go to the issue for the details.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟡 Changes recommended

The second-response escalation contradicts the stated behavior and can reintroduce the teardown exception.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Pull request overview

Updates BlueZ scan shutdown handling for InProgress responses.

Changes:

  • Tolerates the first InProgress response.
  • Tracks repeated responses and raises on the second.
  • Adds regression tests, diagnostics, and troubleshooting guidance.
File summaries
File Description
bleak/backends/bluezdbus/manager.py Adds tolerance, tracking, and logging.
tests/backends/bluezdbus/test_issue_2021.py Tests shutdown response handling.
docs/troubleshooting.rst Documents symptoms and recovery.
CHANGELOG.rst Records the behavioral change.
Review details
  • Files reviewed: 4/4 changed files
  • Comments generated: 1
  • Review effort level: Balanced

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread bleak/backends/bluezdbus/manager.py Outdated
"adapter, or restart bluetoothd)",
adapter_path,
)
raise
@dlech

dlech commented Sep 13, 2026

Copy link
Copy Markdown
Collaborator

I let Fable review this locally and it thinks the heuristic of failing twice is a bit fragile. And it offered the suggestion of rather trying to start a scan again as the test.

I.e. If we get the InProgress error, call StartDiscovery and StopDiscovery again right then. If that succeeds, then it should be the harmless case and if it fails, we know it is the stuck case.

That eliminates needing to track all of the things that happened between the first and second tries.

@cgoudie
cgoudie force-pushed the fix/stopdiscovery-inprogress branch from e6392d1 to 8cf1896 Compare September 28, 2026 00:04
cgoudie added a commit to TechBlueprints/bleak that referenced this pull request Sep 28, 2026
BlueZ answers StopDiscovery with InProgress when the kernel has already
stopped scanning on its own. bluetoothd has removed our discovery session
by the time it replies, so the stop has taken effect; swallow the error
like NotReady. Squash of the review history of hbldh#2022, rebased
onto develop. Fixes hbldh#2021.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
cgoudie added a commit to TechBlueprints/bleak that referenced this pull request Sep 28, 2026
…nting strikes

Review feedback on hbldh#2022. An InProgress reply to StopDiscovery
is usually a race with the kernel's own scan timeout, but bluetoothd can
also be stuck with every stop rejected. Rather than remembering the last
outcome per adapter, start and stop a probe scan right away: bluetoothd
answers the probe's stop from its idle state in the harmless case and the
kernel rejects it again in the stuck case. The harmless case is logged at
INFO and the filters are cleared as usual; the stuck case logs a WARNING
with the remedy and raises the original error.

Also moves the stop path into helper methods with a match statement, as
requested, and drops the per-manager marker that would have outlived an
adapter re-plug.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@cgoudie

cgoudie commented Sep 28, 2026

Copy link
Copy Markdown
Author

Done as suggested. The branch is rebased onto develop (it had drifted into conflict) and squashed to two commits: the original fix, and the probe.

On InProgress the stop path now runs SetDiscoveryFilter with the session's own filters, StartDiscovery, StopDiscovery on the same adapter, right then, under the same bus lock. If the probe's stop succeeds the original stop is treated as complete, logged at INFO, and the filters are cleared as usual. If the probe's stop is rejected too, a WARNING names the adapter and the remedy and the original BleakDBusError is raised. No state is carried between calls, so the per-manager marker that Copilot pointed out could outlive an adapter re-plug is gone.

Why the probe is a clean discriminator, from src/adapter.c: the kernel's discovering = 0 event is on the mgmt socket ahead of the rejected reply to our stop, so by the time bleak sees InProgress in the harmless case bluetoothd has already dropped discovery_enable to 0 and discovery_stop answers the probe's stop from its idle branch without asking the kernel. In the stuck case discovery_enable is still 1, the stop goes to the kernel, and the kernel rejects it again. Both branches were checked against 5.72 and current master; the relevant functions are unchanged between them.

Other review points:

  • match statement and the except body moved into helpers (_stop_discovery, _probe_stuck_discovery, with a small _call_adapter), which also flattened the stop closure to one line.
  • Changelog line shortened to the one sentence plus the issue number.
  • Tests: probe stops cleanly (asserts the exact call sequence including the filter reset), probe rejected too (raises the original error with one WARNING and no filter reset), NotReady still swallowed without probing, other errors still raise.
  • Troubleshooting section updated to describe the probe and the reset remedy.

@dlech

dlech commented Sep 28, 2026

Copy link
Copy Markdown
Collaborator

Did you see #2033? I prefer the way the tests there use the same "fake" BlueZ D-bus service rather than monkey-patching _bus.

I didn't compare the way we implemented the actual code fix yet though.

dlech and others added 2 commits October 10, 2026 11:10
Move the private dbus-daemon and fake BlueZ objects from
test_manager_real_dbus.py to a shared fake_bluez module and the
start_daemon fixture to conftest.py so that other tests can use them.

The fake adapter also gets a Roles property and scriptable
SetDiscoveryFilter, StartDiscovery and StopDiscovery methods so that
scanning can be tested through the public BleakScanner API.
BlueZ answers StopDiscovery with InProgress when the kernel has already
stopped scanning on its own. bluetoothd has removed our discovery session
by then, but it leaves its own discovering flag set, so the next scan on
the adapter would be accepted without a kernel scan and find nothing
(bluez/bluez#807). Start and stop discovery once more right away: that
clears the flag in the harmless case and the stop then completes
normally. If the second stop is rejected too, bluetoothd has lost track
of the kernel's scan state for good; log a warning naming the remedy and
raise the original error so the caller knows the adapter is not scanning.
If the extra StartDiscovery fails, warn and return, since scanning is
stopped as requested. NotReady is still tolerated without probing.

Tests run BleakScanner against the fake BlueZ service on a private
dbus-daemon from the shared fixture. Fixes hbldh#2021.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@cgoudie
cgoudie force-pushed the fix/stopdiscovery-inprogress branch from 8cf1896 to 7b53226 Compare October 10, 2026 17:14
@cgoudie

cgoudie commented Oct 10, 2026

Copy link
Copy Markdown
Author

Yes, seen now, thanks. I have rebuilt this branch on top of your fixture commit from #2033 (it is the first commit here, unchanged) and the tests now run a real BleakScanner against the fake BlueZ on the private dbus-daemon, with StopDiscovery and StartDiscovery answered from the fake adapter's scripted reply lists. No more monkey-patching of _bus.

While I was at it I compared the two code versions and took your two refinements, since your reasoning on both is right: the extra StartDiscovery is sent without re-sending the filter (our filter is still registered, so bluetoothd answers it without kernel traffic, and clearing it first would start a real unfiltered discovery), and a failed extra StartDiscovery now warns and returns instead of raising from a call the user did not make. What is left of the original shape is the match and the helpers you asked for, and INFO rather than DEBUG for the recovered case, since you had said info was good enough; happy to drop that to DEBUG if you prefer. The troubleshooting section is rewritten to match, and the one-empty-scan caveat is gone since the second start and stop resets the flag.

Tests: five cases, all green against dbus-daemon locally and on the earlier CI runs' Linux matrix. One real-hardware data point too: on a Cerbo GX running BlueZ 5.72, a 10.3 s scan window hit the race, the second start and stop cleared it, and the scanner's stop returned normally with the INFO line.

Branch is rebased on current develop. If you would rather land #2033 and close this one, that is fine by me too; the two are now the same change with the same tests, and I am happy either way.

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.

BlueZ: BleakScanner.stop() raises org.bluez.Error.InProgress after the session is already gone; treat it like NotReady

3 participants