//! The platform half of this app's logging: what //! `crate::client::log_ring` needs that only Android can supply, which is //! `android_logger` as the logger to forward to and nothing else. use crate::client::log_ring::{self, LogRing}; use std::{ fs, panic, path::{Path, PathBuf}, sync::OnceLock, }; /// Installs the in-process ring in front of Android's logger. pub fn install(max_level: log::LevelFilter) { let inner = android_logger::AndroidLogger::new( android_logger::Config::default() .with_max_level(max_level) .with_tag("iris-android-app"), ); if log_ring::install_process_logger( Box::new(inner), max_level, iris::diagnostics::trace_enabled, ) .is_err() { log::warn!("iris app log: a logger was already installed, so there is no ring"); } install_panic_hook(); } pub fn ring() -> &'static LogRing { log_ring::process_ring() } #[cfg(feature = "bench")] pub fn diagnostics_line() -> String { let where_to_read = match crate::android::devlog::authority() { Some(authority) => format!("devlog provider: content://{authority}"), None => "devlog provider: declared, not created yet".to_string(), }; format!("{}\n{where_to_read}", ring().summary()) } /// Where the panic hook leaves its report, 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"; /// Enough preceding log lines to explain a crash without evicting the next run. const CRASH_CONTEXT_LINES: usize = 80; const PREVIOUS_RUN_TARGET: &str = "previous_run"; static CRASH_PATH: OnceLock = OnceLock::new(); /// Copies aborting panics into the device-readable log ring. fn install_panic_hook() { let previous = panic::take_hook(); 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(), }; 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() { let context = ring() .try_tail_text(CRASH_CONTEXT_LINES) .unwrap_or_else(|| { "(the log ring was locked as this run died; no context)".to_string() }); let _ = fs::write(path, format!("{line}\n{context}")); } previous(info); })); } /// Configures crash persistence and replays a report left by the previous run. pub fn set_crash_dir(dir: &Path) { let path = dir.join(CRASH_FILE); if let Ok(previous) = fs::read_to_string(&path) { // Delete first so a panic during replay cannot create a replay loop. let _ = fs::remove_file(&path); replay_crash(&previous); } let _ = CRASH_PATH.set(path); } /// Puts a previous run's report back in the ring: its context lines in /// the order they happened, then the panic itself. fn replay_crash(report: &str) { let (panic_line, context) = report.split_once('\n').unwrap_or((report, "")); for line in context.lines().filter(|line| !line.is_empty()) { ring().push(log::Level::Info, PREVIOUS_RUN_TARGET, line.to_string()); } log::error!( "iris app log: the previous run died -- {}", panic_line.trim() ); }