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::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
32pub 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 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#[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 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 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!("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 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}