use std::str::FromStr; use std::{env, error::Error, path::PathBuf, thread}; use native_dialog::{MessageDialog, MessageType}; use parking_lot::Once; use tracing::error; static INIT: Once = Once::new(); pub fn set_panic_hook() { INIT.call_once(|| { let default_hook = std::panic::take_hook(); std::panic::set_hook(Box::new(move |info| { if let Err(e) = process_panic(info) { eprintln!("Error in panic hook: {}", e); default_hook(info); } std::process::exit(1); })); }); } fn process_panic(info: &std::panic::PanicHookInfo<'_>) -> Result<(), Box> { use std::io::Write; use colored::Colorize; let log_dir = PathBuf::from_str("./").expect("wtf, The current directory no longer exists?"); if !log_dir.exists() { std::fs::create_dir_all(&log_dir)?; } let log_path = log_dir.join("panic.log"); let mut file = std::fs::File::create(&log_path)?; let payload = info.payload(); let payload_str = if let Some(s) = payload.downcast_ref::<&str>() { *s } else if let Some(s) = payload.downcast_ref::() { s } else { "" }; writeln!(file, "Panic Occurred: {}", payload_str)?; if let Some(location) = info.location() { writeln!(file, "Panic Location: {}", location)?; } writeln!(file, "{}", capture_backtrace().sanitize_path())?; // Add more contextual information writeln!(file, "\n--- Additional Information ---")?; // Rust Version if let Ok(rust_version) = rust_version() { writeln!(file, "Rust Version: {}", rust_version)?; } // Command-line Arguments writeln!(file, "Command-line Arguments:")?; for arg in env::args() { writeln!(file, " {}", arg)?; } // Environment Variables (consider filtering sensitive ones) writeln!(file, "\nEnvironment Variables (selected):")?; let interesting_env_vars = ["PATH", "RUST_VERSION", "CARGO_TARGET_DIR", "HOME", "USER"]; for (key, value) in env::vars() { if interesting_env_vars.contains(&key.as_str()) { writeln!(file, " {}: {}", key, value)?; } } // Current Working Directory if let Ok(cwd) = env::current_dir() { writeln!(file, "\nCurrent Working Directory: {}", cwd.display())?; } // Thread Information if let Some(thread) = thread::current().name() { writeln!(file, "\nThread Name: {}", thread)?; } else { writeln!(file, "\nThread ID: {:?}", thread::current().id())?; } let panic_msg = format!( r#"Zenyx had a problem and crashed. To help us diagnose the problem you can send us a crash report. We have generated a detailed report file at '{}'. Submit an issue or email with the subject of 'Zenyx Crash Report' and include the report as an attachment. To submit the crash report: https://codeberg.org/Caznix/Zenyx/issues We take privacy seriously, and do not perform any automated error collection. In order to improve the software, we rely on people to submit reports. Thank you kindly!"#, log_path.display() ); let final_msg = format!( r#"{} For future reference, the error summary is as follows: {} More details can be found in the crash report file."#, panic_msg, payload_str ); println!("{}", final_msg.red().bold()); if let Err(e) = MessageDialog::new() .set_type(MessageType::Error) .set_title("A fatal error in Zenyx has occurred") .set_text(&final_msg) .show_confirm() { error!("Failed to show message dialog: {e}") } Ok(()) } fn rust_version() -> Result> { let version = env!("CARGO_PKG_RUST_VERSION"); Ok(version.to_string()) } fn capture_backtrace() -> String { let mut backtrace = String::new(); let sysinfo = crate::metadata::SystemMetadata::current(); backtrace.push_str(&format!( "--- System Information ---\n{}\n", sysinfo.verbose_summary() )); let trace = std::backtrace::Backtrace::force_capture(); let message = format!("\n--- Backtrace ---\n\n"); backtrace.push_str(&message); backtrace.push_str(&format!("{trace:#}")); backtrace } trait Sanitize { fn sanitize_path(&self) -> String; } impl Sanitize for str { fn sanitize_path(&self) -> String { let prefixes = ["/home/", "/Users/", "\\Users\\", "/opt/home/"]; let mut result = String::from(self); for prefix in prefixes { if let Some(start_index) = result.find(prefix) { let start_of_user = start_index + prefix.len(); let mut end_of_user = result[start_of_user..] .find(|c| c == '/' || c == '\\') .map(|i| start_of_user + i) .unwrap_or(result.len()); if end_of_user == start_of_user && start_of_user < result.len() { end_of_user = result.len(); } result.replace_range(start_of_user..end_of_user, ""); break; } } result } } #[cfg(test)] mod tests { use super::*; #[test] fn test_sanitize_home() { assert_eq!( "/home//documents", "/home/john.doe/documents".sanitize_path() ); assert_eq!("/home/", "/home/jane".sanitize_path()); assert_eq!("/opt/home/", "/opt/home/user".sanitize_path()); } #[test] fn test_sanitize_users_unix() { assert_eq!( "/Users//desktop", "/Users/alice/desktop".sanitize_path() ); assert_eq!("/Users//", "/Users/bob/".sanitize_path()); assert_eq!("/user/Users/", "/user/Users/name".sanitize_path()); } #[test] fn test_sanitize_users_windows() { assert_eq!( "\\Users\\\\documents", "\\Users\\charlie\\documents".sanitize_path() ); assert_eq!("\\Users\\", "\\Users\\david".sanitize_path()); assert_eq!( "C:\\Other\\Users\\", "C:\\Other\\Users\\folder".sanitize_path() ); } #[test] fn test_no_match() { assert_eq!("/opt/data/file.txt", "/opt/data/file.txt".sanitize_path()); } #[test] fn test_mixed_separators() { assert_eq!( "/home/\\documents", "/home/eve\\documents".sanitize_path() ); assert_eq!( "\\Users\\/desktop", "\\Users\\frank/desktop".sanitize_path() ); } }