Skip to content

daemon retains an exclusive serial handle after deploy; next open gets EBUSY and is reported as "serial driver may be wedged" #1429

Description

@zackees

Summary

fbuild-daemon keeps an exclusive handle on a device's serial port after the operation that opened it. Every subsequent open() on that port fails with EBUSY, and the failure is surfaced as:

attach failed: open_port(/dev/ttyACM2) exceeded 3s; serial driver may be wedged

The driver is not wedged and the device is not wedged. The port is simply held by the daemon.

Proof

Measured on a Linux bench with an RP2350W and an ESP32-C6 attached, during FastLED's two-device --net-peer --ota autoresearch flow.

1. The holder is identified. Scanning /proc/*/fd for the port, while no run is active:

pid=1427887 fbuild-daemon holds: ttyACM2

Note it holds ttyACM2 (the companion) and not ttyACM0 (the primary).

2. The failure is EBUSY, not a driver fault. A plain os.open() bypassing all tooling:

plain open(): OSError: [Errno 16] Device or resource busy: '/dev/ttyACM2'

3. The device is healthy throughout. Still enumerated, same node:

C6 303a:1001 devnum=20 tty=ttyACM2

4. Removing the holder fixes it immediately. Killing the process that holds the fd:

holders: none
plain open(): SUCCESS

That is a complete causal chain: holder present → EBUSY → attach fails; holder gone → open succeeds.

5. A single-device flow on the same port passes. fbuild-backed autoresearch against the C6 alone, which also flashes it and attaches immediately afterwards:

RESULT: RPC smoke PASS (discovery, help, ping, payload, status, drivers, testNoSerial, error handling)

So neither the board, its USB-CDC, nor the port is at fault. The failure needs a second serial interface on the same port.

Why it is always the second device

In the caller, the primary reuses the interface built during its deploy, while the companion constructs a fresh one:

primary = RpcClient(upload_port, serial_interface=serial_iface)              # reused
peer    = RpcClient(peer_upload_port,
                    serial_interface=create_serial_interface(peer_upload_port))  # fresh

A fresh interface means a fresh monitor, which contends with the handle the daemon still holds from that device's deploy. The primary never contends because it never opens a second one. Across every captured occurrence on this bench the failure is on the companion, never the primary.

Retrying makes it worse rather than better, because closing the client nulls its interface and the next attempt constructs another one:

ESP32-C6 did not answer on /dev/ttyACM2 (attempt 1/3: No response with ID 1 within 10.0s); reconnecting
ESP32-C6 did not answer on /dev/ttyACM2 (attempt 2/3: No response with ID 2 within 10.0s); reconnecting
attach failed: open_port(/dev/ttyACM2) exceeded 3s; serial driver may be wedged

Attempts 1–2 reach a session that holds the port but answers nothing; by attempt 3 the port cannot be opened at all. Backing off further (2 s → 4 s → 6 s) does not recover it.

Requests

  1. Release the port when an operation completes. A deploy or monitor that has finished should not retain an exclusive fd. Release was observed within ~5 s in one instance and not at all in others, so it is not reliable today.
  2. Report EBUSY as contention, naming the holder. serial driver may be wedged sent this investigation to the device and the USB layer for a long time before /proc/*/fd showed a plain lock. Something like port held by fbuild-daemon (pid N) would have made it immediate.
  3. Make re-attaching to a port the daemon already owns non-destructive — either hand back the existing session or release and re-acquire cleanly, so a caller opening a second interface does not deadlock against the first.

Context

🤖 Generated with Claude Code

https://claude.ai/code/session_01KkufoNxfnNRU9psT3R9F51

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions