Skip to main content

reovim_kernel/panic/
report.rs

1//! Crash report generation.
2//!
3//! Linux equivalent: `kernel/panic.c` (panic message formatting)
4//!
5//! Generates crash reports with debugging information.
6
7use std::{fmt::Write, panic::PanicHookInfo, path::PathBuf, time::SystemTime};
8
9/// Format a timestamp for crash reports.
10///
11/// Returns `(filename_format, display_format)`:
12/// - filename: `2026-02-04_12-34-56` (filesystem-safe)
13/// - display: `2026-02-04 12:34:56 UTC +123456789ns`
14fn format_timestamp(timestamp: SystemTime) -> (String, String) {
15    let duration = timestamp
16        .duration_since(std::time::UNIX_EPOCH)
17        .unwrap_or_default();
18
19    let secs = duration.as_secs();
20    let nanos = duration.subsec_nanos();
21
22    // Convert seconds since epoch to date/time components
23    // Using simple calculation (no external crate dependency)
24    let days_since_epoch = secs / 86400;
25    let time_of_day = secs % 86400;
26
27    let hours = time_of_day / 3600;
28    let minutes = (time_of_day % 3600) / 60;
29    let seconds = time_of_day % 60;
30
31    // Calculate year, month, day from days since 1970-01-01
32    let (year, month, day) = days_to_ymd(days_since_epoch);
33
34    let filename =
35        format!("{year:04}-{month:02}-{day:02}_{hours:02}-{minutes:02}-{seconds:02}-{nanos}");
36
37    let display = format!(
38        "{year:04}-{month:02}-{day:02} {hours:02}:{minutes:02}:{seconds:02} UTC +{nanos}ns"
39    );
40
41    (filename, display)
42}
43
44/// Convert days since Unix epoch to (year, month, day).
45#[allow(
46    clippy::missing_const_for_fn,
47    clippy::cast_possible_wrap,
48    clippy::cast_sign_loss
49)]
50#[cfg_attr(coverage_nightly, coverage(off))]
51fn days_to_ymd(days: u64) -> (u64, u64, u64) {
52    // Shift to March 1, 2000 as epoch (simplifies leap year calc)
53    let days = days as i64 + 719_468; // days from year 0 to 1970-01-01
54
55    let era = if days >= 0 { days } else { days - 146_096 } / 146_097;
56    let doe = (days - era * 146_097) as u64; // day of era [0, 146096]
57    let yoe = (doe - doe / 1460 + doe / 36_524 - doe / 146_096) / 365; // year of era [0, 399]
58    let y = yoe as i64 + era * 400;
59    let doy = doe - (365 * yoe + yoe / 4 - yoe / 100); // day of year [0, 365]
60    let mp = (5 * doy + 2) / 153; // month index [0, 11]
61    let d = doy - (153 * mp + 2) / 5 + 1; // day [1, 31]
62    let m = if mp < 10 { mp + 3 } else { mp - 9 }; // month [1, 12]
63    let y = if m <= 2 { y + 1 } else { y };
64
65    (y as u64, m, d)
66}
67
68/// Crash report with all debugging info.
69#[derive(Debug)]
70pub struct CrashReport {
71    /// Timestamp of the crash.
72    pub timestamp: std::time::SystemTime,
73    /// Panic message.
74    pub panic_message: String,
75    /// Source location of the panic.
76    pub panic_location: Option<String>,
77    /// Stack backtrace.
78    pub backtrace: String,
79    /// Rust compiler version.
80    pub rust_version: &'static str,
81    /// Reovim version.
82    pub reovim_version: &'static str,
83    /// Thread name where panic occurred.
84    pub thread_name: Option<String>,
85    /// Thread ID where panic occurred.
86    pub thread_id: Option<u64>,
87    /// Server debug ring buffer dump (Phase #478).
88    /// Set by server via callback during panic.
89    pub server_logs: Option<String>,
90    /// Paths to client debug dump files (Phase #478).
91    /// Set by server via callback during panic.
92    pub client_dump_paths: Vec<PathBuf>,
93}
94
95impl CrashReport {
96    /// Get a short summary of the crash.
97    #[must_use]
98    pub fn summary(&self) -> String {
99        format!(
100            "{} at {}",
101            self.panic_message,
102            self.panic_location.as_deref().unwrap_or("unknown location")
103        )
104    }
105
106    /// Write the crash report to a file.
107    ///
108    /// # Returns
109    ///
110    /// Path to the crash report file.
111    ///
112    /// # Errors
113    ///
114    /// Returns an error if the crash directory cannot be created or the file cannot be written.
115    pub fn write_to_file(&self) -> std::io::Result<PathBuf> {
116        let dir = super::recovery::recovery_dir();
117        std::fs::create_dir_all(&dir)?;
118
119        // Format timestamp for filename and display
120        let (filename_ts, display_ts) = format_timestamp(self.timestamp);
121        let filename = format!("crash-{filename_ts}.txt");
122        let path = dir.join(&filename);
123
124        let mut content = format!(
125            "Reovim Crash Report\n\
126==================\n\n\
127Timestamp: {}\n\
128Reovim Version: {}\n\
129Rust Version: {}\n\
130Thread: {} (id: {})\n\n\
131Panic Message:\n{}\n\n\
132Location:\n{}\n\n\
133Backtrace:\n{}\n",
134            display_ts,
135            self.reovim_version,
136            self.rust_version,
137            self.thread_name.as_deref().unwrap_or("unnamed"),
138            self.thread_id
139                .map_or_else(|| "unknown".to_string(), |id| id.to_string()),
140            self.panic_message,
141            self.panic_location.as_deref().unwrap_or("unknown"),
142            self.backtrace
143        );
144
145        // Append server logs if available
146        if let Some(ref logs) = self.server_logs {
147            content.push_str("\nServer Debug Logs:\n");
148            content.push_str("------------------\n");
149            content.push_str(logs);
150            content.push('\n');
151        }
152
153        // Append client dump paths if available
154        if !self.client_dump_paths.is_empty() {
155            content.push_str("\nClient Debug Dumps:\n");
156            content.push_str("-------------------\n");
157            for path in &self.client_dump_paths {
158                let _ = writeln!(content, "  - {}", path.display());
159            }
160        }
161
162        std::fs::write(&path, content)?;
163        Ok(path)
164    }
165}
166
167/// Generate a crash report from panic info.
168///
169/// # Arguments
170///
171/// * `info` - Panic hook info from the panic handler
172///
173/// # Returns
174///
175/// A `CrashReport` with all available debugging information.
176#[must_use]
177pub fn generate_crash_report(info: &PanicHookInfo<'_>) -> CrashReport {
178    let panic_message = info
179        .payload()
180        .downcast_ref::<&str>()
181        .copied()
182        .map(ToString::to_string)
183        .or_else(|| info.payload().downcast_ref::<String>().cloned())
184        .unwrap_or_else(|| "Unknown panic".to_string());
185
186    let panic_location = info
187        .location()
188        .map(|loc| format!("{}:{}:{}", loc.file(), loc.line(), loc.column()));
189
190    let backtrace = std::backtrace::Backtrace::force_capture().to_string();
191
192    // Get thread info
193    let current_thread = std::thread::current();
194    let thread_name = current_thread.name().map(ToString::to_string);
195    // ThreadId doesn't expose the raw value on stable, so we use Debug format
196    let thread_id_str = format!("{:?}", current_thread.id());
197    // Parse the thread ID from "ThreadId(N)" format
198    let thread_id = thread_id_str
199        .strip_prefix("ThreadId(")
200        .and_then(|s| s.strip_suffix(')'))
201        .and_then(|s| s.parse::<u64>().ok());
202
203    CrashReport {
204        timestamp: std::time::SystemTime::now(),
205        panic_message,
206        panic_location,
207        backtrace,
208        rust_version: option_env!("RUSTC_VERSION").unwrap_or("unknown"),
209        reovim_version: env!("CARGO_PKG_VERSION"),
210        thread_name,
211        thread_id,
212        // These are populated by server via callback before write_to_file()
213        server_logs: None,
214        client_dump_paths: Vec::new(),
215    }
216}