diff --git a/crates/tty7-core/src/core/crash.rs b/crates/tty7-core/src/core/crash.rs index a1460c6e..2f05155d 100644 --- a/crates/tty7-core/src/core/crash.rs +++ b/crates/tty7-core/src/core/crash.rs @@ -121,6 +121,37 @@ mod tests { assert!(body.contains("test v"), "role + version: {body}"); } + /// Every binary whose stderr nobody is watching installs the hook. + /// + /// A panic is only as useful as the place it lands. The GUI and the server + /// both outlive the terminal that started them, and the updater is worse + /// than either: it runs *detached*, after the GUI it is replacing has + /// exited, doing the one job in this product that can leave an install + /// broken. It shipped without the hook — a panic mid-swap was silence, with + /// `tty7-updater.log` stopping mid-sentence and nothing to say why. + /// + /// The CLI is deliberately absent. It is a short-lived foreground process + /// whose panic prints to a terminal someone is already looking at, and a + /// second copy in `crash.log` buys nothing. + #[test] + fn every_detached_binary_records_its_panics() { + for (role, source) in [ + ("app", include_str!("../../../../src/main.rs")), + ( + "updater", + include_str!("../../../../src/bin/tty7-updater.rs"), + ), + ("server", include_str!("../../../tty7-server/src/main.rs")), + ] { + assert!( + source.contains("crash::install"), + "the {role} binary never installs the panic hook, so its crashes \ + go wherever its stderr goes — which for a detached process is \ + nowhere" + ); + } + } + #[test] fn civil_from_days_matches_known_dates() { assert_eq!(civil_from_days(0), (1970, 1, 1)); diff --git a/src/bin/tty7-updater.rs b/src/bin/tty7-updater.rs index 3b91643e..9a6da2e7 100644 --- a/src/bin/tty7-updater.rs +++ b/src/bin/tty7-updater.rs @@ -4162,8 +4162,23 @@ mod windows { } } +/// Record a panic where someone can find it. +/// +/// This binary runs *detached*, after the GUI it is replacing has exited, so +/// its stderr is attached to nothing a user will ever read — and it is doing +/// the one job in the product that can leave an install broken. Without this a +/// panic mid-swap is simply silence: the app does not come back, `tty7-updater.log` +/// stops mid-sentence, and there is nothing to say why. +/// +/// `crash.log` is the same file the app and the server write to, in the config +/// directory, so the three roles land in one place in the order they failed. +fn install_crash_log() { + tty7_core::core::crash::install("updater"); +} + #[cfg(target_os = "macos")] fn main() { + install_crash_log(); if let Err(error) = macos::run() { eprintln!("tty7-updater: {error}"); std::process::exit(1); @@ -4172,6 +4187,7 @@ fn main() { #[cfg(target_os = "windows")] fn main() { + install_crash_log(); if let Err(error) = windows::run() { eprintln!("tty7-updater: {error}"); std::process::exit(1); @@ -4180,6 +4196,7 @@ fn main() { #[cfg(target_os = "linux")] fn main() { + install_crash_log(); if let Err(error) = linux::run() { eprintln!("tty7-updater: {error}"); std::process::exit(1);