102 lines
3.5 KiB
Rust
102 lines
3.5 KiB
Rust
//! 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<PathBuf> = 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<dyn Any>");
|
|
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()
|
|
);
|
|
}
|