Commit Graph
55 Commits
Author SHA1 Message Date
logaritmisk 7631eefc67 feat(supervisor): evaluate wait-for conditions 2026-08-07 15:41:06 +02:00
logaritmisk dbf600c1f5 feat(protocol): Waiting state and WaitInfo on StatusDetail 2026-08-07 15:37:12 +02:00
logaritmisk fbb58ebfef feat(protocol): wait-for schema and KDL parsing 2026-08-07 15:33:19 +02:00
logaritmiskandClaude Opus 5 0c8ae3b617 fix(supervisor): compute uptime at read time, not publish time
set_state computed uptime_secs from started_at.elapsed() and pushed the
result into the watch channel. do_start sets started_at immediately before
calling set_state(Running), so the published value was always ~0, and the
list/status handlers copied that frozen snapshot straight out. A server
that stayed healthy reported 0s forever, because the value was only
recomputed on the next state transition.

Status now carries started_at (an Instant, in-process only) and the
handlers derive uptime_secs when they build ServerSummary. The wire type
is unchanged.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EGntTHCW3sEPy1VBRopNNp
2026-08-07 13:11:01 +02:00
logaritmiskandClaude Opus 5 d75d57ff0b fix(supervisor): don't count the first start as a restart
do_start incremented restart_count on every spawn, including the initial
one, so a server that had never restarted reported 1. do_start now takes a
StartCause: the explicit Start command (which is also how servers auto-start
at daemon boot) is Initial and does not count; the explicit Restart command
and the automatic post-crash respawn are Restart and do.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EGntTHCW3sEPy1VBRopNNp
2026-08-07 13:09:47 +02:00
logaritmiskandClaude Opus 5 ecff75d514 fix(daemon): log fatal startup errors to daemon.log
A daemon that fails during startup reported the reason via eprintln! in
main's error arm. Under launchd stderr is discarded, so the failure was
invisible: daemon.log was created and left empty — exactly the case the
log file was added for.

Found by the manual acceptance run: the launchd agent loaded correctly
(RunAtLoad fired, runs=9) but every spawn exited 1 because a previously
started daemon held the pidfile, and nothing recorded why.

Fatal errors on the daemon path now go through tracing::error!, reaching
both the log file and stderr. Other subcommands keep eprintln!, since
their stderr is the user's terminal.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EGntTHCW3sEPy1VBRopNNp
2026-08-01 09:38:04 +02:00
logaritmiskandClaude Opus 5 7bb80803fe fix(service): strip Disabled before load, verify it, and honour the plist
service-manager 0.11 writes Disabled: true into every plist carrying
KeepAlive, and our RestartPolicy::Always guarantees KeepAlive, so every
agent we installed was born disabled. launchctl load honours the key while
still exiting 0, so install reported success on an agent that would never
start — not then and not at the next login.

service::start now removes the Disabled key itself before loading, via a
pure enable_plist() that rewrites nothing when the key is absent and
preserves every other key, including the EnvironmentVariables PATH
snapshot. That makes start self-healing for plists left disabled by an
earlier build. The crate's own start() is still not used, since without
Disabled it degrades to launchctl start, which fails on an unloaded job.

Because launchctl load exits 0 on failure, start also checks a
post-condition: it asks launchd whether the job now exists and reports a
diagnostic if it does not. stop keeps no such check, since a benign unload
of an already-stopped job also prints a failure while exiting 0.

status now treats the plist on disk as the definition of installed, as the
spec says: a plist that exists but is not loaded reports stopped with its
program, PATH and snapshot date intact instead of collapsing to
not-installed with every field cleared. That is precisely the state the
Disabled bug left users in, so it is the state status most needs to
describe.

status also gains the log path the spec always listed, and the daemon's own
log is renamed xy.log -> daemon.log so a supervised server named xy cannot
share a file, and two rotation counters, with the daemon.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EGntTHCW3sEPy1VBRopNNp
2026-08-01 00:27:34 +02:00
logaritmiskandClaude Opus 5 41acc3e21a fix(service): unescape plist XML, surface both launchctl streams, clear status fields when not installed
Three review findings on task 4:
- read_plist_fields now parses the plist properly via the plist crate
  (already a transitive dep of service-manager, promoted to direct) instead
  of slicing raw XML, so a PATH or program path containing & or < no longer
  round-trips as literal &amp;/&lt; through cli::service::render_status.
- launchctl's error path now includes both stdout and stderr, trimmed and
  joined only on non-empty parts, so a failure never surfaces as
  "launchctl load failed: " with nothing after the colon.
- status() now returns identical None fields (program, path_env,
  snapshotted, pid) whether the plist is absent or the plist exists but the
  crate reports NotInstalled (e.g. written but never loaded, or booted out
  of band) - extracted via a shared not_installed_status/build_status split
  so both paths run the same code.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EGntTHCW3sEPy1VBRopNNp
2026-08-01 00:09:59 +02:00
logaritmisk f48221b09c feat(cli): xy service install/uninstall/start/stop/status 2026-07-31 23:39:32 +02:00
logaritmisk 999134657c feat(service): agent status plus launchctl load/unload 2026-07-31 23:35:36 +02:00
logaritmisk f267efc967 feat(service): AgentSpec and launchd install/uninstall 2026-07-31 23:32:46 +02:00
logaritmisk ae37060160 fix(daemon): drop unneeded Arc from daemon_writer's Mutex return type 2026-07-31 23:28:53 +02:00
logaritmisk b69426f4df feat(daemon): write rotating xy.log alongside stderr 2026-07-31 22:57:30 +02:00
logaritmisk f70092f1a0 feat(logs): impl io::Write for RotatingLogWriter 2026-07-31 22:49:36 +02:00
logaritmiskandClaude Opus 4.8 40ffd9b06c fix(pidfile): replace stale pidfile after unclean shutdown
PidFile::acquire used create_new(true) with Drop-based cleanup, so a
pidfile surviving power loss or SIGKILL made the daemon refuse to start
until the file was deleted by hand.

On AlreadyExists, read the recorded PID and probe it with kill(pid, 0):
ESRCH (or unparseable content) means stale, so remove the file and
retry the atomic create. A live PID keeps the refusal and now names the
holding process. The retry loop is bounded to stay race-safe against a
concurrent starter.

Closes #1

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-05 18:16:54 +02:00
logaritmiskandClaude Opus 4.7 00c7e7e812 feat(paths): auto-create config dir on daemon startup
ensure_dirs() now creates config_dir alongside state_dir and log_dir,
so first daemon run materializes $XDG_CONFIG_HOME/xy/servers/ — making
it obvious where to drop server .kdl files.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-26 12:44:47 +02:00
logaritmiskandClaude Sonnet 4.6 4a0b32d90e fix(supervisor): StartAck::SpawnFailed surfaces real failures
Add StartAck::SpawnFailed(String) so callers can distinguish a successful
start from a failed spawn. The Start command arm now sends SpawnFailed on
io::Error rather than the misleading Started. handlers.rs maps the new
variant to an RpcErrorCode::SpawnFailed JSON-RPC error response.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-25 12:32:07 +02:00
logaritmiskandClaude Sonnet 4.6 b366df0482 fix(supervisor): make backoff sleep interruptible by Stop/Shutdown
Replace the bare sleep(delay).await in the Restart backoff arm with a
tokio::select! over the timer and cmd_rx. Stop/Shutdown are now handled
immediately during backoff (Stop → Stopped, Shutdown → clean exit);
Start/Restart/Reconfigure skip the remaining delay and retry at once.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-25 12:31:32 +02:00
logaritmiskandClaude Sonnet 4.6 3e4ad79137 fix(supervisor): publish full status (pid, port, uptime, restart_count, last_exit) via watch channel
Replace watch::Receiver<ServerState> on SupervisorHandle with watch::Receiver<Status>,
a richer snapshot type that carries pid, port, uptime_secs, restart_count and last_exit.
SupervisorTask maintains current_pid and publishes a fresh Status on every state
transition; handlers.rs reads the full Status so list/status no longer return
zeroed/None fields.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-25 12:30:56 +02:00
logaritmisk 9d5d8f04a2 chore: clippy fixes - allow should_implement_trait and collapse nested if 2026-05-25 12:19:24 +02:00
logaritmisk b1e7dea739 test(xy): logs --tail and --follow
Fix a deadlock in the log-stream handler that caused all logs
requests to hang: Connection used a single Mutex<JsonFramed> for
both reads and writes, so the serve loop holding the read lock
blocked the spawned notification task from writing.  Split
Connection into separate reader and writer mutexes.

Also fix a response/notification ordering race: the log task now
waits for an explicit ready signal sent by serve after writing the
LOGS response, ensuring notifications never arrive at the client
before their initiating response.
2026-05-25 12:17:32 +02:00
logaritmisk 15791c628b test(xy): reload diff 2026-05-25 12:05:58 +02:00
logaritmisk 284b6e7402 test(xy): restart cap escalates to failed 2026-05-25 12:05:45 +02:00
logaritmisk 434828c14e test(xy): auto-start + stop/start lifecycle 2026-05-25 12:05:28 +02:00
logaritmisk 48d63a0549 test(xy): integration test harness 2026-05-25 12:03:38 +02:00
logaritmisk 7107977637 test(xy): helper binaries for integration tests 2026-05-25 12:03:13 +02:00
logaritmiskandClaude Opus 4.7 c1f6225e26 feat(xy): CLI client commands
Replace bail!("not implemented") stubs with real RPC calls over the Unix
socket; add format::list_table for fixed-width list output.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-25 12:02:09 +02:00
logaritmiskandClaude Sonnet 4.6 b434c636a6 feat(xy): logs streaming via subscription notifications
Implement per-connection ConnState tracking active subscriptions, and the
logs/logs_cancel RPC handlers. Snapshot-only streams terminate with a
log_end notification; follow streams forward broadcast lines until
cancelled or connection close.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-25 11:59:46 +02:00
logaritmiskandClaude Sonnet 4.6 c679465f12 feat(xy): reload handler with diff
Implements the `reload` JSON-RPC method: diffs the on-disk config dir
against the in-memory registry and reconciles — stops removed servers,
restarts changed servers (shutdown-then-respawn), and starts new ones.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-25 11:56:45 +02:00
logaritmiskandClaude Sonnet 4.6 736e6d1854 feat(xy): RPC handlers for list/status/start/stop/restart
Per-connection JSON-RPC dispatch in daemon/handlers.rs — list, status,
start, stop, and restart are fully implemented; reload, logs, and
logs_cancel are stubbed with -32601 for later tasks.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-25 11:54:52 +02:00
logaritmiskandClaude Sonnet 4.6 3ab982aea1 feat(xy): daemon boot + accept loop + graceful shutdown
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-25 11:52:41 +02:00
logaritmisk d7aa543ac0 feat(xy): daemon Registry with config-hash entries 2026-05-25 11:49:58 +02:00
logaritmisk 71808783c4 feat(xy): clap CLI scaffold 2026-05-25 11:49:47 +02:00
logaritmisk 49c006df10 feat(xy): exclusive pidfile guard 2026-05-25 11:48:38 +02:00
logaritmisk 58c44e0b48 feat(xy): XDG path resolution 2026-05-25 11:48:36 +02:00
logaritmisk b137f85a0c feat(ipc): server bind + Connection wrapper 2026-05-25 11:47:24 +02:00
logaritmisk fbfb1db427 feat(ipc): client with call + notification reader 2026-05-25 11:47:11 +02:00
logaritmisk e58b6866ef feat(ipc): newline-delimited JSON framing 2026-05-25 11:45:50 +02:00
logaritmisk 53f6b82f2b feat(ipc): JSON-RPC envelope types 2026-05-25 11:45:37 +02:00
logaritmiskandClaude Sonnet 4.6 a3c979511e feat(supervisor): supervisor task with state machine
One async task per managed server owns all state transitions via a
tokio::select! loop over cmd_rx and wait_child. Includes RealSpawner
and a smoke test covering the Start → Running → exit → Stopped →
Shutdown happy path.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-25 11:44:12 +02:00
logaritmiskandClaude Sonnet 4.6 f1b2306156 feat(supervisor): RealChild + spawn_with_logs
Append RealChild (real tokio::process::Child wrapper) and spawn_with_logs
to child.rs. Uses nix::unistd::setpgid via tokio's re-exported pre_exec
to create an own process group, and fires per-stream log pump tasks that
drain stdout/stderr into the provided LogSink. terminate/kill signal the
whole process group via kill(-pgid, SIG*).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-25 11:40:19 +02:00
logaritmisk e121fe28bb feat(supervisor): LogSink fans out to file, ring buffer, broadcast 2026-05-25 11:37:19 +02:00
logaritmisk 7995a53e82 feat(supervisor): ring buffer for recent log lines 2026-05-25 11:36:52 +02:00
logaritmisk d51f25350c feat(supervisor): rotating log writer 2026-05-25 11:36:23 +02:00
logaritmisk d237e980e9 feat(supervisor): sliding retry-window tracker 2026-05-25 11:34:55 +02:00
logaritmisk 54045da2df feat(supervisor): exponential backoff calculator 2026-05-25 11:34:53 +02:00
logaritmisk 4837a73167 feat(supervisor): restart-policy decision logic 2026-05-25 11:34:50 +02:00
logaritmisk 1d2848f03a feat(supervisor): ChildHandle trait + MockChild 2026-05-25 11:33:23 +02:00
logaritmisk bd926061bf feat(protocol): JSON-RPC method param/result types 2026-05-25 11:31:56 +02:00
logaritmisk e8f5846cec feat(protocol): load_all_configs from dir with duplicate port detection 2026-05-25 11:30:38 +02:00