diff --git a/CHANGELOG.md b/CHANGELOG.md index 8f4372e6dc06..6dd6f2e48b3f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -157,3 +157,5 @@ - Support `ws` server-connection `pause()`, `resume()`, and `isPaused` with native socket read backpressure. - Validate `net`, `http`, and `https` listen ports synchronously before binding, preserving asynchronous bind errors and Node-compatible numeric string parsing. Adapts string routing from [oven-sh/bun#34083](https://github.com/oven-sh/bun/pull/34083). Thanks @robobun! + +- Match Node process property descriptors, including lazy `argv`/`execArgv` data properties and descriptor replacement in native argument readers. Adapts [oven-sh/bun#34229](https://github.com/oven-sh/bun/pull/34229) and [oven-sh/bun#44356](https://github.com/oven-sh/bun/pull/44356). Thanks @robobun! diff --git a/docs/runtime/nodejs-compat.mdx b/docs/runtime/nodejs-compat.mdx index 521631950382..b8951ceccea2 100644 --- a/docs/runtime/nodejs-compat.mdx +++ b/docs/runtime/nodejs-compat.mdx @@ -212,6 +212,8 @@ Socket close notifications use internal scheduling and continue to run when user ### [`node:process`](https://nodejs.org/api/process.html) +`process.platform`, `arch`, `version`, `versions`, `pid`, and `release` expose Node-compatible data descriptors. `argv` and `execArgv` initialize lazily and support descriptor-based replacement. `ppid` and `title` retain their native behavior behind data descriptors. `exitCode` remains a non-configurable accessor. + Initializing `process.stdout` or `process.stderr` does not retain a duplicate pipe writer. Explicitly closing the underlying descriptor publishes EOF while the process remains alive. On POSIX, socket and pipe stdio use nonblocking writes. `end()` and pipeline completion flush pending output and shut down the write side of socket stdio, so peers receive EOF before process exit. Actual pipes stay open until process exit or explicit descriptor closure. `destroy()` preserves fd 1/2; after POSIX socket or pipe finalization, later stream writes fail with `EPIPE`. On Windows, piped stdout/stderr reset their writable state after `end()` or pipeline completion and accept subsequent writes. `NODE_OPTIONS` applies Bun-supported Node runtime flags before command-line arguments, including `--require`/`-r` and `--import` preloads. Injected flags are absent from `process.execArgv`. Unsupported Node flags remain ignored; flags outside the Node allowlist warn and are ignored. `process.config.variables.node_without_node_options` remains `true` because support is partial. diff --git a/src/jsc/bindings/BunProcess.cpp b/src/jsc/bindings/BunProcess.cpp index 8713db92eee3..3dec12b1195f 100644 --- a/src/jsc/bindings/BunProcess.cpp +++ b/src/jsc/bindings/BunProcess.cpp @@ -3202,105 +3202,42 @@ static JSValue constructExecPath(VM& vm, JSObject* processObject) return JSValue::decode(Bun__Process__getExecPath(globalObject)); } -extern "C" EncodedJSValue Bun__Process__getArgv(JSGlobalObject* lexicalGlobalObject) +static JSValue constructArgv(VM& vm, JSObject* processObject) { - auto* globalObject = defaultGlobalObject(lexicalGlobalObject); - auto* process = globalObject->processObject(); - if (!process) { - return JSValue::encode(jsUndefined()); - } - - return JSValue::encode(process->getArgv(globalObject)); + return JSValue::decode(Bun__Process__createArgv(processObject->globalObject())); } -// get from js -JSC_DEFINE_CUSTOM_GETTER(processArgv, (JSGlobalObject * globalObject, EncodedJSValue thisValue, PropertyName)) +static JSValue constructExecArgv(VM& vm, JSObject* processObject) { - Process* process = getProcessObject(globalObject, JSValue::decode(thisValue)); - if (!process) { - return JSValue::encode(jsUndefined()); - } - - return JSValue::encode(process->getArgv(globalObject)); + return JSValue::decode(Bun__Process__createExecArgv(processObject->globalObject())); } JSValue Process::getArgv(JSGlobalObject* globalObject) { - if (auto argv = m_argv.get()) { - return argv; - } - - JSValue argv = JSValue::decode(Bun__Process__createArgv(globalObject)); - setArgv(globalObject, argv); - return argv; + return get(globalObject, Identifier::fromString(globalObject->vm(), "argv"_s)); } -void Process::setArgv(JSGlobalObject* globalObject, JSValue value) +JSValue Process::getExecArgv(JSGlobalObject* globalObject) { - auto& vm = globalObject->vm(); - m_argv.set(vm, this, value); + return get(globalObject, Identifier::fromString(globalObject->vm(), "execArgv"_s)); } -JSC_DEFINE_CUSTOM_SETTER(setProcessArgv, (JSGlobalObject * globalObject, EncodedJSValue thisValue, EncodedJSValue encodedValue, PropertyName)) +extern "C" EncodedJSValue Bun__Process__getArgv(JSGlobalObject* lexicalGlobalObject) { - Process* process = getProcessObject(globalObject, JSValue::decode(thisValue)); - if (!process) { - return true; - } - - JSValue value = JSValue::decode(encodedValue); - process->setArgv(globalObject, value); - return true; + auto* globalObject = defaultGlobalObject(lexicalGlobalObject); + auto scope = DECLARE_THROW_SCOPE(globalObject->vm()); + JSValue argv = globalObject->processObject()->getArgv(globalObject); + RETURN_IF_EXCEPTION(scope, {}); + return JSValue::encode(argv); } extern "C" EncodedJSValue Bun__Process__getExecArgv(JSGlobalObject* lexicalGlobalObject) { auto* globalObject = defaultGlobalObject(lexicalGlobalObject); - auto* process = globalObject->processObject(); - if (!process) { - return JSValue::encode(jsUndefined()); - } - - return JSValue::encode(process->getExecArgv(globalObject)); -} - -JSC_DEFINE_CUSTOM_GETTER(processExecArgv, (JSGlobalObject * globalObject, EncodedJSValue thisValue, PropertyName)) -{ - Process* process = getProcessObject(globalObject, JSValue::decode(thisValue)); - if (!process) { - return JSValue::encode(jsUndefined()); - } - - return JSValue::encode(process->getExecArgv(globalObject)); -} - -JSValue Process::getExecArgv(JSGlobalObject* globalObject) -{ - if (auto argv = m_execArgv.get()) { - return argv; - } - - JSValue argv = JSValue::decode(Bun__Process__createExecArgv(globalObject)); - setExecArgv(globalObject, argv); - return argv; -} - -void Process::setExecArgv(JSGlobalObject* globalObject, JSValue value) -{ - auto& vm = globalObject->vm(); - m_execArgv.set(vm, this, value); -} - -JSC_DEFINE_CUSTOM_SETTER(setProcessExecArgv, (JSGlobalObject * globalObject, EncodedJSValue thisValue, EncodedJSValue encodedValue, PropertyName)) -{ - Process* process = getProcessObject(globalObject, JSValue::decode(thisValue)); - if (!process) { - return true; - } - - JSValue value = JSValue::decode(encodedValue); - process->setExecArgv(globalObject, value); - return true; + auto scope = DECLARE_THROW_SCOPE(globalObject->vm()); + JSValue execArgv = globalObject->processObject()->getExecArgv(globalObject); + RETURN_IF_EXCEPTION(scope, {}); + return JSValue::encode(execArgv); } JSC_DEFINE_CUSTOM_GETTER(processGetEval, (JSGlobalObject * globalObject, EncodedJSValue thisValue, PropertyName)) @@ -3808,8 +3745,6 @@ void Process::visitChildrenImpl(JSCell* cell, Visitor& visitor) visitor.append(thisObject->m_uncaughtExceptionCaptureCallback); visitor.append(thisObject->m_nextTickFunction); visitor.append(thisObject->m_cachedCwd); - visitor.append(thisObject->m_argv); - visitor.append(thisObject->m_execArgv); visitor.append(thisObject->m_onWarning); thisObject->m_cpuUsageStructure.visit(visitor); @@ -5103,8 +5038,8 @@ extern "C" void Process__emitErrorEvent(Zig::GlobalObject* global, EncodedJSValu allowedNodeEnvironmentFlags constructAllowedNodeEnvironmentFlags PropertyCallback loadEnvFile constructLoadEnvFile PropertyCallback finalization constructFinalization PropertyCallback - arch constructArch PropertyCallback - argv processArgv CustomAccessor + arch constructArch PropertyCallback|ReadOnly + argv constructArgv PropertyCallback argv0 constructArgv0 PropertyCallback assert Process_functionAssert Function 1 availableMemory Process_availableMemory Function 0 @@ -5123,7 +5058,7 @@ extern "C" void Process__emitErrorEvent(Zig::GlobalObject* global, EncodedJSValu dlopen Process_functionDlopen Function 1 emitWarning Process_emitWarning Function 1 env constructEnv PropertyCallback - execArgv processExecArgv CustomAccessor + execArgv constructExecArgv PropertyCallback execPath constructExecPath PropertyCallback execve Process_functionExecve Function 3 exit Process_functionExit Function 1 @@ -5141,12 +5076,12 @@ extern "C" void Process__emitErrorEvent(Zig::GlobalObject* global, EncodedJSValu moduleLoadList Process_stubEmptyArray PropertyCallback nextTick constructProcessNextTickFn PropertyCallback openStdin Process_functionOpenStdin Function 0 - pid constructPid PropertyCallback - platform constructPlatform PropertyCallback - ppid processPpid CustomAccessor + pid constructPid PropertyCallback|ReadOnly + platform constructPlatform PropertyCallback|ReadOnly + ppid processPpid CustomValue reallyExit Process_functionReallyExit Function 1 ref Process_ref Function 1 - release constructProcessReleaseObject PropertyCallback + release constructProcessReleaseObject PropertyCallback|ReadOnly report constructProcessReportObject PropertyCallback resourceUsage Process_functionResourceUsage Function 0 revision constructRevision PropertyCallback @@ -5156,12 +5091,12 @@ extern "C" void Process__emitErrorEvent(Zig::GlobalObject* global, EncodedJSValu stderr constructStderr PropertyCallback stdin constructStdin PropertyCallback stdout constructStdout PropertyCallback - title processTitle CustomAccessor + title processTitle CustomValue umask Process_functionUmask Function 1 unref Process_unref Function 1 uptime Process_functionUptime Function 1 - version constructVersion PropertyCallback - versions constructVersions PropertyCallback + version constructVersion PropertyCallback|ReadOnly + versions constructVersions PropertyCallback|ReadOnly #if !OS(WINDOWS) getegid Process_functiongetegid Function 0 diff --git a/src/jsc/bindings/BunProcess.h b/src/jsc/bindings/BunProcess.h index cef08adbbeb9..4fb6df75212f 100644 --- a/src/jsc/bindings/BunProcess.h +++ b/src/jsc/bindings/BunProcess.h @@ -32,8 +32,6 @@ class Process : public WebCore::JSEventEmitter { WriteBarrier m_nextTickFunction; // https://github.com/nodejs/node/blob/2eff28fb7a93d3f672f80b582f664a7c701569fb/lib/internal/bootstrap/switches/does_own_process_state.js#L113-L116 WriteBarrier m_cachedCwd; - WriteBarrier m_argv; - WriteBarrier m_execArgv; // The JS warning printer (ProcessObjectInternals createOnWarning), built on the first warning. WriteBarrier m_onWarning; @@ -93,10 +91,7 @@ class Process : public WebCore::JSEventEmitter { void clearCachedCwd() { m_cachedCwd.clear(); } JSValue getArgv(JSGlobalObject* globalObject); - void setArgv(JSGlobalObject* globalObject, JSValue argv); - JSValue getExecArgv(JSGlobalObject* globalObject); - void setExecArgv(JSGlobalObject* globalObject, JSValue execArgv); static JSC::Structure* createStructure(JSC::VM& vm, JSC::JSGlobalObject* globalObject, JSC::JSValue prototype) diff --git a/src/jsc/bindings/InspectorLifecycleAgent.cpp b/src/jsc/bindings/InspectorLifecycleAgent.cpp index dc32891c9187..198e8ffefb16 100644 --- a/src/jsc/bindings/InspectorLifecycleAgent.cpp +++ b/src/jsc/bindings/InspectorLifecycleAgent.cpp @@ -172,9 +172,10 @@ Protocol::ErrorStringOr InspectorLifecycleAgent::getModuleGraph() Ref> argv = JSON::ArrayOf::create(); { - auto* array = uncheckedDowncast(process->getArgv(global)); + JSC::JSValue argvValue = process->getArgv(global); RETURN_IF_EXCEPTION(scope, fail("Failed to get argv"_s)); - for (size_t i = 0, length = array->length(); i < length; i++) { + auto* array = dynamicDowncast(argvValue); + for (size_t i = 0, length = array ? array->length() : 0; i < length; i++) { auto value = array->getIndex(global, i); RETURN_IF_EXCEPTION(scope, fail("Failed to get value at index"_s)); auto string = value.toWTFString(global); diff --git a/test/js/node/process/process.test.js b/test/js/node/process/process.test.js index a38a4515fd6b..52aa6c46413d 100644 --- a/test/js/node/process/process.test.js +++ b/test/js/node/process/process.test.js @@ -34,6 +34,131 @@ async function runInlineFixture(script, expectedStdout = null, expectedCode = 0) return { stdout, exitCode }; } +describe("process property descriptors", () => { + it.each([ + ["platform", false], + ["arch", false], + ["version", false], + ["versions", false], + ["env", true], + ["execPath", true], + ["argv", true], + ["execArgv", true], + ["pid", false], + ["ppid", true], + ["title", true], + ["release", false], + ])("process.%s exposes a Node-compatible data descriptor", async (name, writable) => { + await using proc = Bun.spawn({ + cmd: [ + bunExe(), + "-e", + ` + const assert = require("node:assert/strict"); + const name = ${JSON.stringify(name)}; + const descriptor = Object.getOwnPropertyDescriptor(process, name); + assert.deepEqual(Object.keys(descriptor).sort(), ["configurable", "enumerable", "value", "writable"]); + assert.equal(descriptor.writable, ${writable}); + assert.equal(descriptor.enumerable, true); + assert.equal(descriptor.configurable, true); + assert.equal(descriptor.value, process[name]); + const replacement = { fixture: true }; + Object.defineProperty(process, name, { ...descriptor, value: replacement }); + assert.equal(process[name], replacement); + Object.defineProperty(process, name, descriptor); + assert.deepEqual(Object.getOwnPropertyDescriptor(process, name), descriptor); + if (!descriptor.writable) { + const assignStrict = Function("name", "replacement", '"use strict"; process[name] = replacement;'); + assert.throws(() => assignStrict(name, replacement), TypeError); + Function("replacement", "process[" + JSON.stringify(name) + "] = replacement")(replacement); + assert.equal(process[name], descriptor.value); + } + delete process[name]; + assert.equal(Object.hasOwn(process, name), false); + Object.defineProperty(process, name, descriptor); + console.log("ok"); + `, + ], + env: bunEnv, + stdout: "pipe", + stderr: "pipe", + }); + const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]); + expect({ stdout, stderr, exitCode }).toEqual({ stdout: "ok\n", stderr: "", exitCode: 0 }); + }); + + it("keeps process.exitCode as a non-configurable accessor", () => { + const descriptor = Object.getOwnPropertyDescriptor(process, "exitCode"); + expect(descriptor).toEqual({ + get: expect.any(Function), + set: expect.any(Function), + enumerable: true, + configurable: false, + }); + }); + + it("keeps native setters behind data descriptors until redefined", async () => { + await using proc = Bun.spawn({ + cmd: [ + bunExe(), + "-e", + ` + const assert = require("node:assert/strict"); + for (const name of ["argv", "execArgv", "ppid"]) { + const value = name === "ppid" ? 123 : ["fixture"]; + process[name] = value; + assert.equal(process[name], value); + assert.equal(Object.getOwnPropertyDescriptor(process, name).value, value); + } + process.title = "fixture"; + assert.equal(process.title, "fixture"); + assert.equal(Object.getOwnPropertyDescriptor(process, "title").value, "fixture"); + Object.defineProperty(process, "title", { value: "redefined" }); + assert.equal(process.title, "redefined"); + console.log("ok"); + `, + ], + env: bunEnv, + stdout: "pipe", + stderr: "pipe", + }); + const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]); + expect({ stdout, stderr, exitCode }).toEqual({ stdout: "ok\n", stderr: "", exitCode: 0 }); + }); +}); + +it.concurrent("process.argv redefinition reaches native consumers", async () => { + // parseArgs uses a different argv offset for -e; exercise a script's arguments. + using dir = tempDir("process-argv-redefinition", { + "index.cjs": ` + const assert = require("node:assert/strict"); + const { parseArgs } = require("node:util"); + const options = { before: { type: "boolean" }, after: { type: "boolean" } }; + process.argv = ["bun", "script.js", "--before"]; + assert.equal(parseArgs({ options }).values.before, true); + Object.defineProperty(process, "argv", { value: ["bun", "script.js", "--after"] }); + assert.equal(parseArgs({ options }).values.after, true); + const marker = new Error("argv getter"); + Object.defineProperty(process, "argv", { configurable: true, get() { throw marker; } }); + assert.throws(() => parseArgs({ options }), e => e === marker); + for (let i = 0; i < 2; i++) assert.throws(() => Bun.argv, e => e === marker); + const value = ["bun", "script.js", "--before"]; + Object.defineProperty(process, "argv", { value }); + assert.equal(Bun.argv, value); + assert.equal(parseArgs({ options }).values.before, true); + console.log("ok"); + `, + }); + await using proc = Bun.spawn({ + cmd: [bunExe(), join(String(dir), "index.cjs")], + env: bunEnv, + stdout: "pipe", + stderr: "pipe", + }); + const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]); + expect({ stdout, stderr, exitCode }).toEqual({ stdout: "ok\n", stderr: "", exitCode: 0 }); +}); + it("process", () => { // this property isn't implemented yet but it should at least return a string const isNode = !process.isBun;