From 5b70d460022a3888d207cca75212c84c4ac1ef56 Mon Sep 17 00:00:00 2001 From: Zach Vorhies Date: Mon, 7 Sep 2026 00:40:07 -0700 Subject: [PATCH 1/2] 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 34298416..8560f1ab 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) => ( From e673caf9e131fe2d23a609777775e8d4c71bdd8e Mon Sep 17 00:00:00 2001 From: Zach Vorhies Date: Fri, 18 Sep 2026 15:10:32 -0700 Subject: [PATCH 2/2] fix(daemon): release the serial session when post-deploy attach fails Review follow-up: open_port creates the session before attach_reader; a close/open race can drop its broadcaster so attach_reader returns None, and that arm returned without cleanup -- the same leaked handle and EBUSY this PR fixes on the normal path. Detach and close-after-grace there too. Co-Authored-By: Claude Opus 5 (1M context) --- .../fbuild-daemon/src/handlers/operations/deploy.rs | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/crates/fbuild-daemon/src/handlers/operations/deploy.rs b/crates/fbuild-daemon/src/handlers/operations/deploy.rs index 8560f1ab..b19638ff 100644 --- a/crates/fbuild-daemon/src/handlers/operations/deploy.rs +++ b/crates/fbuild-daemon/src/handlers/operations/deploy.rs @@ -1182,6 +1182,18 @@ pub async fn deploy( { Some(rx) => rx, None => { + // open_port created the session above; a close/open race can + // drop its broadcaster before attach_reader runs. Release the + // handle the same way the normal path below does, or it stays + // open and the next client gets EBUSY (FastLED/fbuild#1426). + ctx.serial_manager.detach_reader(&monitor_port, &request_id); + 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 ( StatusCode::OK, Json(OperationResponse {