fix(daemon): tear down serial session on ws early-return (#51)#60
fix(daemon): tear down serial session on ws early-return (#51)#60
Conversation
…rn (#51) Root cause of fbuild-daemon staying resident after autoresearch ends: in `handle_serial_ws`, two error paths after `open_port` succeeded returned without running the cleanup block at the end of the function: 1. `attach_reader` returning `None` (port not open) 2. `socket.send(Attached)` failing Both left a `SerialSession` entry in `SharedSerialManager.sessions` indefinitely. `has_clients()` then returned `true` forever — because `attach_reader` also mutated `session.reader_client_ids` even on the `None`-return path — so the daemon's self-eviction loop never fired. Fix: - Extract the session teardown into `cleanup_ws_serial_session()` and invoke it before both early returns. - Make `attach_reader` all-or-nothing: return `None` without mutating `reader_client_ids` when the broadcaster is missing, so callers that fail to attach don't leak a reader id. - Regression test in fbuild-serial confirms the all-or-nothing contract. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
📝 WalkthroughWalkthroughRefactored WebSocket serial-session cleanup into a reusable helper function and updated Changes
Estimated code review effort🎯 2 (Simple) | ⏱️ ~12 minutes Poem
🚥 Pre-merge checks | ✅ 3✅ Passed checks (3 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
🧹 Nitpick comments (1)
crates/fbuild-serial/src/manager.rs (1)
560-605: Good regression coverage for the#51contract.The test directly asserts the all-or-nothing invariant by constructing the exact pathological state (session present, broadcaster absent) that produced the leak. The assertion messages document the regression clearly.
Optional nit: constructing
SerialSessioninline with all fields makes this test brittle to unrelated field additions inSerialSession. Consider aSerialSession::new(port, baud)constructor (which already exists per Line 224) if it exposes enough surface for the test, to keep the test tethered to behavior rather than struct shape.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@crates/fbuild-serial/src/manager.rs` around lines 560 - 605, The test builds a full SerialSession struct inline which is brittle; replace the inline construction in attach_reader_missing_broadcaster_does_not_mutate_session_state with SerialSession::new(port, 115200) (the existing constructor around Line 224) and then mutate only the necessary fields (e.g., ensure is_open=false, writer_client_id=None, reader_client_ids empty, stop_flag set) before inserting into SharedSerialManager::sessions so the test checks behavior without depending on every SerialSession field layout; keep the call to mgr.attach_reader(port, client) and the subsequent asserts unchanged.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Nitpick comments:
In `@crates/fbuild-serial/src/manager.rs`:
- Around line 560-605: The test builds a full SerialSession struct inline which
is brittle; replace the inline construction in
attach_reader_missing_broadcaster_does_not_mutate_session_state with
SerialSession::new(port, 115200) (the existing constructor around Line 224) and
then mutate only the necessary fields (e.g., ensure is_open=false,
writer_client_id=None, reader_client_ids empty, stop_flag set) before inserting
into SharedSerialManager::sessions so the test checks behavior without depending
on every SerialSession field layout; keep the call to mgr.attach_reader(port,
client) and the subsequent asserts unchanged.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: d37b35a3-1743-464c-b74c-4c5f935090b2
📒 Files selected for processing (2)
crates/fbuild-daemon/src/handlers/websockets.rscrates/fbuild-serial/src/manager.rs
Summary
Root-cause fix for #51 — the daemon was staying resident after autoresearch ended because
handle_serial_wsleaked serial-session state on two failure paths afteropen_porthad already created it.Specifically:
open_portsucceeds, ifattach_readerreturnsNoneorsocket.send(Attached)fails, the handler returned without running the cleanup block at the end. TheSerialSessionstays inSharedSerialManager.sessionsforever.attach_readeradditionally mutatedsession.reader_client_idseven on theNone-return path, sohas_clients()stayedtrueforever and the self-eviction loop never fired.Changes
cleanup_ws_serial_session()(detach reader, release writer, close port if no remaining clients) and invoke it before both early-return paths.attach_readerall-or-nothing: no session mutation when it returnsNone.Complements the diagnostic-logging PR (#58) — the logs introduced there will now correctly show
Daemon staying alive: ...for cases this PR fixes, andSelf-eviction triggered: ...will actually fire once the leaked session is cleaned up.Test plan
uv run cargo clippy --workspace --all-targets -- -D warningscleanuv run cargo test -p fbuild-serial --lib— 40 passed (1 new)uv run cargo test -p fbuild-daemon --lib— 88 passed🤖 Generated with Claude Code
Summary by CodeRabbit
Bug Fixes
Refactor