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
This commit is contained in:
@@ -79,7 +79,7 @@ bypass it with the `contents: Option<String>` escape hatch and hand-author plist
|
||||
XML, the daemon gains its own log file (see below).
|
||||
|
||||
**Accepted limitation.** Failures occurring before the daemon's logger exists —
|
||||
missing binary, dyld error, malformed plist — appear in neither `xy.log` nor
|
||||
missing binary, dyld error, malformed plist — appear in neither `daemon.log` nor
|
||||
`xy service status`. This is worse than first assumed: `LaunchdServiceManager::
|
||||
status()` returns `ServiceStatus::Stopped(None)` unconditionally
|
||||
(`launchd.rs:288`), so the `Option<String>` reason carried by the enum is always
|
||||
@@ -152,7 +152,7 @@ server config using a relative `working_dir` resolves somewhere predictable.
|
||||
which is too late to open a log file for the logger itself.
|
||||
|
||||
For the `Cmd::Daemon` arm only, the subscriber writes to `stderr.and(file)` via
|
||||
`MakeWriterExt`, where the file half is `log_dir/xy.log` backed by the existing
|
||||
`MakeWriterExt`, where the file half is `log_dir/daemon.log` backed by the existing
|
||||
`xy_supervisor::logs::RotatingLogWriter` (10 MB × 5, the same rotation used for
|
||||
per-server logs). No adapter type is needed: `tracing-subscriber` 0.3
|
||||
implements `MakeWriter` for `Mutex<W> where W: io::Write`
|
||||
@@ -175,7 +175,7 @@ type already tracks `written` and rotates, but today exposes only
|
||||
`write_line(tag, line)`, which prefixes a tag the daemon's own log does not
|
||||
want.
|
||||
|
||||
Result: `~/.local/state/xy/logs/` becomes uniform — `xy.log` for the daemon,
|
||||
Result: `~/.local/state/xy/logs/` becomes uniform — `daemon.log` for the daemon,
|
||||
`<server>.log` per supervised server, all rotated by the same code.
|
||||
|
||||
## launchd mechanics
|
||||
@@ -264,14 +264,16 @@ Sample output:
|
||||
state: running (pid 4821)
|
||||
program: /Users/olsson/.cargo/bin/xy
|
||||
path: /opt/homebrew/bin:… (snapshotted 2026-07-31)
|
||||
log: ~/.local/state/xy/logs/xy.log
|
||||
log: ~/.local/state/xy/logs/daemon.log
|
||||
|
||||
### Exit codes
|
||||
|
||||
Reuses the established scheme, minus the codes that cannot apply. `0` success,
|
||||
`1` operational error (launchctl failed, agent missing, permission denied).
|
||||
Code `2` (daemon unreachable) is structurally impossible because these commands
|
||||
never open the socket; `3` (config invalid) does not arise.
|
||||
never open the socket. Code `3` is reachable only before dispatch: `main.rs`
|
||||
returns it when `Paths::resolve()` fails, which happens ahead of every
|
||||
subcommand including `xy service`. No `xy service` code path returns `3` itself.
|
||||
|
||||
## Testing
|
||||
|
||||
|
||||
Reference in New Issue
Block a user