1use backtrace::Backtrace;
28use regex::Regex;
29use std::{
30 cell::Cell,
31 io::{self, Write},
32 marker::PhantomData,
33 panic::{self, PanicHookInfo},
34 sync::LazyLock,
35 thread,
36};
37
38thread_local! {
39 static ON_PANIC: Cell<OnPanic> = Cell::new(OnPanic::Abort);
40}
41
42#[derive(Debug, Clone, Copy, PartialEq)]
44enum OnPanic {
45 Abort,
47 Unwind,
49 NeverAbort,
51}
52
53pub fn set(bug_url: &str, version: &str) {
60 panic::set_hook(Box::new({
61 let version = version.to_string();
62 let bug_url = bug_url.to_string();
63 move |c| panic_hook(c, &bug_url, &version)
64 }));
65}
66
67macro_rules! ABOUT_PANIC {
68 () => {
69 "
70This is a bug. Please report it at:
71
72 {}
73"
74 };
75}
76
77fn set_abort(on_panic: OnPanic) -> OnPanic {
79 ON_PANIC.with(|val| {
80 let prev = val.get();
81 match prev {
82 OnPanic::Abort | OnPanic::Unwind => val.set(on_panic),
83 OnPanic::NeverAbort => (),
84 }
85 prev
86 })
87}
88
89pub struct AbortGuard {
97 previous_val: OnPanic,
99 _not_send: PhantomData<std::rc::Rc<()>>,
101}
102
103impl AbortGuard {
104 pub fn force_unwind() -> AbortGuard {
107 AbortGuard { previous_val: set_abort(OnPanic::Unwind), _not_send: PhantomData }
108 }
109
110 pub fn force_abort() -> AbortGuard {
113 AbortGuard { previous_val: set_abort(OnPanic::Abort), _not_send: PhantomData }
114 }
115
116 pub fn never_abort() -> AbortGuard {
120 AbortGuard { previous_val: set_abort(OnPanic::NeverAbort), _not_send: PhantomData }
121 }
122}
123
124impl Drop for AbortGuard {
125 fn drop(&mut self) {
126 set_abort(self.previous_val);
127 }
128}
129
130fn strip_control_codes(input: &str) -> std::borrow::Cow<str> {
132 static RE: LazyLock<Regex> = LazyLock::new(|| {
133 Regex::new(
134 r#"(?x)
135 \x1b\[[^m]+m| # VT100 escape codes
136 [
137 \x00-\x09\x0B-\x1F # ASCII control codes / Unicode C0 control codes, except \n
138 \x7F # ASCII delete
139 \u{80}-\u{9F} # Unicode C1 control codes
140 \u{202A}-\u{202E} # Unicode left-to-right / right-to-left control characters
141 \u{2066}-\u{2069} # Same as above
142 ]
143 "#,
144 )
145 .expect("regex parsing doesn't fail; qed")
146 });
147
148 RE.replace_all(input, "")
149}
150
151fn panic_hook(info: &PanicHookInfo, report_url: &str, version: &str) {
153 let location = info.location();
154 let file = location.as_ref().map(|l| l.file()).unwrap_or("<unknown>");
155 let line = location.as_ref().map(|l| l.line()).unwrap_or(0);
156
157 let msg = match info.payload().downcast_ref::<&'static str>() {
158 Some(s) => *s,
159 None => match info.payload().downcast_ref::<String>() {
160 Some(s) => &s[..],
161 None => "Box<Any>",
162 },
163 };
164
165 let msg = strip_control_codes(msg);
166
167 let thread = thread::current();
168 let name = thread.name().unwrap_or("<unnamed>");
169
170 let backtrace = Backtrace::new();
171
172 let mut stderr = io::stderr();
173
174 let _ = writeln!(stderr);
175 let _ = writeln!(stderr, "====================");
176 let _ = writeln!(stderr);
177 let _ = writeln!(stderr, "Version: {}", version);
178 let _ = writeln!(stderr);
179 let _ = writeln!(stderr, "{:?}", backtrace);
180 let _ = writeln!(stderr);
181 let _ = writeln!(stderr, "Thread '{}' panicked at '{}', {}:{}", name, msg, file, line);
182
183 let _ = writeln!(stderr, ABOUT_PANIC!(), report_url);
184 ON_PANIC.with(|val| {
185 if val.get() == OnPanic::Abort {
186 ::std::process::exit(1);
187 }
188 })
189}
190
191#[cfg(test)]
192mod tests {
193 use super::*;
194
195 #[test]
196 fn does_not_abort() {
197 set("test", "1.2.3");
198 let _guard = AbortGuard::force_unwind();
199 ::std::panic::catch_unwind(|| panic!()).ok();
200 }
201
202 #[test]
203 fn does_not_abort_after_never_abort() {
204 set("test", "1.2.3");
205 let _guard = AbortGuard::never_abort();
206 let _guard = AbortGuard::force_abort();
207 std::panic::catch_unwind(|| panic!()).ok();
208 }
209
210 fn run_test_in_another_process(
211 test_name: &str,
212 test_body: impl FnOnce(),
213 ) -> Option<std::process::Output> {
214 if std::env::var("RUN_FORKED_TEST").is_ok() {
215 test_body();
216 None
217 } else {
218 let output = std::process::Command::new(std::env::current_exe().unwrap())
219 .arg(test_name)
220 .env("RUN_FORKED_TEST", "1")
221 .output()
222 .unwrap();
223
224 assert!(output.status.success());
225 Some(output)
226 }
227 }
228
229 #[test]
230 fn control_characters_are_always_stripped_out_from_the_panic_messages() {
231 const RAW_LINE: &str = "$$START$$\x1B[1;32mIn\u{202a}\u{202e}\u{2066}\u{2069}ner\n\r\x7ftext!\u{80}\u{9f}\x1B[0m$$END$$";
232 const SANITIZED_LINE: &str = "$$START$$Inner\ntext!$$END$$";
233
234 let output = run_test_in_another_process(
235 "control_characters_are_always_stripped_out_from_the_panic_messages",
236 || {
237 set("test", "1.2.3");
238 let _guard = AbortGuard::force_unwind();
239 let _ = std::panic::catch_unwind(|| panic!("{}", RAW_LINE));
240 },
241 );
242
243 if let Some(output) = output {
244 let stderr = String::from_utf8(output.stderr).unwrap();
245 assert!(!stderr.contains(RAW_LINE));
246 assert!(stderr.contains(SANITIZED_LINE));
247 }
248 }
249}