Skip to main content

zng_env/
process.rs

1use core::fmt;
2use std::{
3    mem,
4    sync::atomic::{AtomicU8, Ordering},
5};
6
7use parking_lot::Mutex;
8
9#[doc(hidden)]
10#[cfg(not(target_arch = "wasm32"))]
11pub use linkme as __linkme;
12
13/// Register a `FnOnce(&ProcessStartArgs)` closure to be called on [`init!`].
14///
15/// Components that spawn special process instances implemented on the same executable
16/// can use this macro to inject their own "main" without needing to ask the user to plug an init
17/// function on the executable main. The component can spawn an instance of the current executable
18/// with marker environment variables that identify the component's process.
19///
20/// [`init!`]: crate::init!
21///
22/// # Examples
23///
24/// The example below declares a "main" for a foo component and a function that spawns it.
25///
26/// ```
27/// zng_env::on_process_start!(|args| {
28///     if args.yield_count == 0 {
29///         return args.yield_once();
30///     }
31///
32///     if std::env::var("FOO_MARKER").is_ok() {
33///         println!("Spawned as foo!");
34///         zng_env::exit(0);
35///     }
36/// });
37///
38/// fn main() {
39///     zng_env::init!(); // foo_main OR
40///     // normal main
41/// }
42///
43/// pub fn spawn_foo() -> std::io::Result<()> {
44///     std::process::Command::new(std::env::current_exe()?).env("FOO_MARKER", "").spawn()?;
45///     Ok(())
46/// }
47/// ```
48///
49/// Note that the handler yields once, this gives a chance for all handlers to run first before the handler is called again
50/// and takes over the process. It is good practice to yield at least once to ensure handlers that are supposed to affect all
51/// processes actually init, as an example, the trace recorder may never start for the process if it does not yield.
52///
53/// Also note the use of custom [`exit`], it is important to call it to collaborate with [`on_process_exit`] handlers.
54///
55/// # App Context
56///
57/// This event happens on the executable process context, before any `APP` context starts, you can use
58/// `zng::APP::on_init` here to register a handler to be called in the app context, if and when it starts.
59///
60/// # Web Assembly
61///
62/// Crates that declare `on_process_start` must have the [`wasm_bindgen`] dependency to compile for the `wasm32` target.
63///
64/// In `Cargo.toml` add this dependency:
65///
66/// ```toml
67/// [target.'cfg(target_arch = "wasm32")'.dependencies]
68/// wasm-bindgen = "0.2"
69/// ```
70///
71/// Try to match the version used by `zng-env`.
72///
73/// # Linker Optimizer Issues
74///
75/// The macOS system linker can "optimize" away crates that are only referenced via this macro, that is, a crate dependency
76/// that is not otherwise directly addressed by code. To workaround this issue you can add a bogus reference to the crate code, something
77/// that is not trivial to optimize away. Unfortunately this code must be added on the dependent crate, or on an intermediary dependency,
78/// if your crate is at risk of being used this way please document this issue.
79///
80/// See [`zng#437`] for an example of how to fix this issue.
81///
82/// [`wasm_bindgen`]: https://crates.io/crates/wasm-bindgen
83/// [`zng#437`]: https://github.com/zng-ui/zng/pull/437
84#[macro_export]
85macro_rules! on_process_start {
86    ($closure:expr) => {
87        $crate::__on_process_start! {$closure}
88    };
89}
90
91#[cfg(not(target_arch = "wasm32"))]
92#[doc(hidden)]
93#[macro_export]
94macro_rules! __on_process_start {
95    ($closure:expr) => {
96        const _: () = {
97            #[$crate::__linkme::distributed_slice($crate::ZNG_ENV_ON_PROCESS_START)]
98            #[linkme(crate = $crate::__linkme)]
99            #[doc(hidden)]
100            static _ON_PROCESS_START: fn(&$crate::ProcessStartArgs) = _on_process_start;
101            #[doc(hidden)]
102            fn _on_process_start(args: &$crate::ProcessStartArgs) {
103                fn on_process_start(args: &$crate::ProcessStartArgs, handler: impl FnOnce(&$crate::ProcessStartArgs)) {
104                    handler(args)
105                }
106                on_process_start(args, $closure)
107            }
108        };
109    };
110}
111
112#[cfg(target_arch = "wasm32")]
113#[doc(hidden)]
114#[macro_export]
115macro_rules! __on_process_start {
116    ($closure:expr) => {
117        $crate::wasm_process_start! {$crate,$closure}
118    };
119}
120
121#[doc(hidden)]
122#[cfg(target_arch = "wasm32")]
123pub use wasm_bindgen::prelude::wasm_bindgen;
124
125#[doc(hidden)]
126#[cfg(target_arch = "wasm32")]
127pub use zng_env_proc_macros::wasm_process_start;
128use zng_txt::Txt;
129
130#[cfg(target_arch = "wasm32")]
131std::thread_local! {
132    #[doc(hidden)]
133    pub static WASM_INIT: std::cell::RefCell<Vec<fn(&ProcessStartArgs)>> = const { std::cell::RefCell::new(vec![]) };
134}
135
136#[cfg(not(target_arch = "wasm32"))]
137#[doc(hidden)]
138#[linkme::distributed_slice]
139pub static ZNG_ENV_ON_PROCESS_START: [fn(&ProcessStartArgs)];
140
141#[cfg(not(target_arch = "wasm32"))]
142pub(crate) fn process_init() -> impl Drop {
143    process_init_impl(&ZNG_ENV_ON_PROCESS_START)
144}
145
146fn process_init_impl(handlers: &[fn(&ProcessStartArgs)]) -> MainExitHandler {
147    // set path env var
148    let _ = process_path();
149
150    let process_state = std::mem::replace(
151        &mut *zng_unique_id::hot_static_ref!(PROCESS_LIFETIME_STATE).lock(),
152        ProcessLifetimeState::Inited,
153    );
154    assert_eq!(process_state, ProcessLifetimeState::BeforeInit, "init!() already called");
155
156    let mut yielded = vec![];
157    let mut next_handlers_count = handlers.len();
158    for h in handlers {
159        next_handlers_count -= 1;
160        let args = ProcessStartArgs {
161            next_handlers_count,
162            yield_count: 0,
163            yield_requested: AtomicU8::new(0),
164        };
165        h(&args);
166        if args.yield_requested.load(Ordering::Relaxed) == ProcessStartArgs::YIELD_ONCE {
167            yielded.push(h);
168            next_handlers_count += 1;
169        }
170    }
171
172    let mut yield_count = 0;
173    while !yielded.is_empty() {
174        yield_count += 1;
175        if yield_count > ProcessStartArgs::MAX_YIELD_COUNT {
176            eprintln!("start handlers requested `yield_start` more them 32 times");
177            break;
178        }
179
180        next_handlers_count = yielded.len();
181        for h in mem::take(&mut yielded) {
182            next_handlers_count -= 1;
183            let args = ProcessStartArgs {
184                next_handlers_count,
185                yield_count,
186                yield_requested: AtomicU8::new(0),
187            };
188            h(&args);
189            if let ProcessStartArgs::YIELD_ONCE = args.yield_requested.load(Ordering::Relaxed) {
190                yielded.push(h);
191                next_handlers_count += 1;
192            }
193        }
194    }
195
196    MainExitHandler
197}
198
199#[cfg(target_arch = "wasm32")]
200pub(crate) fn process_init() -> impl Drop {
201    std::panic::set_hook(Box::new(console_error_panic_hook::hook));
202
203    let window = web_sys::window().expect("cannot 'init!', no window object");
204    let module = js_sys::Reflect::get(&window, &"__zng_env_init_module".into())
205        .expect("cannot 'init!', missing module in 'window.__zng_env_init_module'");
206
207    if module == wasm_bindgen::JsValue::undefined() || module == wasm_bindgen::JsValue::null() {
208        panic!("cannot 'init!', missing module in 'window.__zng_env_init_module'");
209    }
210
211    let module: js_sys::Object = module.into();
212
213    for entry in js_sys::Object::entries(&module) {
214        let entry: js_sys::Array = entry.into();
215        let ident = entry.get(0).as_string().expect("expected ident at entry[0]");
216
217        if ident.starts_with("__zng_env_start_") {
218            let func: js_sys::Function = entry.get(1).into();
219            if let Err(e) = func.call0(&wasm_bindgen::JsValue::NULL) {
220                panic!("'init!' function error, {e:?}");
221            }
222        }
223    }
224
225    process_init_impl(&WASM_INIT.with_borrow_mut(std::mem::take))
226}
227
228/// Arguments for [`on_process_start`] handlers.
229///
230/// Empty in this release.
231pub struct ProcessStartArgs {
232    /// Number of start handlers yet to run.
233    pub next_handlers_count: usize,
234
235    /// Number of times this handler has yielded.
236    ///
237    /// If this exceeds 32 times the handler is ignored.
238    pub yield_count: u16,
239
240    yield_requested: AtomicU8,
241}
242impl ProcessStartArgs {
243    /// Yield requests after this are ignored.
244    pub const MAX_YIELD_COUNT: u16 = 32;
245
246    const YIELD_ONCE: u8 = 1;
247
248    /// Let other process start handlers run first.
249    ///
250    /// The handler must call this if it takes over the process and it cannot determinate if it should from the environment.
251    ///
252    /// ```
253    /// # macro_rules! on_process_start { ($($tt:tt)*) => { } }
254    /// fn run_foo_process() {}
255    /// on_process_start!(|args| {
256    ///     if args.yield_count == 0 {
257    ///         return args.yield_once();
258    ///     }
259    ///
260    ///     // yielded once, handlers that affect all processes (loggers, tracers) are inited now
261    ///     if std::env::var("IS_FOO").is_ok() {
262    ///         // take over as "foo" process
263    ///         run_foo_process();
264    ///         zng_env::exit(0);
265    ///     }
266    /// });
267    /// ```
268    pub fn yield_once(&self) {
269        self.yield_requested.store(Self::YIELD_ONCE, Ordering::Relaxed);
270    }
271}
272
273struct MainExitHandler;
274impl Drop for MainExitHandler {
275    fn drop(&mut self) {
276        run_exit_handlers(if std::thread::panicking() { 101 } else { 0 })
277    }
278}
279
280type ExitHandler = Box<dyn FnOnce(&ProcessExitArgs) + Send + 'static>;
281
282zng_unique_id::hot_static! {
283    static ON_PROCESS_EXIT: Mutex<Vec<ExitHandler>> = Mutex::new(vec![]);
284}
285
286/// Terminates the current process with the specified exit code.
287///
288/// This function must be used instead of `std::process::exit` as it runs the [`on_process_exit`].
289pub fn exit(code: i32) -> ! {
290    run_exit_handlers(code);
291    std::process::exit(code)
292}
293
294fn run_exit_handlers(code: i32) {
295    *zng_unique_id::hot_static_ref!(PROCESS_LIFETIME_STATE).lock() = ProcessLifetimeState::Exiting;
296
297    let on_exit = mem::take(&mut *zng_unique_id::hot_static_ref!(ON_PROCESS_EXIT).lock());
298    let args = ProcessExitArgs { code };
299    for h in on_exit {
300        h(&args);
301    }
302}
303
304/// Arguments for [`on_process_exit`] handlers.
305#[non_exhaustive]
306pub struct ProcessExitArgs {
307    /// Exit code that will be used.
308    pub code: i32,
309}
310
311/// Register a `handler` to run once when the current process exits.
312///
313/// Note that the handler is only called if the process is terminated by [`exit`], or by the executable main
314/// function returning if [`init!`] is called on it.
315///
316/// [`init!`]: crate::init!
317pub fn on_process_exit(handler: impl FnOnce(&ProcessExitArgs) + Send + 'static) {
318    zng_unique_id::hot_static_ref!(ON_PROCESS_EXIT).lock().push(Box::new(handler))
319}
320
321/// Defines the state of the current process instance.
322///
323/// Use [`process_lifetime_state()`] to get.
324#[derive(Debug, Clone, Copy, PartialEq, Eq)]
325pub enum ProcessLifetimeState {
326    /// Init not called yet.
327    BeforeInit,
328    /// Init called and the function where it is called has not returned yet.
329    Inited,
330    /// Init called and the function where it is called is returning.
331    Exiting,
332}
333
334zng_unique_id::hot_static! {
335    static PROCESS_LIFETIME_STATE: Mutex<ProcessLifetimeState> = Mutex::new(ProcessLifetimeState::BeforeInit);
336}
337zng_unique_id::hot_static! {
338    static PROCESS_NAME: Mutex<Txt> = Mutex::new(Txt::from_static(""));
339}
340
341/// Get the state of the current process instance.
342pub fn process_lifetime_state() -> ProcessLifetimeState {
343    *zng_unique_id::hot_static_ref!(PROCESS_LIFETIME_STATE).lock()
344}
345
346/// Identifies the process as component of an app instance.
347///
348/// The path format as string is the `"instance_id//process_id/process_id"`, the `instance_id` is in in hexadecimal, followed by
349/// the `process_ids` in decimal, separated by `'/'`.
350///
351/// Use [`process_path`] to get the current process path.
352#[derive(PartialEq, Eq, Hash, Clone)]
353pub struct ProcessPath {
354    instance_id: u64,
355    process_ids: Box<[u32]>,
356}
357impl ProcessPath {
358    /// Unique ID of the current app instance.
359    pub fn instance_id(&self) -> u64 {
360        self.instance_id
361    }
362
363    /// The [`std::process::id`] of the process chain, from parent to child.
364    pub fn process_ids(&self) -> &[u32] {
365        &self.process_ids[..]
366    }
367
368    /// Get the parent process path if has parent.
369    pub fn parent(&self) -> Option<Self> {
370        if self.process_ids.len() == 1 {
371            None
372        } else {
373            Some(Self {
374                instance_id: self.instance_id,
375                process_ids: self.process_ids[..self.process_ids.len() - 1].into(),
376            })
377        }
378    }
379}
380impl fmt::Debug for ProcessPath {
381    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
382        if f.alternate() {
383            f.debug_struct("ProcessPath")
384                .field("instance_id", &self.instance_id)
385                .field("process_ids", &self.process_ids)
386                .finish()
387        } else {
388            write!(f, "ProcessPath({self})")
389        }
390    }
391}
392/// Alternate mode (`"{:#}"`) uses `-` separator.
393impl fmt::Display for ProcessPath {
394    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
395        if f.alternate() {
396            write!(f, "{:x}", self.instance_id)?;
397            for id in &self.process_ids {
398                write!(f, "-{id}")?;
399            }
400        } else {
401            write!(f, "{:x}/", self.instance_id)?;
402            for id in &self.process_ids {
403                write!(f, "/{id}")?;
404            }
405        }
406        Ok(())
407    }
408}
409impl serde::Serialize for ProcessPath {
410    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
411    where
412        S: serde::Serializer,
413    {
414        if serializer.is_human_readable() {
415            serializer.collect_str(self)
416        } else {
417            (self.instance_id, &self.process_ids[..]).serialize(serializer)
418        }
419    }
420}
421impl<'de> serde::Deserialize<'de> for ProcessPath {
422    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
423    where
424        D: serde::Deserializer<'de>,
425    {
426        if deserializer.is_human_readable() {
427            struct FromStrVisitor;
428            impl<'de> serde::de::Visitor<'de> for FromStrVisitor {
429                type Value = ProcessPath;
430
431                fn expecting(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
432                    write!(f, "process path string")
433                }
434
435                fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
436                where
437                    E: serde::de::Error,
438                {
439                    v.parse().map_err(serde::de::Error::custom)
440                }
441            }
442            deserializer.deserialize_str(FromStrVisitor)
443        } else {
444            let (instance_id, process_ids) = <(u64, Box<[u32]>)>::deserialize(deserializer)?;
445            Ok(Self { instance_id, process_ids })
446        }
447    }
448}
449impl std::str::FromStr for ProcessPath {
450    type Err = ParseProcessPathError;
451
452    fn from_str(s: &str) -> Result<Self, Self::Err> {
453        parse_process_path(s, false)
454    }
455}
456fn parse_process_path(s: &str, extend: bool) -> Result<ProcessPath, ParseProcessPathError> {
457    if let Some((instance_id, p_ids)) = s.split_once("//")
458        && !instance_id.is_empty()
459        && !p_ids.is_empty()
460    {
461        let instance_id = u64::from_str_radix(instance_id, 16)?;
462        let mut process_ids = Vec::with_capacity(p_ids.split('/').count() + if extend { 1 } else { 0 });
463        for id in p_ids.split('/') {
464            let id = id.parse::<u32>()?;
465            process_ids.push(id);
466        }
467        if extend {
468            process_ids.push(std::process::id());
469        }
470        Ok(ProcessPath {
471            instance_id,
472            process_ids: process_ids.into_boxed_slice(),
473        })
474    } else {
475        Err(ParseProcessPathError::MissingPart)
476    }
477}
478
479/// Represents an error parsing [`ProcessPath`].
480#[derive(Debug, Clone, PartialEq, Eq)]
481pub enum ParseProcessPathError {
482    /// Cannot parse a component.
483    Int(std::num::ParseIntError),
484    /// Missing component.
485    MissingPart,
486}
487impl fmt::Display for ParseProcessPathError {
488    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
489        match self {
490            ParseProcessPathError::Int(e) => fmt::Display::fmt(e, f),
491            ParseProcessPathError::MissingPart => write!(f, "missing part"),
492        }
493    }
494}
495impl std::error::Error for ParseProcessPathError {
496    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
497        match self {
498            ParseProcessPathError::Int(e) => Some(e),
499            ParseProcessPathError::MissingPart => None,
500        }
501    }
502}
503impl From<std::num::ParseIntError> for ParseProcessPathError {
504    fn from(e: std::num::ParseIntError) -> Self {
505        ParseProcessPathError::Int(e)
506    }
507}
508
509zng_unique_id::lazy_static! {
510    static ref PROCESS_PATH: ProcessPath = {
511        let path = match std::env::var("ZNG_PROCESS_PATH") {
512            Ok(s) => match parse_process_path(&s, true) {
513                Ok(p) => Some(p),
514                Err(e) => {
515                    eprintln!("invalid ZNG_PROCESS_PATH, {s:?}, {e}");
516                    None
517                }
518            },
519            Err(e) => match e {
520                std::env::VarError::NotPresent => None,
521                std::env::VarError::NotUnicode(s) => {
522                    eprintln!("invalid ZNG_PROCESS_PATH, {s:?}");
523                    None
524                }
525            },
526        };
527        let path = match path {
528            Some(p) => p,
529            None => ProcessPath {
530                #[cfg(target_arch = "wasm32")]
531                instance_id: 0,
532                #[cfg(not(target_arch = "wasm32"))]
533                instance_id: rand::random(),
534                process_ids: Box::new([std::process::id()]),
535            },
536        };
537        // SAFETY: this runs on `process_init` before anything else
538        // so the variable will remain the same for the lifetime of the process
539        unsafe {
540            std::env::set_var("ZNG_PROCESS_PATH", path.to_string());
541        }
542        path
543    };
544}
545
546/// Gets the current process ID as a component of an app instance.
547pub fn process_path() -> &'static ProcessPath {
548    &PROCESS_PATH
549}
550
551/// Gets a process runtime name.
552///
553/// The primary use of this name is to identify the process in logs, see [`set_process_name`] for details about the logged name.
554/// On set or init the name is logged as an info message "pid: {pid}, name: {name}".
555///
556/// # Common Names
557///
558/// All Zng provided process handlers name the process.
559///
560/// * `"app-process"` - Set by `APP` if no other name was set before the app starts building.
561/// * `"view-process"` - Set by the view-process implementer when running in multi process mode.
562/// * `"crash-handler-process"` - Set by the crash-handler when running with crash handling.
563/// * `"crash-dialog-process"` - Set by the crash-handler on the crash dialog process.
564/// * `"worker-process ({worker_name}, {pid})"` - Set by task worker processes if no name was set before the task runner server starts.
565pub fn process_name() -> Txt {
566    zng_unique_id::hot_static_ref!(PROCESS_NAME).lock().clone()
567}
568
569/// Changes the process runtime name.
570///
571/// This sets [`process_name`] and traces an info message "pid: {pid}, name: {name}". If the same PID is named multiple times
572/// the last name should be used when presenting the process in trace viewers.
573///
574/// The process name ideally should be set only by the [`on_process_start!`] "process takeover" handlers. You can use [`init_process_name`]
575/// to only set the name if it has not been set yet.
576pub fn set_process_name(name: impl Into<Txt>) {
577    set_process_name_impl(name.into(), true);
578}
579
580/// Set the process runtime name if it has not been named yet.
581///
582/// See [`set_process_name`] for more details.
583///
584/// Returns `true` if the name was set.
585pub fn init_process_name(name: impl Into<Txt>) -> bool {
586    set_process_name_impl(name.into(), false)
587}
588
589fn set_process_name_impl(new_name: Txt, replace: bool) -> bool {
590    let mut name = zng_unique_id::hot_static_ref!(PROCESS_NAME).lock();
591    if replace || name.is_empty() {
592        *name = new_name;
593        drop(name);
594        // WARNING: format of this message is public API, changing it is a breaking change
595        tracing::info!("pid: {}, name: {}", std::process::id(), process_name());
596        true
597    } else {
598        false
599    }
600}
601
602/// Panics with an standard message if `zng::env::init!()` was not called or was not called correctly.
603pub fn assert_inited() {
604    match process_lifetime_state() {
605        ProcessLifetimeState::BeforeInit => panic!("env not inited, please call `zng::env::init!()` in main"),
606        ProcessLifetimeState::Inited => {}
607        ProcessLifetimeState::Exiting => {
608            panic!("env not inited correctly, please call `zng::env::init!()` at the beginning of the actual main function")
609        }
610    }
611}