Repository navigation
feat(daemon): /agentd restart — exec self in place, preserve remote URL + password - #99
Merged
Merged
Conversation
Adds a `/agentd restart` slash command (TUI god session) and the matching `daemon.restart` IPC method. The daemon resolves `current_exe()` + args, sends a `RestartCommand` to a new restart-channel arm in main's `tokio::select!`, and exec()s the binary in place. PID is preserved; on-disk binary upgrades are picked up by the new process. Scope for this PR (issue #90, partial): - `daemon.restart` RPC + client wrapper + TUI slash dispatch. - `/agentd` added to zarvis adapter's slash autocomplete list. - Main loop exec self: kernel closes CLOEXEC'd FDs (IPC + WS listeners), cloudflared subprocess is taken down via `kill_on_drop` as the runtime tears down, and the new process rebinds whichever listeners are configured. - New `RemoteSnapshot` type + `RemoteState::persist()` write- through on every state change, persisting `{token, password, port, tunnel_url, tunnel_pid, generated_at}` to `runtime_dir/remote.json`. The new daemon doesn't read it back yet — that's the URL-preservation follow-up below — but the snapshot file is in place so the follow-up doesn't need a schema migration. Deferred to follow-up issues (see #90 comments): - **URL + password preservation across restart.** Requires detaching cloudflared into its own process group + adopting the still-running subprocess from the new daemon. The persistence scaffold is here; the supervisor-side adoption path and restore-from-snapshot bind logic remain. - **TUI auto-reconnect on socket close.** The TUI today shows "daemon disconnected" status and the user re-runs `agent` to reconnect; auto-reconnect is invasive (the App holds an `Arc<Client>` that's hard to swap cleanly). - **Web client.** Already auto-reconnects with exponential backoff — when the new daemon picks up the URL preservation work, the phone will reconnect transparently. The TUI gracefully reports each error class — `BrokenPipe` / `ConnectionReset` / `closed`-style errors after a successful restart get a "restart in flight (socket closed) — reconnect when ready" status instead of a misleading error. Smoke (local Unix-socket end-to-end): - `daemon.restart` → reply with new exe + same PID. - PID preserved across exec (verified with `ps -p <pid>`). - Daemon log shows two "loaded config" + two "listening socket" entries, confirming the runtime tore down and rebuilt. - Post-exec `ping` succeeds after ~5s (orchestrator-spawn timeout dominates in the smoke env where harness adapters aren't on PATH; expect <100 ms in real use).
Extends the restart machinery to keep the cloudflared tunnel
URL and HTTP Basic auth password valid across an `exec()`. The
new daemon adopts the still-running cloudflared subprocess by
PID instead of spawning a fresh one, so phones reconnect to
the same `https://*.trycloudflare.com/t/<token>` URL after a
restart. The TUI's `/agentd restart` reply now reports
`tunnel_preserved: true` when adoption is going to succeed.
How it works:
- cloudflared is spawned with `process_group(0)` (its own
session) and `kill_on_drop` defaulted to `false`. It survives
the daemon's tokio runtime tear-down during `exec()`.
- Every change to `RemoteState` (`set_port`, `set_tunnel_pid`,
`set_tunnel_url`) writes through to
`runtime_dir/remote.json` — that scaffolding was in the
prior commit. The snapshot captures
`{version, token, password, port, tunnel_url, tunnel_pid,
generated_at}`.
- New daemon boot: if `remote.json` exists, `main.rs` calls
`start_remote(None, …)`. The supervisor's `bind_and_install`
runs `load_restore_snapshot()`, which deletes the snapshot
unless it's both fresh (< 5 min old) AND its `tunnel_pid` is
still alive (`kill(pid, 0)`). On success it binds the same
port, hydrates `RemoteState` from the snapshot, and tells
`tunnel::run` to "adopt" via a non-zero `adopt_pid`.
- `tunnel::run` in adopt mode polls the PID every 2s and skips
the spawn loop entirely. When the adopted PID eventually
dies (e.g. cloudflared crash, OOM, manual `kill`), the
watcher falls through to the normal spawn loop with a fresh
URL — same behavior as a cold start.
- `/remote-control stop` explicitly `SIGTERM`s the recorded
PID (since `kill_on_drop(false)` no longer takes it down) and
deletes the snapshot file, so the next daemon boot doesn't
re-adopt a tunnel the user just asked to stop.
- Daemon SIGINT/SIGTERM still calls `shutdown_adapters`; the
cloudflared subprocess will be sniped by the cleanup
triggered on next daemon boot if it's stale (the snapshot's
liveness check fails → file deleted + fresh mint).
Trade-off explicitly documented: a daemon SIGKILL leaks the
cloudflared subprocess until the next daemon boot's snapshot
sweep. URL preservation is worth the brief leak window.
Smoke (local Unix-socket end-to-end, AGENTD_REMOTE_NO_TUNNEL=1
so no real cloudflared):
pre: http://127.0.0.1:65125/t/ed44860fcfbc470abae26a481dbc9247
loyal.yak.39
→ daemon.restart → exec self →
post: http://127.0.0.1:65125/t/ed44860fcfbc470abae26a481dbc9247
loyal.yak.39
Same URL, same password, same port. The daemon log shows
"remote: restored from snapshot — preserving URL + password" on
the new daemon. Real cloudflared smoke pending phone test.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Adds
/agentd restartslash command +daemon.restartIPC method. The daemonexec()s its own current binary in place, picking up any on-disk upgrade. cloudflared is detached so the public URL + HTTP Basic password survive the restart — phones reconnect to the same endpoint.Two-commit story
daemon.restartRPC + main-loop exec arm +RemoteSnapshotpersistence scaffold.process_group(0)+kill_on_drop(false)), boot-time snapshot restore in the supervisor, adopt-by-PID path intunnel::run, explicit SIGTERM in/remote-control stop.How URL + pw preservation works
RemoteStatewrites through toruntime_dir/remote.json(token, password, port, tunnel URL, cloudflared PID, generated_at).process_group(0)— its own session, survives the daemon'sexec().remote.jsonexists,main.rscallsstart_remote. The supervisor'sbind_and_installrunsload_restore_snapshot— deletes the snapshot unless it's both fresh (< 5 min) ANDtunnel_pidis alive (kill(pid, 0)). On success, binds the same port, hydratesRemoteStatefrom the snapshot, and tellstunnel::runto adopt viaadopt_pid != 0.tunnel::runin adopt mode polls the PID every 2s and skips spawning. When the adopted PID dies, the watcher falls through to the normal spawn loop with a fresh URL./remote-control stopexplicitlySIGTERMs the PID + deletes the snapshot so the next boot doesn't re-adopt.Trade-off
A daemon SIGKILL leaks the cloudflared subprocess until the next daemon boot's snapshot sweep notices the stale state. URL preservation is worth the brief leak window for the upgrade use case.
Still deferred
agent. The App holds anArc<Client>that's hard to swap cleanly mid-loop — file a follow-up issue.Binaries
Test plan
cargo build --workspaceclean.cargo test -p agentd— 25 passed.ps).daemon.restartreply: new exe path + same PID./remote-controlto get a tunnel URL, scan from phone,/agentd restart, observe phone reconnects to same URL with no QR rescan./remote-control stopafter restart: cloudflared SIGTERM'd, snapshot deleted, next boot mints fresh.Recording
URL + pw preservation across restart is the visible demo. Happy to record once the real-cloudflared smoke is done if you want a phone-side capture.