Skip to content

Commit 10b2bb5

Browse files
marcopiracciniaduh95
authored andcommitted
child_process: add tracing channel for spawn
Signed-off-by: marcopiraccini <marco.piraccini@gmail.com> PR-URL: #61836 Reviewed-By: Colin Ihrig <cjihrig@gmail.com> Reviewed-By: Paolo Insogna <paolo@cowtech.it> Reviewed-By: Matteo Collina <matteo.collina@gmail.com>
1 parent 1f2025f commit 10b2bb5

File tree

3 files changed

+141
-0
lines changed

3 files changed

+141
-0
lines changed

doc/api/diagnostics_channel.md

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1413,6 +1413,28 @@ added: v16.18.0
14131413

14141414
Emitted when a new process is created.
14151415

1416+
`tracing:child_process.spawn:start`
1417+
1418+
* `process` {ChildProcess}
1419+
* `options` {Object}
1420+
1421+
Emitted when [`child_process.spawn()`][] is invoked, before the process is
1422+
actually spawned.
1423+
1424+
`tracing:child_process.spawn:end`
1425+
1426+
* `process` {ChildProcess}
1427+
1428+
Emitted when [`child_process.spawn()`][] has completed successfully and the
1429+
process has been created.
1430+
1431+
`tracing:child_process.spawn:error`
1432+
1433+
* `process` {ChildProcess}
1434+
* `error` {Error}
1435+
1436+
Emitted when [`child_process.spawn()`][] encounters an error.
1437+
14161438
##### Event: `'execve'`
14171439

14181440
* `execPath` {string}
@@ -1444,6 +1466,7 @@ Emitted when a new thread is created.
14441466
[`channel.runStores(context, ...)`]: #channelrunstorescontext-fn-thisarg-args
14451467
[`channel.subscribe(onMessage)`]: #channelsubscribeonmessage
14461468
[`channel.unsubscribe(onMessage)`]: #channelunsubscribeonmessage
1469+
[`child_process.spawn()`]: child_process.md#child_processspawncommand-args-options
14471470
[`diagnostics_channel.channel(name)`]: #diagnostics_channelchannelname
14481471
[`diagnostics_channel.subscribe(name, onMessage)`]: #diagnostics_channelsubscribename-onmessage
14491472
[`diagnostics_channel.tracingChannel()`]: #diagnostics_channeltracingchannelnameorchannels

lib/internal/child_process.js

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@ const spawn_sync = internalBinding('spawn_sync');
6262
const { kStateSymbol } = require('internal/dgram');
6363
const dc = require('diagnostics_channel');
6464
const childProcessChannel = dc.channel('child_process');
65+
const childProcessSpawn = dc.tracingChannel('child_process.spawn');
6566

6667
const {
6768
UV_EACCES,
@@ -393,6 +394,10 @@ ChildProcess.prototype.spawn = function spawn(options) {
393394
this.spawnargs = options.args;
394395
}
395396

397+
if (childProcessSpawn.hasSubscribers) {
398+
childProcessSpawn.start.publish({ process: this, options });
399+
}
400+
396401
const err = this._handle.spawn(options);
397402

398403
// Run-time errors should emit an error, not throw an exception.
@@ -401,6 +406,13 @@ ChildProcess.prototype.spawn = function spawn(options) {
401406
err === UV_EMFILE ||
402407
err === UV_ENFILE ||
403408
err === UV_ENOENT) {
409+
if (childProcessSpawn.hasSubscribers) {
410+
childProcessSpawn.error.publish({
411+
process: this,
412+
error: new ErrnoException(err, 'spawn'),
413+
});
414+
}
415+
404416
process.nextTick(onErrorNT, this, err);
405417

406418
// There is no point in continuing when we've hit EMFILE or ENFILE
@@ -418,8 +430,20 @@ ChildProcess.prototype.spawn = function spawn(options) {
418430

419431
this._handle.close();
420432
this._handle = null;
433+
434+
if (childProcessSpawn.hasSubscribers) {
435+
childProcessSpawn.error.publish({
436+
process: this,
437+
error: new ErrnoException(err, 'spawn'),
438+
});
439+
}
440+
421441
throw new ErrnoException(err, 'spawn');
422442
} else {
443+
if (childProcessSpawn.hasSubscribers) {
444+
childProcessSpawn.end.publish({ process: this });
445+
}
446+
423447
process.nextTick(onSpawnNT, this);
424448
}
425449

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
'use strict';
2+
const common = require('../common');
3+
const assert = require('assert');
4+
const { spawn, ChildProcess } = require('child_process');
5+
const dc = require('diagnostics_channel');
6+
const path = require('path');
7+
const fs = require('fs');
8+
const tmpdir = require('../common/tmpdir');
9+
10+
const isChildProcess = (process) => process instanceof ChildProcess;
11+
12+
function testDiagnosticChannel(subscribers, test, after) {
13+
dc.tracingChannel('child_process.spawn').subscribe(subscribers);
14+
15+
test(common.mustCall(() => {
16+
dc.tracingChannel('child_process.spawn').unsubscribe(subscribers);
17+
after?.();
18+
}));
19+
}
20+
21+
const testSuccessfulSpawn = common.mustCall(() => {
22+
let cb;
23+
24+
testDiagnosticChannel(
25+
{
26+
start: common.mustCall(({ process: childProcess, options }) => {
27+
assert.strictEqual(isChildProcess(childProcess), true);
28+
assert.strictEqual(options.file, process.execPath);
29+
}),
30+
end: common.mustCall(({ process: childProcess }) => {
31+
assert.strictEqual(isChildProcess(childProcess), true);
32+
}),
33+
error: common.mustNotCall(),
34+
},
35+
common.mustCall((callback) => {
36+
cb = callback;
37+
const child = spawn(process.execPath, ['-e', 'process.exit(0)']);
38+
child.on('close', () => {
39+
cb();
40+
});
41+
}),
42+
testFailingSpawnENOENT
43+
);
44+
});
45+
46+
const testFailingSpawnENOENT = common.mustCall(() => {
47+
testDiagnosticChannel(
48+
{
49+
start: common.mustCall(({ process: childProcess, options }) => {
50+
assert.strictEqual(isChildProcess(childProcess), true);
51+
assert.strictEqual(options.file, 'does-not-exist');
52+
}),
53+
end: common.mustNotCall(),
54+
error: common.mustCall(({ process: childProcess, error }) => {
55+
assert.strictEqual(isChildProcess(childProcess), true);
56+
assert.strictEqual(error.code, 'ENOENT');
57+
}),
58+
},
59+
common.mustCall((callback) => {
60+
const child = spawn('does-not-exist');
61+
child.on('error', () => {});
62+
callback();
63+
}),
64+
common.isWindows ? undefined : testFailingSpawnEACCES,
65+
);
66+
});
67+
68+
const testFailingSpawnEACCES = !common.isWindows ? common.mustCall(() => {
69+
tmpdir.refresh();
70+
const noExecFile = path.join(tmpdir.path, 'no-exec');
71+
fs.writeFileSync(noExecFile, '');
72+
fs.chmodSync(noExecFile, 0o644);
73+
74+
testDiagnosticChannel(
75+
{
76+
start: common.mustCall(({ process: childProcess, options }) => {
77+
assert.strictEqual(isChildProcess(childProcess), true);
78+
assert.strictEqual(options.file, noExecFile);
79+
}),
80+
end: common.mustNotCall(),
81+
error: common.mustCall(({ process: childProcess, error }) => {
82+
assert.strictEqual(isChildProcess(childProcess), true);
83+
assert.strictEqual(error.code, 'EACCES');
84+
}),
85+
},
86+
common.mustCall((callback) => {
87+
const child = spawn(noExecFile);
88+
child.on('error', () => {});
89+
callback();
90+
}),
91+
);
92+
}) : undefined;
93+
94+
testSuccessfulSpawn();

0 commit comments

Comments
 (0)