Skip to content

fix(corebluetooth): Stop the CoreBluetooth thread once its adapter and peripherals are dropped - #508

Open
HermannBjorgvin wants to merge 1 commit into
deviceplug:devfrom
HermannBjorgvin:fix/corebluetooth-thread-exit
Open

HermannBjorgvin wants to merge 1 commit into
deviceplug:devfrom
HermannBjorgvin:fix/corebluetooth-thread-exit

Conversation

@HermannBjorgvin

Copy link
Copy Markdown

Fixes #500.

Not run on macOS or iOS. I don't have a Mac, so this is only compiled. It needs a test on a Mac before merging.

Each Adapter::new(), one per Manager::adapters() call, starts a thread that loops on wait_for_message() forever, so every call leaks a thread and its CBCentralManager. The thread can't stop when its message channel closes, because the adapter's event task and peripheral map hold senders for as long as the thread runs.

  • KeepAlive (src/corebluetooth/adapter.rs) is the sender of a oneshot channel, shared through an Arc. The Adapter holds one. So do every Peripheral returned by peripherals(), peripheral() and retrieve_peripherals() (Peripheral::with_keep_alive in src/corebluetooth/peripheral.rs), and the streams from events() and notifications() (keep_alive_stream). The adapter's internal copies of peripherals don't hold one.
  • run_corebluetooth_thread (src/corebluetooth/internal.rs) polls the oneshot receiver before wait_for_message(), so a busy delegate channel can't starve it, and stops once the last KeepAlive is dropped. CoreBluetoothInternal, with its CBCentralManager, is dropped on that thread.
  • A peripheral or stream the application still holds keeps the thread running, so it keeps working after its Adapter is dropped, as before.

Not covered: CoreBluetoothInternal::new creates a dispatch queue that is never released, so each adapter still leaks that queue object (not a thread).

To test on a Mac: call Manager::adapters() in a loop, drop the adapters, and watch the thread count. The integration tests against the test peripheral would also cover the keep-alive paths.

Agent review

Before opening, separate AI agents (Claude Opus) reviewed the plan and the diff. Their findings are addressed here, except the dispatch queue leak above, which predates this change.

Human involvement

Agent-written (Claude), from reading the code after we found that Clawdmeter's desktop app calls Manager::adapters() on every reconnect attempt. I haven't run it on a Mac or reviewed the diff.

…d peripherals are dropped

Each Adapter started a thread that looped forever, so every
Manager::adapters() call leaked a thread and its CBCentralManager (deviceplug#500).
The message channel can't signal the end, because the adapter's event
task and peripheral map hold senders for as long as the thread runs.

The adapter, every peripheral it hands out and the event and
notification streams from them now hold a shared KeepAlive (the sender
of a oneshot channel). The thread stops when the last of them is
dropped. The adapter's internal copies of peripherals don't hold one,
so they don't keep the thread alive.
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.

1 participant