child_process: add tracing channel for spawn

Signed-off-by: marcopiraccini <marco.piraccini@gmail.com>
PR-URL: https://github.com/nodejs/node/pull/61836
Reviewed-By: Colin Ihrig <cjihrig@gmail.com>
Reviewed-By: Paolo Insogna <paolo@cowtech.it>
Reviewed-By: Matteo Collina <matteo.collina@gmail.com>
This commit is contained in:
Marco authored and GitHub committed 2026-02-25 06:00:03 +00:00
1 parent 2ebe49600e
commit 764e9a7560
3 files changed
+141

No files matched your search

+23
View File
@@ -1417,6 +1417,28 @@ added: v16.18.0
Emitted when a new process is created.
`tracing:child_process.spawn:start`
* `process` {ChildProcess}
* `options` {Object}
Emitted when [`child_process.spawn()`][] is invoked, before the process is
actually spawned.
`tracing:child_process.spawn:end`
* `process` {ChildProcess}
Emitted when [`child_process.spawn()`][] has completed successfully and the
process has been created.
`tracing:child_process.spawn:error`
* `process` {ChildProcess}
* `error` {Error}
Emitted when [`child_process.spawn()`][] encounters an error.
##### Event: `'execve'`
* `execPath` {string}
@@ -1448,6 +1470,7 @@ Emitted when a new thread is created.
[`channel.runStores(context, ...)`]: #channelrunstorescontext-fn-thisarg-args
[`channel.subscribe(onMessage)`]: #channelsubscribeonmessage
[`channel.unsubscribe(onMessage)`]: #channelunsubscribeonmessage
[`child_process.spawn()`]: child_process.md#child_processspawncommand-args-options
[`diagnostics_channel.channel(name)`]: #diagnostics_channelchannelname
[`diagnostics_channel.subscribe(name, onMessage)`]: #diagnostics_channelsubscribename-onmessage
[`diagnostics_channel.tracingChannel()`]: #diagnostics_channeltracingchannelnameorchannels
+24
View File
@@ -61,6 +61,7 @@ const spawn_sync = internalBinding('spawn_sync');
const { kStateSymbol } = require('internal/dgram');
const dc = require('diagnostics_channel');
const childProcessChannel = dc.channel('child_process');
const childProcessSpawn = dc.tracingChannel('child_process.spawn');
const {
UV_EACCES,
@@ -392,6 +393,10 @@ ChildProcess.prototype.spawn = function spawn(options) {
this.spawnargs = options.args;
}
if (childProcessSpawn.hasSubscribers) {
childProcessSpawn.start.publish({ process: this, options });
}
const err = this._handle.spawn(options);
// Run-time errors should emit an error, not throw an exception.
@@ -400,6 +405,13 @@ ChildProcess.prototype.spawn = function spawn(options) {
err === UV_EMFILE ||
err === UV_ENFILE ||
err === UV_ENOENT) {
if (childProcessSpawn.hasSubscribers) {
childProcessSpawn.error.publish({
process: this,
error: new ErrnoException(err, 'spawn'),
});
}
process.nextTick(onErrorNT, this, err);
// There is no point in continuing when we've hit EMFILE or ENFILE
@@ -417,8 +429,20 @@ ChildProcess.prototype.spawn = function spawn(options) {
this._handle.close();
this._handle = null;
if (childProcessSpawn.hasSubscribers) {
childProcessSpawn.error.publish({
process: this,
error: new ErrnoException(err, 'spawn'),
});
}
throw new ErrnoException(err, 'spawn');
} else {
if (childProcessSpawn.hasSubscribers) {
childProcessSpawn.end.publish({ process: this });
}
process.nextTick(onSpawnNT, this);
}
@@ -0,0 +1,94 @@
'use strict';
const common = require('../common');
const assert = require('assert');
const { spawn, ChildProcess } = require('child_process');
const dc = require('diagnostics_channel');
const path = require('path');
const fs = require('fs');
const tmpdir = require('../common/tmpdir');
const isChildProcess = (process) => process instanceof ChildProcess;
function testDiagnosticChannel(subscribers, test, after) {
dc.tracingChannel('child_process.spawn').subscribe(subscribers);
test(common.mustCall(() => {
dc.tracingChannel('child_process.spawn').unsubscribe(subscribers);
after?.();
}));
}
const testSuccessfulSpawn = common.mustCall(() => {
let cb;
testDiagnosticChannel(
{
start: common.mustCall(({ process: childProcess, options }) => {
assert.strictEqual(isChildProcess(childProcess), true);
assert.strictEqual(options.file, process.execPath);
}),
end: common.mustCall(({ process: childProcess }) => {
assert.strictEqual(isChildProcess(childProcess), true);
}),
error: common.mustNotCall(),
},
common.mustCall((callback) => {
cb = callback;
const child = spawn(process.execPath, ['-e', 'process.exit(0)']);
child.on('close', () => {
cb();
});
}),
testFailingSpawnENOENT
);
});
const testFailingSpawnENOENT = common.mustCall(() => {
testDiagnosticChannel(
{
start: common.mustCall(({ process: childProcess, options }) => {
assert.strictEqual(isChildProcess(childProcess), true);
assert.strictEqual(options.file, 'does-not-exist');
}),
end: common.mustNotCall(),
error: common.mustCall(({ process: childProcess, error }) => {
assert.strictEqual(isChildProcess(childProcess), true);
assert.strictEqual(error.code, 'ENOENT');
}),
},
common.mustCall((callback) => {
const child = spawn('does-not-exist');
child.on('error', () => {});
callback();
}),
common.isWindows ? undefined : testFailingSpawnEACCES,
);
});
const testFailingSpawnEACCES = !common.isWindows ? common.mustCall(() => {
tmpdir.refresh();
const noExecFile = path.join(tmpdir.path, 'no-exec');
fs.writeFileSync(noExecFile, '');
fs.chmodSync(noExecFile, 0o644);
testDiagnosticChannel(
{
start: common.mustCall(({ process: childProcess, options }) => {
assert.strictEqual(isChildProcess(childProcess), true);
assert.strictEqual(options.file, noExecFile);
}),
end: common.mustNotCall(),
error: common.mustCall(({ process: childProcess, error }) => {
assert.strictEqual(isChildProcess(childProcess), true);
assert.strictEqual(error.code, 'EACCES');
}),
},
common.mustCall((callback) => {
const child = spawn(noExecFile);
child.on('error', () => {});
callback();
}),
);
}) : undefined;
testSuccessfulSpawn();