use crate::common::diagnostics::NamLogger;
use crate::common::diagnostics::SystemSnapshot;
use crate::common::diagnostics::snapshot::{
ACTIVE_MODEL_INFO, ACTIVE_MODEL_NAME, ACTIVE_SAMPLE_RATE,
};
use core::fmt::Write as FmtWrite;
use std::cell::Cell;
use std::fs::OpenOptions;
use std::io::Write;
use std::path::PathBuf;
use std::sync::OnceLock;
use std::sync::atomic::Ordering;
use std::time::SystemTime;
static SYSTEM_SNAPSHOT: OnceLock<SystemSnapshot> = OnceLock::new();
static SHUTDOWN_IN_PROGRESS: OnceLock<bool> = OnceLock::new();
thread_local! {
static PANIC_HOOK_ACTIVE: Cell<bool> = const { Cell::new(false) };
}
pub fn set_shutdown_in_progress() {
let _ = SHUTDOWN_IN_PROGRESS.set(true);
}
pub fn is_shutdown_in_progress() -> bool {
crate::common::spsc::SHUTDOWN.load(std::sync::atomic::Ordering::Acquire)
|| *SHUTDOWN_IN_PROGRESS.get().unwrap_or(&false)
}
struct LimitWriter<'a> {
buf: &'a mut [u8],
cursor: usize,
}
impl<'a> LimitWriter<'a> {
fn new(buf: &'a mut [u8]) -> Self {
Self { buf, cursor: 0 }
}
}
impl FmtWrite for LimitWriter<'_> {
fn write_str(&mut self, s: &str) -> std::fmt::Result {
let bytes = s.as_bytes();
let remaining = self.buf.len().saturating_sub(self.cursor);
let len = bytes.len().min(remaining);
self.buf[self.cursor..self.cursor + len].copy_from_slice(&bytes[..len]);
self.cursor += len;
Ok(())
}
}
const MAX_CRASH_FILES: usize = 10;
fn prune_old_crash_files(cache_dir: &std::path::Path) {
let mut entries: Vec<(std::path::PathBuf, std::time::SystemTime)> =
match std::fs::read_dir(cache_dir) {
Ok(iter) => iter
.filter_map(|e| e.ok())
.filter(|e| {
e.path()
.file_name()
.and_then(|n| n.to_str())
.map(|n| n.starts_with("crash-") && n.ends_with(".txt"))
.unwrap_or(false)
})
.filter_map(|e| {
let mtime = e.metadata().ok()?.modified().ok()?;
Some((e.path(), mtime))
})
.collect(),
Err(_) => return,
};
if entries.len() <= MAX_CRASH_FILES {
return;
}
entries.sort_by_key(|(_, mtime)| *mtime);
let excess = entries.len() - MAX_CRASH_FILES;
for (path, _) in entries.iter().take(excess) {
let _ = std::fs::remove_file(path);
}
}
fn format_panic_report_to_buf(
buf: &mut [u8],
component: &str,
thread_name: &str,
location: &str,
payload_str: &str,
system: &SystemSnapshot,
) -> usize {
let mut w = LimitWriter::new(buf);
let _ = write!(
w,
"================================================================\n\
NAM-rs CRASH REPORT\n\
================================================================\n\
Version: {}\n\
Component: {}\n\
Thread: {}\n\
Location: {}\n\
Message: {}\n\n",
system.version, component, thread_name, location, payload_str
);
let _ = writeln!(w, "──── Runtime State ─────────────────────────────");
match ACTIVE_MODEL_NAME.try_read() {
Ok(guard) => {
if guard.is_empty() {
let _ = writeln!(w, "model=none");
} else {
let _ = writeln!(w, "model={}", *guard);
}
}
Err(_) => {
let _ = writeln!(w, "model=<unavailable>");
}
}
match ACTIVE_MODEL_INFO.try_read() {
Ok(guard) => {
if let Some(ref info) = *guard {
let _ = writeln!(w, "model.arch={}", info.arch_label);
let _ = writeln!(w, "model.topology={}", info.topology);
let _ = writeln!(w, "model.channels={}", info.channels);
let _ = writeln!(w, "model.receptive_field={}", info.receptive_field);
let _ = writeln!(w, "model.sample_rate={}", info.model_sample_rate);
let _ = writeln!(w, "model.weights_layout={}", info.weights_layout);
let _ = writeln!(w, "model.path_basename={}", info.path_basename);
}
}
Err(_) => {
let _ = writeln!(w, "model.info=<unavailable>");
}
}
let rate = ACTIVE_SAMPLE_RATE.load(Ordering::Relaxed);
if rate != 0 {
let _ = writeln!(w, "audio.sr={}", rate);
}
let _ = writeln!(w, "──── Recent Log Trace ─────────────────────────");
if let Some(buffer) = NamLogger::log_buffer() {
let count = buffer.try_render_trace_into(&mut w, 30);
if count == 0 {
let _ = writeln!(w, "<log buffer locked>");
}
} else {
let _ = writeln!(w, "<log buffer not initialized>");
}
let unix_ts = SystemTime::now()
.duration_since(SystemTime::UNIX_EPOCH)
.map(|d| d.as_secs())
.unwrap_or(0);
let _ = writeln!(w, "──── System Info ─────────────────────────────");
let _ = writeln!(w, "arch={}", system.arch);
let _ = writeln!(w, "os={} kernel={}", system.os, system.kernel);
let _ = write!(
w,
"pipewire={}",
system.pipewire_version.as_deref().unwrap_or("N/A")
);
let _ = write!(w, "\nfeatures=");
if system.features.is_empty() {
let _ = write!(w, "none (baseline x86-64-v3 only)");
} else {
for (i, f) in system.features.iter().enumerate() {
if i > 0 {
let _ = write!(w, ", ");
}
let _ = write!(w, "{}", f);
}
}
let _ = writeln!(w);
let _ = writeln!(w, "timestamp_unix={}", unix_ts);
let _ = write!(
w,
"────────────────────────────────────────────────\n\
Copy the block above when opening a support ticket.\n"
);
w.cursor
}
#[cfg(feature = "heap-audit")]
pub fn format_panic_report_for_audit_test(
buf: &mut [u8],
component: &str,
thread_name: &str,
location: &str,
payload_str: &str,
) -> usize {
let system = SYSTEM_SNAPSHOT
.get()
.expect("SystemSnapshot not initialized");
format_panic_report_to_buf(buf, component, thread_name, location, payload_str, system)
}
pub fn install_panic_hook(component: &'static str) {
SYSTEM_SNAPSHOT.get_or_init(SystemSnapshot::capture);
let prev_hook = std::panic::take_hook();
std::panic::set_hook(Box::new(move |info| {
if is_shutdown_in_progress() {
prev_hook(info);
return;
}
if PANIC_HOOK_ACTIVE.replace(true) {
prev_hook(info);
return;
}
let thread = std::thread::current();
let thread_name = thread.name().unwrap_or("unknown");
let payload = info.payload();
let payload_str = if let Some(s) = payload.downcast_ref::<&str>() {
*s
} else if let Some(s) = payload.downcast_ref::<String>() {
s.as_str()
} else {
"Box<dyn Any>"
};
let mut location_buf = [0u8; 256];
let location_str = {
let mut lw = LimitWriter::new(&mut location_buf);
if let Some(loc) = info.location() {
let _ = write!(lw, "{}:{}:{}", loc.file(), loc.line(), loc.column());
} else {
let _ = write!(lw, "unknown");
}
let len = lw.cursor;
std::str::from_utf8(&location_buf[..len]).unwrap_or("<invalid utf-8>")
};
let system = SYSTEM_SNAPSHOT
.get()
.expect("SystemSnapshot not initialized");
let mut report_buf = [0u8; 16384];
let written = format_panic_report_to_buf(
&mut report_buf,
component,
thread_name,
location_str,
payload_str,
system,
);
if let Some(home_dir) = std::env::var_os("HOME") {
let mut cache_dir = PathBuf::from(home_dir);
cache_dir.push(".cache/nam-rs");
if std::fs::create_dir_all(&cache_dir).is_ok() {
#[cfg(unix)]
{
use std::fs::Permissions;
use std::os::unix::fs::PermissionsExt;
let _ = std::fs::set_permissions(&cache_dir, Permissions::from_mode(0o700));
}
let unix_ts = SystemTime::now()
.duration_since(SystemTime::UNIX_EPOCH)
.map(|d| d.as_secs())
.unwrap_or(0);
let mut filename_buf = [0u8; 128];
let filename = {
let mut lw = LimitWriter::new(&mut filename_buf);
let _ = write!(lw, "crash-{}-{}.txt", unix_ts, component);
let len = lw.cursor;
std::str::from_utf8(&filename_buf[..len]).unwrap_or("crash-unknown.txt")
};
let mut tmp_buf = [0u8; 128];
let tmp_filename = {
let mut lw = LimitWriter::new(&mut tmp_buf);
let _ = write!(lw, "crash-{}-{}.tmp", unix_ts, component);
let len = lw.cursor;
std::str::from_utf8(&tmp_buf[..len]).unwrap_or("crash-unknown.tmp")
};
let file_path = cache_dir.join(filename);
let tmp_file_path = cache_dir.join(tmp_filename);
let mut options = OpenOptions::new();
options.write(true).create(true).truncate(true);
#[cfg(unix)]
{
use std::os::unix::fs::OpenOptionsExt;
options.mode(0o600);
}
if let Ok(mut file) = options.open(&tmp_file_path) {
if file.write_all(&report_buf[..written]).is_ok() {
let _ = file.sync_all();
drop(file);
let _ = std::fs::rename(&tmp_file_path, &file_path);
prune_old_crash_files(&cache_dir);
} else {
let _ = std::fs::remove_file(&tmp_file_path);
}
}
}
}
PANIC_HOOK_ACTIVE.set(false);
prev_hook(info);
}));
}