Skip to main content

scoped_panic_hook/
panic.rs

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/// Panic info gathered as one record
8///
9/// Contains panic's location, message or raw payload, and backtrace, if collected
10#[derive(Debug)]
11pub struct Panic {
12    location: Option<OwnedLocation>,
13    payload: Result<Message, RawPayload>,
14    backtrace: Backtrace,
15}
16/// Backtrace display style
17///
18/// There's [`std::panic::BacktraceStyle`], but it's still experimental
19#[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    /// Panic location, if original [`std::panic::PanicHookInfo`] provided one
34    pub fn location(&self) -> Option<&OwnedLocation> {
35        self.location.as_ref()
36    }
37    /// Original panic message which was supplied to [`std::panic!`] macro
38    ///
39    /// If panic was raised using [`std::panic::panic_any`] with non-string payload,
40    /// this function returns substitute message
41    pub fn message(&self) -> &str {
42        self.payload.as_ref().map_or(UNKNOWN_PANIC, |m| m.as_ref())
43    }
44    /// Raw panic payload if it wasn't recognized as string-like message and converted to it
45    pub fn raw_payload(&self) -> Option<&RawPayload> {
46        self.payload.as_ref().err()
47    }
48    /// Panic's backtrace gathered from within panic hook
49    pub fn backtrace(&self) -> &Backtrace {
50        &self.backtrace
51    }
52    /// Transforms panic info into raw panic payload.
53    /// If it's a string-like message, it gets re-wrapped again.
54    ///
55    /// Useful when you need something like rethrowing panic
56    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    /// Produces [`std::fmt::Display`]'able object
64    /// which displays panic's message, location and short backtrace, if captured
65    pub fn display_with_backtrace(&self) -> impl Display + '_ {
66        self.display_with_backtrace_style(BacktraceStyle::Short)
67    }
68
69    /// Produces [`std::fmt::Display`]'able object
70    /// which displays panic's message, location and backtrace, if captured, with specified style
71    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        // NB: backtrace print style selection via `#` isn't well documented
86        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    /// Displays panic's message and location
96    ///
97    /// To display full panic info including backtrace, use [`Panic::display_with_backtrace`]
98    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/// Determines how to capture backtrace when catching panic
105#[derive(Copy, Clone, Debug, Default)]
106pub enum CaptureBacktrace {
107    /// Use [`std::backtrace::Backtrace::disabled`]
108    No,
109    /// Use [`std::backtrace::Backtrace::capture`]
110    #[default]
111    Yes,
112    /// Use [`std::backtrace::Backtrace::force_capture`]
113    Force,
114}
115/// Options for [`catch_panic_with_config`]
116#[derive(Copy, Clone, Debug, Default)]
117pub struct CatchPanicConfig {
118    /// Whether backtrace should be captured
119    pub capture_backtrace: CaptureBacktrace,
120}
121/// Runs provided closure and captures panic if one happens, along with its backtrace
122///
123/// # Limitations
124///
125/// Stable API doesn't allow to detect from panic hook that panic being handled will unwind.
126/// So if you get unwindable panic, panic info will be swallowed. This will be fixed when
127/// [#92988](https://github.com/rust-lang/rust/issues/92988) stabilizes.
128/// Correct API is used on nightly builds, which resolves issue.
129///
130/// # Panic modes
131///
132/// In case crate is compiled in panic mode other than `panic=unwind`, hook will unconditionally
133/// forward execution to previously installed hook. In case of panic there will be no unwind,
134/// so client code will not get panic info anyway, although we set hook anyway
135/// in case there are nested scoped hooks, to preserve local behavior.
136///
137/// # Parameters
138/// * `body` - closure which should be run with capturing panics.
139///   The closure has same requirements as the parameter to [`std::panic::catch_unwind`].
140///   In particular, if you want to assert that closure is unwind safe when type system
141///   can't deduce it, you can use [`std::panic::AssertUnwindSafe`].
142///   See [`std::panic::catch_unwind`] for details.
143///
144/// # Returns
145/// * `Ok(result)` - with `body1`'s return value if it completes without panic
146/// * `Err(panic)` - if `body` panics in process, with `Panic` as error value
147pub 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}
155/// Runs provided closure and captures panic if one happens
156///
157/// # Limitations
158///
159/// Stable API doesn't allow to detect from panic hook that panic being handled will unwind.
160/// So if you get unwindable panic, panic info will be swallowed. This will be fixed when
161/// [#92988](https://github.com/rust-lang/rust/issues/92988) stabilizes
162///
163/// # Panic modes
164///
165/// In case crate is compiled in panic mode other than `panic=unwind`, hook will unconditionally
166/// forward execution to previously installed hook. In case of panic there will be no unwind,
167/// so client code will not get panic info anyway, although we set hook anyway
168/// in case there are nested scoped hooks, to preserve local behavior.
169///
170/// # Parameters
171/// * `config` - options used when capturing panic info
172/// * `body` - closure which should be run with capturing panics.
173///   The closure has same requirements as the parameter to [`std::panic::catch_unwind`].
174///   In particular, if you want to assert that closure is unwind safe when type system
175///   can't deduce it, you can use [`std::panic::AssertUnwindSafe`].
176///   See [`std::panic::catch_unwind`] for details.
177///
178/// # Returns
179/// * `Ok(result)` - with `body1`'s return value if it completes without panic
180/// * `Err(panic)` - if `body` panics in process, with `Panic` as error value
181pub 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/// Simple structure which mirrors [`std::panic::Location`],
188/// although owns file name and is freely movable around
189#[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                        // Means we encountered new panic while already unwinding
300                        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    /// Constructs intermediate panic, some of its data will be reused
337    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            // std enum is non-exhaustive
363            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}
409/// Creates type whose `Display` and `Debug` implementations
410/// are provided with function `format_fn`
411///
412/// Replica of experimental `std::fmt::from_fn`
413fn 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        // NB: we test backtrace status separately
449        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    /// Find panic message in output
495    ///
496    /// # Parameters
497    /// * `string` - source string
498    /// * `module_path` - `module_path!()` value
499    /// * `test_name` - test function name
500    /// * `panic_message` - concrete panic message, if specified, or any panic message, if not
501    ///
502    /// # Returns
503    /// * `Ok(str)` - found pattern, returns string's tail after pattern
504    /// * `Err(str)` - pattern not found, returns initial string
505    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        // Start of next line after "thread panicked..."
526        let string = if let Some(next) = string.find('\n') {
527            &string[(next + 1)..]
528        } else {
529            return init_string;
530        };
531        // Panic message line and text after it
532        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        // This test may be a bit volatile, in case panicking will change
595        #[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            // We can't depend on some additional panics like landing pad ones,
605            // so we check only for presence of initial and drop panics once;
606            // the rest is deemed undecided
607            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            // We can't depend on some additional panics like landing pad ones,
636            // so we check only for presence of initial FFI panic once;
637            // the rest is deemed undecided
638            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            // Same as with other nounwind tests, we check only for panic we caused explicitly
657            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}