pub mod report;
use std::{
any::Any,
backtrace::Backtrace,
env,
io::{self, Write},
panic::{self, PanicHookInfo},
process,
sync::{
Once,
atomic::{AtomicBool, Ordering},
},
};
use tracing::error;
use crate::fatal::report::{FatalKind, FatalReport, Origin};
static ARMED: AtomicBool = AtomicBool::new(true);
static INSTALLED: Once = Once::new();
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct FatalConfig {
pub armed: bool,
}
impl Default for FatalConfig {
fn default() -> Self {
Self {
armed: env_armed().unwrap_or(true),
}
}
}
impl FatalConfig {
pub fn armed(armed: bool) -> Self {
Self {
armed,
}
}
pub fn disarmed() -> Self {
Self::armed(false)
}
}
fn env_armed() -> Option<bool> {
match env::var("REIFYDB_FATAL").ok()?.as_str() {
"0" | "off" | "false" => Some(false),
"1" | "on" | "true" => Some(true),
_ => None,
}
}
pub fn install(config: FatalConfig) {
ARMED.store(config.armed, Ordering::Release);
INSTALLED.call_once(|| {
panic::set_hook(Box::new(on_panic));
});
}
pub fn arm() {
ARMED.store(true, Ordering::Release);
}
pub fn disarm() {
ARMED.store(false, Ordering::Release);
}
pub fn is_armed() -> bool {
ARMED.load(Ordering::Acquire)
}
pub fn emit(report: &FatalReport) {
let rendered = report.render();
error!(
fatal.id = %report.error_id(),
fatal.kind = report.kind.as_str(),
fatal.thread = %report.thread_name,
"{}",
report.reason
);
let mut err = io::stderr().lock();
let _ = writeln!(err, "{}", rendered);
let _ = err.flush();
}
pub fn fatal(report: FatalReport) -> ! {
emit(&report);
process::abort()
}
fn on_panic(info: &PanicHookInfo<'_>) {
let backtrace = Backtrace::force_capture();
let mut report = FatalReport::new(FatalKind::Panic, panic_message(info));
if let Some(location) = info.location() {
report = report.origin(Origin::new(location.file(), location.line(), location.column()));
}
if !is_armed() {
emit(&report.backtrace(backtrace.to_string()));
return;
}
fatal(report.backtrace(backtrace.to_string()))
}
pub fn panic_message(info: &PanicHookInfo<'_>) -> String {
let payload = info.payload();
if let Some(message) = payload.downcast_ref::<&'static str>() {
(*message).to_string()
} else if let Some(message) = payload.downcast_ref::<String>() {
message.clone()
} else {
"<non-string panic payload>".to_string()
}
}
pub fn describe_payload(payload: &Box<dyn Any + Send>) -> String {
if let Some(message) = payload.downcast_ref::<&'static str>() {
(*message).to_string()
} else if let Some(message) = payload.downcast_ref::<String>() {
message.clone()
} else {
"<non-string panic payload>".to_string()
}
}
#[macro_export]
macro_rules! fatal {
($reason:expr) => {
$crate::fatal::fatal(
$crate::fatal::report::FatalReport::new($crate::fatal::report::FatalKind::Invariant, $reason)
.origin($crate::fatal::report::Origin::new(file!(), line!(), column!()))
.backtrace(std::backtrace::Backtrace::force_capture().to_string()),
)
};
($fmt:expr, $($arg:tt)*) => {
$crate::fatal::fatal(
$crate::fatal::report::FatalReport::new($crate::fatal::report::FatalKind::Invariant, format!($fmt, $($arg)*))
.origin($crate::fatal::report::Origin::new(file!(), line!(), column!()))
.backtrace(std::backtrace::Backtrace::force_capture().to_string()),
)
};
}
#[macro_export]
macro_rules! fatal_on_err {
($expr:expr) => {
match $expr {
Ok(value) => value,
Err(err) => $crate::fatal::fatal(
$crate::fatal::report::FatalReport::new(
$crate::fatal::report::FatalKind::Error,
format!("{:?}", err),
)
.origin($crate::fatal::report::Origin::new(file!(), line!(), column!()))
.backtrace(std::backtrace::Backtrace::force_capture().to_string()),
),
}
};
($expr:expr, $component:expr) => {
match $expr {
Ok(value) => value,
Err(err) => $crate::fatal::fatal(
$crate::fatal::report::FatalReport::new(
$crate::fatal::report::FatalKind::Error,
format!("{:?}", err),
)
.component($component)
.origin($crate::fatal::report::Origin::new(file!(), line!(), column!()))
.backtrace(std::backtrace::Backtrace::force_capture().to_string()),
),
}
};
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn the_default_config_is_armed_because_a_forgotten_flag_must_not_reopen_the_hole() {
assert!(FatalConfig::default().armed);
}
#[test]
fn the_env_override_only_accepts_known_spellings() {
assert_eq!(FatalConfig::disarmed().armed, false);
assert!(FatalConfig::armed(true).armed);
}
#[test]
fn a_string_panic_payload_survives_into_the_reason() {
let payload: Box<dyn Any + Send> = Box::new("boom".to_string());
assert_eq!(describe_payload(&payload), "boom");
}
#[test]
fn a_static_str_panic_payload_survives_into_the_reason() {
let payload: Box<dyn Any + Send> = Box::new("boom");
assert_eq!(describe_payload(&payload), "boom");
}
#[test]
fn an_opaque_panic_payload_is_named_rather_than_rendered_empty() {
let payload: Box<dyn Any + Send> = Box::new(42u32);
assert_eq!(describe_payload(&payload), "<non-string panic payload>");
}
}