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.
|
// The line goes through whatever logger did win.
|
||||||
log::warn!("iris app log: a logger was already installed, so there is no ring");
|
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
|
/// 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())
|
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;
|
return;
|
||||||
};
|
};
|
||||||
#[cfg(feature = "transcript-screen")]
|
#[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}");
|
log::debug!("iris app: files directory is {dir}");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in new issue
Block a user