// The guest-visible `process`: one EventEmitter per worker, installed as the global, // passed to the CJS entry wrapper and served as node:process. argv[0] is "node", // argv[1] the entry path. The version strings name the Node line this runtime // targets for compatibility (DESIGN §Event loop); they are not a claim to be Node. import type { KernelToRuntimeInit, Signal, SyscallSpec } from "../shared/protocol"; import { SCHEME_ONLY_BUILTINS, isBuiltinId } from "../shared/builtin-ids"; import { uvError } from "../shared/nodeerr"; import { ARCH, GLIBC_VERSION, NODE_VERSION, PLATFORM } from "../shared/platform"; import { SIGNALS, isSignal } from "../shared/protocol"; import { invalidArgType } from "./errors"; import { EventEmitter } from "./events"; import type { Loop } from "./loop"; import { refused } from "./refuse"; import type { StdinStream } from "./stdin"; import type { StdioWriter } from "./stdout"; import { parseEnv } from "./util/parse-env"; // package.json's version, inlined by build.mjs. declare const __NIB_VERSION__: string; const NIB_VERSION = __NIB_VERSION__; interface CpuUsage { user: number; system: number; } interface ResourceUsage { userCPUTime: number; systemCPUTime: number; maxRSS: number; sharedMemorySize: number; unsharedDataSize: number; unsharedStackSize: number; minorPageFault: number; majorPageFault: number; swappedOut: number; fsRead: number; fsWrite: number; ipcSent: number; ipcReceived: number; signalsCount: number; voluntaryContextSwitches: number; involuntaryContextSwitches: number; } interface MemoryUsage { rss: number; heapTotal: number; heapUsed: number; external: number; arrayBuffers: number; } export interface GuestProcess extends EventEmitter { title: string; argv: string[]; argv0: string; execArgv: string[]; execPath: string; env: Record; pid: number; ppid: number; platform: string; arch: string; version: string; versions: Record; release: { name: string }; exitCode: number | undefined; stdin: StdinStream; stdout: StdioWriter; stderr: StdioWriter; cwd(): string; chdir(dir: string): void; nextTick(fn: (...args: any[]) => void, ...args: unknown[]): void; exit(code?: number): never; kill(pid: number, signal?: string | number): true; hrtime: { (prev?: [number, number]): [number, number]; bigint(): bigint }; uptime(): number; emitWarning(warning: string | Error, typeOrOptions?: string | WarningOptions, code?: string): void; getBuiltinModule(id: unknown): unknown; dlopen(...args: unknown[]): never; execve(...args: unknown[]): never; initgroups(...args: unknown[]): never; setegid(...args: unknown[]): never; seteuid(...args: unknown[]): never; setgid(...args: unknown[]): never; setgroups(...args: unknown[]): never; setuid(...args: unknown[]): never; getuid(): number; geteuid(): number; getgid(): number; getegid(): number; getgroups(): number[]; umask(mask?: unknown): number; ref(maybeRefable: unknown): void; unref(maybeRefable: unknown): void; finalization: { register(ref: unknown, callback: (ref: unknown, event?: "beforeExit") => void): void; registerBeforeExit(ref: unknown, callback: (ref: unknown, event?: "beforeExit") => void): void; unregister(ref: unknown): void; }; getActiveResourcesInfo(): string[]; hasUncaughtExceptionCaptureCallback(): boolean; setUncaughtExceptionCaptureCallback(fn: unknown): void; setSourceMapsEnabled(enabled: unknown): void; sourceMapsEnabled: boolean; abort(): never; debugPort: number; features: Record; config: Record; report: Record; allowedNodeEnvironmentFlags: Set; loadEnvFile(path?: unknown): void; memoryUsage: (() => MemoryUsage) & { rss(): number }; availableMemory(): number; constrainedMemory(): number; resourceUsage(): ResourceUsage; cpuUsage(previousValue?: CpuUsage): CpuUsage; threadCpuUsage(previousValue?: CpuUsage): CpuUsage; } interface WarningOptions { type?: string; code?: string; detail?: string; } export interface ProcessDeps { msg: KernelToRuntimeInit; /** A worker_threads thread: signals are not delivered to it, and it has no cwd of its own. */ isThread: boolean; loop: Loop; callSync(op: K, args: SyscallSpec[K]["args"]): SyscallSpec[K]["ret"]; cwd: { get(): string; set(dir: string): void }; /** Writes to fd 2 that bypass process.stderr, as Node's own warnings do. */ emitStderr(bytes: Uint8Array): void; /** Report a throw from an 'exit' listener (Node prints it as uncaught). */ reportFatal(error: unknown): void; /** Never returns: the kernel terminates the worker. */ exitWorker(code: number): never; /** Loads (and caches) a known builtin by its bare id; the caller has already * resolved the `node:` prefix and confirmed the id exists. */ getBuiltinModule(id: string): unknown; stdin: StdinStream; stdout: StdioWriter; stderr: StdioWriter; } const enc = new TextEncoder(); const SIGNAL_NAMES = new Map(Object.entries(SIGNALS).map(([name, n]) => [n as number, name as Signal])); const ERRNO: Record = { ESRCH: 3, EINVAL: 22, EPERM: 1 }; /** Node's ErrnoException: `syscall CODE`, a negative errno. */ function errnoException(code: string, syscall: string): Error { return Object.assign(new Error(`${syscall} ${code}`), { errno: -(ERRNO[code] ?? 0), code, syscall }); } const OPEN_READ = { read: true, write: false, append: false, trunc: false, create: false, excl: false }; /** The runtime's half of signal delivery, run at poll-phase entry: each signal pending * in the mailbox (a signal raised twice before then is pending once) becomes a poll * callback that runs its listeners. */ /** setUncaughtExceptionCaptureCallback's callback per process: main.ts's uncaught() * hands an uncaught error to it instead of emitting 'uncaughtException', as Node does. */ export const uncaughtCaptures = new WeakMap void>(); export function signalReceiver(proc: GuestProcess, loop: Loop, take: () => number[]): () => void { return () => { for (const n of take()) { const sig = SIGNAL_NAMES.get(n)!; loop.enqueueCompletion(() => { // The last listener went away while this was pending: the default action applies. if (proc.listenerCount(sig) === 0) proc.kill(proc.pid, sig); else proc.emit(sig, sig, n); }); } }; } export function makeProcess(deps: ProcessDeps): GuestProcess { const { msg, loop } = deps; const proc = new EventEmitter() as GuestProcess; let exiting = false; let warned = false; let umaskValue = 0o22; let sourceMapsEnabledValue = false; const finalizationRegistry = new FinalizationRegistry<() => void>((run) => run()); // registerBeforeExit's callbacks still pending GC when the process exits: run() // flushes and clears them so a later GC of the same ref can't run it twice. const beforeExitFinalizers = new Map void>(); const experimentalWarned = new Set(); const warnExperimental = (name: string): void => { if (experimentalWarned.has(name)) return; experimentalWarned.add(name); proc.emitWarning(`${name} is an experimental feature and might change at any time`, "ExperimentalWarning"); }; const nextTick = (fn: (...args: any[]) => void, ...args: unknown[]): void => { if (typeof fn !== "function") { throw Object.assign(new TypeError(`The "callback" argument must be of type function. Received ${typeof fn}`), { code: "ERR_INVALID_ARG_TYPE", }); } loop.nextTick(args.length ? () => fn(...args) : fn); }; const nowNs = (): bigint => BigInt(Math.round(performance.now() * 1e6)); const hrtime = (prev?: [number, number]): [number, number] => { const ns = nowNs(); let s = Number(ns / 1_000_000_000n); let n = Number(ns % 1_000_000_000n); if (prev) { s -= prev[0]; n -= prev[1]; if (n < 0) { s--; n += 1e9; } } return [s, n]; }; hrtime.bigint = nowNs; // The kernel delivers a signal only while a listener exists, so the first listener // for a signal and the removal of the last are both reported. if (!deps.isThread) proc.on("newListener", (event: string | symbol) => { if (isSignal(event) && proc.listenerCount(event) === 0) deps.callSync("sigaction", { sig: event, handled: true }); }); if (!deps.isThread) proc.on("removeListener", (event: string | symbol) => { if (isSignal(event) && proc.listenerCount(event) === 0) deps.callSync("sigaction", { sig: event, handled: false }); }); return Object.assign(proc, { title: "node", argv: ["node", msg.entry, ...msg.argv], argv0: "node", execArgv: [], execPath: "/usr/local/bin/node", env: msg.env, pid: msg.pid, // No parent pid is tracked yet (a worker_thread shares its process's pid, so this // is right for every thread of the main process; a spawned child process's real // ppid needs the kernel to pass it down init — not wired, so every process here // reports 0, same as the real root of a process tree). ppid: 0, platform: PLATFORM, arch: ARCH, version: `v${NODE_VERSION}`, versions: { node: NODE_VERSION, nib: NIB_VERSION }, release: { name: "node" }, exitCode: undefined, stdin: deps.stdin, stdout: deps.stdout, stderr: deps.stderr, cwd: () => deps.cwd.get(), chdir(dir: string): void { if (deps.isThread) { throw Object.assign(new TypeError("process.chdir() is not supported in workers"), { code: "ERR_WORKER_UNSUPPORTED_OPERATION" }); } const target = dir.startsWith("/") ? dir : `${deps.cwd.get()}/${dir}`; let code: string | null = null; try { if (deps.callSync("stat", { path: target, follow: true }).kind !== "dir") code = "ENOTDIR"; } catch (e) { code = (e as { code?: string }).code ?? "ENOENT"; } if (code) { const text = code === "ENOTDIR" ? "not a directory" : "no such file or directory"; const from = deps.cwd.get(); throw Object.assign(new Error(`${code}: ${text}, chdir '${from}' -> '${dir}'`), { code, syscall: "chdir", path: from, dest: dir, }); } // getcwd(3) answers the physical path, so a chdir through a link lands on its target. deps.cwd.set(deps.callSync("realpath", { path: target }).path); }, nextTick, exit(code?: number): never { if (code !== undefined) proc.exitCode = code; if (!exiting) { exiting = true; // finalization.registerBeforeExit: everything still pending GC runs now, // before 'exit' — Node runs these ahead of the exit event too. for (const run of beforeExitFinalizers.values()) run(); beforeExitFinalizers.clear(); try { proc.emit("exit", proc.exitCode ?? 0); } catch (e) { deps.reportFatal(e); proc.exitCode = 1; } } return deps.exitWorker(proc.exitCode ?? 0); }, kill(pid: number, signal?: string | number): true { // Node's validation order and messages (lib/internal/process/per_thread.js). // eslint-disable-next-line eqeqeq if (pid != (pid | 0)) throw invalidArgType("pid", "of type number", pid); let sig: Signal | null; if (typeof signal === "number" && signal === (signal | 0)) { if (signal === 0) sig = null; else { const name = SIGNAL_NAMES.get(signal); if (!name) throw errnoException("EINVAL", "kill"); sig = name; } } else { const name = signal || "SIGTERM"; if (!isSignal(name)) { throw Object.assign(new TypeError(`Unknown signal: ${String(name)}`), { code: "ERR_UNKNOWN_SIGNAL" }); } sig = name; } try { deps.callSync("kill", { pid, sig }); } catch (e) { throw errnoException((e as { code?: string }).code ?? "EINVAL", "kill"); } return true; }, hrtime, uptime: () => performance.now() / 1000, // Node resolves a bare id ('fs') and its 'node:'-prefixed form the same way, // except for a scheme-only built-in (node:test, node:test/reporters, node:sea), // which only resolves with the prefix — confirmed against real Node 24.16.0: // process.getBuiltinModule('test') is undefined, process.getBuiltinModule('node:test') // is the real module. An unknown id is undefined, never a throw. getBuiltinModule(id: unknown): unknown { if (typeof id !== "string") throw invalidArgType("id", "of type string", id); const prefixed = id.startsWith("node:"); const bare = prefixed ? id.slice(5) : id; if (!isBuiltinId(bare)) return undefined; if (!prefixed && SCHEME_ONLY_BUILTINS.has(bare)) return undefined; return deps.getBuiltinModule(bare); }, emitWarning(warning: string | Error, typeOrOptions?: string | WarningOptions, code?: string): void { const options = typeof typeOrOptions === "object" ? typeOrOptions : { type: typeOrOptions, code }; const w = warning instanceof Error ? warning : new Error(String(warning)); if (!(warning instanceof Error)) w.name = options.type ?? "Warning"; if (options.code) (w as Error & { code?: string }).code = options.code; nextTick(() => { proc.emit("warning", w); const tag = options.code ? `[${options.code}] ` : ""; let text = `(node:${msg.pid}) ${tag}${w.name}: ${w.message}\n`; if (options.detail) text += `${options.detail}\n`; if (!warned) { warned = true; const flag = w.name === "DeprecationWarning" ? "--trace-deprecation" : "--trace-warnings"; text += `(Use \`node ${flag} ...\` to show where the warning was created)\n`; } deps.emitStderr(enc.encode(text)); }); }, dlopen: refused("process", "dlopen", "no native addons"), execve: refused("process", "execve", "a worker can't replace its own image"), initgroups: refused("process", "initgroups", "a page has no users to switch to"), setegid: refused("process", "setegid", "a page has no users to switch to"), seteuid: refused("process", "seteuid", "a page has no users to switch to"), setgid: refused("process", "setgid", "a page has no users to switch to"), setgroups: refused("process", "setgroups", "a page has no users to switch to"), setuid: refused("process", "setuid", "a page has no users to switch to"), // Everyone is uid/gid 0 here (DESIGN: matches what stat reports), so these agree. getuid: () => 0, geteuid: () => 0, getgid: () => 0, getegid: () => 0, getgroups: () => [0], umask(mask?: unknown): number { const prev = umaskValue; if (mask === undefined) return prev; let next: number; if (typeof mask === "string") { if (!/^[0-7]+$/.test(mask)) { throw Object.assign( new TypeError(`The argument 'mask' must be a 32-bit unsigned integer or an octal string. Received '${mask}'`), { code: "ERR_INVALID_ARG_VALUE" }, ); } next = Number.parseInt(mask, 8); } else if (typeof mask === "number" && Number.isInteger(mask)) { next = mask; } else { throw invalidArgType("mask", "a 32-bit unsigned integer or an octal string", mask); } // Node accepts the full 32-bit range here and masks to the low 9 bits when // storing — only out-of-32-bit-range or negative actually throws. if (next < 0 || next > 0xffffffff) { throw Object.assign(new RangeError(`The value of "mask" is out of range. It must be >= 0 && <= 4294967295. Received ${next}`), { code: "ERR_OUT_OF_RANGE" }); } umaskValue = next & 0o777; return prev; }, // Node's generic ref/unref: call the method (or the equally-named well-known // symbol) on anything that has one, and return nothing — a helper for things // like AbortSignal-driven timers, not specific to one handle type. ref(maybeRefable: unknown): void { if (maybeRefable === null || maybeRefable === undefined) return; const o = maybeRefable as Record; const fn = o[Symbol.for("nodejs.ref")] ?? o.ref; if (typeof fn === "function") fn.call(maybeRefable); }, unref(maybeRefable: unknown): void { if (maybeRefable === null || maybeRefable === undefined) return; const o = maybeRefable as Record; const fn = o[Symbol.for("nodejs.unref")] ?? o.unref; if (typeof fn === "function") fn.call(maybeRefable); }, // On FinalizationRegistry (DESIGN primitive list): register's callback only runs // on GC, same as the registry's own contract; registerBeforeExit's also runs at // process exit for anything GC hasn't reclaimed yet (proc.exit() above flushes // beforeExitFinalizers before 'exit'). finalization: { register(ref: unknown, callback: (ref: unknown, event?: "beforeExit") => void): void { warnExperimental("process.finalization.register"); if (ref === null || (typeof ref !== "object" && typeof ref !== "function")) { throw invalidArgType("obj", "of type object", ref); } // Node itself doesn't validate `callback` up front either — a non-function // only fails when GC (or exit, for registerBeforeExit) actually calls it. finalizationRegistry.register(ref, () => (callback as (ref: unknown) => void)(ref), ref); }, registerBeforeExit(ref: unknown, callback: (ref: unknown, event?: "beforeExit") => void): void { warnExperimental("process.finalization.registerBeforeExit"); if (ref === null || (typeof ref !== "object" && typeof ref !== "function")) { throw invalidArgType("obj", "of type object", ref); } const run = (): void => { beforeExitFinalizers.delete(ref); (callback as (ref: unknown, event: "beforeExit") => void)(ref, "beforeExit"); }; beforeExitFinalizers.set(ref, run); finalizationRegistry.register(ref, run, ref); }, unregister(ref: unknown): void { warnExperimental("process.finalization.unregister"); if (ref !== null && (typeof ref === "object" || typeof ref === "function")) finalizationRegistry.unregister(ref); beforeExitFinalizers.delete(ref); }, }, // Partial: only the timers/immediates this loop owns, no TCP/TTY/etc. handle // table exists to report on. getActiveResourcesInfo: () => loop.activeResourcesInfo(), hasUncaughtExceptionCaptureCallback: () => uncaughtCaptures.has(proc), setUncaughtExceptionCaptureCallback(fn: unknown): void { if (fn !== null && typeof fn !== "function") { throw Object.assign(new TypeError('The "fn" argument must be of type function'), { code: "ERR_INVALID_ARG_TYPE" }); } if (fn !== null && uncaughtCaptures.has(proc)) { throw Object.assign(new Error("`process.setupUncaughtExceptionCapture()` was called while a capture callback was already active"), { code: "ERR_UNCAUGHT_EXCEPTION_CAPTURE_ALREADY_SET" }); } if (fn === null) uncaughtCaptures.delete(proc); else uncaughtCaptures.set(proc, fn as (err: unknown) => void); }, // State only: stack traces aren't source-mapped either way here (no real effect, // noted in the chunk report). setSourceMapsEnabled(enabled: unknown): void { sourceMapsEnabledValue = Boolean(enabled); proc.sourceMapsEnabled = sourceMapsEnabledValue; }, sourceMapsEnabled: false, // Node's abort(3) always kills the process, bypassing any signal handler — unlike // proc.kill(), which asks the kernel to deliver SIGABRT and would let a handler // take it. This goes straight to exitWorker with Node's signal-exit code. abort(): never { return deps.exitWorker(128 + SIGNALS.SIGABRT); }, // Node's static default (whether or not --inspect is passed); this runtime has // no inspector listening on it. debugPort: 9229, features: { inspector: false, debug: false, uv: false, ipv6: true, tls_alpn: false, tls_sni: false, tls_ocsp: false, tls: false, openssl_is_boringssl: false, cached_builtins: true, require_module: true, quic: false, typescript: "strip", }, // Node's process.config is the binary's own build configuration (gyp variables); // nothing here was built that way, so there is nothing honest to report beyond // an empty, Node-shaped stub. config: { target_defaults: {}, variables: {} }, report: { reportOnFatalError: false, reportOnSignal: false, reportOnUncaughtException: false, signal: "SIGUSR2", compact: false, excludeNetwork: false, excludeEnv: false, directory: "", filename: "", writeReport: refused("process", "report.writeReport", "no native diagnostic report generator"), // The header is what native-addon loaders read: napi-rs and detect-libc tell glibc // from musl by glibcVersionRuntime. The native sections have nothing to describe. getReport(err?: unknown): Record { const now = new Date(); return { header: { reportVersion: 5, event: "JavaScript API", trigger: "GetReport", filename: null, dumpEventTime: now.toISOString().slice(0, 19) + "Z", dumpEventTimeStamp: String(now.getTime()), processId: proc.pid, threadId: 0, cwd: deps.cwd.get(), commandLine: [...proc.argv], nodejsVersion: proc.version, glibcVersionRuntime: GLIBC_VERSION, glibcVersionCompiler: "2.28", wordSize: 64, arch: ARCH, platform: PLATFORM, componentVersions: { ...proc.versions }, release: { ...proc.release }, osName: "Linux", osRelease: "6.0.0-nib", osVersion: "#1 SMP nib", osMachine: "x86_64", cpus: [], networkInterfaces: [], host: "localhost", }, javascriptStack: { message: err instanceof Error ? `${err.name}: ${err.message}` : "No stack.", stack: err instanceof Error ? (err.stack ?? "").split("\n").slice(1).map((l) => l.trim()) : ["Unavailable."], errorProperties: {} }, javascriptHeap: {}, nativeStack: [], resourceUsage: {}, uvthreadResourceUsage: {}, libuv: [], workers: [], environmentVariables: { ...proc.env }, userLimits: {}, sharedObjects: [], }; }, }, // Node's is a Set of every NODE_OPTIONS-recognized flag string; this runtime // parses none of them, so an honest answer is "none known" rather than Node's list. allowedNodeEnvironmentFlags: new Set(), loadEnvFile(path?: unknown): void { const given = path === undefined ? ".env" : String(path); const abs = given.startsWith("/") ? given : `${deps.cwd.get()}/${given}`; let fd: number; try { fd = deps.callSync("open", { path: abs, flags: OPEN_READ, mode: 0 }).fd; } catch (e) { throw uvError({ code: ((e as { code?: string }).code as Parameters[0]["code"]) ?? "ENOENT", syscall: "open", path: given }); } const chunks: Uint8Array[] = []; for (;;) { const r = deps.callSync("read", { fd, len: 65536, pos: null }); if (r.bytes.length) chunks.push(r.bytes); if (r.eof || r.bytes.length === 0) break; } deps.callSync("close", { fd }); const total = chunks.reduce((n, c) => n + c.length, 0); const bytes = new Uint8Array(total); let at = 0; for (const c of chunks) { bytes.set(c, at); at += c.length; } for (const [key, value] of Object.entries(parseEnv(new TextDecoder().decode(bytes)))) { if (!Object.hasOwn(proc.env, key)) proc.env[key] = value; } }, // Chromium's performance.memory (Window-only — absent from dedicated Workers, // where this code runs) is the one non-standard primitive that could give real // JS-heap numbers; nothing reaches this worker, so there is nothing honest to // report but zero (Node's own "unsupported platform" sentinel for // availableMemory/constrainedMemory, extended here to the rest for consistency). memoryUsage: Object.assign( (): MemoryUsage => ({ rss: 0, heapTotal: 0, heapUsed: 0, external: 0, arrayBuffers: 0 }), { rss: (): number => 0 }, ), availableMemory: () => 0, constrainedMemory: () => 0, resourceUsage: (): ResourceUsage => ({ userCPUTime: 0, systemCPUTime: 0, maxRSS: 0, sharedMemorySize: 0, unsharedDataSize: 0, unsharedStackSize: 0, minorPageFault: 0, majorPageFault: 0, swappedOut: 0, fsRead: 0, fsWrite: 0, ipcSent: 0, ipcReceived: 0, signalsCount: 0, voluntaryContextSwitches: 0, involuntaryContextSwitches: 0, }), // No per-process or per-thread CPU-time primitive reaches a page (performance.now() // is wall-clock, not CPU time); zero is honest, not a measurement. cpuUsage: (previousValue?: CpuUsage): CpuUsage => diffCpuUsage(previousValue), threadCpuUsage: (previousValue?: CpuUsage): CpuUsage => diffCpuUsage(previousValue), }); } function diffCpuUsage(previousValue?: CpuUsage): CpuUsage { if (previousValue === undefined) return { user: 0, system: 0 }; if (typeof previousValue !== "object" || typeof previousValue.user !== "number" || typeof previousValue.system !== "number") { throw invalidArgType("prevValue", "an object with user and system properties", previousValue); } return { user: 0, system: 0 }; }