zng_env/
process.rs

1use std::{
2    mem,
3    sync::atomic::{AtomicBool, Ordering},
4};
5
6use parking_lot::Mutex;
7
8/// Register a `FnOnce(&ProcessStartArgs)` closure to be called on [`init!`].
9///
10/// Components that spawn special process instances implemented on the same executable
11/// can use this macro to inject their own "main" without needing to ask the user to plug an init
12/// function on the executable main. The component can spawn an instance of the current executable
13/// with marker environment variables that identify the component's process.
14///
15/// [`init!`]: crate::init!
16///
17/// # Examples
18///
19/// The example below declares a "main" for a foo component and a function that spawns it.
20///
21/// ```
22/// zng_env::on_process_start!(|args| {
23///     if args.yield_count == 0 {
24///         return args.yield_once();
25///     }
26///
27///     if std::env::var("FOO_MARKER").is_ok() {
28///         println!("Spawned as foo!");
29///         zng_env::exit(0);
30///     }
31/// });
32///
33/// fn main() {
34///     zng_env::init!(); // foo_main OR
35///     // normal main
36/// }
37///
38/// pub fn spawn_foo() -> std::io::Result<()> {
39///     std::process::Command::new(std::env::current_exe()?).env("FOO_MARKER", "").spawn()?;
40///     Ok(())
41/// }
42/// ```
43///
44/// Note that the handler yields once, this gives a chance for all handlers to run first before the handler is called again
45/// and takes over the process. It is good practice to yield at least once to ensure handlers that are supported to affect all
46/// processes actually init, as an example, the trace recorder may never start for the process if it does not yield.
47///
48/// Also note the use of custom [`exit`], it is important to call it to collaborate with [`on_process_exit`] handlers.
49///
50/// # App Context
51///
52/// This event happens on the executable process context, before any `APP` context starts, you can use
53/// `zng::app::on_app_start` here to register a handler to be called in the app context, if and when it starts.
54///
55/// # Web Assembly
56///
57/// Crates that declare `on_process_start` must have the [`wasm_bindgen`] dependency to compile for the `wasm32` target.
58///
59/// In `Cargo.toml` add this dependency:
60///
61/// ```toml
62/// [target.'cfg(target_arch = "wasm32")'.dependencies]
63/// wasm-bindgen = "*"
64/// ```
65///
66/// Try to match the version used by `zng-env`.
67///
68/// # Linker Optimizer Issues
69///
70/// The macOS system linker can "optimize" away crates that are only referenced via this macro, that is, a crate dependency
71/// that is not otherwise directly addressed by code. To workaround this issue you can add a bogus reference to the crate code, something
72/// that is not trivial to optimize away. Unfortunately this code must be added on the dependent crate, or on an intermediary dependency,
73/// if your crate is at risk of being used this way please document this issue.
74///
75/// See [`zng#437`] for an example of how to fix this issue.
76///
77/// [`wasm_bindgen`]: https://crates.io/crates/wasm-bindgen
78/// [`zng#437`]: https://github.com/zng-ui/zng/pull/437
79#[macro_export]
80macro_rules! on_process_start {
81    ($closure:expr) => {
82        $crate::__on_process_start! {$closure}
83    };
84}
85
86#[cfg(not(target_arch = "wasm32"))]
87#[doc(hidden)]
88#[macro_export]
89macro_rules! __on_process_start {
90    ($closure:expr) => {
91        // expanded from:
92        // #[linkme::distributed_slice(ZNG_ENV_ON_PROCESS_START)]
93        // static _ON_PROCESS_START: fn(&FooArgs) = _foo;
94        // so that users don't need to depend on linkme just to call this macro.
95        #[used]
96        #[cfg_attr(
97            any(
98                target_os = "none",
99                target_os = "linux",
100                target_os = "android",
101                target_os = "fuchsia",
102                target_os = "psp"
103            ),
104            unsafe(link_section = "linkme_ZNG_ENV_ON_PROCESS_START")
105        )]
106        #[cfg_attr(
107            any(target_os = "macos", target_os = "ios", target_os = "tvos"),
108            unsafe(link_section = "__DATA,__linkme7nCnSSdn,regular,no_dead_strip")
109        )]
110        #[cfg_attr(
111            any(target_os = "uefi", target_os = "windows"),
112            unsafe(link_section = ".linkme_ZNG_ENV_ON_PROCESS_START$b")
113        )]
114        #[cfg_attr(target_os = "illumos", unsafe(link_section = "set_linkme_ZNG_ENV_ON_PROCESS_START"))]
115        #[cfg_attr(
116            any(target_os = "freebsd", target_os = "openbsd"),
117            unsafe(link_section = "linkme_ZNG_ENV_ON_PROCESS_START")
118        )]
119        #[doc(hidden)]
120        static _ON_PROCESS_START: fn(&$crate::ProcessStartArgs) = _on_process_start;
121        fn _on_process_start(args: &$crate::ProcessStartArgs) {
122            fn on_process_start(args: &$crate::ProcessStartArgs, handler: impl FnOnce(&$crate::ProcessStartArgs)) {
123                handler(args)
124            }
125            on_process_start(args, $closure)
126        }
127    };
128}
129
130#[cfg(target_arch = "wasm32")]
131#[doc(hidden)]
132#[macro_export]
133macro_rules! __on_process_start {
134    ($closure:expr) => {
135        $crate::wasm_process_start! {$crate,$closure}
136    };
137}
138
139#[doc(hidden)]
140#[cfg(target_arch = "wasm32")]
141pub use wasm_bindgen::prelude::wasm_bindgen;
142
143#[doc(hidden)]
144#[cfg(target_arch = "wasm32")]
145pub use zng_env_proc_macros::wasm_process_start;
146use zng_txt::Txt;
147
148#[cfg(target_arch = "wasm32")]
149std::thread_local! {
150    #[doc(hidden)]
151    pub static WASM_INIT: std::cell::RefCell<Vec<fn(&ProcessStartArgs)>> = const { std::cell::RefCell::new(vec![]) };
152}
153
154#[cfg(not(target_arch = "wasm32"))]
155#[doc(hidden)]
156#[linkme::distributed_slice]
157pub static ZNG_ENV_ON_PROCESS_START: [fn(&ProcessStartArgs)];
158
159#[cfg(not(target_arch = "wasm32"))]
160pub(crate) fn process_init() -> impl Drop {
161    process_init_impl(&ZNG_ENV_ON_PROCESS_START)
162}
163
164fn process_init_impl(handlers: &[fn(&ProcessStartArgs)]) -> MainExitHandler {
165    let process_state = std::mem::replace(
166        &mut *zng_unique_id::hot_static_ref!(PROCESS_LIFETIME_STATE).lock(),
167        ProcessLifetimeState::Inited,
168    );
169    assert_eq!(process_state, ProcessLifetimeState::BeforeInit, "init!() already called");
170
171    let mut yielded = vec![];
172    let mut next_handlers_count = handlers.len();
173    for h in handlers {
174        next_handlers_count -= 1;
175        let args = ProcessStartArgs {
176            next_handlers_count,
177            yield_count: 0,
178            yield_requested: AtomicBool::new(false),
179        };
180        h(&args);
181        if args.yield_requested.load(Ordering::Relaxed) {
182            yielded.push(h);
183            next_handlers_count += 1;
184        }
185    }
186
187    let mut yield_count = 0;
188    while !yielded.is_empty() {
189        yield_count += 1;
190        if yield_count > ProcessStartArgs::MAX_YIELD_COUNT {
191            eprintln!("start handlers requested `yield_start` more them 32 times");
192            break;
193        }
194
195        next_handlers_count = yielded.len();
196        for h in mem::take(&mut yielded) {
197            next_handlers_count -= 1;
198            let args = ProcessStartArgs {
199                next_handlers_count,
200                yield_count,
201                yield_requested: AtomicBool::new(false),
202            };
203            h(&args);
204            if args.yield_requested.load(Ordering::Relaxed) {
205                yielded.push(h);
206                next_handlers_count += 1;
207            }
208        }
209    }
210    MainExitHandler
211}
212
213#[cfg(target_arch = "wasm32")]
214pub(crate) fn process_init() -> impl Drop {
215    std::panic::set_hook(Box::new(console_error_panic_hook::hook));
216
217    let window = web_sys::window().expect("cannot 'init!', no window object");
218    let module = js_sys::Reflect::get(&window, &"__zng_env_init_module".into())
219        .expect("cannot 'init!', missing module in 'window.__zng_env_init_module'");
220
221    if module == wasm_bindgen::JsValue::undefined() || module == wasm_bindgen::JsValue::null() {
222        panic!("cannot 'init!', missing module in 'window.__zng_env_init_module'");
223    }
224
225    let module: js_sys::Object = module.into();
226
227    for entry in js_sys::Object::entries(&module) {
228        let entry: js_sys::Array = entry.into();
229        let ident = entry.get(0).as_string().expect("expected ident at entry[0]");
230
231        if ident.starts_with("__zng_env_start_") {
232            let func: js_sys::Function = entry.get(1).into();
233            if let Err(e) = func.call0(&wasm_bindgen::JsValue::NULL) {
234                panic!("'init!' function error, {e:?}");
235            }
236        }
237    }
238
239    process_init_impl(&WASM_INIT.with_borrow_mut(std::mem::take))
240}
241
242/// Arguments for [`on_process_start`] handlers.
243///
244/// Empty in this release.
245pub struct ProcessStartArgs {
246    /// Number of start handlers yet to run.
247    pub next_handlers_count: usize,
248
249    /// Number of times this handler has yielded.
250    ///
251    /// If this exceeds 32 times the handler is ignored.
252    pub yield_count: u16,
253
254    yield_requested: AtomicBool,
255}
256impl ProcessStartArgs {
257    /// Yield requests after this are ignored.
258    pub const MAX_YIELD_COUNT: u16 = 32;
259
260    /// Let other process start handlers run first.
261    ///
262    /// The handler must call this if it takes over the process and it cannot determinate if it should from the environment.
263    pub fn yield_once(&self) {
264        self.yield_requested.store(true, Ordering::Relaxed);
265    }
266}
267
268struct MainExitHandler;
269impl Drop for MainExitHandler {
270    fn drop(&mut self) {
271        run_exit_handlers(if std::thread::panicking() { 101 } else { 0 })
272    }
273}
274
275type ExitHandler = Box<dyn FnOnce(&ProcessExitArgs) + Send + 'static>;
276
277zng_unique_id::hot_static! {
278    static ON_PROCESS_EXIT: Mutex<Vec<ExitHandler>> = Mutex::new(vec![]);
279}
280
281/// Terminates the current process with the specified exit code.
282///
283/// This function must be used instead of `std::process::exit` as it runs the [`on_process_exit`].
284pub fn exit(code: i32) -> ! {
285    run_exit_handlers(code);
286    std::process::exit(code)
287}
288
289fn run_exit_handlers(code: i32) {
290    *zng_unique_id::hot_static_ref!(PROCESS_LIFETIME_STATE).lock() = ProcessLifetimeState::Exiting;
291
292    let on_exit = mem::take(&mut *zng_unique_id::hot_static_ref!(ON_PROCESS_EXIT).lock());
293    let args = ProcessExitArgs { code };
294    for h in on_exit {
295        h(&args);
296    }
297}
298
299/// Arguments for [`on_process_exit`] handlers.
300#[non_exhaustive]
301pub struct ProcessExitArgs {
302    /// Exit code that will be used.
303    pub code: i32,
304}
305
306/// Register a `handler` to run once when the current process exits.
307///
308/// Note that the handler is only called if the process is terminated by [`exit`], or by the executable main
309/// function returning if [`init!`] is called on it.
310///
311/// [`init!`]: crate::init!
312pub fn on_process_exit(handler: impl FnOnce(&ProcessExitArgs) + Send + 'static) {
313    zng_unique_id::hot_static_ref!(ON_PROCESS_EXIT).lock().push(Box::new(handler))
314}
315
316/// Defines the state of the current process instance.
317///
318/// Use [`process_lifetime_state()`] to get.
319#[derive(Debug, Clone, Copy, PartialEq, Eq)]
320pub enum ProcessLifetimeState {
321    /// Init not called yet.
322    BeforeInit,
323    /// Init called and the function where it is called has not returned yet.
324    Inited,
325    /// Init called and the function where it is called is returning.
326    Exiting,
327}
328
329zng_unique_id::hot_static! {
330    static PROCESS_LIFETIME_STATE: Mutex<ProcessLifetimeState> = Mutex::new(ProcessLifetimeState::BeforeInit);
331}
332zng_unique_id::hot_static! {
333    static PROCESS_NAME: Mutex<Txt> = Mutex::new(Txt::from_static(""));
334}
335
336/// Get the state of the current process instance.
337pub fn process_lifetime_state() -> ProcessLifetimeState {
338    *zng_unique_id::hot_static_ref!(PROCESS_LIFETIME_STATE).lock()
339}
340
341/// Gets a process runtime name.
342///
343/// The primary use of this name is to identify the process in logs, see [`set_process_name`] for details about the logged name.
344/// On set or init the name is logged as an info message "pid: {pid}, name: {name}".
345///
346/// # Common Names
347///
348/// All Zng provided process handlers name the process.
349///
350/// * `"app-process"` - Set by `APP` if no other name was set before the app starts building.
351/// * `"view-process"` - Set by the view-process implementer when running in multi process mode.
352/// * `"crash-handler-process"` - Set by the crash-handler when running with crash handling.
353/// * `"crash-dialog-process"` - Set by the crash-handler on the crash dialog process.
354/// * `"worker-process ({worker_name}, {pid})"` - Set by task worker processes if no name was set before the task runner server starts.
355pub fn process_name() -> Txt {
356    zng_unique_id::hot_static_ref!(PROCESS_NAME).lock().clone()
357}
358
359/// Changes the process runtime name.
360///
361/// This sets [`process_name`] and traces an info message "pid: {pid}, name: {name}". If the same PID is named multiple times
362/// the last name should be used when presenting the process in trace viewers.
363///
364/// The process name ideally should be set only by the [`on_process_start!`] "process takeover" handlers. You can use [`init_process_name`]
365/// to only set the name if it has not been set yet.
366pub fn set_process_name(name: impl Into<Txt>) {
367    set_process_name_impl(name.into(), true);
368}
369
370/// Set the process runtime name if it has not been named yet.
371///
372/// See [`set_process_name`] for more details.
373///
374/// Returns `true` if the name was set.
375pub fn init_process_name(name: impl Into<Txt>) -> bool {
376    set_process_name_impl(name.into(), false)
377}
378
379fn set_process_name_impl(new_name: Txt, replace: bool) -> bool {
380    let mut name = zng_unique_id::hot_static_ref!(PROCESS_NAME).lock();
381    if replace || name.is_empty() {
382        *name = new_name;
383        drop(name);
384        tracing::info!("pid: {}, name: {}", std::process::id(), process_name());
385        true
386    } else {
387        false
388    }
389}
390
391/// Panics with an standard message if `zng::env::init!()` was not called or was not called correctly.
392pub fn assert_inited() {
393    match process_lifetime_state() {
394        ProcessLifetimeState::BeforeInit => panic!("env not inited, please call `zng::env::init!()` in main"),
395        ProcessLifetimeState::Inited => {}
396        ProcessLifetimeState::Exiting => {
397            panic!("env not inited correctly, please call `zng::env::init!()` at the beginning of the actual main function")
398        }
399    }
400}