Skip to main content

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_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
34// l10n-## Debug Crash Handler
35
36/// Debug dialog window.
37///
38/// Used by `zng::app::init_debug`.
39pub 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            // force to foreground
54            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// l10n-## Panels
124
125#[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                    // same as AnsiText
237                    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        // l10n-# name for the minidump file type in the save file dialog
315        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-# default file name
326            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            // same as AnsiText
401            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}