diff --git a/iris/android-app/src/app_log.rs b/iris/android-app/src/app_log.rs index 8c997fc..2b83147 100644 --- a/iris/android-app/src/app_log.rs +++ b/iris/android-app/src/app_log.rs @@ -34,6 +34,7 @@ pub fn install(max_level: log::LevelFilter) { // The line goes through whatever logger did win. log::warn!("iris app log: a logger was already installed, so there is no ring"); } + install_panic_hook(); } /// The process's ring -- what `Copy report` appends, what the diagnostics @@ -63,3 +64,61 @@ pub fn diagnostics_line() -> String { }; format!("{}\n{where_to_read}", ring().summary()) } + +/// Where the panic hook leaves its one line, under the app's private +/// directory. Read back and dropped by [`set_crash_dir`] on the next +/// start. +const CRASH_FILE: &str = "last-panic.txt"; + +static CRASH_PATH: std::sync::OnceLock = std::sync::OnceLock::new(); + +/// Installs a `log`-level panic hook, so a panic's message and location +/// reach the ring and `logcat` rather than only the tombstone. +/// +/// **Why this is needed at all**: these builds are `panic = "abort"` +/// (`Cargo.toml`), and the default hook writes to `stderr` plus +/// `android_set_abort_message` -- the crash report. Iris runs these on a +/// phone with no `adb`, so the crash report is exactly the surface she +/// cannot read, and an `assert!` that fired said nothing anywhere she +/// could see it. Routing it through `log::error!` puts it in front of +/// `android_logger` *and* in the ring `devlog`'s provider hands to Dev +/// Updater. +/// +/// The ring is memory only, so after an abort the process that holds it +/// is gone -- hence the file half. [`set_crash_dir`] replays it. +fn install_panic_hook() { + let previous = std::panic::take_hook(); + std::panic::set_hook(Box::new(move |info| { + let where_at = match info.location() { + Some(at) => format!("{}:{}:{}", at.file(), at.line(), at.column()), + None => "an unknown location".to_string(), + }; + // `info`'s own `Display` repeats the location and a newline; + // the payload alone keeps this to the one line the ring wants. + let message = info.payload_as_str().unwrap_or("Box"); + let line = format!("iris panic at {where_at}: {message}"); + log::error!("{line}"); + if let Some(path) = CRASH_PATH.get() { + // Best effort by design: a panic is already the failure, and + // failing to record it must not become a second one. + let _ = std::fs::write(path, &line); + } + previous(info); + })); +} + +/// Tells the panic hook where to leave its line, and replays the line a +/// previous run left there into the ring before deleting it. +/// +/// Called from `nativeSetFilesDir`, which is the first moment the app's +/// private directory is known. The replay is at `error` level and says +/// it is from the previous run, so a crash loop shows the reason it is +/// looping in the Runtime tab of the run that is still up. +pub fn set_crash_dir(dir: &std::path::Path) { + let path = dir.join(CRASH_FILE); + if let Ok(previous) = std::fs::read_to_string(&path) { + log::error!("iris app log: the previous run died -- {}", previous.trim()); + let _ = std::fs::remove_file(&path); + } + let _ = CRASH_PATH.set(path); +} diff --git a/iris/android-app/src/lib.rs b/iris/android-app/src/lib.rs index 8ed320a..5bdacd6 100644 --- a/iris/android-app/src/lib.rs +++ b/iris/android-app/src/lib.rs @@ -180,7 +180,10 @@ pub extern "system" fn Java_dev_iris_android_demo_MainActivity_nativeSetFilesDir return; }; #[cfg(feature = "transcript-screen")] - enrollment::set_files_dir(std::path::PathBuf::from(&dir)); + { + app_log::set_crash_dir(std::path::Path::new(&dir)); + enrollment::set_files_dir(std::path::PathBuf::from(&dir)); + } log::debug!("iris app: files directory is {dir}"); }