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