diff --git a/crates/xy/src/main.rs b/crates/xy/src/main.rs index ade518d..ca94c02 100644 --- a/crates/xy/src/main.rs +++ b/crates/xy/src/main.rs @@ -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) } } diff --git a/crates/xy/tests/daemon_log.rs b/crates/xy/tests/daemon_log.rs new file mode 100644 index 0000000..a51d212 --- /dev/null +++ b/crates/xy/tests/daemon_log.rs @@ -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:?}" + ); +}