Host._send_command does not release command_semaphore on timeout, causing subsequent commands to hang
- Dominant language
- Python
- Stars
- 555
- Forks
- 138
- Avg merge
- 3d 15h
- Merged PRs (30d)
- 9
Description
When Host._send_command times out waiting for an HCI response, the command_semaphore is never released. Every subsequent call to _send_command then blocks indefinitely at await self.command_semaphore.acquire(), making the host permanently unresponsive with no error or exception.
The release in the finally block is gated on response is not None. On a timeout, response remains None, so the semaphore is never released:
```
response = None
try:
...
response = await asyncio.wait_for(self.pending_response, timeout=response_timeout)
return response
finally:
if (
response is not None # <-- None on timeout
and response.num_hci_command_packets
and self.command_semaphore.locked()
):
self.command_semaphore.release() # <-- never reached
```
I'm wondering if this is intentional, to perhaps indicate that the controller is unresponsive/pending? The controller I'm using definitely isn't the most reliable (Raspberry Pi 3B+), but I'm not sure how to detect / recover from this, other than wrapping all bumble calls in a timeout.
There was about 3 minutes between when I issued the command that timed out (`!!! Command HCI_LE_CLEAR_FILTER_ACCEPT_LIST_COMMAND timed out`) to when I tried issuing the next command.
I haven't been able to capture any proper logs of this happening on real hardware, but this script reproduces what I've seen (reproduction code generated with Claude).
```
import asyncio
from bumble import hci
from bumble.controller import Controller
from bumble.device import Device
from bumble.host import Host
from bumble.link import LocalLink
from bumble.transport.common import AsyncPipeSink
def make_device() -> Device:
link = LocalLink()
controller = Controller("test-controller", link=link)
device = Device(host=Host())
device.host.set_packet_sink(controller)
controller.host = AsyncPipeSink(device.host)
return device
async def main():
device = make_device()
await device.power_on()
print("Step 1: Send a command with a near-zero timeout to force a timeout...")
try:
await device.host._send_command(
hci.HCI_LE_Clear_Filter_Accept_List_Command(),
response_timeout=0.00001,
)
except TimeoutError:
print(" -> Timed out as expected.")
print("Step 2: Send a normal command — hangs forever if semaphore was leaked...")
try:
await asyncio.wait_for(
device.host._send_command(
hci.HCI_LE_Clear_Filter_Accept_List_Command(),
),
timeout=3.0,
)
print(" -> Completed (bug is fixed).")
except asyncio.TimeoutError:
print(
" -> HUNG for 3 seconds and timed out: semaphore was leaked. Bug confirmed."
)
asyncio.run(main())
```
Contributor guide
Assessment
This issue has not been assessed yet.