Repository: nodejs/node
Pull request: util: process signal to exit code utility

### Description

Adds `util.convertProcessSignalToExitCode()` utility function to convert signal names (e.g., `SIGTERM`, `SIGKILL`) to their corresponding POSIX exit codes.

When a child process is terminated by a signal, the `code` parameter in the `'exit'` and `'close'` events is `null`. This utility allows users to convert the `signal` parameter to the POSIX standard exit code.

### Example

```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
```

**Note**: While Windows doesn't natively support POSIX signals, Node.js provides a cross-platform abstraction that emulates signal behavior. This allows `convertProcessSignalToExitCode()` to work consistently across all platforms, returning the same POSIX exit codes on both Unix-like systems and Windows.

### Refs
- https://github.com/nodejs/node/issues/60285
- https://github.com/nodejs/node/pull/60720

<!--
Before submitting a pull request, please read:

- the CONTRIBUTING guide at https://github.com/nodejs/node/blob/HEAD/CONTRIBUTING.md
- the commit message formatting guidelines at
  https://github.com/nodejs/node/blob/HEAD/doc/contributing/pull-requests.md#commit-message-guidelines

For code changes:
1. Include tests for any bug fixes or new features.
2. Update documentation if relevant.
3. Ensure that `make -j4 test` (UNIX), or `vcbuild test` (Windows) passes.

If you believe this PR should be highlighted in the Node.js CHANGELOG
please add the `notable-change` label.

Developer's Certificate of Origin 1.1

By making a contribution to this project, I certify that:

(a) The contribution was created in whole or in part by me and I
    have the right to submit it under the open source license
    indicated in the file; or

(b) The contribution is based upon previous work that, to the best
    of my knowledge, is covered under an appropriate open source
    license and I have the right under that license to submit that
    work with modifications, whether created in whole or in part
    by me, under the same open source license (unless I am
    permitted to submit under a different license), as indicated
    in the file; or

(c) The contribution was provided directly to me by some other
    person who certified (a), (b) or (c) and I have not modified
    it.

(d) I understand and agree that this project and the contribution
    are public and that a record of the contribution (including all
    personal information I submit with it, including my sign-off) is
    maintained indefinitely and may be redistributed consistent with
    this project or the open source license(s) involved.
-->


Changed files:
filename: doc/api/child_process.md
patch: @@ -1557,6 +1557,10 @@ re-raise the handled signal.
 
 See waitpid(2).
 
+When `code` is `null` due to signal termination, you can use
+[`util.convertProcessSignalToExitCode()`][] to convert the signal to a POSIX
+exit code.
+
 ### Event: `'message'`
 
 <!-- YAML
@@ -1671,6 +1675,11 @@ 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` and [`subprocess.signalCode`][] will be set. To get the corresponding
+POSIX exit code, use
+[`util.convertProcessSignalToExitCode(subprocess.signalCode)`][`util.convertProcessSignalToExitCode()`].
+
 ### `subprocess.kill([signal])`
 
 <!-- YAML
@@ -2107,6 +2116,10 @@ connection to the child.
 The `subprocess.signalCode` property indicates the signal received by
 the child process if any, else `null`.
 
+When the child process is terminated by a signal, [`subprocess.exitCode`][] will be `null`.
+To get the corresponding POSIX exit code, use
+[`util.convertProcessSignalToExitCode(subprocess.signalCode)`][`util.convertProcessSignalToExitCode()`].
+
 ### `subprocess.spawnargs`
 
 * Type: {Array}
@@ -2387,12 +2400,15 @@ or [`child_process.fork()`][].
 [`stdio`]: #optionsstdio
 [`subprocess.connected`]: #subprocessconnected
 [`subprocess.disconnect()`]: #subprocessdisconnect
+[`subprocess.exitCode`]: #subprocessexitcode
 [`subprocess.kill()`]: #subprocesskillsignal
 [`subprocess.send()`]: #subprocesssendmessage-sendhandle-options-callback
+[`subprocess.signalCode`]: #subprocesssignalcode
 [`subprocess.stderr`]: #subprocessstderr
 [`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

filename: doc/api/util.md
patch: @@ -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

filename: lib/internal/util.js
patch: @@ -16,6 +16,7 @@ const {
   ObjectGetOwnPropertyDescriptor,
   ObjectGetOwnPropertyDescriptors,
   ObjectGetPrototypeOf,
+  ObjectKeys,
   ObjectPrototypeHasOwnProperty,
   ObjectSetPrototypeOf,
   ObjectValues,
@@ -106,6 +107,7 @@ function isError(e) {
 const codesWarned = new SafeSet();
 
 let validateString;
+let validateOneOf;
 
 function getDeprecationWarningEmitter(
   code, msg, deprecated, useEmitSync,
@@ -393,6 +395,17 @@ function convertToValidSignal(signal) {
   throw new ERR_UNKNOWN_SIGNAL(signal);
 }
 
+function convertProcessSignalToExitCode(signalCode) {
+  // Lazy-load to avoid a circular dependency.
+  if (validateOneOf === undefined)
+    ({ validateOneOf } = require('internal/validators'));
+
+  validateOneOf(signalCode, 'signalCode', ObjectKeys(signals));
+
+  // POSIX standard: exit code for signal termination is 128 + signal number.
+  return 128 + signals[signalCode];
+}
+
 function getConstructorOf(obj) {
   while (obj) {
     const descriptor = ObjectGetOwnPropertyDescriptor(obj, 'constructor');
@@ -956,6 +969,7 @@ module.exports = {
   assignFunctionName,
   cachedResult,
   constructSharedArrayBuffer,
+  convertProcessSignalToExitCode,
   convertToValidSignal,
   createClassWrapper,
   decorateErrorStack,

filename: lib/util.js
patch: @@ -84,6 +84,7 @@ const { getOptionValue } = require('internal/options');
 const binding = internalBinding('util');
 
 const {
+  convertProcessSignalToExitCode,
   deprecate: internalDeprecate,
   getLazy,
   getSystemErrorMap,
@@ -472,6 +473,7 @@ module.exports = {
                              'The `util._extend` API is deprecated. Please use Object.assign() instead.',
                              'DEP0060'),
   callbackify,
+  convertProcessSignalToExitCode,
   debug: debuglog,
   debuglog,
   deprecate,

filename: test/parallel/test-util-convert-signal-to-exit-code.mjs
patch: @@ -0,0 +1,61 @@
+import { mustCall, mustNotCall, isWindows } from '../common/index.mjs';
+import assert from 'assert';
+import { convertProcessSignalToExitCode } from 'util';
+import { spawn } from 'child_process';
+import { constants } from 'os';
+const { signals } = constants;
+
+{
+
+  assert.strictEqual(convertProcessSignalToExitCode('SIGTERM'), 128 + signals.SIGTERM);
+  assert.strictEqual(convertProcessSignalToExitCode('SIGKILL'), 128 + signals.SIGKILL);
+  assert.strictEqual(convertProcessSignalToExitCode('SIGINT'), 128 + signals.SIGINT);
+  assert.strictEqual(convertProcessSignalToExitCode('SIGHUP'), 128 + signals.SIGHUP);
+  assert.strictEqual(convertProcessSignalToExitCode('SIGABRT'), 128 + signals.SIGABRT);
+}
+
+{
+  [
+    'INVALID',
+    '',
+    'SIG',
+    undefined,
+    null,
+    123,
+    true,
+    false,
+    {},
+    [],
+    Symbol('test'),
+    () => {},
+  ].forEach((value) => {
+    assert.throws(
+      () => convertProcessSignalToExitCode(value),
+      {
+        code: 'ERR_INVALID_ARG_VALUE',
+        name: 'TypeError',
+      }
+    );
+  });
+}
+
+{
+  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');
+
+    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);
+}