zng_wgt_inspector/
crash_handler.rs

1#![cfg(all(
2    feature = "crash_handler",
3    not(any(target_arch = "wasm32", target_os = "android", target_os = "ios"))
4))]
5
6//! Debug crash handler.
7
8use 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
33// l10n-## Debug Crash Handler
34
35/// Debug dialog window.
36///
37/// Used by `zng::app::init_debug`.
38pub 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            // force to foreground
51            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// l10n-## Panels
121
122#[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                    // same as AnsiText
234                    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        // l10n-# name for the minidump file type in the save file dialog
312        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-# default file name
323            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            // same as AnsiText
398            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}