Skip to main content

nodejs/stdlib/
process.rs

1//! Node `process` global — the subset packages read at load time.
2//!
3//! Data properties (`process.env`, `process.argv`, `process.platform`, the
4//! `stdout`/`stderr` stream stand-ins, …) are served through `constant`;
5//! callable members (`process.cwd()`, `process.hrtime()`, the EventEmitter-style
6//! `on`/`emit` no-ops, …) through `call`. `process.nextTick` is intentionally NOT
7//! handled here — it stays on the core microtask path in `builtins.rs`.
8
9use crate::host::{with_host, JsObj};
10use fusevm::Value;
11use indexmap::IndexMap;
12
13/// Callable members. `nextTick` is deliberately absent (handled in `builtins`).
14pub const METHODS: &[&str] = &[
15    "cwd",
16    "chdir",
17    "exit",
18    "hrtime",
19    // `process.hrtime.bigint()` is a real member, not just a property that
20    // answers `typeof "function"`. It resolves as the qualified builtin
21    // `process.hrtime.bigint` (`namespace_property` composes `ns` + `name`),
22    // so it has to be declared here for `is_method` to route the call.
23    "hrtime.bigint",
24    "uptime",
25    "memoryUsage",
26    "cpuUsage",
27    "umask",
28    "binding",
29    "emit",
30    "on",
31    "once",
32    "off",
33    "addListener",
34    "removeListener",
35    "removeAllListeners",
36    "listeners",
37    "emitWarning",
38    "kill",
39    "getuid",
40    "getgid",
41    "geteuid",
42    "getegid",
43    "getgroups",
44    "setuid",
45    "setgid",
46    "seteuid",
47    "setegid",
48    "setgroups",
49    "initgroups",
50    "ref",
51    "unref",
52    "abort",
53    "getActiveResourcesInfo",
54    "resourceUsage",
55    "threadCpuUsage",
56    "availableMemory",
57    "constrainedMemory",
58    "getBuiltinModule",
59    "openStdin",
60    "hasUncaughtExceptionCaptureCallback",
61    "setUncaughtExceptionCaptureCallback",
62    "addUncaughtExceptionCaptureCallback",
63    "execve",
64    "reallyExit",
65    "loadEnvFile",
66    "setSourceMapsEnabled",
67];
68
69thread_local! {
70    /// The single `process.setUncaughtExceptionCaptureCallback` slot. Stored as a
71    /// heap handle (thread-local like the JS heap); read by
72    /// `hasUncaughtExceptionCaptureCallback`.
73    static UNCAUGHT_CAPTURE: std::cell::RefCell<Option<Value>> =
74        const { std::cell::RefCell::new(None) };
75}
76
77/// Whether the one-shot "(Use `node --trace-… ...`)" hint has been printed.
78/// Node prints it after the FIRST warning only, per process.
79static TRACE_HINT_SHOWN: std::sync::atomic::AtomicBool = std::sync::atomic::AtomicBool::new(false);
80
81/// Port of `internal/process/warning.js` `onWarning`: render a warning to
82/// stderr as `(node:PID) [CODE] Name: message`, followed by an optional detail
83/// line and the one-time trace hint. Suppressed by `--no-warnings`, and
84/// deprecations additionally by `--no-deprecation`.
85pub fn emit_warning(name: &str, code: Option<&str>, message: &str, detail: Option<&str>) {
86    let argv: Vec<String> = std::env::args().collect();
87    let flag = |f: &str| argv.iter().any(|a| a == f);
88    let is_deprecation = name == "DeprecationWarning";
89    if flag("--no-warnings") || (is_deprecation && flag("--no-deprecation")) {
90        return;
91    }
92    let trace = flag("--trace-warnings") || (is_deprecation && flag("--trace-deprecation"));
93
94    let mut msg = std::format!("(node:{}) ", std::process::id());
95    if let Some(c) = code {
96        msg.push_str(&std::format!("[{c}] "));
97    }
98    msg.push_str(&std::format!("{name}: {message}"));
99    if let Some(d) = detail {
100        msg.push_str(&std::format!("\n{d}"));
101    }
102    if !trace && !TRACE_HINT_SHOWN.swap(true, std::sync::atomic::Ordering::Relaxed) {
103        let trace_flag = if is_deprecation {
104            "--trace-deprecation"
105        } else {
106            "--trace-warnings"
107        };
108        msg.push_str(&std::format!(
109            "\n(Use `node {trace_flag} ...` to show where the warning was created)"
110        ));
111    }
112    eprintln!("{msg}");
113}
114
115/// A `DeprecationWarning` fires at most once per `code` per process, matching
116/// the `warned` latches Node keeps at each deprecation site.
117pub fn emit_deprecation_warning(code: &str, message: &str) {
118    use std::cell::RefCell;
119    thread_local! {
120        static SEEN: RefCell<std::collections::HashSet<String>> =
121            RefCell::new(std::collections::HashSet::new());
122    }
123    let first = SEEN.with(|s| s.borrow_mut().insert(code.to_string()));
124    if first {
125        emit_warning("DeprecationWarning", Some(code), message, None);
126    }
127}
128
129/// A signal NAME (`"SIGKILL"`, case-insensitive) to its number, or `None` if the
130/// name is not one this platform defines.
131///
132/// The numbers come from `libc`, not from a hand-written table: signal numbering
133/// differs between macOS and Linux above SIGTERM (`SIGUSR1` is 30 on Darwin and
134/// 10 on Linux), so a literal table is only correct on the platform it was
135/// written for. Shared with `cluster`'s `worker.kill`.
136pub fn signal_number(name: &str) -> Option<libc::c_int> {
137    Some(match name.to_uppercase().as_str() {
138        "SIGHUP" => libc::SIGHUP,
139        "SIGINT" => libc::SIGINT,
140        "SIGQUIT" => libc::SIGQUIT,
141        "SIGILL" => libc::SIGILL,
142        "SIGTRAP" => libc::SIGTRAP,
143        "SIGABRT" => libc::SIGABRT,
144        "SIGBUS" => libc::SIGBUS,
145        "SIGFPE" => libc::SIGFPE,
146        "SIGKILL" => libc::SIGKILL,
147        "SIGUSR1" => libc::SIGUSR1,
148        "SIGSEGV" => libc::SIGSEGV,
149        "SIGUSR2" => libc::SIGUSR2,
150        "SIGPIPE" => libc::SIGPIPE,
151        "SIGALRM" => libc::SIGALRM,
152        "SIGTERM" => libc::SIGTERM,
153        "SIGCHLD" => libc::SIGCHLD,
154        "SIGCONT" => libc::SIGCONT,
155        "SIGSTOP" => libc::SIGSTOP,
156        "SIGTSTP" => libc::SIGTSTP,
157        "SIGWINCH" => libc::SIGWINCH,
158        _ => return None,
159    })
160}
161
162/// `process.emitWarning(warning[, options])` / `(warning[, type[, code]])`.
163fn emit_warning_args(args: &[Value]) {
164    let message = super::arg_str(args, 0);
165    let mut name = "Warning".to_string();
166    let mut code: Option<String> = None;
167    let mut detail: Option<String> = None;
168    match args.get(1) {
169        Some(v) if with_host(|h| matches!(h.get(v), Some(JsObj::Object(_)))) => {
170            let field = |k: &str| {
171                with_host(|h| match h.get(v) {
172                    Some(JsObj::Object(p)) => {
173                        p.get(k).filter(|x| !h.is_nullish(x)).map(|x| h.str_of(x))
174                    }
175                    _ => None,
176                })
177            };
178            if let Some(t) = field("type") {
179                name = t;
180            }
181            code = field("code");
182            detail = field("detail");
183        }
184        Some(_) => {
185            name = super::arg_str(args, 1);
186            code = args.get(2).map(|_| super::arg_str(args, 2));
187        }
188        None => {}
189    }
190    emit_warning(&name, code.as_deref(), &message, detail.as_deref());
191}
192
193/// Data properties, served through `namespace_property` → `stdlib::constant`.
194/// Memoize an OBJECT-valued `process` property for the host's lifetime.
195///
196/// `process.env`, `process.argv` and the std streams were rebuilt on every read,
197/// so `process.env === process.env` was `false` and — much worse —
198/// `process.env.NODE_ENV = "production"` wrote to a throwaway object and read
199/// back `undefined`. Node hands out one object per property, and packages both
200/// mutate it and compare it by identity.
201///
202/// `builtin_static` is the existing side table for exactly this: state that must
203/// survive the fresh `Builtin` handle each `process` reference allocates. It is
204/// per-host, so `reset_host` clears it and a stale handle cannot outlive its heap.
205fn memo(name: &str, make: impl FnOnce() -> Value) -> Value {
206    if let Some(v) = with_host(|h| h.builtin_static("process", name)) {
207        return v;
208    }
209    let v = make();
210    with_host(|h| h.set_builtin_static("process", name, v.clone()));
211    v
212}
213
214pub fn constant(name: &str) -> Option<Value> {
215    Some(match name {
216        "env" => memo("env", env_object),
217        "argv" => memo("argv", argv),
218        "argv0" => with_host(|h| h.new_str(exec_path())),
219        "execPath" => with_host(|h| h.new_str(exec_path())),
220        "execArgv" => memo("execArgv", exec_argv),
221        "platform" => with_host(|h| h.new_str(super::os::platform())),
222        "arch" => with_host(|h| h.new_str(super::os::arch())),
223        "pid" => Value::Float(std::process::id() as f64),
224        "ppid" => Value::Float(0.0),
225        "title" => with_host(|h| h.new_str("node")),
226        // A best-effort Node-compatible version string. Kept low so a dep's
227        // `if (semver.lt(process.version, ...))` gate takes the conservative path.
228        "version" => with_host(|h| h.new_str("v26.5.0")),
229        "versions" => memo("versions", versions),
230        "stdout" => memo("stdout", || std_stream(1)),
231        "stderr" => memo("stderr", || std_stream(2)),
232        "stdin" => memo("stdin", || std_stream(0)),
233        // Unset reads back as `undefined`, not `0` — `process.exitCode` starts
234        // life absent and a script may test for that.
235        "exitCode" => match with_host(|h| h.exit_code) {
236            Some(c) => Value::Float(c as f64),
237            None => Value::Undef,
238        },
239        _ => return None,
240    })
241}
242
243/// The `process.exitCode` setter, ported from Node's
244/// `lib/internal/bootstrap/node.js` accessor:
245///
246/// ```js
247/// set(code) {
248///   if (code !== null && code !== undefined) {
249///     let value = code;
250///     if (typeof code === 'string' && code !== '' &&
251///       NumberIsNaN((value = Number(code)))) {
252///       value = code;
253///     }
254///     validateInteger(value, 'code');
255///     …
256///   } else { /* clear */ }
257/// }
258/// ```
259///
260/// So a NUMERIC string is accepted and coerced (`"3"` → 3, `"0x10"` → 16,
261/// `"  "` → 0), a non-numeric or empty string keeps its string identity and
262/// fails `validateInteger` as a TYPE error, a non-integer number fails as a
263/// RANGE error, and `null`/`undefined` clear the slot. Verified on node
264/// v26.7.0: `process.exitCode = "0x10"` exits 16, `= 3.7` throws
265/// `ERR_OUT_OF_RANGE`, `= ""` throws `ERR_INVALID_ARG_TYPE`, `= "  "` exits 0.
266pub fn set_exit_code(val: &Value) -> Result<(), String> {
267    if matches!(val, Value::Undef) || with_host(|h| h.is_null(val)) {
268        with_host(|h| h.exit_code = None);
269        return Ok(());
270    }
271    // A numeric string coerces; anything else keeps its own type for the error.
272    let numeric = match with_host(|h| h.as_str(val)) {
273        Some(s) if !s.is_empty() => {
274            let n = with_host(|h| h.to_number(val));
275            if n.is_nan() {
276                None
277            } else {
278                Some(n)
279            }
280        }
281        Some(_) => None,
282        None => match val {
283            Value::Float(_) | Value::Int(_) => Some(with_host(|h| h.to_number(val))),
284            _ => None,
285        },
286    };
287    match numeric {
288        Some(n) if n.fract() == 0.0 && n.is_finite() => {
289            with_host(|h| h.exit_code = Some(n as i32));
290            Ok(())
291        }
292        Some(n) => Err(crate::host::coded_error(
293            "RangeError",
294            "ERR_OUT_OF_RANGE",
295            &format!(
296                "The value of \"code\" is out of range. It must be an integer. Received {}",
297                crate::host::fmt_number(n)
298            ),
299        )),
300        None => Err(crate::host::invalid_arg_type(
301            "code", "argument", "number", val,
302        )),
303    }
304}
305
306pub fn call(method: &str, args: &[Value]) -> Option<Result<Value, String>> {
307    Some(match method {
308        "cwd" => {
309            let d = std::env::current_dir()
310                .map(|p| p.to_string_lossy().into_owned())
311                .unwrap_or_default();
312            Ok(with_host(|h| h.new_str(d)))
313        }
314        // `hrtime()` → `[seconds, nanoseconds]` since an arbitrary epoch (here the
315        // monotonic clock via `Instant` is unavailable statically, so use the
316        // system clock — sufficient for the timing scaffolding deps set up).
317        "hrtime" => Ok(hrtime(args)),
318        // Nanoseconds since an arbitrary epoch, as a BigInt.
319        "hrtime.bigint" => Ok(with_host(|h| {
320            let now = std::time::SystemTime::now()
321                .duration_since(std::time::UNIX_EPOCH)
322                .unwrap_or_default();
323            h.new_bigint(num_bigint::BigInt::from(now.as_nanos()))
324        })),
325        "uptime" => Ok(Value::Float(0.0)),
326        "memoryUsage" => Ok(memory_usage()),
327        "cpuUsage" => Ok(with_host(|h| {
328            let mut m = IndexMap::new();
329            m.insert("user".into(), Value::Float(0.0));
330            m.insert("system".into(), Value::Float(0.0));
331            h.new_object(m)
332        })),
333        "umask" => Ok(Value::Float(0.0)),
334        "binding" => Err(crate::host::type_error("process.binding is not supported")),
335        // EventEmitter-style registration. Listeners are REMEMBERED (the runtime
336        // emits `unhandledRejection`; signals still never fire), and every form
337        // returns the process namespace so `.on(...).on(...)` chains work.
338        "on" | "once" | "addListener" => {
339            let (event, f) = (event_name(args), args.get(1).cloned());
340            if let Some(f) = f {
341                let once = method == "once";
342                with_host(|h| {
343                    h.process_listeners
344                        .entry(event)
345                        .or_default()
346                        .push(crate::host::ProcListener { f, once })
347                });
348            }
349            Ok(with_host(|h| h.alloc(JsObj::Builtin("process".into()))))
350        }
351        "off" | "removeListener" => {
352            let (event, f) = (event_name(args), args.get(1).cloned());
353            if let Some(f) = f {
354                with_host(|h| {
355                    if let Some(l) = h.process_listeners.get_mut(&event) {
356                        if let Some(i) = l.iter().position(|x| x.f == f) {
357                            l.remove(i);
358                        }
359                    }
360                });
361            }
362            Ok(with_host(|h| h.alloc(JsObj::Builtin("process".into()))))
363        }
364        "removeAllListeners" => {
365            let event = event_name(args);
366            with_host(|h| {
367                if event.is_empty() {
368                    h.process_listeners.clear();
369                } else {
370                    h.process_listeners.shift_remove(&event);
371                }
372            });
373            Ok(with_host(|h| h.alloc(JsObj::Builtin("process".into()))))
374        }
375        "listeners" => {
376            let event = event_name(args);
377            Ok(with_host(|h| {
378                let l = h
379                    .process_listeners
380                    .get(&event)
381                    .map(|v| v.iter().map(|x| x.f.clone()).collect())
382                    .unwrap_or_default();
383                h.new_array(l)
384            }))
385        }
386        "emit" => {
387            let event = event_name(args);
388            let rest: Vec<Value> = args.iter().skip(1).cloned().collect();
389            let listeners = with_host(|h| h.take_process_listeners(&event));
390            let any = !listeners.is_empty();
391            let mut r = Ok(Value::Bool(any));
392            for f in listeners {
393                if let Err(e) = crate::host::invoke(&f, rest.clone(), None) {
394                    r = Err(e);
395                    break;
396                }
397            }
398            r
399        }
400        "emitWarning" => {
401            emit_warning_args(args);
402            Ok(Value::Undef)
403        }
404        // `process.exit([code])` really exits, and does so IMMEDIATELY — nothing
405        // after the call runs. It used to return `undefined` and let execution
406        // continue, which is a silent lie with teeth: the idiom
407        // `if (done) { server.close(); process.exit(0); }` (no `return`, because
408        // in Node none is needed) fell through to the statement after it. In a
409        // request-sequencing loop that meant re-entering the loop past the end of
410        // its array and destructuring `undefined`. Measured on node v26.7.0,
411        // `console.log('before'); process.exit(0); console.log('after')` prints
412        // only `before`; it printed both here.
413        //
414        // Under `--build`/`--dap`/embedding this is still a real process exit,
415        // exactly as it is in Node — there is no "exit but keep going" in the API.
416        // stdout/stderr are flushed first because `std::process::exit` runs no
417        // destructors.
418        //
419        // Port of Node's `process.exit`: an argument (even `undefined`) is
420        // ASSIGNED to `process.exitCode` first — through the validating setter,
421        // so `process.exit(3.7)` throws instead of exiting — then the `exit`
422        // event fires with the resulting code, then the process leaves. With no
423        // argument the already-set `process.exitCode` decides, which is why
424        // `process.exitCode = 3; process.exit()` exits 3 on node v26.7.0.
425        "exit" | "reallyExit" => {
426            if !args.is_empty() {
427                if let Err(e) = set_exit_code(&args[0]) {
428                    return Some(Err(e));
429                }
430            }
431            let code = with_host(|h| h.exit_code).unwrap_or(0);
432            if let Err(e) = emit_exit_event(code) {
433                return Some(Err(e));
434            }
435            // An `exit` listener may raise the code; re-read before leaving.
436            let code = with_host(|h| h.exit_code).unwrap_or(0);
437            use std::io::Write;
438            let _ = std::io::stdout().flush();
439            let _ = std::io::stderr().flush();
440            // `std::process::exit` runs no destructors, so the bytecode cache
441            // has to reach disk here too — otherwise a script that ends in
442            // `process.exit()` would recompile every module it loaded, every
443            // run, and never benefit from the cache at all.
444            crate::cache::flush();
445            std::process::exit(code);
446        }
447        // `process.chdir(dir)` really changes the working directory, and throws on
448        // failure; it used to silently do nothing, so every later relative path
449        // still resolved against the old directory.
450        "chdir" => {
451            let dir = super::arg_str(args, 0);
452            std::env::set_current_dir(&dir)
453                .map(|()| Value::Undef)
454                // Node reports the libuv message and BOTH directories:
455                // `ENOENT: no such file or directory, chdir <cwd> -> <dir>`.
456                // The old text spliced in Rust's `io::Error` Display, whose
457                // `No such file or directory (os error 2)` no Node ever printed.
458                .map_err(|e| {
459                    let from = std::env::current_dir()
460                        .map(|p| p.display().to_string())
461                        .unwrap_or_default();
462                    format!(
463                        "Error: {}, chdir '{from}' -> '{dir}'",
464                        crate::stdlib::fs::libuv_message(&e)
465                    )
466                })
467        }
468        // `process.kill(pid[, signal])` really signals the process. Node's default
469        // is SIGTERM, and a numeric or `'SIGxxx'` signal is accepted; signal `0`
470        // is the existence probe and sends nothing.
471        "kill" => {
472            let pid = with_host(|h| args.first().map(|v| h.to_number(v)).unwrap_or(0.0)) as i32;
473            let sig: Result<libc::c_int, String> = match args.get(1) {
474                Some(v) if !matches!(v, Value::Undef) => match with_host(|h| h.as_str(v)) {
475                    Some(name) => signal_number(&name).ok_or(crate::host::coded_error(
476                        "TypeError",
477                        "ERR_UNKNOWN_SIGNAL",
478                        &format!("Unknown signal: {name}"),
479                    )),
480                    None => Ok(with_host(|h| h.to_number(v)) as libc::c_int),
481                },
482                _ => Ok(libc::SIGTERM),
483            };
484            sig.and_then(|sig| {
485                // SAFETY: `kill` is a plain syscall on a pid/signal pair; it
486                // mutates no process memory and reports failure through `errno`.
487                if unsafe { libc::kill(pid, sig) } != 0 {
488                    Err(format!("Error: {}", std::io::Error::last_os_error()))
489                } else {
490                    Ok(Value::Undef)
491                }
492            })
493        }
494        // A genuine no-op: node-js emits no source maps, so enabling their use
495        // changes nothing. Returning `undefined` is the whole of Node's contract
496        // here, so this is not a stub.
497        "setSourceMapsEnabled" => Ok(Value::Undef),
498
499        // POSIX identity queries (libc; pure reads, always safe).
500        "getuid" => Ok(Value::Float(unsafe { libc::getuid() } as f64)),
501        "geteuid" => Ok(Value::Float(unsafe { libc::geteuid() } as f64)),
502        "getgid" => Ok(Value::Float(unsafe { libc::getgid() } as f64)),
503        "getegid" => Ok(Value::Float(unsafe { libc::getegid() } as f64)),
504        "getgroups" => {
505            let groups = supplementary_groups();
506            Ok(with_host(|h| {
507                h.new_array(groups.into_iter().map(Value::Float).collect())
508            }))
509        }
510
511        // POSIX identity mutation (libc; best-effort — silently ignored when the
512        // process lacks the privilege, matching a no-throw best-effort surface).
513        "setuid" | "seteuid" | "setgid" | "setegid" => {
514            let id = super::arg_num(args, 0);
515            if id.is_finite() {
516                let id = id as u32;
517                // SAFETY: id is a plain uid/gid number; a failed call just returns -1.
518                unsafe {
519                    match method {
520                        "setuid" => libc::setuid(id),
521                        "seteuid" => libc::seteuid(id),
522                        "setgid" => libc::setgid(id),
523                        _ => libc::setegid(id),
524                    };
525                }
526            }
527            Ok(Value::Undef)
528        }
529        "setgroups" => {
530            let groups = gid_array(args.first());
531            // SAFETY: `groups` is a valid gid buffer of the given length.
532            unsafe {
533                libc::setgroups(groups.len() as _, groups.as_ptr());
534            }
535            Ok(Value::Undef)
536        }
537        "initgroups" => {
538            let user = super::arg_str(args, 0);
539            let extra = super::arg_num(args, 1);
540            if let Ok(c) = std::ffi::CString::new(user) {
541                let gid = if extra.is_finite() { extra as u32 } else { 0 };
542                // SAFETY: `c` is NUL-terminated; a failed call just returns -1.
543                unsafe {
544                    libc::initgroups(c.as_ptr(), gid as _);
545                }
546            }
547            Ok(Value::Undef)
548        }
549
550        // `ref`/`unref` on the process object are chainable no-ops (no libuv
551        // handle refcount to touch); return the process namespace.
552        "ref" | "unref" => Ok(with_host(|h| h.alloc(JsObj::Builtin("process".into())))),
553        "abort" => std::process::abort(),
554        "getActiveResourcesInfo" => Ok(with_host(|h| h.new_array(Vec::new()))),
555        "resourceUsage" => Ok(resource_usage()),
556        "threadCpuUsage" => Ok(thread_cpu_usage()),
557        "availableMemory" | "constrainedMemory" => Ok(Value::Float(0.0)),
558        "getBuiltinModule" => {
559            let id = super::arg_str(args, 0);
560            let id = id.strip_prefix("node:").unwrap_or(&id);
561            match crate::stdlib::resolve(id) {
562                Some(ns) => Ok(with_host(|h| h.alloc(JsObj::Builtin(ns.to_string())))),
563                None => Ok(Value::Undef),
564            }
565        }
566        "openStdin" => Ok(std_stream(0)),
567
568        "hasUncaughtExceptionCaptureCallback" => {
569            Ok(Value::Bool(UNCAUGHT_CAPTURE.with(|c| c.borrow().is_some())))
570        }
571        "setUncaughtExceptionCaptureCallback" => {
572            let cb = args.first().cloned().unwrap_or(Value::Undef);
573            let clear = matches!(cb, Value::Undef) || with_host(|h| h.is_null(&cb));
574            if clear {
575                UNCAUGHT_CAPTURE.with(|c| *c.borrow_mut() = None);
576            } else if UNCAUGHT_CAPTURE.with(|c| c.borrow().is_some()) {
577                return Some(Err(crate::host::type_error(
578                    "`process.setUncaughtExceptionCaptureCallback()` was called \
579                     while a capture callback was already active",
580                )));
581            } else {
582                UNCAUGHT_CAPTURE.with(|c| *c.borrow_mut() = Some(cb));
583            }
584            Ok(Value::Undef)
585        }
586        "addUncaughtExceptionCaptureCallback" => {
587            let cb = args.first().cloned().unwrap_or(Value::Undef);
588            if !matches!(cb, Value::Undef) {
589                UNCAUGHT_CAPTURE.with(|c| *c.borrow_mut() = Some(cb));
590            }
591            Ok(Value::Undef)
592        }
593        "execve" => exec_ve(args),
594        "loadEnvFile" => load_env_file(&super::arg_str(args, 0)),
595        _ => return None,
596    })
597}
598
599/// `process.env` as a plain object built from the real environment.
600fn env_object() -> Value {
601    with_host(|h| {
602        let mut m = IndexMap::new();
603        for (k, v) in std::env::vars() {
604            m.insert(k, h.new_str(v));
605        }
606        h.new_object(m)
607    })
608}
609
610/// The `(execArgv, argv)` split installed by the binary's entry point.
611///
612/// Unset when the library is embedded (or driven by a sibling binary such as
613/// `parity-fuzz`, whose own command line is not a `node` command line), and the
614/// accessors below then fall back to the raw process arguments.
615static ARGV: std::sync::OnceLock<(Vec<String>, Vec<String>)> = std::sync::OnceLock::new();
616
617/// Publish Node's `process.argv` / `process.execArgv` split for this run, read
618/// off the real command line. Called once by the `node` binary's entry point;
619/// first call wins.
620///
621/// `argv[1]` is the entry script RESOLVED against the current directory, so
622/// `node ./x.js` reports the same absolute path `path.resolve` would — not the
623/// spelling that was typed.
624pub fn install_argv() {
625    let split = crate::cli::split_argv(std::env::args());
626    let mut argv = vec![exec_path()];
627    if let Some(s) = &split.script {
628        // `-` is Node's stdin entry point, not a path; it stays verbatim.
629        argv.push(if s == "-" {
630            s.clone()
631        } else {
632            super::path::resolve_one(s)
633        });
634    }
635    argv.extend(split.user);
636    let _ = ARGV.set((split.exec, argv));
637}
638
639/// `process.argv`: `[execPath, entryScript, ...userArgs]`.
640///
641/// The runtime's OWN flags are not in it — they are `process.execArgv` — and
642/// under `-e` there is no `argv[1]` at all. Returning the raw OS arguments put
643/// `-e` and the whole one-liner source into `argv`, which is what any script
644/// that reads `process.argv.slice(2)` for its options would have parsed.
645fn argv() -> Value {
646    with_host(|h| {
647        let items: Vec<Value> = match ARGV.get() {
648            Some((_, argv)) => argv.iter().map(|a| h.new_str(a.clone())).collect(),
649            None => std::env::args().map(|a| h.new_str(a)).collect(),
650        };
651        h.new_array(items)
652    })
653}
654
655/// `process.execArgv`: the runtime flags, `-e`/`--eval` and its source included.
656fn exec_argv() -> Value {
657    with_host(|h| {
658        let items: Vec<Value> = ARGV
659            .get()
660            .map(|(e, _)| e.iter().map(|a| h.new_str(a.clone())).collect())
661            .unwrap_or_default();
662        h.new_array(items)
663    })
664}
665
666fn exec_path() -> String {
667    std::env::current_exe()
668        .map(|p| p.to_string_lossy().into_owned())
669        .unwrap_or_else(|_| "node".into())
670}
671
672/// `process.versions` — a small map; only `node` is commonly gated on.
673fn versions() -> Value {
674    with_host(|h| {
675        let mut m = IndexMap::new();
676        m.insert("node".into(), h.new_str("26.5.0"));
677        m.insert("v8".into(), h.new_str("0.0.0"));
678        h.new_object(m)
679    })
680}
681
682/// A minimal `process.stdout`/`stderr`/`stdin` stand-in: enough surface
683/// (`fd`, `isTTY`, `writable`, a `write`) for load-time probes like
684/// `tty.isatty(process.stderr.fd)`.
685fn std_stream(fd: i32) -> Value {
686    with_host(|h| {
687        let mut m = IndexMap::new();
688        m.insert("@@native".into(), h.new_str("WriteStream"));
689        m.insert("fd".into(), Value::Float(fd as f64));
690        // SAFETY: isatty is a pure query on the fd number.
691        let is_tty = unsafe { libc::isatty(fd) == 1 };
692        m.insert("isTTY".into(), Value::Bool(is_tty));
693        m.insert("writable".into(), Value::Bool(fd != 0));
694        m.insert("readable".into(), Value::Bool(fd == 0));
695        // A tty stream exposes its terminal dimensions (real ioctl reading).
696        if is_tty {
697            if let Some((cols, rows)) = super::tty::window_size(fd) {
698                m.insert("columns".into(), Value::Float(cols as f64));
699                m.insert("rows".into(), Value::Float(rows as f64));
700            }
701        }
702        h.new_object(m)
703    })
704}
705
706/// Instance methods of a `process.stdout`/`stderr` `WriteStream`: `write`/`end`
707/// emit the chunk raw (no newline) to the stream's fd, so ordering interleaves
708/// correctly with `console.log`.
709pub fn stream_instance_call(recv: &Value, method: &str, args: &[Value]) -> Result<Value, String> {
710    match method {
711        "write" | "end" => {
712            let fd = with_host(|h| match h.get(recv) {
713                Some(JsObj::Object(p)) => p.get("fd").map(|v| h.to_number(v)).unwrap_or(1.0),
714                _ => 1.0,
715            });
716            // `end()` with no chunk closes without writing; `write()` with no
717            // chunk is the argument error below.
718            if method == "end" && args.first().map(|v| matches!(v, Value::Undef)) != Some(false) {
719                return Ok(Value::Bool(true));
720            }
721            let bytes = chunk_bytes(args)?;
722            with_host(|h| h.write_out_bytes(&bytes, fd == 2.0));
723            Ok(Value::Bool(true))
724        }
725        // A no-op stream surface so `.on('data')`/`.once`/`.end()` chaining loads.
726        "on" | "once" | "removeListener" | "cork" | "uncork" | "setEncoding" => Ok(recv.clone()),
727        // `tty.WriteStream` cursor/erase control — emit the corresponding ANSI
728        // escape to the stream's fd (best-effort; only meaningful on a real tty).
729        "cursorTo" | "moveCursor" | "clearLine" | "clearScreenDown" => {
730            let seq = tty_control(method, args);
731            write_fd(stream_fd(recv), seq.as_bytes());
732            Ok(Value::Bool(true))
733        }
734        "getWindowSize" => {
735            let (c, r) = super::tty::window_size(stream_fd(recv) as i32).unwrap_or((80, 24));
736            Ok(with_host(|h| {
737                h.new_array(vec![Value::Float(c as f64), Value::Float(r as f64)])
738            }))
739        }
740        // A truecolor terminal advertises 24-bit depth; hasColors(count) is true
741        // for any request within that range.
742        "getColorDepth" => Ok(Value::Float(24.0)),
743        "hasColors" => Ok(Value::Bool(true)),
744        _ => Err(crate::host::type_error(&format!(
745            "{method} is not a function"
746        ))),
747    }
748}
749
750fn hrtime(args: &[Value]) -> Value {
751    let now = std::time::SystemTime::now()
752        .duration_since(std::time::UNIX_EPOCH)
753        .unwrap_or_default();
754    let (mut secs, mut nanos) = (now.as_secs() as f64, now.subsec_nanos() as f64);
755    // `hrtime(prev)` returns the diff from a prior reading.
756    if let Some(Value::Obj(_)) = args.first() {
757        if let Some(prev) = with_host(|h| match h.get(&args[0]) {
758            Some(JsObj::Array(a)) if a.len() == 2 => Some((h.to_number(&a[0]), h.to_number(&a[1]))),
759            _ => None,
760        }) {
761            secs -= prev.0;
762            nanos -= prev.1;
763        }
764    }
765    with_host(|h| h.new_array(vec![Value::Float(secs), Value::Float(nanos)]))
766}
767
768fn memory_usage() -> Value {
769    with_host(|h| {
770        let mut m = IndexMap::new();
771        for k in ["rss", "heapTotal", "heapUsed", "external", "arrayBuffers"] {
772            m.insert(k.into(), Value::Float(0.0));
773        }
774        h.new_object(m)
775    })
776}
777
778/// Emit `process.on('exit', code)` exactly once per process, the way Node's
779/// `process._exiting` latch does — `process.exit()` inside an `exit` handler
780/// must not re-enter it.
781///
782/// The handlers run SYNCHRONOUSLY and nothing they schedule ever runs: Node
783/// leaves the loop straight after them, so a `setTimeout` or `.then` queued
784/// here is dropped. An `exit` listener may still raise `process.exitCode`, and
785/// that later value is the one the process uses, which is why the caller reads
786/// the slot back after this returns.
787pub fn emit_exit_event(code: i32) -> Result<(), String> {
788    if with_host(|h| std::mem::replace(&mut h.exiting, true)) {
789        return Ok(());
790    }
791    let listeners = with_host(|h| h.take_process_listeners("exit"));
792    for f in listeners {
793        crate::host::invoke(&f, vec![Value::Float(code as f64)], None)?;
794    }
795    Ok(())
796}
797
798/// Emit `process.on('beforeExit', code)`. Node fires this when the loop has
799/// drained but the process has NOT been told to exit, and — unlike `exit` —
800/// work scheduled from a handler is honoured, so the loop runs again and
801/// `beforeExit` can fire repeatedly. It never fires after an explicit
802/// `process.exit()` or an uncaught exception.
803///
804/// Reports whether any listener ran, so the caller knows to re-drain.
805pub fn emit_before_exit(code: i32) -> Result<bool, String> {
806    let listeners = with_host(|h| h.take_process_listeners("beforeExit"));
807    let any = !listeners.is_empty();
808    for f in listeners {
809        crate::host::invoke(&f, vec![Value::Float(code as f64)], None)?;
810    }
811    Ok(any)
812}
813
814/// The bytes a `stream.write(chunk[, encoding])` call puts on the wire.
815///
816/// Node writes a `Buffer`/`TypedArray`/`DataView` chunk through UNTOUCHED, and
817/// decodes a string chunk with the named encoding (default `utf8`). Both were
818/// funnelled through `ToString` here, which is lossy in two separate ways:
819/// `process.stdout.write(Buffer.from([0xff,0xfe,0x41]))` printed the 15 bytes of
820/// `[object Object]` instead of `ff fe 41`, and even once the Buffer path
821/// existed, a `String` round-trip would have replaced each non-UTF-8 byte with
822/// `U+FFFD` (3 bytes out, 7 bytes on the wire). `write("4142","hex")` likewise
823/// printed the four characters of the literal instead of the two bytes `AB`.
824///
825/// Anything that is neither a string nor a byte view is the same
826/// `ERR_INVALID_ARG_TYPE` Node raises — a JS array of byte values included,
827/// which is why this does NOT reuse `buffer::bytes_like` (that helper
828/// deliberately accepts plain arrays, which `write` rejects).
829fn chunk_bytes(args: &[Value]) -> Result<Vec<u8>, String> {
830    let chunk = args.first().cloned().unwrap_or(Value::Undef);
831    if with_host(|h| h.is_null(&chunk)) {
832        return Err(crate::host::type_error(
833            "May not write null values to stream",
834        ));
835    }
836    if let Some(s) = with_host(|h| h.as_str(&chunk)) {
837        let enc = match args.get(1) {
838            Some(v) if !matches!(v, Value::Undef) => with_host(|h| h.str_of(v)),
839            _ => "utf8".to_string(),
840        };
841        return Ok(super::buffer::decode_str(&s, &enc));
842    }
843    match super::native_tag(&chunk).as_deref() {
844        Some("Buffer") | Some("TypedArray") | Some("DataView") => {
845            Ok(super::buffer::bytes_like(&chunk).unwrap_or_default())
846        }
847        _ => Err(crate::host::type_error(&format!(
848            "The \"chunk\" argument must be of type string or an instance of \
849             Buffer, TypedArray, or DataView. Received {}",
850            super::received_desc(&chunk)
851        ))),
852    }
853}
854
855/// The `fd` numeric property of a stream stand-in (default stdout).
856fn stream_fd(recv: &Value) -> f64 {
857    with_host(|h| match h.get(recv) {
858        Some(JsObj::Object(p)) => p.get("fd").map(|v| h.to_number(v)).unwrap_or(1.0),
859        _ => 1.0,
860    })
861}
862
863/// Write raw bytes as program output on stdout/stderr (chosen by fd) — through
864/// the host funnel, so an embedder capturing output receives these too.
865fn write_fd(fd: f64, bytes: &[u8]) {
866    let text = String::from_utf8_lossy(bytes).into_owned();
867    with_host(|h| h.write_out(&text, fd == 2.0));
868}
869
870/// The ANSI control sequence for a `tty.WriteStream` cursor/erase method.
871fn tty_control(method: &str, args: &[Value]) -> String {
872    match method {
873        // cursorTo(x[, y]) → absolute column (`\e[<x+1>G`) or position.
874        "cursorTo" => {
875            let x = super::arg_num(args, 0);
876            let y = super::arg_num(args, 1);
877            let x = if x.is_finite() { x as i64 } else { 0 };
878            if y.is_finite() {
879                format!("\x1b[{};{}H", y as i64 + 1, x + 1)
880            } else {
881                format!("\x1b[{}G", x + 1)
882            }
883        }
884        // moveCursor(dx, dy) → relative moves.
885        "moveCursor" => {
886            let dx = super::arg_num(args, 0);
887            let dy = super::arg_num(args, 1);
888            let mut s = String::new();
889            let dx = if dx.is_finite() { dx as i64 } else { 0 };
890            let dy = if dy.is_finite() { dy as i64 } else { 0 };
891            if dx > 0 {
892                s.push_str(&format!("\x1b[{dx}C"));
893            } else if dx < 0 {
894                s.push_str(&format!("\x1b[{}D", -dx));
895            }
896            if dy > 0 {
897                s.push_str(&format!("\x1b[{dy}B"));
898            } else if dy < 0 {
899                s.push_str(&format!("\x1b[{}A", -dy));
900            }
901            s
902        }
903        // clearLine(dir): -1 left, 1 right, 0 whole line.
904        "clearLine" => match super::arg_num(args, 0) {
905            d if d < 0.0 => "\x1b[1K".into(),
906            d if d > 0.0 => "\x1b[0K".into(),
907            _ => "\x1b[2K".into(),
908        },
909        // clearScreenDown → erase from cursor to end of screen.
910        _ => "\x1b[0J".into(),
911    }
912}
913
914/// The process's supplementary group ids (`getgroups(2)`).
915fn supplementary_groups() -> Vec<f64> {
916    // SAFETY: first call queries the count, second fills a buffer of that size.
917    unsafe {
918        let n = libc::getgroups(0, std::ptr::null_mut());
919        if n <= 0 {
920            return Vec::new();
921        }
922        let mut buf = vec![0 as libc::gid_t; n as usize];
923        let filled = libc::getgroups(n, buf.as_mut_ptr());
924        if filled < 0 {
925            return Vec::new();
926        }
927        buf.truncate(filled as usize);
928        buf.into_iter().map(|g| g as f64).collect()
929    }
930}
931
932/// Read a JS array of numbers as a gid buffer.
933fn gid_array(v: Option<&Value>) -> Vec<libc::gid_t> {
934    let Some(v) = v else { return Vec::new() };
935    with_host(|h| match h.get(v) {
936        Some(JsObj::Array(a)) => a.iter().map(|x| h.to_number(x) as libc::gid_t).collect(),
937        _ => Vec::new(),
938    })
939}
940
941/// `getrusage(RUSAGE_SELF)` — `None` if the syscall fails.
942fn get_rusage() -> Option<libc::rusage> {
943    // SAFETY: getrusage fills a zeroed rusage; RUSAGE_SELF is a valid `who`.
944    unsafe {
945        let mut ru: libc::rusage = std::mem::zeroed();
946        (libc::getrusage(libc::RUSAGE_SELF, &mut ru) == 0).then_some(ru)
947    }
948}
949
950/// microseconds from a `timeval`.
951fn tv_micros(t: &libc::timeval) -> f64 {
952    t.tv_sec as f64 * 1e6 + t.tv_usec as f64
953}
954
955/// `process.resourceUsage()` — the full `getrusage` breakdown (zeros on failure).
956fn resource_usage() -> Value {
957    let ru = get_rusage();
958    with_host(|h| {
959        let mut m = IndexMap::new();
960        let (utime, stime) = ru
961            .as_ref()
962            .map(|r| (tv_micros(&r.ru_utime), tv_micros(&r.ru_stime)))
963            .unwrap_or((0.0, 0.0));
964        m.insert("userCPUTime".into(), Value::Float(utime));
965        m.insert("systemCPUTime".into(), Value::Float(stime));
966        let fields = [
967            ("maxRSS", ru.as_ref().map(|r| r.ru_maxrss)),
968            ("sharedMemorySize", ru.as_ref().map(|r| r.ru_ixrss)),
969            ("unsharedDataSize", ru.as_ref().map(|r| r.ru_idrss)),
970            ("unsharedStackSize", ru.as_ref().map(|r| r.ru_isrss)),
971            ("minorPageFault", ru.as_ref().map(|r| r.ru_minflt)),
972            ("majorPageFault", ru.as_ref().map(|r| r.ru_majflt)),
973            ("swappedOut", ru.as_ref().map(|r| r.ru_nswap)),
974            ("fsRead", ru.as_ref().map(|r| r.ru_inblock)),
975            ("fsWrite", ru.as_ref().map(|r| r.ru_oublock)),
976            ("ipcSent", ru.as_ref().map(|r| r.ru_msgsnd)),
977            ("ipcReceived", ru.as_ref().map(|r| r.ru_msgrcv)),
978            ("signalsCount", ru.as_ref().map(|r| r.ru_nsignals)),
979            ("voluntaryContextSwitches", ru.as_ref().map(|r| r.ru_nvcsw)),
980            (
981                "involuntaryContextSwitches",
982                ru.as_ref().map(|r| r.ru_nivcsw),
983            ),
984        ];
985        for (k, v) in fields {
986            m.insert(k.into(), Value::Float(v.unwrap_or(0) as f64));
987        }
988        h.new_object(m)
989    })
990}
991
992/// `process.threadCpuUsage()` — best-effort via process-wide `getrusage` (no
993/// per-thread accounting substrate), reported as `{user, system}` microseconds.
994fn thread_cpu_usage() -> Value {
995    let (u, s) = get_rusage()
996        .map(|r| (tv_micros(&r.ru_utime), tv_micros(&r.ru_stime)))
997        .unwrap_or((0.0, 0.0));
998    with_host(|h| {
999        let mut m = IndexMap::new();
1000        m.insert("user".into(), Value::Float(u));
1001        m.insert("system".into(), Value::Float(s));
1002        h.new_object(m)
1003    })
1004}
1005
1006/// `process.execve(file, args[, env])` — replace the process image (never returns
1007/// on success; throws the OS error otherwise).
1008fn exec_ve(args: &[Value]) -> Result<Value, String> {
1009    use std::ffi::CString;
1010    let prog = CString::new(super::arg_str(args, 0))
1011        .map_err(|_| crate::host::type_error("process.execve: invalid file path"))?;
1012
1013    let argv_strs: Vec<String> = with_host(|h| match args.get(1).and_then(|v| h.get(v)) {
1014        Some(JsObj::Array(a)) => a.iter().map(|x| h.str_of(x)).collect(),
1015        _ => Vec::new(),
1016    });
1017    let env_strs: Vec<String> = {
1018        let from_arg = with_host(|h| match args.get(2).and_then(|v| h.get(v)) {
1019            Some(JsObj::Object(p)) => Some(
1020                p.iter()
1021                    .map(|(k, v)| format!("{k}={}", h.str_of(v)))
1022                    .collect::<Vec<_>>(),
1023            ),
1024            _ => None,
1025        });
1026        from_arg.unwrap_or_else(|| std::env::vars().map(|(k, v)| format!("{k}={v}")).collect())
1027    };
1028
1029    let to_c = |s: String| {
1030        CString::new(s).map_err(|_| crate::host::type_error("process.execve: NUL in argument"))
1031    };
1032    let argv_c: Vec<CString> = argv_strs.into_iter().map(to_c).collect::<Result<_, _>>()?;
1033    let env_c: Vec<CString> = env_strs.into_iter().map(to_c).collect::<Result<_, _>>()?;
1034
1035    let mut argv_p: Vec<*const libc::c_char> = argv_c.iter().map(|c| c.as_ptr()).collect();
1036    argv_p.push(std::ptr::null());
1037    let mut envp_p: Vec<*const libc::c_char> = env_c.iter().map(|c| c.as_ptr()).collect();
1038    envp_p.push(std::ptr::null());
1039
1040    // SAFETY: argv/envp are NUL-terminated arrays of valid C strings kept alive
1041    // above; on success execve never returns.
1042    unsafe {
1043        libc::execve(prog.as_ptr(), argv_p.as_ptr(), envp_p.as_ptr());
1044    }
1045    Err(crate::host::type_error(&format!(
1046        "process.execve failed: {}",
1047        std::io::Error::last_os_error()
1048    )))
1049}
1050
1051/// `process.loadEnvFile([path])` — parse a `.env` file into `process.env`
1052/// (persisted through the real environment so a later `process.env` read sees it).
1053fn load_env_file(path: &str) -> Result<Value, String> {
1054    let path = if path.is_empty() { ".env" } else { path };
1055    let text =
1056        std::fs::read_to_string(path).map_err(|e| format!("Error: ENOENT: {e}, open '{path}'"))?;
1057    for line in text.lines() {
1058        let line = line.trim();
1059        if line.is_empty() || line.starts_with('#') {
1060            continue;
1061        }
1062        let line = line.strip_prefix("export ").unwrap_or(line);
1063        let Some((key, val)) = line.split_once('=') else {
1064            continue;
1065        };
1066        let key = key.trim();
1067        if key.is_empty() {
1068            continue;
1069        }
1070        let mut val = val.trim();
1071        if val.len() >= 2
1072            && ((val.starts_with('"') && val.ends_with('"'))
1073                || (val.starts_with('\'') && val.ends_with('\'')))
1074        {
1075            val = &val[1..val.len() - 1];
1076        }
1077        std::env::set_var(key, val);
1078    }
1079    Ok(Value::Undef)
1080}
1081
1082/// The event name argument of an EventEmitter-style `process` call.
1083fn event_name(args: &[Value]) -> String {
1084    args.first()
1085        .map(|v| with_host(|h| h.str_of(v)))
1086        .unwrap_or_default()
1087}