1use std::any::Any;
2use std::backtrace::Backtrace;
3use std::borrow::Cow;
4use std::error::Error;
5use std::fmt::{self, Display};
6use std::panic::{Location, UnwindSafe};
7#[derive(Debug)]
11pub struct Panic {
12 location: Option<OwnedLocation>,
13 payload: Result<Message, RawPayload>,
14 backtrace: Backtrace,
15}
16#[derive(Copy, Clone, Debug)]
20pub enum BacktraceStyle {
21 Off,
22 Short,
23 Full,
24}
25
26type Message = Cow<'static, str>;
27
28type RawPayload = Box<dyn Any + Send + 'static>;
29
30const UNKNOWN_PANIC: &str = "<unknown panic>";
31
32impl Panic {
33 pub fn location(&self) -> Option<&OwnedLocation> {
35 self.location.as_ref()
36 }
37 pub fn message(&self) -> &str {
42 self.payload.as_ref().map_or(UNKNOWN_PANIC, |m| m.as_ref())
43 }
44 pub fn raw_payload(&self) -> Option<&RawPayload> {
46 self.payload.as_ref().err()
47 }
48 pub fn backtrace(&self) -> &Backtrace {
50 &self.backtrace
51 }
52 pub fn into_raw_payload(self) -> RawPayload {
57 match self.payload {
58 Err(raw_payload) => raw_payload,
59 Ok(Cow::Borrowed(str)) => Box::new(str) as RawPayload,
60 Ok(Cow::Owned(str)) => Box::new(str) as RawPayload,
61 }
62 }
63 pub fn display_with_backtrace(&self) -> impl Display + '_ {
66 self.display_with_backtrace_style(BacktraceStyle::Short)
67 }
68
69 pub fn display_with_backtrace_style(&self, style: BacktraceStyle) -> impl Display + '_ {
72 format_from_fn(move |f| self.fmt_panic(style, f))
73 }
74
75 fn fmt_panic(
76 &self,
77 backtrace_style: BacktraceStyle,
78 f: &mut std::fmt::Formatter<'_>,
79 ) -> std::fmt::Result {
80 if let Some(loc) = self.location() {
81 write!(f, "Panic \"{}\" at {}", self.message(), loc)?;
82 } else {
83 write!(f, "Panic \"{}\"", self.message())?;
84 }
85 match backtrace_style {
87 BacktraceStyle::Off => Ok(()),
88 BacktraceStyle::Short => writeln!(f, "\nBacktrace: {}", self.backtrace),
89 BacktraceStyle::Full => writeln!(f, "\nBacktrace: {:#}", self.backtrace),
90 }
91 }
92}
93
94impl Display for Panic {
95 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
99 self.fmt_panic(BacktraceStyle::Off, f)
100 }
101}
102
103impl Error for Panic {}
104#[derive(Copy, Clone, Debug, Default)]
106pub enum CaptureBacktrace {
107 No,
109 #[default]
111 Yes,
112 Force,
114}
115#[derive(Copy, Clone, Debug, Default)]
117pub struct CatchPanicConfig {
118 pub capture_backtrace: CaptureBacktrace,
120}
121pub fn catch_panic<R>(body: impl FnOnce() -> R + UnwindSafe) -> Result<R, Panic> {
148 catch_panic_with_config(
149 CatchPanicConfig {
150 capture_backtrace: CaptureBacktrace::Yes,
151 },
152 body,
153 )
154}
155pub fn catch_panic_with_config<R>(
182 config: CatchPanicConfig,
183 body: impl FnOnce() -> R + UnwindSafe,
184) -> Result<R, Panic> {
185 catch_impl::catch_panic_with_config(config, body)
186}
187#[derive(Clone, Debug, Hash, PartialEq, Eq, PartialOrd, Ord)]
190pub struct OwnedLocation {
191 file: String,
192 line: u32,
193 column: u32,
194}
195
196impl OwnedLocation {
197 pub fn file(&self) -> &str {
198 &self.file
199 }
200
201 pub fn line(&self) -> u32 {
202 self.line
203 }
204
205 pub fn column(&self) -> u32 {
206 self.column
207 }
208}
209
210impl From<&'_ Location<'_>> for OwnedLocation {
211 fn from(value: &Location<'_>) -> Self {
212 Self {
213 file: value.file().to_owned(),
214 line: value.line(),
215 column: value.column(),
216 }
217 }
218}
219
220impl Display for OwnedLocation {
221 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
222 write!(f, "{}:{}:{}", self.file, self.line, self.column)
223 }
224}
225
226#[cfg(panic = "unwind")]
227mod catch_impl {
228 use super::{
229 BacktraceStyle, CaptureBacktrace, CatchPanicConfig, Message, Panic, RawPayload,
230 UNKNOWN_PANIC,
231 };
232 use crate::hook::{NextHook, catch_unwind_with_scoped_hook};
233 use std::backtrace::Backtrace;
234 use std::panic::{PanicHookInfo, UnwindSafe};
235
236 pub fn catch_panic_with_config<R>(
237 config: CatchPanicConfig,
238 body: impl FnOnce() -> R + UnwindSafe,
239 ) -> Result<R, Panic> {
240 let mut hook = CatchPanicHook::new(config);
241
242 match catch_unwind_with_scoped_hook(hook.hook_fn(), body) {
243 Ok(ok) => Ok(ok),
244 Err(raw_payload) => {
245 let panic = hook.into_panic();
246 Err(Panic {
247 payload: transform_payload(raw_payload),
248 ..panic
249 })
250 }
251 }
252 }
253
254 struct CatchPanicHook {
255 config: CatchPanicConfig,
256 state: HookState,
257 }
258
259 enum HookState {
260 NoPanic,
261 Panic(Panic),
262 NoUnwind,
263 }
264
265 #[cfg(nightly)]
266 fn can_unwind(info: &PanicHookInfo<'_>) -> bool {
267 info.can_unwind()
268 }
269
270 #[cfg(not(nightly))]
271 fn can_unwind(_: &PanicHookInfo<'_>) -> bool {
272 true
273 }
274
275 impl CatchPanicHook {
276 fn new(config: CatchPanicConfig) -> Self {
277 Self {
278 config,
279 state: HookState::NoPanic,
280 }
281 }
282
283 fn hook_fn(&mut self) -> impl FnMut(&PanicHookInfo<'_>) -> NextHook + '_ {
284 move |info| {
285 match &self.state {
286 HookState::NoPanic => {
287 if can_unwind(info) {
288 eprintln!("NoPanic can unwind");
289 self.state = HookState::Panic(panic_from_hook_info(info, &self.config));
290 NextHook::Break
291 } else {
292 eprintln!("NoPanic no unwind");
293 self.state = HookState::NoUnwind;
294 NextHook::PrevInstalledHook
295 }
296 }
297 HookState::Panic(panic) => {
298 eprintln!("Panic");
299 print_panic_in_hook(panic);
301 self.state = HookState::NoUnwind;
302 NextHook::PrevInstalledHook
303 }
304 HookState::NoUnwind => {
305 eprintln!("NoUnwind");
306 NextHook::PrevInstalledHook
307 }
308 }
309 }
310 }
311
312 fn into_panic(self) -> Panic {
313 match self.state {
314 HookState::NoPanic => panic!("Panic info wasn't recorded"),
315 HookState::Panic(panic) => panic,
316 HookState::NoUnwind => {
317 panic!("`catch_unwind` unexpectedly returned from no-unwind situation")
318 }
319 }
320 }
321 }
322
323 fn transform_payload(payload: RawPayload) -> Result<Message, RawPayload> {
324 let payload = match payload.downcast::<&str>() {
325 Ok(str) => return Ok((*str).into()),
326 Err(p) => p,
327 };
328
329 let payload = match payload.downcast::<String>() {
330 Ok(str) => return Ok((*str).into()),
331 Err(p) => p,
332 };
333
334 Err(payload)
335 }
336 fn panic_from_hook_info(info: &PanicHookInfo<'_>, config: &CatchPanicConfig) -> Panic {
338 let message = if let Some(str) = info.payload().downcast_ref::<&str>() {
339 (*str).into()
340 } else if let Some(str) = info.payload().downcast_ref::<String>() {
341 str.clone().into()
342 } else {
343 UNKNOWN_PANIC.into()
344 };
345
346 Panic {
347 location: info.location().map(Into::into),
348 payload: Ok(message),
349 backtrace: match config.capture_backtrace {
350 CaptureBacktrace::No => Backtrace::disabled(),
351 CaptureBacktrace::Yes => Backtrace::capture(),
352 CaptureBacktrace::Force => Backtrace::force_capture(),
353 },
354 }
355 }
356
357 #[cfg(nightly)]
358 fn in_hook_backtrace_style() -> BacktraceStyle {
359 match std::panic::get_backtrace_style() {
360 None | Some(std::panic::BacktraceStyle::Off) => BacktraceStyle::Off,
361 Some(std::panic::BacktraceStyle::Full) => BacktraceStyle::Full,
362 Some(_) => BacktraceStyle::Short,
364 }
365 }
366
367 #[cfg(not(nightly))]
368 fn in_hook_backtrace_style() -> BacktraceStyle {
369 BacktraceStyle::Short
370 }
371
372 fn print_panic_in_hook(panic: &Panic) {
373 use std::fmt::Write as _;
374 use std::io::Write as _;
375
376 let thread = std::thread::current();
377 let name = thread.name().unwrap_or("<unnamed>");
378 let mut msg = if let Some(loc) = panic.location() {
379 format!("\nthread '{name}' panicked at {loc}:\n{}", panic.message())
380 } else {
381 format!("\nthread '{name}' panicked:\n{}", panic.message())
382 };
383 let _ = match in_hook_backtrace_style() {
384 BacktraceStyle::Off => writeln!(msg),
385 BacktraceStyle::Short => writeln!(msg, "\nstack backtrace:\n{}", panic.backtrace()),
386 BacktraceStyle::Full => writeln!(msg, "\nstack backtrace:\n{:#}", panic.backtrace()),
387 };
388
389 let _ = std::io::stderr().lock().write_all(msg.as_bytes());
390 }
391}
392
393#[cfg(not(panic = "unwind"))]
394mod catch_impl {
395 use super::{CatchPanicConfig, Panic};
396 use crate::hook::{NextHook, catch_unwind_with_scoped_hook};
397 use std::panic::UnwindSafe;
398
399 pub fn catch_panic_with_config<R>(
400 _: CatchPanicConfig,
401 body: impl FnOnce() -> R + UnwindSafe,
402 ) -> Result<R, Panic> {
403 match catch_unwind_with_scoped_hook(|_| NextHook::PrevInstalledHook, body) {
404 Ok(ok) => Ok(ok),
405 Err(_) => unreachable!(),
406 }
407 }
408}
409fn format_from_fn<F: Fn(&mut fmt::Formatter<'_>) -> fmt::Result>(
414 format_fn: F,
415) -> impl fmt::Debug + fmt::Display {
416 struct FormatFromFn<F> {
417 format_fn: F,
418 }
419
420 impl<F: Fn(&mut fmt::Formatter<'_>) -> fmt::Result> fmt::Debug for FormatFromFn<F> {
421 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
422 (self.format_fn)(f)
423 }
424 }
425
426 impl<F: Fn(&mut fmt::Formatter<'_>) -> fmt::Result> fmt::Display for FormatFromFn<F> {
427 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
428 (self.format_fn)(f)
429 }
430 }
431
432 FormatFromFn { format_fn }
433}
434
435#[cfg(test)]
436mod tests {
437 use super::*;
438 use std::backtrace::BacktraceStatus;
439 use std::env;
440 use std::panic::panic_any;
441 use std::path::Path;
442 use subprocess_test::subprocess_test;
443
444 #[test]
445 fn simple_catch_panic() {
446 let line = line!();
447 let panic = catch_panic(|| panic!("Oops!")).unwrap_err();
448 assert!(panic.raw_payload().is_none());
450 assert_eq!(panic.message(), "Oops!");
451
452 let loc = panic.location().unwrap();
453 assert!(Path::new(loc.file()).ends_with("panic.rs"));
454 assert_eq!(loc.line(), line + 1);
455 assert_eq!(loc.column(), 36);
456
457 let payload = panic.into_raw_payload();
458 assert_eq!(payload.downcast_ref::<&str>(), Some(&"Oops!"));
459 }
460
461 #[test]
462 fn catch_panic_any() {
463 let panic = catch_panic(|| panic_any("Oops!")).unwrap_err();
464 assert_eq!(panic.message(), "Oops!");
465 assert!(panic.raw_payload().is_none());
466
467 let panic = catch_panic(|| panic_any(42usize)).unwrap_err();
468 assert_eq!(panic.message(), "<unknown panic>");
469 assert_eq!(
470 panic.raw_payload().and_then(|p| p.downcast_ref::<usize>()),
471 Some(&42usize)
472 );
473 }
474
475 #[test]
476 fn catch_panic_no_backtrace() {
477 let panic = catch_panic_with_config(
478 CatchPanicConfig {
479 capture_backtrace: CaptureBacktrace::No,
480 },
481 || panic!("Oops!"),
482 )
483 .unwrap_err();
484 assert_eq!(panic.backtrace().status(), BacktraceStatus::Disabled);
485 }
486
487 struct JustDie;
488
489 impl Drop for JustDie {
490 fn drop(&mut self) {
491 panic!("Die!");
492 }
493 }
494 fn find_panic_message<'a, 'b>(
506 string: &'a str,
507 module_path: &str,
508 test_name: &str,
509 panic_message: impl Into<Option<&'b str>>,
510 ) -> Result<&'a str, &'a str> {
511 let init_string = Err(string);
512 let thread_name = format!("{module_path}::{test_name}");
513 let thread_name = &thread_name[thread_name
514 .find("::")
515 .expect("Full test path is expected to include crate name")
516 + 2..];
517 let panic_message: Option<&str> = panic_message.into();
518
519 let panic_prefix = format!("thread '{thread_name}' panicked at");
520 let string = if let Some(next) = string.find(&panic_prefix) {
521 &string[next + panic_prefix.len()..]
522 } else {
523 return init_string;
524 };
525 let string = if let Some(next) = string.find('\n') {
527 &string[(next + 1)..]
528 } else {
529 return init_string;
530 };
531 let (msg_line, string) = if let Some(next) = string.find('\n') {
533 (&string[..next], &string[(next + 1)..])
534 } else {
535 (string, "")
536 };
537
538 if let Some(panic_message) = panic_message {
539 if panic_message == msg_line {
540 Ok(string)
541 } else {
542 init_string
543 }
544 } else {
545 Ok(string)
546 }
547 }
548
549 subprocess_test! {
550 #[test]
551 fn catch_panic_backtrace_disabled() {
552 unsafe {
553 env::set_var("RUST_BACKTRACE", "0");
554 env::set_var("RUST_LIB_BACKTRACE", "0");
555 }
556
557 let panic = catch_panic_with_config(
558 CatchPanicConfig {
559 capture_backtrace: CaptureBacktrace::Yes,
560 },
561 || panic!("Oops!"),
562 )
563 .unwrap_err();
564 assert_eq!(panic.backtrace().status(), BacktraceStatus::Disabled);
565
566 let panic = catch_panic_with_config(
567 CatchPanicConfig {
568 capture_backtrace: CaptureBacktrace::Force,
569 },
570 || panic!("Oops!"),
571 )
572 .unwrap_err();
573 assert_eq!(panic.backtrace().status(), BacktraceStatus::Captured);
574 }
575
576 #[test]
577 fn catch_panic_backtrace_enabled() {
578 unsafe {
579 env::set_var("RUST_BACKTRACE", "1");
580 }
581
582 let panic = catch_panic(|| panic!("Oops!")).unwrap_err();
583 assert_eq!(panic.backtrace().status(), BacktraceStatus::Captured);
584 }
585
586 #[test]
587 fn panic_in_drop() {
588 let panic = catch_panic(|| {
589 let _ = JustDie;
590 }).unwrap_err();
591 assert_eq!(panic.message(), "Die!");
592 }
593
594 #[test]
596 fn panic_in_drop_on_unwind() {
597 let _ = catch_panic(|| {
598 let _keeper = JustDie;
599 panic!("Cause unwind");
600 });
601 }
602 verify |success, output| {
603 assert!(!success, "Panic during unwind from within `catch_panic` should fail");
604 let Ok(output) = find_panic_message(&output, module_path!(), "panic_in_drop_on_unwind", "Cause unwind") else {
608 panic!("Couldn't find initial panic");
609 };
610
611 let Err(_) = find_panic_message(output, module_path!(), "panic_in_drop_on_unwind", "Cause unwind") else {
612 panic!("Found initial panic for the second time");
613 };
614
615 let Ok(output) = find_panic_message(output, module_path!(), "panic_in_drop_on_unwind", "Die!") else {
616 panic!("Couldn't find panic from drop");
617 };
618
619 let Err(_) = find_panic_message(output, module_path!(), "panic_in_drop_on_unwind", "Die!") else {
620 panic!("Found panic from drop for the second time");
621 };
622 }
623
624 #[test]
625 fn panic_from_ffi() {
626 let _ = catch_panic(|| {
627 extern "C" fn ffi_panic() {
628 panic!("Hi from FFI");
629 }
630 ffi_panic();
631 });
632 }
633 verify |success, output| {
634 assert!(!success, "Nounwind panic should've failed even from within catch_panic");
635 let Ok(output) = find_panic_message(&output, module_path!(), "panic_from_ffi", "Hi from FFI") else {
639 panic!("Couldn't find initial panic from FFI");
640 };
641
642 let Err(_) = find_panic_message(output, module_path!(), "panic_from_ffi", "Hi from FFI") else {
643 panic!("Found initial panic from FFI for the second time");
644 };
645 }
646
647 #[test]
648 #[cfg(nightly)]
649 fn panic_nounwind() {
650 let _ = catch_panic(|| {
651 core::panicking::panic_nounwind("Nounwind");
652 });
653 }
654 verify |success, output| {
655 assert!(!success, "Nounwind panic should've failed even from within catch_panic");
656 let Ok(output) = find_panic_message(&output, module_path!(), "panic_nounwind", "Nounwind") else {
658 panic!("Couldn't find initial panic from `panic_nounwind`");
659 };
660
661 let Err(_) = find_panic_message(output, module_path!(), "panic_nounwind", "Nounwind") else {
662 panic!("Found initial panic from `panic_nounwind` for the second time");
663 };
664 }
665 }
666}