diff --git a/doc/api/diagnostics_channel.md b/doc/api/diagnostics_channel.md index e9ac279cc62917..4587ee649b5173 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} @@ -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 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();