Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
103 changes: 103 additions & 0 deletions doc/api/child_process.md
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,109 @@ For certain use cases, such as automating shell scripts, the
the synchronous methods can have significant impact on performance due to
stalling the event loop while spawned processes complete.

The `node:child_process/promises` API provides promise-based versions of
`exec()` and `execFile()`:

```mjs
import { execFile } from 'node:child_process/promises';
const { stdout } = await execFile('node', ['--version']);
console.log(stdout);
```

```cjs
const { execFile } = require('node:child_process/promises');
(async () => {
const { stdout } = await execFile('node', ['--version']);
console.log(stdout);
})();
```

## Promises API

<!-- YAML
added: REPLACEME
-->

The `node:child_process/promises` API provides promise-returning versions of
`child_process.exec()` and `child_process.execFile()`. The API is accessible
via `require('node:child_process/promises')` or
`import from 'node:child_process/promises'`.

### `exec(command[, options])`

<!-- YAML
added: REPLACEME
-->

* `command` {string} The command to run, with space-separated arguments.
* `options` {Object}
* `cwd` {string|URL} Current working directory of the child process.
**Default:** `process.cwd()`.
* `env` {Object} Environment key-value pairs. **Default:** `process.env`.
* `encoding` {string} **Default:** `'utf8'`
* `shell` {string} Shell to run the command with.
**Default:** `'/bin/sh'` on Unix, `process.env.ComSpec` on Windows.
* `signal` {AbortSignal} Allows aborting the child process using an
AbortSignal.
* `timeout` {number} **Default:** `0`
* `maxBuffer` {number} Largest amount of data in bytes allowed on stdout or
stderr. If exceeded, the child process is terminated and any output is
truncated. **Default:** `1024 * 1024`.
* `killSignal` {string|integer} **Default:** `'SIGTERM'`
* `uid` {number} Sets the user identity of the process (see setuid(2)).
* `gid` {number} Sets the group identity of the process (see setgid(2)).
* `windowsHide` {boolean} Hide the subprocess console window that would
normally be created on Windows systems. **Default:** `false`.
* Returns: {Promise} Fulfills with an {Object} containing:
* `stdout` {string|Buffer}
* `stderr` {string|Buffer}

The returned promise has a `child` property with a reference to the
[`ChildProcess`][] instance.

If the child process exits with a non-zero code or encounters an error, the
promise is rejected with an error containing `stdout`, `stderr`, `code`,
`signal`, `cmd`, and `killed` properties.

### `execFile(file[, args][, options])`

<!-- YAML
added: REPLACEME
-->

* `file` {string} The name or path of the executable file to run.
* `args` {string\[]} List of string arguments.
* `options` {Object}
* `cwd` {string|URL} Current working directory of the child process.
* `env` {Object} Environment key-value pairs. **Default:** `process.env`.
* `encoding` {string} **Default:** `'utf8'`
* `timeout` {number} **Default:** `0`
* `maxBuffer` {number} Largest amount of data in bytes allowed on stdout or
stderr. If exceeded, the child process is terminated and any output is
truncated. **Default:** `1024 * 1024`.
* `killSignal` {string|integer} **Default:** `'SIGTERM'`
* `uid` {number} Sets the user identity of the process (see setuid(2)).
* `gid` {number} Sets the group identity of the process (see setgid(2)).
* `windowsHide` {boolean} Hide the subprocess console window that would
normally be created on Windows systems. **Default:** `false`.
* `windowsVerbatimArguments` {boolean} No quoting or escaping of arguments is
done on Windows. Ignored on Unix. **Default:** `false`.
* `shell` {boolean|string} If `true`, runs `file` inside of a shell. Uses
`'/bin/sh'` on Unix, and `process.env.ComSpec` on Windows. A different
shell can be specified as a string. **Default:** `false` (no shell).
* `signal` {AbortSignal} Allows aborting the child process using an
AbortSignal.
* Returns: {Promise} Fulfills with an {Object} containing:
* `stdout` {string|Buffer}
* `stderr` {string|Buffer}

The returned promise has a `child` property with a reference to the
[`ChildProcess`][] instance.

If the child process exits with a non-zero code or encounters an error, the
promise is rejected with an error containing `stdout`, `stderr`, `code`,
`signal`, `cmd`, and `killed` properties.

## Asynchronous process creation

The [`child_process.spawn()`][], [`child_process.fork()`][], [`child_process.exec()`][],
Expand Down
9 changes: 9 additions & 0 deletions lib/child_process/promises.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
'use strict';

const { promisify } = require('internal/util');
const { exec, execFile } = require('child_process');

module.exports = {
exec: promisify(exec),
execFile: promisify(execFile),
};
169 changes: 169 additions & 0 deletions test/parallel/test-child-process-promises.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,169 @@
// Flags: --no-warnings
'use strict';
const common = require('../common');
const assert = require('assert');
const child_process = require('child_process');

const { exec, execFile } = require('child_process/promises');

// Delegated functions are the same as util.promisify(child_process.exec/execFile).
{
const { promisify } = require('util');
assert.strictEqual(exec, promisify(child_process.exec));
assert.strictEqual(execFile, promisify(child_process.execFile));
}

// exec resolves with { stdout, stderr }.
{
const promise = exec(...common.escapePOSIXShell`"${process.execPath}" -p 42`);

promise.then(common.mustCall((result) => {
assert.deepStrictEqual(result, { stdout: '42\n', stderr: '' });
}));
}

// execFile resolves with { stdout, stderr }.
{
const promise = execFile(process.execPath, ['-p', '42']);

promise.then(common.mustCall((result) => {
assert.deepStrictEqual(result, { stdout: '42\n', stderr: '' });
}));
}

// exec rejects when command does not exist.
{
const promise = exec('doesntexist');

promise.catch(common.mustCall((err) => {
assert(err.message.includes('doesntexist'));
}));
}

// execFile rejects when file does not exist.
{
const promise = execFile('doesntexist', ['-p', '42']);

promise.catch(common.mustCall((err) => {
assert(err.message.includes('doesntexist'));
}));
}

// Rejected errors include stdout, stderr, and code properties.
const failingCodeWithStdoutErr =
'console.log(42);console.error(43);process.exit(1)';

{
assert.rejects(exec(...common.escapePOSIXShell`"${process.execPath}" -e "${failingCodeWithStdoutErr}"`),
{
code: 1,
stdout: '42\n',
stderr: '43\n',
}).then(common.mustCall());
}

{
execFile(process.execPath, ['-e', failingCodeWithStdoutErr])
.catch(common.mustCall((err) => {
assert.strictEqual(err.code, 1);
assert.strictEqual(err.stdout, '42\n');
assert.strictEqual(err.stderr, '43\n');
}));
}

// execFile with timeout rejects with killed process.
{
assert.rejects(execFile(process.execPath, ['-e', 'setInterval(()=>{}, 99)'], { timeout: 5 }), {
killed: true,
}).then(common.mustCall());
}

// encoding option returns strings.
{
execFile(process.execPath, ['-p', '"hello"'], { encoding: 'utf8' })
.then(common.mustCall((result) => {
assert.deepStrictEqual(result, {
stdout: 'hello\n',
stderr: '',
});
}));
}

// encoding 'buffer' returns Buffer instances.
{
execFile(process.execPath, ['-p', '"hello"'], { encoding: 'buffer' })
.then(common.mustCall((result) => {
assert.deepStrictEqual(result, {
stderr: Buffer.from(''),
stdout: Buffer.from('hello\n'),
});
}));
}

// AbortSignal cancels exec.
{
const waitCommand = common.isWindows ?
`"${process.execPath}" -e "setInterval(()=>{}, 99)"` :
'sleep 2m';
const ac = new AbortController();
const promise = exec(waitCommand, { signal: ac.signal });

assert.rejects(promise, {
name: 'AbortError',
}).then(common.mustCall());
ac.abort();
}

// AbortSignal cancels execFile.
{
const ac = new AbortController();
const promise = execFile(
process.execPath,
['-e', 'setInterval(()=>{}, 99)'],
{ signal: ac.signal },
);

assert.rejects(promise, {
name: 'AbortError',
}).then(common.mustCall());
ac.abort();
}

// Already-aborted signal rejects immediately.
{
const signal = AbortSignal.abort();
const promise = exec('echo hello', { signal });

assert.rejects(promise, { name: 'AbortError' })
.then(common.mustCall());
}

// maxBuffer causes rejection.
{
assert.rejects(execFile(
process.execPath,
['-e', "console.log('a'.repeat(100))"],
{ maxBuffer: 10 },
), {
name: 'RangeError',
code: 'ERR_CHILD_PROCESS_STDIO_MAXBUFFER',
}).then(common.mustCall());
}

// timeout causes the child process to be killed.
{
const promise = execFile(
process.execPath,
['-e', 'setInterval(()=>{}, 99)'],
{ timeout: 1 },
);

promise.catch(common.mustCall((err) => {
assert.ok(err.killed || err.signal);
}));
}

// Module can be loaded with node: scheme.
{
assert.strictEqual(require('node:child_process/promises'), require('child_process/promises'));
}
Comment on lines +165 to +169
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think this is needed

Suggested change
// Test: module can be loaded with node: scheme.
{
const promises = require('node:child_process/promises');
assert.strictEqual(typeof promises.exec, 'function');
assert.strictEqual(typeof promises.execFile, 'function');
}

If we really want it, we should be checking for strict equality.

Suggested change
// Test: module can be loaded with node: scheme.
{
const promises = require('node:child_process/promises');
assert.strictEqual(typeof promises.exec, 'function');
assert.strictEqual(typeof promises.execFile, 'function');
}
assert.strictEqual(require('node:child_process/promises'), require('child_process/promises'));

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good call — replaced it with the strict equality check between node: and non-prefixed require, which is more useful.