Compare commits

...

9 Commits

Author SHA1 Message Date
pfg
45b1582620 REMAINING 2025-04-01 17:39:31 -07:00
pfg
4da8ce1c1c test-child-process-detached 2025-04-01 16:20:43 -07:00
pfg
58dbcb0e91 test-child-process-windows-hide 2025-03-31 19:43:56 -07:00
pfg
57376f2a2c test-child-process-spawnsync-shell.js 2025-03-31 19:17:10 -07:00
pfg
2d0829b5bb test-child-process-spawnsync-kill-signal 2025-03-31 19:12:56 -07:00
pfg
887ee539ba support both emfile and enfile 2025-03-31 18:34:51 -07:00
pfg
d828f25837 match node logic more closely 2025-03-31 18:30:04 -07:00
pfg
f1dc926d68 test-child-process-emfile 2025-03-31 18:22:53 -07:00
pfg
be1fdbe810 test-child-process-reject-null-bytes 2025-03-31 16:08:40 -07:00
39 changed files with 2899 additions and 13 deletions

View File

@@ -2236,9 +2236,21 @@ pub fn spawnMaybeSync(
&spawn_options,
@ptrCast(argv.items.ptr),
@ptrCast(env_array.items.ptr),
) catch |err| {
spawn_options.deinit();
return globalThis.throwError(err, ": failed to spawn process") catch return .zero;
) catch |err| switch (err) {
error.EMFILE, error.ENFILE => {
spawn_options.deinit();
const display_path: [:0]const u8 = if (argv.items.len > 0 and argv.items[0] != null)
std.mem.sliceTo(argv.items[0].?, 0)
else
"";
var systemerror = bun.sys.Error.fromCode(if (err == error.EMFILE) .MFILE else .NFILE, .posix_spawn).withPath(display_path).toSystemError();
systemerror.errno = if (err == error.EMFILE) -bun.C.UV_EMFILE else -bun.C.UV_ENFILE;
return globalThis.throwValue(systemerror.toErrorInstance(globalThis));
},
else => {
spawn_options.deinit();
return globalThis.throwError(err, ": failed to spawn process") catch return .zero;
},
}) {
.err => |err| {
spawn_options.deinit();

View File

@@ -1163,6 +1163,8 @@ class ChildProcess extends EventEmitter {
return null;
case "destroyed":
return new ShimmedStdin();
case "undefined":
return undefined;
default:
return null;
}
@@ -1183,6 +1185,8 @@ class ChildProcess extends EventEmitter {
}
case "destroyed":
return new ShimmedStdioOutStream();
case "undefined":
return undefined;
default:
return null;
}
@@ -1213,6 +1217,9 @@ class ChildProcess extends EventEmitter {
for (let i = 0; i < length; i++) {
const element = opts[i];
if (element === "undefined") {
return undefined;
}
if (element !== "pipe") {
result[i] = null;
continue;
@@ -1357,14 +1364,33 @@ class ChildProcess extends EventEmitter {
}
}
} catch (ex) {
if (ex == null || typeof ex !== "object" || !Object.hasOwn(ex, "errno")) throw ex;
this.#handle = null;
ex.syscall = "spawn " + this.spawnfile;
ex.spawnargs = Array.prototype.slice.$call(this.spawnargs, 1);
process.nextTick(() => {
this.emit("error", ex);
this.emit("close", (ex as SystemError).errno ?? -1);
});
if (
ex != null &&
typeof ex === "object" &&
Object.hasOwn(ex, "code") &&
// node sends these errors on the next tick rather than throwing
(ex.code === "EACCES" ||
ex.code === "EAGAIN" ||
ex.code === "EMFILE" ||
ex.code === "ENFILE" ||
ex.code === "ENOENT")
) {
this.#handle = null;
ex.syscall = "spawn " + this.spawnfile;
ex.spawnargs = Array.prototype.slice.$call(this.spawnargs, 1);
process.nextTick(() => {
this.emit("error", ex);
this.emit("close", (ex as SystemError).errno ?? -1);
});
if (ex.code === "EMFILE" || ex.code === "ENFILE") {
// emfile/enfile error; in this case node does not initialize stdio streams.
this.#stdioOptions[0] = "undefined";
this.#stdioOptions[1] = "undefined";
this.#stdioOptions[2] = "undefined";
}
} else {
throw ex;
}
}
}

View File

@@ -3872,8 +3872,7 @@ pub fn wtf8Sequence(code_point: u32) [4]u8 {
pub inline fn wtf8ByteSequenceLength(first_byte: u8) u3 {
return switch (first_byte) {
0 => 0,
1...0x80 - 1 => 1,
0...0x80 - 1 => 1,
else => if ((first_byte & 0xE0) == 0xC0)
@as(u3, 2)
else if ((first_byte & 0xF0) == 0xE0)

View File

@@ -0,0 +1,43 @@
// Copyright Joyent, Inc. and other Node contributors.
//
// Permission is hereby granted, free of charge, to any person obtaining a
// copy of this software and associated documentation files (the
// "Software"), to deal in the Software without restriction, including
// without limitation the rights to use, copy, modify, merge, publish,
// distribute, sublicense, and/or sell copies of the Software, and to permit
// persons to whom the Software is furnished to do so, subject to the
// following conditions:
//
// The above copyright notice and this permission notice shall be included
// in all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN
// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE
// USE OR OTHER DEALINGS IN THE SOFTWARE.
'use strict';
require('../common');
const assert = require('assert');
const fixtures = require('../common/fixtures');
const spawn = require('child_process').spawn;
const childPath = fixtures.path('parent-process-nonpersistent.js');
let persistentPid = -1;
const child = spawn(process.execPath, [ childPath ]);
child.stdout.on('data', function(data) {
persistentPid = parseInt(data, 10);
});
process.on('exit', function() {
assert.notStrictEqual(persistentPid, -1);
assert.throws(function() {
process.kill(child.pid);
}, /^Error: kill ESRCH$|^SystemError: kill\(\) failed: ESRCH: No such process$/);
process.kill(persistentPid);
});

View File

@@ -0,0 +1,78 @@
// Copyright Joyent, Inc. and other Node contributors.
//
// Permission is hereby granted, free of charge, to any person obtaining a
// copy of this software and associated documentation files (the
// "Software"), to deal in the Software without restriction, including
// without limitation the rights to use, copy, modify, merge, publish,
// distribute, sublicense, and/or sell copies of the Software, and to permit
// persons to whom the Software is furnished to do so, subject to the
// following conditions:
//
// The above copyright notice and this permission notice shall be included
// in all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN
// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE
// USE OR OTHER DEALINGS IN THE SOFTWARE.
'use strict';
const common = require('../common');
if (common.isWindows)
common.skip('no RLIMIT_NOFILE on Windows');
const assert = require('assert');
const child_process = require('child_process');
const fs = require('fs');
const ulimit = Number(child_process.execSync('ulimit -Hn'));
if (ulimit > 64 || Number.isNaN(ulimit)) {
const [cmd, opts] = common.escapePOSIXShell`ulimit -n 64 && "${process.execPath}" "${__filename}"`;
// Sorry about this nonsense. It can be replaced if
// https://github.com/nodejs/node-v0.x-archive/pull/2143#issuecomment-2847886
// ever happens.
const result = child_process.spawnSync(
'/bin/sh',
['-c', cmd],
opts,
);
assert.strictEqual(result.stdout.toString(), '');
assert.strictEqual(result.stderr.toString(), '');
assert.strictEqual(result.status, 0);
assert.strictEqual(result.error, undefined);
return;
}
const openFds = [];
for (;;) {
try {
openFds.push(fs.openSync(__filename, 'r'));
} catch (err) {
assert.strictEqual(err.code, 'EMFILE');
break;
}
}
// Should emit an error, not throw.
const proc = child_process.spawn(process.execPath, ['-e', '0']);
// Verify that stdio is not setup on EMFILE or ENFILE.
assert.strictEqual(proc.stdin, undefined);
assert.strictEqual(proc.stdout, undefined);
assert.strictEqual(proc.stderr, undefined);
assert.strictEqual(proc.stdio, undefined);
proc.on('error', common.mustCall(function(err) {
assert.strictEqual(err.code, 'EMFILE');
}));
proc.on('exit', common.mustNotCall('"exit" event should not be emitted'));
// Close one fd for LSan
if (openFds.length >= 1) {
fs.closeSync(openFds.pop());
}

View File

@@ -0,0 +1,88 @@
'use strict';
const common = require('../common');
const assert = require('assert');
const exec = require('child_process').exec;
const { promisify } = require('util');
const execPromisifed = promisify(exec);
const invalidArgTypeError = {
code: 'ERR_INVALID_ARG_TYPE',
name: 'TypeError'
};
const waitCommand = common.isWindows ?
// `"` is forbidden for Windows paths, no need for escaping.
`"${process.execPath}" -e "setInterval(()=>{}, 99)"` :
'sleep 2m';
{
const ac = new AbortController();
const signal = ac.signal;
const promise = execPromisifed(waitCommand, { signal });
assert.rejects(promise, {
name: 'AbortError',
cause: new DOMException('This operation was aborted', 'AbortError'),
}).then(common.mustCall());
ac.abort();
}
{
const err = new Error('boom');
const ac = new AbortController();
const signal = ac.signal;
const promise = execPromisifed(waitCommand, { signal });
assert.rejects(promise, {
name: 'AbortError',
cause: err
}).then(common.mustCall());
ac.abort(err);
}
{
const ac = new AbortController();
const signal = ac.signal;
const promise = execPromisifed(waitCommand, { signal });
assert.rejects(promise, {
name: 'AbortError',
cause: 'boom'
}).then(common.mustCall());
ac.abort('boom');
}
{
assert.throws(() => {
execPromisifed(waitCommand, { signal: {} });
}, invalidArgTypeError);
}
{
function signal() {}
assert.throws(() => {
execPromisifed(waitCommand, { signal });
}, invalidArgTypeError);
}
{
const signal = AbortSignal.abort(); // Abort in advance
const promise = execPromisifed(waitCommand, { signal });
assert.rejects(promise, { name: 'AbortError' })
.then(common.mustCall());
}
{
const err = new Error('boom');
const signal = AbortSignal.abort(err); // Abort in advance
const promise = execPromisifed(waitCommand, { signal });
assert.rejects(promise, { name: 'AbortError', cause: err })
.then(common.mustCall());
}
{
const signal = AbortSignal.abort('boom'); // Abort in advance
const promise = execPromisifed(waitCommand, { signal });
assert.rejects(promise, { name: 'AbortError', cause: 'boom' })
.then(common.mustCall());
}

View File

@@ -0,0 +1,146 @@
'use strict';
const common = require('../common');
const assert = require('assert');
const cp = require('child_process');
function runChecks(err, stdio, streamName, expected) {
assert.strictEqual(err.message, `${streamName} maxBuffer length exceeded`);
assert(err instanceof RangeError);
assert.strictEqual(err.code, 'ERR_CHILD_PROCESS_STDIO_MAXBUFFER');
assert.deepStrictEqual(stdio[streamName], expected);
}
// The execPath might contain chars that should be escaped in a shell context.
// On non-Windows, we can pass the path via the env; `"` is not a valid char on
// Windows, so we can simply pass the path.
const execNode = (args, optionsOrCallback, callback) => {
const [cmd, opts] = common.escapePOSIXShell`"${process.execPath}" `;
let options = optionsOrCallback;
if (typeof optionsOrCallback === 'function') {
options = undefined;
callback = optionsOrCallback;
}
return cp.exec(
cmd + args,
{ ...opts, ...options },
callback,
);
};
// default value
{
execNode(`-e "console.log('a'.repeat(1024 * 1024))"`, common.mustCall((err) => {
assert(err instanceof RangeError);
assert.strictEqual(err.message, 'stdout maxBuffer length exceeded');
assert.strictEqual(err.code, 'ERR_CHILD_PROCESS_STDIO_MAXBUFFER');
}));
}
// default value
{
execNode(`-e "console.log('a'.repeat(1024 * 1024 - 1))"`, common.mustSucceed((stdout, stderr) => {
assert.strictEqual(stdout.trim(), 'a'.repeat(1024 * 1024 - 1));
assert.strictEqual(stderr, '');
}));
}
{
const options = { maxBuffer: Infinity };
execNode(`-e "console.log('hello world');"`, options, common.mustSucceed((stdout, stderr) => {
assert.strictEqual(stdout.trim(), 'hello world');
assert.strictEqual(stderr, '');
}));
}
{
const cmd = 'echo hello world';
cp.exec(
cmd,
{ maxBuffer: 5 },
common.mustCall((err, stdout, stderr) => {
runChecks(err, { stdout, stderr }, 'stdout', 'hello');
})
);
}
// default value
{
execNode(
`-e "console.log('a'.repeat(1024 * 1024))"`,
common.mustCall((err, stdout, stderr) => {
runChecks(
err,
{ stdout, stderr },
'stdout',
'a'.repeat(1024 * 1024)
);
})
);
}
// default value
{
execNode(`-e "console.log('a'.repeat(1024 * 1024 - 1))"`, common.mustSucceed((stdout, stderr) => {
assert.strictEqual(stdout.trim(), 'a'.repeat(1024 * 1024 - 1));
assert.strictEqual(stderr, '');
}));
}
const unicode = '中文测试'; // length = 4, byte length = 12
{
execNode(
`-e "console.log('${unicode}');"`,
{ maxBuffer: 10 },
common.mustCall((err, stdout, stderr) => {
runChecks(err, { stdout, stderr }, 'stdout', '中文测试\n');
})
);
}
{
execNode(
`-e "console.error('${unicode}');"`,
{ maxBuffer: 3 },
common.mustCall((err, stdout, stderr) => {
runChecks(err, { stdout, stderr }, 'stderr', '中文测');
})
);
}
{
const child = execNode(
`-e "console.log('${unicode}');"`,
{ encoding: null, maxBuffer: 10 },
common.mustCall((err, stdout, stderr) => {
runChecks(err, { stdout, stderr }, 'stdout', '中文测试\n');
})
);
child.stdout.setEncoding('utf-8');
}
{
const child = execNode(
`-e "console.error('${unicode}');"`,
{ encoding: null, maxBuffer: 3 },
common.mustCall((err, stdout, stderr) => {
runChecks(err, { stdout, stderr }, 'stderr', '中文测');
})
);
child.stderr.setEncoding('utf-8');
}
{
execNode(
`-e "console.error('${unicode}');"`,
{ encoding: null, maxBuffer: 5 },
common.mustCall((err, stdout, stderr) => {
const buf = Buffer.from(unicode).slice(0, 5);
runChecks(err, { stdout, stderr }, 'stderr', buf);
})
);
}

View File

@@ -0,0 +1,92 @@
'use strict';
const common = require('../common');
const assert = require('assert');
const { execFile } = require('child_process');
function checkFactory(streamName) {
return common.mustCall((err) => {
assert(err instanceof RangeError);
assert.strictEqual(err.message, `${streamName} maxBuffer length exceeded`);
assert.strictEqual(err.code, 'ERR_CHILD_PROCESS_STDIO_MAXBUFFER');
});
}
// default value
{
execFile(
process.execPath,
['-e', 'console.log("a".repeat(1024 * 1024))'],
checkFactory('stdout')
);
}
// default value
{
execFile(
process.execPath,
['-e', 'console.log("a".repeat(1024 * 1024 - 1))'],
common.mustSucceed((stdout, stderr) => {
assert.strictEqual(stdout.trim(), 'a'.repeat(1024 * 1024 - 1));
assert.strictEqual(stderr, '');
})
);
}
{
const options = { maxBuffer: Infinity };
execFile(
process.execPath,
['-e', 'console.log("hello world");'],
options,
common.mustSucceed((stdout, stderr) => {
assert.strictEqual(stdout.trim(), 'hello world');
assert.strictEqual(stderr, '');
})
);
}
{
execFile('echo', ['hello world'], { maxBuffer: 5 }, checkFactory('stdout'));
}
const unicode = '中文测试'; // length = 4, byte length = 12
{
execFile(
process.execPath,
['-e', `console.log('${unicode}');`],
{ maxBuffer: 10 },
checkFactory('stdout'));
}
{
execFile(
process.execPath,
['-e', `console.error('${unicode}');`],
{ maxBuffer: 10 },
checkFactory('stderr')
);
}
{
const child = execFile(
process.execPath,
['-e', `console.log('${unicode}');`],
{ encoding: null, maxBuffer: 10 },
checkFactory('stdout')
);
child.stdout.setEncoding('utf-8');
}
{
const child = execFile(
process.execPath,
['-e', `console.error('${unicode}');`],
{ encoding: null, maxBuffer: 10 },
checkFactory('stderr')
);
child.stderr.setEncoding('utf-8');
}

View File

@@ -0,0 +1,53 @@
'use strict';
require('../common');
// This test checks that the maxBuffer option for child_process.execFileSync()
// works as expected.
const assert = require('assert');
const { getSystemErrorName } = require('util');
const { execFileSync } = require('child_process');
const msgOut = 'this is stdout';
const msgOutBuf = Buffer.from(`${msgOut}\n`);
const args = [
'-e',
`console.log("${msgOut}");`,
];
// Verify that an error is returned if maxBuffer is surpassed.
{
assert.throws(() => {
execFileSync(process.execPath, args, { maxBuffer: 1 });
}, (e) => {
assert.ok(e, 'maxBuffer should error');
assert.strictEqual(e.code, 'ENOBUFS');
assert.strictEqual(getSystemErrorName(e.errno), 'ENOBUFS');
// We can have buffers larger than maxBuffer because underneath we alloc 64k
// that matches our read sizes.
assert.deepStrictEqual(e.stdout, msgOutBuf);
return true;
});
}
// Verify that a maxBuffer size of Infinity works.
{
const ret = execFileSync(process.execPath, args, { maxBuffer: Infinity });
assert.deepStrictEqual(ret, msgOutBuf);
}
// Default maxBuffer size is 1024 * 1024.
{
assert.throws(() => {
execFileSync(
process.execPath,
['-e', "console.log('a'.repeat(1024 * 1024))"]
);
}, (e) => {
assert.ok(e, 'maxBuffer should error');
assert.strictEqual(e.code, 'ENOBUFS');
assert.strictEqual(getSystemErrorName(e.errno), 'ENOBUFS');
return true;
});
}

View File

@@ -0,0 +1,60 @@
'use strict';
const { escapePOSIXShell } = require('../common');
// This test checks that the maxBuffer option for child_process.spawnSync()
// works as expected.
const assert = require('assert');
const { getSystemErrorName } = require('util');
const { execSync } = require('child_process');
const msgOut = 'this is stdout';
const msgOutBuf = Buffer.from(`${msgOut}\n`);
const [cmd, opts] = escapePOSIXShell`"${process.execPath}" -e "${`console.log('${msgOut}')`}"`;
// Verify that an error is returned if maxBuffer is surpassed.
{
assert.throws(() => {
execSync(cmd, { ...opts, maxBuffer: 1 });
}, (e) => {
assert.ok(e, 'maxBuffer should error');
assert.strictEqual(e.code, 'ENOBUFS');
assert.strictEqual(getSystemErrorName(e.errno), 'ENOBUFS');
// We can have buffers larger than maxBuffer because underneath we alloc 64k
// that matches our read sizes.
assert.deepStrictEqual(e.stdout, msgOutBuf);
return true;
});
}
// Verify that a maxBuffer size of Infinity works.
{
const ret = execSync(
cmd,
{ ...opts, maxBuffer: Infinity },
);
assert.deepStrictEqual(ret, msgOutBuf);
}
// Default maxBuffer size is 1024 * 1024.
{
assert.throws(() => {
execSync(...escapePOSIXShell`"${process.execPath}" -e "console.log('a'.repeat(1024 * 1024))"`);
}, (e) => {
assert.ok(e, 'maxBuffer should error');
assert.strictEqual(e.code, 'ENOBUFS');
assert.strictEqual(getSystemErrorName(e.errno), 'ENOBUFS');
return true;
});
}
// Default maxBuffer size is 1024 * 1024.
{
const ret = execSync(...escapePOSIXShell`"${process.execPath}" -e "console.log('a'.repeat(1024 * 1024 - 1))"`);
assert.deepStrictEqual(
ret.toString().trim(),
'a'.repeat(1024 * 1024 - 1)
);
}

View File

@@ -0,0 +1,88 @@
'use strict';
const common = require('../common');
// Before https://github.com/nodejs/node/pull/2847 a child process trying
// (asynchronously) to use the closed channel to it's creator caused a segfault.
const assert = require('assert');
const cluster = require('cluster');
const net = require('net');
if (!cluster.isPrimary) {
// Exit on first received handle to leave the queue non-empty in primary
process.on('message', function() {
process.exit(1);
});
return;
}
const server = net
.createServer(function(s) {
if (common.isWindows) {
s.on('error', function(err) {
// Prevent possible ECONNRESET errors from popping up
if (err.code !== 'ECONNRESET') throw err;
});
}
setTimeout(function() {
s.destroy();
}, 100);
})
.listen(0, function() {
const worker = cluster.fork();
worker.on('error', function(err) {
if (
err.code !== 'ECONNRESET' &&
err.code !== 'ECONNREFUSED' &&
err.code !== 'EMFILE'
) {
throw err;
}
});
function send(callback) {
const s = net.connect(server.address().port, function() {
worker.send({}, s, callback);
});
// https://github.com/nodejs/node/issues/3635#issuecomment-157714683
// ECONNREFUSED or ECONNRESET errors can happen if this connection is
// still establishing while the server has already closed.
// EMFILE can happen if the worker __and__ the server had already closed.
s.on('error', function(err) {
if (
err.code !== 'ECONNRESET' &&
err.code !== 'ECONNREFUSED' &&
err.code !== 'EMFILE'
) {
throw err;
}
});
}
worker.process.once(
'close',
common.mustCall(function() {
// Otherwise the crash on `channel.fd` access may happen
assert.strictEqual(worker.process.channel, null);
server.close();
})
);
worker.on('online', function() {
send(function(err) {
assert.ifError(err);
send(function(err) {
// Ignore errors when sending the second handle because the worker
// may already have exited.
if (err && err.code !== 'ERR_IPC_CHANNEL_CLOSED' &&
err.code !== 'ECONNRESET' &&
err.code !== 'ECONNREFUSED' &&
err.code !== 'EMFILE') {
throw err;
}
});
});
});
});

View File

@@ -0,0 +1,109 @@
// Copyright Joyent, Inc. and other Node contributors.
//
// Permission is hereby granted, free of charge, to any person obtaining a
// copy of this software and associated documentation files (the
// "Software"), to deal in the Software without restriction, including
// without limitation the rights to use, copy, modify, merge, publish,
// distribute, sublicense, and/or sell copies of the Software, and to permit
// persons to whom the Software is furnished to do so, subject to the
// following conditions:
//
// The above copyright notice and this permission notice shall be included
// in all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN
// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE
// USE OR OTHER DEALINGS IN THE SOFTWARE.
'use strict';
// The purpose of this test is to make sure that when forking a process,
// sending a fd representing a UDP socket to the child and sending messages
// to this endpoint, these messages are distributed to the parent and the
// child process.
const common = require('../common');
if (common.isWindows)
common.skip('Sending dgram sockets to child processes is not supported');
const dgram = require('dgram');
const fork = require('child_process').fork;
const assert = require('assert');
if (process.argv[2] === 'child') {
let childServer;
process.once('message', (msg, clusterServer) => {
childServer = clusterServer;
childServer.once('message', () => {
process.send('gotMessage');
childServer.close();
});
process.send('handleReceived');
});
} else {
const parentServer = dgram.createSocket('udp4');
const client = dgram.createSocket('udp4');
const child = fork(__filename, ['child']);
const msg = Buffer.from('Some bytes');
let childGotMessage = false;
let parentGotMessage = false;
parentServer.once('message', (msg, rinfo) => {
parentGotMessage = true;
parentServer.close();
});
parentServer.on('listening', () => {
child.send('server', parentServer);
child.on('message', (msg) => {
if (msg === 'gotMessage') {
childGotMessage = true;
} else if (msg === 'handleReceived') {
sendMessages();
}
});
});
function sendMessages() {
const serverPort = parentServer.address().port;
const timer = setInterval(() => {
// Both the parent and the child got at least one message,
// test passed, clean up everything.
if (parentGotMessage && childGotMessage) {
clearInterval(timer);
client.close();
} else {
client.send(
msg,
0,
msg.length,
serverPort,
'127.0.0.1',
(err) => {
assert.ifError(err);
}
);
}
}, 1);
}
parentServer.bind(0, '127.0.0.1');
process.once('exit', () => {
assert(parentGotMessage);
assert(childGotMessage);
});
}

View File

@@ -0,0 +1,111 @@
// Copyright Joyent, Inc. and other Node contributors.
//
// Permission is hereby granted, free of charge, to any person obtaining a
// copy of this software and associated documentation files (the
// "Software"), to deal in the Software without restriction, including
// without limitation the rights to use, copy, modify, merge, publish,
// distribute, sublicense, and/or sell copies of the Software, and to permit
// persons to whom the Software is furnished to do so, subject to the
// following conditions:
//
// The above copyright notice and this permission notice shall be included
// in all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN
// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE
// USE OR OTHER DEALINGS IN THE SOFTWARE.
'use strict';
const common = require('../common');
const assert = require('assert');
const fork = require('child_process').fork;
const net = require('net');
const count = 12;
if (process.argv[2] === 'child') {
const sockets = [];
process.on('message', common.mustCall((m, socket) => {
function sendClosed(id) {
process.send({ id: id, status: 'closed' });
}
if (m.cmd === 'new') {
assert(socket);
assert(socket instanceof net.Socket, 'should be a net.Socket');
sockets.push(socket);
}
if (m.cmd === 'close') {
assert.strictEqual(socket, undefined);
if (sockets[m.id].destroyed) {
// Workaround for https://github.com/nodejs/node/issues/2610
sendClosed(m.id);
// End of workaround. When bug is fixed, this code can be used instead:
// throw new Error('socket destroyed unexpectedly!');
} else {
sockets[m.id].once('close', sendClosed.bind(null, m.id));
sockets[m.id].destroy();
}
}
}));
} else {
const child = fork(process.argv[1], ['child']);
child.on('exit', common.mustCall((code, signal) => {
if (!subprocessKilled) {
assert.fail('subprocess died unexpectedly! ' +
`code: ${code} signal: ${signal}`);
}
}));
const server = net.createServer();
const sockets = [];
server.on('connection', common.mustCall((socket) => {
child.send({ cmd: 'new' }, socket);
sockets.push(socket);
if (sockets.length === count) {
closeSockets(0);
}
}, count));
const onClose = common.mustCall(count);
server.on('listening', common.mustCall(() => {
let j = count;
while (j--) {
const client = net.connect(server.address().port, '127.0.0.1');
client.on('close', onClose);
}
}));
let subprocessKilled = false;
function closeSockets(i) {
if (i === count) {
subprocessKilled = true;
server.close();
child.kill();
return;
}
child.once('message', common.mustCall((m) => {
assert.strictEqual(m.status, 'closed');
server.getConnections(common.mustSucceed((num) => {
assert.strictEqual(num, count - (i + 1));
closeSockets(i + 1);
}));
}));
child.send({ id: i, cmd: 'close' });
}
server.on('close', common.mustCall());
server.listen(0, '127.0.0.1');
}

View File

@@ -0,0 +1,159 @@
// Copyright Joyent, Inc. and other Node contributors.
//
// Permission is hereby granted, free of charge, to any person obtaining a
// copy of this software and associated documentation files (the
// "Software"), to deal in the Software without restriction, including
// without limitation the rights to use, copy, modify, merge, publish,
// distribute, sublicense, and/or sell copies of the Software, and to permit
// persons to whom the Software is furnished to do so, subject to the
// following conditions:
//
// The above copyright notice and this permission notice shall be included
// in all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN
// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE
// USE OR OTHER DEALINGS IN THE SOFTWARE.
'use strict';
const common = require('../common');
const assert = require('assert');
const fork = require('child_process').fork;
const net = require('net');
const debug = require('util').debuglog('test');
const Countdown = require('../common/countdown');
if (process.argv[2] === 'child') {
let serverScope;
// TODO(@jasnell): The message event is not called consistently
// across platforms. Need to investigate if it can be made
// more consistent.
const onServer = (msg, server) => {
if (msg.what !== 'server') return;
process.removeListener('message', onServer);
serverScope = server;
// TODO(@jasnell): This is apparently not called consistently
// across platforms. Need to investigate if it can be made
// more consistent.
server.on('connection', (socket) => {
debug('CHILD: got connection');
process.send({ what: 'connection' });
socket.destroy();
});
// Start making connection from parent.
debug('CHILD: server listening');
process.send({ what: 'listening' });
};
process.on('message', onServer);
// TODO(@jasnell): The close event is not called consistently
// across platforms. Need to investigate if it can be made
// more consistent.
const onClose = (msg) => {
if (msg.what !== 'close') return;
process.removeListener('message', onClose);
serverScope.on('close', common.mustCall(() => {
process.send({ what: 'close' });
}));
serverScope.close();
};
process.on('message', onClose);
process.send({ what: 'ready' });
} else {
const child = fork(process.argv[1], ['child']);
child.on('exit', common.mustCall((code, signal) => {
const message = `CHILD: died with ${code}, ${signal}`;
assert.strictEqual(code, 0, message);
}));
// Send net.Server to child and test by connecting.
function testServer(callback) {
// Destroy server execute callback when done.
const countdown = new Countdown(2, () => {
server.on('close', common.mustCall(() => {
debug('PARENT: server closed');
child.send({ what: 'close' });
}));
server.close();
});
// We expect 4 connections and close events.
const connections = new Countdown(4, () => countdown.dec());
const closed = new Countdown(4, () => countdown.dec());
// Create server and send it to child.
const server = net.createServer();
// TODO(@jasnell): The specific number of times the connection
// event is emitted appears to be variable across platforms.
// Need to investigate why and whether it can be made
// more consistent.
server.on('connection', (socket) => {
debug('PARENT: got connection');
socket.destroy();
connections.dec();
});
server.on('listening', common.mustCall(() => {
debug('PARENT: server listening');
child.send({ what: 'server' }, server);
}));
server.listen(0);
// Handle client messages.
// TODO(@jasnell): The specific number of times the message
// event is emitted appears to be variable across platforms.
// Need to investigate why and whether it can be made
// more consistent.
const messageHandlers = (msg) => {
if (msg.what === 'listening') {
// Make connections.
let socket;
for (let i = 0; i < 4; i++) {
socket = net.connect(server.address().port, common.mustCall(() => {
debug('CLIENT: connected');
}));
socket.on('close', common.mustCall(() => {
closed.dec();
debug('CLIENT: closed');
}));
}
} else if (msg.what === 'connection') {
// Child got connection
connections.dec();
} else if (msg.what === 'close') {
child.removeListener('message', messageHandlers);
callback();
}
};
child.on('message', messageHandlers);
}
const onReady = common.mustCall((msg) => {
if (msg.what !== 'ready') return;
child.removeListener('message', onReady);
testServer(common.mustCall());
});
// Create server and send it to child.
child.on('message', onReady);
}

View File

@@ -0,0 +1,96 @@
// Copyright Joyent, Inc. and other Node contributors.
//
// Permission is hereby granted, free of charge, to any person obtaining a
// copy of this software and associated documentation files (the
// "Software"), to deal in the Software without restriction, including
// without limitation the rights to use, copy, modify, merge, publish,
// distribute, sublicense, and/or sell copies of the Software, and to permit
// persons to whom the Software is furnished to do so, subject to the
// following conditions:
//
// The above copyright notice and this permission notice shall be included
// in all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN
// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE
// USE OR OTHER DEALINGS IN THE SOFTWARE.
'use strict';
const {
mustCall,
mustCallAtLeast,
} = require('../common');
const assert = require('assert');
const fork = require('child_process').fork;
const net = require('net');
const debug = require('util').debuglog('test');
if (process.argv[2] === 'child') {
const onSocket = mustCall((msg, socket) => {
if (msg.what !== 'socket') return;
process.removeListener('message', onSocket);
socket.end('echo');
debug('CHILD: got socket');
});
process.on('message', onSocket);
process.send({ what: 'ready' });
} else {
const child = fork(process.argv[1], ['child']);
child.on('exit', mustCall((code, signal) => {
const message = `CHILD: died with ${code}, ${signal}`;
assert.strictEqual(code, 0, message);
}));
// Send net.Socket to child.
function testSocket() {
// Create a new server and connect to it,
// but the socket will be handled by the child.
const server = net.createServer();
server.on('connection', mustCall((socket) => {
// TODO(@jasnell): Close does not seem to actually be called.
// It is not clear if it is needed.
socket.on('close', () => {
debug('CLIENT: socket closed');
});
child.send({ what: 'socket' }, socket);
}));
server.on('close', mustCall(() => {
debug('PARENT: server closed');
}));
server.listen(0, mustCall(() => {
debug('testSocket, listening');
const connect = net.connect(server.address().port);
let store = '';
connect.on('data', mustCallAtLeast((chunk) => {
store += chunk;
debug('CLIENT: got data');
}));
connect.on('close', mustCall(() => {
debug('CLIENT: closed');
assert.strictEqual(store, 'echo');
server.close();
}));
}));
}
const onReady = mustCall((msg) => {
if (msg.what !== 'ready') return;
child.removeListener('message', onReady);
testSocket();
});
// Create socket and send it to child.
child.on('message', onReady);
}

View File

@@ -0,0 +1,188 @@
// Copyright Joyent, Inc. and other Node contributors.
//
// Permission is hereby granted, free of charge, to any person obtaining a
// copy of this software and associated documentation files (the
// "Software"), to deal in the Software without restriction, including
// without limitation the rights to use, copy, modify, merge, publish,
// distribute, sublicense, and/or sell copies of the Software, and to permit
// persons to whom the Software is furnished to do so, subject to the
// following conditions:
//
// The above copyright notice and this permission notice shall be included
// in all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN
// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE
// USE OR OTHER DEALINGS IN THE SOFTWARE.
// This tests that a socket sent to the forked process works.
// See https://github.com/nodejs/node/commit/dceebbfa
'use strict';
const {
mustCall,
mustCallAtLeast,
platformTimeout,
} = require('../common');
const assert = require('assert');
const fork = require('child_process').fork;
const net = require('net');
const debug = require('util').debuglog('test');
const count = 12;
if (process.argv[2] === 'child') {
const needEnd = [];
const id = process.argv[3];
process.on('message', mustCall((m, socket) => {
if (!socket) return;
debug(`[${id}] got socket ${m}`);
// Will call .end('end') or .write('write');
socket[m](m);
socket.resume();
socket.on('data', mustCallAtLeast(() => {
debug(`[${id}] socket.data ${m}`);
}));
socket.on('end', mustCall(() => {
debug(`[${id}] socket.end ${m}`);
}));
// Store the unfinished socket
if (m === 'write') {
needEnd.push(socket);
}
socket.on('close', mustCall((had_error) => {
debug(`[${id}] socket.close ${had_error} ${m}`);
}));
socket.on('finish', mustCall(() => {
debug(`[${id}] socket finished ${m}`);
}));
}, 4));
process.on('message', mustCall((m) => {
if (m !== 'close') return;
debug(`[${id}] got close message`);
needEnd.forEach((endMe, i) => {
debug(`[${id}] ending ${i}/${needEnd.length}`);
endMe.end('end');
});
}, 4));
process.on('disconnect', mustCall(() => {
debug(`[${id}] process disconnect, ending`);
needEnd.forEach((endMe, i) => {
debug(`[${id}] ending ${i}/${needEnd.length}`);
endMe.end('end');
});
}));
} else {
const child1 = fork(process.argv[1], ['child', '1']);
const child2 = fork(process.argv[1], ['child', '2']);
const child3 = fork(process.argv[1], ['child', '3']);
const server = net.createServer();
let connected = 0;
let closed = 0;
server.on('connection', function(socket) {
switch (connected % 6) {
case 0:
child1.send('end', socket); break;
case 1:
child1.send('write', socket); break;
case 2:
child2.send('end', socket); break;
case 3:
child2.send('write', socket); break;
case 4:
child3.send('end', socket); break;
case 5:
child3.send('write', socket); break;
}
connected += 1;
// TODO(@jasnell): This is not actually being called.
// It is not clear if it is needed.
socket.once('close', () => {
debug(`[m] socket closed, total ${++closed}`);
});
if (connected === count) {
closeServer();
}
});
let disconnected = 0;
server.on('listening', mustCall(() => {
let j = count;
while (j--) {
const client = net.connect(server.address().port, '127.0.0.1');
client.on('error', () => {
// This can happen if we kill the subprocess too early.
// The client should still get a close event afterwards.
// It likely won't so don't wrap in a mustCall.
debug('[m] CLIENT: error event');
});
client.on('close', mustCall(() => {
debug('[m] CLIENT: close event');
disconnected += 1;
}));
client.resume();
}
}));
let closeEmitted = false;
server.on('close', mustCall(function() {
closeEmitted = true;
// Clean up child processes.
try {
child1.kill();
} catch {
debug('child process already terminated');
}
try {
child2.kill();
} catch {
debug('child process already terminated');
}
try {
child3.kill();
} catch {
debug('child process already terminated');
}
}));
server.listen(0, '127.0.0.1');
function closeServer() {
server.close();
setTimeout(() => {
assert(!closeEmitted);
child1.send('close');
child2.send('close');
child3.disconnect();
}, platformTimeout(200));
}
process.on('exit', function() {
assert.strictEqual(server._workers.length, 0);
assert.strictEqual(disconnected, count);
assert.strictEqual(connected, count);
});
}

View File

@@ -0,0 +1,54 @@
'use strict';
const common = require('../common');
const assert = require('assert');
const cp = require('child_process');
const net = require('net');
if (process.argv[2] === 'child') {
process.stdout.write('this should be ignored');
process.stderr.write('this should not be ignored');
const pipe = new net.Socket({ fd: 4 });
process.on('disconnect', () => {
pipe.unref();
});
pipe.setEncoding('utf8');
pipe.on('data', (data) => {
process.send(data);
});
} else {
assert.throws(
() => cp.fork(__filename, { stdio: ['pipe', 'pipe', 'pipe', 'pipe'] }),
{ code: 'ERR_CHILD_PROCESS_IPC_REQUIRED', name: 'Error' });
let ipc = '';
let stderr = '';
const buf = Buffer.from('data to send via pipe');
const child = cp.fork(__filename, ['child'], {
stdio: [0, 'ignore', 'pipe', 'ipc', 'pipe']
});
assert.strictEqual(child.stdout, null);
child.on('message', (msg) => {
ipc += msg;
if (ipc === buf.toString()) {
child.disconnect();
}
});
child.stderr.on('data', (chunk) => {
stderr += chunk;
});
child.on('exit', common.mustCall((code, signal) => {
assert.strictEqual(code, 0);
assert.strictEqual(signal, null);
assert.strictEqual(stderr, 'this should not be ignored');
}));
child.stdio[4].write(buf);
}

View File

@@ -0,0 +1,57 @@
// Flags: --expose-internals
'use strict';
const common = require('../common');
const assert = require('assert');
const { fork } = require('child_process');
const http = require('http');
if (process.argv[2] === 'child') {
process.once('message', (req, socket) => {
const res = new http.ServerResponse(req);
res.assignSocket(socket);
res.end();
});
process.send('ready');
return;
}
const { kTimeout } = require('internal/timers');
let child;
let socket;
const server = http.createServer(common.mustCall((req, res) => {
const r = {
method: req.method,
headers: req.headers,
path: req.path,
httpVersionMajor: req.httpVersionMajor,
query: req.query,
};
socket = res.socket;
child.send(r, socket);
server.close();
}));
server.listen(0, common.mustCall(() => {
child = fork(__filename, [ 'child' ]);
child.once('message', (msg) => {
assert.strictEqual(msg, 'ready');
const req = http.request({
port: server.address().port,
}, common.mustCall((res) => {
res.on('data', () => {});
res.on('end', common.mustCall(() => {
assert.strictEqual(socket[kTimeout], null);
assert.strictEqual(socket.parser, null);
assert.strictEqual(socket._httpMessage, null);
}));
}));
req.end();
});
}));

View File

@@ -0,0 +1,78 @@
'use strict';
const common = require('../common');
if (common.isWindows) {
// https://github.com/nodejs/node/issues/48300
common.skip('Does not work with cygwin quirks on Windows');
}
const assert = require('assert');
const fs = require('fs');
const spawn = require('child_process').spawn;
const tmpdir = require('../common/tmpdir');
let cat, grep, wc;
const KB = 1024;
const MB = KB * KB;
// Make sure process chaining allows desired data flow:
// check cat <file> | grep 'x' | wc -c === 1MB
// This helps to make sure no data is lost between pipes.
{
tmpdir.refresh();
const file = tmpdir.resolve('data.txt');
const buf = Buffer.alloc(MB).fill('x');
// Most OS commands that deal with data, attach special meanings to new line -
// for example, line buffering. So cut the buffer into lines at some points,
// forcing data flow to be split in the stream. Do not use os.EOL for \n as
// that is 2 characters on Windows and is sometimes converted to 1 character
// which causes the test to fail.
for (let i = 1; i < KB; i++)
buf.write('\n', i * KB);
fs.writeFileSync(file, buf.toString());
cat = spawn('cat', [file]);
grep = spawn('grep', ['x'], { stdio: [cat.stdout, 'pipe', 'pipe'] });
wc = spawn('wc', ['-c'], { stdio: [grep.stdout, 'pipe', 'pipe'] });
// Extra checks: We never try to start reading data ourselves.
cat.stdout._handle.readStart = common.mustNotCall();
grep.stdout._handle.readStart = common.mustNotCall();
// Keep an array of error codes and assert on them during process exit. This
// is because stdio can still be open when a child process exits, and we don't
// want to lose information about what caused the error.
const errors = [];
process.on('exit', () => {
assert.deepStrictEqual(errors, []);
});
[cat, grep, wc].forEach((child, index) => {
const errorHandler = (thing, type) => {
// Don't want to assert here, as we might miss error code info.
console.error(`unexpected ${type} from child #${index}:\n${thing}`);
};
child.stderr.on('data', (d) => { errorHandler(d, 'data'); });
child.on('error', (err) => { errorHandler(err, 'error'); });
child.on('exit', common.mustCall((code) => {
if (code !== 0) {
errors.push(`child ${index} exited with code ${code}`);
}
}));
});
let wcBuf = '';
wc.stdout.on('data', common.mustCall((data) => {
wcBuf += data;
}));
process.on('exit', () => {
// Grep always adds one extra byte at the end.
assert.strictEqual(wcBuf.trim(), (MB + 1).toString());
});
}

View File

@@ -0,0 +1,91 @@
import * as common from '../common/index.mjs';
import * as fixtures from '../common/fixtures.mjs';
import { EOL } from 'node:os';
import { strictEqual, notStrictEqual, throws } from 'node:assert';
import cp from 'node:child_process';
// TODO(LiviaMedeiros): test on different platforms
if (!common.isLinux)
common.skip();
const expectedCWD = process.cwd();
const expectedUID = process.getuid();
for (const tamperedCwd of ['', '/tmp', '/not/existing/malicious/path', 42n]) {
Object.prototype.cwd = tamperedCwd;
cp.exec('pwd', common.mustSucceed((out) => {
strictEqual(`${out}`, `${expectedCWD}${EOL}`);
}));
strictEqual(`${cp.execSync('pwd')}`, `${expectedCWD}${EOL}`);
cp.execFile('pwd', common.mustSucceed((out) => {
strictEqual(`${out}`, `${expectedCWD}${EOL}`);
}));
strictEqual(`${cp.execFileSync('pwd')}`, `${expectedCWD}${EOL}`);
cp.spawn('pwd').stdout.on('data', common.mustCall((out) => {
strictEqual(`${out}`, `${expectedCWD}${EOL}`);
}));
strictEqual(`${cp.spawnSync('pwd').stdout}`, `${expectedCWD}${EOL}`);
delete Object.prototype.cwd;
}
for (const tamperedUID of [0, 1, 999, 1000, 0n, 'gwak']) {
Object.prototype.uid = tamperedUID;
cp.exec('id -u', common.mustSucceed((out) => {
strictEqual(`${out}`, `${expectedUID}${EOL}`);
}));
strictEqual(`${cp.execSync('id -u')}`, `${expectedUID}${EOL}`);
cp.execFile('id', ['-u'], common.mustSucceed((out) => {
strictEqual(`${out}`, `${expectedUID}${EOL}`);
}));
strictEqual(`${cp.execFileSync('id', ['-u'])}`, `${expectedUID}${EOL}`);
cp.spawn('id', ['-u']).stdout.on('data', common.mustCall((out) => {
strictEqual(`${out}`, `${expectedUID}${EOL}`);
}));
strictEqual(`${cp.spawnSync('id', ['-u']).stdout}`, `${expectedUID}${EOL}`);
delete Object.prototype.uid;
}
{
Object.prototype.execPath = '/not/existing/malicious/path';
// Does not throw ENOENT
cp.fork(fixtures.path('empty.js'));
delete Object.prototype.execPath;
}
for (const shellCommandArgument of ['-L && echo "tampered"']) {
Object.prototype.shell = true;
const cmd = 'pwd';
let cmdExitCode = '';
const program = cp.spawn(cmd, [shellCommandArgument], { cwd: expectedCWD });
program.stderr.on('data', common.mustCall());
program.stdout.on('data', common.mustNotCall());
program.on('exit', common.mustCall((code) => {
notStrictEqual(code, 0);
}));
cp.execFile(cmd, [shellCommandArgument], { cwd: expectedCWD },
common.mustCall((err) => {
notStrictEqual(err.code, 0);
})
);
throws(() => {
cp.execFileSync(cmd, [shellCommandArgument], { cwd: expectedCWD });
}, (e) => {
notStrictEqual(e.status, 0);
return true;
});
cmdExitCode = cp.spawnSync(cmd, [shellCommandArgument], { cwd: expectedCWD }).status;
notStrictEqual(cmdExitCode, 0);
delete Object.prototype.shell;
}

View File

@@ -0,0 +1,86 @@
// Copyright Joyent, Inc. and other Node contributors.
//
// Permission is hereby granted, free of charge, to any person obtaining a
// copy of this software and associated documentation files (the
// "Software"), to deal in the Software without restriction, including
// without limitation the rights to use, copy, modify, merge, publish,
// distribute, sublicense, and/or sell copies of the Software, and to permit
// persons to whom the Software is furnished to do so, subject to the
// following conditions:
//
// The above copyright notice and this permission notice shall be included
// in all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN
// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE
// USE OR OTHER DEALINGS IN THE SOFTWARE.
'use strict';
// Test that a Linux specific quirk in the handle passing protocol is handled
// correctly. See https://github.com/joyent/node/issues/5330 for details.
const common = require('../common');
const assert = require('assert');
const net = require('net');
const spawn = require('child_process').spawn;
if (process.argv[2] === 'worker')
worker();
else
primary();
function primary() {
// spawn() can only create one IPC channel so we use stdin/stdout as an
// ad-hoc command channel.
const proc = spawn(process.execPath, [
'--expose-internals', __filename, 'worker',
], {
stdio: ['pipe', 'pipe', 'pipe', 'ipc']
});
let handle = null;
proc.on('exit', () => {
handle.close();
});
proc.stdout.on('data', common.mustCall((data) => {
assert.strictEqual(data.toString(), 'ok\r\n');
net.createServer(common.mustNotCall()).listen(0, function() {
handle = this._handle;
proc.send('one');
proc.send('two', handle);
proc.send('three');
proc.stdin.write('ok\r\n');
});
}));
proc.stderr.pipe(process.stderr);
}
function worker() {
const { kChannelHandle } = require('internal/child_process');
process[kChannelHandle].readStop(); // Make messages batch up.
process.stdout.ref();
process.stdout.write('ok\r\n');
process.stdin.once('data', common.mustCall((data) => {
assert.strictEqual(data.toString(), 'ok\r\n');
process[kChannelHandle].readStart();
}));
let n = 0;
process.on('message', common.mustCall((msg, handle) => {
n += 1;
if (n === 1) {
assert.strictEqual(msg, 'one');
assert.strictEqual(handle, undefined);
} else if (n === 2) {
assert.strictEqual(msg, 'two');
assert.ok(handle !== null && typeof handle === 'object');
handle.close();
} else if (n === 3) {
assert.strictEqual(msg, 'three');
assert.strictEqual(handle, undefined);
process.exit();
}
}, 3));
}

View File

@@ -0,0 +1,296 @@
'use strict';
const { mustNotCall } = require('../common');
// Regression test for https://github.com/nodejs/node/issues/44768
const { throws } = require('assert');
const {
exec,
execFile,
execFileSync,
execSync,
fork,
spawn,
spawnSync,
} = require('child_process');
// Tests for the 'command' argument
throws(() => exec(`${process.execPath} ${__filename} AAA BBB\0XXX CCC`, mustNotCall()), {
code: 'ERR_INVALID_ARG_VALUE',
name: 'TypeError',
});
throws(() => exec('BBB\0XXX AAA CCC', mustNotCall()), {
code: 'ERR_INVALID_ARG_VALUE',
name: 'TypeError',
});
throws(() => execSync(`${process.execPath} ${__filename} AAA BBB\0XXX CCC`), {
code: 'ERR_INVALID_ARG_VALUE',
name: 'TypeError',
});
throws(() => execSync('BBB\0XXX AAA CCC'), {
code: 'ERR_INVALID_ARG_VALUE',
name: 'TypeError',
});
// Tests for the 'file' argument
throws(() => spawn('BBB\0XXX'), {
code: 'ERR_INVALID_ARG_VALUE',
name: 'TypeError',
});
throws(() => execFile('BBB\0XXX', mustNotCall()), {
code: 'ERR_INVALID_ARG_VALUE',
name: 'TypeError',
});
throws(() => execFileSync('BBB\0XXX'), {
code: 'ERR_INVALID_ARG_VALUE',
name: 'TypeError',
});
throws(() => spawn('BBB\0XXX'), {
code: 'ERR_INVALID_ARG_VALUE',
name: 'TypeError',
});
throws(() => spawnSync('BBB\0XXX'), {
code: 'ERR_INVALID_ARG_VALUE',
name: 'TypeError',
});
// Tests for the 'modulePath' argument
throws(() => fork('BBB\0XXX'), {
code: 'ERR_INVALID_ARG_VALUE',
name: 'TypeError',
});
// Tests for the 'args' argument
// Not testing exec() and execSync() because these accept 'args' as a part of
// 'command' as space-separated arguments.
throws(() => execFile(process.execPath, [__filename, 'AAA', 'BBB\0XXX', 'CCC'], mustNotCall()), {
code: 'ERR_INVALID_ARG_VALUE',
name: 'TypeError',
});
throws(() => execFileSync(process.execPath, [__filename, 'AAA', 'BBB\0XXX', 'CCC']), {
code: 'ERR_INVALID_ARG_VALUE',
name: 'TypeError',
});
throws(() => fork(__filename, ['AAA', 'BBB\0XXX', 'CCC']), {
code: 'ERR_INVALID_ARG_VALUE',
name: 'TypeError',
});
throws(() => spawn(process.execPath, [__filename, 'AAA', 'BBB\0XXX', 'CCC']), {
code: 'ERR_INVALID_ARG_VALUE',
name: 'TypeError',
});
throws(() => spawnSync(process.execPath, [__filename, 'AAA', 'BBB\0XXX', 'CCC']), {
code: 'ERR_INVALID_ARG_VALUE',
name: 'TypeError',
});
// Tests for the 'options.cwd' argument
throws(() => exec(process.execPath, { cwd: 'BBB\0XXX' }, mustNotCall()), {
code: 'ERR_INVALID_ARG_VALUE',
name: 'TypeError',
});
throws(() => execFile(process.execPath, { cwd: 'BBB\0XXX' }, mustNotCall()), {
code: 'ERR_INVALID_ARG_VALUE',
name: 'TypeError',
});
throws(() => execFileSync(process.execPath, { cwd: 'BBB\0XXX' }), {
code: 'ERR_INVALID_ARG_VALUE',
name: 'TypeError',
});
throws(() => execSync(process.execPath, { cwd: 'BBB\0XXX' }), {
code: 'ERR_INVALID_ARG_VALUE',
name: 'TypeError',
});
throws(() => fork(__filename, { cwd: 'BBB\0XXX' }), {
code: 'ERR_INVALID_ARG_VALUE',
name: 'TypeError',
});
throws(() => spawn(process.execPath, { cwd: 'BBB\0XXX' }), {
code: 'ERR_INVALID_ARG_VALUE',
name: 'TypeError',
});
throws(() => spawnSync(process.execPath, { cwd: 'BBB\0XXX' }), {
code: 'ERR_INVALID_ARG_VALUE',
name: 'TypeError',
});
// Tests for the 'options.argv0' argument
throws(() => exec(process.execPath, { argv0: 'BBB\0XXX' }, mustNotCall()), {
code: 'ERR_INVALID_ARG_VALUE',
name: 'TypeError',
});
throws(() => execFile(process.execPath, { argv0: 'BBB\0XXX' }, mustNotCall()), {
code: 'ERR_INVALID_ARG_VALUE',
name: 'TypeError',
});
throws(() => execFileSync(process.execPath, { argv0: 'BBB\0XXX' }), {
code: 'ERR_INVALID_ARG_VALUE',
name: 'TypeError',
});
throws(() => execSync(process.execPath, { argv0: 'BBB\0XXX' }), {
code: 'ERR_INVALID_ARG_VALUE',
name: 'TypeError',
});
throws(() => fork(__filename, { argv0: 'BBB\0XXX' }), {
code: 'ERR_INVALID_ARG_VALUE',
name: 'TypeError',
});
throws(() => spawn(process.execPath, { argv0: 'BBB\0XXX' }), {
code: 'ERR_INVALID_ARG_VALUE',
name: 'TypeError',
});
throws(() => spawnSync(process.execPath, { argv0: 'BBB\0XXX' }), {
code: 'ERR_INVALID_ARG_VALUE',
name: 'TypeError',
});
// Tests for the 'options.shell' argument
throws(() => exec(process.execPath, { shell: 'BBB\0XXX' }, mustNotCall()), {
code: 'ERR_INVALID_ARG_VALUE',
name: 'TypeError',
});
throws(() => execFile(process.execPath, { shell: 'BBB\0XXX' }, mustNotCall()), {
code: 'ERR_INVALID_ARG_VALUE',
name: 'TypeError',
});
throws(() => execFileSync(process.execPath, { shell: 'BBB\0XXX' }), {
code: 'ERR_INVALID_ARG_VALUE',
name: 'TypeError',
});
throws(() => execSync(process.execPath, { shell: 'BBB\0XXX' }), {
code: 'ERR_INVALID_ARG_VALUE',
name: 'TypeError',
});
// Not testing fork() because it doesn't accept the shell option (internally it
// explicitly sets shell to false).
throws(() => spawn(process.execPath, { shell: 'BBB\0XXX' }), {
code: 'ERR_INVALID_ARG_VALUE',
name: 'TypeError',
});
throws(() => spawnSync(process.execPath, { shell: 'BBB\0XXX' }), {
code: 'ERR_INVALID_ARG_VALUE',
name: 'TypeError',
});
// Tests for the 'options.env' argument
throws(() => exec(process.execPath, { env: { 'AAA': 'BBB\0XXX' } }, mustNotCall()), {
code: 'ERR_INVALID_ARG_VALUE',
name: 'TypeError',
});
throws(() => exec(process.execPath, { env: { 'BBB\0XXX': 'AAA' } }, mustNotCall()), {
code: 'ERR_INVALID_ARG_VALUE',
name: 'TypeError',
});
throws(() => execFile(process.execPath, { env: { 'AAA': 'BBB\0XXX' } }, mustNotCall()), {
code: 'ERR_INVALID_ARG_VALUE',
name: 'TypeError',
});
throws(() => execFile(process.execPath, { env: { 'BBB\0XXX': 'AAA' } }, mustNotCall()), {
code: 'ERR_INVALID_ARG_VALUE',
name: 'TypeError',
});
throws(() => execFileSync(process.execPath, { env: { 'AAA': 'BBB\0XXX' } }), {
code: 'ERR_INVALID_ARG_VALUE',
name: 'TypeError',
});
throws(() => execFileSync(process.execPath, { env: { 'BBB\0XXX': 'AAA' } }), {
code: 'ERR_INVALID_ARG_VALUE',
name: 'TypeError',
});
throws(() => execSync(process.execPath, { env: { 'AAA': 'BBB\0XXX' } }), {
code: 'ERR_INVALID_ARG_VALUE',
name: 'TypeError',
});
throws(() => execSync(process.execPath, { env: { 'BBB\0XXX': 'AAA' } }), {
code: 'ERR_INVALID_ARG_VALUE',
name: 'TypeError',
});
throws(() => fork(__filename, { env: { 'AAA': 'BBB\0XXX' } }), {
code: 'ERR_INVALID_ARG_VALUE',
name: 'TypeError',
});
throws(() => fork(__filename, { env: { 'BBB\0XXX': 'AAA' } }), {
code: 'ERR_INVALID_ARG_VALUE',
name: 'TypeError',
});
throws(() => spawn(process.execPath, { env: { 'AAA': 'BBB\0XXX' } }), {
code: 'ERR_INVALID_ARG_VALUE',
name: 'TypeError',
});
throws(() => spawn(process.execPath, { env: { 'BBB\0XXX': 'AAA' } }), {
code: 'ERR_INVALID_ARG_VALUE',
name: 'TypeError',
});
throws(() => spawnSync(process.execPath, { env: { 'AAA': 'BBB\0XXX' } }), {
code: 'ERR_INVALID_ARG_VALUE',
name: 'TypeError',
});
throws(() => spawnSync(process.execPath, { env: { 'BBB\0XXX': 'AAA' } }), {
code: 'ERR_INVALID_ARG_VALUE',
name: 'TypeError',
});
// Tests for the 'options.execPath' argument
throws(() => fork(__filename, { execPath: 'BBB\0XXX' }), {
code: 'ERR_INVALID_ARG_VALUE',
name: 'TypeError',
});
// Tests for the 'options.execArgv' argument
if(typeof Bun === 'undefined') { // This test is disabled in bun because bun does not support execArgv.
throws(() => fork(__filename, { execArgv: ['AAA', 'BBB\0XXX', 'CCC'] }), {
code: 'ERR_INVALID_ARG_VALUE',
name: 'TypeError',
});
}

View File

@@ -0,0 +1,50 @@
'use strict';
const common = require('../common');
const assert = require('assert');
const cp = require('child_process');
const net = require('net');
if (process.argv[2] !== 'child') {
// The parent process forks a child process, starts a TCP server, and connects
// to the server. The accepted connection is passed to the child process,
// where the socket is written. Then, the child signals the parent process to
// write to the same socket.
let result = '';
process.on('exit', () => {
assert.strictEqual(result, 'childparent');
});
const child = cp.fork(__filename, ['child']);
// Verify that the child exits successfully
child.on('exit', common.mustCall((exitCode, signalCode) => {
assert.strictEqual(exitCode, 0);
assert.strictEqual(signalCode, null);
}));
const server = net.createServer((socket) => {
child.on('message', common.mustCall((msg) => {
assert.strictEqual(msg, 'child_done');
socket.end('parent', () => {
server.close();
child.disconnect();
});
}));
child.send('socket', socket, { keepOpen: true }, common.mustSucceed());
});
server.listen(0, () => {
const socket = net.connect(server.address().port, common.localhostIPv4);
socket.setEncoding('utf8');
socket.on('data', (data) => result += data);
});
} else {
// The child process receives the socket from the parent, writes data to
// the socket, then signals the parent process to write
process.on('message', common.mustCall((msg, socket) => {
assert.strictEqual(msg, 'socket');
socket.write('child', () => process.send('child_done'));
}));
}

View File

@@ -0,0 +1,58 @@
'use strict';
const common = require('../common');
// subprocess.send() will return false if the channel has closed or when the
// backlog of unsent messages exceeds a threshold that makes it unwise to send
// more. Otherwise, the method returns true.
const assert = require('assert');
const net = require('net');
const { fork, spawn } = require('child_process');
const fixtures = require('../common/fixtures');
// Just a script that stays alive (does not listen to `process.on('message')`).
const subScript = fixtures.path('child-process-persistent.js');
{
// Test `send` return value on `fork` that opens and IPC by default.
const n = fork(subScript);
// `subprocess.send` should always return `true` for the first send.
const rv = n.send({ h: 'w' }, assert.ifError);
assert.strictEqual(rv, true);
n.kill('SIGKILL');
}
{
// Test `send` return value on `spawn` and saturate backlog with handles.
// Call `spawn` with options that open an IPC channel.
const spawnOptions = { stdio: ['pipe', 'pipe', 'pipe', 'ipc'] };
const s = spawn(process.execPath, [subScript], spawnOptions);
const server = net.createServer(common.mustNotCall()).listen(0, () => {
const handle = server._handle;
// Sending a handle and not giving the tickQueue time to acknowledge should
// create the internal backlog, but leave it empty.
const rv1 = s.send('one', handle, (err) => { if (err) assert.fail(err); });
assert.strictEqual(rv1, true);
// Since the first `send` included a handle (should be unacknowledged),
// we can safely queue up only one more message.
const rv2 = s.send('two', (err) => { if (err) assert.fail(err); });
assert.strictEqual(rv2, true);
// The backlog should now be indicate to backoff.
const rv3 = s.send('three', (err) => { if (err) assert.fail(err); });
assert.strictEqual(rv3, false);
const rv4 = s.send('four', (err) => {
if (err) assert.fail(err);
// `send` queue should have been drained.
const rv5 = s.send('5', handle, (err) => { if (err) assert.fail(err); });
assert.strictEqual(rv5, true);
// End test and cleanup.
s.kill();
handle.close();
server.close();
});
assert.strictEqual(rv4, false);
});
}

View File

@@ -0,0 +1,40 @@
'use strict';
const common = require('../common');
const assert = require('assert');
const { fork, spawn } = require('child_process');
const net = require('net');
const tmpdir = require('../common/tmpdir');
// Run in a child process because the PIPE file descriptor stays open until
// Node.js completes, blocking the tmpdir and preventing cleanup.
if (process.argv[2] !== 'child') {
// Parent
tmpdir.refresh();
// Run test
const child = fork(__filename, ['child'], { stdio: 'inherit' });
child.on('exit', common.mustCall(function(code) {
assert.strictEqual(code, 0);
}));
return;
}
// Child
const server = net.createServer((conn) => {
spawn(process.execPath, ['-v'], {
stdio: ['ignore', conn, 'ignore']
}).on('close', common.mustCall(() => {
conn.end();
}));
}).listen(common.PIPE, () => {
const client = net.connect(common.PIPE, common.mustCall());
client.once('data', () => {
client.end(() => {
server.close();
});
});
});

View File

@@ -0,0 +1,97 @@
'use strict';
// Node.js on Windows should not be able to spawn batch files directly,
// only when the 'shell' option is set. An undocumented feature of the
// Win32 CreateProcess API allows spawning .bat and .cmd files directly
// but it does not sanitize arguments. We cannot do that automatically
// because it's sometimes impossible to escape arguments unambiguously.
//
// Expectation: spawn() and spawnSync() raise EINVAL if and only if:
//
// 1. 'shell' option is unset
// 2. Platform is Windows
// 3. Filename ends in .bat or .cmd, case-insensitive
//
// exec() and execSync() are unchanged.
const common = require('../common');
const cp = require('child_process');
const assert = require('assert');
const { isWindows } = common;
const expectedCode = isWindows ? 'EINVAL' : 'ENOENT';
const expectedStatus = isWindows ? 1 : 127;
const suffixes =
'BAT|bAT|BaT|baT|BAt|bAt|Bat|bat|CMD|cMD|CmD|cmD|CMd|cMd|Cmd|cmd|cmd |cmd .|cmd ....'
.split('|');
function testExec(filename) {
return new Promise((resolve) => {
cp.exec(filename).once('exit', common.mustCall(function(status) {
assert.strictEqual(status, expectedStatus);
resolve();
}));
});
}
function testExecSync(filename) {
let e;
try {
cp.execSync(filename);
} catch (_e) {
e = _e;
}
if (!e) throw new Error(`Exception expected for ${filename}`);
assert.strictEqual(e.status, expectedStatus);
}
function testSpawn(filename, code) {
// Batch file case is a synchronous error, file-not-found is asynchronous.
if (code === 'EINVAL') {
let e;
try {
cp.spawn(filename);
} catch (_e) {
e = _e;
}
if (!e) throw new Error(`Exception expected for ${filename}`);
assert.strictEqual(e.code, code);
} else {
return new Promise((resolve) => {
cp.spawn(filename).once('error', common.mustCall(function(e) {
assert.strictEqual(e.code, code);
resolve();
}));
});
}
}
function testSpawnSync(filename, code) {
{
const r = cp.spawnSync(filename);
assert.strictEqual(r.error.code, code);
}
{
const r = cp.spawnSync(filename, { shell: true });
assert.strictEqual(r.status, expectedStatus);
}
}
testExecSync('./nosuchdir/nosuchfile');
testSpawnSync('./nosuchdir/nosuchfile', 'ENOENT');
for (const suffix of suffixes) {
testExecSync(`./nosuchdir/nosuchfile.${suffix}`);
testSpawnSync(`./nosuchdir/nosuchfile.${suffix}`, expectedCode);
}
go().catch((ex) => { throw ex; });
async function go() {
await testExec('./nosuchdir/nosuchfile');
await testSpawn('./nosuchdir/nosuchfile', 'ENOENT');
for (const suffix of suffixes) {
await testExec(`./nosuchdir/nosuchfile.${suffix}`);
await testSpawn(`./nosuchdir/nosuchfile.${suffix}`, expectedCode);
}
}

View File

@@ -0,0 +1,49 @@
// This test is modified to not test node internals, only public APIs.
'use strict';
const common = require('../common');
const assert = require('assert');
const cp = require('child_process');
if (process.argv[2] === 'child') {
setInterval(() => {}, 1000);
} else {
const { SIGKILL } = require('os').constants.signals;
function spawn(killSignal) {
const child = cp.spawnSync(process.execPath,
[__filename, 'child'],
{ killSignal, timeout: 100 });
assert.strictEqual(child.status, null);
assert.strictEqual(child.error.code, 'ETIMEDOUT');
return child;
}
// Verify that an error is thrown for unknown signals.
assert.throws(() => {
spawn('SIG_NOT_A_REAL_SIGNAL');
}, { code: 'ERR_UNKNOWN_SIGNAL', name: 'TypeError' });
// Verify that the default kill signal is SIGTERM.
{
const child = spawn(undefined);
assert.strictEqual(child.signal, 'SIGTERM');
}
// Verify that a string signal name is handled properly.
{
const child = spawn('SIGKILL');
assert.strictEqual(child.signal, 'SIGKILL');
}
// Verify that a numeric signal is handled properly.
{
assert.strictEqual(typeof SIGKILL, 'number');
const child = spawn(SIGKILL);
assert.strictEqual(child.signal, 'SIGKILL');
}
}

View File

@@ -0,0 +1,58 @@
'use strict';
require('../common');
// This test checks that the maxBuffer option for child_process.spawnSync()
// works as expected.
const assert = require('assert');
const spawnSync = require('child_process').spawnSync;
const { getSystemErrorName } = require('util');
const msgOut = 'this is stdout';
const msgOutBuf = Buffer.from(`${msgOut}\n`);
const args = [
'-e',
`console.log("${msgOut}");`,
];
// Verify that an error is returned if maxBuffer is surpassed.
{
const ret = spawnSync(process.execPath, args, { maxBuffer: 1 });
assert.ok(ret.error, 'maxBuffer should error');
assert.strictEqual(ret.error.code, 'ENOBUFS');
assert.strictEqual(getSystemErrorName(ret.error.errno), 'ENOBUFS');
// We can have buffers larger than maxBuffer because underneath we alloc 64k
// that matches our read sizes.
assert.deepStrictEqual(ret.stdout, msgOutBuf);
}
// Verify that a maxBuffer size of Infinity works.
{
const ret = spawnSync(process.execPath, args, { maxBuffer: Infinity });
assert.ifError(ret.error);
assert.deepStrictEqual(ret.stdout, msgOutBuf);
}
// Default maxBuffer size is 1024 * 1024.
{
const args = ['-e', "console.log('a'.repeat(1024 * 1024))"];
const ret = spawnSync(process.execPath, args);
assert.ok(ret.error, 'maxBuffer should error');
assert.strictEqual(ret.error.code, 'ENOBUFS');
assert.strictEqual(getSystemErrorName(ret.error.errno), 'ENOBUFS');
}
// Default maxBuffer size is 1024 * 1024.
{
const args = ['-e', "console.log('a'.repeat(1024 * 1024 - 1))"];
const ret = spawnSync(process.execPath, args);
assert.ifError(ret.error);
assert.deepStrictEqual(
ret.stdout.toString().trim(),
'a'.repeat(1024 * 1024 - 1)
);
}

View File

@@ -0,0 +1,81 @@
// This test is modified to not test node internals, only public APIs. It is also modified to use `-p` rather than `-pe` because Bun does not support `-pe`.
'use strict';
const common = require('../common');
const assert = require('assert');
const cp = require('child_process');
// Verify that a shell is, in fact, executed
const doesNotExist = cp.spawnSync('does-not-exist', { shell: true });
assert.notStrictEqual(doesNotExist.file, 'does-not-exist');
assert.strictEqual(doesNotExist.error, undefined);
assert.strictEqual(doesNotExist.signal, null);
if (common.isWindows)
assert.strictEqual(doesNotExist.status, 1); // Exit code of cmd.exe
else
assert.strictEqual(doesNotExist.status, 127); // Exit code of /bin/sh
// Verify that passing arguments works
const echo = cp.spawnSync('echo', ['foo'], { shell: true });
assert.strictEqual(echo.stdout.toString().trim(), 'foo');
// Verify that shell features can be used
const cmd = 'echo bar | cat';
const command = cp.spawnSync(cmd, { shell: true });
assert.strictEqual(command.stdout.toString().trim(), 'bar');
// Verify that the environment is properly inherited
const env = cp.spawnSync(`"${common.isWindows ? process.execPath : '$NODE'}" -p process.env.BAZ`, {
env: { ...process.env, BAZ: 'buzz', NODE: process.execPath },
shell: true
});
assert.strictEqual(env.stdout.toString().trim(), 'buzz');
// Verify that the shell internals work properly across platforms.
{
const originalComspec = process.env.comspec;
// Enable monkey patching process.platform.
const originalPlatform = process.platform;
let platform = null;
Object.defineProperty(process, 'platform', { get: () => platform });
function test(testPlatform, shell, shellOutput) {
platform = testPlatform;
const cmd = 'not_a_real_command';
cp.spawnSync(cmd, { shell });
}
// Test Unix platforms with the default shell.
test('darwin', true, '/bin/sh');
// Test Unix platforms with a user specified shell.
test('darwin', '/bin/csh', '/bin/csh');
// Test Android platforms.
test('android', true, '/system/bin/sh');
// Test Windows platforms with a user specified shell.
test('win32', 'powershell.exe', 'powershell.exe');
// Test Windows platforms with the default shell and no comspec.
delete process.env.comspec;
test('win32', true, 'cmd.exe');
// Test Windows platforms with the default shell and a comspec value.
process.env.comspec = 'powershell.exe';
test('win32', true, process.env.comspec);
// Restore the original value of process.platform.
platform = originalPlatform;
// Restore the original comspec environment variable if necessary.
if (originalComspec)
process.env.comspec = originalComspec;
}

View File

@@ -0,0 +1,62 @@
// Copyright Joyent, Inc. and other Node contributors.
//
// Permission is hereby granted, free of charge, to any person obtaining a
// copy of this software and associated documentation files (the
// "Software"), to deal in the Software without restriction, including
// without limitation the rights to use, copy, modify, merge, publish,
// distribute, sublicense, and/or sell copies of the Software, and to permit
// persons to whom the Software is furnished to do so, subject to the
// following conditions:
//
// The above copyright notice and this permission notice shall be included
// in all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN
// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE
// USE OR OTHER DEALINGS IN THE SOFTWARE.
'use strict';
const {
mustCall,
mustCallAtLeast,
mustNotCall,
} = require('../common');
const assert = require('assert');
const debug = require('util').debuglog('test');
const spawn = require('child_process').spawn;
const cat = spawn('cat');
cat.stdin.write('hello');
cat.stdin.write(' ');
cat.stdin.write('world');
assert.strictEqual(cat.stdin.writable, true);
assert.strictEqual(cat.stdin.readable, false);
cat.stdin.end();
let response = '';
cat.stdout.setEncoding('utf8');
cat.stdout.on('data', mustCallAtLeast((chunk) => {
debug(`stdout: ${chunk}`);
response += chunk;
}));
cat.stdout.on('end', mustCall());
cat.stderr.on('data', mustNotCall());
cat.stderr.on('end', mustCall());
cat.on('exit', mustCall((status) => {
assert.strictEqual(status, 0);
}));
cat.on('close', mustCall(() => {
assert.strictEqual(response, 'hello world');
}));

View File

@@ -0,0 +1,30 @@
'use strict';
const common = require('../common');
const assert = require('assert');
const { spawn } = require('child_process');
// Regression test for https://github.com/nodejs/node/issues/27097.
// Check that (cat [p1] ; cat [p2]) | cat [p3] works.
const p3 = spawn('cat', { stdio: ['pipe', 'pipe', 'inherit'] });
const p1 = spawn('cat', { stdio: ['pipe', p3.stdin, 'inherit'] });
const p2 = spawn('cat', { stdio: ['pipe', p3.stdin, 'inherit'] });
p3.stdout.setEncoding('utf8');
// Write three different chunks:
// - 'hello' from this process to p1 to p3 back to us
// - 'world' from this process to p2 to p3 back to us
// - 'foobar' from this process to p3 back to us
// Do so sequentially in order to avoid race conditions.
p1.stdin.end('hello\n');
p3.stdout.once('data', common.mustCall((chunk) => {
assert.strictEqual(chunk, 'hello\n');
p2.stdin.end('world\n');
p3.stdout.once('data', common.mustCall((chunk) => {
assert.strictEqual(chunk, 'world\n');
p3.stdin.end('foobar\n');
p3.stdout.once('data', common.mustCall((chunk) => {
assert.strictEqual(chunk, 'foobar\n');
}));
}));
}));

View File

@@ -0,0 +1,33 @@
'use strict';
const common = require('../common');
if (common.isWindows) {
// https://github.com/nodejs/node/issues/48300
common.skip('Does not work with cygwin quirks on Windows');
}
const assert = require('assert');
const { spawn } = require('child_process');
// Check that, once a child process has ended, its safe to read from a pipe
// that the child had used as input.
// We simulate that using cat | (head -n1; ...)
const p1 = spawn('cat', { stdio: ['pipe', 'pipe', 'inherit'] });
const p2 = spawn('head', ['-n1'], { stdio: [p1.stdout, 'pipe', 'inherit'] });
// First, write the line that gets passed through p2, making 'head' exit.
p1.stdin.write('hello\n');
p2.stdout.setEncoding('utf8');
p2.stdout.on('data', common.mustCall((chunk) => {
assert.strictEqual(chunk, 'hello\n');
}));
p2.on('exit', common.mustCall(() => {
// We can now use cats output, because 'head' is no longer reading from it.
p1.stdin.end('world\n');
p1.stdout.setEncoding('utf8');
p1.stdout.on('data', common.mustCall((chunk) => {
assert.strictEqual(chunk, 'world\n');
}));
p1.stdout.resume();
}));

View File

@@ -0,0 +1,20 @@
'use strict';
const common = require('../common');
const assert = require('assert');
const spawn = require('child_process').spawn;
const expectedError = common.isWindows ? /\bENOTSUP\b/ : /\bEPERM\b/;
if (common.isIBMi)
common.skip('IBMi has a different behavior');
if (common.isWindows || process.getuid() !== 0) {
assert.throws(() => {
spawn('echo', ['fhqwhgads'], { uid: 0 });
}, expectedError);
}
if (common.isWindows || !process.getgroups().some((gid) => gid === 0)) {
assert.throws(() => {
spawn('echo', ['fhqwhgads'], { gid: 0 });
}, expectedError);
}

View File

@@ -0,0 +1,38 @@
// This test is modified to not test node internals, only public APIs. windowsHide is not observable,
// so this only tests that the flag does not cause an error.
'use strict';
const common = require('../common');
const assert = require('assert');
const cp = require('child_process');
const { test } = require('node:test');
const cmd = process.execPath;
const args = ['-p', '42'];
const options = { windowsHide: true };
test('spawnSync() passes windowsHide correctly', (t) => {
const child = cp.spawnSync(cmd, args, options);
assert.strictEqual(child.status, 0);
assert.strictEqual(child.signal, null);
assert.strictEqual(child.stdout.toString().trim(), '42');
assert.strictEqual(child.stderr.toString().trim(), '');
});
test('spawn() passes windowsHide correctly', (t, done) => {
const child = cp.spawn(cmd, args, options);
child.on('exit', common.mustCall((code, signal) => {
assert.strictEqual(code, 0);
assert.strictEqual(signal, null);
done();
}));
});
test('execFile() passes windowsHide correctly', (t, done) => {
cp.execFile(cmd, args, options, common.mustSucceed((stdout, stderr) => {
assert.strictEqual(stdout.trim(), '42');
assert.strictEqual(stderr.trim(), '');
done();
}));
});

View File

@@ -0,0 +1,29 @@
'use strict';
const common = require('../common');
const net = require('net');
const assert = require('assert');
const { fork } = require('child_process');
// This test should end immediately after `unref` is called
// The pipe will stay open as Node.js completes, thus run in a child process
// so that tmpdir can be cleaned up.
const tmpdir = require('../common/tmpdir');
if (process.argv[2] !== 'child') {
// Parent
tmpdir.refresh();
// Run test
const child = fork(__filename, ['child'], { stdio: 'inherit' });
child.on('exit', common.mustCall(function(code) {
assert.strictEqual(code, 0);
}));
return;
}
// Child
const s = net.Server();
s.listen(common.PIPE);
s.unref();

View File

@@ -0,0 +1,42 @@
'use strict';
const common = require('../common');
if (!common.isMainThread)
common.skip("Workers don't have process-like stdio");
// Test if Node handles redirecting one child process stdout to another
// process stdin without crashing.
const spawn = require('child_process').spawn;
const writeSize = 100;
const totalDots = 10000;
const who = process.argv.length <= 2 ? 'parent' : process.argv[2];
switch (who) {
case 'parent': {
const consumer = spawn(process.argv0, [process.argv[1], 'consumer'], {
stdio: ['pipe', 'ignore', 'inherit'],
});
const producer = spawn(process.argv0, [process.argv[1], 'producer'], {
stdio: ['pipe', consumer.stdin, 'inherit'],
});
process.stdin.on('data', () => {});
producer.on('exit', process.exit);
break;
}
case 'producer': {
const buffer = Buffer.alloc(writeSize, '.');
let written = 0;
const write = () => {
if (written < totalDots) {
written += writeSize;
process.stdout.write(buffer, write);
}
};
write();
break;
}
case 'consumer':
process.stdin.on('data', () => {});
break;
}

View File

@@ -0,0 +1,27 @@
'use strict';
const common = require('../common');
const assert = require('assert');
const child_process = require('child_process');
const fixtures = require('../common/fixtures');
const { getSystemErrorName } = require('util');
const testScript = fixtures.path('catch-stdout-error.js');
const child = child_process.exec(
...common.escapePOSIXShell`"${process.execPath}" "${testScript}" | "${process.execPath}" -pe "process.stdin.on('data' , () => process.exit(1))"`
);
let output = '';
child.stderr.on('data', function(c) {
output += c;
});
child.on('close', common.mustCall(function(code) {
output = JSON.parse(output);
assert.strictEqual(output.code, 'EPIPE');
assert.strictEqual(getSystemErrorName(output.errno), 'EPIPE');
assert.strictEqual(output.syscall, 'write');
console.log('ok');
}));

View File

@@ -0,0 +1,78 @@
// Copyright Joyent, Inc. and other Node contributors.
//
// Permission is hereby granted, free of charge, to any person obtaining a
// copy of this software and associated documentation files (the
// "Software"), to deal in the Software without restriction, including
// without limitation the rights to use, copy, modify, merge, publish,
// distribute, sublicense, and/or sell copies of the Software, and to permit
// persons to whom the Software is furnished to do so, subject to the
// following conditions:
//
// The above copyright notice and this permission notice shall be included
// in all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN
// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE
// USE OR OTHER DEALINGS IN THE SOFTWARE.
'use strict';
const common = require('../common');
if (common.isWindows)
common.skip('no RLIMIT_NOFILE on Windows');
const assert = require('assert');
const child_process = require('child_process');
const fs = require('fs');
const ulimit = Number(child_process.execSync('ulimit -Hn'));
if (ulimit > 64 || Number.isNaN(ulimit)) {
const [cmd, opts] = common.escapePOSIXShell`ulimit -n 64 && "${process.execPath}" "${__filename}"`;
// Sorry about this nonsense. It can be replaced if
// https://github.com/nodejs/node-v0.x-archive/pull/2143#issuecomment-2847886
// ever happens.
const result = child_process.spawnSync(
'/bin/sh',
['-c', cmd],
opts,
);
assert.strictEqual(result.stdout.toString(), '');
assert.strictEqual(result.stderr.toString(), '');
assert.strictEqual(result.status, 0);
assert.strictEqual(result.error, undefined);
return;
}
const openFds = [];
for (;;) {
try {
openFds.push(fs.openSync(__filename, 'r'));
} catch (err) {
assert.strictEqual(err.code, 'EMFILE');
break;
}
}
// Should emit an error, not throw.
const proc = child_process.spawn(process.execPath, ['-e', '0']);
// Verify that stdio is not setup on EMFILE or ENFILE.
assert.strictEqual(proc.stdin, undefined);
assert.strictEqual(proc.stdout, undefined);
assert.strictEqual(proc.stderr, undefined);
assert.strictEqual(proc.stdio, undefined);
proc.on('error', common.mustCall(function(err) {
assert.strictEqual(err.code, 'EMFILE');
}));
proc.on('exit', common.mustNotCall('"exit" event should not be emitted'));
// Close one fd for LSan
if (openFds.length >= 1) {
fs.closeSync(openFds.pop());
}

View File

@@ -0,0 +1,84 @@
'use strict';
const common = require('../common');
// On some OS X versions, when passing fd's between processes:
// When the handle associated to a specific file descriptor is closed by the
// sender process before it's received in the destination, the handle is indeed
// closed while it should remain opened. In order to fix this behavior, don't
// close the handle until the `NODE_HANDLE_ACK` is received by the sender.
// This test is basically `test-cluster-net-send` but creating lots of workers
// so the issue reproduces on OS X consistently.
if (common.isPi) {
common.skip('Too slow for Raspberry Pi devices');
}
const assert = require('assert');
const { fork } = require('child_process');
const net = require('net');
const N = 80;
let messageCallbackCount = 0;
function forkWorker() {
const messageCallback = (msg, handle) => {
messageCallbackCount++;
assert.strictEqual(msg, 'handle');
assert.ok(handle);
worker.send('got');
let recvData = '';
handle.on('data', common.mustCall((data) => {
recvData += data;
}));
handle.on('end', () => {
assert.strictEqual(recvData, 'hello');
worker.kill();
});
};
const worker = fork(__filename, ['child']);
worker.on('error', (err) => {
if (/\bEAGAIN\b/.test(err.message)) {
forkWorker();
return;
}
throw err;
});
worker.once('message', messageCallback);
}
if (process.argv[2] !== 'child') {
for (let i = 0; i < N; ++i) {
forkWorker();
}
process.on('exit', () => { assert.strictEqual(messageCallbackCount, N); });
} else {
let socket;
let cbcalls = 0;
function socketConnected() {
if (++cbcalls === 2)
process.send('handle', socket);
}
// As a side-effect, listening for the message event will ref the IPC channel,
// so the child process will stay alive as long as it has a parent process/IPC
// channel. Once this is done, we can unref our client and server sockets, and
// the only thing keeping this worker alive will be IPC. This is important,
// because it means a worker with no parent will have no referenced handles,
// thus no work to do, and will exit immediately, preventing process leaks.
process.on('message', common.mustCall());
const server = net.createServer((c) => {
process.once('message', (msg) => {
assert.strictEqual(msg, 'got');
c.end('hello');
});
socketConnected();
}).unref();
server.listen(0, common.localhostIPv4, () => {
const { port } = server.address();
socket = net.connect(port, common.localhostIPv4, socketConnected).unref();
});
}