fix(updater): record a panic where someone can find it

`crash::install` puts a panic hook in front of `crash.log`. The GUI
installs it and the server installs it. The updater did not — and of the
three it is the one that needs it most.

It runs *detached*, after the GUI it is replacing has exited, so its
stderr is attached to nothing anybody will read. And it is doing the one
job in this product that can leave an install broken. A panic mid-swap
was therefore silence: the app does not come back, `tty7-updater.log`
stops mid-sentence, and there is nothing anywhere that says why.

All three of its `cfg`'d mains install it now, so the role travels with
whichever platform failed. `crash.log` is the file the other two roles
already write, in the config directory, so the three land in one place in
the order they failed.

The CLI stays out deliberately: 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.

The guard reads the three entry points rather than a list kept here, and
was checked against the state that shipped — it names the updater.

The mechanism itself was already covered: `a_panic_lands_in_the_crash_log`
proves the hook writes the record. What nothing held was whether each
binary calls it, which is exactly what was missing.
This commit is contained in:
l0ng-ai
2026-08-23 02:23:23 +08:00
parent ebfc08368b
commit 21ddd01bc9
2 changed files with 48 additions and 0 deletions
+31
View File
@@ -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));
+17
View File
@@ -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);