From 119b08656f05592245e46c57708cc9e72df29297 Mon Sep 17 00:00:00 2001 From: marcopiraccini Date: Sun, 15 Feb 2026 11:42:21 +0100 Subject: [PATCH 1/2] child_process: add tracing channel for spawn Signed-off-by: marcopiraccini --- doc/api/diagnostics_channel.md | 23 +++++ lib/internal/child_process.js | 24 +++++ .../test-diagnostics-channel-child-process.js | 94 +++++++++++++++++++ 3 files changed, 141 insertions(+) create mode 100644 test/parallel/test-diagnostics-channel-child-process.js diff --git a/doc/api/diagnostics_channel.md b/doc/api/diagnostics_channel.md index e9ac279cc62917..71ab32b125a748 100644 --- a/doc/api/diagnostics_channel.md +++ b/doc/api/diagnostics_channel.md @@ -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} @@ -1453,6 +1475,7 @@ Emitted when a new thread is created. [`diagnostics_channel.tracingChannel()`]: #diagnostics_channeltracingchannelnameorchannels [`end` event]: #endevent [`error` event]: #errorevent +[`child_process.spawn()`]: child_process.md#child_processspawncommand-args-options [`net.Server.listen()`]: net.md#serverlisten [`process.execve()`]: process.md#processexecvefile-args-env [`start` event]: #startevent diff --git a/lib/internal/child_process.js b/lib/internal/child_process.js index 45ae95614a88b5..9eac06d1fdf145 100644 --- a/lib/internal/child_process.js +++ b/lib/internal/child_process.js @@ -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); } diff --git a/test/parallel/test-diagnostics-channel-child-process.js b/test/parallel/test-diagnostics-channel-child-process.js new file mode 100644 index 00000000000000..1d16fd4298779e --- /dev/null +++ b/test/parallel/test-diagnostics-channel-child-process.js @@ -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(); From 082952e4ea6eaafaca017f6dc5ae39c993660556 Mon Sep 17 00:00:00 2001 From: marcopiraccini Date: Sun, 15 Feb 2026 16:34:57 +0100 Subject: [PATCH 2/2] child_process: add tracing channel for spawn - linting fixup Signed-off-by: marcopiraccini --- doc/api/diagnostics_channel.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/api/diagnostics_channel.md b/doc/api/diagnostics_channel.md index 71ab32b125a748..4587ee649b5173 100644 --- a/doc/api/diagnostics_channel.md +++ b/doc/api/diagnostics_channel.md @@ -1470,12 +1470,12 @@ 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 [`end` event]: #endevent [`error` event]: #errorevent -[`child_process.spawn()`]: child_process.md#child_processspawncommand-args-options [`net.Server.listen()`]: net.md#serverlisten [`process.execve()`]: process.md#processexecvefile-args-env [`start` event]: #startevent