diff --git a/src/delta.rs b/src/delta.rs index 80c5d7a85..1133c333a 100644 --- a/src/delta.rs +++ b/src/delta.rs @@ -15,6 +15,7 @@ use crate::handlers::{self, merge_conflict}; use crate::paint::Painter; use crate::style::DecorationStyle; use crate::utils; +use crate::utils::RecordDeltaCall; #[derive(Clone, Debug, PartialEq, Eq)] pub enum State { @@ -147,7 +148,11 @@ impl<'a> StateMachine<'a> { where I: BufRead, { + let mut debug_helper = RecordDeltaCall::new(self.config); + while let Some(Ok(raw_line_bytes)) = lines.next() { + debug_helper.write(raw_line_bytes); + self.ingest_line(raw_line_bytes); if self.source == Source::Unknown { diff --git a/src/env.rs b/src/env.rs index a289d8433..31d2daa8b 100644 --- a/src/env.rs +++ b/src/env.rs @@ -10,7 +10,11 @@ const DELTA_EXPERIMENTAL_MAX_LINE_DISTANCE_FOR_NAIVELY_PAIRED_LINES: &str = "DELTA_EXPERIMENTAL_MAX_LINE_DISTANCE_FOR_NAIVELY_PAIRED_LINES"; const DELTA_PAGER: &str = "DELTA_PAGER"; -#[derive(Default, Clone)] +// debug env variables: +pub const DELTA_DEBUG_LOGFILE: &str = "DELTA_DEBUG_LOGFILE"; +pub const DELTA_DEBUG_LOGFILE_MAX_SIZE: &str = "DELTA_DEBUG_LOGFILE_MAX_SIZE"; + +#[derive(Default, Clone, Debug)] pub struct DeltaEnv { pub bat_theme: Option, pub colorterm: Option, diff --git a/src/main.rs b/src/main.rs index e952a3bcb..8bd246500 100644 --- a/src/main.rs +++ b/src/main.rs @@ -43,7 +43,7 @@ where { #[cfg(not(test))] { - eprintln!("{errmsg}"); + eprintln!("delta: {errmsg}"); // As in Config::error_exit_code: use 2 for error // because diff uses 0 and 1 for non-error. process::exit(2); @@ -81,6 +81,7 @@ pub fn run_app( args: Vec, capture_output: Option<&mut Cursor>>, ) -> std::io::Result { + let _panic_printer = utils::PrintNoticeOnPanic::new(); let env = env::DeltaEnv::init(); let assets = utils::bat::assets::load_highlighting_assets(); let (call, opt) = cli::Opt::from_args_and_git_config(args, &env, assets); diff --git a/src/utils/debug.rs b/src/utils/debug.rs new file mode 100644 index 000000000..49f6b9dff --- /dev/null +++ b/src/utils/debug.rs @@ -0,0 +1,201 @@ +use crate::config::Config; +use crate::env::{DeltaEnv, DELTA_DEBUG_LOGFILE, DELTA_DEBUG_LOGFILE_MAX_SIZE}; +use crate::fatal; +use crate::utils::DELTA_ATOMIC_ORDERING; + +use console::Term; + +use std::ffi::OsString; +use std::fs::File; +use std::io::{Seek, SeekFrom, Write}; +use std::sync::atomic::AtomicBool; + +const RUST_BACKTRACE: &str = "RUST_BACKTRACE"; + +// Use a global because the config where this might be stored could +// itself panic while it is being built. +static USING_DELTA_DEBUG_LOGFILE: AtomicBool = AtomicBool::new(false); + +#[derive(Debug)] +pub struct PrintNoticeOnPanic; + +impl PrintNoticeOnPanic { + pub fn new() -> Self { + Self {} + } +} + +#[cfg(not(test))] +impl Drop for PrintNoticeOnPanic { + fn drop(&mut self) { + // Nothing elaborate with std::panic::set_hook or std::panic::catch_unwind to also get the backtrace, + // only set RUST_BACKTRACE when recording + if std::thread::panicking() { + if USING_DELTA_DEBUG_LOGFILE.load(DELTA_ATOMIC_ORDERING) { + if let Some(logfile) = std::env::var_os(DELTA_DEBUG_LOGFILE) { + eprintln!(" Wrote {logfile:?} (if you want to share it, ensure no sensitive information is contained). You may also want to include the stack backtrace."); + } + } else { + // Setting GIT_PAGER however does not override interactive.diffFilter! + eprintln!("\n\ + delta panicked and crashed, sorry :(\n\ + To quickly repeat the previous command without delta, run 'export GIT_PAGER=less' first. If you want\n\ + to report the crash and it is easy to repeat, run 'export DELTA_DEBUG_LOGFILE=crash.log', then delta again,\n\ + then submit the logfile to github at . Thank you!\n\n\ + !! Make sure there is NO sensitive information in the log file, it will contain the entire diff !!\n\ + ") + } + } + } +} + +#[derive(Debug)] +struct DeltaDetailInternal { + file: File, + bytes_written: u64, + max_log_size: u64, + truncate_back_to: u64, +} + +pub struct RecordDeltaCall(Option>); + +impl RecordDeltaCall { + pub fn new(config: &Config) -> Self { + fn make(logfile: &OsString, config: &Config) -> Result, String> { + let mut file = File::create(logfile).map_err(|e| e.to_string())?; + + let mut write = |s: String| file.write_all(s.as_bytes()).map_err(|e| e.to_string()); + + write( + "
\n\ + Input which caused delta to crash\n\n```\n" + .into(), + )?; + + if let Some(cfg) = config.git_config.as_ref() { + write("git config values:\n".into())?; + cfg.for_each(".*", |entry, value: Option<&str>| { + if !(entry.starts_with("user.") + || entry.starts_with("remote.") + || entry.starts_with("branch.") + || entry.starts_with("gui.")) + { + let _ = write(format!("{} = {:?}\n", entry, value.unwrap_or(""))); + } + }) + } else { + write("(NO git config)\n".into())?; + }; + + write(format!( + "command line: {:?}\n", + std::env::args_os().collect::>() + ))?; + let mut delta_env = DeltaEnv::init(); + // redact, not interesting: + delta_env.current_dir = None; + delta_env.hostname = None; + write(format!("DeltaEnv: {:?}\n", delta_env))?; + + let term = Term::stdout(); + + write(format!( + "TERM: {:?}, is_term: {}, size: {:?}\n", + std::env::var_os("TERM"), + term.is_term(), + term.size() + ))?; + + write( + "raw crash input to delta (usually something git diff etc. generated):\n".into(), + )?; + write("================================\n".into())?; + + file.flush().map_err(|e| e.to_string())?; + let truncate_back_to = file.stream_position().map_err(|e| e.to_string())?; + + let max_log_size = std::env::var_os(DELTA_DEBUG_LOGFILE_MAX_SIZE) + .map(|v| { + v.to_string_lossy().parse::().unwrap_or_else(|_| { + fatal(format!( + "Invalid env var value in {} (got {:?}, expected integer)", + DELTA_DEBUG_LOGFILE_MAX_SIZE, v + )); + }) + }) + .unwrap_or(512 * 1024); + + if std::env::var_os(RUST_BACKTRACE).is_none() { + // SAFETY: + // a) we only get here when `DELTA_DEBUG_LOGFILE` is set, which means a user is expecting a crash anyhow + // b) the requirement is "no other threads concurrently writing or reading(!) [env vars], + // other than the ones in this [std::env] module.", the rust backtrace handler should use std::env. + unsafe { + std::env::set_var(RUST_BACKTRACE, "1"); + } + } + + USING_DELTA_DEBUG_LOGFILE.store(true, DELTA_ATOMIC_ORDERING); + + Ok(Box::new(DeltaDetailInternal { + file, + bytes_written: 0, + max_log_size, + truncate_back_to, + })) + } + + if let Some(logfile) = std::env::var_os(DELTA_DEBUG_LOGFILE) { + Self( + make(&logfile, config) + .map_err(|e| { + eprintln!( + "\nnotice: failed to write {logfile:?} given by {DELTA_DEBUG_LOGFILE}: {e}" + ); + }) + .ok(), + ) + } else { + Self(None) + } + } + + #[inline] + pub fn write(&mut self, line: &[u8]) { + if self.0.is_some() { + self._write(line); + } + } + + fn _write(&mut self, line: &[u8]) { + let internal = self.0.as_mut().unwrap(); + if internal.bytes_written > internal.max_log_size { + let _ = internal.file.flush(); + let _ = internal + .file + .seek(SeekFrom::Start(internal.truncate_back_to)); + let _ = internal.file.set_len(internal.truncate_back_to); + let _ = internal.file.write_all( + format!( + "(truncated [max log size is {},\ + set {DELTA_DEBUG_LOGFILE_MAX_SIZE} to override])\n", + internal.max_log_size + ) + .as_bytes(), + ); + internal.bytes_written = 0; + } + let _ = internal.file.write_all(line); + let _ = internal.file.write_all(b"\n"); + internal.bytes_written += line.len() as u64 + 1; + let _ = internal.file.flush(); + } +} + +impl Drop for RecordDeltaCall { + fn drop(&mut self) { + if let Some(ref mut internal) = self.0 { + let _ = internal.file.write_all(b"\n```\n
\n"); + } + } +} diff --git a/src/utils/mod.rs b/src/utils/mod.rs index 2841fcb93..91f1164e2 100644 --- a/src/utils/mod.rs +++ b/src/utils/mod.rs @@ -10,6 +10,10 @@ pub mod syntect; pub mod tabs; pub mod workarounds; +mod debug; +pub use debug::PrintNoticeOnPanic; +pub use debug::RecordDeltaCall; + // Use the most (even overly) strict ordering. Atomics are not used in hot loops so // a one-size-fits-all approach which is never incorrect is okay. pub const DELTA_ATOMIC_ORDERING: std::sync::atomic::Ordering = std::sync::atomic::Ordering::SeqCst;