Skip to content

Commit 0e3c527

Browse files
committed
child_process: measure buffer length in bytes
This change fixes a known issue where `maxBuffer` limits by characters rather than bytes. Benchmark added to confirm no performance regression occurs with this change. This necessarily changes default behavior of `stdout` and `stderr` callbacks such that they receive buffers rather than strings. The alternative would be a performance hit on the defaults. Refs: nodejs#6764 Refs: nodejs#1901
1 parent 0bff9b3 commit 0e3c527

8 files changed

+84
-54
lines changed

benchmark/child_process/child-process-exec-stdout.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
'use strict';
22
const common = require('../common.js');
33

4-
var messagesLength = [64, 256, 1024, 4096];
4+
const messagesLength = [64, 256, 1024, 4096];
55
// Windows does not support that long arguments
66
if (process.platform !== 'win32')
77
messagesLength.push(32768);

benchmark/child_process/child-process-read.js

+2-2
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
const common = require('../common.js');
33
const os = require('os');
44

5-
var messagesLength = [64, 256, 1024, 4096];
5+
const messagesLength = [64, 256, 1024, 4096];
66
// Windows does not support that long arguments
77
if (os.platform() !== 'win32')
88
messagesLength.push(32768);
@@ -20,7 +20,7 @@ function main(conf) {
2020
const len = +conf.len;
2121

2222
const msg = '"' + Array(len).join('.') + '"';
23-
const options = { 'stdio': ['ignore', 'pipe', 'ignore'] };
23+
const options = {'stdio': ['ignore', 'ipc', 'ignore']};
2424
const child = spawn('yes', [msg], options);
2525

2626
var bytes = 0;

doc/api/child_process.md

+4-4
Original file line numberDiff line numberDiff line change
@@ -161,11 +161,11 @@ signal that terminated the process. Any exit code other than `0` is considered
161161
to be an error.
162162

163163
The `stdout` and `stderr` arguments passed to the callback will contain the
164-
stdout and stderr output of the child process. By default, Node.js will decode
165-
the output as UTF-8 and pass strings to the callback. The `encoding` option
164+
stdout and stderr output of the child process. The `encoding` option
166165
can be used to specify the character encoding used to decode the stdout and
167-
stderr output. If `encoding` is `'buffer'`, `Buffer` objects will be passed to
168-
the callback instead.
166+
stderr output. If `encoding` is `'buffer'` (the default), `Buffer` objects will
167+
be passed to the callback. If a valid string encoding is specified instead,
168+
strings will be passed to the callback.
169169

170170
The `options` argument may be passed as the second argument to customize how
171171
the process is spawned. The default options are:

lib/child_process.js

+41-30
Original file line numberDiff line numberDiff line change
@@ -146,15 +146,13 @@ exports.execFile = function(file /*, args, options, callback*/) {
146146
});
147147

148148
var encoding;
149-
var _stdout;
150-
var _stderr;
149+
var stdoutState;
150+
var stderrState;
151+
var _stdout = [];
152+
var _stderr = [];
151153
if (options.encoding !== 'buffer' && Buffer.isEncoding(options.encoding)) {
152154
encoding = options.encoding;
153-
_stdout = '';
154-
_stderr = '';
155155
} else {
156-
_stdout = [];
157-
_stderr = [];
158156
encoding = null;
159157
}
160158
var stdoutLen = 0;
@@ -176,16 +174,23 @@ exports.execFile = function(file /*, args, options, callback*/) {
176174

177175
if (!callback) return;
178176

179-
// merge chunks
180-
var stdout;
181-
var stderr;
182-
if (!encoding) {
183-
stdout = Buffer.concat(_stdout);
184-
stderr = Buffer.concat(_stderr);
185-
} else {
186-
stdout = _stdout;
187-
stderr = _stderr;
188-
}
177+
var stdout = Buffer.concat(_stdout, stdoutLen);
178+
var stderr = Buffer.concat(_stderr, stderrLen);
179+
180+
var stdoutEncoding = encoding;
181+
var stderrEncoding = encoding;
182+
183+
if (stdoutState && stdoutState.decoder)
184+
stdoutEncoding = stdoutState.decoder.encoding;
185+
186+
if (stderrState && stderrState.decoder)
187+
stderrEncoding = stderrState.decoder.encoding;
188+
189+
if (stdoutEncoding)
190+
stdout = stdout.toString(stdoutEncoding);
191+
192+
if (stderrEncoding)
193+
stderr = stderr.toString(stderrEncoding);
189194

190195
if (ex) {
191196
// Will be handled later
@@ -245,39 +250,45 @@ exports.execFile = function(file /*, args, options, callback*/) {
245250
}
246251

247252
if (child.stdout) {
248-
if (encoding)
249-
child.stdout.setEncoding(encoding);
253+
stdoutState = child.stdout._readableState;
250254

251255
child.stdout.addListener('data', function(chunk) {
252-
stdoutLen += chunk.length;
256+
// If `child.stdout.setEncoding()` happened in userland, convert string to
257+
// Buffer.
258+
if (stdoutState.decoder) {
259+
chunk = Buffer.from(chunk, stdoutState.decoder.encoding);
260+
}
261+
262+
stdoutLen += chunk.byteLength;
253263

254264
if (stdoutLen > options.maxBuffer) {
255265
ex = new Error('stdout maxBuffer exceeded');
266+
stdoutLen -= chunk.byteLength;
256267
kill();
257268
} else {
258-
if (!encoding)
259-
_stdout.push(chunk);
260-
else
261-
_stdout += chunk;
269+
_stdout.push(chunk);
262270
}
263271
});
264272
}
265273

266274
if (child.stderr) {
267-
if (encoding)
268-
child.stderr.setEncoding(encoding);
275+
stderrState = child.stderr._readableState;
269276

270277
child.stderr.addListener('data', function(chunk) {
271-
stderrLen += chunk.length;
278+
// If `child.stderr.setEncoding()` happened in userland, convert string to
279+
// Buffer.
280+
if (stderrState.decoder) {
281+
chunk = Buffer.from(chunk, stderrState.decoder.encoding);
282+
}
283+
284+
stderrLen += chunk.byteLength;
272285

273286
if (stderrLen > options.maxBuffer) {
274287
ex = new Error('stderr maxBuffer exceeded');
288+
stderrLen -= chunk.byteLength;
275289
kill();
276290
} else {
277-
if (!encoding)
278-
_stderr.push(chunk);
279-
else
280-
_stderr += chunk;
291+
_stderr.push(chunk);
281292
}
282293
});
283294
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
'use strict';
2+
const common = require('../common');
3+
const assert = require('assert');
4+
const exec = require('child_process').exec;
5+
6+
const keepAlive = setInterval(() => {}, 9999);
7+
var cbCalls = 0;
8+
const expectedCalls = 2;
9+
10+
const cb = common.mustCall((data) => {
11+
assert(data instanceof Buffer);
12+
if (++cbCalls === expectedCalls) {
13+
clearInterval(keepAlive);
14+
}
15+
}, expectedCalls);
16+
17+
const command = common.isWindows ? 'dir' : 'ls';
18+
exec(command).stdout.on('data', cb);
19+
20+
exec('fhqwhgads').stderr.on('data', cb);

test/parallel/test-child-process-exec-stdout-data-string.js

-15
This file was deleted.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
'use strict';
2+
const common = require('../common');
3+
const assert = require('assert');
4+
const cp = require('child_process');
5+
const unicode = '中文测试'; // Length = 4, Byte length = 13
6+
7+
if (process.argv[2] === 'child') {
8+
console.error(unicode);
9+
} else {
10+
const cmd = `${process.execPath} ${__filename} child`;
11+
12+
cp.exec(cmd, {maxBuffer: 10}, common.mustCall((err, stdout, stderr) => {
13+
assert.strictEqual(err.message, 'stderr maxBuffer exceeded');
14+
}));
15+
}

test/known_issues/test-child-process-max-buffer.js test/parallel/test-child-process-maxBuffer-stdout.js

+1-2
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
'use strict';
2-
// Refs: https://github.com/nodejs/node/issues/1901
32
const common = require('../common');
43
const assert = require('assert');
54
const cp = require('child_process');
@@ -10,7 +9,7 @@ if (process.argv[2] === 'child') {
109
} else {
1110
const cmd = `${process.execPath} ${__filename} child`;
1211

13-
cp.exec(cmd, { maxBuffer: 10 }, common.mustCall((err, stdout, stderr) => {
12+
cp.exec(cmd, {maxBuffer: 10}, common.mustCall((err, stdout, stderr) => {
1413
assert.strictEqual(err.message, 'stdout maxBuffer exceeded');
1514
}));
1615
}

0 commit comments

Comments
 (0)