This commit is contained in:
Ayzen
2026-06-13 12:07:23 +03:00
parent f0d095de80
commit e7f2d25585
20 changed files with 903 additions and 148 deletions
@@ -130,7 +130,7 @@ class USBTransport:
raise DeviceDisconnectedError(f"Failed to prepare USB kernel driver state: {exc}") from exc
try:
selected_handle.claimInterface(self.INTERFACE)
self._claim_interface_with_busy_recovery(selected_handle)
except usb1.USBError as exc:
selected_handle.close()
if self._ctx is not None:
@@ -152,6 +152,29 @@ class USBTransport:
self._rx_thread = threading.Thread(target=self._rx_loop, name="librevna-usb-rx", daemon=True)
self._rx_thread.start()
def _claim_interface_with_busy_recovery(self, handle: "usb1.USBDeviceHandle") -> None: # type: ignore[name-defined]
"""Claim the data interface, clearing a stale BUSY claim once if needed.
When a previous session's RX thread wedged inside libusb, ``disconnect()``
deliberately leaks that handle with the interface still claimed to avoid a
use-after-free. A fresh ``connect()`` then fails with
``LIBUSB_ERROR_BUSY``. Resetting the device clears the orphaned claim so
the retry succeeds, instead of the device being unusable until it is
physically replugged. If the reset forces a re-enumeration the retry raises
and the caller falls back to its normal reopen backoff.
"""
try:
handle.claimInterface(self.INTERFACE)
return
except usb1.USBErrorBusy as exc:
logger.warning(
"USB interface %d busy on claim; resetting device to clear a stale claim: %s",
self.INTERFACE,
exc,
)
handle.resetDevice()
handle.claimInterface(self.INTERFACE)
def disconnect(self) -> None:
"""Stop RX thread and close USB resources."""
logger.debug("USB disconnect requested")
@@ -234,8 +257,8 @@ class USBTransport:
continue
except usb1.USBErrorNoDevice as exc:
if self._on_disconnect is not None:
self._on_disconnect(DeviceDisconnectedError("USB device disconnected"))
logger.warning("USB RX stopped: device disconnected")
self._on_disconnect(DeviceDisconnectedError(f"USB device disconnected: {exc}"))
logger.warning("USB RX stopped: device disconnected: %s", exc)
return
except usb1.USBError as exc:
if self._stop_event.is_set():