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