Files
node/test/parallel/test-child-process-default-env.js
Shelley Vohr bd83a5cd7a child_process: build the default env block in one native pass
When spawn()/spawnSync() are called without options.env,
normalizeSpawnArguments() copied process.env with a spread and then
walked the copy to build the KEY=value array uv_spawn() takes.
Spreading the process.env proxy costs one enumerator callback plus a
query and a getter interceptor per variable, each doing a linear
getenv() scan and allocating; with a couple of hundred variables that
was the single largest JS-side cost of spawning a process.

Add KVStore::Pairs() (Enumerate() + Get() by default, one
uv_os_environ() pass for the real environment, skipping hidden
variables on Windows exactly like Enumerate() does), expose it as
process_wrap.getEnvPairs(), and use it for the default-environment
case. A user supplied options.env and the permission model case keep
the existing code. On Windows the same sort/first-wins-case-insensitive
filter is applied to the pairs. The variables copyProcessEnvToEnv()
propagates are part of the real environment by definition, and its
entries cannot contain null bytes, so those steps only remain on the
options.env path.

Signed-off-by: Shelley Vohr <shelley.vohr@gmail.com>
PR-URL: https://github.com/nodejs/node/pull/65325
Reviewed-By: Antoine du Hamel <duhamelantoine1995@gmail.com>
Reviewed-By: James M Snell <jasnell@gmail.com>
2026-09-30 13:30:42 +00:00

77 lines
3.0 KiB
JavaScript

'use strict';
// When no `env` option is given, a child process must inherit exactly the
// parent's current environment: same variables and values, reflecting runtime
// additions/deletions made through process.env.
const common = require('../common');
const assert = require('assert');
const { spawn, spawnSync, execFileSync } = require('child_process');
// Mutate the environment at runtime in a few ways first.
process.env.TEST_DEFAULT_ENV_ADDED = 'added ünïcödé ✓';
process.env.TEST_DEFAULT_ENV_EMPTY = '';
process.env.TEST_DEFAULT_ENV_EQUALS = 'a=b=c';
process.env.TEST_DEFAULT_ENV_DELETED = 'x';
delete process.env.TEST_DEFAULT_ENV_DELETED;
function expectedEnv() {
// What `{ ...process.env }` yields, minus keys whose value is undefined
// (there are none for the real environment, but keep the definition exact).
const copy = { ...process.env };
for (const key of Object.keys(copy)) {
if (copy[key] === undefined) delete copy[key];
}
return copy;
}
const printEnv = ['-e', 'process.stdout.write(JSON.stringify(process.env))'];
function run(options) {
const child = spawnSync(process.execPath, printEnv, { encoding: 'utf8', ...options });
assert.strictEqual(child.status, 0, child.stderr || String(child.error));
return child.stdout;
}
function check(output, label, expected = expectedEnv()) {
const childEnv = JSON.parse(output);
assert.deepStrictEqual(childEnv, expected, `${label}: contents`);
assert.strictEqual(childEnv.TEST_DEFAULT_ENV_ADDED, 'added ünïcödé ✓');
assert.strictEqual(childEnv.TEST_DEFAULT_ENV_EMPTY, '');
assert.strictEqual(childEnv.TEST_DEFAULT_ENV_EQUALS, 'a=b=c');
assert.ok(!('TEST_DEFAULT_ENV_DELETED' in childEnv));
}
// spawnSync, options omitted entirely.
check(run(), 'spawnSync no options');
// Explicitly undefined / null env behave like the default.
check(run({ env: undefined }), 'spawnSync env undefined');
check(run({ env: null }), 'spawnSync env null');
// execFileSync goes through the same normalization.
check(execFileSync(process.execPath, printEnv, { encoding: 'utf8' }), 'execFileSync');
// A user-supplied env is still passed through as given (not merged).
{
const env = { ...process.env, ONLY: 'this' };
delete env.TEST_DEFAULT_ENV_ADDED;
const childEnv = JSON.parse(run({ env }));
assert.strictEqual(childEnv.ONLY, 'this');
assert.ok(!('TEST_DEFAULT_ENV_ADDED' in childEnv));
}
// Async spawn (the environment is captured at spawn() time).
{
const expectedAtSpawn = expectedEnv();
const child = spawn(process.execPath, printEnv);
let out = '';
child.stdout.setEncoding('utf8').on('data', (d) => { out += d; });
let err = '';
child.stderr.setEncoding('utf8').on('data', (d) => { err += d; });
child.on('close', common.mustCall((code) => {
assert.strictEqual(code, 0, err);
check(out, 'spawn', expectedAtSpawn);
}));
}
// A variable added after an earlier spawn is seen by a later one (no caching).
process.env.TEST_DEFAULT_ENV_LATE = 'late';
{
const childEnv = JSON.parse(run());
assert.strictEqual(childEnv.TEST_DEFAULT_ENV_LATE, 'late');
}