1#![cfg(all(
2 feature = "crash_handler",
3 not(any(target_arch = "wasm32", target_os = "android", target_os = "ios"))
4))]
5
6use std::path::PathBuf;
9use zng_app::crash_handler::*;
10use zng_ext_config::CONFIG;
11use zng_ext_l10n::l10n;
12use zng_ext_window::{StartPosition, WINDOWS_FOCUS, WindowRoot};
13use zng_wgt::node::VarPresent as _;
14use zng_wgt::prelude::task::process::tap::PanicInfo;
15use zng_wgt::prelude::*;
16use zng_wgt::{align, corner_radius, enabled, margin};
17use zng_wgt_ansi_text::AnsiText;
18use zng_wgt_button::Button;
19use zng_wgt_container::{Container, padding};
20use zng_wgt_dialog::{DIALOG, FileDialogFilters, FileDialogResponse};
21use zng_wgt_fill::background_color;
22use zng_wgt_scroll::Scroll;
23use zng_wgt_stack::Stack;
24use zng_wgt_stack::StackDirection;
25use zng_wgt_style::Style;
26use zng_wgt_style::style_fn;
27use zng_wgt_text::Text;
28use zng_wgt_text_input::selectable::SelectableText;
29use zng_wgt_toggle::{self as toggle, Toggle};
30use zng_wgt_tooltip::{Tip, tooltip};
31use zng_wgt_window::Window;
32use zng_wgt_wrap::Wrap;
33
34pub fn debug_dialog(args: CrashArgs) -> WindowRoot {
40 let error = args.latest();
41 Window! {
42 title = l10n!(
43 "crash-handler/window.title",
44 "{$app} - App Crashed",
45 app = zng_env::about().app.clone()
46 );
47 start_position = StartPosition::CenterMonitor;
48 color_scheme = ColorScheme::Dark;
49 can_fullscreen = false;
50 can_maximize = false;
51
52 on_load = hn_once!(|_| {
53 WINDOWS_FOCUS.focus(WINDOW.id());
55 });
56 on_close = hn_once!(args, |_| {
57 args.exit(0);
58 });
59
60 padding = 5;
61 child_spacing = 5;
62 child_top = header(error);
63 child = panels(error);
64 child_bottom = commands(args);
65 }
66}
67
68fn header(error: &CrashError) -> UiNode {
69 SelectableText! {
70 txt = error.message();
71 margin = 10;
72 }
73}
74
75fn panels(error: &CrashError) -> UiNode {
76 let mut options = vec![ErrorPanel::Summary];
77 let mut active = ErrorPanel::Summary;
78
79 if !error.stdout.is_empty() {
80 options.push(ErrorPanel::Stdout);
81 active = ErrorPanel::Stdout;
82 }
83
84 if !error.stderr.is_empty() {
85 options.push(ErrorPanel::Stderr);
86 active = ErrorPanel::Stderr;
87 }
88
89 if error.has_panic() {
90 options.push(ErrorPanel::Panic);
91 active = ErrorPanel::Panic;
92 }
93 if error.has_panic_widget() {
94 options.push(ErrorPanel::Widget);
95 }
96 if error.minidump.is_some() {
97 options.push(ErrorPanel::Minidump);
98 active = ErrorPanel::Minidump;
99 }
100
101 let active = var(active);
102
103 Container! {
104 child_spacing = 5;
105 child_top = Wrap! {
106 toggle::selector = toggle::Selector::single(active.clone());
107 children = options.iter().map(|p| {
108 Toggle! {
109 child = Text!(p.title());
110 value = *p;
111 }
112 });
113 toggle::style_fn = Style! {
114 padding = (2, 4);
115 corner_radius = 2;
116 };
117 spacing = 5;
118 };
119 child = active.present(wgt_fn!(error, |p: ErrorPanel| p.panel(&error)));
120 }
121}
122
123#[derive(Debug, Clone, Copy, PartialEq, Eq)]
126enum ErrorPanel {
127 Summary,
128 Stdout,
129 Stderr,
130 Panic,
131 Widget,
132 Minidump,
133}
134impl ErrorPanel {
135 fn title(&self) -> Txt {
136 match self {
137 ErrorPanel::Summary => l10n!("crash-handler/summary.title", "Summary").get(),
138 ErrorPanel::Stdout => l10n!("crash-handler/stdout.title", "Stdout").get(),
139 ErrorPanel::Stderr => l10n!("crash-handler/stderr.title", "Stderr").get(),
140 ErrorPanel::Panic => l10n!("crash-handler/panic.title", "Panic").get(),
141 ErrorPanel::Widget => l10n!("crash-handler/widget.title", "Widget").get(),
142 ErrorPanel::Minidump => l10n!("crash-handler/minidump.title", "Minidump").get(),
143 }
144 }
145
146 fn panel(&self, error: &CrashError) -> UiNode {
147 match self {
148 ErrorPanel::Summary => summary_panel(error),
149 ErrorPanel::Stdout => std_panel(error.stdout.clone(), "stdout"),
150 ErrorPanel::Stderr => std_panel(error.stderr.clone(), "stderr"),
151 ErrorPanel::Panic => panic_panel(error.find_panic().unwrap()),
152 ErrorPanel::Widget => widget_panel(error.find_panic().unwrap().widget_path),
153 ErrorPanel::Minidump => minidump_panel(error.minidump.clone().unwrap()),
154 }
155 }
156}
157
158fn summary_panel(error: &CrashError) -> UiNode {
159 let s = l10n!(
160 "crash-handler/summary.text",
161 "Timestamp: {$timestamp}
162Exit Code: {$exit_code}
163Signal: {$signal}
164Stderr: {$stderr_len} bytes
165Stdout: {$stdout_len} bytes
166Panic: {$is_panic}
167Minidump: {$minidump_path}
168
169Args: {$args}
170OS: {$os}
171",
172 timestamp = error.unix_time(),
173 exit_code = match error.code {
174 Some(c) => format!("{c:#x}"),
175 None => String::new(),
176 },
177 signal = match error.signal {
178 Some(c) => format!("{c}"),
179 None => String::new(),
180 },
181 stderr_len = error.stderr.len(),
182 stdout_len = error.stdout.len(),
183 is_panic = error.find_panic().is_some(),
184 minidump_path = match &error.minidump {
185 Some(p) => {
186 let path = p.display().to_string();
187 let path = path.trim_start_matches(r"\\?\");
188 path.to_owned()
189 }
190 None => "none".to_owned(),
191 },
192 args = format!("{:?}", error.args),
193 os = error.os.clone(),
194 );
195 plain_panel(s.get(), "summary")
196}
197
198fn std_panel(std: Txt, config_key: &'static str) -> UiNode {
199 Scroll! {
200 child_align = Align::TOP_START;
201 background_color = colors::BLACK;
202 padding = 5;
203 horizontal_offset = CONFIG.get(formatx!("{config_key}.scroll.h"), 0.fct());
204 vertical_offset = CONFIG.get(formatx!("{config_key}.scroll.v"), 0.fct());
205 child = AnsiText! {
206 txt = std;
207 txt_selectable = true;
208 font_size = 0.9.em();
209 };
210 }
211}
212fn panic_panel(panic: PanicInfo) -> UiNode {
213 plain_panel(panic.to_txt(), "panic")
214}
215fn widget_panel(widget_path: Txt) -> UiNode {
216 plain_panel(widget_path, "widget")
217}
218fn minidump_panel(path: PathBuf) -> UiNode {
219 let path_str = path.display().to_string();
220 #[cfg(windows)]
221 let path_str = path_str.trim_start_matches(r"\\?\").replace('/', "\\");
222 let path_txt = path_str.to_txt();
223 Scroll! {
224 child_align = Align::TOP_START;
225 background_color = colors::BLACK;
226 padding = 5;
227 horizontal_offset = CONFIG.get(formatx!("minidump.scroll.h"), 0.fct());
228 vertical_offset = CONFIG.get(formatx!("minidump.scroll.v"), 0.fct());
229 child = Stack! {
230 direction = StackDirection::top_to_bottom();
231 spacing = 5;
232 children = ui_vec![
233 SelectableText! {
234 txt = path_txt;
235 font_size = 0.9.em();
236 font_family = ["JetBrains Mono", "Consolas", "monospace"];
238 },
239 Stack! {
240 direction = StackDirection::top_to_bottom();
241 zng_wgt_button::style_fn = style_fn!(|_| zng_wgt_button::LinkStyle!());
242 children = ui_vec![
243 {
244 let enabled = var(true);
245 Button! {
246 child = Text!("Open Minidump");
247 on_click = async_hn!(enabled, path, |_| {
248 open_path(enabled, path).await;
249 });
250 enabled;
251 }
252 },
253 {
254 let enabled = var(true);
255 Button! {
256 child = Text!("Open Minidump Dir");
257 on_click = async_hn!(enabled, path, |_| {
258 open_path(enabled, path.parent().unwrap().to_owned()).await;
259 });
260 }
261 },
262 {
263 let enabled = var(true);
264 Button! {
265 child = Text!("Save Minidump");
266 tooltip = Tip!(Text!("Save copy of the minidump"));
267 on_click = async_hn!(enabled, path, |_| {
268 save_copy(enabled, path).await;
269 });
270 }
271 },
272 {
273 let enabled = var(true);
274 Button! {
275 child = Text!("Delete Minidump");
276 on_click = async_hn!(enabled, path, |_| {
277 remove_path(enabled, path).await;
278 });
279 }
280 },
281 ];
282 }
283 ];
284 };
285 }
286}
287async fn open_path(enabled: Var<bool>, path: PathBuf) {
288 enabled.set(false);
289
290 #[cfg(windows)]
291 let path = path.display().to_string().replace('/', "\\");
292
293 if let Err(e) = task::wait(move || open::that_detached(path)).await {
294 DIALOG
295 .error(
296 "",
297 l10n!(
298 "crash-handler/minidump.open-error",
299 "Failed to open minidump.\n{$error}",
300 error = e.to_string()
301 ),
302 )
303 .wait_done()
304 .await;
305 }
306
307 enabled.set(true);
308}
309async fn save_copy(enabled: Var<bool>, path: PathBuf) {
310 enabled.set(false);
311
312 let mut filters = FileDialogFilters::new();
313 if let Some(ext) = path.extension() {
314 filters.push_filter(
316 l10n!("crash-handler/minidump.save-copy-filter-name", "Minidump").get().as_str(),
317 [ext.to_string_lossy().as_ref()],
318 );
319 }
320
321 let r = DIALOG
322 .save_file(
323 l10n!("crash-handler/minidump.save-copy-title", "Save Copy"),
324 path.parent().unwrap().to_owned(),
325 l10n!("crash-handler/minidump.save-copy-starting-name", "minidump"),
327 filters,
328 )
329 .wait_rsp()
330 .await;
331
332 match r {
333 FileDialogResponse::Selected(mut paths) => {
334 let destiny = paths.remove(0);
335 if let Err(e) = task::wait(move || std::fs::copy(path, destiny)).await {
336 DIALOG
337 .error(
338 "",
339 l10n!(
340 "crash-handler/minidump.save-error",
341 "Failed so save minidump copy.\n{$error}",
342 error = format!("[copy] {e}"),
343 ),
344 )
345 .wait_done()
346 .await;
347 }
348 }
349 FileDialogResponse::Cancel => {}
350 FileDialogResponse::Error(e) => {
351 DIALOG
352 .error(
353 "",
354 l10n!(
355 "crash-handler/minidump.save-error",
356 "Failed so save minidump copy.\n{$error}",
357 error = format!("[dialog] {e}"),
358 ),
359 )
360 .wait_done()
361 .await
362 }
363 _ => {}
364 }
365
366 enabled.set(true);
367}
368async fn remove_path(enabled: Var<bool>, path: PathBuf) {
369 enabled.set(false);
370
371 if let Err(e) = task::wait(move || std::fs::remove_file(path)).await
372 && e.kind() != std::io::ErrorKind::NotFound
373 {
374 DIALOG
375 .error(
376 "",
377 l10n!(
378 "crash-handler/minidump.remove-error",
379 "Failed to remove minidump.\n{$error}",
380 error = e.to_string()
381 ),
382 )
383 .wait_rsp()
384 .await
385 }
386
387 enabled.set(true);
388}
389
390fn plain_panel(txt: Txt, config_key: &'static str) -> UiNode {
391 Scroll! {
392 child_align = Align::TOP_START;
393 background_color = colors::BLACK;
394 padding = 5;
395 horizontal_offset = CONFIG.get(formatx!("{config_key}.scroll.h"), 0.fct());
396 vertical_offset = CONFIG.get(formatx!("{config_key}.scroll.v"), 0.fct());
397 child = SelectableText! {
398 txt;
399 font_size = 0.9.em();
400 font_family = ["JetBrains Mono", "Consolas", "monospace"];
402 };
403 }
404}
405
406fn commands(args: CrashArgs) -> UiNode {
407 Stack! {
408 spacing = 5;
409 direction = StackDirection::start_to_end();
410 align = Align::END;
411 children = ui_vec![
412 Button! {
413 child = Text!("Restart App");
414 on_click = hn_once!(args, |_| {
415 args.restart();
416 });
417 },
418 Button! {
419 child = Text!("Exit App");
420 on_click = hn_once!(|_| {
421 args.exit(0);
422 });
423 }
424 ];
425 }
426}