Skip to content

Fix serial device disconnects: concurrent, exactly-once URB handling#15

Open
kkkkk1k1 wants to merge 1 commit into
abrinkman:mainfrom
kkkkk1k1:fix/async-urb-exactly-once
Open

Fix serial device disconnects: concurrent, exactly-once URB handling#15
kkkkk1k1 wants to merge 1 commit into
abrinkman:mainfrom
kkkkk1k1:fix/async-urb-exactly-once

Conversation

@kkkkk1k1

@kkkkk1k1 kkkkk1k1 commented Jul 9, 2026

Copy link
Copy Markdown

Problem

Sharing a USB serial device (CDC-ACM, CH34x, …) over usbipd-python did not work reliably: the client-side port either failed to open (EIO) or dropped mid-transfer. Root causes in the URB traffic loop:

  1. IN-endpoint timeouts were reported as errors. _do_bulk_interrupt_transfer returned ETIMEDOUT (-110) whenever an IN endpoint had no data ready. On real hardware an IN endpoint with nothing to send just NAKs and the URB stays pending. Returning -110 poisoned the CH340 interrupt status endpoint and made the ch341/cdc_acm open() fail:
    usb x-y: failed to receive control message: -110
    ch341-uart ttyUSB0: failed to read modem status: -110
    
  2. Head-of-line blocking. URBs were processed sequentially, so one blocking IN read stalled the control/OUT transfers that open() depends on.
  3. Double completion → connection abort. Under load (e.g. flashing) the client unlinks slow URBs. The worker still sent a RET_SUBMIT for an already-unlinked seqnum, so vhci_hcd aborted the whole connection and dropped the port:
    vhci_hcd: cannot find a urb of seqnum 167493 max seqnum 167495
    vhci_hcd: stop threads / release socket / disconnect device
    

Change

Rework _handle_urb_traffic so the receive thread only does socket I/O and dispatches each URB to a worker thread:

  • IN endpoints stay pending (polled in coarse slices) and complete only when data arrives or the URB is unlinked — no more spurious -110.
  • Per-endpoint locks: same-endpoint transfers stay ordered (serial bytes are never reordered) while different endpoints run concurrently, so a pending IN read never blocks control/OUT.
  • Exactly-once completion (per-URB lock + completed flag): a URB is finished by either RET_SUBMIT or RET_UNLINK, never both — fixing the fatal RET_UNLINK-before-RET_SUBMIT ordering.
  • Socket writes are serialised; USBIP_CMD_UNLINK now actually cancels the pending URB.

Tests

Adds tests/test_urb_handling.py — mocked USB device + scripted in-memory socket (no libusb, no hardware, no sudo), covering:

  • a pending IN read does not block OUT / echo (loopback),
  • an unlinked pending IN read emits no spurious RET_SUBMIT,
  • exactly-once completion under a 200× UNLINK-vs-completion race.

Also adds pytest to the dev extra and a [tool.pytest.ini_options] section.

Validation

  • ruff format --check ., ruff check ., mypy --ignore-missing-imports ., reuse lint, pytest all pass.
  • Verified on real hardware: CH340 (ttyUSB0) and CH343/CDC-ACM (ttyACM0) shared from macOS to a Linux client — serial loopback is byte-accurate and firmware flashing (cskburn) completes without the port dropping.

🤖 Generated with Claude Code

The URB traffic loop processed submissions sequentially and returned
ETIMEDOUT (-110) whenever an IN endpoint had no data ready. For serial
devices (CDC-ACM, CH34x, ...) this broke the whole chain:

- Interrupt/bulk IN endpoints that legitimately have no data pending were
  completed with -110 instead of staying pending like real hardware, which
  made the CH340 modem-status read fail and ttyUSB/ttyACM open() return EIO.
- A single blocking IN read head-of-line blocked control and OUT transfers,
  so open() timed out.
- During flashing the client frequently unlinks slow URBs; the worker still
  sent a RET_SUBMIT for an already-unlinked seqnum, which makes the Linux
  vhci_hcd abort the connection ("cannot find a urb of seqnum ...") and drop
  the serial port.

Rework the loop so the receive thread only does socket I/O and dispatches
each URB to a worker thread:

- IN endpoints stay pending (polled in coarse slices) and complete only when
  data arrives or the URB is unlinked - no more spurious -110.
- Per-endpoint locks keep same-endpoint transfers ordered while different
  endpoints run concurrently, so a pending IN read never stalls control/OUT.
- Exactly-once completion (per-URB lock + flag) guarantees a URB is finished
  by either RET_SUBMIT or RET_UNLINK, never both, fixing the fatal
  RET_UNLINK-before-RET_SUBMIT ordering.
- Socket writes are serialised; USBIP_CMD_UNLINK now actually cancels the
  pending URB.

Add mocked pytest regression tests (no hardware) covering concurrency,
pending-IN unlink, and the exactly-once unlink race.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
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