From 45b15826205569ed5eeefbaea15ac538cdcfd3cc Mon Sep 17 00:00:00 2001 From: pfg Date: Tue, 1 Apr 2025 17:39:31 -0700 Subject: [PATCH] REMAINING --- ...rocess-exec-abortcontroller-promisified.js | 88 ++++++++ .../test-child-process-exec-maxbuf.js | 146 ++++++++++++++ .../test-child-process-execfile-maxbuf.js | 92 +++++++++ .../test-child-process-execfilesync-maxbuf.js | 53 +++++ .../test-child-process-execsync-maxbuf.js | 60 ++++++ ...ld-process-fork-closed-channel-segfault.js | 88 ++++++++ .../parallel/test-child-process-fork-dgram.js | 109 ++++++++++ .../test-child-process-fork-getconnections.js | 111 +++++++++++ .../test-child-process-fork-net-server.js | 159 +++++++++++++++ .../test-child-process-fork-net-socket.js | 96 +++++++++ .../parallel/test-child-process-fork-net.js | 188 ++++++++++++++++++ .../parallel/test-child-process-fork-stdio.js | 54 +++++ .../test-child-process-http-socket-leak.js | 57 ++++++ .../test-child-process-pipe-dataflow.js | 78 ++++++++ ...test-child-process-prototype-tampering.mjs | 91 +++++++++ .../test-child-process-recv-handle.js | 86 ++++++++ .../test-child-process-send-keep-open.js | 50 +++++ ...test-child-process-send-returns-boolean.js | 58 ++++++ .../test-child-process-server-close.js | 40 ++++ ...-child-process-spawn-windows-batch-file.js | 97 +++++++++ .../test-child-process-spawnsync-maxbuf.js | 58 ++++++ .../test/parallel/test-child-process-stdin.js | 62 ++++++ ...ld-process-stdio-merge-stdouts-into-cat.js | 30 +++ ...hild-process-stdio-reuse-readable-stdio.js | 33 +++ .../parallel/test-child-process-uid-gid.js | 20 ++ test/js/node/test/parallel/test-pipe-unref.js | 29 +++ .../test/parallel/test-stdio-pipe-redirect.js | 42 ++++ .../test/parallel/test-stdout-close-catch.js | 27 +++ .../sequential/test-child-process-emfile.js | 78 ++++++++ .../sequential/test-child-process-pass-fd.js | 84 ++++++++ 30 files changed, 2264 insertions(+) create mode 100644 test/js/node/test/parallel/test-child-process-exec-abortcontroller-promisified.js create mode 100644 test/js/node/test/parallel/test-child-process-exec-maxbuf.js create mode 100644 test/js/node/test/parallel/test-child-process-execfile-maxbuf.js create mode 100644 test/js/node/test/parallel/test-child-process-execfilesync-maxbuf.js create mode 100644 test/js/node/test/parallel/test-child-process-execsync-maxbuf.js create mode 100644 test/js/node/test/parallel/test-child-process-fork-closed-channel-segfault.js create mode 100644 test/js/node/test/parallel/test-child-process-fork-dgram.js create mode 100644 test/js/node/test/parallel/test-child-process-fork-getconnections.js create mode 100644 test/js/node/test/parallel/test-child-process-fork-net-server.js create mode 100644 test/js/node/test/parallel/test-child-process-fork-net-socket.js create mode 100644 test/js/node/test/parallel/test-child-process-fork-net.js create mode 100644 test/js/node/test/parallel/test-child-process-fork-stdio.js create mode 100644 test/js/node/test/parallel/test-child-process-http-socket-leak.js create mode 100644 test/js/node/test/parallel/test-child-process-pipe-dataflow.js create mode 100644 test/js/node/test/parallel/test-child-process-prototype-tampering.mjs create mode 100644 test/js/node/test/parallel/test-child-process-recv-handle.js create mode 100644 test/js/node/test/parallel/test-child-process-send-keep-open.js create mode 100644 test/js/node/test/parallel/test-child-process-send-returns-boolean.js create mode 100644 test/js/node/test/parallel/test-child-process-server-close.js create mode 100644 test/js/node/test/parallel/test-child-process-spawn-windows-batch-file.js create mode 100644 test/js/node/test/parallel/test-child-process-spawnsync-maxbuf.js create mode 100644 test/js/node/test/parallel/test-child-process-stdin.js create mode 100644 test/js/node/test/parallel/test-child-process-stdio-merge-stdouts-into-cat.js create mode 100644 test/js/node/test/parallel/test-child-process-stdio-reuse-readable-stdio.js create mode 100644 test/js/node/test/parallel/test-child-process-uid-gid.js create mode 100644 test/js/node/test/parallel/test-pipe-unref.js create mode 100644 test/js/node/test/parallel/test-stdio-pipe-redirect.js create mode 100644 test/js/node/test/parallel/test-stdout-close-catch.js create mode 100644 test/js/node/test/sequential/test-child-process-emfile.js create mode 100644 test/js/node/test/sequential/test-child-process-pass-fd.js diff --git a/test/js/node/test/parallel/test-child-process-exec-abortcontroller-promisified.js b/test/js/node/test/parallel/test-child-process-exec-abortcontroller-promisified.js new file mode 100644 index 0000000000..04e7b70671 --- /dev/null +++ b/test/js/node/test/parallel/test-child-process-exec-abortcontroller-promisified.js @@ -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()); +} diff --git a/test/js/node/test/parallel/test-child-process-exec-maxbuf.js b/test/js/node/test/parallel/test-child-process-exec-maxbuf.js new file mode 100644 index 0000000000..d13454d25b --- /dev/null +++ b/test/js/node/test/parallel/test-child-process-exec-maxbuf.js @@ -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); + }) + ); +} diff --git a/test/js/node/test/parallel/test-child-process-execfile-maxbuf.js b/test/js/node/test/parallel/test-child-process-execfile-maxbuf.js new file mode 100644 index 0000000000..22fb9264ea --- /dev/null +++ b/test/js/node/test/parallel/test-child-process-execfile-maxbuf.js @@ -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'); +} diff --git a/test/js/node/test/parallel/test-child-process-execfilesync-maxbuf.js b/test/js/node/test/parallel/test-child-process-execfilesync-maxbuf.js new file mode 100644 index 0000000000..63f8cc26eb --- /dev/null +++ b/test/js/node/test/parallel/test-child-process-execfilesync-maxbuf.js @@ -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; + }); +} diff --git a/test/js/node/test/parallel/test-child-process-execsync-maxbuf.js b/test/js/node/test/parallel/test-child-process-execsync-maxbuf.js new file mode 100644 index 0000000000..5700d02ab6 --- /dev/null +++ b/test/js/node/test/parallel/test-child-process-execsync-maxbuf.js @@ -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) + ); +} diff --git a/test/js/node/test/parallel/test-child-process-fork-closed-channel-segfault.js b/test/js/node/test/parallel/test-child-process-fork-closed-channel-segfault.js new file mode 100644 index 0000000000..47eb87c45f --- /dev/null +++ b/test/js/node/test/parallel/test-child-process-fork-closed-channel-segfault.js @@ -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; + } + }); + }); + }); + }); diff --git a/test/js/node/test/parallel/test-child-process-fork-dgram.js b/test/js/node/test/parallel/test-child-process-fork-dgram.js new file mode 100644 index 0000000000..4ea2edc60c --- /dev/null +++ b/test/js/node/test/parallel/test-child-process-fork-dgram.js @@ -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); + }); +} diff --git a/test/js/node/test/parallel/test-child-process-fork-getconnections.js b/test/js/node/test/parallel/test-child-process-fork-getconnections.js new file mode 100644 index 0000000000..62376c489f --- /dev/null +++ b/test/js/node/test/parallel/test-child-process-fork-getconnections.js @@ -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'); +} diff --git a/test/js/node/test/parallel/test-child-process-fork-net-server.js b/test/js/node/test/parallel/test-child-process-fork-net-server.js new file mode 100644 index 0000000000..3a3f01c6d6 --- /dev/null +++ b/test/js/node/test/parallel/test-child-process-fork-net-server.js @@ -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); +} diff --git a/test/js/node/test/parallel/test-child-process-fork-net-socket.js b/test/js/node/test/parallel/test-child-process-fork-net-socket.js new file mode 100644 index 0000000000..28da94f4ef --- /dev/null +++ b/test/js/node/test/parallel/test-child-process-fork-net-socket.js @@ -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); +} diff --git a/test/js/node/test/parallel/test-child-process-fork-net.js b/test/js/node/test/parallel/test-child-process-fork-net.js new file mode 100644 index 0000000000..bf19a2bdd1 --- /dev/null +++ b/test/js/node/test/parallel/test-child-process-fork-net.js @@ -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); + }); +} diff --git a/test/js/node/test/parallel/test-child-process-fork-stdio.js b/test/js/node/test/parallel/test-child-process-fork-stdio.js new file mode 100644 index 0000000000..e76ef27797 --- /dev/null +++ b/test/js/node/test/parallel/test-child-process-fork-stdio.js @@ -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); +} diff --git a/test/js/node/test/parallel/test-child-process-http-socket-leak.js b/test/js/node/test/parallel/test-child-process-http-socket-leak.js new file mode 100644 index 0000000000..cae4b051bc --- /dev/null +++ b/test/js/node/test/parallel/test-child-process-http-socket-leak.js @@ -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(); + }); +})); diff --git a/test/js/node/test/parallel/test-child-process-pipe-dataflow.js b/test/js/node/test/parallel/test-child-process-pipe-dataflow.js new file mode 100644 index 0000000000..e61bcde5b0 --- /dev/null +++ b/test/js/node/test/parallel/test-child-process-pipe-dataflow.js @@ -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 | 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()); + }); +} diff --git a/test/js/node/test/parallel/test-child-process-prototype-tampering.mjs b/test/js/node/test/parallel/test-child-process-prototype-tampering.mjs new file mode 100644 index 0000000000..d94c4bdbc6 --- /dev/null +++ b/test/js/node/test/parallel/test-child-process-prototype-tampering.mjs @@ -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; +} diff --git a/test/js/node/test/parallel/test-child-process-recv-handle.js b/test/js/node/test/parallel/test-child-process-recv-handle.js new file mode 100644 index 0000000000..b67bc206ac --- /dev/null +++ b/test/js/node/test/parallel/test-child-process-recv-handle.js @@ -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)); +} diff --git a/test/js/node/test/parallel/test-child-process-send-keep-open.js b/test/js/node/test/parallel/test-child-process-send-keep-open.js new file mode 100644 index 0000000000..54169dc188 --- /dev/null +++ b/test/js/node/test/parallel/test-child-process-send-keep-open.js @@ -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')); + })); +} diff --git a/test/js/node/test/parallel/test-child-process-send-returns-boolean.js b/test/js/node/test/parallel/test-child-process-send-returns-boolean.js new file mode 100644 index 0000000000..8c3ef46438 --- /dev/null +++ b/test/js/node/test/parallel/test-child-process-send-returns-boolean.js @@ -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); + }); +} diff --git a/test/js/node/test/parallel/test-child-process-server-close.js b/test/js/node/test/parallel/test-child-process-server-close.js new file mode 100644 index 0000000000..832cf97017 --- /dev/null +++ b/test/js/node/test/parallel/test-child-process-server-close.js @@ -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(); + }); + }); +}); diff --git a/test/js/node/test/parallel/test-child-process-spawn-windows-batch-file.js b/test/js/node/test/parallel/test-child-process-spawn-windows-batch-file.js new file mode 100644 index 0000000000..4d30574360 --- /dev/null +++ b/test/js/node/test/parallel/test-child-process-spawn-windows-batch-file.js @@ -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); + } +} diff --git a/test/js/node/test/parallel/test-child-process-spawnsync-maxbuf.js b/test/js/node/test/parallel/test-child-process-spawnsync-maxbuf.js new file mode 100644 index 0000000000..3f452a41e6 --- /dev/null +++ b/test/js/node/test/parallel/test-child-process-spawnsync-maxbuf.js @@ -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) + ); +} diff --git a/test/js/node/test/parallel/test-child-process-stdin.js b/test/js/node/test/parallel/test-child-process-stdin.js new file mode 100644 index 0000000000..24a79d6238 --- /dev/null +++ b/test/js/node/test/parallel/test-child-process-stdin.js @@ -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'); +})); diff --git a/test/js/node/test/parallel/test-child-process-stdio-merge-stdouts-into-cat.js b/test/js/node/test/parallel/test-child-process-stdio-merge-stdouts-into-cat.js new file mode 100644 index 0000000000..64373e9e16 --- /dev/null +++ b/test/js/node/test/parallel/test-child-process-stdio-merge-stdouts-into-cat.js @@ -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'); + })); + })); +})); diff --git a/test/js/node/test/parallel/test-child-process-stdio-reuse-readable-stdio.js b/test/js/node/test/parallel/test-child-process-stdio-reuse-readable-stdio.js new file mode 100644 index 0000000000..19f746e259 --- /dev/null +++ b/test/js/node/test/parallel/test-child-process-stdio-reuse-readable-stdio.js @@ -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, it’s 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 cat’s 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(); +})); diff --git a/test/js/node/test/parallel/test-child-process-uid-gid.js b/test/js/node/test/parallel/test-child-process-uid-gid.js new file mode 100644 index 0000000000..748214294c --- /dev/null +++ b/test/js/node/test/parallel/test-child-process-uid-gid.js @@ -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); +} diff --git a/test/js/node/test/parallel/test-pipe-unref.js b/test/js/node/test/parallel/test-pipe-unref.js new file mode 100644 index 0000000000..78419a1d77 --- /dev/null +++ b/test/js/node/test/parallel/test-pipe-unref.js @@ -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(); diff --git a/test/js/node/test/parallel/test-stdio-pipe-redirect.js b/test/js/node/test/parallel/test-stdio-pipe-redirect.js new file mode 100644 index 0000000000..8b48133c8b --- /dev/null +++ b/test/js/node/test/parallel/test-stdio-pipe-redirect.js @@ -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; +} diff --git a/test/js/node/test/parallel/test-stdout-close-catch.js b/test/js/node/test/parallel/test-stdout-close-catch.js new file mode 100644 index 0000000000..8a06acfda0 --- /dev/null +++ b/test/js/node/test/parallel/test-stdout-close-catch.js @@ -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'); +})); diff --git a/test/js/node/test/sequential/test-child-process-emfile.js b/test/js/node/test/sequential/test-child-process-emfile.js new file mode 100644 index 0000000000..8ee6dd52e3 --- /dev/null +++ b/test/js/node/test/sequential/test-child-process-emfile.js @@ -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()); +} diff --git a/test/js/node/test/sequential/test-child-process-pass-fd.js b/test/js/node/test/sequential/test-child-process-pass-fd.js new file mode 100644 index 0000000000..86092f56da --- /dev/null +++ b/test/js/node/test/sequential/test-child-process-pass-fd.js @@ -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(); + }); +}