Skip to content
Open
Show file tree
Hide file tree
Changes from 2 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
47 changes: 47 additions & 0 deletions doc/api/child_process.md
Original file line number Diff line number Diff line change
Expand Up @@ -1547,6 +1547,42 @@ exited, `code` is the final exit code of the process, otherwise `null`. If the
process terminated due to receipt of a signal, `signal` is the string name of
the signal, otherwise `null`. One of the two will always be non-`null`.

When `code` is `null` due to signal termination, you can use
[`util.convertProcessSignalToExitCode()`][] to convert the signal to a POSIX
exit code.

```cjs
const { spawn } = require('node:child_process');
const { convertProcessSignalToExitCode } = require('node:util');

const ls = spawn('ls', ['-lh', '/usr']);

ls.kill();

ls.on('exit', (code, signal) => {
const exitCode = convertProcessSignalToExitCode(signal);
console.log(`signal ${signal}, POSIX exit code: ${exitCode}`);
});

// signal SIGTERM, POSIX exit code: 143
```

```mjs
import { spawn } from 'node:child_process';
import { once } from 'node:events';
import { convertProcessSignalToExitCode } from 'node:util';

const ls = spawn('ls', ['-lh', '/usr']);

ls.kill();

const [code, signal] = await once(ls, 'exit');
const exitCode = convertProcessSignalToExitCode(signal);
console.log(`signal ${signal}, POSIX exit code: ${exitCode}`);

// signal SIGTERM, POSIX exit code: 143
```
Copy link
Member

Choose a reason for hiding this comment

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

I would remove the code samples and move the text to the bottom of the section – there's very little reason why somebody would want to do this, while information such as that stdio streams may still be open is much more directly relevant to what this event does

Copy link
Member Author

Choose a reason for hiding this comment

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

done


When the `'exit'` event is triggered, child process stdio streams might still be
open.

Expand Down Expand Up @@ -1671,6 +1707,16 @@ within the child process to close the IPC channel as well.
The `subprocess.exitCode` property indicates the exit code of the child process.
If the child process is still running, the field will be `null`.

When the child process is terminated by a signal, `subprocess.exitCode` will be
`null`. To get the corresponding POSIX exit code, use
Copy link
Member

Choose a reason for hiding this comment

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

I think the really important thing we should mention is that subprocess.signalCode will be set, and we should link to that.

Copy link
Member Author

Choose a reason for hiding this comment

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

done

[`util.convertProcessSignalToExitCode(subprocess.signalCode)`][`util.convertProcessSignalToExitCode()`].

**Note:** The `child_process` module does not automatically set `exitCode` when
a process is terminated by a signal to avoid breaking changes in existing code
that may depend on `exitCode` being `null` in such cases. The
[`util.convertProcessSignalToExitCode()`][] utility function is provided to
allow applications to opt-in to this conversion when needed.

### `subprocess.kill([signal])`

<!-- YAML
Expand Down Expand Up @@ -2393,6 +2439,7 @@ or [`child_process.fork()`][].
[`subprocess.stdin`]: #subprocessstdin
[`subprocess.stdio`]: #subprocessstdio
[`subprocess.stdout`]: #subprocessstdout
[`util.convertProcessSignalToExitCode()`]: util.md#utilconvertprocesssignaltoexitcodesignalcode
[`util.promisify()`]: util.md#utilpromisifyoriginal
[synchronous counterparts]: #synchronous-process-creation
[v8.serdes]: v8.md#serialization-api
32 changes: 32 additions & 0 deletions doc/api/util.md
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,38 @@ callbackFunction((err, ret) => {
});
```

## `util.convertProcessSignalToExitCode(signalCode)`

<!-- YAML
added: REPLACEME
-->

* `signalCode` {string} A signal name (e.g., `'SIGTERM'`, `'SIGKILL'`).
* Returns: {number|null} The exit code, or `null` if the signal is invalid.

The `util.convertProcessSignalToExitCode()` method converts a signal name to its
corresponding POSIX exit code. Following the POSIX standard, the exit code
for a process terminated by a signal is calculated as `128 + signal number`.

```mjs
import { convertProcessSignalToExitCode } from 'node:util';

console.log(convertProcessSignalToExitCode('SIGTERM')); // 143 (128 + 15)
console.log(convertProcessSignalToExitCode('SIGKILL')); // 137 (128 + 9)
console.log(convertProcessSignalToExitCode('INVALID')); // null
```

```cjs
const { convertProcessSignalToExitCode } = require('node:util');

console.log(convertProcessSignalToExitCode('SIGTERM')); // 143 (128 + 15)
console.log(convertProcessSignalToExitCode('SIGKILL')); // 137 (128 + 9)
console.log(convertProcessSignalToExitCode('INVALID')); // null
```

This is particularly useful when working with processes to determine
the exit code based on the signal that terminated the process.

## `util.debuglog(section[, callback])`

<!-- YAML
Expand Down
13 changes: 13 additions & 0 deletions lib/internal/util.js
Original file line number Diff line number Diff line change
Expand Up @@ -393,6 +393,18 @@ function convertToValidSignal(signal) {
throw new ERR_UNKNOWN_SIGNAL(signal);
}

function convertProcessSignalToExitCode(signalCode) {
validateString(signalCode, 'signalCode');

const signalNumber = signals[signalCode];
if (signalNumber === undefined) {
return null;
}

// POSIX standard: exit code for signal termination is 128 + signal number.
return 128 + signalNumber;
}

function getConstructorOf(obj) {
while (obj) {
const descriptor = ObjectGetOwnPropertyDescriptor(obj, 'constructor');
Expand Down Expand Up @@ -956,6 +968,7 @@ module.exports = {
assignFunctionName,
cachedResult,
constructSharedArrayBuffer,
convertProcessSignalToExitCode,
convertToValidSignal,
createClassWrapper,
decorateErrorStack,
Expand Down
2 changes: 2 additions & 0 deletions lib/util.js
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@ const { getOptionValue } = require('internal/options');
const binding = internalBinding('util');

const {
convertProcessSignalToExitCode,
deprecate: internalDeprecate,
getLazy,
getSystemErrorMap,
Expand Down Expand Up @@ -472,6 +473,7 @@ module.exports = {
'The `util._extend` API is deprecated. Please use Object.assign() instead.',
'DEP0060'),
callbackify,
convertProcessSignalToExitCode,
debug: debuglog,
debuglog,
deprecate,
Expand Down
76 changes: 76 additions & 0 deletions test/parallel/test-util-convert-signal-to-exit-code.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import { mustCall, mustNotCall, isWindows } from '../common/index.mjs';
import assert from 'assert';
import { convertProcessSignalToExitCode } from 'util';
import { spawn } from 'child_process';
import os from 'os';

Check failure on line 5 in test/parallel/test-util-convert-signal-to-exit-code.mjs

View workflow job for this annotation

GitHub Actions / lint-js-and-md

'os' is defined but never used

// Test valid signal names
{
// SIGTERM = 15, so 128 + 15 = 143
assert.strictEqual(convertProcessSignalToExitCode('SIGTERM'), 143);

// SIGKILL = 9, so 128 + 9 = 137
assert.strictEqual(convertProcessSignalToExitCode('SIGKILL'), 137);

// SIGINT = 2, so 128 + 2 = 130
assert.strictEqual(convertProcessSignalToExitCode('SIGINT'), 130);

// SIGHUP = 1, so 128 + 1 = 129
assert.strictEqual(convertProcessSignalToExitCode('SIGHUP'), 129);

// SIGABRT = 6, so 128 + 6 = 134
assert.strictEqual(convertProcessSignalToExitCode('SIGABRT'), 134);
}

// Test invalid signal names return null
{
assert.strictEqual(convertProcessSignalToExitCode('INVALID'), null);
assert.strictEqual(convertProcessSignalToExitCode(''), null);
assert.strictEqual(convertProcessSignalToExitCode('SIG'), null);
}

// Test invalid argument types
{
[
undefined,
null,
123,
true,
false,
{},
[],
Symbol('test'),
() => {},
].forEach((value) => {
assert.throws(
() => convertProcessSignalToExitCode(value),
{
code: 'ERR_INVALID_ARG_TYPE',
name: 'TypeError',
}
);
});
}

// Test with actual child process termination
{
const cat = spawn(isWindows ? 'cmd' : 'cat');
cat.stdout.on('end', mustCall());
cat.stderr.on('data', mustNotCall());
cat.stderr.on('end', mustCall());

cat.on('exit', mustCall((code, signal) => {
assert.strictEqual(code, null);
assert.strictEqual(signal, 'SIGTERM');
assert.strictEqual(cat.signalCode, 'SIGTERM');

// Verify the utility function returns correct exit code
const exitCode = convertProcessSignalToExitCode(signal);
assert.strictEqual(exitCode, 143);
}));

assert.strictEqual(cat.signalCode, null);
assert.strictEqual(cat.killed, false);
cat[Symbol.dispose]();
assert.strictEqual(cat.killed, true);
}
Loading