From fc4f0420f38d85e5ca5038ce3218b0d369784405 Mon Sep 17 00:00:00 2001 From: Zach Vorhies Date: Mon, 7 Sep 2026 00:40:07 -0700 Subject: [PATCH] fix(daemon): close the serial port after deploy's post-flash monitor (#1426) `detach_reader()` only drops the client from `reader_client_ids`; it does not touch `serial_handle`. Both other detach sites follow it with `close_port_after_grace_if_idle` to release the OS handle: - handlers/operations/monitor.rs:361 - handlers/websockets.rs:101 The deploy path's post-flash monitor did not, so after every deploy the `SerialSession` and its file descriptor stayed open for the life of the daemon. The next client then got EBUSY on a board that was enumerated and perfectly healthy. Observed on a FastLED bench: four distinct fbuild-daemon pids each held /dev/ttyACM0 across one session, every one of them after a deploy. `open()` took 13.3s while held and 0.00s once the daemon was killed. It reads as a wedged device -- the error even says "serial driver may be wedged" -- and `lsof` does not show it, because the fd lives in the daemon rather than in the CLI process that appears to be at fault. It also broke a real test path: FastLED's `autoresearch rp2350w --net-peer --ota` failed at its first RPC because the companion's port was still held by that same run's deploy. Plain `--net-peer` passes, and the difference is deploy duration -- 96.5s there versus 25.7s in OTA mode -- so the shorter deploy loses the race against the leaked handle. That intermittency is what made it look like an OTA-logic fault. Mirrors the monitor.rs cleanup exactly: same `has_clients` guard, same 2s grace, so a close -> immediate reconnect pattern still does not thrash the USB CDC handle. cargo check -p fbuild-daemon --all-targets: clean. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01KkufoNxfnNRU9psT3R9F51 --- .../fbuild-daemon/src/handlers/operations/deploy.rs | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/crates/fbuild-daemon/src/handlers/operations/deploy.rs b/crates/fbuild-daemon/src/handlers/operations/deploy.rs index 0ce7d646..984a5d1d 100644 --- a/crates/fbuild-daemon/src/handlers/operations/deploy.rs +++ b/crates/fbuild-daemon/src/handlers/operations/deploy.rs @@ -1228,6 +1228,19 @@ pub async fn deploy( }; ctx.serial_manager.detach_reader(&monitor_port, &request_id); + // Detaching the reader alone leaves the session -- and its OS serial + // handle -- open forever, so the next client gets EBUSY on a board + // that is enumerated and perfectly healthy. The monitor and WebSocket + // cleanup paths both schedule the physical close here; this one did + // not, which is why the leak only showed up after a deploy. + // See FastLED/fbuild#1426. + if !ctx.serial_manager.has_clients(&monitor_port) { + ctx.serial_manager.close_port_after_grace_if_idle( + &monitor_port, + &request_id, + std::time::Duration::from_secs(2), + ); + } return match monitor_result { MonitorOutcome::Success(msg) => (