From 2f9594d6a1d979a99442c33ec3c114aa3375ed13 Mon Sep 17 00:00:00 2001 From: greateggsgreg <36009512+greateggsgreg@users.noreply.github.com> Date: Thu, 16 Jul 2026 23:42:51 -0400 Subject: [PATCH 1/2] Update usb.py --- bumble/transport/usb.py | 30 +++++++++++++++++++++--------- 1 file changed, 21 insertions(+), 9 deletions(-) diff --git a/bumble/transport/usb.py b/bumble/transport/usb.py index 70e5b8cb..93b5801d 100644 --- a/bumble/transport/usb.py +++ b/bumble/transport/usb.py @@ -414,10 +414,10 @@ async def terminate(self): except usb1.USBError as error: logger.debug(f'OUT transfer likely already completed ({error})') - try: - transfer.close() - except usb1.USBError as error: - logger.warning(f'failed to close transfer ({error})') + # Do not close the transfer objects here. The callbacks that wake this + # coroutine run on the libusb event thread and may not have returned yet. + # UsbTransport.close() stops and joins that thread before device.close() + # finalizes the transfers. READ_SIZE = 4096 @@ -669,6 +669,7 @@ def __init__(self, context, device, acl_interface, sco_interface, source, sink): self.event_loop_done = self.loop.create_future() self.event_loop_should_exit = False self.lock = threading.Lock() + self.closed = False # Get exclusive access device.claimInterface(acl_interface.getNumber()) @@ -721,6 +722,12 @@ def run(self): _safe_call_soon(self.loop, self.event_loop_done.set_result, None) async def close(self): + # close() may be reached both explicitly and through Transport.__aexit__. + # Guard the native resources from a second teardown. + if self.closed: + return + self.closed = True + self.source.close() self.sink.close() await self.source.terminate() @@ -734,17 +741,22 @@ async def close(self): except (AttributeError, usb1.USBError) as error: logger.warning(f"Failed to interrupt event handler: {error}") + # handleEvents() owns libusb state until the event thread has returned. + # In particular, transfer cancellation callbacks only schedule their + # asyncio completion signals; those signals do not prove that the native + # callback has returned. Wait and join before freeing transfers, the + # device handle, or the context. + logger.debug("waiting for USB event loop to be done...") + await self.event_loop_done + self.event_thread.join() + logger.debug("USB event loop done") + self.device.releaseInterface(self.acl_interface.getNumber()) if self.sco_interface: self.device.releaseInterface(self.sco_interface.getNumber()) self.device.close() self.context.close() - # Wait for the thread to terminate - logger.debug("waiting for USB event loop to be done...") - await self.event_loop_done - logger.debug("USB event loop done") - async def open_usb_transport(spec: str) -> Transport: ''' From e72ad8618bd5fdc11d258fb7f647929574097668 Mon Sep 17 00:00:00 2001 From: greateggsgreg <36009512+greateggsgreg@users.noreply.github.com> Date: Wed, 22 Jul 2026 21:26:43 -0400 Subject: [PATCH 2/2] Update usb.py --- bumble/transport/usb.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/bumble/transport/usb.py b/bumble/transport/usb.py index 93b5801d..2dcb48aa 100644 --- a/bumble/transport/usb.py +++ b/bumble/transport/usb.py @@ -665,8 +665,6 @@ def __init__(self, context, device, acl_interface, sco_interface, source, sink): self.device = device self.acl_interface = acl_interface self.sco_interface = sco_interface - self.loop = asyncio.get_running_loop() - self.event_loop_done = self.loop.create_future() self.event_loop_should_exit = False self.lock = threading.Lock() self.closed = False @@ -719,7 +717,6 @@ def run(self): logger.warning(f'!!! Exception while handling events: {error}') logger.debug('ending USB event loop') - _safe_call_soon(self.loop, self.event_loop_done.set_result, None) async def close(self): # close() may be reached both explicitly and through Transport.__aexit__. @@ -747,8 +744,7 @@ async def close(self): # callback has returned. Wait and join before freeing transfers, the # device handle, or the context. logger.debug("waiting for USB event loop to be done...") - await self.event_loop_done - self.event_thread.join() + await asyncio.to_thread(self.event_thread.join) logger.debug("USB event loop done") self.device.releaseInterface(self.acl_interface.getNumber())