1use console::style;
29use std::borrow::Cow;
30use std::fs::File;
31use std::io::{self, BufRead, BufReader, ErrorKind, Write};
32use std::panic::PanicInfo;
33use std::path::{Path, PathBuf};
34
35#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
37pub enum Verbosity {
38 Minimal,
40 Medium,
42 Full,
44}
45
46impl Verbosity {
47 pub fn from_env() -> Self {
49 match std::env::var("RUST_BACKTRACE") {
50 Ok(ref x) if x == "full" => Verbosity::Full,
51 Ok(_) => Verbosity::Medium,
52 Err(_) => Verbosity::Minimal,
53 }
54 }
55
56 fn apply_to_process(self) {
57 let val = match self {
58 Verbosity::Full => "full",
59 Verbosity::Medium => "1",
60 Verbosity::Minimal => "",
61 };
62 if val.is_empty() {
63 std::env::remove_var("RUST_BACKTRACE");
64 } else {
65 std::env::set_var("RUST_BACKTRACE", val);
66 }
67 }
68}
69
70pub fn install() {
72 Settings::auto().install()
73}
74
75pub fn debug_install() {
77 Settings::debug().install()
78}
79
80struct Frame {
81 name: Option<String>,
82 lineno: Option<u32>,
83 filename: Option<PathBuf>,
84}
85
86impl Frame {
87 fn name_without_hash(&self) -> Option<&str> {
88 let name = self.name.as_ref()?;
89 let has_hash_suffix = name.len() > 19
90 && &name[name.len() - 19..name.len() - 16] == "::h"
91 && name[name.len() - 16..].chars().all(|x| x.is_digit(16));
92 if has_hash_suffix {
93 Some(&name[..name.len() - 19])
94 } else {
95 Some(name)
96 }
97 }
98
99 fn is_dependency_code(&self) -> bool {
100 const SYM_PREFIXES: &[&str] = &[
101 "std::",
102 "core::",
103 "backtrace::backtrace::",
104 "_rust_begin_unwind",
105 "better_panic::",
106 "__rust_",
107 "___rust_",
108 "__pthread",
109 "_main",
110 "main",
111 "__scrt_common_main_seh",
112 "BaseThreadInitThunk",
113 "_start",
114 "__libc_start_main",
115 "start_thread",
116 ];
117
118 if let Some(ref name) = self.name {
120 if SYM_PREFIXES.iter().any(|x| name.starts_with(x)) {
121 return true;
122 }
123 }
124
125 const FILE_PREFIXES: &[&str] = &[
126 "rust:",
127 "/rustc/",
128 "src/libstd/",
129 "src/libpanic_unwind/",
130 "src/libtest/",
131 ];
132
133 if let Some(filename) = self.filename.as_ref().and_then(|x| x.to_str()) {
135 if filename.contains('<') {
138 return true;
139 }
140 if FILE_PREFIXES.iter().any(|x| filename.starts_with(x))
141 || filename.contains("/.cargo/registry/src/")
142 {
143 return true;
144 }
145 }
146
147 false
148 }
149
150 fn is_post_panic_code(&self) -> bool {
157 const SYM_PREFIXES: &[&str] = &[
158 "_rust_begin_unwind",
159 "panic_bounds_check",
160 "core::result::unwrap_failed",
161 "core::panicking::panic_fmt",
162 "core::panicking::panic_bounds_check",
163 "color_backtrace::create_panic_handler",
164 "std::panicking::begin_panic",
165 "begin_panic_fmt",
166 "rust_begin_panic",
167 "panic_bounds_check",
168 "panic_fmt",
169 ];
170
171 if let Some(filename) = self.filename.as_ref().and_then(|x| x.to_str()) {
172 if filename.contains("libcore/panicking.rs") {
173 return true;
174 }
175 }
176
177 match self.name_without_hash() {
178 Some(name) => SYM_PREFIXES
179 .iter()
180 .any(|x| name.starts_with(x) || name.ends_with("__rust_end_short_backtrace")),
181 None => false,
182 }
183 }
184
185 fn is_runtime_init_code(&self) -> bool {
188 const SYM_PREFIXES: &[&str] =
189 &["std::rt::lang_start::", "test::run_test::run_test_inner::"];
190
191 let (name, file) = match (self.name_without_hash(), self.filename.as_ref()) {
192 (Some(name), Some(filename)) => (name, filename.to_string_lossy()),
193 _ => return false,
194 };
195
196 if SYM_PREFIXES
197 .iter()
198 .any(|x| name.starts_with(x) || name.ends_with("__rust_start_short_backtrace"))
199 {
200 return true;
201 }
202
203 if name == "{{closure}}" && file == "src/libtest/lib.rs" {
205 return true;
206 }
207
208 false
209 }
210
211 fn is_call_once(&self) -> bool {
213 if let Some(name) = self.name_without_hash() {
214 name.ends_with("FnOnce::call_once")
215 } else {
216 false
217 }
218 }
219
220 fn print_source(&self, s: &Settings) -> Result<(), io::Error> {
221 let (lineno, filename) = match (self.lineno, self.filename.as_ref()) {
222 (Some(a), Some(b)) => (a, b),
223 _ => return Ok(()),
225 };
226
227 print_source(filename, lineno, s)
228 }
229
230 fn print(&self, s: &Settings) -> Result<(), io::Error> {
231 let is_dependency_code = self.is_dependency_code();
232
233 let name = self.name_without_hash().unwrap_or("<unknown>");
234
235 let mut name_style = console::Style::new();
237 if is_dependency_code {
238 name_style = name_style.cyan();
239 } else {
240 name_style = name_style.green();
241 }
242
243 let file = match &self.filename {
245 Some(filename) => trim_filename(filename),
246 None => Cow::Borrowed("<unknown>"),
247 };
248
249 if s.lineno_suffix {
250 writeln!(
251 &s.out,
252 " File \"{}:{}\", in {}",
253 style(file).underlined(),
254 style(self.lineno.unwrap_or(0)).yellow(),
255 name_style.apply_to(name)
256 )?;
257 } else {
258 writeln!(
259 &s.out,
260 " File \"{}\", line {}, in {}",
261 style(file).underlined(),
262 style(self.lineno.unwrap_or(0)).yellow(),
263 name_style.apply_to(name)
264 )?;
265 }
266
267 if s.verbosity >= Verbosity::Full {
269 self.print_source(s)?;
270 }
271
272 Ok(())
273 }
274}
275
276#[derive(Debug, Clone)]
278pub struct Settings {
279 message: String,
280 out: console::Term,
281 verbosity: Verbosity,
282 backtrace_first: bool,
283 most_recent_first: bool,
284 lineno_suffix: bool,
285}
286
287impl Default for Settings {
288 fn default() -> Self {
289 Self {
290 verbosity: Verbosity::from_env(),
291 message: "The application panicked (crashed).".to_owned(),
292 out: console::Term::stderr(),
293 backtrace_first: true,
294 most_recent_first: true,
295 lineno_suffix: false,
296 }
297 }
298}
299
300impl Settings {
301 pub fn new() -> Self {
303 Self::default()
304 }
305
306 pub fn debug() -> Self {
308 Self::new().verbosity(Verbosity::Full)
309 }
310
311 pub fn auto() -> Self {
313 #[cfg(debug_assertions)]
314 {
315 Self::debug()
316 }
317 #[cfg(not(debug_assertions))]
318 {
319 Self::new()
320 }
321 }
322
323 pub fn message(mut self, message: impl Into<String>) -> Self {
327 self.message = message.into();
328 self
329 }
330
331 pub fn verbosity(mut self, v: Verbosity) -> Self {
335 self.verbosity = v;
336 self
337 }
338
339 pub fn backtrace_first(mut self, value: bool) -> Self {
344 self.backtrace_first = value;
345 self
346 }
347
348 pub fn most_recent_first(mut self, value: bool) -> Self {
353 self.most_recent_first = value;
354 self
355 }
356
357 pub fn lineno_suffix(mut self, value: bool) -> Self {
364 self.lineno_suffix = value;
365 self
366 }
367
368 pub fn create_panic_handler(self) -> Box<dyn Fn(&PanicInfo<'_>) + 'static + Sync + Send> {
370 Box::new(move |pi| {
371 print_panic_and_backtrace(pi, &self).unwrap();
372 })
373 }
374
375 pub fn install(self) {
377 self.verbosity.apply_to_process();
378 std::panic::set_hook(self.create_panic_handler())
379 }
380}
381
382fn print_source(filename: &Path, lineno: u32, s: &Settings) -> Result<(), io::Error> {
383 let file = match File::open(filename) {
384 Ok(file) => file,
385 Err(ref e) if e.kind() == ErrorKind::NotFound => return Ok(()),
386 e @ Err(_) => e?,
387 };
388
389 let reader = BufReader::new(file);
390 let source_line = reader.lines().nth((lineno - 1) as usize);
391 if let Some(Ok(source_line)) = source_line {
392 writeln!(&s.out, " {}", style(source_line.trim()).dim())?;
393 }
394
395 Ok(())
396}
397
398fn print_backtrace(bt: Option<&backtrace::Backtrace>, s: &Settings) -> Result<(), io::Error> {
399 if s.most_recent_first {
400 writeln!(
401 &s.out,
402 "{}",
403 style("Backtrace (most recent call first):").bold()
404 )?;
405 } else {
406 writeln!(
407 &s.out,
408 "{}",
409 style("Backtrace (most recent call last):").bold()
410 )?;
411 }
412
413 let mut frames = Vec::new();
415 if let Some(bt) = bt {
416 for frame in bt.frames() {
417 for sym in frame.symbols() {
418 frames.push(Frame {
419 name: sym.name().map(|x| x.to_string()),
420 lineno: sym.lineno(),
421 filename: sym.filename().map(|x| x.into()),
422 });
423 }
424 }
425 } else {
426 backtrace::trace(|x| {
427 backtrace::resolve(x.ip(), |sym| {
429 frames.push(Frame {
430 name: sym.name().map(|x| x.to_string()),
431 lineno: sym.lineno(),
432 filename: sym.filename().map(|x| x.into()),
433 });
434 });
435
436 true
437 });
438 }
439
440 let top_cutoff = frames
442 .iter()
443 .rposition(Frame::is_post_panic_code)
444 .map(|x| x + 1)
445 .unwrap_or(0);
446
447 let bottom_cutoff = frames
449 .iter()
450 .position(Frame::is_runtime_init_code)
451 .map(|x| x - 1)
452 .unwrap_or_else(|| frames.len());
453
454 let mut frames = &frames[top_cutoff..bottom_cutoff];
456
457 if !frames.is_empty() && frames[frames.len() - 1].is_call_once() {
458 frames = &frames[..frames.len() - 1];
459 }
460
461 if s.most_recent_first {
462 for frame in frames {
463 frame.print(s)?;
464 }
465 } else {
466 for frame in frames.iter().rev() {
467 frame.print(s)?;
468 }
469 }
470
471 Ok(())
472}
473
474fn print_panic_and_backtrace(pi: &PanicInfo, s: &Settings) -> Result<(), io::Error> {
475 if s.backtrace_first {
476 print_backtrace_info(s)?;
477 writeln!(&s.out)?;
478 }
479 print_panic_info(pi, s)?;
480 if !s.backtrace_first {
481 writeln!(&s.out)?;
482 print_backtrace_info(s)?;
483 }
484 Ok(())
485}
486
487fn trim_filename(file: &Path) -> Cow<'_, str> {
488 let filename = file.to_str().unwrap_or("<bad utf8>");
489 if filename.starts_with("/rustc/") {
490 if let Some(filename) = filename.get(48..) {
491 Cow::Owned(format!("rust:{}", filename))
492 } else {
493 Cow::Borrowed(filename)
494 }
495 } else if let Some(basename) = file.file_name().and_then(|x| x.to_str()) {
496 if basename.starts_with('<') && basename.ends_with('>') {
497 Cow::Borrowed(basename)
498 } else {
499 Cow::Borrowed(filename)
500 }
501 } else {
502 Cow::Borrowed(filename)
503 }
504}
505
506fn print_panic_info(pi: &PanicInfo, s: &Settings) -> Result<(), io::Error> {
507 writeln!(&s.out, "{}", style(&s.message).bold())?;
508
509 let thread = std::thread::current();
510 let thread_name = thread.name().unwrap_or("<unnamed>");
511
512 let payload = pi
514 .payload()
515 .downcast_ref::<String>()
516 .map(String::as_str)
517 .or_else(|| pi.payload().downcast_ref::<&str>().cloned())
518 .unwrap_or("Box<Any>");
519
520 for line in payload.lines() {
521 writeln!(&s.out, " {}", style(line).yellow())?;
522 }
523
524 write!(&s.out, "in ")?;
526 if let Some(loc) = pi.location() {
527 if s.lineno_suffix {
528 writeln!(
529 &s.out,
530 "{}:{}",
531 style(trim_filename(Path::new(loc.file()))).underlined(),
532 style(loc.line()).yellow()
533 )?;
534 } else {
535 writeln!(
536 &s.out,
537 "{}, line {}",
538 style(trim_filename(Path::new(loc.file()))).underlined(),
539 style(loc.line()).yellow()
540 )?;
541 }
542 } else {
543 writeln!(&s.out, "<unknown>")?;
544 }
545 writeln!(&s.out, "thread: {}", style(thread_name).yellow())?;
546 Ok(())
547}
548
549fn print_backtrace_info(s: &Settings) -> Result<(), io::Error> {
550 if s.verbosity == Verbosity::Minimal {
552 writeln!(
553 &s.out,
554 "\nBacktrace omitted. Run with RUST_BACKTRACE=1 to display it."
555 )?;
556 }
557 if s.verbosity <= Verbosity::Medium {
558 if s.verbosity == Verbosity::Medium {
559 writeln!(&s.out)?;
561 }
562
563 writeln!(
564 &s.out,
565 "Run with RUST_BACKTRACE=full to include source snippets."
566 )?;
567 }
568
569 if s.verbosity >= Verbosity::Medium {
570 print_backtrace(None, s)?;
571 }
572
573 Ok(())
574}