use std::any::Any;
use std::io::Write as _;
use std::path::{Path, PathBuf};
use std::sync::atomic::{AtomicBool, Ordering};
pub(crate) const PANIC_LOG_NAME: &str = "panic.log";
pub(crate) const BACKTRACE_CAP: usize = 64 * 1024;
pub(crate) const TRUNCATION_MARK: &str = "... [backtrace truncated at 65536 bytes]";
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct PanicRecord {
pub(crate) file: Option<String>,
pub(crate) line: Option<u32>,
pub(crate) column: Option<u32>,
pub(crate) message: String,
pub(crate) opencrabs_frame: Option<String>,
}
impl PanicRecord {
pub(crate) fn location(&self) -> String {
match (self.file.as_deref(), self.line) {
(Some(file), Some(line)) => match self.column {
Some(column) => format!("{file}:{line}:{column}"),
None => format!("{file}:{line}"),
},
_ => "unknown location".to_string(),
}
}
pub(crate) fn header(&self) -> String {
match self.opencrabs_frame.as_deref() {
Some(frame) => format!("PANIC {} {frame} :: {}", self.location(), self.message),
None => format!("PANIC {} :: {}", self.location(), self.message),
}
}
pub(crate) fn report(&self, backtrace: &str) -> String {
let mut out = String::with_capacity(self.header().len() + 1024);
out.push_str(&self.header());
out.push('\n');
if backtrace.is_empty() {
out.push_str("backtrace: none captured\n");
return out;
}
if backtrace.len() <= BACKTRACE_CAP {
out.push_str(backtrace);
} else {
let mut end = BACKTRACE_CAP;
while end > 0 && !backtrace.is_char_boundary(end) {
end -= 1;
}
out.push_str(&backtrace[..end]);
out.push('\n');
out.push_str(TRUNCATION_MARK);
}
if !out.ends_with('\n') {
out.push('\n');
}
out
}
}
pub(crate) fn payload_message(payload: &(dyn Any + Send)) -> String {
if let Some(s) = payload.downcast_ref::<&str>() {
(*s).to_string()
} else if let Some(s) = payload.downcast_ref::<String>() {
s.clone()
} else if let Some(s) = payload.downcast_ref::<char>() {
s.to_string()
} else {
"panic payload was not a string".to_string()
}
}
pub(crate) fn panic_log_path(log_dir: &Path) -> PathBuf {
log_dir.join(PANIC_LOG_NAME)
}
static PERSISTING: AtomicBool = AtomicBool::new(false);
pub(crate) struct PersistGuard;
impl PersistGuard {
pub(crate) fn try_enter() -> Option<Self> {
if PERSISTING.swap(true, Ordering::AcqRel) {
None
} else {
Some(Self)
}
}
}
impl Drop for PersistGuard {
fn drop(&mut self) {
PERSISTING.store(false, Ordering::Release);
}
}
pub(crate) fn persist(record: &PanicRecord, backtrace: &str, log_dir: &Path) -> Option<PathBuf> {
let _guard = PersistGuard::try_enter()?;
let path = panic_log_path(log_dir);
persist_into(record, backtrace, &path).then_some(path)
}
pub(crate) fn persist_into(record: &PanicRecord, backtrace: &str, path: &Path) -> bool {
tracing::error!("{}", record.header());
match write_record(path, &record.report(backtrace)) {
Ok(()) => true,
Err(e) => {
tracing::warn!("panic record not written to {}: {e}", path.display());
false
}
}
}
fn write_record(path: &Path, body: &str) -> std::io::Result<()> {
if let Some(dir) = path.parent() {
std::fs::create_dir_all(dir)?;
}
let mut file = std::fs::OpenOptions::new()
.create(true)
.append(true)
.open(path)?;
file.write_all(body.as_bytes())?;
file.flush()
}