use std::sync::atomic::{AtomicUsize, Ordering};
use std::{cell::UnsafeCell, mem::MaybeUninit};
pub(crate) static CAPTURE: Capture = Capture::new();
pub(crate) struct Capture {
state: AtomicUsize,
slot: UnsafeCell<MaybeUninit<ntex_error::BacktraceRaw>>,
}
unsafe impl Sync for Capture {}
impl Capture {
const IDLE: usize = 0;
const ARMED: usize = 1;
const WRITING: usize = 2;
const READY: usize = 3;
const ABANDONED: usize = 4;
const fn new() -> Self {
Self {
state: AtomicUsize::new(Self::IDLE),
slot: UnsafeCell::new(MaybeUninit::uninit()),
}
}
#[allow(clippy::cast_sign_loss)]
fn state(tid: i32, tag: usize) -> usize {
((tid as u32 as usize) << 3) | tag
}
pub(crate) fn arm(&self, tid: i32) -> bool {
self.state
.compare_exchange(
Self::IDLE,
Self::state(tid, Self::ARMED),
Ordering::Acquire,
Ordering::Relaxed,
)
.is_ok()
}
pub(crate) fn capture(&self, tid: i32, location: &'static str) {
if self
.state
.compare_exchange(
Self::state(tid, Self::ARMED),
Self::state(tid, Self::WRITING),
Ordering::Acquire,
Ordering::Relaxed,
)
.is_err()
{
return;
}
let bt = unsafe { ntex_error::BacktraceRaw::with_filename_unsynchronized(location) };
unsafe { (*self.slot.get()).write(bt) };
if self
.state
.compare_exchange(
Self::state(tid, Self::WRITING),
Self::state(tid, Self::READY),
Ordering::Release,
Ordering::Relaxed,
)
.is_err()
{
unsafe { (*self.slot.get()).assume_init_drop() };
self.state.store(Self::IDLE, Ordering::Release);
}
}
pub(crate) fn take(&self, tid: i32) -> Option<ntex_error::BacktraceRaw> {
if self.state.load(Ordering::Acquire) == Self::state(tid, Self::READY) {
let bt = unsafe { (*self.slot.get()).assume_init_read() };
self.state.store(Self::IDLE, Ordering::Release);
Some(bt)
} else {
None
}
}
pub(crate) fn disarm(&self, tid: i32) {
loop {
let cur = self.state.load(Ordering::Acquire);
let next = if cur == Self::state(tid, Self::ARMED) {
Self::IDLE
} else if cur == Self::state(tid, Self::WRITING) {
Self::state(tid, Self::ABANDONED)
} else if cur == Self::state(tid, Self::READY) {
let _ = self.take(tid);
return;
} else {
return;
};
if self
.state
.compare_exchange(cur, next, Ordering::AcqRel, Ordering::Acquire)
.is_ok()
{
return;
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
static CAPTURE_LOCK: std::sync::Mutex<()> = std::sync::Mutex::new(());
#[allow(clippy::cast_possible_truncation)]
fn gettid() -> i32 {
unsafe { libc::syscall(libc::SYS_gettid) as i32 }
}
#[test]
fn capture_slot() {
let _guard = CAPTURE_LOCK
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
let tid = gettid();
assert!(CAPTURE.arm(tid));
assert!(!CAPTURE.arm(tid + 1));
CAPTURE.capture(tid + 1, file!());
assert!(CAPTURE.take(tid).is_none());
assert!(CAPTURE.take(tid + 1).is_none());
CAPTURE.capture(tid, file!());
assert!(CAPTURE.take(tid + 1).is_none());
assert!(CAPTURE.take(tid).is_some());
assert!(CAPTURE.take(tid).is_none());
CAPTURE.disarm(tid);
assert!(CAPTURE.arm(tid));
CAPTURE.disarm(tid);
CAPTURE.capture(tid, file!());
assert!(CAPTURE.take(tid).is_none());
assert!(CAPTURE.arm(tid));
CAPTURE.capture(tid, file!());
CAPTURE.disarm(tid);
assert!(CAPTURE.arm(tid));
CAPTURE.disarm(tid);
}
#[test]
fn capture_from_signal_handler() {
let _guard = CAPTURE_LOCK
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
let id = unsafe {
signal_hook::low_level::register(signal_hook::consts::SIGUSR2, crate::system::sig_usr2)
.unwrap()
};
let tid = gettid();
assert!(CAPTURE.arm(tid));
let res = unsafe { libc::syscall(libc::SYS_tgkill, libc::getpid(), tid, libc::SIGUSR2) };
assert_eq!(res, 0);
let bt = CAPTURE
.take(tid)
.expect("backtrace captured in signal handler");
CAPTURE.disarm(tid);
signal_hook::low_level::unregister(id);
let bt = ntex_error::Backtrace::from(bt);
drop(bt.resolver().resolve());
assert!(bt.is_resolved());
}
}