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        "@@stdinPump" => stdin_pump().map(|_| Value::Undef),
411        "cwd" => {
412            let d = std::env::current_dir()
413                .map(|p| p.to_string_lossy().into_owned())
414                .unwrap_or_default();
415            Ok(with_host(|h| h.new_str(d)))
416        }
417        // `hrtime()` → `[seconds, nanoseconds]` since an arbitrary epoch (here the
418        // monotonic clock via `Instant` is unavailable statically, so use the
419        // system clock — sufficient for the timing scaffolding deps set up).
420        "hrtime" => Ok(hrtime(args)),
421        // Nanoseconds since an arbitrary epoch, as a BigInt.
422        "hrtime.bigint" => Ok(with_host(|h| {
423            let now = std::time::SystemTime::now()
424                .duration_since(std::time::UNIX_EPOCH)
425                .unwrap_or_default();
426            h.new_bigint(num_bigint::BigInt::from(now.as_nanos()))
427        })),
428        "uptime" => Ok(Value::Float(0.0)),
429        "memoryUsage" => Ok(memory_usage()),
430        "cpuUsage" => Ok(with_host(|h| {
431            let mut m = IndexMap::new();
432            m.insert("user".into(), Value::Float(0.0));
433            m.insert("system".into(), Value::Float(0.0));
434            h.new_object(m)
435        })),
436        "umask" => Ok(Value::Float(0.0)),
437        "binding" => Err(crate::host::type_error("process.binding is not supported")),
438        // EventEmitter-style registration. Listeners are REMEMBERED (the runtime
439        // emits `unhandledRejection`; signals still never fire), and every form
440        // returns the process namespace so `.on(...).on(...)` chains work.
441        "on" | "once" | "addListener" => {
442            let (event, f) = (event_name(args), args.get(1).cloned());
443            if let Some(f) = f {
444                let once = method == "once";
445                with_host(|h| {
446                    h.process_listeners
447                        .entry(event)
448                        .or_default()
449                        .push(crate::host::ProcListener { f, once })
450                });
451            }
452            Ok(with_host(|h| h.alloc(JsObj::Builtin("process".into()))))
453        }
454        "off" | "removeListener" => {
455            let (event, f) = (event_name(args), args.get(1).cloned());
456            if let Some(f) = f {
457                with_host(|h| {
458                    if let Some(l) = h.process_listeners.get_mut(&event) {
459                        if let Some(i) = l.iter().position(|x| x.f == f) {
460                            l.remove(i);
461                        }
462                    }
463                });
464            }
465            Ok(with_host(|h| h.alloc(JsObj::Builtin("process".into()))))
466        }
467        "removeAllListeners" => {
468            let event = event_name(args);
469            with_host(|h| {
470                if event.is_empty() {
471                    h.process_listeners.clear();
472                } else {
473                    h.process_listeners.shift_remove(&event);
474                }
475            });
476            Ok(with_host(|h| h.alloc(JsObj::Builtin("process".into()))))
477        }
478        "listeners" => {
479            let event = event_name(args);
480            Ok(with_host(|h| {
481                let l = h
482                    .process_listeners
483                    .get(&event)
484                    .map(|v| v.iter().map(|x| x.f.clone()).collect())
485                    .unwrap_or_default();
486                h.new_array(l)
487            }))
488        }
489        "emit" => {
490            let event = event_name(args);
491            let rest: Vec<Value> = args.iter().skip(1).cloned().collect();
492            let listeners = with_host(|h| h.take_process_listeners(&event));
493            let any = !listeners.is_empty();
494            let mut r = Ok(Value::Bool(any));
495            for f in listeners {
496                if let Err(e) = crate::host::invoke(&f, rest.clone(), None) {
497                    r = Err(e);
498                    break;
499                }
500            }
501            r
502        }
503        "emitWarning" => {
504            emit_warning_args(args);
505            Ok(Value::Undef)
506        }
507        // `process.exit([code])` really exits, and does so IMMEDIATELY — nothing
508        // after the call runs. It used to return `undefined` and let execution
509        // continue, which is a silent lie with teeth: the idiom
510        // `if (done) { server.close(); process.exit(0); }` (no `return`, because
511        // in Node none is needed) fell through to the statement after it. In a
512        // request-sequencing loop that meant re-entering the loop past the end of
513        // its array and destructuring `undefined`. Measured on node v26.7.0,
514        // `console.log('before'); process.exit(0); console.log('after')` prints
515        // only `before`; it printed both here.
516        //
517        // Under `--build`/`--dap`/embedding this is still a real process exit,
518        // exactly as it is in Node — there is no "exit but keep going" in the API.
519        // stdout/stderr are flushed first because `std::process::exit` runs no
520        // destructors.
521        //
522        // Port of Node's `process.exit`: an argument (even `undefined`) is
523        // ASSIGNED to `process.exitCode` first — through the validating setter,
524        // so `process.exit(3.7)` throws instead of exiting — then the `exit`
525        // event fires with the resulting code, then the process leaves. With no
526        // argument the already-set `process.exitCode` decides, which is why
527        // `process.exitCode = 3; process.exit()` exits 3 on node v26.7.0.
528        "exit" | "reallyExit" => {
529            if !args.is_empty() {
530                if let Err(e) = set_exit_code(&args[0]) {
531                    return Some(Err(e));
532                }
533            }
534            let code = with_host(|h| h.exit_code).unwrap_or(0);
535            if let Err(e) = emit_exit_event(code) {
536                return Some(Err(e));
537            }
538            // An `exit` listener may raise the code; re-read before leaving.
539            let code = with_host(|h| h.exit_code).unwrap_or(0);
540            use std::io::Write;
541            let _ = std::io::stdout().flush();
542            let _ = std::io::stderr().flush();
543            // `std::process::exit` runs no destructors, so the bytecode cache
544            // has to reach disk here too — otherwise a script that ends in
545            // `process.exit()` would recompile every module it loaded, every
546            // run, and never benefit from the cache at all.
547            crate::cache::flush();
548            std::process::exit(code);
549        }
550        // `process.chdir(dir)` really changes the working directory, and throws on
551        // failure; it used to silently do nothing, so every later relative path
552        // still resolved against the old directory.
553        "chdir" => {
554            let dir = super::arg_str(args, 0);
555            std::env::set_current_dir(&dir)
556                .map(|()| Value::Undef)
557                // Node reports the libuv message and BOTH directories:
558                // `ENOENT: no such file or directory, chdir <cwd> -> <dir>`.
559                // The old text spliced in Rust's `io::Error` Display, whose
560                // `No such file or directory (os error 2)` no Node ever printed.
561                .map_err(|e| {
562                    let from = std::env::current_dir()
563                        .map(|p| p.display().to_string())
564                        .unwrap_or_default();
565                    format!(
566                        "Error: {}, chdir '{from}' -> '{dir}'",
567                        crate::stdlib::fs::libuv_message(&e)
568                    )
569                })
570        }
571        // `process.kill(pid[, signal])` really signals the process. Node's default
572        // is SIGTERM, and a numeric or `'SIGxxx'` signal is accepted; signal `0`
573        // is the existence probe and sends nothing.
574        "kill" => {
575            let pid = with_host(|h| args.first().map(|v| h.to_number(v)).unwrap_or(0.0)) as i32;
576            let sig: Result<libc::c_int, String> = match args.get(1) {
577                Some(v) if !matches!(v, Value::Undef) => match with_host(|h| h.as_str(v)) {
578                    Some(name) => signal_number(&name).ok_or(crate::host::coded_error(
579                        "TypeError",
580                        "ERR_UNKNOWN_SIGNAL",
581                        &format!("Unknown signal: {name}"),
582                    )),
583                    None => Ok(with_host(|h| h.to_number(v)) as libc::c_int),
584                },
585                _ => Ok(libc::SIGTERM),
586            };
587            sig.and_then(|sig| {
588                // SAFETY: `kill` is a plain syscall on a pid/signal pair; it
589                // mutates no process memory and reports failure through `errno`.
590                if unsafe { libc::kill(pid, sig) } != 0 {
591                    Err(format!("Error: {}", std::io::Error::last_os_error()))
592                } else {
593                    Ok(Value::Undef)
594                }
595            })
596        }
597        // A genuine no-op: node-js emits no source maps, so enabling their use
598        // changes nothing. Returning `undefined` is the whole of Node's contract
599        // here, so this is not a stub.
600        "setSourceMapsEnabled" => Ok(Value::Undef),
601
602        // POSIX identity queries (libc; pure reads, always safe).
603        "getuid" => Ok(Value::Float(unsafe { libc::getuid() } as f64)),
604        "geteuid" => Ok(Value::Float(unsafe { libc::geteuid() } as f64)),
605        "getgid" => Ok(Value::Float(unsafe { libc::getgid() } as f64)),
606        "getegid" => Ok(Value::Float(unsafe { libc::getegid() } as f64)),
607        "getgroups" => {
608            let groups = supplementary_groups();
609            Ok(with_host(|h| {
610                h.new_array(groups.into_iter().map(Value::Float).collect())
611            }))
612        }
613
614        // POSIX identity mutation (libc; best-effort — silently ignored when the
615        // process lacks the privilege, matching a no-throw best-effort surface).
616        "setuid" | "seteuid" | "setgid" | "setegid" => {
617            let id = super::arg_num(args, 0);
618            if id.is_finite() {
619                let id = id as u32;
620                // SAFETY: id is a plain uid/gid number; a failed call just returns -1.
621                unsafe {
622                    match method {
623                        "setuid" => libc::setuid(id),
624                        "seteuid" => libc::seteuid(id),
625                        "setgid" => libc::setgid(id),
626                        _ => libc::setegid(id),
627                    };
628                }
629            }
630            Ok(Value::Undef)
631        }
632        "setgroups" => {
633            let groups = gid_array(args.first());
634            // SAFETY: `groups` is a valid gid buffer of the given length.
635            unsafe {
636                libc::setgroups(groups.len() as _, groups.as_ptr());
637            }
638            Ok(Value::Undef)
639        }
640        "initgroups" => {
641            let user = super::arg_str(args, 0);
642            let extra = super::arg_num(args, 1);
643            if let Ok(c) = std::ffi::CString::new(user) {
644                let gid = if extra.is_finite() { extra as u32 } else { 0 };
645                // SAFETY: `c` is NUL-terminated; a failed call just returns -1.
646                unsafe {
647                    libc::initgroups(c.as_ptr(), gid as _);
648                }
649            }
650            Ok(Value::Undef)
651        }
652
653        // `ref`/`unref` on the process object are chainable no-ops (no libuv
654        // handle refcount to touch); return the process namespace.
655        "ref" | "unref" => Ok(with_host(|h| h.alloc(JsObj::Builtin("process".into())))),
656        "abort" => std::process::abort(),
657        "getActiveResourcesInfo" => Ok(with_host(|h| h.new_array(Vec::new()))),
658        "resourceUsage" => Ok(resource_usage()),
659        "threadCpuUsage" => Ok(thread_cpu_usage()),
660        "availableMemory" | "constrainedMemory" => Ok(Value::Float(0.0)),
661        "getBuiltinModule" => {
662            let id = super::arg_str(args, 0);
663            let id = id.strip_prefix("node:").unwrap_or(&id);
664            match crate::stdlib::resolve(id) {
665                Some(ns) => Ok(with_host(|h| h.alloc(JsObj::Builtin(ns.to_string())))),
666                None => Ok(Value::Undef),
667            }
668        }
669        "openStdin" => Ok(std_stream(0)),
670
671        "hasUncaughtExceptionCaptureCallback" => {
672            Ok(Value::Bool(UNCAUGHT_CAPTURE.with(|c| c.borrow().is_some())))
673        }
674        "setUncaughtExceptionCaptureCallback" => {
675            let cb = args.first().cloned().unwrap_or(Value::Undef);
676            let clear = matches!(cb, Value::Undef) || with_host(|h| h.is_null(&cb));
677            if clear {
678                UNCAUGHT_CAPTURE.with(|c| *c.borrow_mut() = None);
679            } else if UNCAUGHT_CAPTURE.with(|c| c.borrow().is_some()) {
680                return Some(Err(crate::host::type_error(
681                    "`process.setUncaughtExceptionCaptureCallback()` was called \
682                     while a capture callback was already active",
683                )));
684            } else {
685                UNCAUGHT_CAPTURE.with(|c| *c.borrow_mut() = Some(cb));
686            }
687            Ok(Value::Undef)
688        }
689        "addUncaughtExceptionCaptureCallback" => {
690            let cb = args.first().cloned().unwrap_or(Value::Undef);
691            if !matches!(cb, Value::Undef) {
692                UNCAUGHT_CAPTURE.with(|c| *c.borrow_mut() = Some(cb));
693            }
694            Ok(Value::Undef)
695        }
696        "execve" => exec_ve(args),
697        "loadEnvFile" => load_env_file(&super::arg_str(args, 0)),
698        _ => return None,
699    })
700}
701
702/// `process.env` as a plain object built from the real environment.
703fn env_object() -> Value {
704    with_host(|h| {
705        let mut m = IndexMap::new();
706        // A marker the property-write path recognises, so an assignment into
707        // `process.env` coerces to a string the way a real environment does.
708        // Nothing dispatches on it; it is hidden from enumeration like any
709        // `@@` key.
710        m.insert("@@envObject".into(), Value::Bool(true));
711        for (k, v) in std::env::vars() {
712            m.insert(k, h.new_str(v));
713        }
714        h.new_object(m)
715    })
716}
717
718/// The `(execArgv, argv)` split installed by the binary's entry point.
719///
720/// Unset when the library is embedded (or driven by a sibling binary such as
721/// `parity-fuzz`, whose own command line is not a `node` command line), and the
722/// accessors below then fall back to the raw process arguments.
723static ARGV: std::sync::OnceLock<(Vec<String>, Vec<String>)> = std::sync::OnceLock::new();
724
725/// Publish Node's `process.argv` / `process.execArgv` split for this run, read
726/// off the real command line. Called once by the `node` binary's entry point;
727/// first call wins.
728///
729/// `argv[1]` is the entry script RESOLVED against the current directory, so
730/// `node ./x.js` reports the same absolute path `path.resolve` would — not the
731/// spelling that was typed.
732pub fn install_argv() {
733    let split = crate::cli::split_argv(std::env::args());
734    let mut argv = vec![exec_path()];
735    if let Some(s) = &split.script {
736        // `-` is Node's stdin entry point, not a path; it stays verbatim.
737        argv.push(if s == "-" {
738            s.clone()
739        } else {
740            super::path::resolve_one(s)
741        });
742    }
743    argv.extend(split.user);
744    let _ = ARGV.set((split.exec, argv));
745}
746
747/// `process.argv`: `[execPath, entryScript, ...userArgs]`.
748///
749/// The runtime's OWN flags are not in it — they are `process.execArgv` — and
750/// under `-e` there is no `argv[1]` at all. Returning the raw OS arguments put
751/// `-e` and the whole one-liner source into `argv`, which is what any script
752/// that reads `process.argv.slice(2)` for its options would have parsed.
753fn argv() -> Value {
754    with_host(|h| {
755        let items: Vec<Value> = match ARGV.get() {
756            Some((_, argv)) => argv.iter().map(|a| h.new_str(a.clone())).collect(),
757            None => std::env::args().map(|a| h.new_str(a)).collect(),
758        };
759        h.new_array(items)
760    })
761}
762
763/// `process.execArgv`: the runtime flags, `-e`/`--eval` and its source included.
764fn exec_argv() -> Value {
765    with_host(|h| {
766        let items: Vec<Value> = ARGV
767            .get()
768            .map(|(e, _)| e.iter().map(|a| h.new_str(a.clone())).collect())
769            .unwrap_or_default();
770        h.new_array(items)
771    })
772}
773
774fn exec_path() -> String {
775    std::env::current_exe()
776        .map(|p| p.to_string_lossy().into_owned())
777        .unwrap_or_else(|_| "node".into())
778}
779
780/// `process.versions` — a small map; only `node` is commonly gated on.
781fn versions() -> Value {
782    with_host(|h| {
783        let mut m = IndexMap::new();
784        m.insert("node".into(), h.new_str("26.5.0"));
785        m.insert("v8".into(), h.new_str("0.0.0"));
786        h.new_object(m)
787    })
788}
789
790/// A minimal `process.stdout`/`stderr`/`stdin` stand-in: enough surface
791/// (`fd`, `isTTY`, `writable`, a `write`) for load-time probes like
792/// `tty.isatty(process.stderr.fd)`.
793fn std_stream(fd: i32) -> Value {
794    with_host(|h| {
795        let mut m = IndexMap::new();
796        m.insert("@@native".into(), h.new_str("WriteStream"));
797        m.insert("fd".into(), Value::Float(fd as f64));
798        // SAFETY: isatty is a pure query on the fd number.
799        let is_tty = unsafe { libc::isatty(fd) == 1 };
800        // Node defines `isTTY` only when the fd IS a terminal; off a pipe the
801        // property is absent, not `false`. Defining it either way made
802        // `typeof process.stdout.isTTY` report "boolean" where node says
803        // "undefined", which is exactly the check a library uses to decide
804        // whether to emit colour.
805        if is_tty {
806            m.insert("isTTY".into(), Value::Bool(true));
807        }
808        m.insert("writable".into(), Value::Bool(fd != 0));
809        m.insert("readable".into(), Value::Bool(fd == 0));
810        // A tty stream exposes its terminal dimensions (real ioctl reading).
811        if is_tty {
812            if let Some((cols, rows)) = super::tty::window_size(fd) {
813                m.insert("columns".into(), Value::Float(cols as f64));
814                m.insert("rows".into(), Value::Float(rows as f64));
815            }
816        }
817        h.new_object(m)
818    })
819}
820
821/// Instance methods of a `process.stdout`/`stderr` `WriteStream`: `write`/`end`
822/// emit the chunk raw (no newline) to the stream's fd, so ordering interleaves
823/// correctly with `console.log`.
824pub fn stream_instance_call(recv: &Value, method: &str, args: &[Value]) -> Result<Value, String> {
825    if stream_fd(recv) == 0.0 {
826        if let Some(r) = stdin_call(recv, method, args) {
827            return r;
828        }
829    }
830    match method {
831        "write" | "end" => {
832            let fd = with_host(|h| match h.get(recv) {
833                Some(JsObj::Object(p)) => p.get("fd").map(|v| h.to_number(v)).unwrap_or(1.0),
834                _ => 1.0,
835            });
836            // `end()` with no chunk closes without writing; `write()` with no
837            // chunk is the argument error below.
838            if method == "end" && args.first().map(|v| matches!(v, Value::Undef)) != Some(false) {
839                return Ok(Value::Bool(true));
840            }
841            let bytes = chunk_bytes(args)?;
842            with_host(|h| h.write_out_bytes(&bytes, fd == 2.0));
843            Ok(Value::Bool(true))
844        }
845        // A no-op stream surface on stdout/stderr so `.on('drain')`/`.once`/
846        // `.end()` chaining loads; standard input answers these for real
847        // (`stdin_call`).
848        "on" | "once" | "addListener" | "prependListener" | "removeListener" | "off"
849        | "removeAllListeners" | "cork" | "uncork" | "setEncoding" | "resume" | "pause" => {
850            Ok(recv.clone())
851        }
852        "listenerCount" => Ok(Value::Float(0.0)),
853        "read" => Ok(with_host(|h| h.null())),
854        "pipe" => Ok(args.first().cloned().unwrap_or(Value::Undef)),
855        // `tty.WriteStream` cursor/erase control — emit the corresponding ANSI
856        // escape to the stream's fd (best-effort; only meaningful on a real tty).
857        "cursorTo" | "moveCursor" | "clearLine" | "clearScreenDown" => {
858            let seq = tty_control(method, args);
859            write_fd(stream_fd(recv), seq.as_bytes());
860            Ok(Value::Bool(true))
861        }
862        "getWindowSize" => {
863            let (c, r) = super::tty::window_size(stream_fd(recv) as i32).unwrap_or((80, 24));
864            Ok(with_host(|h| {
865                h.new_array(vec![Value::Float(c as f64), Value::Float(r as f64)])
866            }))
867        }
868        // A truecolor terminal advertises 24-bit depth; hasColors(count) is true
869        // for any request within that range.
870        "getColorDepth" => Ok(Value::Float(24.0)),
871        "hasColors" => Ok(Value::Bool(true)),
872        _ => Err(crate::host::type_error(&format!(
873            "{method} is not a function"
874        ))),
875    }
876}
877
878fn hrtime(args: &[Value]) -> Value {
879    let now = std::time::SystemTime::now()
880        .duration_since(std::time::UNIX_EPOCH)
881        .unwrap_or_default();
882    let (mut secs, mut nanos) = (now.as_secs() as f64, now.subsec_nanos() as f64);
883    // `hrtime(prev)` returns the diff from a prior reading.
884    if let Some(Value::Obj(_)) = args.first() {
885        if let Some(prev) = with_host(|h| match h.get(&args[0]) {
886            Some(JsObj::Array(a)) if a.len() == 2 => Some((h.to_number(&a[0]), h.to_number(&a[1]))),
887            _ => None,
888        }) {
889            secs -= prev.0;
890            nanos -= prev.1;
891        }
892    }
893    with_host(|h| h.new_array(vec![Value::Float(secs), Value::Float(nanos)]))
894}
895
896/// The process's resident set size in bytes, or `None` where it cannot be read.
897///
898/// `process.memoryUsage()` reported a flat zero for every field, which is not a
899/// measurement — a caller comparing it against a threshold got a wrong answer
900/// rather than an honest refusal. RSS is the one figure both platforms expose
901/// cheaply; the V8 heap figures below stay zero because this runtime has no V8
902/// heap to report, and saying zero there is the truthful answer.
903#[cfg(target_os = "macos")]
904fn resident_bytes() -> Option<u64> {
905    let mut info: libc::proc_taskinfo = unsafe { std::mem::zeroed() };
906    let size = std::mem::size_of::<libc::proc_taskinfo>() as libc::c_int;
907    let got = unsafe {
908        libc::proc_pidinfo(
909            std::process::id() as libc::c_int,
910            libc::PROC_PIDTASKINFO,
911            0,
912            (&mut info as *mut libc::proc_taskinfo).cast(),
913            size,
914        )
915    };
916    (got == size).then_some(info.pti_resident_size)
917}
918
919#[cfg(target_os = "linux")]
920fn resident_bytes() -> Option<u64> {
921    // `/proc/self/statm` field 2 is the resident page count.
922    let statm = std::fs::read_to_string("/proc/self/statm").ok()?;
923    let pages: u64 = statm.split_whitespace().nth(1)?.parse().ok()?;
924    let page = unsafe { libc::sysconf(libc::_SC_PAGESIZE) };
925    (page > 0).then(|| pages * page as u64)
926}
927
928#[cfg(not(any(target_os = "macos", target_os = "linux")))]
929fn resident_bytes() -> Option<u64> {
930    None
931}
932
933fn memory_usage() -> Value {
934    let rss = resident_bytes().unwrap_or(0) as f64;
935    with_host(|h| {
936        let mut m = IndexMap::new();
937        m.insert("rss".into(), Value::Float(rss));
938        // The V8 heap figures have no counterpart here, so they stay zero
939        // rather than being invented.
940        for k in ["heapTotal", "heapUsed", "external", "arrayBuffers"] {
941            m.insert(k.into(), Value::Float(0.0));
942        }
943        h.new_object(m)
944    })
945}
946
947/// `process.memoryUsage.rss()` — the same figure without building the object.
948pub fn memory_usage_rss() -> Value {
949    Value::Float(resident_bytes().unwrap_or(0) as f64)
950}
951
952/// Emit `process.on('exit', code)` exactly once per process, the way Node's
953/// `process._exiting` latch does — `process.exit()` inside an `exit` handler
954/// must not re-enter it.
955///
956/// The handlers run SYNCHRONOUSLY and nothing they schedule ever runs: Node
957/// leaves the loop straight after them, so a `setTimeout` or `.then` queued
958/// here is dropped. An `exit` listener may still raise `process.exitCode`, and
959/// that later value is the one the process uses, which is why the caller reads
960/// the slot back after this returns.
961pub fn emit_exit_event(code: i32) -> Result<(), String> {
962    if with_host(|h| std::mem::replace(&mut h.exiting, true)) {
963        return Ok(());
964    }
965    let listeners = with_host(|h| h.take_process_listeners("exit"));
966    for f in listeners {
967        crate::host::invoke(&f, vec![Value::Float(code as f64)], None)?;
968    }
969    Ok(())
970}
971
972/// Emit `process.on('beforeExit', code)`. Node fires this when the loop has
973/// drained but the process has NOT been told to exit, and — unlike `exit` —
974/// work scheduled from a handler is honoured, so the loop runs again and
975/// `beforeExit` can fire repeatedly. It never fires after an explicit
976/// `process.exit()` or an uncaught exception.
977///
978/// Reports whether any listener ran, so the caller knows to re-drain.
979pub fn emit_before_exit(code: i32) -> Result<bool, String> {
980    let listeners = with_host(|h| h.take_process_listeners("beforeExit"));
981    let any = !listeners.is_empty();
982    for f in listeners {
983        crate::host::invoke(&f, vec![Value::Float(code as f64)], None)?;
984    }
985    Ok(any)
986}
987
988/// The bytes a `stream.write(chunk[, encoding])` call puts on the wire.
989///
990/// Node writes a `Buffer`/`TypedArray`/`DataView` chunk through UNTOUCHED, and
991/// decodes a string chunk with the named encoding (default `utf8`). Both were
992/// funnelled through `ToString` here, which is lossy in two separate ways:
993/// `process.stdout.write(Buffer.from([0xff,0xfe,0x41]))` printed the 15 bytes of
994/// `[object Object]` instead of `ff fe 41`, and even once the Buffer path
995/// existed, a `String` round-trip would have replaced each non-UTF-8 byte with
996/// `U+FFFD` (3 bytes out, 7 bytes on the wire). `write("4142","hex")` likewise
997/// printed the four characters of the literal instead of the two bytes `AB`.
998///
999/// Anything that is neither a string nor a byte view is the same
1000/// `ERR_INVALID_ARG_TYPE` Node raises — a JS array of byte values included,
1001/// which is why this does NOT reuse `buffer::bytes_like` (that helper
1002/// deliberately accepts plain arrays, which `write` rejects).
1003fn chunk_bytes(args: &[Value]) -> Result<Vec<u8>, String> {
1004    let chunk = args.first().cloned().unwrap_or(Value::Undef);
1005    if with_host(|h| h.is_null(&chunk)) {
1006        return Err(crate::host::type_error(
1007            "May not write null values to stream",
1008        ));
1009    }
1010    if let Some(s) = with_host(|h| h.as_str(&chunk)) {
1011        let enc = match args.get(1) {
1012            Some(v) if !matches!(v, Value::Undef) => with_host(|h| h.str_of(v)),
1013            _ => "utf8".to_string(),
1014        };
1015        return Ok(super::buffer::decode_str(&s, &enc));
1016    }
1017    match super::native_tag(&chunk).as_deref() {
1018        Some("Buffer") | Some("TypedArray") | Some("DataView") => {
1019            Ok(super::buffer::bytes_like(&chunk).unwrap_or_default())
1020        }
1021        _ => Err(crate::host::type_error(&format!(
1022            "The \"chunk\" argument must be of type string or an instance of \
1023             Buffer, TypedArray, or DataView. Received {}",
1024            super::received_desc(&chunk)
1025        ))),
1026    }
1027}
1028
1029/// The `fd` numeric property of a stream stand-in (default stdout).
1030fn stream_fd(recv: &Value) -> f64 {
1031    with_host(|h| match h.get(recv) {
1032        Some(JsObj::Object(p)) => p.get("fd").map(|v| h.to_number(v)).unwrap_or(1.0),
1033        _ => 1.0,
1034    })
1035}
1036
1037/// Write raw bytes as program output on stdout/stderr (chosen by fd) — through
1038/// the host funnel, so an embedder capturing output receives these too.
1039fn write_fd(fd: f64, bytes: &[u8]) {
1040    let text = String::from_utf8_lossy(bytes).into_owned();
1041    with_host(|h| h.write_out(&text, fd == 2.0));
1042}
1043
1044/// The ANSI control sequence for a `tty.WriteStream` cursor/erase method.
1045fn tty_control(method: &str, args: &[Value]) -> String {
1046    match method {
1047        // cursorTo(x[, y]) → absolute column (`\e[<x+1>G`) or position.
1048        "cursorTo" => {
1049            let x = super::arg_num(args, 0);
1050            let y = super::arg_num(args, 1);
1051            let x = if x.is_finite() { x as i64 } else { 0 };
1052            if y.is_finite() {
1053                format!("\x1b[{};{}H", y as i64 + 1, x + 1)
1054            } else {
1055                format!("\x1b[{}G", x + 1)
1056            }
1057        }
1058        // moveCursor(dx, dy) → relative moves.
1059        "moveCursor" => {
1060            let dx = super::arg_num(args, 0);
1061            let dy = super::arg_num(args, 1);
1062            let mut s = String::new();
1063            let dx = if dx.is_finite() { dx as i64 } else { 0 };
1064            let dy = if dy.is_finite() { dy as i64 } else { 0 };
1065            if dx > 0 {
1066                s.push_str(&format!("\x1b[{dx}C"));
1067            } else if dx < 0 {
1068                s.push_str(&format!("\x1b[{}D", -dx));
1069            }
1070            if dy > 0 {
1071                s.push_str(&format!("\x1b[{dy}B"));
1072            } else if dy < 0 {
1073                s.push_str(&format!("\x1b[{}A", -dy));
1074            }
1075            s
1076        }
1077        // clearLine(dir): -1 left, 1 right, 0 whole line.
1078        "clearLine" => match super::arg_num(args, 0) {
1079            d if d < 0.0 => "\x1b[1K".into(),
1080            d if d > 0.0 => "\x1b[0K".into(),
1081            _ => "\x1b[2K".into(),
1082        },
1083        // clearScreenDown → erase from cursor to end of screen.
1084        _ => "\x1b[0J".into(),
1085    }
1086}
1087
1088/// The process's supplementary group ids (`getgroups(2)`).
1089fn supplementary_groups() -> Vec<f64> {
1090    // SAFETY: first call queries the count, second fills a buffer of that size.
1091    unsafe {
1092        let n = libc::getgroups(0, std::ptr::null_mut());
1093        if n <= 0 {
1094            return Vec::new();
1095        }
1096        let mut buf = vec![0 as libc::gid_t; n as usize];
1097        let filled = libc::getgroups(n, buf.as_mut_ptr());
1098        if filled < 0 {
1099            return Vec::new();
1100        }
1101        buf.truncate(filled as usize);
1102        buf.into_iter().map(|g| g as f64).collect()
1103    }
1104}
1105
1106/// Read a JS array of numbers as a gid buffer.
1107fn gid_array(v: Option<&Value>) -> Vec<libc::gid_t> {
1108    let Some(v) = v else { return Vec::new() };
1109    with_host(|h| match h.get(v) {
1110        Some(JsObj::Array(a)) => a.iter().map(|x| h.to_number(x) as libc::gid_t).collect(),
1111        _ => Vec::new(),
1112    })
1113}
1114
1115/// `getrusage(RUSAGE_SELF)` — `None` if the syscall fails.
1116fn get_rusage() -> Option<libc::rusage> {
1117    // SAFETY: getrusage fills a zeroed rusage; RUSAGE_SELF is a valid `who`.
1118    unsafe {
1119        let mut ru: libc::rusage = std::mem::zeroed();
1120        (libc::getrusage(libc::RUSAGE_SELF, &mut ru) == 0).then_some(ru)
1121    }
1122}
1123
1124/// microseconds from a `timeval`.
1125fn tv_micros(t: &libc::timeval) -> f64 {
1126    t.tv_sec as f64 * 1e6 + t.tv_usec as f64
1127}
1128
1129/// `process.resourceUsage()` — the full `getrusage` breakdown (zeros on failure).
1130fn resource_usage() -> Value {
1131    let ru = get_rusage();
1132    with_host(|h| {
1133        let mut m = IndexMap::new();
1134        let (utime, stime) = ru
1135            .as_ref()
1136            .map(|r| (tv_micros(&r.ru_utime), tv_micros(&r.ru_stime)))
1137            .unwrap_or((0.0, 0.0));
1138        m.insert("userCPUTime".into(), Value::Float(utime));
1139        m.insert("systemCPUTime".into(), Value::Float(stime));
1140        let fields = [
1141            ("maxRSS", ru.as_ref().map(|r| r.ru_maxrss)),
1142            ("sharedMemorySize", ru.as_ref().map(|r| r.ru_ixrss)),
1143            ("unsharedDataSize", ru.as_ref().map(|r| r.ru_idrss)),
1144            ("unsharedStackSize", ru.as_ref().map(|r| r.ru_isrss)),
1145            ("minorPageFault", ru.as_ref().map(|r| r.ru_minflt)),
1146            ("majorPageFault", ru.as_ref().map(|r| r.ru_majflt)),
1147            ("swappedOut", ru.as_ref().map(|r| r.ru_nswap)),
1148            ("fsRead", ru.as_ref().map(|r| r.ru_inblock)),
1149            ("fsWrite", ru.as_ref().map(|r| r.ru_oublock)),
1150            ("ipcSent", ru.as_ref().map(|r| r.ru_msgsnd)),
1151            ("ipcReceived", ru.as_ref().map(|r| r.ru_msgrcv)),
1152            ("signalsCount", ru.as_ref().map(|r| r.ru_nsignals)),
1153            ("voluntaryContextSwitches", ru.as_ref().map(|r| r.ru_nvcsw)),
1154            (
1155                "involuntaryContextSwitches",
1156                ru.as_ref().map(|r| r.ru_nivcsw),
1157            ),
1158        ];
1159        for (k, v) in fields {
1160            m.insert(k.into(), Value::Float(v.unwrap_or(0) as f64));
1161        }
1162        h.new_object(m)
1163    })
1164}
1165
1166/// `process.threadCpuUsage()` — best-effort via process-wide `getrusage` (no
1167/// per-thread accounting substrate), reported as `{user, system}` microseconds.
1168fn thread_cpu_usage() -> Value {
1169    let (u, s) = get_rusage()
1170        .map(|r| (tv_micros(&r.ru_utime), tv_micros(&r.ru_stime)))
1171        .unwrap_or((0.0, 0.0));
1172    with_host(|h| {
1173        let mut m = IndexMap::new();
1174        m.insert("user".into(), Value::Float(u));
1175        m.insert("system".into(), Value::Float(s));
1176        h.new_object(m)
1177    })
1178}
1179
1180/// `process.execve(file, args[, env])` — replace the process image (never returns
1181/// on success; throws the OS error otherwise).
1182fn exec_ve(args: &[Value]) -> Result<Value, String> {
1183    use std::ffi::CString;
1184    let prog = CString::new(super::arg_str(args, 0))
1185        .map_err(|_| crate::host::type_error("process.execve: invalid file path"))?;
1186
1187    let argv_strs: Vec<String> = with_host(|h| match args.get(1).and_then(|v| h.get(v)) {
1188        Some(JsObj::Array(a)) => a.iter().map(|x| h.str_of(x)).collect(),
1189        _ => Vec::new(),
1190    });
1191    let env_strs: Vec<String> = {
1192        let from_arg = with_host(|h| match args.get(2).and_then(|v| h.get(v)) {
1193            Some(JsObj::Object(p)) => Some(
1194                p.iter()
1195                    .map(|(k, v)| format!("{k}={}", h.str_of(v)))
1196                    .collect::<Vec<_>>(),
1197            ),
1198            _ => None,
1199        });
1200        from_arg.unwrap_or_else(|| std::env::vars().map(|(k, v)| format!("{k}={v}")).collect())
1201    };
1202
1203    let to_c = |s: String| {
1204        CString::new(s).map_err(|_| crate::host::type_error("process.execve: NUL in argument"))
1205    };
1206    let argv_c: Vec<CString> = argv_strs.into_iter().map(to_c).collect::<Result<_, _>>()?;
1207    let env_c: Vec<CString> = env_strs.into_iter().map(to_c).collect::<Result<_, _>>()?;
1208
1209    let mut argv_p: Vec<*const libc::c_char> = argv_c.iter().map(|c| c.as_ptr()).collect();
1210    argv_p.push(std::ptr::null());
1211    let mut envp_p: Vec<*const libc::c_char> = env_c.iter().map(|c| c.as_ptr()).collect();
1212    envp_p.push(std::ptr::null());
1213
1214    // SAFETY: argv/envp are NUL-terminated arrays of valid C strings kept alive
1215    // above; on success execve never returns.
1216    unsafe {
1217        libc::execve(prog.as_ptr(), argv_p.as_ptr(), envp_p.as_ptr());
1218    }
1219    Err(crate::host::type_error(&format!(
1220        "process.execve failed: {}",
1221        std::io::Error::last_os_error()
1222    )))
1223}
1224
1225/// `process.loadEnvFile([path])` — parse a `.env` file into `process.env`
1226/// (persisted through the real environment so a later `process.env` read sees it).
1227fn load_env_file(path: &str) -> Result<Value, String> {
1228    let path = if path.is_empty() { ".env" } else { path };
1229    let text =
1230        std::fs::read_to_string(path).map_err(|e| format!("Error: ENOENT: {e}, open '{path}'"))?;
1231    for line in text.lines() {
1232        let line = line.trim();
1233        if line.is_empty() || line.starts_with('#') {
1234            continue;
1235        }
1236        let line = line.strip_prefix("export ").unwrap_or(line);
1237        let Some((key, val)) = line.split_once('=') else {
1238            continue;
1239        };
1240        let key = key.trim();
1241        if key.is_empty() {
1242            continue;
1243        }
1244        let mut val = val.trim();
1245        if val.len() >= 2
1246            && ((val.starts_with('"') && val.ends_with('"'))
1247                || (val.starts_with('\'') && val.ends_with('\'')))
1248        {
1249            val = &val[1..val.len() - 1];
1250        }
1251        std::env::set_var(key, val);
1252    }
1253    Ok(Value::Undef)
1254}
1255
1256/// The event name argument of an EventEmitter-style `process` call.
1257fn event_name(args: &[Value]) -> String {
1258    args.first()
1259        .map(|v| with_host(|h| h.str_of(v)))
1260        .unwrap_or_default()
1261}
1262
1263// ── process.stdin as a readable stream ───────────────────────────────────────
1264//
1265// Standard input is read by a PUMP: a macrotask that reads one chunk (blocking,
1266// up to 64 KiB, through Rust's shared stdin buffer, which `readline` and
1267// `fs.readFileSync(0)` read through too) and re-arms itself until end of
1268// input. What a chunk becomes depends on how the program consumes the stream,
1269// as in node: a `'data'` listener or `resume()` makes it FLOW (each chunk is a
1270// `'data'` event), a `'readable'` listener buffers it for `read()`, and an
1271// async iterator hands it to `next()`. End of input is `'end'`, then `'close'`.
1272
1273/// The state of `process.stdin`.
1274#[derive(Default)]
1275struct Stdin {
1276    /// Registered `(event, callback, once)` listeners, in call order.
1277    listeners: Vec<(String, Value, bool)>,
1278    /// `setEncoding`: chunks are strings in this encoding instead of Buffers.
1279    encoding: Option<String>,
1280    /// The tail of a UTF-8 sequence a chunk split, held for the next one.
1281    partial: Vec<u8>,
1282    /// The pump has been scheduled and has not reached end of input.
1283    pumping: bool,
1284    /// `pause()` stops the pump; `resume()` restarts it.
1285    paused: bool,
1286    flowing: bool,
1287    readable_mode: bool,
1288    /// Chunks waiting for `read()` or an async iterator's `next()`.
1289    buffered: std::collections::VecDeque<Value>,
1290    waiters: std::collections::VecDeque<u32>,
1291    iterating: bool,
1292    ended: bool,
1293    /// Where `pipe(dest)` sends each chunk.
1294    pipe_dest: Option<Value>,
1295}
1296
1297thread_local! {
1298    static STDIN: std::cell::RefCell<Stdin> = std::cell::RefCell::new(Stdin::default());
1299}
1300
1301fn with_stdin<R>(f: impl FnOnce(&mut Stdin) -> R) -> R {
1302    STDIN.with(|s| f(&mut s.borrow_mut()))
1303}
1304
1305/// Schedule the pump if it is not running and the stream is not paused.
1306fn start_stdin() {
1307    let start = with_stdin(|s| {
1308        if s.pumping || s.paused || s.ended {
1309            return false;
1310        }
1311        s.pumping = true;
1312        true
1313    });
1314    if start {
1315        schedule_stdin_pump();
1316    }
1317}
1318
1319fn schedule_stdin_pump() {
1320    with_host(|h| {
1321        let cb = h.alloc(JsObj::Builtin("process.@@stdinPump".into()));
1322        h.add_timer(-1.0, cb, Vec::new(), None);
1323    });
1324}
1325
1326fn emit_stdin(recv: &Value, event: &str, args: Vec<Value>) -> Result<(), String> {
1327    let entries: Vec<(Value, bool)> = with_stdin(|s| {
1328        let found = s
1329            .listeners
1330            .iter()
1331            .filter(|(e, _, _)| e == event)
1332            .map(|(_, cb, once)| (cb.clone(), *once))
1333            .collect();
1334        s.listeners.retain(|(e, _, once)| !(e == event && *once));
1335        found
1336    });
1337    for (cb, _) in entries {
1338        crate::host::invoke(&cb, args.clone(), Some(recv.clone()))?;
1339    }
1340    Ok(())
1341}
1342
1343/// A chunk as the program sees it: a Buffer, or a string under `setEncoding`.
1344/// A UTF-8 sequence split between two reads is held back and completed by the
1345/// next one, as node's `StringDecoder` does.
1346fn stdin_chunk(bytes: &[u8]) -> Option<Value> {
1347    let enc = with_stdin(|s| s.encoding.clone());
1348    let Some(enc) = enc else {
1349        return Some(super::buffer::from_bytes(bytes));
1350    };
1351    if enc != "utf8" && enc != "utf-8" {
1352        return Some(with_host(|h| {
1353            h.new_str(super::buffer::encode_bytes(bytes, &enc))
1354        }));
1355    }
1356    let mut all = with_stdin(|s| std::mem::take(&mut s.partial));
1357    all.extend_from_slice(bytes);
1358    let keep = incomplete_utf8_tail(&all);
1359    let tail = all.split_off(all.len() - keep);
1360    with_stdin(|s| s.partial = tail);
1361    if all.is_empty() {
1362        return None;
1363    }
1364    Some(with_host(|h| {
1365        h.new_str(String::from_utf8_lossy(&all).into_owned())
1366    }))
1367}
1368
1369/// How many bytes at the end of `b` begin a UTF-8 sequence that is not yet
1370/// complete.
1371fn incomplete_utf8_tail(b: &[u8]) -> usize {
1372    for back in 1..=3.min(b.len()) {
1373        let c = b[b.len() - back];
1374        if c & 0xC0 == 0x80 {
1375            continue;
1376        }
1377        let need = match c {
1378            0xC0..=0xDF => 2,
1379            0xE0..=0xEF => 3,
1380            0xF0..=0xF7 => 4,
1381            _ => 1,
1382        };
1383        return if need > back { back } else { 0 };
1384    }
1385    0
1386}
1387
1388/// One turn of the stdin pump.
1389fn stdin_pump() -> Result<(), String> {
1390    use std::io::Read;
1391    let recv = stdin_value();
1392    if with_stdin(|s| s.paused) {
1393        with_stdin(|s| s.pumping = false);
1394        return Ok(());
1395    }
1396    let mut buf = vec![0u8; 65536];
1397    let n = std::io::stdin().lock().read(&mut buf).unwrap_or(0);
1398    if n == 0 {
1399        return stdin_end(&recv);
1400    }
1401    schedule_stdin_pump();
1402    let Some(chunk) = stdin_chunk(&buf[..n]) else {
1403        return Ok(());
1404    };
1405    stdin_deliver(&recv, chunk)
1406}
1407
1408/// Hand a chunk to whatever is consuming the stream.
1409fn stdin_deliver(recv: &Value, chunk: Value) -> Result<(), String> {
1410    if let Some(dest) = with_stdin(|s| s.pipe_dest.clone()) {
1411        crate::host::call_method(&dest, "write", vec![chunk.clone()])?;
1412    }
1413    let (flowing, readable, waiter) = with_stdin(|s| {
1414        let waiter = if s.iterating {
1415            s.waiters.pop_front()
1416        } else {
1417            None
1418        };
1419        (s.flowing, s.readable_mode, waiter)
1420    });
1421    if let Some(id) = waiter {
1422        crate::host::resolve_promise_val(id, stdin_iter_result(chunk, false));
1423        return Ok(());
1424    }
1425    if flowing {
1426        emit_stdin(recv, "data", vec![chunk])?;
1427    } else if readable || with_stdin(|s| s.iterating) {
1428        with_stdin(|s| s.buffered.push_back(chunk));
1429        if readable {
1430            emit_stdin(recv, "readable", Vec::new())?;
1431        }
1432    }
1433    Ok(())
1434}
1435
1436/// End of input: a last `'readable'` (for `read()` to report `null`), `'end'`,
1437/// the end of any async iteration, then `'close'` on a later tick.
1438fn stdin_end(recv: &Value) -> Result<(), String> {
1439    let (readable, waiters, dest) = with_stdin(|s| {
1440        s.ended = true;
1441        s.pumping = false;
1442        (
1443            s.readable_mode,
1444            std::mem::take(&mut s.waiters),
1445            s.pipe_dest.clone(),
1446        )
1447    });
1448    if readable {
1449        emit_stdin(recv, "readable", Vec::new())?;
1450    }
1451    for id in waiters {
1452        crate::host::resolve_promise_val(id, stdin_iter_result(Value::Undef, true));
1453    }
1454    // `pipe` ends its destination, except the process's own stdout/stderr.
1455    if let Some(dest) = dest {
1456        let is_std = with_host(|h| match h.get(&dest) {
1457            Some(JsObj::Object(p)) => {
1458                p.get("@@native").map(|v| h.str_of(v)).as_deref() == Some("WriteStream")
1459            }
1460            _ => false,
1461        });
1462        if !is_std {
1463            crate::host::call_method(&dest, "end", Vec::new())?;
1464        }
1465    }
1466    emit_stdin(recv, "end", Vec::new())?;
1467    let target = recv.clone();
1468    with_host(|h| h.queue_micro_native(Box::new(move || emit_stdin(&target, "close", Vec::new()))));
1469    Ok(())
1470}
1471
1472fn stdin_iter_result(value: Value, done: bool) -> Value {
1473    with_host(|h| {
1474        let mut m = IndexMap::new();
1475        m.insert("value".into(), value);
1476        m.insert("done".into(), Value::Bool(done));
1477        h.new_object(m)
1478    })
1479}
1480
1481/// The `process.stdin` object.
1482fn stdin_value() -> Value {
1483    constant("stdin").unwrap_or(Value::Undef)
1484}
1485
1486/// The methods `process.stdin` answers as a readable stream. `None` hands the
1487/// call to the shared `WriteStream` surface.
1488fn stdin_call(recv: &Value, method: &str, args: &[Value]) -> Option<Result<Value, String>> {
1489    let str_arg = |i: usize| with_host(|h| args.get(i).map(|v| h.str_of(v)).unwrap_or_default());
1490    Some(match method {
1491        "on" | "once" | "addListener" | "prependListener" => {
1492            let event = str_arg(0);
1493            if let Some(cb) = args.get(1).cloned() {
1494                with_stdin(|s| {
1495                    let entry = (event.clone(), cb, method == "once");
1496                    if method == "prependListener" {
1497                        s.listeners.insert(0, entry);
1498                    } else {
1499                        s.listeners.push(entry);
1500                    }
1501                    match event.as_str() {
1502                        "data" => s.flowing = true,
1503                        "readable" => s.readable_mode = true,
1504                        _ => {}
1505                    }
1506                });
1507                if matches!(event.as_str(), "data" | "readable") {
1508                    start_stdin();
1509                }
1510            }
1511            Ok(recv.clone())
1512        }
1513        "removeListener" | "off" => {
1514            let event = str_arg(0);
1515            if let Some(cb) = args.get(1) {
1516                with_stdin(|s| {
1517                    let pos = s
1518                        .listeners
1519                        .iter()
1520                        .position(|(e, f, _)| *e == event && with_host(|h| h.strict_eq(f, cb)));
1521                    if let Some(i) = pos {
1522                        s.listeners.remove(i);
1523                    }
1524                });
1525            }
1526            Ok(recv.clone())
1527        }
1528        "removeAllListeners" => {
1529            let event = args
1530                .first()
1531                .filter(|v| !matches!(v, Value::Undef))
1532                .map(|_| str_arg(0));
1533            with_stdin(|s| match &event {
1534                Some(e) => s.listeners.retain(|(x, _, _)| x != e),
1535                None => s.listeners.clear(),
1536            });
1537            Ok(recv.clone())
1538        }
1539        "listenerCount" => {
1540            let event = str_arg(0);
1541            let n = with_stdin(|s| s.listeners.iter().filter(|(e, _, _)| *e == event).count());
1542            Ok(Value::Float(n as f64))
1543        }
1544        "setEncoding" => {
1545            let enc = match args.first() {
1546                Some(v) if !matches!(v, Value::Undef) => str_arg(0).to_ascii_lowercase(),
1547                _ => "utf8".to_string(),
1548            };
1549            with_stdin(|s| s.encoding = Some(enc));
1550            Ok(recv.clone())
1551        }
1552        "resume" => {
1553            with_stdin(|s| {
1554                s.paused = false;
1555                s.flowing = true;
1556            });
1557            start_stdin();
1558            Ok(recv.clone())
1559        }
1560        "pause" => {
1561            with_stdin(|s| s.paused = true);
1562            Ok(recv.clone())
1563        }
1564        "read" => {
1565            let chunk = with_stdin(|s| {
1566                let parts: Vec<Value> = s.buffered.drain(..).collect();
1567                (!parts.is_empty()).then_some(parts)
1568            });
1569            match chunk {
1570                None => Ok(with_host(|h| h.null())),
1571                Some(parts) if parts.len() == 1 => {
1572                    Ok(parts.into_iter().next().unwrap_or(Value::Undef))
1573                }
1574                Some(parts) => {
1575                    // Several buffered chunks come back joined, as node's
1576                    // `read()` with no size returns everything buffered.
1577                    if with_stdin(|s| s.encoding.is_some()) {
1578                        let joined: String =
1579                            parts.iter().map(|p| with_host(|h| h.str_of(p))).collect();
1580                        Ok(with_host(|h| h.new_str(joined)))
1581                    } else {
1582                        let bytes: Vec<u8> = parts
1583                            .iter()
1584                            .flat_map(|p| super::buffer::view_bytes(p).unwrap_or_default())
1585                            .collect();
1586                        Ok(super::buffer::from_bytes(&bytes))
1587                    }
1588                }
1589            }
1590        }
1591        "pipe" => {
1592            let dest = args.first().cloned().unwrap_or(Value::Undef);
1593            with_stdin(|s| {
1594                s.pipe_dest = Some(dest.clone());
1595                s.flowing = true;
1596            });
1597            start_stdin();
1598            Ok(dest)
1599        }
1600        "@@asyncIterator" => {
1601            with_stdin(|s| s.iterating = true);
1602            start_stdin();
1603            Ok(with_host(|h| {
1604                let mut m = IndexMap::new();
1605                m.insert("@@native".into(), h.new_str("StdinIterator"));
1606                h.new_object(m)
1607            }))
1608        }
1609        _ => return None,
1610    })
1611}
1612
1613/// The methods of the async iterator `for await (const chunk of process.stdin)`
1614/// reads.
1615pub const STDIN_ITERATOR_METHODS: &[&str] = &["next", "return", "@@asyncIterator"];
1616
1617pub fn stdin_iterator_call(recv: &Value, method: &str) -> Result<Value, String> {
1618    match method {
1619        "@@asyncIterator" => Ok(recv.clone()),
1620        "next" => {
1621            let ready = with_stdin(|s| match s.buffered.pop_front() {
1622                Some(c) => Some(Some(c)),
1623                None if s.ended => Some(None),
1624                None => None,
1625            });
1626            match ready {
1627                Some(Some(c)) => crate::builtins::promise_resolve_pub(stdin_iter_result(c, false)),
1628                Some(None) => {
1629                    crate::builtins::promise_resolve_pub(stdin_iter_result(Value::Undef, true))
1630                }
1631                None => {
1632                    let (promise, id) = with_host(|h| {
1633                        let p = h.new_promise();
1634                        let id = h.promise_id(&p).unwrap_or(0);
1635                        (p, id)
1636                    });
1637                    with_stdin(|s| s.waiters.push_back(id));
1638                    Ok(promise)
1639                }
1640            }
1641        }
1642        // Leaving the loop early stops reading, as destroying the stream does.
1643        "return" => {
1644            with_stdin(|s| {
1645                s.iterating = false;
1646                s.paused = true;
1647            });
1648            crate::builtins::promise_resolve_pub(stdin_iter_result(Value::Undef, true))
1649        }
1650        _ => Err(crate::host::type_error(&format!(
1651            "{method} is not a function"
1652        ))),
1653    }
1654}