Skip to content

iter-123: daemon survives tabless-Firefox autostart + per-port registry - #162

Merged
ractive merged 3 commits into
mainfrom
iter-123/daemon-autostart-and-per-port-registry
Jul 19, 2026
Merged

iter-123: daemon survives tabless-Firefox autostart + per-port registry#162
ractive merged 3 commits into
mainfrom
iter-123/daemon-autostart-and-per-port-registry

Conversation

@ractive

@ractive ractive commented Jul 19, 2026

Copy link
Copy Markdown
Owner

Summary

Fixes two daemon-lifecycle defects found in dogfooding session 61 (ff-rdp v0.3.0 / Firefox 152), both in the client/daemon lifecycle code (no RDP spec-method changes).

Theme A — autostart no longer dies on a tabless Firefox. A freshly-launched headless Firefox can come up with zero page tabs (listTabstotal: 0) until the first navigation lazily creates one. The old run_daemon bailed at tabs.first().context("no tabs available")? before writing the registry, so the daemon never ran in this environment and every command silently fell back to a per-command direct connection with a daemon_autostart_failed warning.

  • Tab presence is needed only for the daemon's own resource watcher (network/console buffering), not for proxying, so a tabless start is now non-fatal. Tab resolution moved into establish_watcher (returns Ok(None) on zero tabs) + establish_watcher_with_retry (short bounded retry for the momentary-tab case).
  • On a persistently tabless start the registry is still written, the daemon reaches running:true, and a supervised watcher-establisher thread (background_establish_watcher_loop) resolves the watcher lazily once a tab appears — via a dedicated second RDP connection (the split main connection can't serve synchronous replies), handed to the dispatcher over a rendezvous channel. watcher_actor became Mutex<String> (empty until established).
  • The daemon_autostart_failed warning is now rendered in --format text too (render_warnings → stderr), not only via --jq '.warnings'.

Theme B — registry keyed per Firefox port. DaemonInfo is now written to daemon.<port>.json (and the spawn lock to daemon.<port>.spawn.lock), so concurrent instances on different ports no longer clobber each other's record. All registry::* call sites thread the port (cli.port / firefox_port / expected_port); find_running_daemon / wait_for_registry needed no logic change (already firefox_port-validated). A stale legacy single-slot daemon.json is retired on the next write.

Plan: kb/iterations/iteration-123-daemon-autostart-and-per-port-registry.md (ACs 4/4).

Test plan

  • cargo fmt / cargo clippy --workspace --all-targets -- -D warnings / cargo test --workspace -q — all clean.
  • cargo run -p xtask -- check-iteration-ready — 10/10 gates pass; plus check-daemon-locks and check-oneway-conformance.
  • Live tests against real Firefox (FF_RDP_LIVE_TESTS=1 cargo test-live --test live live_123): all 3 pass —
    • live_daemon_autostart_tabless — daemon reaches running:true despite zero tabs at start, no daemon_autostart_failed warning.
    • live_daemon_two_ports_no_clobber — two daemons on distinct ports each keep their own running:true record (both daemon.<port>.json files coexist).
    • live_daemon_warning_text_parity — JSON↔text warning parity.
  • Unit tests: per_port_writes_do_not_clobber, remove_only_affects_the_named_port, spawn_lock_is_per_port_and_does_not_cross_block, write_removes_stale_legacy_registry, establish_watcher_returns_none_on_zero_tabs, establish_watcher_with_retry_gives_up_when_no_tab_appears, render_warnings_handles_array_and_none, render_warnings_emits_line_for_each_entry.
  • Serialized the process-global warning-recorder tests (daemon_status::test_lock) to fix a pre-existing concurrency flake; updated e2e/live tests that hard-coded daemon.json.

🤖 Generated with Claude Code## Claims vs code
<generated 2026-07-19T12:45:19Z by ralph-loop>

  • two → ✅ matched in diff

iter-123 fixes two daemon lifecycle defects found in dogfooding session 61
(ff-rdp v0.3.0 / Firefox 152):

Theme A — autostart no longer dies when Firefox has no page tab yet. The
`tabs.first().context("no tabs available")?` bail in run_daemon is gone; tab
resolution moved into establish_watcher (returns Ok(None) on zero tabs) +
establish_watcher_with_retry. On a persistently tabless start the registry is
still written and the daemon reaches running:true, with a supervised
watcher-establisher thread resolving the watcher lazily once a tab appears
(dedicated 2nd connection, handed to the dispatcher via a rendezvous channel;
watcher_actor became Mutex<String>). The daemon_autostart_failed warning is now
also rendered in --format text (render_warnings on stderr), not only via
--jq '.warnings'.

Theme B — the registry is keyed per firefox_port (daemon.<port>.json +
daemon.<port>.spawn.lock) so concurrent instances on different ports no longer
clobber each other. All registry call sites thread the port; the stale legacy
single-slot daemon.json is retired on the next write.

Tests: 3 live tests (all pass against real Firefox), 5 registry + 2 watcher +
2 output-pipeline unit tests; serialized the process-global warning-recorder
tests to fix a pre-existing flake. Updated e2e/live tests that hard-coded
daemon.json.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@ractive

ractive commented Jul 19, 2026

Copy link
Copy Markdown
Owner Author

Reviewer focus request (orchestrator): live-test evidence is present — thanks. Please spend the review budget on the concurrency of the lazy watcher path: (1) the establisher thread's rendezvous hand-off vs daemon shutdown — no deadlock/leak if the daemon stops before the watcher resolves; (2) watcher_actor: Mutex<String> — no lock held across blocking RDP I/O; (3) requests arriving before the watcher is established degrade gracefully (clear error/warning, not a hang); (4) the second RDP connection is closed on all exit paths.

stop_prior_instance(cli, port) resolves an explicit target port (from
--debug-port), but run_daemon_stop and daemon_rpc still hardcoded
cli.port internally after the registry became per-port keyed. When
--port and --debug-port diverge (launch --replace --debug-port N with
--port != N), the graceful-stop RPC/registry path silently acted on
whatever daemon was registered under --port instead of the addressed
--debug-port daemon.

Thread an explicit port parameter through daemon_rpc and
run_daemon_stop instead of implicitly reading cli.port, so
stop_prior_instance always acts on the daemon it actually resolved.
Adds a live regression test that reproduces the pre-fix misrouting
(the port-still-listening error named the wrong port) and pins the
post-fix behavior.

Found during self-review of PR #162 (iter-123).
@ractive

ractive commented Jul 19, 2026

Copy link
Copy Markdown
Owner Author

PR Review Summary — PR #162

# File:Line Issue Source
1 crates/ff-rdp-cli/src/daemon/client.rs:1108 stop_prior_instance calls run_daemon_stop(cli) after resolving an explicit port that may differ from cli.port (e.g. launch --replace --debug-port N where N != --port); run_daemon_stop/daemon_rpc internally hardcoded cli.port for the per-port registry lookup, so --replace could silently act on the wrong daemon (or none) instead of the one actually addressed by --debug-port. Local

Fix

Threaded an explicit port: u16 parameter through daemon_rpc and run_daemon_stop instead of implicitly reading cli.port, so stop_prior_instance always acts on the daemon it resolved. Top-level callers (daemon stop CLI handler, dispatch.rs) pass cli.port explicitly; stop_prior_instance passes its own resolved port.

Verification

  • Added live_daemon_stop_prior_instance_targets_debug_port_not_cli_port (crates/ff-rdp-cli/tests/live/live_123_daemon_autostart_and_registry.rs) — reproduces the bug pre-fix (the port-still-listening error named the wrong/decoy port) and confirms post-fix it correctly targets the --debug-port daemon while leaving the decoy daemon (registered under --port) untouched.
  • Confirmed the test fails against the pre-fix code and passes against the fix (verified both directions locally via git stash).
  • cargo fmt / cargo clippy --workspace --all-targets -- -D warnings / cargo test --workspace -q all clean.
  • cargo run -p xtask -- check-iteration-ready — 10/10 gates pass (re-run after the fix, including with FF_RDP_LIVE_TESTS=1).
  • check-daemon-locks / check-oneway-conformance — clean.

No other issues found in local review (Copilot review skipped — local-only mode).

Corrects the Theme B task note that claimed all registry::* call sites
pass cli.port (the exact assumption that caused the stop_prior_instance
port-scoping bug), and adds AC 5/5 for the new regression test.
@ractive
ractive merged commit e5f486f into main Jul 19, 2026
10 checks passed
@ractive
ractive deleted the iter-123/daemon-autostart-and-per-port-registry branch July 19, 2026 13:17
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