use std::collections::HashMap;
use std::panic::{self, UnwindSafe};
use std::process;
use std::sync::{self, Mutex};
use std::thread::{self, ThreadId};
use std::time::Instant;
#[cfg(feature = "panic-failure-backtraces")]
use backtrace::Backtrace;
use once_cell::sync::Lazy;
use crate::expr::Expr;
static CAUGHT_PANICS: Lazy<Mutex<HashMap<ThreadId, (Instant, CaughtPanic)>>> =
Lazy::new(|| Default::default());
#[derive(Clone)]
pub struct CaughtPanic {
message: Option<String>,
location: Option<String>,
#[cfg(feature = "panic-failure-backtraces")]
backtrace: Option<Backtrace>,
}
impl CaughtPanic {
pub(crate) fn to_library_error(&self) -> crate::errors::LibraryError {
let CaughtPanic {
message,
location,
#[cfg(feature = "panic-failure-backtraces")]
backtrace,
} = self.clone();
let message = message.unwrap_or("Rust panic (no message)".into());
let location = location.unwrap_or("Unknown".into());
let backtrace = {
#[cfg(feature = "panic-failure-backtraces")]
{
if should_show_backtrace() {
display_backtrace(backtrace)
} else {
crate::expr::expr!(System::Missing["NotEnabled"])
}
}
#[cfg(not(feature = "panic-failure-backtraces"))]
{
crate::expr::expr!(System::Missing["NotEnabled"])
}
};
crate::errors::LibraryError::RustPanic {
message,
source_location: location,
backtrace,
}
}
}
#[cfg(feature = "panic-failure-backtraces")]
fn should_show_backtrace() -> bool {
std::env::var(crate::BACKTRACE_ENV_VAR).is_ok()
}
#[cfg(feature = "panic-failure-backtraces")]
fn display_backtrace(bt: Option<Backtrace>) -> Expr {
let bt: Expr = if let Some(mut bt) = bt {
bt.resolve();
let mut frames = Vec::new();
for frame in bt.frames() {
use backtrace::{BacktraceSymbol, SymbolName};
let bt_symbol: Option<&BacktraceSymbol> = frame.symbols().last();
let name: String = bt_symbol
.and_then(BacktraceSymbol::name)
.as_ref()
.map(|sym: &SymbolName| format!("{}", sym).trim().to_owned())
.unwrap_or("<unknown>".into());
if name.starts_with("backtrace::") {
continue;
}
let filename = bt_symbol.and_then(BacktraceSymbol::filename);
let lineno = bt_symbol.and_then(BacktraceSymbol::lineno);
let path_str = filename
.map(|p| p.display().to_string().trim().to_owned())
.unwrap_or_default();
let label = match lineno {
Some(line) => format!("{}:{}", path_str, line),
None => path_str.clone(),
};
let file_exists = filename.map(|p| p.exists()).unwrap_or(false);
let location = if file_exists {
let path_clone = path_str.clone();
crate::expr::expr!(System::Button[System::Style[label, System::RGBColor[0.25f64, 0.48f64, 1.0f64], "Small", "FontFamily" -> "Courier"], System::SystemOpen[path_clone], "Appearance" -> "Frameless"])
} else {
crate::expr::expr!(System::Style[label, "Small", "FontFamily" -> "Courier"])
};
let row = if path_str.is_empty() {
Expr::string(name.clone())
} else {
let name_expr = name.clone();
crate::expr::expr!(System::Row[System::List[location, " in ", name_expr]])
};
frames.push(row);
}
crate::expr::expr!(System::Style[System::Column[frames], "FontFamily" -> "Courier"])
} else {
Expr::string("<unable to capture backtrace>")
};
bt
}
pub fn call_and_catch_panic<T, F>(func: F) -> Result<T, CaughtPanic>
where
F: FnOnce() -> T + UnwindSafe,
{
let prev_hook = panic::take_hook();
let _: () = panic::set_hook(Box::new(custom_hook));
let result: Result<T, ()> = panic::catch_unwind(|| func()).map_err(|_| ());
panic::set_hook(prev_hook);
let result: Result<T, CaughtPanic> = result.map_err(|()| get_caught_panic());
result
}
fn get_caught_panic() -> CaughtPanic {
let id = thread::current().id();
let mut map = acquire_lock();
let caught_panic = match map.remove(&id) {
Some((_time, caught_panic)) => caught_panic.clone(),
None => {
match map.len() {
0 => {
let message = format!(
"could not get panic info for current thread. \
Operation of custom panic hook was interrupted"
);
CaughtPanic {
message: Some(message),
location: None,
#[cfg(feature = "panic-failure-backtraces")]
backtrace: None,
}
},
1 => map.values().next().unwrap().1.clone(),
_ => map
.values()
.max_by(|a, b| a.0.cmp(&b.0))
.map(|(_time, info)| info)
.cloned()
.unwrap(),
}
},
};
caught_panic
}
fn custom_hook(info: &panic::PanicHookInfo) {
let caught_panic = {
let message: Option<String> = get_panic_message(info);
let location: Option<String> = info.location().map(ToString::to_string);
#[cfg(feature = "panic-failure-backtraces")]
let backtrace = Some(Backtrace::new_unresolved());
CaughtPanic {
message,
location,
#[cfg(feature = "panic-failure-backtraces")]
backtrace,
}
};
let thread = thread::current();
let data = (Instant::now(), caught_panic);
let mut lock = acquire_lock();
if let Some(_previous) = lock.insert(thread.id(), data) {
}
}
fn get_panic_message(info: &panic::PanicHookInfo) -> Option<String> {
if let Some(string) = info.payload().downcast_ref::<&str>() {
return Some(string.to_string());
}
if let Some(string) = info.payload().downcast_ref::<String>() {
return Some(string.to_owned());
}
#[cfg(feature = "nightly")]
if let Some(fmt_arguments) = info.message() {
return Some(format!("{}", fmt_arguments));
}
None
}
fn acquire_lock() -> sync::MutexGuard<'static, HashMap<ThreadId, (Instant, CaughtPanic)>>
{
let lock = match CAUGHT_PANICS.lock() {
Ok(lock) => lock,
Err(_err) => {
println!(
"catch_panic: acquire_lock: failed to acquire lock. Exiting process."
);
process::exit(-1);
},
};
lock
}