iris-android-app: a panic hook, so an abort says something Iris can read
Checked before writing anything: under `panic = "abort"` (this crate's
Cargo.toml) a panic's message reaches the tombstone's `Abort message`
and nowhere else -- not `log`, so not `client_core::log_ring`, so not
Dev Updater's Runtime tab. That tab is the only surface Iris has on a
phone with no `adb`, so every `assert!` and `expect!` in these builds
has been failing silently as far as she is concerned; the adapter crash
fixed in the next commit looked like the app simply relaunching.
`install_panic_hook` (called from `app_log::install`) writes the
message and its location at `error` level. The ring is memory only and
the process is about to die, so it also writes `last-panic.txt` in the
app's private directory; `set_crash_dir`, called from
`nativeSetFilesDir`, replays that into the ring at `error` level on the
next start and deletes it. A crash loop therefore explains itself in
the run that is still up, which is the run somebody can look at.
Verified on this checkout's emulator against the unfixed renderer:
`iris panic at .../render.rs:140:14: Could not get adapter!: NotFound
{...}` on the run that died, and `iris app log: the previous run died
-- ...` on the next one.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
1 parent
203f53470c
commit
f99ae4c366
2 files changed
+63
-1
No files matched your search
@@ -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::path::PathBuf> = 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<dyn Any>");
|
||||
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);
|
||||
}
|
||||
@@ -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}");
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user