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
This commit is contained in:
2026-08-01 09:38:04 +02:00
co-authored by Claude Opus 5
parent 7bb80803fe
commit ecff75d514
2 changed files with 42 additions and 3 deletions
+12 -3
View File
@@ -92,7 +92,9 @@ async fn main() -> std::process::ExitCode {
let filter = tracing_subscriber::EnvFilter::try_from_default_env()
.unwrap_or_else(|_| tracing_subscriber::EnvFilter::new("info"));
if matches!(cli.cmd, Cmd::Daemon) {
let is_daemon = matches!(cli.cmd, Cmd::Daemon);
if is_daemon {
use tracing_subscriber::fmt::writer::MakeWriterExt;
if let Err(err) = paths.ensure_dirs() {
@@ -134,8 +136,15 @@ async fn main() -> std::process::ExitCode {
match result {
Ok(code) => std::process::ExitCode::from(code as u8),
Err(e) => {
eprintln!("xy: {e:#}");
Err(err) => {
// Under launchd the daemon's stderr is discarded, so a fatal
// startup error is only diagnosable if it reaches the log file.
if is_daemon {
tracing::error!("{err:#}");
} else {
eprintln!("xy: {err:#}");
}
std::process::ExitCode::from(1)
}
}
+30
View File
@@ -0,0 +1,30 @@
mod common;
use common::*;
#[tokio::test]
async fn fatal_startup_error_is_written_to_daemon_log() {
let xy = xy_bin();
let mut h = Harness::new();
h.start_daemon(&xy).await;
let log = h.state_dir.join("logs/daemon.log");
let before = std::fs::read_to_string(&log).unwrap_or_default();
assert!(
!before.contains("another xy daemon"),
"log already reports contention before the second daemon ran: {before}"
);
let (code, _out, _err) = h.run_cli(&xy, &["daemon"]).await;
assert_eq!(code, 1, "second daemon should exit 1 on pidfile contention");
let after = std::fs::read_to_string(&log).expect("daemon.log should exist");
assert!(
after.contains("another xy daemon"),
"daemon.log must record why the daemon refused to start, got: {after:?}"
);
}