diff --git a/CHANGELOG.md b/CHANGELOG.md index 204633061..22b8e1ed6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,7 @@ - Adds `zng::app::crash_handler`. - Can be used to easily implement crash reporting, stacktrace and minidump collection, app restart on crash. + - Call `zng::app::crash_handler::init_debug()` to quickly setup panic and minidump collection. * Fix view-process kill by user not working after respawn. * Fix view-process assuming any signal kill was requested by the user. * Fix potential issue retrieving current_exe trough symbolic links. diff --git a/crates/zng-app/src/crash_handler.rs b/crates/zng-app/src/crash_handler.rs index 254af61af..19a860c74 100644 --- a/crates/zng-app/src/crash_handler.rs +++ b/crates/zng-app/src/crash_handler.rs @@ -9,7 +9,7 @@ use std::{ fmt, io::{BufRead, BufReader}, path::{Path, PathBuf}, - time::{Duration, SystemTime}, + time::SystemTime, }; use zng_layout::unit::TimeUnits as _; @@ -184,18 +184,12 @@ pub struct CrashError { /// Minidump file. pub minidump: Option, } +/// Alternate mode `{:#}` prints plain stdout and stderr (no ANSI escape sequences). impl fmt::Display for CrashError { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - writeln!( - f, - "timestamp: {}", - self.timestamp - .duration_since(SystemTime::UNIX_EPOCH) - .unwrap_or(Duration::ZERO) - .as_secs() - )?; + writeln!(f, "timestamp: {}", self.unix_time())?; if let Some(c) = self.code { - writeln!(f, "exit code: {c:#x}")? + writeln!(f, "exit code: {c:#X}")? } if let Some(c) = self.signal { writeln!(f, "exit signal: {c}")? @@ -203,7 +197,11 @@ impl fmt::Display for CrashError { if let Some(p) = self.minidump.as_ref() { writeln!(f, "minidump: {}", p.display())? } - write!(f, "\nSTDOUT:\n{}\nSTDERR:\n{}\n", self.stdout, self.stderr) + if f.alternate() { + write!(f, "\nSTDOUT:\n{}\nSTDERR:\n{}\n", self.stdout_plain(), self.stderr_plain()) + } else { + write!(f, "\nSTDOUT:\n{}\nSTDERR:\n{}\n", self.stdout, self.stderr) + } } } impl CrashError { @@ -233,13 +231,56 @@ impl CrashError { } } + /// Seconds since Unix epoch. + pub fn unix_time(&self) -> u64 { + self.timestamp.duration_since(SystemTime::UNIX_EPOCH).unwrap_or_default().as_secs() + } + + /// Gets if `stdout` does not contain any ANSI scape sequences. + pub fn is_stdout_plain(&self) -> bool { + !self.stdout.contains(CSI) + } + + /// Gets if `stderr` does not contain any ANSI scape sequences. + pub fn is_stderr_plain(&self) -> bool { + !self.stderr.contains(CSI) + } + + /// Get `stdout` without any ANSI escape sequences (CSI). + pub fn stdout_plain(&self) -> Txt { + remove_ansi_csi(&self.stdout) + } + + /// Get `stderr` without any ANSI escape sequences (CSI). + pub fn stderr_plain(&self) -> Txt { + remove_ansi_csi(&self.stderr) + } + + /// Gets if `stderr` contains a crash panic. + pub fn has_panic(&self) -> bool { + if self.code == Some(101) { + CrashPanic::contains(&self.stderr_plain()) + } else { + false + } + } + + /// Gets if `stderr` contains a crash panic that traced widget/window path. + pub fn has_panic_widget(&self) -> bool { + if self.code == Some(101) { + CrashPanic::contains_widget(&self.stderr_plain()) + } else { + false + } + } + /// Try parse `stderr` for the crash panic. /// /// Only reliably works if the panic fully printed correctly and was formatted by the panic /// hook installed by `crash_handler` or by the display print of [`CrashPanic`]. pub fn find_panic(&self) -> Option { if self.code == Some(101) { - CrashPanic::find(&self.stderr) + CrashPanic::find(&self.stderr_plain()) } else { None } @@ -254,15 +295,17 @@ impl CrashError { } else if let Some(msg) = self.minidump_message() { msg } else { - "Error.".into() + "".into() }; use std::fmt::Write as _; if let Some(c) = self.code { - write!(&mut msg, " Code: {c:#x}.").unwrap(); + let sep = if msg.is_empty() { "" } else { "\n" }; + write!(&mut msg, "{sep}Code: {c:#X}").unwrap(); } if let Some(c) = self.signal { - write!(&mut msg, " Signal: {c}.").unwrap(); + let sep = if msg.is_empty() { "" } else { "\n" }; + write!(&mut msg, "{sep}Signal: {c}").unwrap(); } msg.end_mut(); msg @@ -300,6 +343,29 @@ impl CrashError { } } +const CSI: &str = "\x1b["; + +/// Remove ANSI escape sequences (CSI) from `s`. +pub fn remove_ansi_csi(mut s: &str) -> Txt { + fn is_esc_end(byte: u8) -> bool { + (0x40..=0x7e).contains(&byte) + } + + let mut r = String::new(); + while let Some(i) = s.find(CSI) { + r.push_str(&s[..i]); + s = &s[i + CSI.len()..]; + let mut esc_end = 0; + while esc_end < s.len() && !is_esc_end(s.as_bytes()[esc_end]) { + esc_end += 1; + } + esc_end += 1; + s = &s[esc_end..]; + } + r.push_str(s); + r.into() +} + /// Panic parsed from a `stderr` dump. #[derive(Debug, Clone, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)] pub struct CrashPanic { @@ -332,11 +398,32 @@ impl fmt::Display for CrashPanic { } } impl CrashPanic { + /// Gets if `stderr` contains a panic that can be parsed by [`find`]. + /// + /// [`find`]: Self::find + pub fn contains(stderr: &str) -> bool { + Self::find_impl(stderr, false).is_some() + } + + /// Gets if `stderr` contains a panic that can be parsed by [`find`] and traced a widget/window path. + /// + /// [`find`]: Self::find + pub fn contains_widget(stderr: &str) -> bool { + match Self::find_impl(stderr, false) { + Some(p) => !p.widget_path.is_empty(), + None => false, + } + } + /// Try parse `stderr` for the crash panic. /// /// Only reliably works if the panic fully printed correctly and was formatted by the panic /// hook installed by `crash_handler` or by the display print of this type. pub fn find(stderr: &str) -> Option { + Self::find_impl(stderr, true) + } + + fn find_impl(stderr: &str, parse: bool) -> Option { let mut panic_at = usize::MAX; let mut widget_path = usize::MAX; let mut stack_backtrace = usize::MAX; @@ -358,6 +445,22 @@ impl CrashPanic { return None; } + if !parse { + return Some(Self { + thread: Txt::from(""), + message: Txt::from(""), + file: Txt::from(""), + line: 0, + column: 0, + widget_path: if widget_path < stderr.len() { + Txt::from("true") + } else { + Txt::from("") + }, + backtrace: Txt::from(""), + }); + } + let panic_str = stderr[panic_at..].lines().next().unwrap(); let (thread, location) = panic_str.strip_prefix("thread '").unwrap().split_once("' panicked at ").unwrap(); let mut location = location.split(':'); @@ -382,7 +485,7 @@ impl CrashPanic { } let widget_path = if widget_path < stderr.len() { - stderr[widget_path..].lines().nth(1).unwrap().trim() + stderr[widget_path..].lines().next().unwrap().trim() } else { "" }; @@ -482,7 +585,7 @@ fn crash_handler_monitor_process(mut cfg_app: ConfigProcess, mut cfg_dialog: Con } tracing::error!( - "app-process crashed with exit code ({:#x}), signal ({:#?}), {} crashes previously", + "app-process crashed with exit code ({:#X}), signal ({:#?}), {} crashes previously", code.unwrap_or(0), signal.unwrap_or(0), dialog_args.app_crashes.len() diff --git a/crates/zng-view-api/src/app_process.rs b/crates/zng-view-api/src/app_process.rs index 5e926f320..ea8751ab5 100644 --- a/crates/zng-view-api/src/app_process.rs +++ b/crates/zng-view-api/src/app_process.rs @@ -411,7 +411,7 @@ impl Controller { if !killed_by_us { let code = code.unwrap_or(0); let signal = signal.unwrap_or(0); - tracing::error!(target: "vp_respawn", "view-process exit code: {code:#x}, signal: {signal}"); + tracing::error!(target: "vp_respawn", "view-process exit code: {code:#X}, signal: {signal}"); } let stderr = match String::from_utf8(c.stderr) { diff --git a/crates/zng/src/app.rs b/crates/zng/src/app.rs index 8c9e47aaa..9b6531185 100644 --- a/crates/zng/src/app.rs +++ b/crates/zng/src/app.rs @@ -537,4 +537,256 @@ pub use zng_ext_single_instance::{is_single_instance, single_instance, single_in pub mod crash_handler { #[cfg(feature = "crash_handler")] pub use zng_app::crash_handler::{init, restart_count, CrashArgs, CrashConfig, CrashError, CrashPanic}; + + /// Init a crash-handler with dialog that shows detailed debug info. + pub fn init_debug() { + init(debug_config()) + } + + /// Config used by [`init_debug`] + pub fn debug_config() -> CrashConfig { + CrashConfig::new(super::crash_handler_dbg::dialog) + } +} + +mod crash_handler_dbg { + use std::path::PathBuf; + + use crate::{ + ansi_text::AnsiText, + app::crash_handler::*, + color::ColorScheme, + prelude::*, + widget::node::{presenter, BoxedUiNode}, + }; + + pub fn dialog(args: CrashArgs) -> ! { + if let Some(c) = &args.dialog_crash { + eprintln!("DEBUG CRASH DIALOG CRASHED"); + eprintln!(" {}", c.message()); + eprintln!("APP CRASH"); + eprintln!(" {}", args.latest().message()); + args.exit(0xBADC0DE) + } + + // Just in case no view-process was inited before the crash_handler setup. + // Multiple view-process inits are ok because they either do nothing or never return. + #[cfg(feature = "view_prebuilt")] + crate::view_process::prebuilt::init(); + #[cfg(feature = "view")] + crate::view_process::default::init(); + + APP.defaults().run_window(async_clmv!(args, { + let error = args.latest(); + Window! { + title = APP.about().map(|a| formatx!("{}App Crashed", a.title_prefix())); + start_position = zng::window::StartPosition::CenterMonitor; + color_scheme = ColorScheme::Dark; + + on_load = hn_once!(|_| { + // force to foreground + let _ = WINDOWS.focus(WINDOW.id()); + }); + on_close = hn_once!(args, |_| { + args.exit(0); + }); + + padding = 5; + child_top = header(error), 5; + child = panels(error); + child_bottom = commands(args), 5; + } + })); + + args.exit(0); + } + + fn header(error: &CrashError) -> impl UiNode { + SelectableText! { + txt = error.message(); + layout::margin = 10; + } + } + + fn panels(error: &CrashError) -> impl UiNode { + let mut options = vec![ErrorPanel::Summary]; + let mut active = ErrorPanel::Summary; + + if !error.stdout.is_empty() { + options.push(ErrorPanel::StdoutPlain); + active = ErrorPanel::StdoutPlain; + if !error.is_stdout_plain() { + options.push(ErrorPanel::Stdout); + active = ErrorPanel::Stdout; + } + } + + if !error.stderr.is_empty() { + options.push(ErrorPanel::StderrPlain); + active = ErrorPanel::StderrPlain; + if !error.is_stderr_plain() { + options.push(ErrorPanel::Stderr); + active = ErrorPanel::Stderr; + } + } + + if error.has_panic() { + options.push(ErrorPanel::Panic); + active = ErrorPanel::Panic; + } + if error.has_panic_widget() { + options.push(ErrorPanel::Widget); + } + if error.minidump.is_some() { + options.push(ErrorPanel::Minidump); + active = ErrorPanel::Minidump; + } + + let active = var(active); + + Container! { + child_top = Wrap! { + toggle::selector = toggle::Selector::single(active.clone()); + children = options.iter().map(|p| Toggle! { + child = Text!(p.title()); + value = *p; + }).collect::(); + toggle::style_fn = Style! { + layout::padding = (2, 4); + widget::corner_radius = 2; + }; + spacing = 5; + }, 5; + child = presenter(active, wgt_fn!(error, |p: ErrorPanel| p.panel(&error))); + } + } + + #[derive(Debug, Clone, Copy, PartialEq, Eq)] + enum ErrorPanel { + Summary, + Stdout, + Stderr, + StdoutPlain, + StderrPlain, + Panic, + Widget, + Minidump, + } + impl ErrorPanel { + fn title(&self) -> Txt { + match self { + ErrorPanel::Summary => "Summary", + ErrorPanel::Stdout => "Stdout", + ErrorPanel::Stderr => "Stderr", + ErrorPanel::StdoutPlain => "Stdout (plain)", + ErrorPanel::StderrPlain => "Stderr (plain)", + ErrorPanel::Panic => "Panic", + ErrorPanel::Widget => "Widget", + ErrorPanel::Minidump => "Minidump", + } + .into() + } + + fn panel(&self, error: &CrashError) -> BoxedUiNode { + match self { + ErrorPanel::Summary => summary_panel(error).boxed(), + ErrorPanel::Stdout => std_panel(error.stdout.clone()).boxed(), + ErrorPanel::Stderr => std_panel(error.stderr.clone()).boxed(), + ErrorPanel::StdoutPlain => std_plain_panel(error.stdout_plain()).boxed(), + ErrorPanel::StderrPlain => std_plain_panel(error.stderr_plain()).boxed(), + ErrorPanel::Panic => panic_panel(error.find_panic().unwrap()).boxed(), + ErrorPanel::Widget => widget_panel(error.find_panic().unwrap().widget_path).boxed(), + ErrorPanel::Minidump => minidump_panel(error.minidump.clone().unwrap()).boxed(), + } + } + } + + fn summary_panel(error: &CrashError) -> impl UiNode { + let s = formatx!( + "Timestamp: {}\nExit Code: {}\nSignal: {}\nStderr: {} bytes\nStdout: {} bytes\nPanic: {}\nMinidump: {}\n\nArgs: {:?}\n", + error.unix_time(), + match error.code { + Some(c) => format!("{c:#x}"), + None => "".to_owned(), + }, + match error.signal { + Some(c) => format!("{c}"), + None => "".to_owned(), + }, + error.stderr.len(), + error.stdout.len(), + error.find_panic().is_some(), + match &error.minidump { + Some(p) => { + let path = p.display().to_string(); + let path = path.trim_start_matches(r"\\?\"); + path.to_owned() + } + None => "".to_owned(), + }, + error.args, + ); + plain_panel(s) + } + + fn std_panel(std: Txt) -> impl UiNode { + Scroll! { + child_align = Align::TOP_START; + widget::background_color = colors::BLACK; + layout::padding = 5; + child = AnsiText! { + txt = std; + font_size = 0.9.em(); + } + } + } + fn std_plain_panel(std: Txt) -> impl UiNode { + plain_panel(std) + } + fn panic_panel(panic: CrashPanic) -> impl UiNode { + plain_panel(panic.to_txt()) + } + fn widget_panel(widget_path: Txt) -> impl UiNode { + plain_panel(widget_path) + } + fn minidump_panel(path: PathBuf) -> impl UiNode { + let path = path.display().to_string(); + let path = path.trim_start_matches(r"\\?\"); + plain_panel(path.to_txt()) + } + fn plain_panel(txt: Txt) -> impl UiNode { + Scroll! { + child_align = Align::TOP_START; + widget::background_color = colors::BLACK; + layout::padding = 5; + child = SelectableText! { + txt; + font_size = 0.9.em(); + // same as AnsiText + font_family = ["JetBrains Mono", "Consolas", "monospace"]; + } + } + } + + fn commands(args: CrashArgs) -> impl UiNode { + Stack! { + spacing = 5; + direction = StackDirection::start_to_end(); + layout::align = Align::END; + children = ui_vec![ + Button! { + child = Text!("Restart App"); + on_click = hn_once!(args, |_| { + args.restart(); + }); + }, + Button! { + child = Text!("Exit App"); + on_click = hn_once!(|_| { + args.exit(0); + }); + } + ]; + } + } } diff --git a/examples/README.md b/examples/README.md index 028d724d7..2925d1c3f 100644 --- a/examples/README.md +++ b/examples/README.md @@ -198,7 +198,7 @@ Source: [respawn.rs](./respawn.rs) cargo do run respawn ``` -Demonstrates the view-process respawn error recovery feature. +Demonstrates app-process crash handler and view-process respawn. ### `scroll` diff --git a/examples/animation.rs b/examples/animation.rs index f8108112b..b5f0a6a20 100644 --- a/examples/animation.rs +++ b/examples/animation.rs @@ -27,6 +27,7 @@ use zng::view_process::prebuilt as view_process; fn main() { examples_util::print_info(); view_process::init(); + zng::app::crash_handler::init_debug(); // let rec = examples_util::record_profile("animation"); diff --git a/examples/border.rs b/examples/border.rs index 9fba4849a..b5f0a0303 100644 --- a/examples/border.rs +++ b/examples/border.rs @@ -11,6 +11,7 @@ use zng::view_process::prebuilt as view_process; fn main() { examples_util::print_info(); view_process::init(); + zng::app::crash_handler::init_debug(); //let rec = examples_util::record_profile("border"); diff --git a/examples/button.rs b/examples/button.rs index df1ca3fac..d90c8bb94 100644 --- a/examples/button.rs +++ b/examples/button.rs @@ -17,6 +17,7 @@ use zng::view_process::prebuilt as view_process; fn main() { examples_util::print_info(); view_process::init(); + zng::app::crash_handler::init_debug(); // let rec = examples_util::record_profile("button"); diff --git a/examples/calculator.rs b/examples/calculator.rs index 929d6eb42..d09f9fdf8 100644 --- a/examples/calculator.rs +++ b/examples/calculator.rs @@ -12,6 +12,7 @@ use zng::view_process::prebuilt as view_process; fn main() { examples_util::print_info(); view_process::init(); + zng::app::crash_handler::init_debug(); //let rec = examples_util::record_profile("calculator"); diff --git a/examples/config.rs b/examples/config.rs index 4c0669950..b4c814fb0 100644 --- a/examples/config.rs +++ b/examples/config.rs @@ -17,6 +17,7 @@ use zng::view_process::prebuilt as view_process; fn main() { examples_util::print_info(); view_process::init(); + zng::app::crash_handler::init_debug(); // let rec = examples_util::record_profile("button"); diff --git a/examples/countdown.rs b/examples/countdown.rs index 4543c9083..02b3c6c21 100644 --- a/examples/countdown.rs +++ b/examples/countdown.rs @@ -8,6 +8,7 @@ use zng::view_process::prebuilt as view_process; fn main() { examples_util::print_info(); view_process::init(); + zng::app::crash_handler::init_debug(); // view_process::run_same_process(app_main); app_main(); diff --git a/examples/cursor.rs b/examples/cursor.rs index a1cdb4a3d..60131e6ab 100644 --- a/examples/cursor.rs +++ b/examples/cursor.rs @@ -15,6 +15,7 @@ use zng::view_process::prebuilt as view_process; fn main() { examples_util::print_info(); view_process::init(); + zng::app::crash_handler::init_debug(); // view_process::run_same_process(app_main); app_main(); diff --git a/examples/extend_view.rs b/examples/extend_view.rs index 218b28e04..7d9577a9b 100644 --- a/examples/extend_view.rs +++ b/examples/extend_view.rs @@ -11,6 +11,7 @@ use zng_view::extensions::ViewExtensions; fn main() { examples_util::print_info(); + zng::app::crash_handler::init_debug(); // zng_view::init_extended(view_extensions); // app_main(); diff --git a/examples/focus.rs b/examples/focus.rs index 4a981ddc2..c02e6b844 100644 --- a/examples/focus.rs +++ b/examples/focus.rs @@ -25,6 +25,7 @@ use zng::view_process::prebuilt as view_process; fn main() { examples_util::print_info(); view_process::init(); + zng::app::crash_handler::init_debug(); // let rec = examples_util::record_profile("focus"); diff --git a/examples/gradient.rs b/examples/gradient.rs index 34bf34fec..b5ddc21c6 100644 --- a/examples/gradient.rs +++ b/examples/gradient.rs @@ -18,6 +18,7 @@ use zng::view_process::prebuilt as view_process; fn main() { examples_util::print_info(); view_process::init(); + zng::app::crash_handler::init_debug(); // view_process::run_same_process(app_main); app_main(); diff --git a/examples/icon.rs b/examples/icon.rs index d9cb37600..5db1b795a 100644 --- a/examples/icon.rs +++ b/examples/icon.rs @@ -27,6 +27,7 @@ use zng::view_process::prebuilt as view_process; fn main() { examples_util::print_info(); view_process::init(); + zng::app::crash_handler::init_debug(); // let rec = examples_util::record_profile("icon"); diff --git a/examples/image.rs b/examples/image.rs index 28429aa04..7436b3fa3 100644 --- a/examples/image.rs +++ b/examples/image.rs @@ -27,6 +27,7 @@ use zng_wgt_webrender_debug as wr; fn main() { examples_util::print_info(); // view_process::init(); + zng::app::crash_handler::init_debug(); // let rec = examples_util::record_profile("image"); diff --git a/examples/layer.rs b/examples/layer.rs index 87cd2359d..de9d62ee8 100644 --- a/examples/layer.rs +++ b/examples/layer.rs @@ -17,6 +17,7 @@ use zng::view_process::prebuilt as view_process; fn main() { examples_util::print_info(); view_process::init(); + zng::app::crash_handler::init_debug(); // let rec = examples_util::record_profile("layer"); diff --git a/examples/localize.rs b/examples/localize.rs index 9cfa4e2bf..0f646dc3a 100644 --- a/examples/localize.rs +++ b/examples/localize.rs @@ -26,6 +26,7 @@ use zng::view_process::prebuilt as view_process; fn main() { examples_util::print_info(); view_process::init(); + zng::app::crash_handler::init_debug(); // let rec = examples_util::record_profile("localize"); diff --git a/examples/markdown.rs b/examples/markdown.rs index 797c8f086..7961f43d0 100644 --- a/examples/markdown.rs +++ b/examples/markdown.rs @@ -15,6 +15,7 @@ use zng::view_process::prebuilt as view_process; fn main() { examples_util::print_info(); view_process::init(); + zng::app::crash_handler::init_debug(); // let rec = examples_util::record_profile("markdown"); diff --git a/examples/res/screenshots/respawn.png b/examples/res/screenshots/respawn.png index e109a0974..be93f3ed2 100644 Binary files a/examples/res/screenshots/respawn.png and b/examples/res/screenshots/respawn.png differ diff --git a/examples/respawn.rs b/examples/respawn.rs index ece4d3c8e..fb874876b 100644 --- a/examples/respawn.rs +++ b/examples/respawn.rs @@ -15,7 +15,8 @@ fn main() { examples_util::print_info(); // init crash-handler before view to use different view for the crash dialog app. - zng::app::crash_handler::init(zng::app::crash_handler::CrashConfig::new(app_crash_dialog)); + zng::app::crash_handler::init_debug(); + // zng::app::crash_handler::init(zng::app::crash_handler::CrashConfig::new(app_crash_dialog)); // this is the normal app-process: @@ -32,29 +33,49 @@ fn main() { child = Stack! { direction = StackDirection::top_to_bottom(); layout::margin = 10; - spacing = 5; + spacing = 10; children_align = Align::TOP; children = ui_vec![ Markdown! { + layout::margin = (20, 0, 0, 0); txt = "The renderer and OS windows are created in separate process, the `view-process`. \ It automatically respawns in case of a graphics driver crash or other similar fatal error."; }, - view_respawn(), - view_crash(), + Wrap! { + spacing = 5; + children = ui_vec![ + view_respawn(), + view_crash(), + ], + }, Markdown! { + layout::margin = (20, 0, 0, 0); txt = "When the app is instantiated the crash handler takes over the process, becoming the `monitor-process`. \ It spawns the `app-process` that is the normal execution. If the `app-process` crashes it spawns the \ `dialog-process` that runs a different app that shows an error message."; }, - app_crash(true), - app_crash(false), + Wrap! { + spacing = 5; + children = ui_vec![ + app_crash("panic"), + app_crash("access violation"), + app_crash("stack overflow"), + app_crash("custom exit"), + ], + }, Markdown! { + layout::margin = (20, 0, 0, 0); txt = "The states of these buttons is only preserved for `view-process` crashes. \ use `CONFIG` or some other state saving to better recover from `app-process` crashes."; }, - click_counter(), - click_counter(), - image(), + Wrap! { + spacing = 5; + children = ui_vec![ + click_counter(), + image(), + click_counter(), + ], + }, ]; }; } @@ -62,33 +83,28 @@ fn main() { } // Crash dialog app, runs in the dialog-process. +#[allow(unused)] fn app_crash_dialog(args: zng::app::crash_handler::CrashArgs) -> ! { zng::view_process::prebuilt::init(); APP.defaults().run_window(async move { Window! { title = "Respawn Example - App Crashed"; icon = WindowIcon::render(icon); + start_position = zng::window::StartPosition::CenterMonitor; + auto_size = window::AutoSize::CONTENT; + min_size = (300, 100); on_load = hn_once!(|_| { // force to foreground let _ = WINDOWS.focus(WINDOW.id()); }); - on_close = hn_once!(args, |_| { - args.exit(0); - }); padding = 5; - child_top = Markdown!( - "The Respawn Example app has crashed.\n\n{}\n\n**Details:**\n", + child = Markdown!( + "The Respawn Example app has crashed.\n\n{}\n\n", args.latest().message(), - ), 5; - child = Scroll! { - padding = 5; - child_align = Align::TOP_START; - child = zng::ansi_text::AnsiText!(args.latest().to_txt()); - widget::background_color = colors::BLACK; - }; + ); child_bottom = Stack! { spacing = 5; direction = StackDirection::start_to_end(); @@ -117,7 +133,7 @@ fn app_crash_dialog(args: zng::app::crash_handler::CrashArgs) -> ! { }, 5; } }); - panic!("dialog app did not respond correctly") + std::process::exit(0) } fn view_respawn() -> impl UiNode { @@ -143,18 +159,32 @@ fn view_crash() -> impl UiNode { } } -fn app_crash(panic: bool) -> impl UiNode { +fn app_crash(crash_name: &'static str) -> impl UiNode { Button! { - child = Text!("Crash App-Process ({})", if panic { "panic" } else { "access violation" }); + child = Text!("Crash ({crash_name})"); on_click = hn!(|_| { - if panic { - panic!("Test app-process crash!"); - } else { - // SAFETY: deliberate access violation - #[allow(deref_nullptr)] - unsafe { - *std::ptr::null_mut() = true; + match crash_name { + "panic" => panic!("Test app-process crash!"), + "access violation" => { + // SAFETY: deliberate access violation + #[allow(deref_nullptr)] + unsafe { + *std::ptr::null_mut() = true; + } } + "stack overflow" => { + fn overflow(c: u64) { + if c < u64::MAX { + overflow(c + 1) + } + } + overflow(0) + } + "custom exit" => { + eprintln!("custom error"); + std::process::exit(0xBAD); + } + n => panic!("Unknown crash '{n}'"), } }); } @@ -175,13 +205,9 @@ fn click_counter() -> impl UiNode { } fn image() -> impl UiNode { - Stack! { - direction = StackDirection::top_to_bottom(); - spacing = 3; - children = ui_vec![ - text::Strong!("Image:"), - Image! { source = include_bytes!("res/window/icon-bytes.png"); size = (32, 32); }, - ]; + Image! { + source = include_bytes!("res/window/icon-bytes.png"); size = (32, 32); + tooltip = Tip!(Text!("Image reloads after respawn")); } } diff --git a/examples/scroll.rs b/examples/scroll.rs index 3263325ce..dc32ece97 100644 --- a/examples/scroll.rs +++ b/examples/scroll.rs @@ -15,6 +15,7 @@ use rand::SeedableRng; fn main() { examples_util::print_info(); view_process::init(); + zng::app::crash_handler::init_debug(); // let rec = examples_util::record_profile("scroll"); // view_process::run_same_process(app_main); diff --git a/examples/shortcut.rs b/examples/shortcut.rs index 3f2e80ba0..0b9e963c4 100644 --- a/examples/shortcut.rs +++ b/examples/shortcut.rs @@ -9,6 +9,7 @@ use zng::view_process::prebuilt as view_process; fn main() { examples_util::print_info(); view_process::init(); + zng::app::crash_handler::init_debug(); // let rec = examples_util::record_profile("shortcuts"); diff --git a/examples/text.rs b/examples/text.rs index d8d2e3eaf..a4a861bab 100644 --- a/examples/text.rs +++ b/examples/text.rs @@ -32,6 +32,7 @@ use zng::view_process::prebuilt as view_process; fn main() { examples_util::print_info(); view_process::init(); + zng::app::crash_handler::init_debug(); //let rec = examples_util::record_profile("text"); diff --git a/examples/transform.rs b/examples/transform.rs index d0b2dde42..bcf5288bb 100644 --- a/examples/transform.rs +++ b/examples/transform.rs @@ -21,6 +21,7 @@ use zng::view_process::prebuilt as view_process; fn main() { examples_util::print_info(); view_process::init(); + zng::app::crash_handler::init_debug(); // let rec = examples_util::record_profile("transform"); diff --git a/examples/window.rs b/examples/window.rs index 93b3fb2f4..5fdfe14cb 100644 --- a/examples/window.rs +++ b/examples/window.rs @@ -29,6 +29,7 @@ use zng::view_process::default as view_process; fn main() { examples_util::print_info(); // view_process::init(); + zng::app::crash_handler::init_debug(); // let rec = examples_util::record_profile("window");