Skip to main content

pacenotes_installer/
crash.rs

1//! A GUI binary with `panic = "abort"` has nowhere to print, so a panic would
2//! be a vanished window and an offset into a stripped exe. The hook runs before
3//! the abort: it writes where and why into the run log, and tells the user in
4//! their language where that is, offering to show the file.
5
6/// Until the wizard knows the run log's file; a panic before that goes beside the exe.
7static LOG: std::sync::Mutex<Option<std::path::PathBuf>> = std::sync::Mutex::new(None);
8
9/// Writes the report but shows no window, for `--selftest-panic`, which has nobody to click.
10static QUIET: std::sync::atomic::AtomicBool = std::sync::atomic::AtomicBool::new(false);
11
12pub fn install_panic_hook() {
13    std::panic::set_hook(Box::new(|info| {
14        let location = info.location().map_or_else(
15            || "unknown location".to_string(),
16            |location| format!("{}:{}", location.file(), location.line()),
17        );
18        let message = info
19            .payload()
20            .downcast_ref::<&str>()
21            .map(|text| text.to_string())
22            .or_else(|| info.payload().downcast_ref::<String>().cloned())
23            .unwrap_or_else(|| "no message".to_string());
24        let text = format!("pacenotes-installer panicked at {location}: {message}");
25        let written = record(&text);
26
27        // The modal pumps messages and can redeliver the one that panicked, so only the first shows.
28        static SHOWN: std::sync::atomic::AtomicBool = std::sync::atomic::AtomicBool::new(false);
29        if QUIET.load(std::sync::atomic::Ordering::SeqCst)
30            || SHOWN.swap(true, std::sync::atomic::Ordering::SeqCst)
31        {
32            return;
33        }
34        // The worker thread never had the chosen language applied.
35        crate::i18n::use_chosen_language();
36        tell_the_user(written.as_deref(), &text);
37    }));
38}
39
40/// Points later panics at this run's log file.
41pub fn log_to(path: std::path::PathBuf) {
42    if let Ok(mut slot) = LOG.lock() {
43        *slot = Some(path);
44    }
45}
46
47pub fn be_quiet() {
48    QUIET.store(true, std::sync::atomic::Ordering::SeqCst);
49}
50
51/// A plain message and the file to send; the technical line is only shown
52/// when there is no file to hold it.
53fn tell_the_user(written: Option<&std::path::Path>, text: &str) {
54    match written {
55        Some(path) => {
56            let ask =
57                crate::i18n::load_string_arg(crate::i18n::IDS_CRASH, &path.display().to_string());
58            if crate::ui::ask_error(core::ptr::null_mut(), &ask) {
59                crate::ui::reveal_in_explorer(path);
60            }
61        }
62        None => {
63            let nowhere = crate::i18n::load_string(crate::i18n::IDS_CRASH_NOFILE);
64            crate::ui::message_box_error(
65                core::ptr::null_mut(),
66                &format!("{nowhere}\r\n\r\n{text}"),
67            );
68        }
69    }
70}
71
72/// Where `text` was appended, if anywhere; best effort, since failing to
73/// write a panic must not become a second one.
74fn record(text: &str) -> Option<std::path::PathBuf> {
75    let path = LOG
76        .lock()
77        .ok()
78        .and_then(|slot| slot.clone())
79        .or_else(fallback)?;
80    let mut file = std::fs::OpenOptions::new()
81        .create(true)
82        .append(true)
83        .open(&path)
84        .ok()?;
85    // Timed like every other line of the run log it usually lands in.
86    let now = pacenotes_installer::utc_time::iso_stamp(pacenotes_installer::utc_time::unix_now());
87    std::io::Write::write_all(&mut file, format!("{now}  {text}\n").as_bytes()).ok()?;
88    Some(path)
89}
90
91/// `pacenotes-installer-crash.log` next to the exe.
92fn fallback() -> Option<std::path::PathBuf> {
93    let exe = std::env::current_exe().ok()?;
94    Some(exe.with_file_name("pacenotes-installer-crash.log"))
95}