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