Skip to content

fix(transport): close all BLE notification-watcher leak paths#138

Merged
g4bri3lDev merged 1 commit into
OpenDisplay:mainfrom
davelee98:fix/ble-notification-watcher-leak
Jul 17, 2026
Merged

fix(transport): close all BLE notification-watcher leak paths#138
g4bri3lDev merged 1 commit into
OpenDisplay:mainfrom
davelee98:fix/ble-notification-watcher-leak

Conversation

@davelee98

Copy link
Copy Markdown
Contributor

What

Close three BLE notification-watcher leak paths on the BlueZ backend. A watcher registered by start_notify() is only released on disconnect; any path that reaches a connected+subscribed client without a guaranteed disconnect leaks the watcher, which then delivers stray/duplicate notifications to a later connection (observed as a monotonically rising BlueZ device-watcher count across failed probes, and duplicate rx on a live connection).

The three paths

  1. OpenDisplayDevice.__aenter__ ran authenticate()/interrogate() after connect() with no guard. Any raise there — AuthenticationRequiredError on a keyless probe, or CancelledError from an outer asyncio.timeout() (e.g. an HA config-flow probe) — propagates out of __aenter__, so __aexit__ never runs and the live connection leaks. Now wrapped: on BaseException we disconnect and re-raise (BaseException, not Exception, because CancelledError is not an Exception).

  2. _clear_cache_and_drop() and 3. disconnect() nulled the client even when disconnect() raised, stranding the watcher. Added _stop_notifications() (best-effort, never raises) and call it before every disconnect, so the watcher is released even if the subsequent ACL disconnect is flaky; _notification_characteristic is reset alongside _client.

Tests

Adds tests/unit/test_connection_watcher_leak.py — one regression test per leak path (6 cases total).

Notes

  • BlueZ-focused; the ESP32/proxy backend is unaffected.
  • Logging uses the module _LOGGER at existing levels — no new instance state.

A BlueZ notification watcher registered by start_notify() is only
released on disconnect. Three paths could leave a connected+subscribed
client without a guaranteed disconnect, leaking a watcher that then
delivers stray/duplicate frames to later connections (observed as a
monotonically rising "BlueZ device watcher count" across failed probes,
and duplicate rx on a live connection while a prior probe's watcher
lingered).

Path 1 — OpenDisplayDevice.__aenter__ ran authenticate/interrogate after
connect() with no guard. Any raise there (AuthenticationRequiredError on
a keyless probe, or CancelledError from an outer asyncio.timeout in the
HA config-flow probe) propagates out of __aenter__, so __aexit__ never
runs and the live connection leaks. Wrap the post-connect steps and
disconnect() on BaseException (CancelledError is not an Exception).

Paths 2 & 3 — _clear_cache_and_drop() and disconnect() null the client
even when disconnect() raised, stranding the watcher. Add
_stop_notifications() and call it before every disconnect so the watcher
is released even if the subsequent ACL disconnect is flaky; reset
_notification_characteristic alongside _client.

Adds regression tests for all three paths.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@davelee98
davelee98 requested a review from g4bri3lDev as a code owner July 17, 2026 02:37
@codecov

codecov Bot commented Jul 17, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 85.18519% with 4 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
src/opendisplay/device.py 85.71% 2 Missing ⚠️
src/opendisplay/transport/connection.py 84.61% 2 Missing ⚠️

📢 Thoughts on this report? Let us know!

@davelee98

Copy link
Copy Markdown
Contributor Author

FYI @jonasniesner this fixes the encryption bug with the Solum tags. It is due to "watcher" leaks due to BLE connections not properly unwinding themselves on error

@g4bri3lDev
g4bri3lDev merged commit 3f878fc into OpenDisplay:main Jul 17, 2026
6 checks passed
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.

2 participants