Skip to content

Commit

Permalink
Print a notice when delta panics
Browse files Browse the repository at this point in the history
Setting DELTA_DEBUG_LOGFILE=crash.log and repeating the command
writes all information needed to reproduce the crash into that file.
  • Loading branch information
th1000s committed Dec 9, 2024
1 parent 440cdd3 commit c0c4e6a
Show file tree
Hide file tree
Showing 5 changed files with 217 additions and 2 deletions.
5 changes: 5 additions & 0 deletions src/delta.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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 {
Expand Down
6 changes: 5 additions & 1 deletion src/env.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<String>,
pub colorterm: Option<String>,
Expand Down
3 changes: 2 additions & 1 deletion src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -81,6 +81,7 @@ pub fn run_app(
args: Vec<OsString>,
capture_output: Option<&mut Cursor<Vec<u8>>>,
) -> std::io::Result<i32> {
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);
Expand Down
201 changes: 201 additions & 0 deletions src/utils/debug.rs
Original file line number Diff line number Diff line change
@@ -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, do so after 'export DELTA_DEBUG_LOGFILE=crash.log'\n\
then submit the logfile to github at <https://github.com/dandavison/delta/issues>. 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<Box<DeltaDetailInternal>>);

impl RecordDeltaCall {
pub fn new(config: &Config) -> Self {
fn make(logfile: &OsString, config: &Config) -> Result<Box<DeltaDetailInternal>, 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(
"<details>\n\
<summary>Input which caused delta to crash</summary>\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::<Vec<_>>()
))?;
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::<u64>().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</details>\n");
}
}
}
4 changes: 4 additions & 0 deletions src/utils/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;

0 comments on commit c0c4e6a

Please sign in to comment.