Repository: zng-ui/zng (language: rust)

Issue:
Refactor crash handler to use new `zng::env::init!`
Crash handler still needs manual init because of dialog selection.

Also review the usage of process:exit. This causes problems with tracing logs not flushing.


Discussion / hints:

Fix patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 19cf52a8f..913fd77a3 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -7,15 +7,19 @@
 * Implement `From<Txt>` for `std::ffi::OsString`.
 * **Breaking** Remove `zng_view::init`, `zng_view_prebuilt::init`, `zng::view_process::default::init`, `zng::view_process::prebuilt::init`.
     - Only `zng::env::init!()` needs to be called to setup the view-process.
+* **Breaking** Remove `zng_view::extensions::ViewExtensions::new`.
+    - Now use `zng_view::view_process_extension!` to declare.
 * **Breaking** `zng_view_api::Controller::start` now requires the exe path and supports optional env variables to set.
     - This only affects custom view-process implementers.
 * **Breaking** New default `zng::env::res`, was `./assets` now is `./res`.
 * **Breaking** Renamed `zng_view_prebuilt::ViewLib::init` to `view_process_main`.
 * **Breaking** Remove `is_single_instance`, `single_instance`, `single_instance_named`.
     - Single instance now enabled automatically just by setting `feature="single_instance"`.
-* **Braking** Add name requirement for `zng::task::ipc` workers.
+* **Breaking** Add name requirement for `zng::task::ipc` workers.
     - Workers are no longer limited to a single entry with an enum switch.
     - Use `zng::env::on_process_start!` to declare the worker entry anywhere.
+* **Breaking** Remove `zng::app::crash_handler::init` and `CrashConfig::new`.
+    - Use `zng::app::crash_handler::crash_handler_config` to config.
 
 # 0.6.2
 
diff --git a/crates/zng-app/Cargo.toml b/crates/zng-app/Cargo.toml
index fdb8c3687..580667844 100644
--- a/crates/zng-app/Cargo.toml
+++ b/crates/zng-app/Cargo.toml
@@ -56,9 +56,7 @@ trace_widget = []
 trace_wgt_item = []
 
 # Allow app-process crash handler.
-#
-# Note that `crash_handler::init(_)` must be called to enable.
-crash_handler = ["dep:serde_json", "dep:breakpad-handler", "dep:minidump"]
+crash_handler = ["dep:serde_json", "dep:breakpad-handler", "dep:minidump", "dep:linkme"]
 
 # Enables IPC tasks and pre-build views and connecting to views running in another process.
 ipc = ["zng-view-api/ipc", "zng-task/ipc"]
@@ -105,6 +103,7 @@ unicase = "2.7"
 serde_json = { version = "1.0", optional = true }
 breakpad-handler = { version = "0.2", optional = true }
 minidump = { version = "0.21", optional = true }
+linkme = { version = "=0.3.26", optional = true }
 
 dunce = "1.0"
 
diff --git a/crates/zng-app/README.md b/crates/zng-app/README.md
index f23be800a..bf60839e0 100644
--- a/crates/zng-app/README.md
+++ b/crates/zng-app/README.md
@@ -52,8 +52,6 @@ Note that this can cause very large trace files and bad performance.
 #### `"crash_handler"`
 Allow app-process crash handler.
 
-Note that `crash_handler::init(_)` must be called to enable.
-
 *Enabled by default.*
 
 #### `"ipc"`
diff --git a/crates/zng-app/src/crash_handler.rs b/crates/zng-app/src/crash_handler.rs
index fc4e62f77..672ad8bb7 100644
--- a/crates/zng-app/src/crash_handler.rs
+++ b/crates/zng-app/src/crash_handler.rs
@@ -15,26 +15,42 @@ use zng_layout::unit::TimeUnits as _;
 
 use zng_txt::{ToTxt as _, Txt};
 
-/// Starts the current app-process in a monitored instance.
-///
-/// This function takes over the first process turning it into the monitor-process, it spawns another process that
-/// is the monitored app-process. If the app-process crashes it spawns a dialog-process that calls the dialog handler
-/// to show an error message, upload crash reports, etc.
-pub fn init(config: CrashConfig) {
+zng_env::on_process_start!(|process_start_args| {
+    let mut config = CrashConfig::new();
+    for ext in CRASH_CONFIG {
+        ext(&mut config);
+    }
+
     if std::env::var(APP_PROCESS) != Err(std::env::VarError::NotPresent) {
         return crash_handler_app_process(config.dump_dir.as_deref());
     }
 
     match std::env::var(DIALOG_PROCESS) {
-        Ok(args_file) => crash_handler_dialog_process(config.dump_dir.as_deref(), config.dialog, args_file),
+        Ok(args_file) => crash_handler_dialog_process(
+            config.dump_dir.as_deref(),
+            config
+                .dialog
+                .or(config.default_dialog)
+                .expect("dialog-process spawned without dialog handler"),
+            args_file,
+        ),
         Err(e) => match e {
             std::env::VarError::NotPresent => {}
             e => panic!("invalid dialog env args, {e:?}"),
         },
     }
 
-    crash_handler_monitor_process(config.cfg_app, config.cfg_dialog);
-}
+    if process_start_args.next_handlers_count > 0 && process_start_args.yield_count < zng_env::ProcessStartArgs::MAX_YIELD_COUNT - 10 {
+        // extra sure that this is the app-process
+        return process_start_args.yield_once();
+    }
+
+    crash_handler_monitor_process(
+        config.app_process,
+        config.dialog_process,
+        config.default_dialog.is_some() || config.dialog.is_some(),
+    );
+});
 
 /// Gets the number of crash restarts in the app-process.
 ///
@@ -50,61 +66,119 @@ const APP_PROCESS: &str = "ZNG_CRASH_HANDLER_APP";
 const DIALOG_PROCESS: &str = "ZNG_CRASH_HANDLER_DIALOG";
 const RESPONSE_PREFIX: &str = "zng_crash_response: ";
 
+#[linkme::distributed_slice]
+static CRASH_CONFIG: [fn(&mut CrashConfig)];
+
+/// <span data-del-macro-root></span> Register a `FnOnce(&mut CrashConfig)` closure to be
+/// called on process init to configure the crash handler.
+///
+/// See [`CrashConfig`] for more details.
+#[macro_export]
+macro_rules! crash_handler_config {
+    ($closure:expr) => {
+        #[used]
+        #[cfg_attr(
+            any(
+                target_os = "none",
+                target_os = "linux",
+                target_os = "android",
+                target_os = "fuchsia",
+                target_os = "psp"
+            ),
+            link_section = "linkme_CRASH_CONFIG"
+        )]
+        #[cfg_attr(
+            any(target_os = "macos", target_os = "ios", target_os = "tvos"),
+            link_section = "__DATA,__linkmeK3uV0Fq0,regular,no_dead_strip"
+        )]
+        #[cfg_attr(target_os = "windows", link_section = ".linkme_CRASH_CONFIG$b")]
+        #[cfg_attr(target_os = "illumos", link_section = "set_linkme_CRASH_CONFIG")]
+        #[cfg_attr(target_os = "freebsd", link_section = "linkme_CRASH_CONFIG")]
+        #[doc(hidden)]
+        static _CRASH_CONFIG: fn(&mut $crate::crash_handler::CrashConfig) = _crash_config;
+        #[doc(hidden)]
+        fn _crash_config(cfg: &mut $crate::crash_handler::CrashConfig) {
+            fn crash_config(cfg: &mut $crate::crash_handler::CrashConfig, handler: impl FnOnce(&mut $crate::crash_handler::CrashConfig)) {
+                handler(cfg)
+            }
+            crash_config(cfg, $closure)
+        }
+    };
+}
+pub use crate::crash_handler_config;
+
 type ConfigProcess = Vec<Box<dyn for<'a, 'b> FnMut(&'a mut std::process::Command, &'b CrashArgs) -> &'a mut std::process::Command>>;
+type CrashDialogHandler = Box<dyn FnOnce(CrashArgs)>;
 
 /// Crash handler config.
+///
+/// Use [`crash_handler_config!`] to set config.
+///
+/// [`crash_handler_config!`]: crate::crash_handler_config!
 pub struct CrashConfig {
-    dialog: fn(CrashArgs) -> !,
-    cfg_app: ConfigProcess,
-    cfg_dialog: ConfigProcess,
+    default_dialog: Option<CrashDialogHandler>,
+    dialog: Option<CrashDialogHandler>,
+    app_process: ConfigProcess,
+    dialog_process: ConfigProcess,
     dump_dir: Option<PathBuf>,
 }
 impl CrashConfig {
-    /// New with function called in the dialog-process.
-    pub fn new(dialog: fn(CrashArgs) -> !) -> Self {
+    fn new() -> Self {
         Self {
-            dialog,
-            cfg_app: vec![],
-            cfg_dialog: vec![],
-            dump_dir: Some(std::env::temp_dir().join("zng_minidump")),
+            default_dialog: None,
+            dialog: None,
+            app_process: vec![],
+            dialog_process: vec![],
+            dump_dir: Some(zng_env::cache("zng_minidump")),
+        }
+    }
+
+    /// Set the crash dialog process handler.
+    ///
+    /// The dialog `handler` can run an app or show a native dialog, it must use the [`CrashArgs`] process
+    /// terminating methods to respond, if it returns [`CrashArgs::exit`] will run.
+    ///
+    /// Note that the handler does not need to actually show any dialog, it can just save crash info and
+    /// restart the app for example.
+    pub fn dialog(&mut self, handler: impl FnOnce(CrashArgs) + 'static) {
+        if self.dialog.is_none() {
+            self.dialog = Some(Box::new(handler));
         }
     }
 
+    /// Set the crash dialog-handler used if `crash_dialog` is not set.
+    ///
+    /// This is used by app libraries or themes to provide a default dialog.
+    pub fn default_dialog(&mut self, handler: impl FnOnce(CrashArgs) + 'static) {
+        self.default_dialog = Some(Box::new(handler));
+    }
+
     /// Add a closure that is called just before the app-process is spawned.
-    pub fn cfg_app(
-        mut self,
+    pub fn app_process(
+        &mut self,
         cfg: impl for<'a, 'b> FnMut(&'a mut std::process::Command, &'b CrashArgs) -> &'a mut std::process::Command + 'static,
-    ) -> Self {
-        self.cfg_app.push(Box::new(cfg));
-        self
+    ) {
+        self.app_process.push(Box::new(cfg));
     }
 
     /// Add a closure that is called just before the dialog-process is spawned.
-    pub fn cfg_dialog(
-        mut self,
+    pub fn dialog_process(
+        &mut self,
         cfg: impl for<'a, 'b> FnMut(&'a mut std::process::Command, &'b CrashArgs) -> &'a mut std::process::Command + 'static,
-    ) -> Self {
-        self.cfg_dialog.push(Box::new(cfg));
-        self
+    ) {
+        self.dialog_process.push(Box::new(cfg));
     }
 
     /// Change the minidump directory.
     ///
-    /// Is the temp dir by default.
-    pub fn minidump_dir(mut self, dir: impl Into<PathBuf>) -> Self {
+    /// Is `zng::env::cache("zng_minidump")` by default.
+    pub fn minidump_dir(&mut self, dir: impl Into<PathBuf>) {
         self.dump_dir = Some(dir.into());
-        self
     }
 
     /// Do not collect a minidump.
-    pub fn no_minidump(mut self) -> Self {
+    pub fn no_minidump(&mut self) {
         self.dump_dir = None;
-        self
-    }
-}
-impl From<fn(CrashArgs) -> !> for CrashConfig {
-    fn from(dialog: fn(CrashArgs) -> !) -> Self {
-        Self::new(dialog)
     }
 }
 
@@ -683,7 +757,7 @@ impl BacktraceFrame {
     }
 }
 
-fn crash_handler_monitor_process(mut cfg_app: ConfigProcess, mut cfg_dialog: ConfigProcess) -> ! {
+fn crash_handler_monitor_process(mut cfg_app: ConfigProcess, mut cfg_dialog: ConfigProcess, has_dialog_handler: bool) -> ! {
     // monitor-process:
     tracing::info!("crash monitor-process is running");
 
@@ -797,11 +871,15 @@ fn crash_handler_monitor_process(mut cfg_app: ConfigProcess, mut cfg_dialog: Con
                             retries += 1;
                         };
 
-                        let mut dialog_process = std::process::Command::new(&exe);
-                        for cfg in &mut cfg_dialog {
-                            cfg(&mut dialog_process, &dialog_args);
-                        }
-                        let dialog_result = run_process(dialog_process.env(DIALOG_PROCESS, &crash_file));
+                        let dialog_result = if has_dialog_handler {
+                            let mut dialog_process = std::process::Command::new(&exe);
+                            for cfg in &mut cfg_dialog {
+                                cfg(&mut dialog_process, &dialog_args);
+                            }
+                            run_process(dialog_process.env(DIALOG_PROCESS, &crash_file))
+                        } else {
+                            Ok((std::process::ExitStatus::default(), [String::new(), String::new()]))
+                        };
 
                         for _ in 0..5 {
                             if !crash_file.exists() || std::fs::remove_file(&crash_file).is_ok() {
@@ -817,7 +895,7 @@ fn crash_handler_monitor_process(mut cfg_app: ConfigProcess, mut cfg_dialog: Con
                                         .lines()
                                         .filter_map(|l| l.trim().strip_prefix(RESPONSE_PREFIX))
                                         .last()
-                                        .expect("crash dialog-process did not respond correctly")
+                                        .unwrap_or("exit 0")
                                         .to_owned()
                                 } else {
                                     let code = dlg_status.code();
@@ -955,7 +1033,7 @@ fn crash_handler_app_process(dump_dir: Option<&Path>) {
     // app-process execution happens after the `crash_handler` function returns.
 }
 
-fn crash_handler_dialog_process(dump_dir: Option<&Path>, dialog: fn(CrashArgs) -> !, args_file: String) -> ! {
+fn crash_handler_dialog_process(dump_dir: Option<&Path>, dialog: CrashDialogHandler, args_file: String) -> ! {
     tracing::info!("crash dialog-process is running");
 
     std::panic::set_hook(Box::new(panic_handler));
@@ -977,7 +1055,12 @@ fn crash_handler_dialog_process(dump_dir: Option<&Path>, dialog: fn(CrashArgs) -
         }
     };
 
-    dialog(serde_json::from_str(&args).expect("error deserializing args"))
+    dialog(serde_json::from_str(&args).expect("error deserializing args"));
+    CrashArgs {
+        app_crashes: vec![],
+        dialog_crash: None,
+    }
+    .exit(0)
 }
 
 fn panic_handler(info: &std::panic::PanicInfo) {
diff --git a/crates/zng-env/src/process.rs b/crates/zng-env/src/process.rs
index 7cc53f579..b920b83ed 100644
--- a/crates/zng-env/src/process.rs
+++ b/crates/zng-env/src/process.rs
@@ -1,4 +1,7 @@
-use std::mem;
+use std::{
+    mem,
+    sync::atomic::{AtomicBool, Ordering},
+};
 
 use parking_lot::Mutex;
 
@@ -87,9 +90,44 @@ pub(crate) fn process_init() -> impl Drop {
     );
     assert_eq!(process_state, ProcessLifetimeState::BeforeInit, "init!() already called");
 
-    let args = ProcessStartArgs { _private: () };
+    let mut yielded = vec![];
+    let mut next_handlers_count = ZNG_ENV_ON_PROCESS_START.len();
     for h in ZNG_ENV_ON_PROCESS_START {
+        next_handlers_count -= 1;
+        let args = ProcessStartArgs {
+            next_handlers_count,
+            yield_count: 0,
+            yield_requested: AtomicBool::new(false),
+        };
         h(&args);
+        if args.yield_requested.load(Ordering::Relaxed) {
+            yielded.push(h);
+            next_handlers_count += 1;
+        }
+    }
+
+    let mut yield_count = 0;
+    while !yielded.is_empty() {
+        yield_count += 1;
+        if yield_count > ProcessStartArgs::MAX_YIELD_COUNT {
+            eprintln!("start handlers requested `yield_start` more them 32 times");
+            break;
+        }
+
+        next_handlers_count = yielded.len();
+        for h in mem::take(&mut yielded) {
+            next_handlers_count -= 1;
+            let args = ProcessStartArgs {
+                next_handlers_count,
+                yield_count,
+                yield_requested: AtomicBool::new(false),
+            };
+            h(&args);
+            if args.yield_requested.load(Ordering::Relaxed) {
+                yielded.push(h);
+                next_handlers_count += 1;
+            }
+        }
     }
     MainExitHandler
 }
@@ -98,7 +136,26 @@ pub(crate) fn process_init() -> impl Drop {
 ///
 /// Empty in this release.
 pub struct ProcessStartArgs {
-    _private: (),
+    /// Number of start handlers yet to run.
+    pub next_handlers_count: usize,
+
+    /// Number of times this handler has yielded.
+    ///
+    /// If this exceeds 32 times the handler is ignored.
+    pub yield_count: u16,
+
+    yield_requested: AtomicBool,
+}
+impl ProcessStartArgs {
+    /// Yield requests after this are ignored.
+    pub const MAX_YIELD_COUNT: u16 = 32;
+
+    /// Let other process start handlers run first.
+    ///
+    /// The handler must call this if it takes over the process and it cannot determinate if it should from the environment.
+    pub fn yield_once(&self) {
+        self.yield_requested.store(true, Ordering::Relaxed);
+    }
 }
 
 struct MainExitHandler;
@@ -133,6 +190,7 @@ fn run_exit_handlers(code: i32) {
 }
 
 /// Arguments for [`on_process_exit`] handlers.
+#[non_exhaustive]
 pub struct ProcessExitArgs {
     /// Exit code that will be used.
     pub code: i32,
diff --git a/crates/zng-ext-single-instance/src/lib.rs b/crates/zng-ext-single-instance/src/lib.rs
index 4298d4a7f..f7cd159f8 100644
--- a/crates/zng-ext-single-instance/src/lib.rs
+++ b/crates/zng-ext-single-instance/src/lib.rs
@@ -34,68 +34,69 @@ impl AppExtension for SingleInstanceManager {
         let args: Box<[_]> = std::env::args().map(Txt::from).collect();
         APP_INSTANCE_EVENT.notify(AppInstanceArgs::now(args, 0usize));
 
-        if let Some(name) = SINGLE_INSTANCE.lock().as_ref().map(|l| l.name.clone()) {
-            let args_file = std::env::temp_dir().join(name);
-            let mut count = 1usize;
-            WATCHER
-                .on_file_changed(
-                    &args_file,
-                    async_app_hn!(args_file, |_, _| {
-                        let args = zng_task::wait(clmv!(args_file, || {
-                            for i in 0..5 {
-                                if i > 0 {
-                                    std::thread::sleep(Duration::from_millis(200));
-                                }
+        let name = match SINGLE_INSTANCE.lock().as_ref().map(|l| l.name.clone()) {
+            Some(n) => n,
+            None => return, // app is running in a special process, like a crash dialog
+        };
 
-                                // take args
-                                // read all text and truncates the file
-                                match std::fs::File::options().read(true).write(true).open(&args_file) {
-                                    Ok(mut file) => {
-                                        let mut s = String::new();
-                                        if let Err(e) = file.read_to_string(&mut s) {
-                                            tracing::error!("error reading args (retry {i}), {e}");
-                                            continue;
-                                        }
-                                        file.set_len(0).unwrap();
-                                        return s;
+        let args_file = std::env::temp_dir().join(name);
+        let mut count = 1usize;
+        WATCHER
+            .on_file_changed(
+                &args_file,
+                async_app_hn!(args_file, |_, _| {
+                    let args = zng_task::wait(clmv!(args_file, || {
+                        for i in 0..5 {
+                            if i > 0 {
+                                std::thread::sleep(Duration::from_millis(200));
+                            }
+
+                            // take args
+                            // read all text and truncates the file
+                            match std::fs::File::options().read(true).write(true).open(&args_file) {
+                                Ok(mut file) => {
+                                    let mut s = String::new();
+                                    if let Err(e) = file.read_to_string(&mut s) {
+                                        tracing::error!("error reading args (retry {i}), {e}");
+                                        continue;
                                     }
-                                    Err(e) => {
-                                        if e.kind() == std::io::ErrorKind::NotFound {
-                                            return String::new();
-                                        }
-                                        tracing::error!("error reading args (retry {i}), {e}")
+                                    file.set_len(0).unwrap();
+                                    return s;
+                                }
+                                Err(e) => {
+                                    if e.kind() == std::io::ErrorKind::NotFound {
+                                        return String::new();
                                     }
+                                    tracing::error!("error reading args (retry {i}), {e}")
                                 }
                             }
-                            String::new()
-                        }))
-                        .await;
-
-                        // parse args
-                        for line in args.lines() {
-                            let line = line.trim();
-                            if line.is_empty() {
-                                continue;
-                            }
+                        }
+                        String::new()
+                    }))
+                    .await;
+
+                    // parse args
+                    for line in args.lines() {
+                        let line = line.trim();
+                        if line.is_empty() {
+                            continue;
+                        }
 
-                            let args = match serde_json::from_str::<Box<[Txt]>>(line) {
-                                Ok(args) => args,
-                                Err(e) => {
-                                    tracing::error!("invalid args, {e}");
-                                    Box::new([])
-                                }
-                            };
+                        let args = match serde_json::from_str::<Box<[Txt]>>(line) {
+                            Ok(args) => args,
+                            Err(e) => {
+                                tracing::error!("invalid args, {e}");
+                                Box::new([])
+                            }
+                        };
 
-                            APP_INSTANCE_EVENT.notify(AppInstanceArgs::now(args, count));
+                        APP_INSTANCE_EVENT.notify(AppInstanceArgs::now(args, count));
 
-                            count += 1;
-                        }
-                    }),
-                )
-                .perm();
-        } else {
-            tracing::warn!("using `SingleInstanceManager` without calling `single_instance()`");
-        }
+                        count += 1;
+                    }
+                }),
+            )
+            .perm();
     }
 }
 
@@ -133,20 +134,20 @@ event! {
     pub static APP_INSTANCE_EVENT: AppInstanceArgs;
 }
 
-zng_env::on_process_start!(|_| {
-    single_instance_impl(
-        std::env::current_exe()
-            .and_then(dunce::canonicalize)
-            .expect("current exe is required")
-            .display()
-            .to_txt(),
-    )
-});
+zng_env::on_process_start!(|args| {
+    if args.next_handlers_count > 0 && args.yield_count < zng_env::ProcessStartArgs::MAX_YIELD_COUNT {
+        // absolute sure that this is the app-process
+        return args.yield_once();
+    }
 
-fn single_instance_impl(name: Txt) {
     let mut lock = SINGLE_INSTANCE.lock();
     assert!(lock.is_none(), "single_instance already called in this process");
 
+    let name = std::env::current_exe()
+        .and_then(dunce::canonicalize)
+        .expect("current exe is required")
+        .display()
+        .to_txt();
     let name: String = name
         .chars()
         .map(|c| if c.is_ascii_alphanumeric() || c == '-' { c } else { '_' })
@@ -162,7 +163,7 @@ fn single_instance_impl(name: Txt) {
     if l.is_single() {
         *lock = Some(SingleInstanceData { _lock: l, name });
     } else {
-        tracing::info!("another instance running, will send args an exit");
+        tracing::info!("another instance running, will send args and exit");
 
         let args: Box<[_]> = std::env::args().collect();
         let args = format!("\n{}\n", serde_json::to_string(&args).unwrap());
@@ -188,7 +189,7 @@ fn single_instance_impl(name: Txt) {
         }
         zng_env::exit(1);
     }
-}
+});
 
 struct SingleInstanceData {
     _lock: single_instance::SingleInstance,
diff --git a/crates/zng-view/src/extensions.rs b/crates/zng-view/src/extensions.rs
index 7d682d9c7..8793c9dff 100644
--- a/crates/zng-view/src/extensions.rs
+++ b/crates/zng-view/src/extensions.rs
@@ -660,14 +660,13 @@ pub struct RendererCommandArgs<'a> {
 }
 
 /// View extensions register.
-#[derive(Default)]
 pub struct ViewExtensions {
     exts: Vec<Box<dyn ViewExtension>>,
 }
 impl ViewExtensions {
     /// New empty.
-    pub fn new() -> Self {
-        Self::default()
+    pub(crate) fn new() -> Self {
+        Self { exts: vec![] }
     }
 
     /// Register an extension with the ID that will be assigned to it.
@@ -1083,15 +1082,15 @@ impl AsyncBlobImageRasterizer for BlockExtensionsImgRasterizer {
     }
 }
 
-/// Register a `fn() -> ViewExtensions` to be called on view-process init to inject custom API extensions.
+/// Register a `FnOnce(&mut ViewExtensions)` closure to be called on view-process init to inject custom API extensions.
 ///
 /// See [`ViewExtensions`] for more details.
 #[macro_export]
 macro_rules! view_process_extension {
-    ($extension_fn:path) => {
+    ($closure:expr) => {
         // expanded from:
         // #[linkme::distributed_slice(ZNG_ENV_RUN_PROCESS)]
-        // static _VIEW_EXTENSIONS = fn() -> $crate::extensions::ViewExtensions = $extension_fn;
+        // static _VIEW_EXTENSIONS = fn...;
         // so that users don't need to depend on linkme just to call this macro.
         #[used]
         #[cfg_attr(
@@ -1112,10 +1111,20 @@ macro_rules! view_process_extension {
         #[cfg_attr(target_os = "illumos", link_section = "set_linkme_VIEW_EXTENSIONS")]
         #[cfg_attr(target_os = "freebsd", link_section = "linkme_VIEW_EXTENSIONS")]
         #[doc(hidden)]
-        static _VIEW_EXTENSIONS: fn() -> $crate::extensions::ViewExtensions = $extension_fn;
+        static _VIEW_EXTENSIONS: fn(&mut $crate::extensions::ViewExtensions) = _view_extensions;
+        #[doc(hidden)]
+        fn _view_extensions(ext: &mut $crate::extensions::ViewExtensions) {
+            fn view_extensions(
+                ext: &mut $crate::extensions::ViewExtensions,
+                handler: impl FnOnce(&mut $crate::extensions::ViewExtensions),
+            ) {
+                handler(ext)
+            }
+            view_extensions(ext, $closure)
+        }
     };
 }
 
 #[doc(hidden)]
 #[linkme::distributed_slice]
-pub static VIEW_EXTENSIONS: [fn() -> ViewExtensions];
+pub static VIEW_EXTENSIONS: [fn(&mut ViewExtensions)];
diff --git a/crates/zng-view/src/lib.rs b/crates/zng-view/src/lib.rs
index f91f7fd59..ce792cfab 100644
--- a/crates/zng-view/src/lib.rs
+++ b/crates/zng-view/src/lib.rs
@@ -171,7 +171,7 @@ pub fn view_process_main() {
 
     let mut ext = ViewExtensions::new();
     for e in extensions::VIEW_EXTENSIONS {
-        ext.append(e());
+        e(&mut ext);
     }
 
     if config.headless {
diff --git a/crates/zng/Cargo.toml b/crates/zng/Cargo.toml
index 010134c74..575208f08 100644
--- a/crates/zng/Cargo.toml
+++ b/crates/zng/Cargo.toml
@@ -82,7 +82,7 @@ single_instance = ["dep:zng-ext-single-instance"]
 
 # Allow app-process crash handler.
 #
-# Note that `zng::app::crash_handler::init(_)` must be called to enable.
+# Builds with this feature spawn a crash monitor-process for each app-process.
 crash_handler = ["zng-app/crash_handler", "zng-wgt-inspector/crash_handler"]
 
 # Instrument every widget outer-most node to trace UI methods.
diff --git a/crates/zng/README.md b/crates/zng/README.md
index f857e2ecd..90b780d7a 100644
--- a/crates/zng/README.md
+++ b/crates/zng/README.md
@@ -106,7 +106,7 @@ the running app-process.
 #### `"crash_handler"`
 Allow app-process crash handler.
 
-Note that `zng::app::crash_handler::init(_)` must be called to enable.
+Builds with this feature spawn a crash monitor-process for each app-process.
 
 *Enabled by default.*
 
diff --git a/crates/zng/src/app.rs b/crates/zng/src/app.rs
index 704a7e7e5..758431070 100644
--- a/crates/zng/src/app.rs
+++ b/crates/zng/src/app.rs
@@ -435,13 +435,17 @@ pub use zng_ext_single_instance::{AppInstanceArgs, APP_INSTANCE_EVENT};
 
 /// App-process crash handler.
 ///
-/// The [`zng::app::crash_handler::init`] function takes over the first process turning it into the monitor-process,
-/// it spawns another process that is the monitored app-process. If the app-process crashes the monitor-process spawns a
-/// dialog-process that calls the dialog handler to show an error message, upload crash reports, etc.
+/// In builds with `"crash_handler"` feature the crash handler takes over the first "app-process" turning it into
+/// the monitor-process, it spawns another process that is the monitored app-process. If the app-process crashes
+/// the monitor-process spawns a dialog-process that calls the dialog handler to show an error message, upload crash reports, etc.
+///
+/// The dialog handler can be set using [`crash_handler_config!`].
+///
+/// [`crash_handler_config!`]: crate::app::crash_handler::crash_handler_config
 ///
 /// # Examples
 ///
-/// The example below demonstrates an app setup to handle crashes in the app-process.
+/// The example below demonstrates an app setup to show a custom crash dialog.
 ///
 /// ```no_run
 /// use zng::prelude::*;
@@ -449,21 +453,11 @@ pub use zng_ext_single_instance::{AppInstanceArgs, APP_INSTANCE_EVENT};
 /// fn main() {
 ///     // tracing applied to all processes.
 ///     zng::app::print_tracing(tracing::Level::INFO);
-///     zng::env::init!();
-///     
-///     // monitor-process or dialog-process init.
-///     //
-///     // Note that the dialog-process will use the same view-process init. Alternatively you
-///     // can call this before the view-process init and then init a custom view_process just
-///     // for the dialog or don't use Zng for the dialog.
-///     #[cfg(debug_assertions)]
-///     zng::app::crash_handler::init_debug();
-///     #[cfg(not(debug_assertions))]
-///     zng::app::crash_handler::init(zng::app::crash_handler::CrashConfig::new(dialog_main));
 ///
-///     // normal app-process
+///     // monitor-process spawns app-process and if needed dialog-process here.
+///     zng::env::init!();
 ///
-///     // zng::app::single_instance();
+///     // app-process:
 ///     app_main();
 /// }
 ///
@@ -497,40 +491,44 @@ pub use zng_ext_single_instance::{AppInstanceArgs, APP_INSTANCE_EVENT};
 ///     });
 /// }
 ///
-/// #[cfg(not(debug_assertions))]
-/// fn dialog_main(args: zng::app::crash_handler::CrashArgs) -> ! {
-///     APP.defaults().run_window(async move {
-///         Window! {
-///             title = "App Crashed!";
-///             auto_size = true;
-///             min_size = (300, 100);
-///             start_position = window::StartPosition::CenterMonitor;
-///             on_load = hn_once!(|_| WINDOW.bring_to_top());
-///             padding = 10;
-///             child = Text!(args.latest().message());
-///             child_bottom = Stack! {
-///                 direction = StackDirection::start_to_end();
-///                 layout::align = Align::BOTTOM_END;
-///                 spacing = 5;
-///                 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);
-///                         });
-///                     },
-///                 ]
-///             }, 10;
-///         }
+/// zng::app::crash_handler::crash_handler_config!(|cfg| {
+///     // monitor-process and dialog-process
+///
+///     cfg.dialog(|args| {
+///         // dialog-process
+///         APP.defaults().run_window(async move {
+///             Window! {
+///                 title = "App Crashed!";
+///                 auto_size = true;
+///                 min_size = (300, 100);
+///                 start_position = window::StartPosition::CenterMonitor;
+///                 on_load = hn_once!(|_| WINDOW.bring_to_top());
+///                 padding = 10;
+///                 child = Text!(args.latest().message());
+///                 child_bottom = Stack! {
+///                     direction = StackDirection::start_to_end();
+///                     layout::align = Align::BOTTOM_END;
+///                     spacing = 5;
+///                     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);
+///                             });
+///                         },
+///                     ]
+///                 }, 10;
+///             }
+///         });
 ///     });
-///     zng::env::exit(0)
-/// }
+/// });
+///
 /// ```
 ///
 /// # Full API
@@ -538,28 +536,25 @@ pub use zng_ext_single_instance::{AppInstanceArgs, APP_INSTANCE_EVENT};
 /// See [`zng_app::crash_handler`] and [`zng_wgt_inspector::crash_handler`] for the full API.
 #[cfg(feature = "crash_handler")]
 pub mod crash_handler {
-    use crate::prelude::*;
-    pub use zng_app::crash_handler::{init, BacktraceFrame, CrashArgs, CrashConfig, CrashError, CrashPanic};
+    pub use zng_app::crash_handler::{crash_handler_config, BacktraceFrame, CrashArgs, CrashConfig, CrashError, CrashPanic};
 
     pub use zng_wgt_inspector::crash_handler::debug_dialog;
 
-    /// Init a crash-handler with dialog that shows detailed debug info.
-    pub fn init_debug() {
-        init(CrashConfig::new(debug_app))
-    }
-
-    fn debug_app(args: CrashArgs) -> ! {
-        if let Some(c) = &args.dialog_crash {
-            eprintln!("DEBUG CRASH DIALOG ALSO CRASHED");
-            eprintln!("   {}", c.message());
-            eprintln!("ORIGINAL APP CRASH");
-            eprintln!("   {}", args.latest().message());
-            args.exit(0xBADC0DE)
-        }
+    crash_handler_config!(|cfg| {
+        use crate::prelude::*;
+        cfg.default_dialog(|args| {
+            if let Some(c) = &args.dialog_crash {
+                eprintln!("DEBUG CRASH DIALOG ALSO CRASHED");
+                eprintln!("   {}", c.message());
+                eprintln!("ORIGINAL APP CRASH");
+                eprintln!("   {}", args.latest().message());
+                args.exit(0xBADC0DE)
+            }
 
-        APP.defaults()
-            .run_window(async_clmv!(args, { zng_wgt_inspector::crash_handler::debug_dialog(args) }));
+            APP.defaults()
+                .run_window(async_clmv!(args, { zng_wgt_inspector::crash_handler::debug_dialog(args) }));
 
-        args.exit(0)
-    }
+            args.exit(0)
+        });
+    });
 }
diff --git a/crates/zng/src/lib.rs b/crates/zng/src/lib.rs
index 40078ca98..24e0f5575 100644
--- a/crates/zng/src/lib.rs
+++ b/crates/zng/src/lib.rs
@@ -448,8 +448,8 @@
 //! fire-and-forget task runners will also just log the panic as an error.
 //!
 //! You can also use the [`zng::app::crash_handler`] to collect panic backtraces, crash minidumps, show a crash dialog to the user
-//! and restart the app. During development you can use [`zng::app::crash_handler::init_debug`] to quickly setup a crash dialog that
-//! helps you debug the app.
+//! and restart the app. During development a debug crash dialog is provided, it shows the stdout/stderr, panics stacktrace and
+//! minidumps collected if any non-panic fatal error happens.
 //!
 //! [`tracing`]: https://docs.rs/tracing
 //!
diff --git a/examples/README.md b/examples/README.md
index ae145d18d..7f730399f 100644
--- a/examples/README.md
+++ b/examples/README.md
@@ -302,13 +302,8 @@ zng = { path = "../../crates/zng", features = ["view_prebuilt"] }
 use zng::prelude::*;
 
 fn main() {
-    examples_util::print_info();
     zng::env::init!();
-    zng::app::crash_handler::init_debug();
-    app_main();
-}
 
-fn app_main() {
     APP.defaults().run_window(async {
         Window! {
             title = "Foo Example";
diff --git a/examples/animation/src/main.rs b/examples/animation/src/main.rs
index 2c18d207f..e5552c833 100644
--- a/examples/animation/src/main.rs
+++ b/examples/animation/src/main.rs
@@ -15,11 +15,7 @@ use widgets::{ease_btn, ruler};
 
 fn main() {
     zng::env::init!();
-    zng::app::crash_handler::init_debug();
-    app_main();
-}
 
-fn app_main() {
     APP.defaults().run_window(async {
         Window! {
             title = "Animation Example";
diff --git a/examples/border/src/main.rs b/examples/border/src/main.rs
index 407ff5a0b..333ae49a2 100644
--- a/examples/border/src/main.rs
+++ b/examples/border/src/main.rs
@@ -9,11 +9,7 @@ mod widgets;
 
 fn main() {
     zng::env::init!();
-    zng::app::crash_handler::init_debug();
-    app_main();
-}
 
-fn app_main() {
     APP.defaults().run_window(async {
         Window! {
             title = "Border Example";
diff --git a/examples/button/src/main.rs b/examples/button/src/main.rs
index 5d872e1c1..94a5e4a2f 100644
--- a/examples/button/src/main.rs
+++ b/examples/button/src/main.rs
@@ -13,11 +13,7 @@ use zng::{
 
 fn main() {
     zng::env::init!();
-    zng::app::crash_handler::init_debug();
-    app_main();
-}
 
-fn app_main() {
     APP.defaults().run_window(async {
         Window! {
             title = "Button Example";
diff --git a/examples/calculator/src/main.rs b/examples/calculator/src/main.rs
index d060c2310..c1feb83a0 100644
--- a/examples/calculator/src/main.rs
+++ b/examples/calculator/src/main.rs
@@ -8,11 +8,7 @@ use zng::{
 
 fn main() {
     zng::env::init!();
-    zng::app::crash_handler::init_debug();
-    app_main();
-}
 
-fn app_main() {
     APP.defaults().run_window(async {
         set_fallback_font().await;
 
diff --git a/examples/config/src/main.rs b/examples/config/src/main.rs
index c102bb4e4..87505fd4b 100644
--- a/examples/config/src/main.rs
+++ b/examples/config/src/main.rs
@@ -12,9 +12,8 @@ use zng::{
 use zng::config::*;
 
 fn main() {
-    zng::env::init!();
     zng::env::init_res(concat!(env!("CARGO_MANIFEST_DIR"), "/res"));
-    zng::app::crash_handler::init_debug();
+    zng::env::init!();
     app_main();
 }
 
diff --git a/examples/countdown/src/main.rs b/examples/countdown/src/main.rs
index bab801973..9cfc02efb 100644
--- a/examples/countdown/src/main.rs
+++ b/examples/countdown/src/main.rs
@@ -5,7 +5,6 @@ use zng::{image, prelude::*, widget::background_color};
 
 fn main() {
     zng::env::init!();
-    zng::app::crash_handler::init_debug();
 
     // record profiler, use chrome://tracing or <ui.perfetto.dev>.
     let (chrome_layer, _guard) = tracing_chrome::ChromeLayerBuilder::new().build();
diff --git a/examples/cursor/src/main.rs b/examples/cursor/src/main.rs
index d5ca4b1c9..451d24c3a 100644
--- a/examples/cursor/src/main.rs
+++ b/examples/cursor/src/main.rs
@@ -12,11 +12,7 @@ mod widgets;
 
 fn main() {
     zng::env::init!();
-    zng::app::crash_handler::init_debug();
-    app_main();
-}
 
-fn app_main() {
     APP.defaults().run_window(async {
         let mut demos = ui_vec![];
         for icon in CURSORS {
diff --git a/examples/extend-view/src/get_window_handle.rs b/examples/extend-view/src/get_window_handle.rs
index a176eff49..73c7da48b 100644
--- a/examples/extend-view/src/get_window_handle.rs
+++ b/examples/extend-view/src/get_window_handle.rs
@@ -36,12 +36,12 @@ pub mod app_side {
 /// View-process stuff, the actual extension.
 pub mod view_side {
     use zng::text::formatx;
-    use zng_view::extensions::{ViewExtensions, WindowExtension};
+    use zng_view::extensions::WindowExtension;
     use zng_view_api::api_extension::{ApiExtensionId, ApiExtensionPayload};
 
-    pub fn extend(exts: &mut ViewExtensions) {
+    zng_view::view_process_extension!(|exts| {
         exts.window(super::api::extension_name(), CustomExtension::new);
-    }
+    });
 
     struct CustomExtension {
         id: ApiExtensionId,
diff --git a/examples/extend-view/src/main.rs b/examples/extend-view/src/main.rs
index 290f71967..b1794b2f0 100644
--- a/examples/extend-view/src/main.rs
+++ b/examples/extend-view/src/main.rs
@@ -1,7 +1,6 @@
 //! Demonstrates the `zng-view` extension API and render extensions API.
 
 use zng::{color::filter::hue_rotate, layout::size, prelude::*};
-use zng_view::extensions::ViewExtensions;
 // use zng_wgt_webrender_debug as wr;
 
 // Examples of how to extend the view-process with custom renderers.
@@ -11,23 +10,9 @@ use zng_view::extensions::ViewExtensions;
 
 fn main() {
     zng::env::init!();
-    zng::app::crash_handler::init_debug();
     app_main();
 }
 
-zng_view::view_process_extension!(view_extensions);
-
-/// Called in the view-process to init extensions.
-fn view_extensions() -> ViewExtensions {
-    let mut exts = ViewExtensions::new();
-    get_window_handle::view_side::extend(&mut exts);
-    using_display_items::view_side::extend(&mut exts);
-    using_blob::view_side::extend(&mut exts);
-    using_gl_overlay::view_side::extend(&mut exts);
-    using_gl_texture::view_side::extend(&mut exts);
-    exts
-}
-
 mod get_window_handle;
 mod using_blob;
 mod using_display_items;
diff --git a/examples/extend-view/src/using_blob.rs b/examples/extend-view/src/using_blob.rs
index e3857439e..f3b865688 100644
--- a/examples/extend-view/src/using_blob.rs
+++ b/examples/extend-view/src/using_blob.rs
@@ -28,7 +28,7 @@ pub mod view_side {
     use zng::prelude::task::parking_lot::Mutex;
     use zng_view::extensions::PxToWr as _;
     use zng_view::{
-        extensions::{AsyncBlobRasterizer, BlobExtension, RenderItemArgs, RenderUpdateArgs, RendererExtension, ViewExtensions},
+        extensions::{AsyncBlobRasterizer, BlobExtension, RenderItemArgs, RenderUpdateArgs, RendererExtension},
         webrender::{
             api::{
                 units::{BlobDirtyRect, DeviceIntRect, DeviceIntSize, LayoutRect},
@@ -40,9 +40,9 @@ pub mod view_side {
     };
     use zng_view_api::api_extension::ApiExtensionId;
 
-    pub fn extend(exts: &mut ViewExtensions) {
+    zng_view::view_process_extension!(|exts| {
         exts.renderer(super::api::extension_name(), CustomExtension::new);
-    }
+    });
 
     struct CustomExtension {
         // id of this extension, for tracing.
diff --git a/examples/extend-view/src/using_display_items.rs b/examples/extend-view/src/using_display_items.rs
index 3f5987547..012de3ab9 100644
--- a/examples/extend-view/src/using_display_items.rs
+++ b/examples/extend-view/src/using_display_items.rs
@@ -125,7 +125,7 @@ pub mod view_side {
 
     use zng::prelude_wgt::PxPoint;
     use zng_view::{
-        extensions::{PxToWr as _, RenderItemArgs, RenderUpdateArgs, RendererExtension, ViewExtensions},
+        extensions::{PxToWr as _, RenderItemArgs, RenderUpdateArgs, RendererExtension},
         webrender::{
             api::{
                 units::{LayoutPoint, LayoutRect},
@@ -136,9 +136,9 @@ pub mod view_side {
     };
     use zng_view_api::api_extension::ApiExtensionId;
 
-    pub fn extend(exts: &mut ViewExtensions) {
+    zng_view::view_process_extension!(|exts| {
         exts.renderer(super::api::extension_name(), CustomExtension::new);
-    }
+    });
 
     struct CustomExtension {
         // id of this extension, for tracing.
diff --git a/examples/extend-view/src/using_gl_overlay.rs b/examples/extend-view/src/using_gl_overlay.rs
index 1096efa43..1c8768387 100644
--- a/examples/extend-view/src/using_gl_overlay.rs
+++ b/examples/extend-view/src/using_gl_overlay.rs
@@ -24,16 +24,16 @@ pub mod app_side {
 pub mod view_side {
     use zng::layout::{Px, PxPoint, PxRect, PxSize};
     use zng_view::{
-        extensions::{RenderItemArgs, RenderUpdateArgs, RendererExtension, ViewExtensions},
+        extensions::{RenderItemArgs, RenderUpdateArgs, RendererExtension},
         gleam::gl,
     };
     use zng_view_api::api_extension::ApiExtensionId;
 
     use super::api::BindingId;
 
-    pub fn extend(exts: &mut ViewExtensions) {
+    zng_view::view_process_extension!(|exts| {
         exts.renderer(super::api::extension_name(), CustomExtension::new);
-    }
+    });
 
     struct CustomExtension {
         // id of this extension, for tracing.
diff --git a/examples/extend-view/src/using_gl_texture.rs b/examples/extend-view/src/using_gl_texture.rs
index 609ac51b1..57f54b70d 100644
--- a/examples/extend-view/src/using_gl_texture.rs
+++ b/examples/extend-view/src/using_gl_texture.rs
@@ -24,7 +24,7 @@ pub mod app_side {
 pub mod view_side {
     use zng::layout::PxRect;
     use zng_view::{
-        extensions::{PxToWr as _, RenderItemArgs, RendererExtension, ViewExtensions},
+        extensions::{PxToWr as _, RenderItemArgs, RendererExtension},
         gleam::gl,
         webrender::api::{
             units::{DeviceIntSize, TexelRect},
@@ -34,9 +34,9 @@ pub mod view_side {
     };
     use zng_view_api::api_extension::ApiExtensionId;
 
-    pub fn extend(exts: &mut ViewExtensions) {
+    zng_view::view_process_extension!(|exts| {
         exts.renderer(super::api::extension_name(), CustomExtension::new);
-    }
+    });
 
     struct TextureInfo {
         // texture in OpenGL.
diff --git a/examples/focus/src/main.rs b/examples/focus/src/main.rs
index 86aab2ba8..3808bdf5a 100644
--- a/examples/focus/src/main.rs
+++ b/examples/focus/src/main.rs
@@ -20,11 +20,7 @@ use zng::{
 
 fn main() {
     zng::env::init!();
-    zng::app::crash_handler::init_debug();
-    app_main();
-}
 
-fn app_main() {
     APP.defaults().run_window(async {
         WINDOW.id().set_name("main").unwrap();
 
diff --git a/examples/gradient/src/main.rs b/examples/gradient/src/main.rs
index 3e326a80d..9ffd68079 100644
--- a/examples/gradient/src/main.rs
+++ b/examples/gradient/src/main.rs
@@ -13,11 +13,7 @@ use zng::{
 
 fn main() {
     zng::env::init!();
-    zng::app::crash_handler::init_debug();
-    app_main();
-}
 
-fn app_main() {
     APP.defaults().run_window(async {
         Window! {
             title = "Gradient Example";
diff --git a/examples/hot-reload/src/main.rs b/examples/hot-reload/src/main.rs
index 5aca358e6..ff4da9e25 100644
--- a/examples/hot-reload/src/main.rs
+++ b/examples/hot-reload/src/main.rs
@@ -6,11 +6,7 @@ use zng::prelude::*;
 
 fn main() {
     zng::env::init!();
-    zng::app::crash_handler::init_debug();
-    app_main();
-}
 
-fn app_main() {
     // examples/Cargo.toml enables the `"hot_reload"` feature for `zng`,
     // so the hot reload extension is available in `APP.defaults()`.
     APP.defaults().run_window(async {
diff --git a/examples/icon/src/main.rs b/examples/icon/src/main.rs
index d1a03bddd..a1a74cd89 100644
--- a/examples/icon/src/main.rs
+++ b/examples/icon/src/main.rs
@@ -24,11 +24,7 @@ use zng::{
 
 fn main() {
     zng::env::init!();
-    zng::app::crash_handler::init_debug();
-    app_main();
-}
 
-fn app_main() {
     APP.defaults().run_window(async {
         Window! {
             title = "Icon Example";
diff --git a/examples/image/src/main.rs b/examples/image/src/main.rs
index 93a62efc8..0f02fb465 100644
--- a/examples/image/src/main.rs
+++ b/examples/image/src/main.rs
@@ -22,13 +22,9 @@ use zng::{
 use zng_wgt_webrender_debug as wr;
 
 fn main() {
-    zng::env::init!();
     zng::env::init_res(concat!(env!("CARGO_MANIFEST_DIR"), "/res"));
-    zng::app::crash_handler::init_debug();
-    app_main();
-}
+    zng::env::init!();
 
-fn app_main() {
     APP.defaults().run_window(async {
         // by default all "ImageSource::Download" requests are blocked and "ImageSource::Read"
         // is limited to only the exe dir. The limits can be set globally in here and overridden 
diff --git a/examples/layer/src/main.rs b/examples/layer/src/main.rs
index 874634bf5..ab394d208 100644
--- a/examples/layer/src/main.rs
+++ b/examples/layer/src/main.rs
@@ -12,11 +12,7 @@ use zng::{
 
 fn main() {
     zng::env::init!();
-    zng::app::crash_handler::init_debug();
-    app_main();
-}
 
-fn app_main() {
     APP.defaults().run_window(async {
         // layer will init with the window, when it opens.
         LAYERS.insert(
diff --git a/examples/localize/src/main.rs b/examples/localize/src/main.rs
index 51cf9454e..53312ae65 100644
--- a/examples/localize/src/main.rs
+++ b/examples/localize/src/main.rs
@@ -24,13 +24,9 @@ use zng::{
 // cargo run -p cargo-zng -- zng l10n "examples/localize/src" "examples/localize/res"
 
 fn main() {
-    zng::env::init!();
     zng::env::init_res(concat!(env!("CARGO_MANIFEST_DIR"), "/res"));
-    zng::app::crash_handler::init_debug();
-    app_main();
-}
+    zng::env::init!();
 
-fn app_main() {
     APP.defaults().run_window(async {
         // load `available_langs`
         L10N.load_dir(zng::env::res(""));
diff --git a/examples/markdown/src/main.rs b/examples/markdown/src/main.rs
index 857e8f64f..5ee9fb0a3 100644
--- a/examples/markdown/src/main.rs
+++ b/examples/markdown/src/main.rs
@@ -8,13 +8,9 @@ use zng::{
 };
 
 fn main() {
-    zng::env::init!();
     zng::env::init_res(concat!(env!("CARGO_MANIFEST_DIR"), "/res"));
-    zng::app::crash_handler::init_debug();
-    app_main();
-}
+    zng::env::init!();
 
-fn app_main() {
     APP.defaults().run_window(async {
         Window! {
             title = "Markdown Example";
diff --git a/examples/respawn/src/main.rs b/examples/respawn/src/main.rs
index b746b0cf5..5dab6e2f3 100644
--- a/examples/respawn/src/main.rs
+++ b/examples/respawn/src/main.rs
@@ -7,7 +7,6 @@ use zng::{
     prelude::*,
 };
 use zng_app::view_process::VIEW_PROCESS;
-use zng_view::extensions::ViewExtensions;
 
 fn main() {
     // log other processes too.
@@ -15,18 +14,7 @@ fn main() {
     // init metadata, view-process, crash-dialog-process.
     zng::env::init!();
 
-    // init crash-handler before view to use different view for the crash dialog app.
-    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:
-    app_main();
-}
-
-// init view with extensions used to cause a crash in the view-process.
-zng_view::view_process_extension!(test_extensions);
-
-fn app_main() {
     APP.defaults().run_window(async {
         Window! {
             title = "Respawn Example";
@@ -87,8 +75,9 @@ fn app_main() {
 }
 
 // Crash dialog app, runs in the dialog-process.
+// zng::app::crash_handler::crash_handler_config!(|cfg| cfg.dialog(app_crash_handler));
 #[allow(unused)]
-fn app_crash_dialog(args: zng::app::crash_handler::CrashArgs) -> ! {
+fn app_crash_dialog(args: zng::app::crash_handler::CrashArgs) {
     APP.defaults().run_window(async move {
         Window! {
             title = "Respawn Example - App Crashed";
@@ -137,7 +126,6 @@ fn app_crash_dialog(args: zng::app::crash_handler::CrashArgs) -> ! {
             }, 5;
         }
     });
-    zng::env::exit(0)
 }
 
 fn view_respawn() -> impl UiNode {
@@ -253,8 +241,7 @@ fn icon() -> impl UiNode {
     }
 }
 
-fn test_extensions() -> ViewExtensions {
-    let mut ext = ViewExtensions::new();
+// init view with extensions used to cause a crash in the view-process.
+zng_view::view_process_extension!(|ext| {
     ext.command::<(), ()>("zng.examples.respawn.crash", |_, _| panic!("Test view-process crash!"));
-    ext
-}
+});
diff --git a/examples/scroll/src/main.rs b/examples/scroll/src/main.rs
index b243f6743..256d85ae2 100644
--- a/examples/scroll/src/main.rs
+++ b/examples/scroll/src/main.rs
@@ -10,11 +10,7 @@ use rand::SeedableRng;
 
 fn main() {
     zng::env::init!();
-    zng::app::crash_handler::init_debug();
-    app_main();
-}
 
-fn app_main() {
     APP.defaults().run_window(async {
         let mouse_pan = var(false);
         let smooth_scrolling = var(true);
diff --git a/examples/shortcut/src/main.rs b/examples/shortcut/src/main.rs
index 1fe72ab07..4c2b0c212 100644
--- a/examples/shortcut/src/main.rs
+++ b/examples/shortcut/src/main.rs
@@ -6,11 +6,7 @@ use zng::{font::FontName, layout::align, prelude::*};
 
 fn main() {
     zng::env::init!();
-    zng::app::crash_handler::init_debug();
-    app_main();
-}
 
-fn app_main() {
     APP.defaults().run_window(async {
         let shortcut_text = var(Txt::from_str(""));
         let keypress_text = var(Txt::from_str(""));
diff --git a/examples/text/src/main.rs b/examples/text/src/main.rs
index b21fc8375..fd672db0e 100644
--- a/examples/text/src/main.rs
+++ b/examples/text/src/main.rs
@@ -17,11 +17,7 @@ mod form;
 
 fn main() {
     zng::env::init!();
-    zng::app::crash_handler::init_debug();
-    app_main();
-}
 
-fn app_main() {
     APP.defaults().run_window(async {
         let fs = var(Length::Pt(11.0));
 
diff --git a/examples/transform/src/main.rs b/examples/transform/src/main.rs
index ee0c9d552..e4b6c7788 100644
--- a/examples/transform/src/main.rs
+++ b/examples/transform/src/main.rs
@@ -16,11 +16,7 @@ use zng::{
 
 fn main() {
     zng::env::init!();
-    zng::app::crash_handler::init_debug();
-    app_main();
-}
 
-fn app_main() {
     APP.defaults().run_window(async {
         Window! {
             title = "Transform Example";
diff --git a/examples/window/src/main.rs b/examples/window/src/main.rs
index f6ae0f37b..b71d2ce89 100644
--- a/examples/window/src/main.rs
+++ b/examples/window/src/main.rs
@@ -24,7 +24,6 @@ use zng::{
 
 fn main() {
     zng::env::init!();
-    zng::app::crash_handler::init_debug();
     zng::view_process::prebuilt::run_same_process(app_main);
 }
 


Test patch: