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
214/// `process.features` — what this runtime can do.
215fn features() -> Value {
216    with_host(|h| {
217        let mut m = IndexMap::new();
218        for (k, v) in [
219            ("inspector", false),
220            ("debug", false),
221            ("uv", false),
222            ("ipv6", true),
223            ("tls_alpn", false),
224            ("tls_sni", false),
225            ("tls_ocsp", false),
226            ("tls", true),
227            ("openssl_is_boringssl", false),
228            ("cached_builtins", true),
229            ("require_module", true),
230            ("quic", false),
231        ] {
232            m.insert(k.to_string(), Value::Bool(v));
233        }
234        // A string, not a boolean, in node too: it names the TypeScript mode.
235        let ts = h.new_str("none");
236        m.insert("typescript".into(), ts);
237        h.new_object(m)
238    })
239}
240
241/// `process.config`.
242fn config() -> Value {
243    with_host(|h| {
244        let mut vars = IndexMap::new();
245        let arch = h.new_str(super::os::arch());
246        let plat = h.new_str(super::os::platform());
247        vars.insert("host_arch".to_string(), arch.clone());
248        vars.insert("target_arch".to_string(), arch);
249        vars.insert("node_shared".to_string(), Value::Bool(false));
250        vars.insert("node_use_openssl".to_string(), Value::Bool(false));
251        vars.insert("v8_enable_i18n_support".to_string(), Value::Bool(false));
252        vars.insert("node_platform".to_string(), plat);
253        let variables = h.new_object(vars);
254        let defaults = h.new_object(IndexMap::new());
255        let mut m = IndexMap::new();
256        m.insert("target_defaults".to_string(), defaults);
257        m.insert("variables".to_string(), variables);
258        h.new_object(m)
259    })
260}
261
262/// The `NODE_OPTIONS` flags this runtime accepts, as a `Set`.
263fn allowed_flags() -> Value {
264    let flags = [
265        "--enable-source-maps",
266        "--max-old-space-size",
267        "--no-warnings",
268        "--preserve-symlinks",
269        "--stack-trace-limit",
270        "--throw-deprecation",
271        "--trace-warnings",
272        "--unhandled-rejections",
273        "--zero-fill-buffers",
274    ];
275    let vals: Vec<Value> = flags.iter().map(|f| with_host(|h| h.new_str(*f))).collect();
276    let set = with_host(|h| {
277        h.alloc(crate::host::JsObj::Set {
278            entries: indexmap::IndexMap::new(),
279            weak: false,
280        })
281    });
282    for v in vals {
283        let _ = crate::host::call_method(&set, "add", vec![v]);
284    }
285    set
286}
287
288pub fn constant(name: &str) -> Option<Value> {
289    Some(match name {
290        "env" => memo("env", env_object),
291        // `process.release` carried nothing, so the common
292        // `process.release.name === 'node'` probe threw on `undefined.name`.
293        // Only `name` is reported: it is consistent with the node version this
294        // already claims through `process.version`, whereas the `sourceUrl` and
295        // `headersUrl` node also carries would point at a release tarball that
296        // does not exist for this engine.
297        "release" => memo("release", || {
298            with_host(|h| {
299                let mut m = IndexMap::new();
300                let name = h.new_str("node");
301                m.insert("name".into(), name);
302                h.new_object(m)
303            })
304        }),
305        "argv" => memo("argv", argv),
306        "argv0" => with_host(|h| h.new_str(exec_path())),
307        "execPath" => with_host(|h| h.new_str(exec_path())),
308        "execArgv" => memo("execArgv", exec_argv),
309        "platform" => with_host(|h| h.new_str(super::os::platform())),
310        "arch" => with_host(|h| h.new_str(super::os::arch())),
311        "pid" => Value::Float(std::process::id() as f64),
312        "ppid" => Value::Float(0.0),
313        "title" => with_host(|h| h.new_str("node")),
314        // A best-effort Node-compatible version string. Kept low so a dep's
315        // `if (semver.lt(process.version, ...))` gate takes the conservative path.
316        "version" => with_host(|h| h.new_str("v26.5.0")),
317        "versions" => memo("versions", versions),
318        // A capability map tooling probes before reaching for an optional API.
319        // It reports what THIS runtime supports, not what node's own build
320        // does — claiming a feature that is not here would defeat the point of
321        // the probe. It was absent entirely, so `process.features.X` threw on
322        // `undefined`.
323        "features" => memo("features", features),
324        // Node's shape is `{ target_defaults, variables }`; the contents
325        // describe the build. Only what is true of this build is reported —
326        // there is no configure step to echo.
327        "config" => memo("config", config),
328        // The flags this runtime accepts in `NODE_OPTIONS`, as the Set-like
329        // node exposes. `process.allowedNodeEnvironmentFlags.has(f)` used to
330        // throw: the value was undefined.
331        "allowedNodeEnvironmentFlags" => memo("allowedNodeEnvironmentFlags", allowed_flags),
332        "stdout" => memo("stdout", || std_stream(1)),
333        "stderr" => memo("stderr", || std_stream(2)),
334        "stdin" => memo("stdin", || std_stream(0)),
335        // Unset reads back as `undefined`, not `0` — `process.exitCode` starts
336        // life absent and a script may test for that.
337        "exitCode" => match with_host(|h| h.exit_code) {
338            Some(c) => Value::Float(c as f64),
339            None => Value::Undef,
340        },
341        _ => return None,
342    })
343}
344
345/// The `process.exitCode` setter, ported from Node's
346/// `lib/internal/bootstrap/node.js` accessor:
347///
348/// ```js
349/// set(code) {
350///   if (code !== null && code !== undefined) {
351///     let value = code;
352///     if (typeof code === 'string' && code !== '' &&
353///       NumberIsNaN((value = Number(code)))) {
354///       value = code;
355///     }
356///     validateInteger(value, 'code');
357///     …
358///   } else { /* clear */ }
359/// }
360/// ```
361///
362/// So a NUMERIC string is accepted and coerced (`"3"` → 3, `"0x10"` → 16,
363/// `"  "` → 0), a non-numeric or empty string keeps its string identity and
364/// fails `validateInteger` as a TYPE error, a non-integer number fails as a
365/// RANGE error, and `null`/`undefined` clear the slot. Verified on node
366/// v26.7.0: `process.exitCode = "0x10"` exits 16, `= 3.7` throws
367/// `ERR_OUT_OF_RANGE`, `= ""` throws `ERR_INVALID_ARG_TYPE`, `= "  "` exits 0.
368pub fn set_exit_code(val: &Value) -> Result<(), String> {
369    if matches!(val, Value::Undef) || with_host(|h| h.is_null(val)) {
370        with_host(|h| h.exit_code = None);
371        return Ok(());
372    }
373    // A numeric string coerces; anything else keeps its own type for the error.
374    let numeric = match with_host(|h| h.as_str(val)) {
375        Some(s) if !s.is_empty() => {
376            let n = with_host(|h| h.to_number(val));
377            if n.is_nan() {
378                None
379            } else {
380                Some(n)
381            }
382        }
383        Some(_) => None,
384        None => match val {
385            Value::Float(_) | Value::Int(_) => Some(with_host(|h| h.to_number(val))),
386            _ => None,
387        },
388    };
389    match numeric {
390        Some(n) if n.fract() == 0.0 && n.is_finite() => {
391            with_host(|h| h.exit_code = Some(n as i32));
392            Ok(())
393        }
394        Some(n) => Err(crate::host::coded_error(
395            "RangeError",
396            "ERR_OUT_OF_RANGE",
397            &format!(
398                "The value of \"code\" is out of range. It must be an integer. Received {}",
399                crate::host::fmt_number(n)
400            ),
401        )),
402        None => Err(crate::host::invalid_arg_type(
403            "code", "argument", "number", val,
404        )),
405    }
406}
407
408pub fn call(method: &str, args: &[Value]) -> Option<Result<Value, String>> {
409    Some(match method {
410        "cwd" => {
411            let d = std::env::current_dir()
412                .map(|p| p.to_string_lossy().into_owned())
413                .unwrap_or_default();
414            Ok(with_host(|h| h.new_str(d)))
415        }
416        // `hrtime()` → `[seconds, nanoseconds]` since an arbitrary epoch (here the
417        // monotonic clock via `Instant` is unavailable statically, so use the
418        // system clock — sufficient for the timing scaffolding deps set up).
419        "hrtime" => Ok(hrtime(args)),
420        // Nanoseconds since an arbitrary epoch, as a BigInt.
421        "hrtime.bigint" => Ok(with_host(|h| {
422            let now = std::time::SystemTime::now()
423                .duration_since(std::time::UNIX_EPOCH)
424                .unwrap_or_default();
425            h.new_bigint(num_bigint::BigInt::from(now.as_nanos()))
426        })),
427        "uptime" => Ok(Value::Float(0.0)),
428        "memoryUsage" => Ok(memory_usage()),
429        "cpuUsage" => Ok(with_host(|h| {
430            let mut m = IndexMap::new();
431            m.insert("user".into(), Value::Float(0.0));
432            m.insert("system".into(), Value::Float(0.0));
433            h.new_object(m)
434        })),
435        "umask" => Ok(Value::Float(0.0)),
436        "binding" => Err(crate::host::type_error("process.binding is not supported")),
437        // EventEmitter-style registration. Listeners are REMEMBERED (the runtime
438        // emits `unhandledRejection`; signals still never fire), and every form
439        // returns the process namespace so `.on(...).on(...)` chains work.
440        "on" | "once" | "addListener" => {
441            let (event, f) = (event_name(args), args.get(1).cloned());
442            if let Some(f) = f {
443                let once = method == "once";
444                with_host(|h| {
445                    h.process_listeners
446                        .entry(event)
447                        .or_default()
448                        .push(crate::host::ProcListener { f, once })
449                });
450            }
451            Ok(with_host(|h| h.alloc(JsObj::Builtin("process".into()))))
452        }
453        "off" | "removeListener" => {
454            let (event, f) = (event_name(args), args.get(1).cloned());
455            if let Some(f) = f {
456                with_host(|h| {
457                    if let Some(l) = h.process_listeners.get_mut(&event) {
458                        if let Some(i) = l.iter().position(|x| x.f == f) {
459                            l.remove(i);
460                        }
461                    }
462                });
463            }
464            Ok(with_host(|h| h.alloc(JsObj::Builtin("process".into()))))
465        }
466        "removeAllListeners" => {
467            let event = event_name(args);
468            with_host(|h| {
469                if event.is_empty() {
470                    h.process_listeners.clear();
471                } else {
472                    h.process_listeners.shift_remove(&event);
473                }
474            });
475            Ok(with_host(|h| h.alloc(JsObj::Builtin("process".into()))))
476        }
477        "listeners" => {
478            let event = event_name(args);
479            Ok(with_host(|h| {
480                let l = h
481                    .process_listeners
482                    .get(&event)
483                    .map(|v| v.iter().map(|x| x.f.clone()).collect())
484                    .unwrap_or_default();
485                h.new_array(l)
486            }))
487        }
488        "emit" => {
489            let event = event_name(args);
490            let rest: Vec<Value> = args.iter().skip(1).cloned().collect();
491            let listeners = with_host(|h| h.take_process_listeners(&event));
492            let any = !listeners.is_empty();
493            let mut r = Ok(Value::Bool(any));
494            for f in listeners {
495                if let Err(e) = crate::host::invoke(&f, rest.clone(), None) {
496                    r = Err(e);
497                    break;
498                }
499            }
500            r
501        }
502        "emitWarning" => {
503            emit_warning_args(args);
504            Ok(Value::Undef)
505        }
506        // `process.exit([code])` really exits, and does so IMMEDIATELY — nothing
507        // after the call runs. It used to return `undefined` and let execution
508        // continue, which is a silent lie with teeth: the idiom
509        // `if (done) { server.close(); process.exit(0); }` (no `return`, because
510        // in Node none is needed) fell through to the statement after it. In a
511        // request-sequencing loop that meant re-entering the loop past the end of
512        // its array and destructuring `undefined`. Measured on node v26.7.0,
513        // `console.log('before'); process.exit(0); console.log('after')` prints
514        // only `before`; it printed both here.
515        //
516        // Under `--build`/`--dap`/embedding this is still a real process exit,
517        // exactly as it is in Node — there is no "exit but keep going" in the API.
518        // stdout/stderr are flushed first because `std::process::exit` runs no
519        // destructors.
520        //
521        // Port of Node's `process.exit`: an argument (even `undefined`) is
522        // ASSIGNED to `process.exitCode` first — through the validating setter,
523        // so `process.exit(3.7)` throws instead of exiting — then the `exit`
524        // event fires with the resulting code, then the process leaves. With no
525        // argument the already-set `process.exitCode` decides, which is why
526        // `process.exitCode = 3; process.exit()` exits 3 on node v26.7.0.
527        "exit" | "reallyExit" => {
528            if !args.is_empty() {
529                if let Err(e) = set_exit_code(&args[0]) {
530                    return Some(Err(e));
531                }
532            }
533            let code = with_host(|h| h.exit_code).unwrap_or(0);
534            if let Err(e) = emit_exit_event(code) {
535                return Some(Err(e));
536            }
537            // An `exit` listener may raise the code; re-read before leaving.
538            let code = with_host(|h| h.exit_code).unwrap_or(0);
539            use std::io::Write;
540            let _ = std::io::stdout().flush();
541            let _ = std::io::stderr().flush();
542            // `std::process::exit` runs no destructors, so the bytecode cache
543            // has to reach disk here too — otherwise a script that ends in
544            // `process.exit()` would recompile every module it loaded, every
545            // run, and never benefit from the cache at all.
546            crate::cache::flush();
547            std::process::exit(code);
548        }
549        // `process.chdir(dir)` really changes the working directory, and throws on
550        // failure; it used to silently do nothing, so every later relative path
551        // still resolved against the old directory.
552        "chdir" => {
553            let dir = super::arg_str(args, 0);
554            std::env::set_current_dir(&dir)
555                .map(|()| Value::Undef)
556                // Node reports the libuv message and BOTH directories:
557                // `ENOENT: no such file or directory, chdir <cwd> -> <dir>`.
558                // The old text spliced in Rust's `io::Error` Display, whose
559                // `No such file or directory (os error 2)` no Node ever printed.
560                .map_err(|e| {
561                    let from = std::env::current_dir()
562                        .map(|p| p.display().to_string())
563                        .unwrap_or_default();
564                    format!(
565                        "Error: {}, chdir '{from}' -> '{dir}'",
566                        crate::stdlib::fs::libuv_message(&e)
567                    )
568                })
569        }
570        // `process.kill(pid[, signal])` really signals the process. Node's default
571        // is SIGTERM, and a numeric or `'SIGxxx'` signal is accepted; signal `0`
572        // is the existence probe and sends nothing.
573        "kill" => {
574            let pid = with_host(|h| args.first().map(|v| h.to_number(v)).unwrap_or(0.0)) as i32;
575            let sig: Result<libc::c_int, String> = match args.get(1) {
576                Some(v) if !matches!(v, Value::Undef) => match with_host(|h| h.as_str(v)) {
577                    Some(name) => signal_number(&name).ok_or(crate::host::coded_error(
578                        "TypeError",
579                        "ERR_UNKNOWN_SIGNAL",
580                        &format!("Unknown signal: {name}"),
581                    )),
582                    None => Ok(with_host(|h| h.to_number(v)) as libc::c_int),
583                },
584                _ => Ok(libc::SIGTERM),
585            };
586            sig.and_then(|sig| {
587                // SAFETY: `kill` is a plain syscall on a pid/signal pair; it
588                // mutates no process memory and reports failure through `errno`.
589                if unsafe { libc::kill(pid, sig) } != 0 {
590                    Err(format!("Error: {}", std::io::Error::last_os_error()))
591                } else {
592                    Ok(Value::Undef)
593                }
594            })
595        }
596        // A genuine no-op: node-js emits no source maps, so enabling their use
597        // changes nothing. Returning `undefined` is the whole of Node's contract
598        // here, so this is not a stub.
599        "setSourceMapsEnabled" => Ok(Value::Undef),
600
601        // POSIX identity queries (libc; pure reads, always safe).
602        "getuid" => Ok(Value::Float(unsafe { libc::getuid() } as f64)),
603        "geteuid" => Ok(Value::Float(unsafe { libc::geteuid() } as f64)),
604        "getgid" => Ok(Value::Float(unsafe { libc::getgid() } as f64)),
605        "getegid" => Ok(Value::Float(unsafe { libc::getegid() } as f64)),
606        "getgroups" => {
607            let groups = supplementary_groups();
608            Ok(with_host(|h| {
609                h.new_array(groups.into_iter().map(Value::Float).collect())
610            }))
611        }
612
613        // POSIX identity mutation (libc; best-effort — silently ignored when the
614        // process lacks the privilege, matching a no-throw best-effort surface).
615        "setuid" | "seteuid" | "setgid" | "setegid" => {
616            let id = super::arg_num(args, 0);
617            if id.is_finite() {
618                let id = id as u32;
619                // SAFETY: id is a plain uid/gid number; a failed call just returns -1.
620                unsafe {
621                    match method {
622                        "setuid" => libc::setuid(id),
623                        "seteuid" => libc::seteuid(id),
624                        "setgid" => libc::setgid(id),
625                        _ => libc::setegid(id),
626                    };
627                }
628            }
629            Ok(Value::Undef)
630        }
631        "setgroups" => {
632            let groups = gid_array(args.first());
633            // SAFETY: `groups` is a valid gid buffer of the given length.
634            unsafe {
635                libc::setgroups(groups.len() as _, groups.as_ptr());
636            }
637            Ok(Value::Undef)
638        }
639        "initgroups" => {
640            let user = super::arg_str(args, 0);
641            let extra = super::arg_num(args, 1);
642            if let Ok(c) = std::ffi::CString::new(user) {
643                let gid = if extra.is_finite() { extra as u32 } else { 0 };
644                // SAFETY: `c` is NUL-terminated; a failed call just returns -1.
645                unsafe {
646                    libc::initgroups(c.as_ptr(), gid as _);
647                }
648            }
649            Ok(Value::Undef)
650        }
651
652        // `ref`/`unref` on the process object are chainable no-ops (no libuv
653        // handle refcount to touch); return the process namespace.
654        "ref" | "unref" => Ok(with_host(|h| h.alloc(JsObj::Builtin("process".into())))),
655        "abort" => std::process::abort(),
656        "getActiveResourcesInfo" => Ok(with_host(|h| h.new_array(Vec::new()))),
657        "resourceUsage" => Ok(resource_usage()),
658        "threadCpuUsage" => Ok(thread_cpu_usage()),
659        "availableMemory" | "constrainedMemory" => Ok(Value::Float(0.0)),
660        "getBuiltinModule" => {
661            let id = super::arg_str(args, 0);
662            let id = id.strip_prefix("node:").unwrap_or(&id);
663            match crate::stdlib::resolve(id) {
664                Some(ns) => Ok(with_host(|h| h.alloc(JsObj::Builtin(ns.to_string())))),
665                None => Ok(Value::Undef),
666            }
667        }
668        "openStdin" => Ok(std_stream(0)),
669
670        "hasUncaughtExceptionCaptureCallback" => {
671            Ok(Value::Bool(UNCAUGHT_CAPTURE.with(|c| c.borrow().is_some())))
672        }
673        "setUncaughtExceptionCaptureCallback" => {
674            let cb = args.first().cloned().unwrap_or(Value::Undef);
675            let clear = matches!(cb, Value::Undef) || with_host(|h| h.is_null(&cb));
676            if clear {
677                UNCAUGHT_CAPTURE.with(|c| *c.borrow_mut() = None);
678            } else if UNCAUGHT_CAPTURE.with(|c| c.borrow().is_some()) {
679                return Some(Err(crate::host::type_error(
680                    "`process.setUncaughtExceptionCaptureCallback()` was called \
681                     while a capture callback was already active",
682                )));
683            } else {
684                UNCAUGHT_CAPTURE.with(|c| *c.borrow_mut() = Some(cb));
685            }
686            Ok(Value::Undef)
687        }
688        "addUncaughtExceptionCaptureCallback" => {
689            let cb = args.first().cloned().unwrap_or(Value::Undef);
690            if !matches!(cb, Value::Undef) {
691                UNCAUGHT_CAPTURE.with(|c| *c.borrow_mut() = Some(cb));
692            }
693            Ok(Value::Undef)
694        }
695        "execve" => exec_ve(args),
696        "loadEnvFile" => load_env_file(&super::arg_str(args, 0)),
697        _ => return None,
698    })
699}
700
701/// `process.env` as a plain object built from the real environment.
702fn env_object() -> Value {
703    with_host(|h| {
704        let mut m = IndexMap::new();
705        // A marker the property-write path recognises, so an assignment into
706        // `process.env` coerces to a string the way a real environment does.
707        // Nothing dispatches on it; it is hidden from enumeration like any
708        // `@@` key.
709        m.insert("@@envObject".into(), Value::Bool(true));
710        for (k, v) in std::env::vars() {
711            m.insert(k, h.new_str(v));
712        }
713        h.new_object(m)
714    })
715}
716
717/// The `(execArgv, argv)` split installed by the binary's entry point.
718///
719/// Unset when the library is embedded (or driven by a sibling binary such as
720/// `parity-fuzz`, whose own command line is not a `node` command line), and the
721/// accessors below then fall back to the raw process arguments.
722static ARGV: std::sync::OnceLock<(Vec<String>, Vec<String>)> = std::sync::OnceLock::new();
723
724/// Publish Node's `process.argv` / `process.execArgv` split for this run, read
725/// off the real command line. Called once by the `node` binary's entry point;
726/// first call wins.
727///
728/// `argv[1]` is the entry script RESOLVED against the current directory, so
729/// `node ./x.js` reports the same absolute path `path.resolve` would — not the
730/// spelling that was typed.
731pub fn install_argv() {
732    let split = crate::cli::split_argv(std::env::args());
733    let mut argv = vec![exec_path()];
734    if let Some(s) = &split.script {
735        // `-` is Node's stdin entry point, not a path; it stays verbatim.
736        argv.push(if s == "-" {
737            s.clone()
738        } else {
739            super::path::resolve_one(s)
740        });
741    }
742    argv.extend(split.user);
743    let _ = ARGV.set((split.exec, argv));
744}
745
746/// `process.argv`: `[execPath, entryScript, ...userArgs]`.
747///
748/// The runtime's OWN flags are not in it — they are `process.execArgv` — and
749/// under `-e` there is no `argv[1]` at all. Returning the raw OS arguments put
750/// `-e` and the whole one-liner source into `argv`, which is what any script
751/// that reads `process.argv.slice(2)` for its options would have parsed.
752fn argv() -> Value {
753    with_host(|h| {
754        let items: Vec<Value> = match ARGV.get() {
755            Some((_, argv)) => argv.iter().map(|a| h.new_str(a.clone())).collect(),
756            None => std::env::args().map(|a| h.new_str(a)).collect(),
757        };
758        h.new_array(items)
759    })
760}
761
762/// `process.execArgv`: the runtime flags, `-e`/`--eval` and its source included.
763fn exec_argv() -> Value {
764    with_host(|h| {
765        let items: Vec<Value> = ARGV
766            .get()
767            .map(|(e, _)| e.iter().map(|a| h.new_str(a.clone())).collect())
768            .unwrap_or_default();
769        h.new_array(items)
770    })
771}
772
773fn exec_path() -> String {
774    std::env::current_exe()
775        .map(|p| p.to_string_lossy().into_owned())
776        .unwrap_or_else(|_| "node".into())
777}
778
779/// `process.versions` — a small map; only `node` is commonly gated on.
780fn versions() -> Value {
781    with_host(|h| {
782        let mut m = IndexMap::new();
783        m.insert("node".into(), h.new_str("26.5.0"));
784        m.insert("v8".into(), h.new_str("0.0.0"));
785        h.new_object(m)
786    })
787}
788
789/// A minimal `process.stdout`/`stderr`/`stdin` stand-in: enough surface
790/// (`fd`, `isTTY`, `writable`, a `write`) for load-time probes like
791/// `tty.isatty(process.stderr.fd)`.
792fn std_stream(fd: i32) -> Value {
793    with_host(|h| {
794        let mut m = IndexMap::new();
795        m.insert("@@native".into(), h.new_str("WriteStream"));
796        m.insert("fd".into(), Value::Float(fd as f64));
797        // SAFETY: isatty is a pure query on the fd number.
798        let is_tty = unsafe { libc::isatty(fd) == 1 };
799        // Node defines `isTTY` only when the fd IS a terminal; off a pipe the
800        // property is absent, not `false`. Defining it either way made
801        // `typeof process.stdout.isTTY` report "boolean" where node says
802        // "undefined", which is exactly the check a library uses to decide
803        // whether to emit colour.
804        if is_tty {
805            m.insert("isTTY".into(), Value::Bool(true));
806        }
807        m.insert("writable".into(), Value::Bool(fd != 0));
808        m.insert("readable".into(), Value::Bool(fd == 0));
809        // A tty stream exposes its terminal dimensions (real ioctl reading).
810        if is_tty {
811            if let Some((cols, rows)) = super::tty::window_size(fd) {
812                m.insert("columns".into(), Value::Float(cols as f64));
813                m.insert("rows".into(), Value::Float(rows as f64));
814            }
815        }
816        h.new_object(m)
817    })
818}
819
820/// Instance methods of a `process.stdout`/`stderr` `WriteStream`: `write`/`end`
821/// emit the chunk raw (no newline) to the stream's fd, so ordering interleaves
822/// correctly with `console.log`.
823pub fn stream_instance_call(recv: &Value, method: &str, args: &[Value]) -> Result<Value, String> {
824    match method {
825        "write" | "end" => {
826            let fd = with_host(|h| match h.get(recv) {
827                Some(JsObj::Object(p)) => p.get("fd").map(|v| h.to_number(v)).unwrap_or(1.0),
828                _ => 1.0,
829            });
830            // `end()` with no chunk closes without writing; `write()` with no
831            // chunk is the argument error below.
832            if method == "end" && args.first().map(|v| matches!(v, Value::Undef)) != Some(false) {
833                return Ok(Value::Bool(true));
834            }
835            let bytes = chunk_bytes(args)?;
836            with_host(|h| h.write_out_bytes(&bytes, fd == 2.0));
837            Ok(Value::Bool(true))
838        }
839        // A no-op stream surface so `.on('data')`/`.once`/`.end()` chaining loads.
840        "on" | "once" | "removeListener" | "cork" | "uncork" | "setEncoding" => Ok(recv.clone()),
841        // `tty.WriteStream` cursor/erase control — emit the corresponding ANSI
842        // escape to the stream's fd (best-effort; only meaningful on a real tty).
843        "cursorTo" | "moveCursor" | "clearLine" | "clearScreenDown" => {
844            let seq = tty_control(method, args);
845            write_fd(stream_fd(recv), seq.as_bytes());
846            Ok(Value::Bool(true))
847        }
848        "getWindowSize" => {
849            let (c, r) = super::tty::window_size(stream_fd(recv) as i32).unwrap_or((80, 24));
850            Ok(with_host(|h| {
851                h.new_array(vec![Value::Float(c as f64), Value::Float(r as f64)])
852            }))
853        }
854        // A truecolor terminal advertises 24-bit depth; hasColors(count) is true
855        // for any request within that range.
856        "getColorDepth" => Ok(Value::Float(24.0)),
857        "hasColors" => Ok(Value::Bool(true)),
858        _ => Err(crate::host::type_error(&format!(
859            "{method} is not a function"
860        ))),
861    }
862}
863
864fn hrtime(args: &[Value]) -> Value {
865    let now = std::time::SystemTime::now()
866        .duration_since(std::time::UNIX_EPOCH)
867        .unwrap_or_default();
868    let (mut secs, mut nanos) = (now.as_secs() as f64, now.subsec_nanos() as f64);
869    // `hrtime(prev)` returns the diff from a prior reading.
870    if let Some(Value::Obj(_)) = args.first() {
871        if let Some(prev) = with_host(|h| match h.get(&args[0]) {
872            Some(JsObj::Array(a)) if a.len() == 2 => Some((h.to_number(&a[0]), h.to_number(&a[1]))),
873            _ => None,
874        }) {
875            secs -= prev.0;
876            nanos -= prev.1;
877        }
878    }
879    with_host(|h| h.new_array(vec![Value::Float(secs), Value::Float(nanos)]))
880}
881
882/// The process's resident set size in bytes, or `None` where it cannot be read.
883///
884/// `process.memoryUsage()` reported a flat zero for every field, which is not a
885/// measurement — a caller comparing it against a threshold got a wrong answer
886/// rather than an honest refusal. RSS is the one figure both platforms expose
887/// cheaply; the V8 heap figures below stay zero because this runtime has no V8
888/// heap to report, and saying zero there is the truthful answer.
889#[cfg(target_os = "macos")]
890fn resident_bytes() -> Option<u64> {
891    let mut info: libc::proc_taskinfo = unsafe { std::mem::zeroed() };
892    let size = std::mem::size_of::<libc::proc_taskinfo>() as libc::c_int;
893    let got = unsafe {
894        libc::proc_pidinfo(
895            std::process::id() as libc::c_int,
896            libc::PROC_PIDTASKINFO,
897            0,
898            (&mut info as *mut libc::proc_taskinfo).cast(),
899            size,
900        )
901    };
902    (got == size).then_some(info.pti_resident_size)
903}
904
905#[cfg(target_os = "linux")]
906fn resident_bytes() -> Option<u64> {
907    // `/proc/self/statm` field 2 is the resident page count.
908    let statm = std::fs::read_to_string("/proc/self/statm").ok()?;
909    let pages: u64 = statm.split_whitespace().nth(1)?.parse().ok()?;
910    let page = unsafe { libc::sysconf(libc::_SC_PAGESIZE) };
911    (page > 0).then(|| pages * page as u64)
912}
913
914#[cfg(not(any(target_os = "macos", target_os = "linux")))]
915fn resident_bytes() -> Option<u64> {
916    None
917}
918
919fn memory_usage() -> Value {
920    let rss = resident_bytes().unwrap_or(0) as f64;
921    with_host(|h| {
922        let mut m = IndexMap::new();
923        m.insert("rss".into(), Value::Float(rss));
924        // The V8 heap figures have no counterpart here, so they stay zero
925        // rather than being invented.
926        for k in ["heapTotal", "heapUsed", "external", "arrayBuffers"] {
927            m.insert(k.into(), Value::Float(0.0));
928        }
929        h.new_object(m)
930    })
931}
932
933/// `process.memoryUsage.rss()` — the same figure without building the object.
934pub fn memory_usage_rss() -> Value {
935    Value::Float(resident_bytes().unwrap_or(0) as f64)
936}
937
938/// Emit `process.on('exit', code)` exactly once per process, the way Node's
939/// `process._exiting` latch does — `process.exit()` inside an `exit` handler
940/// must not re-enter it.
941///
942/// The handlers run SYNCHRONOUSLY and nothing they schedule ever runs: Node
943/// leaves the loop straight after them, so a `setTimeout` or `.then` queued
944/// here is dropped. An `exit` listener may still raise `process.exitCode`, and
945/// that later value is the one the process uses, which is why the caller reads
946/// the slot back after this returns.
947pub fn emit_exit_event(code: i32) -> Result<(), String> {
948    if with_host(|h| std::mem::replace(&mut h.exiting, true)) {
949        return Ok(());
950    }
951    let listeners = with_host(|h| h.take_process_listeners("exit"));
952    for f in listeners {
953        crate::host::invoke(&f, vec![Value::Float(code as f64)], None)?;
954    }
955    Ok(())
956}
957
958/// Emit `process.on('beforeExit', code)`. Node fires this when the loop has
959/// drained but the process has NOT been told to exit, and — unlike `exit` —
960/// work scheduled from a handler is honoured, so the loop runs again and
961/// `beforeExit` can fire repeatedly. It never fires after an explicit
962/// `process.exit()` or an uncaught exception.
963///
964/// Reports whether any listener ran, so the caller knows to re-drain.
965pub fn emit_before_exit(code: i32) -> Result<bool, String> {
966    let listeners = with_host(|h| h.take_process_listeners("beforeExit"));
967    let any = !listeners.is_empty();
968    for f in listeners {
969        crate::host::invoke(&f, vec![Value::Float(code as f64)], None)?;
970    }
971    Ok(any)
972}
973
974/// The bytes a `stream.write(chunk[, encoding])` call puts on the wire.
975///
976/// Node writes a `Buffer`/`TypedArray`/`DataView` chunk through UNTOUCHED, and
977/// decodes a string chunk with the named encoding (default `utf8`). Both were
978/// funnelled through `ToString` here, which is lossy in two separate ways:
979/// `process.stdout.write(Buffer.from([0xff,0xfe,0x41]))` printed the 15 bytes of
980/// `[object Object]` instead of `ff fe 41`, and even once the Buffer path
981/// existed, a `String` round-trip would have replaced each non-UTF-8 byte with
982/// `U+FFFD` (3 bytes out, 7 bytes on the wire). `write("4142","hex")` likewise
983/// printed the four characters of the literal instead of the two bytes `AB`.
984///
985/// Anything that is neither a string nor a byte view is the same
986/// `ERR_INVALID_ARG_TYPE` Node raises — a JS array of byte values included,
987/// which is why this does NOT reuse `buffer::bytes_like` (that helper
988/// deliberately accepts plain arrays, which `write` rejects).
989fn chunk_bytes(args: &[Value]) -> Result<Vec<u8>, String> {
990    let chunk = args.first().cloned().unwrap_or(Value::Undef);
991    if with_host(|h| h.is_null(&chunk)) {
992        return Err(crate::host::type_error(
993            "May not write null values to stream",
994        ));
995    }
996    if let Some(s) = with_host(|h| h.as_str(&chunk)) {
997        let enc = match args.get(1) {
998            Some(v) if !matches!(v, Value::Undef) => with_host(|h| h.str_of(v)),
999            _ => "utf8".to_string(),
1000        };
1001        return Ok(super::buffer::decode_str(&s, &enc));
1002    }
1003    match super::native_tag(&chunk).as_deref() {
1004        Some("Buffer") | Some("TypedArray") | Some("DataView") => {
1005            Ok(super::buffer::bytes_like(&chunk).unwrap_or_default())
1006        }
1007        _ => Err(crate::host::type_error(&format!(
1008            "The \"chunk\" argument must be of type string or an instance of \
1009             Buffer, TypedArray, or DataView. Received {}",
1010            super::received_desc(&chunk)
1011        ))),
1012    }
1013}
1014
1015/// The `fd` numeric property of a stream stand-in (default stdout).
1016fn stream_fd(recv: &Value) -> f64 {
1017    with_host(|h| match h.get(recv) {
1018        Some(JsObj::Object(p)) => p.get("fd").map(|v| h.to_number(v)).unwrap_or(1.0),
1019        _ => 1.0,
1020    })
1021}
1022
1023/// Write raw bytes as program output on stdout/stderr (chosen by fd) — through
1024/// the host funnel, so an embedder capturing output receives these too.
1025fn write_fd(fd: f64, bytes: &[u8]) {
1026    let text = String::from_utf8_lossy(bytes).into_owned();
1027    with_host(|h| h.write_out(&text, fd == 2.0));
1028}
1029
1030/// The ANSI control sequence for a `tty.WriteStream` cursor/erase method.
1031fn tty_control(method: &str, args: &[Value]) -> String {
1032    match method {
1033        // cursorTo(x[, y]) → absolute column (`\e[<x+1>G`) or position.
1034        "cursorTo" => {
1035            let x = super::arg_num(args, 0);
1036            let y = super::arg_num(args, 1);
1037            let x = if x.is_finite() { x as i64 } else { 0 };
1038            if y.is_finite() {
1039                format!("\x1b[{};{}H", y as i64 + 1, x + 1)
1040            } else {
1041                format!("\x1b[{}G", x + 1)
1042            }
1043        }
1044        // moveCursor(dx, dy) → relative moves.
1045        "moveCursor" => {
1046            let dx = super::arg_num(args, 0);
1047            let dy = super::arg_num(args, 1);
1048            let mut s = String::new();
1049            let dx = if dx.is_finite() { dx as i64 } else { 0 };
1050            let dy = if dy.is_finite() { dy as i64 } else { 0 };
1051            if dx > 0 {
1052                s.push_str(&format!("\x1b[{dx}C"));
1053            } else if dx < 0 {
1054                s.push_str(&format!("\x1b[{}D", -dx));
1055            }
1056            if dy > 0 {
1057                s.push_str(&format!("\x1b[{dy}B"));
1058            } else if dy < 0 {
1059                s.push_str(&format!("\x1b[{}A", -dy));
1060            }
1061            s
1062        }
1063        // clearLine(dir): -1 left, 1 right, 0 whole line.
1064        "clearLine" => match super::arg_num(args, 0) {
1065            d if d < 0.0 => "\x1b[1K".into(),
1066            d if d > 0.0 => "\x1b[0K".into(),
1067            _ => "\x1b[2K".into(),
1068        },
1069        // clearScreenDown → erase from cursor to end of screen.
1070        _ => "\x1b[0J".into(),
1071    }
1072}
1073
1074/// The process's supplementary group ids (`getgroups(2)`).
1075fn supplementary_groups() -> Vec<f64> {
1076    // SAFETY: first call queries the count, second fills a buffer of that size.
1077    unsafe {
1078        let n = libc::getgroups(0, std::ptr::null_mut());
1079        if n <= 0 {
1080            return Vec::new();
1081        }
1082        let mut buf = vec![0 as libc::gid_t; n as usize];
1083        let filled = libc::getgroups(n, buf.as_mut_ptr());
1084        if filled < 0 {
1085            return Vec::new();
1086        }
1087        buf.truncate(filled as usize);
1088        buf.into_iter().map(|g| g as f64).collect()
1089    }
1090}
1091
1092/// Read a JS array of numbers as a gid buffer.
1093fn gid_array(v: Option<&Value>) -> Vec<libc::gid_t> {
1094    let Some(v) = v else { return Vec::new() };
1095    with_host(|h| match h.get(v) {
1096        Some(JsObj::Array(a)) => a.iter().map(|x| h.to_number(x) as libc::gid_t).collect(),
1097        _ => Vec::new(),
1098    })
1099}
1100
1101/// `getrusage(RUSAGE_SELF)` — `None` if the syscall fails.
1102fn get_rusage() -> Option<libc::rusage> {
1103    // SAFETY: getrusage fills a zeroed rusage; RUSAGE_SELF is a valid `who`.
1104    unsafe {
1105        let mut ru: libc::rusage = std::mem::zeroed();
1106        (libc::getrusage(libc::RUSAGE_SELF, &mut ru) == 0).then_some(ru)
1107    }
1108}
1109
1110/// microseconds from a `timeval`.
1111fn tv_micros(t: &libc::timeval) -> f64 {
1112    t.tv_sec as f64 * 1e6 + t.tv_usec as f64
1113}
1114
1115/// `process.resourceUsage()` — the full `getrusage` breakdown (zeros on failure).
1116fn resource_usage() -> Value {
1117    let ru = get_rusage();
1118    with_host(|h| {
1119        let mut m = IndexMap::new();
1120        let (utime, stime) = ru
1121            .as_ref()
1122            .map(|r| (tv_micros(&r.ru_utime), tv_micros(&r.ru_stime)))
1123            .unwrap_or((0.0, 0.0));
1124        m.insert("userCPUTime".into(), Value::Float(utime));
1125        m.insert("systemCPUTime".into(), Value::Float(stime));
1126        let fields = [
1127            ("maxRSS", ru.as_ref().map(|r| r.ru_maxrss)),
1128            ("sharedMemorySize", ru.as_ref().map(|r| r.ru_ixrss)),
1129            ("unsharedDataSize", ru.as_ref().map(|r| r.ru_idrss)),
1130            ("unsharedStackSize", ru.as_ref().map(|r| r.ru_isrss)),
1131            ("minorPageFault", ru.as_ref().map(|r| r.ru_minflt)),
1132            ("majorPageFault", ru.as_ref().map(|r| r.ru_majflt)),
1133            ("swappedOut", ru.as_ref().map(|r| r.ru_nswap)),
1134            ("fsRead", ru.as_ref().map(|r| r.ru_inblock)),
1135            ("fsWrite", ru.as_ref().map(|r| r.ru_oublock)),
1136            ("ipcSent", ru.as_ref().map(|r| r.ru_msgsnd)),
1137            ("ipcReceived", ru.as_ref().map(|r| r.ru_msgrcv)),
1138            ("signalsCount", ru.as_ref().map(|r| r.ru_nsignals)),
1139            ("voluntaryContextSwitches", ru.as_ref().map(|r| r.ru_nvcsw)),
1140            (
1141                "involuntaryContextSwitches",
1142                ru.as_ref().map(|r| r.ru_nivcsw),
1143            ),
1144        ];
1145        for (k, v) in fields {
1146            m.insert(k.into(), Value::Float(v.unwrap_or(0) as f64));
1147        }
1148        h.new_object(m)
1149    })
1150}
1151
1152/// `process.threadCpuUsage()` — best-effort via process-wide `getrusage` (no
1153/// per-thread accounting substrate), reported as `{user, system}` microseconds.
1154fn thread_cpu_usage() -> Value {
1155    let (u, s) = get_rusage()
1156        .map(|r| (tv_micros(&r.ru_utime), tv_micros(&r.ru_stime)))
1157        .unwrap_or((0.0, 0.0));
1158    with_host(|h| {
1159        let mut m = IndexMap::new();
1160        m.insert("user".into(), Value::Float(u));
1161        m.insert("system".into(), Value::Float(s));
1162        h.new_object(m)
1163    })
1164}
1165
1166/// `process.execve(file, args[, env])` — replace the process image (never returns
1167/// on success; throws the OS error otherwise).
1168fn exec_ve(args: &[Value]) -> Result<Value, String> {
1169    use std::ffi::CString;
1170    let prog = CString::new(super::arg_str(args, 0))
1171        .map_err(|_| crate::host::type_error("process.execve: invalid file path"))?;
1172
1173    let argv_strs: Vec<String> = with_host(|h| match args.get(1).and_then(|v| h.get(v)) {
1174        Some(JsObj::Array(a)) => a.iter().map(|x| h.str_of(x)).collect(),
1175        _ => Vec::new(),
1176    });
1177    let env_strs: Vec<String> = {
1178        let from_arg = with_host(|h| match args.get(2).and_then(|v| h.get(v)) {
1179            Some(JsObj::Object(p)) => Some(
1180                p.iter()
1181                    .map(|(k, v)| format!("{k}={}", h.str_of(v)))
1182                    .collect::<Vec<_>>(),
1183            ),
1184            _ => None,
1185        });
1186        from_arg.unwrap_or_else(|| std::env::vars().map(|(k, v)| format!("{k}={v}")).collect())
1187    };
1188
1189    let to_c = |s: String| {
1190        CString::new(s).map_err(|_| crate::host::type_error("process.execve: NUL in argument"))
1191    };
1192    let argv_c: Vec<CString> = argv_strs.into_iter().map(to_c).collect::<Result<_, _>>()?;
1193    let env_c: Vec<CString> = env_strs.into_iter().map(to_c).collect::<Result<_, _>>()?;
1194
1195    let mut argv_p: Vec<*const libc::c_char> = argv_c.iter().map(|c| c.as_ptr()).collect();
1196    argv_p.push(std::ptr::null());
1197    let mut envp_p: Vec<*const libc::c_char> = env_c.iter().map(|c| c.as_ptr()).collect();
1198    envp_p.push(std::ptr::null());
1199
1200    // SAFETY: argv/envp are NUL-terminated arrays of valid C strings kept alive
1201    // above; on success execve never returns.
1202    unsafe {
1203        libc::execve(prog.as_ptr(), argv_p.as_ptr(), envp_p.as_ptr());
1204    }
1205    Err(crate::host::type_error(&format!(
1206        "process.execve failed: {}",
1207        std::io::Error::last_os_error()
1208    )))
1209}
1210
1211/// `process.loadEnvFile([path])` — parse a `.env` file into `process.env`
1212/// (persisted through the real environment so a later `process.env` read sees it).
1213fn load_env_file(path: &str) -> Result<Value, String> {
1214    let path = if path.is_empty() { ".env" } else { path };
1215    let text =
1216        std::fs::read_to_string(path).map_err(|e| format!("Error: ENOENT: {e}, open '{path}'"))?;
1217    for line in text.lines() {
1218        let line = line.trim();
1219        if line.is_empty() || line.starts_with('#') {
1220            continue;
1221        }
1222        let line = line.strip_prefix("export ").unwrap_or(line);
1223        let Some((key, val)) = line.split_once('=') else {
1224            continue;
1225        };
1226        let key = key.trim();
1227        if key.is_empty() {
1228            continue;
1229        }
1230        let mut val = val.trim();
1231        if val.len() >= 2
1232            && ((val.starts_with('"') && val.ends_with('"'))
1233                || (val.starts_with('\'') && val.ends_with('\'')))
1234        {
1235            val = &val[1..val.len() - 1];
1236        }
1237        std::env::set_var(key, val);
1238    }
1239    Ok(Value::Undef)
1240}
1241
1242/// The event name argument of an EventEmitter-style `process` call.
1243fn event_name(args: &[Value]) -> String {
1244    args.first()
1245        .map(|v| with_host(|h| h.str_of(v)))
1246        .unwrap_or_default()
1247}