Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Print a notice when delta panics #1917

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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);

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So IIUC, the pattern we're using is

Create the debug_helper = RecordDeltaCall::new(); This causes some default diagnostics to be written.

...
Append additional arbitrary diagnostics via debug_helper.write
...
Terminate the file in the Drop impl for RecordDeltaCall.

But if the DELTA_DEBUG_LOGFILE is not set, then, although we do a few function calls, they are all no-ops.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Indeed, write() will usually return after an an easily branch-predicted if.

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, run 'export DELTA_DEBUG_LOGFILE=crash.log', then delta again,\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."))
Comment on lines +76 to +81
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note: I inverted the logic, now printing all git configs (see git config --get-regexp '.*') except a those listed here.

{
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");
}
Comment on lines +133 to +135
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Preparing for the 2024 edition :)

}

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;
Loading