diff --git a/Cargo.toml b/Cargo.toml index ebc86c2b2..80d1529c6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -108,5 +108,7 @@ opt-level = 2 codegen-units = 1 lto = true opt-level = 3 -panic = "abort" -strip = true +# Unwind, so a panic fails one task or command instead of quitting the IDE. +panic = "unwind" +# Keep the symbol table: crash reports and logged backtraces name functions. +strip = "debuginfo" diff --git a/src-tauri/src/logging.rs b/src-tauri/src/logging.rs index 03245cf9b..635812fd6 100644 --- a/src-tauri/src/logging.rs +++ b/src-tauri/src/logging.rs @@ -2,9 +2,13 @@ //! (`~/Library/Logs/` on macOS, `//logs` //! elsewhere — the same place as Tauri's `app_log_dir`). +use std::fmt::Write as _; +use std::io::Write as _; use std::path::{Path, PathBuf}; use std::sync::OnceLock; use tracing_appender::non_blocking::WorkerGuard; +use tracing_subscriber::fmt::format::Writer; +use tracing_subscriber::fmt::time::{FormatTime, SystemTime}; use tracing_subscriber::layer::SubscriberExt; use tracing_subscriber::util::SubscriberInitExt; use tracing_subscriber::{EnvFilter, Layer}; @@ -40,12 +44,41 @@ pub fn init(identifier: &str) { .ok()?; let (writer, guard) = tracing_appender::non_blocking(appender); let _ = GUARD.set(guard); + log_panics(dir.clone()); let _ = LOG_DIR.set(dir); Some(tracing_subscriber::fmt::layer().with_ansi(false).with_writer(writer).with_filter(filter())) }); let _ = tracing_subscriber::registry().with(stdout).with(file).try_init(); } +/// Panics go to the log file synchronously: the non-blocking writer may not +/// get to them before a panic that aborts ends the process. +fn log_panics(dir: PathBuf) { + let default = std::panic::take_hook(); + std::panic::set_hook(Box::new(move |info| { + let _ = append(&dir, &panic_record(info)); + default(info); + })); +} + +fn panic_record(info: &std::panic::PanicHookInfo) -> String { + let mut record = String::new(); + let _ = SystemTime.format_time(&mut Writer::new(&mut record)); + let thread = std::thread::current(); + let _ = write!( + record, + " ERROR panic: thread '{}' {info}\n{}\n", + thread.name().unwrap_or(""), + std::backtrace::Backtrace::force_capture() + ); + record +} + +fn append(dir: &Path, record: &str) -> std::io::Result<()> { + let path = latest_log(dir).unwrap_or_else(|| dir.join(format!("{PREFIX}.panic.{SUFFIX}"))); + std::fs::OpenOptions::new().create(true).append(true).open(path)?.write_all(record.as_bytes()) +} + pub fn log_dir() -> Option<&'static Path> { LOG_DIR.get().map(PathBuf::as_path) } @@ -76,6 +109,27 @@ mod tests { std::fs::remove_dir_all(&dir).unwrap(); } + #[test] + fn panic_record_names_thread_location_and_message() { + let dir = std::env::temp_dir().join(format!("openidea-panic-test-{}", std::process::id())); + std::fs::create_dir_all(&dir).unwrap(); + std::fs::write(dir.join("openidea.2026-01-01.log"), "earlier\n").unwrap(); + let record = std::sync::Arc::new(std::sync::Mutex::new(String::new())); + let sink = record.clone(); + let default = std::panic::take_hook(); + std::panic::set_hook(Box::new(move |info| *sink.lock().unwrap() = panic_record(info))); + let _ = std::thread::Builder::new().name("worker".into()).spawn(|| panic!("boom")).unwrap().join(); + std::panic::set_hook(default); + let record = record.lock().unwrap().clone(); + let location = format!("ERROR panic: thread 'worker' panicked at {}:", file!()); + assert!(record.contains(&location), "{record}"); + assert!(record.contains("boom"), "{record}"); + append(&dir, &record).unwrap(); + let log = std::fs::read_to_string(dir.join("openidea.2026-01-01.log")).unwrap(); + assert!(log.starts_with("earlier\n") && log.ends_with(&record)); + std::fs::remove_dir_all(&dir).unwrap(); + } + #[test] fn log_dir_is_per_identifier() { let d = log_dir_for("com.example.x").unwrap();