1//! Configuration options for customizing the behavior of the provided panic
2//! and error reporting hooks
3use crate::{
4 section::PanicMessage,
5 writers::{EnvSection, WriterExt},
6};
7use fmt::Display;
8use indenter::{indented, Format};
9use owo_colors::{style, OwoColorize, Style};
10use std::env;
11use std::fmt::Write as _;
12use std::{fmt, path::PathBuf, sync::Arc};
13
14#[derive(Debug)]
15struct InstallError;
16
17impl fmt::Display for InstallError {
18 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
19 f.write_str(data:"could not install the BacktracePrinter as another was already installed")
20 }
21}
22
23impl std::error::Error for InstallError {}
24
25#[derive(Debug)]
26struct InstallThemeError;
27
28impl fmt::Display for InstallThemeError {
29 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
30 f.write_str(data:"could not set the provided `Theme` globally as another was already set")
31 }
32}
33
34impl std::error::Error for InstallThemeError {}
35
36#[derive(Debug)]
37struct InstallColorSpantraceThemeError;
38
39impl fmt::Display for InstallColorSpantraceThemeError {
40 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
41 f.write_str(data:"could not set the provided `Theme` via `color_spantrace::set_theme` globally as another was already set")
42 }
43}
44
45impl std::error::Error for InstallColorSpantraceThemeError {}
46
47/// A struct that represents a theme that is used by `color_eyre`
48#[derive(Debug, Copy, Clone, Default)]
49pub struct Theme {
50 pub(crate) file: Style,
51 pub(crate) line_number: Style,
52 pub(crate) spantrace_target: Style,
53 pub(crate) spantrace_fields: Style,
54 pub(crate) active_line: Style,
55 pub(crate) error: Style,
56 pub(crate) help_info_note: Style,
57 pub(crate) help_info_warning: Style,
58 pub(crate) help_info_suggestion: Style,
59 pub(crate) help_info_error: Style,
60 pub(crate) dependency_code: Style,
61 pub(crate) crate_code: Style,
62 pub(crate) code_hash: Style,
63 pub(crate) panic_header: Style,
64 pub(crate) panic_message: Style,
65 pub(crate) panic_file: Style,
66 pub(crate) panic_line_number: Style,
67 pub(crate) hidden_frames: Style,
68}
69
70macro_rules! theme_setters {
71 ($(#[$meta:meta] $name:ident),* $(,)?) => {
72 $(
73 #[$meta]
74 pub fn $name(mut self, style: Style) -> Self {
75 self.$name = style;
76 self
77 }
78 )*
79 };
80}
81
82impl Theme {
83 /// Creates a blank theme
84 pub fn new() -> Self {
85 Self::default()
86 }
87
88 /// Returns a theme for dark backgrounds. This is the default
89 pub fn dark() -> Self {
90 Self {
91 file: style().purple(),
92 line_number: style().purple(),
93 active_line: style().white().bold(),
94 error: style().bright_red(),
95 help_info_note: style().bright_cyan(),
96 help_info_warning: style().bright_yellow(),
97 help_info_suggestion: style().bright_cyan(),
98 help_info_error: style().bright_red(),
99 dependency_code: style().green(),
100 crate_code: style().bright_red(),
101 code_hash: style().bright_black(),
102 panic_header: style().red(),
103 panic_message: style().cyan(),
104 panic_file: style().purple(),
105 panic_line_number: style().purple(),
106 hidden_frames: style().bright_cyan(),
107 spantrace_target: style().bright_red(),
108 spantrace_fields: style().bright_cyan(),
109 }
110 }
111
112 // XXX it would be great, if someone with more style optimizes the light theme below. I just fixed the biggest problems, but ideally there would be darker colors (however, the standard ANSI colors don't seem to have many dark enough colors. Maybe xterm colors or RGB colors would be better (however, again, see my comment regarding xterm colors in `color_spantrace`))
113
114 /// Returns a theme for light backgrounds
115 pub fn light() -> Self {
116 Self {
117 file: style().purple(),
118 line_number: style().purple(),
119 spantrace_target: style().red(),
120 spantrace_fields: style().blue(),
121 active_line: style().bold(),
122 error: style().red(),
123 help_info_note: style().blue(),
124 help_info_warning: style().bright_red(),
125 help_info_suggestion: style().blue(),
126 help_info_error: style().red(),
127 dependency_code: style().green(),
128 crate_code: style().red(),
129 code_hash: style().bright_black(),
130 panic_header: style().red(),
131 panic_message: style().blue(),
132 panic_file: style().purple(),
133 panic_line_number: style().purple(),
134 hidden_frames: style().blue(),
135 }
136 }
137
138 theme_setters! {
139 /// Styles printed paths
140 file,
141 /// Styles the line number of a file
142 line_number,
143 /// Styles the `color_spantrace` target (i.e. the module and function name, and so on)
144 spantrace_target,
145 /// Styles fields associated with a the `tracing::Span`.
146 spantrace_fields,
147 /// Styles the selected line of displayed code
148 active_line,
149 // XXX not sure how to describe this better (or if this is even completely correct)
150 /// Styles errors printed by `EyreHandler`
151 error,
152 /// Styles the "note" section header
153 help_info_note,
154 /// Styles the "warning" section header
155 help_info_warning,
156 /// Styles the "suggestion" section header
157 help_info_suggestion,
158 /// Styles the "error" section header
159 help_info_error,
160 /// Styles code that is not part of your crate
161 dependency_code,
162 /// Styles code that's in your crate
163 crate_code,
164 /// Styles the hash after `dependency_code` and `crate_code`
165 code_hash,
166 /// Styles the header of a panic
167 panic_header,
168 /// Styles the message of a panic
169 panic_message,
170 /// Styles paths of a panic
171 panic_file,
172 /// Styles the line numbers of a panic
173 panic_line_number,
174 /// Styles the "N frames hidden" message
175 hidden_frames,
176 }
177}
178
179/// A representation of a Frame from a Backtrace or a SpanTrace
180#[derive(Debug)]
181#[non_exhaustive]
182pub struct Frame {
183 /// Frame index
184 pub n: usize,
185 /// frame symbol name
186 pub name: Option<String>,
187 /// source line number
188 pub lineno: Option<u32>,
189 /// source file path
190 pub filename: Option<PathBuf>,
191}
192
193#[derive(Debug)]
194struct StyledFrame<'a>(&'a Frame, Theme);
195
196impl<'a> fmt::Display for StyledFrame<'a> {
197 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
198 let Self(frame, theme) = self;
199
200 let is_dependency_code = frame.is_dependency_code();
201
202 // Print frame index.
203 write!(f, "{:>2}: ", frame.n)?;
204
205 // Does the function have a hash suffix?
206 // (dodging a dep on the regex crate here)
207 let name = frame.name.as_deref().unwrap_or("<unknown>");
208 let has_hash_suffix = name.len() > 19
209 && &name[name.len() - 19..name.len() - 16] == "::h"
210 && name[name.len() - 16..]
211 .chars()
212 .all(|x| x.is_ascii_hexdigit());
213
214 let hash_suffix = if has_hash_suffix {
215 &name[name.len() - 19..]
216 } else {
217 "<unknown>"
218 };
219
220 // Print function name.
221 let name = if has_hash_suffix {
222 &name[..name.len() - 19]
223 } else {
224 name
225 };
226
227 if is_dependency_code {
228 write!(f, "{}", (name).style(theme.dependency_code))?;
229 } else {
230 write!(f, "{}", (name).style(theme.crate_code))?;
231 }
232
233 write!(f, "{}", (hash_suffix).style(theme.code_hash))?;
234
235 let mut separated = f.header("\n");
236
237 // Print source location, if known.
238 let file = frame.filename.as_ref().map(|path| path.display());
239 let file: &dyn fmt::Display = if let Some(ref filename) = file {
240 filename
241 } else {
242 &"<unknown source file>"
243 };
244 let lineno = frame
245 .lineno
246 .map_or("<unknown line>".to_owned(), |x| x.to_string());
247 write!(
248 &mut separated.ready(),
249 " at {}:{}",
250 file.style(theme.file),
251 lineno.style(theme.line_number),
252 )?;
253
254 let v = if std::thread::panicking() {
255 panic_verbosity()
256 } else {
257 lib_verbosity()
258 };
259
260 // Maybe print source.
261 if v >= Verbosity::Full {
262 write!(&mut separated.ready(), "{}", SourceSection(frame, *theme))?;
263 }
264
265 Ok(())
266 }
267}
268
269struct SourceSection<'a>(&'a Frame, Theme);
270
271impl fmt::Display for SourceSection<'_> {
272 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
273 let Self(frame, theme) = self;
274
275 let (lineno, filename) = match (frame.lineno, frame.filename.as_ref()) {
276 (Some(a), Some(b)) => (a, b),
277 // Without a line number and file name, we can't sensibly proceed.
278 _ => return Ok(()),
279 };
280
281 let file = match std::fs::File::open(filename) {
282 Ok(file) => file,
283 Err(ref e) if e.kind() == std::io::ErrorKind::NotFound => return Ok(()),
284 e @ Err(_) => e.unwrap(),
285 };
286
287 use std::fmt::Write;
288 use std::io::BufRead;
289
290 // Extract relevant lines.
291 let reader = std::io::BufReader::new(file);
292 let start_line = lineno - 2.min(lineno - 1);
293 let surrounding_src = reader.lines().skip(start_line as usize - 1).take(5);
294 let mut separated = f.header("\n");
295 let mut f = separated.in_progress();
296 for (line, cur_line_no) in surrounding_src.zip(start_line..) {
297 let line = line.unwrap();
298 if cur_line_no == lineno {
299 write!(
300 &mut f,
301 "{:>8} {} {}",
302 cur_line_no.style(theme.active_line),
303 ">".style(theme.active_line),
304 line.style(theme.active_line),
305 )?;
306 } else {
307 write!(&mut f, "{:>8}{}", cur_line_no, line)?;
308 }
309 f = separated.ready();
310 }
311
312 Ok(())
313 }
314}
315
316impl Frame {
317 fn is_dependency_code(&self) -> bool {
318 const SYM_PREFIXES: &[&str] = &[
319 "std::",
320 "core::",
321 "backtrace::backtrace::",
322 "_rust_begin_unwind",
323 "color_traceback::",
324 "__rust_",
325 "___rust_",
326 "__pthread",
327 "_main",
328 "main",
329 "__scrt_common_main_seh",
330 "BaseThreadInitThunk",
331 "_start",
332 "__libc_start_main",
333 "start_thread",
334 ];
335
336 // Inspect name.
337 if let Some(ref name) = self.name {
338 if SYM_PREFIXES.iter().any(|x| name.starts_with(x)) {
339 return true;
340 }
341 }
342
343 const FILE_PREFIXES: &[&str] = &[
344 "/rustc/",
345 "src/libstd/",
346 "src/libpanic_unwind/",
347 "src/libtest/",
348 ];
349
350 // Inspect filename.
351 if let Some(ref filename) = self.filename {
352 let filename = filename.to_string_lossy();
353 if FILE_PREFIXES.iter().any(|x| filename.starts_with(x))
354 || filename.contains("/.cargo/registry/src/")
355 {
356 return true;
357 }
358 }
359
360 false
361 }
362
363 /// Heuristically determine whether a frame is likely to be a post panic
364 /// frame.
365 ///
366 /// Post panic frames are frames of a functions called after the actual panic
367 /// is already in progress and don't contain any useful information for a
368 /// reader of the backtrace.
369 fn is_post_panic_code(&self) -> bool {
370 const SYM_PREFIXES: &[&str] = &[
371 "_rust_begin_unwind",
372 "rust_begin_unwind",
373 "core::result::unwrap_failed",
374 "core::option::expect_none_failed",
375 "core::panicking::panic_fmt",
376 "color_backtrace::create_panic_handler",
377 "std::panicking::begin_panic",
378 "begin_panic_fmt",
379 "failure::backtrace::Backtrace::new",
380 "backtrace::capture",
381 "failure::error_message::err_msg",
382 "<failure::error::Error as core::convert::From<F>>::from",
383 ];
384
385 match self.name.as_ref() {
386 Some(name) => SYM_PREFIXES.iter().any(|x| name.starts_with(x)),
387 None => false,
388 }
389 }
390
391 /// Heuristically determine whether a frame is likely to be part of language
392 /// runtime.
393 fn is_runtime_init_code(&self) -> bool {
394 const SYM_PREFIXES: &[&str] = &[
395 "std::rt::lang_start::",
396 "test::run_test::run_test_inner::",
397 "std::sys_common::backtrace::__rust_begin_short_backtrace",
398 ];
399
400 let (name, file) = match (self.name.as_ref(), self.filename.as_ref()) {
401 (Some(name), Some(filename)) => (name, filename.to_string_lossy()),
402 _ => return false,
403 };
404
405 if SYM_PREFIXES.iter().any(|x| name.starts_with(x)) {
406 return true;
407 }
408
409 // For Linux, this is the best rule for skipping test init I found.
410 if name == "{{closure}}" && file == "src/libtest/lib.rs" {
411 return true;
412 }
413
414 false
415 }
416}
417
418/// Builder for customizing the behavior of the global panic and error report hooks
419pub struct HookBuilder {
420 filters: Vec<Box<FilterCallback>>,
421 capture_span_trace_by_default: bool,
422 display_env_section: bool,
423 #[cfg(feature = "track-caller")]
424 display_location_section: bool,
425 panic_section: Option<Box<dyn Display + Send + Sync + 'static>>,
426 panic_message: Option<Box<dyn PanicMessage>>,
427 theme: Theme,
428 #[cfg(feature = "issue-url")]
429 issue_url: Option<String>,
430 #[cfg(feature = "issue-url")]
431 issue_metadata: Vec<(String, Box<dyn Display + Send + Sync + 'static>)>,
432 #[cfg(feature = "issue-url")]
433 issue_filter: Arc<IssueFilterCallback>,
434}
435
436impl HookBuilder {
437 /// Construct a HookBuilder
438 ///
439 /// # Details
440 ///
441 /// By default this function calls `add_default_filters()` and
442 /// `capture_span_trace_by_default(true)`. To get a `HookBuilder` with all
443 /// features disabled by default call `HookBuilder::blank()`.
444 ///
445 /// # Example
446 ///
447 /// ```rust
448 /// use color_eyre::config::HookBuilder;
449 ///
450 /// HookBuilder::new()
451 /// .install()
452 /// .unwrap();
453 /// ```
454 pub fn new() -> Self {
455 Self::blank()
456 .add_default_filters()
457 .capture_span_trace_by_default(true)
458 }
459
460 /// Construct a HookBuilder with minimal features enabled
461 pub fn blank() -> Self {
462 HookBuilder {
463 filters: vec![],
464 capture_span_trace_by_default: false,
465 display_env_section: true,
466 #[cfg(feature = "track-caller")]
467 display_location_section: true,
468 panic_section: None,
469 panic_message: None,
470 theme: Theme::dark(),
471 #[cfg(feature = "issue-url")]
472 issue_url: None,
473 #[cfg(feature = "issue-url")]
474 issue_metadata: vec![],
475 #[cfg(feature = "issue-url")]
476 issue_filter: Arc::new(|_| true),
477 }
478 }
479
480 /// Set the global styles that `color_eyre` should use.
481 ///
482 /// **Tip:** You can test new styles by editing `examples/theme.rs` in the `color-eyre` repository.
483 pub fn theme(mut self, theme: Theme) -> Self {
484 self.theme = theme;
485 self
486 }
487
488 /// Add a custom section to the panic hook that will be printed
489 /// in the panic message.
490 ///
491 /// # Examples
492 ///
493 /// ```rust
494 /// color_eyre::config::HookBuilder::default()
495 /// .panic_section("consider reporting the bug at https://github.com/eyre-rs/eyre/issues")
496 /// .install()
497 /// .unwrap()
498 /// ```
499 pub fn panic_section<S: Display + Send + Sync + 'static>(mut self, section: S) -> Self {
500 self.panic_section = Some(Box::new(section));
501 self
502 }
503
504 /// Overrides the main error message printing section at the start of panic
505 /// reports
506 ///
507 /// # Examples
508 ///
509 /// ```rust
510 /// use std::{panic::Location, fmt};
511 /// use color_eyre::section::PanicMessage;
512 /// use owo_colors::OwoColorize;
513 ///
514 /// struct MyPanicMessage;
515 ///
516 /// color_eyre::config::HookBuilder::default()
517 /// .panic_message(MyPanicMessage)
518 /// .install()
519 /// .unwrap();
520 ///
521 /// impl PanicMessage for MyPanicMessage {
522 /// fn display(&self, pi: &std::panic::PanicInfo<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
523 /// writeln!(f, "{}", "The application panicked (crashed).".red())?;
524 ///
525 /// // Print panic message.
526 /// let payload = pi
527 /// .payload()
528 /// .downcast_ref::<String>()
529 /// .map(String::as_str)
530 /// .or_else(|| pi.payload().downcast_ref::<&str>().cloned())
531 /// .unwrap_or("<non string panic payload>");
532 ///
533 /// write!(f, "Message: ")?;
534 /// writeln!(f, "{}", payload.cyan())?;
535 ///
536 /// // If known, print panic location.
537 /// write!(f, "Location: ")?;
538 /// if let Some(loc) = pi.location() {
539 /// write!(f, "{}", loc.file().purple())?;
540 /// write!(f, ":")?;
541 /// write!(f, "{}", loc.line().purple())?;
542 ///
543 /// write!(f, "\n\nConsider reporting the bug at {}", custom_url(loc, payload))?;
544 /// } else {
545 /// write!(f, "<unknown>")?;
546 /// }
547 ///
548 /// Ok(())
549 /// }
550 /// }
551 ///
552 /// fn custom_url(location: &Location<'_>, message: &str) -> impl fmt::Display {
553 /// "todo"
554 /// }
555 /// ```
556 pub fn panic_message<S: PanicMessage>(mut self, section: S) -> Self {
557 self.panic_message = Some(Box::new(section));
558 self
559 }
560
561 /// Set an upstream github repo and enable issue reporting url generation
562 ///
563 /// # Details
564 ///
565 /// Once enabled, color-eyre will generate urls that will create customized
566 /// issues pre-populated with information about the associated error report.
567 ///
568 /// Additional information can be added to the metadata table in the
569 /// generated urls by calling `add_issue_metadata` when configuring the
570 /// HookBuilder.
571 ///
572 /// # Examples
573 ///
574 /// ```rust
575 /// color_eyre::config::HookBuilder::default()
576 /// .issue_url(concat!(env!("CARGO_PKG_REPOSITORY"), "/issues/new"))
577 /// .install()
578 /// .unwrap();
579 /// ```
580 #[cfg(feature = "issue-url")]
581 #[cfg_attr(docsrs, doc(cfg(feature = "issue-url")))]
582 pub fn issue_url<S: ToString>(mut self, url: S) -> Self {
583 self.issue_url = Some(url.to_string());
584 self
585 }
586
587 /// Add a new entry to the metadata table in generated github issue urls
588 ///
589 /// **Note**: this metadata will be ignored if no `issue_url` is set.
590 ///
591 /// # Examples
592 ///
593 /// ```rust
594 /// color_eyre::config::HookBuilder::default()
595 /// .issue_url(concat!(env!("CARGO_PKG_REPOSITORY"), "/issues/new"))
596 /// .add_issue_metadata("version", env!("CARGO_PKG_VERSION"))
597 /// .install()
598 /// .unwrap();
599 /// ```
600 #[cfg(feature = "issue-url")]
601 #[cfg_attr(docsrs, doc(cfg(feature = "issue-url")))]
602 pub fn add_issue_metadata<K, V>(mut self, key: K, value: V) -> Self
603 where
604 K: Display,
605 V: Display + Send + Sync + 'static,
606 {
607 let pair = (key.to_string(), Box::new(value) as _);
608 self.issue_metadata.push(pair);
609 self
610 }
611
612 /// Configures a filter for disabling issue url generation for certain kinds of errors
613 ///
614 /// If the closure returns `true`, then the issue url will be generated.
615 ///
616 /// # Examples
617 ///
618 /// ```rust
619 /// color_eyre::config::HookBuilder::default()
620 /// .issue_url(concat!(env!("CARGO_PKG_REPOSITORY"), "/issues/new"))
621 /// .issue_filter(|kind| match kind {
622 /// color_eyre::ErrorKind::NonRecoverable(payload) => {
623 /// let payload = payload
624 /// .downcast_ref::<String>()
625 /// .map(String::as_str)
626 /// .or_else(|| payload.downcast_ref::<&str>().cloned())
627 /// .unwrap_or("<non string panic payload>");
628 ///
629 /// !payload.contains("my irrelevant error message")
630 /// },
631 /// color_eyre::ErrorKind::Recoverable(error) => !error.is::<std::fmt::Error>(),
632 /// })
633 /// .install()
634 /// .unwrap();
635 ///
636 #[cfg(feature = "issue-url")]
637 #[cfg_attr(docsrs, doc(cfg(feature = "issue-url")))]
638 pub fn issue_filter<F>(mut self, predicate: F) -> Self
639 where
640 F: Fn(crate::ErrorKind<'_>) -> bool + Send + Sync + 'static,
641 {
642 self.issue_filter = Arc::new(predicate);
643 self
644 }
645
646 /// Configures the default capture mode for `SpanTraces` in error reports and panics
647 pub fn capture_span_trace_by_default(mut self, cond: bool) -> Self {
648 self.capture_span_trace_by_default = cond;
649 self
650 }
651
652 /// Configures the enviroment varible info section and whether or not it is displayed
653 pub fn display_env_section(mut self, cond: bool) -> Self {
654 self.display_env_section = cond;
655 self
656 }
657
658 /// Configures the location info section and whether or not it is displayed.
659 ///
660 /// # Notes
661 ///
662 /// This will not disable the location section in a panic message.
663 #[cfg(feature = "track-caller")]
664 #[cfg_attr(docsrs, doc(cfg(feature = "track-caller")))]
665 pub fn display_location_section(mut self, cond: bool) -> Self {
666 self.display_location_section = cond;
667 self
668 }
669
670 /// Add a custom filter to the set of frame filters
671 ///
672 /// # Examples
673 ///
674 /// ```rust
675 /// color_eyre::config::HookBuilder::default()
676 /// .add_frame_filter(Box::new(|frames| {
677 /// let filters = &[
678 /// "uninteresting_function",
679 /// ];
680 ///
681 /// frames.retain(|frame| {
682 /// !filters.iter().any(|f| {
683 /// let name = if let Some(name) = frame.name.as_ref() {
684 /// name.as_str()
685 /// } else {
686 /// return true;
687 /// };
688 ///
689 /// name.starts_with(f)
690 /// })
691 /// });
692 /// }))
693 /// .install()
694 /// .unwrap();
695 /// ```
696 pub fn add_frame_filter(mut self, filter: Box<FilterCallback>) -> Self {
697 self.filters.push(filter);
698 self
699 }
700
701 /// Install the given Hook as the global error report hook
702 pub fn install(self) -> Result<(), crate::eyre::Report> {
703 let (panic_hook, eyre_hook) = self.try_into_hooks()?;
704 eyre_hook.install()?;
705 panic_hook.install();
706 Ok(())
707 }
708
709 /// Add the default set of filters to this `HookBuilder`'s configuration
710 pub fn add_default_filters(self) -> Self {
711 self.add_frame_filter(Box::new(default_frame_filter))
712 .add_frame_filter(Box::new(eyre_frame_filters))
713 }
714
715 /// Create a `PanicHook` and `EyreHook` from this `HookBuilder`.
716 /// This can be used if you want to combine these handlers with other handlers.
717 pub fn into_hooks(self) -> (PanicHook, EyreHook) {
718 self.try_into_hooks().expect("into_hooks should only be called when no `color_spantrace` themes have previously been set")
719 }
720
721 /// Create a `PanicHook` and `EyreHook` from this `HookBuilder`.
722 /// This can be used if you want to combine these handlers with other handlers.
723 pub fn try_into_hooks(self) -> Result<(PanicHook, EyreHook), crate::eyre::Report> {
724 let theme = self.theme;
725 #[cfg(feature = "issue-url")]
726 let metadata = Arc::new(self.issue_metadata);
727 let panic_hook = PanicHook {
728 filters: self.filters.into(),
729 section: self.panic_section,
730 #[cfg(feature = "capture-spantrace")]
731 capture_span_trace_by_default: self.capture_span_trace_by_default,
732 display_env_section: self.display_env_section,
733 panic_message: self
734 .panic_message
735 .unwrap_or_else(|| Box::new(DefaultPanicMessage(theme))),
736 theme,
737 #[cfg(feature = "issue-url")]
738 issue_url: self.issue_url.clone(),
739 #[cfg(feature = "issue-url")]
740 issue_metadata: metadata.clone(),
741 #[cfg(feature = "issue-url")]
742 issue_filter: self.issue_filter.clone(),
743 };
744
745 let eyre_hook = EyreHook {
746 filters: panic_hook.filters.clone(),
747 #[cfg(feature = "capture-spantrace")]
748 capture_span_trace_by_default: self.capture_span_trace_by_default,
749 display_env_section: self.display_env_section,
750 #[cfg(feature = "track-caller")]
751 display_location_section: self.display_location_section,
752 theme,
753 #[cfg(feature = "issue-url")]
754 issue_url: self.issue_url,
755 #[cfg(feature = "issue-url")]
756 issue_metadata: metadata,
757 #[cfg(feature = "issue-url")]
758 issue_filter: self.issue_filter,
759 };
760
761 #[cfg(feature = "capture-spantrace")]
762 eyre::WrapErr::wrap_err(color_spantrace::set_theme(self.theme.into()), "could not set the provided `Theme` via `color_spantrace::set_theme` globally as another was already set")?;
763
764 Ok((panic_hook, eyre_hook))
765 }
766}
767
768#[cfg(feature = "capture-spantrace")]
769impl From<Theme> for color_spantrace::Theme {
770 fn from(src: Theme) -> color_spantrace::Theme {
771 color_spantrace::Theme::new()
772 .file(src.file)
773 .line_number(src.line_number)
774 .target(src.spantrace_target)
775 .fields(src.spantrace_fields)
776 .active_line(style:src.active_line)
777 }
778}
779
780#[allow(missing_docs)]
781impl Default for HookBuilder {
782 fn default() -> Self {
783 Self::new()
784 }
785}
786
787fn default_frame_filter(frames: &mut Vec<&Frame>) {
788 let top_cutoff: usize = frames
789 .iter()
790 .rposition(|x| x.is_post_panic_code())
791 .map(|x| x + 2) // indices are 1 based
792 .unwrap_or(default:0);
793
794 let bottom_cutoff: usize = frames
795 .iter()
796 .position(|x| x.is_runtime_init_code())
797 .unwrap_or(default:frames.len());
798
799 let rng: RangeInclusive = top_cutoff..=bottom_cutoff;
800 frames.retain(|x: &&Frame| rng.contains(&x.n))
801}
802
803fn eyre_frame_filters(frames: &mut Vec<&Frame>) {
804 let filters: &[&'static str; 3] = &[
805 "<color_eyre::Handler as eyre::EyreHandler>::default",
806 "eyre::",
807 "color_eyre::",
808 ];
809
810 frames.retain(|frame: &&Frame| {
811 !filters.iter().any(|f: &&str| {
812 let name: &str = if let Some(name: &String) = frame.name.as_ref() {
813 name.as_str()
814 } else {
815 return true;
816 };
817
818 name.starts_with(f)
819 })
820 });
821}
822
823struct DefaultPanicMessage(Theme);
824
825impl PanicMessage for DefaultPanicMessage {
826 fn display(&self, pi: &std::panic::PanicInfo<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
827 // XXX is my assumption correct that this function is guaranteed to only run after `color_eyre` was setup successfully (including setting `THEME`), and that therefore the following line will never panic? Otherwise, we could return `fmt::Error`, but if the above is true, I like `unwrap` + a comment why this never fails better
828 let theme = &self.0;
829
830 writeln!(
831 f,
832 "{}",
833 "The application panicked (crashed).".style(theme.panic_header)
834 )?;
835
836 // Print panic message.
837 let payload = pi
838 .payload()
839 .downcast_ref::<String>()
840 .map(String::as_str)
841 .or_else(|| pi.payload().downcast_ref::<&str>().cloned())
842 .unwrap_or("<non string panic payload>");
843
844 write!(f, "Message: ")?;
845 writeln!(f, "{}", payload.style(theme.panic_message))?;
846
847 // If known, print panic location.
848 write!(f, "Location: ")?;
849 write!(f, "{}", crate::fmt::LocationSection(pi.location(), *theme))?;
850
851 Ok(())
852 }
853}
854
855/// A type representing an error report for a panic.
856pub struct PanicReport<'a> {
857 hook: &'a PanicHook,
858 panic_info: &'a std::panic::PanicInfo<'a>,
859 backtrace: Option<backtrace::Backtrace>,
860 #[cfg(feature = "capture-spantrace")]
861 span_trace: Option<tracing_error::SpanTrace>,
862}
863
864fn print_panic_info(report: &PanicReport<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
865 report.hook.panic_message.display(report.panic_info, f)?;
866
867 let v = panic_verbosity();
868 let capture_bt = v != Verbosity::Minimal;
869
870 let mut separated = f.header("\n\n");
871
872 if let Some(ref section) = report.hook.section {
873 write!(&mut separated.ready(), "{}", section)?;
874 }
875
876 #[cfg(feature = "capture-spantrace")]
877 {
878 if let Some(span_trace) = report.span_trace.as_ref() {
879 write!(
880 &mut separated.ready(),
881 "{}",
882 crate::writers::FormattedSpanTrace(span_trace)
883 )?;
884 }
885 }
886
887 if let Some(bt) = report.backtrace.as_ref() {
888 let fmted_bt = report.hook.format_backtrace(bt);
889 write!(
890 indented(&mut separated.ready()).with_format(Format::Uniform { indentation: " " }),
891 "{}",
892 fmted_bt
893 )?;
894 }
895
896 if report.hook.display_env_section {
897 let env_section = EnvSection {
898 bt_captured: &capture_bt,
899 #[cfg(feature = "capture-spantrace")]
900 span_trace: report.span_trace.as_ref(),
901 };
902
903 write!(&mut separated.ready(), "{}", env_section)?;
904 }
905
906 #[cfg(feature = "issue-url")]
907 {
908 let payload = report.panic_info.payload();
909
910 if report.hook.issue_url.is_some()
911 && (*report.hook.issue_filter)(crate::ErrorKind::NonRecoverable(payload))
912 {
913 let url = report.hook.issue_url.as_ref().unwrap();
914 let payload = payload
915 .downcast_ref::<String>()
916 .map(String::as_str)
917 .or_else(|| payload.downcast_ref::<&str>().cloned())
918 .unwrap_or("<non string panic payload>");
919
920 let issue_section = crate::section::github::IssueSection::new(url, payload)
921 .with_backtrace(report.backtrace.as_ref())
922 .with_location(report.panic_info.location())
923 .with_metadata(&report.hook.issue_metadata);
924
925 #[cfg(feature = "capture-spantrace")]
926 let issue_section = issue_section.with_span_trace(report.span_trace.as_ref());
927
928 write!(&mut separated.ready(), "{}", issue_section)?;
929 }
930 }
931
932 Ok(())
933}
934
935impl fmt::Display for PanicReport<'_> {
936 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
937 print_panic_info(self, f)
938 }
939}
940
941/// A panic reporting hook
942pub struct PanicHook {
943 filters: Arc<[Box<FilterCallback>]>,
944 section: Option<Box<dyn Display + Send + Sync + 'static>>,
945 panic_message: Box<dyn PanicMessage>,
946 theme: Theme,
947 #[cfg(feature = "capture-spantrace")]
948 capture_span_trace_by_default: bool,
949 display_env_section: bool,
950 #[cfg(feature = "issue-url")]
951 issue_url: Option<String>,
952 #[cfg(feature = "issue-url")]
953 issue_metadata: Arc<Vec<(String, Box<dyn Display + Send + Sync + 'static>)>>,
954 #[cfg(feature = "issue-url")]
955 issue_filter: Arc<IssueFilterCallback>,
956}
957
958impl PanicHook {
959 pub(crate) fn format_backtrace<'a>(
960 &'a self,
961 trace: &'a backtrace::Backtrace,
962 ) -> BacktraceFormatter<'a> {
963 BacktraceFormatter {
964 filters: &self.filters,
965 inner: trace,
966 theme: self.theme,
967 }
968 }
969
970 #[cfg(feature = "capture-spantrace")]
971 fn spantrace_capture_enabled(&self) -> bool {
972 std::env::var("RUST_SPANTRACE")
973 .map(|val| val != "0")
974 .unwrap_or(self.capture_span_trace_by_default)
975 }
976
977 /// Install self as a global panic hook via `std::panic::set_hook`.
978 pub fn install(self) {
979 std::panic::set_hook(self.into_panic_hook());
980 }
981
982 /// Convert self into the type expected by `std::panic::set_hook`.
983 pub fn into_panic_hook(
984 self,
985 ) -> Box<dyn Fn(&std::panic::PanicInfo<'_>) + Send + Sync + 'static> {
986 Box::new(move |panic_info| {
987 eprintln!("{}", self.panic_report(panic_info));
988 })
989 }
990
991 /// Construct a panic reporter which prints it's panic report via the
992 /// `Display` trait.
993 pub fn panic_report<'a>(
994 &'a self,
995 panic_info: &'a std::panic::PanicInfo<'_>,
996 ) -> PanicReport<'a> {
997 let v = panic_verbosity();
998 let capture_bt = v != Verbosity::Minimal;
999
1000 #[cfg(feature = "capture-spantrace")]
1001 let span_trace = if self.spantrace_capture_enabled() {
1002 Some(tracing_error::SpanTrace::capture())
1003 } else {
1004 None
1005 };
1006
1007 let backtrace = if capture_bt {
1008 Some(backtrace::Backtrace::new())
1009 } else {
1010 None
1011 };
1012
1013 PanicReport {
1014 panic_info,
1015 #[cfg(feature = "capture-spantrace")]
1016 span_trace,
1017 backtrace,
1018 hook: self,
1019 }
1020 }
1021}
1022
1023/// An eyre reporting hook used to construct `EyreHandler`s
1024pub struct EyreHook {
1025 filters: Arc<[Box<FilterCallback>]>,
1026 #[cfg(feature = "capture-spantrace")]
1027 capture_span_trace_by_default: bool,
1028 display_env_section: bool,
1029 #[cfg(feature = "track-caller")]
1030 display_location_section: bool,
1031 theme: Theme,
1032 #[cfg(feature = "issue-url")]
1033 issue_url: Option<String>,
1034 #[cfg(feature = "issue-url")]
1035 issue_metadata: Arc<Vec<(String, Box<dyn Display + Send + Sync + 'static>)>>,
1036 #[cfg(feature = "issue-url")]
1037 issue_filter: Arc<IssueFilterCallback>,
1038}
1039
1040type HookFunc = Box<
1041 dyn Fn(&(dyn std::error::Error + 'static)) -> Box<dyn eyre::EyreHandler>
1042 + Send
1043 + Sync
1044 + 'static,
1045>;
1046
1047impl EyreHook {
1048 #[allow(unused_variables)]
1049 pub(crate) fn default(&self, error: &(dyn std::error::Error + 'static)) -> crate::Handler {
1050 let backtrace = if lib_verbosity() != Verbosity::Minimal {
1051 Some(backtrace::Backtrace::new())
1052 } else {
1053 None
1054 };
1055
1056 #[cfg(feature = "capture-spantrace")]
1057 let span_trace = if self.spantrace_capture_enabled()
1058 && crate::handler::get_deepest_spantrace(error).is_none()
1059 {
1060 Some(tracing_error::SpanTrace::capture())
1061 } else {
1062 None
1063 };
1064
1065 crate::Handler {
1066 filters: self.filters.clone(),
1067 backtrace,
1068 suppress_backtrace: false,
1069 #[cfg(feature = "capture-spantrace")]
1070 span_trace,
1071 sections: Vec::new(),
1072 display_env_section: self.display_env_section,
1073 #[cfg(feature = "track-caller")]
1074 display_location_section: self.display_location_section,
1075 #[cfg(feature = "issue-url")]
1076 issue_url: self.issue_url.clone(),
1077 #[cfg(feature = "issue-url")]
1078 issue_metadata: self.issue_metadata.clone(),
1079 #[cfg(feature = "issue-url")]
1080 issue_filter: self.issue_filter.clone(),
1081 theme: self.theme,
1082 #[cfg(feature = "track-caller")]
1083 location: None,
1084 }
1085 }
1086
1087 #[cfg(feature = "capture-spantrace")]
1088 fn spantrace_capture_enabled(&self) -> bool {
1089 std::env::var("RUST_SPANTRACE")
1090 .map(|val| val != "0")
1091 .unwrap_or(self.capture_span_trace_by_default)
1092 }
1093
1094 /// Installs self as the global eyre handling hook via `eyre::set_hook`
1095 pub fn install(self) -> Result<(), crate::eyre::InstallError> {
1096 crate::eyre::set_hook(self.into_eyre_hook())
1097 }
1098
1099 /// Convert the self into the boxed type expected by `eyre::set_hook`.
1100 pub fn into_eyre_hook(self) -> HookFunc {
1101 Box::new(move |e| Box::new(self.default(e)))
1102 }
1103}
1104
1105pub(crate) struct BacktraceFormatter<'a> {
1106 pub(crate) filters: &'a [Box<FilterCallback>],
1107 pub(crate) inner: &'a backtrace::Backtrace,
1108 pub(crate) theme: Theme,
1109}
1110
1111impl fmt::Display for BacktraceFormatter<'_> {
1112 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1113 write!(f, "{:━^80}", " BACKTRACE ")?;
1114
1115 // Collect frame info.
1116 let frames: Vec<_> = self
1117 .inner
1118 .frames()
1119 .iter()
1120 .flat_map(|frame| frame.symbols())
1121 .zip(1usize..)
1122 .map(|(sym, n)| Frame {
1123 name: sym.name().map(|x| x.to_string()),
1124 lineno: sym.lineno(),
1125 filename: sym.filename().map(|x| x.into()),
1126 n,
1127 })
1128 .collect();
1129
1130 let mut filtered_frames = frames.iter().collect();
1131 match env::var("COLORBT_SHOW_HIDDEN").ok().as_deref() {
1132 Some("1") | Some("on") | Some("y") => (),
1133 _ => {
1134 for filter in self.filters {
1135 filter(&mut filtered_frames);
1136 }
1137 }
1138 }
1139
1140 if filtered_frames.is_empty() {
1141 // TODO: Would probably look better centered.
1142 return write!(f, "\n<empty backtrace>");
1143 }
1144
1145 let mut separated = f.header("\n");
1146
1147 // Don't let filters mess with the order.
1148 filtered_frames.sort_by_key(|x| x.n);
1149
1150 let mut buf = String::new();
1151
1152 macro_rules! print_hidden {
1153 ($n:expr) => {
1154 let n = $n;
1155 buf.clear();
1156 write!(
1157 &mut buf,
1158 "{decorator} {n} frame{plural} hidden {decorator}",
1159 n = n,
1160 plural = if n == 1 { "" } else { "s" },
1161 decorator = "⋮",
1162 )
1163 .expect("writing to strings doesn't panic");
1164 write!(
1165 &mut separated.ready(),
1166 "{:^80}",
1167 buf.style(self.theme.hidden_frames)
1168 )?;
1169 };
1170 }
1171
1172 let mut last_n = 0;
1173 for frame in &filtered_frames {
1174 let frame_delta = frame.n - last_n - 1;
1175 if frame_delta != 0 {
1176 print_hidden!(frame_delta);
1177 }
1178 write!(&mut separated.ready(), "{}", StyledFrame(frame, self.theme))?;
1179 last_n = frame.n;
1180 }
1181
1182 let last_filtered_n = filtered_frames.last().unwrap().n;
1183 let last_unfiltered_n = frames.last().unwrap().n;
1184 if last_filtered_n < last_unfiltered_n {
1185 print_hidden!(last_unfiltered_n - last_filtered_n);
1186 }
1187
1188 Ok(())
1189 }
1190}
1191
1192#[derive(PartialEq, Eq, PartialOrd, Ord, Clone, Copy)]
1193pub(crate) enum Verbosity {
1194 Minimal,
1195 Medium,
1196 Full,
1197}
1198
1199pub(crate) fn panic_verbosity() -> Verbosity {
1200 match env::var(key:"RUST_BACKTRACE") {
1201 Ok(s: String) if s == "full" => Verbosity::Full,
1202 Ok(s: String) if s != "0" => Verbosity::Medium,
1203 _ => Verbosity::Minimal,
1204 }
1205}
1206
1207pub(crate) fn lib_verbosity() -> Verbosity {
1208 match env::var("RUST_LIB_BACKTRACE").or_else(|_| env::var(key:"RUST_BACKTRACE")) {
1209 Ok(s: String) if s == "full" => Verbosity::Full,
1210 Ok(s: String) if s != "0" => Verbosity::Medium,
1211 _ => Verbosity::Minimal,
1212 }
1213}
1214
1215/// Callback for filtering a vector of `Frame`s
1216pub type FilterCallback = dyn Fn(&mut Vec<&Frame>) + Send + Sync + 'static;
1217
1218/// Callback for filtering issue url generation in error reports
1219#[cfg(feature = "issue-url")]
1220#[cfg_attr(docsrs, doc(cfg(feature = "issue-url")))]
1221pub type IssueFilterCallback = dyn Fn(crate::ErrorKind<'_>) -> bool + Send + Sync + 'static;
1222