use std::fs::OpenOptions;
use std::io::Write;
use crate::db::store;
const FILE_NAME: &str = "crashes.log";
pub(crate) fn install() {
let default = std::panic::take_hook();
std::panic::set_hook(Box::new(move |info| {
default(info);
let _ = append(info);
}));
}
pub(crate) fn pending() -> Option<String> {
let text = std::fs::read_to_string(store::config_dir().ok()?.join(FILE_NAME)).ok()?;
(!text.trim().is_empty()).then_some(text)
}
pub(crate) fn clear() {
if let Ok(dir) = store::config_dir() {
let _ = std::fs::remove_file(dir.join(FILE_NAME));
}
}
pub(crate) fn summary(report: &str) -> String {
report.lines().take(4).collect::<Vec<_>>().join("\n")
}
fn append(info: &std::panic::PanicHookInfo) -> std::io::Result<()> {
let dir = store::config_dir().map_err(|error| std::io::Error::other(format!("{error:#}")))?;
std::fs::create_dir_all(&dir)?;
let mut file = OpenOptions::new()
.create(true)
.append(true)
.open(dir.join(FILE_NAME))?;
let now = chrono::Local::now().format("%Y-%m-%d %H:%M:%S %:z");
writeln!(file, "=== Zippa DB crashed at {now} ===")?;
let payload = info.payload();
if let Some(message) = payload.downcast_ref::<&str>() {
writeln!(file, "message: {message}")?;
} else if let Some(message) = payload.downcast_ref::<String>() {
writeln!(file, "message: {message}")?;
} else {
writeln!(file, "message: <non-string panic payload>")?;
}
if let Some(location) = info.location() {
writeln!(file, "location: {location}")?;
}
writeln!(file, "{}", std::backtrace::Backtrace::force_capture())?;
writeln!(file)?;
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
fn scratch_config_dir() -> std::path::PathBuf {
let dir = std::env::temp_dir().join(format!("zippa-db-crash-{}", uuid::Uuid::new_v4()));
std::fs::create_dir_all(&dir).expect("could not create the scratch directory");
store::set_config_dir_for_test(dir.clone());
dir
}
#[test]
fn pending_reads_and_clear_forgets() {
let dir = scratch_config_dir();
assert_eq!(pending(), None);
std::fs::write(dir.join(FILE_NAME), "=== Zippa DB crashed at then ===\n")
.expect("could not write the crash log");
let report = pending().expect("the crash should be pending");
assert_eq!(summary(&report), "=== Zippa DB crashed at then ===");
clear();
assert_eq!(pending(), None);
assert!(!dir.join(FILE_NAME).exists());
}
#[test]
fn a_panic_is_recorded() {
scratch_config_dir();
type Hook = Box<dyn Fn(&std::panic::PanicHookInfo<'_>) + Send + Sync + 'static>;
let original = std::panic::take_hook();
struct Restore(Option<Hook>);
impl Drop for Restore {
fn drop(&mut self) {
if let Some(hook) = self.0.take() {
std::panic::set_hook(hook);
}
}
}
let _restore = Restore(Some(original));
std::panic::set_hook(Box::new(|_| {}));
install();
let _ = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
panic!("the test crash");
}));
let report = pending().expect("the panic should have been recorded");
assert!(report.contains("the test crash"), "report was:\n{report}");
assert!(report.contains("location:"), "report was:\n{report}");
clear();
}
}