use std::{fmt::Write, panic::PanicHookInfo, path::PathBuf, time::SystemTime};
fn format_timestamp(timestamp: SystemTime) -> (String, String) {
let duration = timestamp
.duration_since(std::time::UNIX_EPOCH)
.unwrap_or_default();
let secs = duration.as_secs();
let nanos = duration.subsec_nanos();
let days_since_epoch = secs / 86400;
let time_of_day = secs % 86400;
let hours = time_of_day / 3600;
let minutes = (time_of_day % 3600) / 60;
let seconds = time_of_day % 60;
let (year, month, day) = days_to_ymd(days_since_epoch);
let filename =
format!("{year:04}-{month:02}-{day:02}_{hours:02}-{minutes:02}-{seconds:02}-{nanos}");
let display = format!(
"{year:04}-{month:02}-{day:02} {hours:02}:{minutes:02}:{seconds:02} UTC +{nanos}ns"
);
(filename, display)
}
#[allow(
clippy::missing_const_for_fn,
clippy::cast_possible_wrap,
clippy::cast_sign_loss
)]
#[cfg_attr(coverage_nightly, coverage(off))]
fn days_to_ymd(days: u64) -> (u64, u64, u64) {
let days = days as i64 + 719_468;
let era = if days >= 0 { days } else { days - 146_096 } / 146_097;
let doe = (days - era * 146_097) as u64; let yoe = (doe - doe / 1460 + doe / 36_524 - doe / 146_096) / 365; let y = yoe as i64 + era * 400;
let doy = doe - (365 * yoe + yoe / 4 - yoe / 100); let mp = (5 * doy + 2) / 153; let d = doy - (153 * mp + 2) / 5 + 1; let m = if mp < 10 { mp + 3 } else { mp - 9 }; let y = if m <= 2 { y + 1 } else { y };
(y as u64, m, d)
}
#[derive(Debug)]
pub struct CrashReport {
pub timestamp: std::time::SystemTime,
pub panic_message: String,
pub panic_location: Option<String>,
pub backtrace: String,
pub rust_version: &'static str,
pub reovim_version: &'static str,
pub thread_name: Option<String>,
pub thread_id: Option<u64>,
pub server_logs: Option<String>,
pub client_dump_paths: Vec<PathBuf>,
}
impl CrashReport {
#[must_use]
pub fn summary(&self) -> String {
format!(
"{} at {}",
self.panic_message,
self.panic_location.as_deref().unwrap_or("unknown location")
)
}
pub fn write_to_file(&self) -> std::io::Result<PathBuf> {
let dir = super::recovery::recovery_dir();
std::fs::create_dir_all(&dir)?;
let (filename_ts, display_ts) = format_timestamp(self.timestamp);
let filename = format!("crash-{filename_ts}.txt");
let path = dir.join(&filename);
let mut content = format!(
"Reovim Crash Report\n\
==================\n\n\
Timestamp: {}\n\
Reovim Version: {}\n\
Rust Version: {}\n\
Thread: {} (id: {})\n\n\
Panic Message:\n{}\n\n\
Location:\n{}\n\n\
Backtrace:\n{}\n",
display_ts,
self.reovim_version,
self.rust_version,
self.thread_name.as_deref().unwrap_or("unnamed"),
self.thread_id
.map_or_else(|| "unknown".to_string(), |id| id.to_string()),
self.panic_message,
self.panic_location.as_deref().unwrap_or("unknown"),
self.backtrace
);
if let Some(ref logs) = self.server_logs {
content.push_str("\nServer Debug Logs:\n");
content.push_str("------------------\n");
content.push_str(logs);
content.push('\n');
}
if !self.client_dump_paths.is_empty() {
content.push_str("\nClient Debug Dumps:\n");
content.push_str("-------------------\n");
for path in &self.client_dump_paths {
let _ = writeln!(content, " - {}", path.display());
}
}
std::fs::write(&path, content)?;
Ok(path)
}
}
#[must_use]
pub fn generate_crash_report(info: &PanicHookInfo<'_>) -> CrashReport {
let panic_message = info
.payload()
.downcast_ref::<&str>()
.copied()
.map(ToString::to_string)
.or_else(|| info.payload().downcast_ref::<String>().cloned())
.unwrap_or_else(|| "Unknown panic".to_string());
let panic_location = info
.location()
.map(|loc| format!("{}:{}:{}", loc.file(), loc.line(), loc.column()));
let backtrace = std::backtrace::Backtrace::force_capture().to_string();
let current_thread = std::thread::current();
let thread_name = current_thread.name().map(ToString::to_string);
let thread_id_str = format!("{:?}", current_thread.id());
let thread_id = thread_id_str
.strip_prefix("ThreadId(")
.and_then(|s| s.strip_suffix(')'))
.and_then(|s| s.parse::<u64>().ok());
CrashReport {
timestamp: std::time::SystemTime::now(),
panic_message,
panic_location,
backtrace,
rust_version: option_env!("RUSTC_VERSION").unwrap_or("unknown"),
reovim_version: env!("CARGO_PKG_VERSION"),
thread_name,
thread_id,
server_logs: None,
client_dump_paths: Vec::new(),
}
}