Skip to content

Commit c0c4e6a

Browse files
committed
Print a notice when delta panics
Setting DELTA_DEBUG_LOGFILE=crash.log and repeating the command writes all information needed to reproduce the crash into that file.
1 parent 440cdd3 commit c0c4e6a

File tree

5 files changed

+217
-2
lines changed

5 files changed

+217
-2
lines changed

src/delta.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ use crate::handlers::{self, merge_conflict};
1515
use crate::paint::Painter;
1616
use crate::style::DecorationStyle;
1717
use crate::utils;
18+
use crate::utils::RecordDeltaCall;
1819

1920
#[derive(Clone, Debug, PartialEq, Eq)]
2021
pub enum State {
@@ -147,7 +148,11 @@ impl<'a> StateMachine<'a> {
147148
where
148149
I: BufRead,
149150
{
151+
let mut debug_helper = RecordDeltaCall::new(self.config);
152+
150153
while let Some(Ok(raw_line_bytes)) = lines.next() {
154+
debug_helper.write(raw_line_bytes);
155+
151156
self.ingest_line(raw_line_bytes);
152157

153158
if self.source == Source::Unknown {

src/env.rs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,11 @@ const DELTA_EXPERIMENTAL_MAX_LINE_DISTANCE_FOR_NAIVELY_PAIRED_LINES: &str =
1010
"DELTA_EXPERIMENTAL_MAX_LINE_DISTANCE_FOR_NAIVELY_PAIRED_LINES";
1111
const DELTA_PAGER: &str = "DELTA_PAGER";
1212

13-
#[derive(Default, Clone)]
13+
// debug env variables:
14+
pub const DELTA_DEBUG_LOGFILE: &str = "DELTA_DEBUG_LOGFILE";
15+
pub const DELTA_DEBUG_LOGFILE_MAX_SIZE: &str = "DELTA_DEBUG_LOGFILE_MAX_SIZE";
16+
17+
#[derive(Default, Clone, Debug)]
1418
pub struct DeltaEnv {
1519
pub bat_theme: Option<String>,
1620
pub colorterm: Option<String>,

src/main.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ where
4343
{
4444
#[cfg(not(test))]
4545
{
46-
eprintln!("{errmsg}");
46+
eprintln!("delta: {errmsg}");
4747
// As in Config::error_exit_code: use 2 for error
4848
// because diff uses 0 and 1 for non-error.
4949
process::exit(2);
@@ -81,6 +81,7 @@ pub fn run_app(
8181
args: Vec<OsString>,
8282
capture_output: Option<&mut Cursor<Vec<u8>>>,
8383
) -> std::io::Result<i32> {
84+
let _panic_printer = utils::PrintNoticeOnPanic::new();
8485
let env = env::DeltaEnv::init();
8586
let assets = utils::bat::assets::load_highlighting_assets();
8687
let (call, opt) = cli::Opt::from_args_and_git_config(args, &env, assets);

src/utils/debug.rs

Lines changed: 201 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,201 @@
1+
use crate::config::Config;
2+
use crate::env::{DeltaEnv, DELTA_DEBUG_LOGFILE, DELTA_DEBUG_LOGFILE_MAX_SIZE};
3+
use crate::fatal;
4+
use crate::utils::DELTA_ATOMIC_ORDERING;
5+
6+
use console::Term;
7+
8+
use std::ffi::OsString;
9+
use std::fs::File;
10+
use std::io::{Seek, SeekFrom, Write};
11+
use std::sync::atomic::AtomicBool;
12+
13+
const RUST_BACKTRACE: &str = "RUST_BACKTRACE";
14+
15+
// Use a global because the config where this might be stored could
16+
// itself panic while it is being built.
17+
static USING_DELTA_DEBUG_LOGFILE: AtomicBool = AtomicBool::new(false);
18+
19+
#[derive(Debug)]
20+
pub struct PrintNoticeOnPanic;
21+
22+
impl PrintNoticeOnPanic {
23+
pub fn new() -> Self {
24+
Self {}
25+
}
26+
}
27+
28+
#[cfg(not(test))]
29+
impl Drop for PrintNoticeOnPanic {
30+
fn drop(&mut self) {
31+
// Nothing elaborate with std::panic::set_hook or std::panic::catch_unwind to also get the backtrace,
32+
// only set RUST_BACKTRACE when recording
33+
if std::thread::panicking() {
34+
if USING_DELTA_DEBUG_LOGFILE.load(DELTA_ATOMIC_ORDERING) {
35+
if let Some(logfile) = std::env::var_os(DELTA_DEBUG_LOGFILE) {
36+
eprintln!(" Wrote {logfile:?} (if you want to share it, ensure no sensitive information is contained). You may also want to include the stack backtrace.");
37+
}
38+
} else {
39+
// Setting GIT_PAGER however does not override interactive.diffFilter!
40+
eprintln!("\n\
41+
delta panicked and crashed, sorry :(\n\
42+
To quickly repeat the previous command without delta, run 'export GIT_PAGER=less' first. If you want\n\
43+
to report the crash and it is easy to repeat, do so after 'export DELTA_DEBUG_LOGFILE=crash.log'\n\
44+
then submit the logfile to github at <https://github.com/dandavison/delta/issues>. Thank you!\n\n\
45+
!! Make sure there is NO sensitive information in the log file, it will contain the entire diff !!\n\
46+
")
47+
}
48+
}
49+
}
50+
}
51+
52+
#[derive(Debug)]
53+
struct DeltaDetailInternal {
54+
file: File,
55+
bytes_written: u64,
56+
max_log_size: u64,
57+
truncate_back_to: u64,
58+
}
59+
60+
pub struct RecordDeltaCall(Option<Box<DeltaDetailInternal>>);
61+
62+
impl RecordDeltaCall {
63+
pub fn new(config: &Config) -> Self {
64+
fn make(logfile: &OsString, config: &Config) -> Result<Box<DeltaDetailInternal>, String> {
65+
let mut file = File::create(logfile).map_err(|e| e.to_string())?;
66+
67+
let mut write = |s: String| file.write_all(s.as_bytes()).map_err(|e| e.to_string());
68+
69+
write(
70+
"<details>\n\
71+
<summary>Input which caused delta to crash</summary>\n```\n\n"
72+
.into(),
73+
)?;
74+
75+
if let Some(cfg) = config.git_config.as_ref() {
76+
write("git config values:\n".into())?;
77+
cfg.for_each(".*", |entry, value: Option<&str>| {
78+
if !(entry.starts_with("user.")
79+
|| entry.starts_with("remote.")
80+
|| entry.starts_with("branch.")
81+
|| entry.starts_with("gui."))
82+
{
83+
let _ = write(format!("{} = {:?}\n", entry, value.unwrap_or("")));
84+
}
85+
})
86+
} else {
87+
write("(NO git config)\n".into())?;
88+
};
89+
90+
write(format!(
91+
"command line: {:?}\n",
92+
std::env::args_os().collect::<Vec<_>>()
93+
))?;
94+
let mut delta_env = DeltaEnv::init();
95+
// redact, not interesting:
96+
delta_env.current_dir = None;
97+
delta_env.hostname = None;
98+
write(format!("DeltaEnv: {:?}\n", delta_env))?;
99+
100+
let term = Term::stdout();
101+
102+
write(format!(
103+
"TERM: {:?}, is_term: {}, size: {:?}\n",
104+
std::env::var_os("TERM"),
105+
term.is_term(),
106+
term.size()
107+
))?;
108+
109+
write(
110+
"raw crash input to delta (usually something git diff etc. generated):\n".into(),
111+
)?;
112+
write("================================\n".into())?;
113+
114+
file.flush().map_err(|e| e.to_string())?;
115+
let truncate_back_to = file.stream_position().map_err(|e| e.to_string())?;
116+
117+
let max_log_size = std::env::var_os(DELTA_DEBUG_LOGFILE_MAX_SIZE)
118+
.map(|v| {
119+
v.to_string_lossy().parse::<u64>().unwrap_or_else(|_| {
120+
fatal(format!(
121+
"Invalid env var value in {} (got {:?}, expected integer)",
122+
DELTA_DEBUG_LOGFILE_MAX_SIZE, v
123+
));
124+
})
125+
})
126+
.unwrap_or(512 * 1024);
127+
128+
if std::env::var_os(RUST_BACKTRACE).is_none() {
129+
// SAFETY:
130+
// a) we only get here when `DELTA_DEBUG_LOGFILE` is set, which means a user is expecting a crash anyhow
131+
// b) the requirement is "no other threads concurrently writing or reading(!) [env vars],
132+
// other than the ones in this [std::env] module.", the rust backtrace handler should use std::env.
133+
unsafe {
134+
std::env::set_var(RUST_BACKTRACE, "1");
135+
}
136+
}
137+
138+
USING_DELTA_DEBUG_LOGFILE.store(true, DELTA_ATOMIC_ORDERING);
139+
140+
Ok(Box::new(DeltaDetailInternal {
141+
file,
142+
bytes_written: 0,
143+
max_log_size,
144+
truncate_back_to,
145+
}))
146+
}
147+
148+
if let Some(logfile) = std::env::var_os(DELTA_DEBUG_LOGFILE) {
149+
Self(
150+
make(&logfile, config)
151+
.map_err(|e| {
152+
eprintln!(
153+
"\nnotice: failed to write {logfile:?} given by {DELTA_DEBUG_LOGFILE}: {e}"
154+
);
155+
})
156+
.ok(),
157+
)
158+
} else {
159+
Self(None)
160+
}
161+
}
162+
163+
#[inline]
164+
pub fn write(&mut self, line: &[u8]) {
165+
if self.0.is_some() {
166+
self._write(line);
167+
}
168+
}
169+
170+
fn _write(&mut self, line: &[u8]) {
171+
let internal = self.0.as_mut().unwrap();
172+
if internal.bytes_written > internal.max_log_size {
173+
let _ = internal.file.flush();
174+
let _ = internal
175+
.file
176+
.seek(SeekFrom::Start(internal.truncate_back_to));
177+
let _ = internal.file.set_len(internal.truncate_back_to);
178+
let _ = internal.file.write_all(
179+
format!(
180+
"(truncated [max log size is {},\
181+
set {DELTA_DEBUG_LOGFILE_MAX_SIZE} to override])\n",
182+
internal.max_log_size
183+
)
184+
.as_bytes(),
185+
);
186+
internal.bytes_written = 0;
187+
}
188+
let _ = internal.file.write_all(line);
189+
let _ = internal.file.write_all(b"\n");
190+
internal.bytes_written += line.len() as u64 + 1;
191+
let _ = internal.file.flush();
192+
}
193+
}
194+
195+
impl Drop for RecordDeltaCall {
196+
fn drop(&mut self) {
197+
if let Some(ref mut internal) = self.0 {
198+
let _ = internal.file.write_all(b"\n```\n</details>\n");
199+
}
200+
}
201+
}

src/utils/mod.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,10 @@ pub mod syntect;
1010
pub mod tabs;
1111
pub mod workarounds;
1212

13+
mod debug;
14+
pub use debug::PrintNoticeOnPanic;
15+
pub use debug::RecordDeltaCall;
16+
1317
// Use the most (even overly) strict ordering. Atomics are not used in hot loops so
1418
// a one-size-fits-all approach which is never incorrect is okay.
1519
pub const DELTA_ATOMIC_ORDERING: std::sync::atomic::Ordering = std::sync::atomic::Ordering::SeqCst;

0 commit comments

Comments
 (0)