Skip to content

Commit 6882f0f

Browse files
committed
child_process: set exitCode when process is killed by signal
When a child process is terminated by a signal, the exitCode property is now set to the POSIX standard exit code (128 + signal number) instead of null. A new utility function convertProcessSignalToExitCode() has been added to both internal/util and the public util API to convert signal names to their corresponding exit codes. Fixes: #60285
1 parent 4a868fd commit 6882f0f

11 files changed

+93
-24
lines changed

doc/api/util.md

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,38 @@ callbackFunction((err, ret) => {
8989
});
9090
```
9191

92+
## `util.convertProcessSignalToExitCode(signalCode)`
93+
94+
<!-- YAML
95+
added: REPLACEME
96+
-->
97+
98+
* `signalCode` {string} A signal name (e.g., `'SIGTERM'`, `'SIGKILL'`).
99+
* Returns: {number|null} The exit code, or `null` if the signal is invalid.
100+
101+
The `util.convertProcessSignalToExitCode()` method converts a signal name to its
102+
corresponding POSIX exit code. Following the POSIX standard, the exit code
103+
for a process terminated by a signal is calculated as `128 + signal number`.
104+
105+
```mjs
106+
import { convertProcessSignalToExitCode } from 'node:util';
107+
108+
console.log(convertProcessSignalToExitCode('SIGTERM')); // 143 (128 + 15)
109+
console.log(convertProcessSignalToExitCode('SIGKILL')); // 137 (128 + 9)
110+
console.log(convertProcessSignalToExitCode('INVALID')); // null
111+
```
112+
113+
```cjs
114+
const { convertProcessSignalToExitCode } = require('node:util');
115+
116+
console.log(convertProcessSignalToExitCode('SIGTERM')); // 143 (128 + 15)
117+
console.log(convertProcessSignalToExitCode('SIGKILL')); // 137 (128 + 9)
118+
console.log(convertProcessSignalToExitCode('INVALID')); // null
119+
```
120+
121+
This is particularly useful when working with processes to determine
122+
the exit code based on the signal that terminated the process.
123+
92124
## `util.debuglog(section[, callback])`
93125

94126
<!-- YAML

lib/internal/child_process.js

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ const { TTY } = internalBinding('tty_wrap');
5555
const { UDP } = internalBinding('udp_wrap');
5656
const SocketList = require('internal/socket_list');
5757
const { owner_symbol } = require('internal/async_hooks').symbols;
58-
const { convertToValidSignal } = require('internal/util');
58+
const { convertProcessSignalToExitCode, convertToValidSignal } = require('internal/util');
5959
const { isArrayBufferView } = require('internal/util/types');
6060
const spawn_sync = internalBinding('spawn_sync');
6161
const { kStateSymbol } = require('internal/dgram');
@@ -269,8 +269,11 @@ function ChildProcess() {
269269
this._handle.onexit = (exitCode, signalCode) => {
270270
if (signalCode) {
271271
this.signalCode = signalCode;
272+
this.exitCode = convertProcessSignalToExitCode(signalCode);
272273
} else {
274+
// Normal exit: use the exit code directly
273275
this.exitCode = exitCode;
276+
this.signalCode = null;
274277
}
275278

276279
if (this.stdin) {

lib/internal/util.js

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -393,6 +393,20 @@ function convertToValidSignal(signal) {
393393
throw new ERR_UNKNOWN_SIGNAL(signal);
394394
}
395395

396+
function convertProcessSignalToExitCode(signalCode) {
397+
if (typeof signalCode !== 'string') {
398+
return null;
399+
}
400+
401+
const signalNumber = signals[signalCode];
402+
if (signalNumber === undefined) {
403+
return null;
404+
}
405+
406+
// POSIX standard: exit code for signal termination is 128 + signal number
407+
return 128 + signalNumber;
408+
}
409+
396410
function getConstructorOf(obj) {
397411
while (obj) {
398412
const descriptor = ObjectGetOwnPropertyDescriptor(obj, 'constructor');
@@ -956,6 +970,7 @@ module.exports = {
956970
assignFunctionName,
957971
cachedResult,
958972
constructSharedArrayBuffer,
973+
convertProcessSignalToExitCode,
959974
convertToValidSignal,
960975
createClassWrapper,
961976
decorateErrorStack,

lib/util.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,7 @@ const { getOptionValue } = require('internal/options');
8484
const binding = internalBinding('util');
8585

8686
const {
87+
convertProcessSignalToExitCode,
8788
deprecate: internalDeprecate,
8889
getLazy,
8990
getSystemErrorMap,
@@ -472,6 +473,7 @@ module.exports = {
472473
'The `util._extend` API is deprecated. Please use Object.assign() instead.',
473474
'DEP0060'),
474475
callbackify,
476+
convertProcessSignalToExitCode,
475477
debug: debuglog,
476478
debuglog,
477479
deprecate,

test/parallel/test-child-process-destroy.js

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,22 @@
11
'use strict';
22
const common = require('../common');
33
const assert = require('assert');
4+
const { convertProcessSignalToExitCode } = require('util');
45
const spawn = require('child_process').spawn;
56
const cat = spawn(common.isWindows ? 'cmd' : 'cat');
67

78
cat.stdout.on('end', common.mustCall());
89
cat.stderr.on('data', common.mustNotCall());
910
cat.stderr.on('end', common.mustCall());
1011

12+
const exitCode = convertProcessSignalToExitCode('SIGTERM');
1113
cat.on('exit', common.mustCall((code, signal) => {
12-
assert.strictEqual(code, null);
14+
assert.strictEqual(code, exitCode);
1315
assert.strictEqual(signal, 'SIGTERM');
1416
assert.strictEqual(cat.signalCode, 'SIGTERM');
1517
}));
1618
cat.on('exit', common.mustCall((code, signal) => {
17-
assert.strictEqual(code, null);
19+
assert.strictEqual(code, exitCode);
1820
assert.strictEqual(signal, 'SIGTERM');
1921
assert.strictEqual(cat.signalCode, 'SIGTERM');
2022
}));

test/parallel/test-child-process-exec-timeout-expire.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
const common = require('../common');
66
const assert = require('assert');
77
const cp = require('child_process');
8+
const { convertProcessSignalToExitCode } = require('util');
89

910
const {
1011
cleanupStaleProcess,
@@ -34,7 +35,7 @@ cp.exec(cmd, {
3435
assert.strictEqual(err.code, 143);
3536
sigterm = null;
3637
} else {
37-
assert.strictEqual(err.code, null);
38+
assert.strictEqual(err.code, convertProcessSignalToExitCode(sigterm));
3839
}
3940
// At least starting with Darwin Kernel Version 16.4.0, sending a SIGTERM to a
4041
// process that is still starting up kills it with SIGKILL instead of SIGTERM.

test/parallel/test-child-process-exec-timeout-kill.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
const common = require('../common');
66
const assert = require('assert');
77
const cp = require('child_process');
8+
const { convertProcessSignalToExitCode } = require('util');
89

910
const {
1011
cleanupStaleProcess,
@@ -30,7 +31,7 @@ cp.exec(cmd, {
3031
console.log('[stderr]', stderr.trim());
3132

3233
assert.strictEqual(err.killed, true);
33-
assert.strictEqual(err.code, null);
34+
assert.strictEqual(err.code, convertProcessSignalToExitCode('SIGKILL'));
3435
assert.strictEqual(err.signal, 'SIGKILL');
3536
assert.strictEqual(err.cmd, cmd);
3637
assert.strictEqual(stdout.trim(), '');

test/parallel/test-child-process-fork-abort-signal.js

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ const { mustCall, mustNotCall } = require('../common');
44
const assert = require('assert');
55
const fixtures = require('../common/fixtures');
66
const { fork } = require('child_process');
7+
const { convertProcessSignalToExitCode } = require('util');
78

89
{
910
// Test aborting a forked child_process after calling fork
@@ -13,7 +14,7 @@ const { fork } = require('child_process');
1314
signal
1415
});
1516
cp.on('exit', mustCall((code, killSignal) => {
16-
assert.strictEqual(code, null);
17+
assert.strictEqual(code, convertProcessSignalToExitCode('SIGTERM'));
1718
assert.strictEqual(killSignal, 'SIGTERM');
1819
}));
1920
cp.on('error', mustCall((err) => {
@@ -30,7 +31,7 @@ const { fork } = require('child_process');
3031
signal
3132
});
3233
cp.on('exit', mustCall((code, killSignal) => {
33-
assert.strictEqual(code, null);
34+
assert.strictEqual(code, convertProcessSignalToExitCode('SIGTERM'));
3435
assert.strictEqual(killSignal, 'SIGTERM');
3536
}));
3637
cp.on('error', mustCall((err) => {
@@ -48,7 +49,7 @@ const { fork } = require('child_process');
4849
signal
4950
});
5051
cp.on('exit', mustCall((code, killSignal) => {
51-
assert.strictEqual(code, null);
52+
assert.strictEqual(code, convertProcessSignalToExitCode('SIGTERM'));
5253
assert.strictEqual(killSignal, 'SIGTERM');
5354
}));
5455
cp.on('error', mustCall((err) => {
@@ -63,7 +64,7 @@ const { fork } = require('child_process');
6364
signal
6465
});
6566
cp.on('exit', mustCall((code, killSignal) => {
66-
assert.strictEqual(code, null);
67+
assert.strictEqual(code, convertProcessSignalToExitCode('SIGTERM'));
6768
assert.strictEqual(killSignal, 'SIGTERM');
6869
}));
6970
cp.on('error', mustCall((err) => {
@@ -81,7 +82,7 @@ const { fork } = require('child_process');
8182
killSignal: 'SIGKILL',
8283
});
8384
cp.on('exit', mustCall((code, killSignal) => {
84-
assert.strictEqual(code, null);
85+
assert.strictEqual(code, convertProcessSignalToExitCode('SIGKILL'));
8586
assert.strictEqual(killSignal, 'SIGKILL');
8687
}));
8788
cp.on('error', mustCall((err) => {

test/parallel/test-child-process-kill.js

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -16,12 +16,13 @@
1616
// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN
1717
// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
1818
// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
19-
// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE
20-
// USE OR OTHER DEALINGS IN THE SOFTWARE.
19+
// OTHERWISE, ARISING IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
20+
// DEALINGS IN THE SOFTWARE.
2121

2222
'use strict';
2323
const common = require('../common');
2424
const assert = require('assert');
25+
const { convertProcessSignalToExitCode } = require('util');
2526
const spawn = require('child_process').spawn;
2627
const cat = spawn(common.isWindows ? 'cmd' : 'cat');
2728

@@ -30,9 +31,11 @@ cat.stderr.on('data', common.mustNotCall());
3031
cat.stderr.on('end', common.mustCall());
3132

3233
cat.on('exit', common.mustCall((code, signal) => {
33-
assert.strictEqual(code, null);
34+
const expectedExitCode = convertProcessSignalToExitCode('SIGTERM');
35+
assert.strictEqual(code, expectedExitCode);
3436
assert.strictEqual(signal, 'SIGTERM');
3537
assert.strictEqual(cat.signalCode, 'SIGTERM');
38+
assert.strictEqual(cat.exitCode, expectedExitCode);
3639
}));
3740

3841
assert.strictEqual(cat.signalCode, null);
@@ -45,16 +48,21 @@ if (common.isWindows) {
4548
for (const sendSignal of ['SIGTERM', 'SIGKILL', 'SIGQUIT', 'SIGINT']) {
4649
const process = spawn('cmd');
4750
process.on('exit', (code, signal) => {
48-
assert.strictEqual(code, null);
51+
const expectedExitCode = convertProcessSignalToExitCode(sendSignal);
52+
assert.strictEqual(code, expectedExitCode);
4953
assert.strictEqual(signal, sendSignal);
54+
assert.strictEqual(process.exitCode, expectedExitCode);
5055
});
5156
process.kill(sendSignal);
5257
}
5358

5459
const process = spawn('cmd');
5560
process.on('exit', (code, signal) => {
56-
assert.strictEqual(code, null);
61+
const expectedExitCode = convertProcessSignalToExitCode('SIGKILL');
62+
assert.strictEqual(code, expectedExitCode);
5763
assert.strictEqual(signal, 'SIGKILL');
64+
assert.strictEqual(process.exitCode, expectedExitCode);
65+
assert.strictEqual(process.signalCode, 'SIGKILL');
5866
});
5967
process.kill('SIGHUP');
6068
}
@@ -70,6 +78,8 @@ const checkProcess = spawn(process.execPath, ['-e', code]);
7078
checkProcess.on('exit', common.mustCall((code, signal) => {
7179
assert.strictEqual(code, 0);
7280
assert.strictEqual(signal, null);
81+
assert.strictEqual(checkProcess.exitCode, 0);
82+
assert.strictEqual(checkProcess.signalCode, null);
7383
}));
7484

7585
checkProcess.stdout.on('data', common.mustCall((chunk) => {

test/parallel/test-child-process-spawn-controller.js

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ const common = require('../common');
44
const assert = require('assert');
55
const { spawn } = require('child_process');
66
const fixtures = require('../common/fixtures');
7+
const { convertProcessSignalToExitCode } = require('util');
78

89
const aliveScript = fixtures.path('child-process-stay-alive-forever.js');
910
{
@@ -16,7 +17,7 @@ const aliveScript = fixtures.path('child-process-stay-alive-forever.js');
1617
});
1718

1819
cp.on('exit', common.mustCall((code, killSignal) => {
19-
assert.strictEqual(code, null);
20+
assert.strictEqual(code, convertProcessSignalToExitCode('SIGTERM'));
2021
assert.strictEqual(killSignal, 'SIGTERM');
2122
}));
2223

@@ -36,7 +37,7 @@ const aliveScript = fixtures.path('child-process-stay-alive-forever.js');
3637
});
3738

3839
cp.on('exit', common.mustCall((code, killSignal) => {
39-
assert.strictEqual(code, null);
40+
assert.strictEqual(code, convertProcessSignalToExitCode('SIGTERM'));
4041
assert.strictEqual(killSignal, 'SIGTERM');
4142
}));
4243

@@ -57,7 +58,7 @@ const aliveScript = fixtures.path('child-process-stay-alive-forever.js');
5758
});
5859

5960
cp.on('exit', common.mustCall((code, killSignal) => {
60-
assert.strictEqual(code, null);
61+
assert.strictEqual(code, convertProcessSignalToExitCode('SIGTERM'));
6162
assert.strictEqual(killSignal, 'SIGTERM');
6263
}));
6364

@@ -77,7 +78,7 @@ const aliveScript = fixtures.path('child-process-stay-alive-forever.js');
7778
signal,
7879
});
7980
cp.on('exit', common.mustCall((code, killSignal) => {
80-
assert.strictEqual(code, null);
81+
assert.strictEqual(code, convertProcessSignalToExitCode('SIGTERM'));
8182
assert.strictEqual(killSignal, 'SIGTERM');
8283
}));
8384

@@ -94,7 +95,7 @@ const aliveScript = fixtures.path('child-process-stay-alive-forever.js');
9495
signal,
9596
});
9697
cp.on('exit', common.mustCall((code, killSignal) => {
97-
assert.strictEqual(code, null);
98+
assert.strictEqual(code, convertProcessSignalToExitCode('SIGTERM'));
9899
assert.strictEqual(killSignal, 'SIGTERM');
99100
}));
100101

@@ -111,7 +112,7 @@ const aliveScript = fixtures.path('child-process-stay-alive-forever.js');
111112
signal,
112113
});
113114
cp.on('exit', common.mustCall((code, killSignal) => {
114-
assert.strictEqual(code, null);
115+
assert.strictEqual(code, convertProcessSignalToExitCode('SIGTERM'));
115116
assert.strictEqual(killSignal, 'SIGTERM');
116117
}));
117118

@@ -131,7 +132,7 @@ const aliveScript = fixtures.path('child-process-stay-alive-forever.js');
131132
});
132133

133134
cp.on('exit', common.mustCall((code, killSignal) => {
134-
assert.strictEqual(code, null);
135+
assert.strictEqual(code, convertProcessSignalToExitCode('SIGTERM'));
135136
assert.strictEqual(killSignal, 'SIGTERM');
136137
}));
137138

@@ -153,7 +154,7 @@ const aliveScript = fixtures.path('child-process-stay-alive-forever.js');
153154
});
154155

155156
cp.on('exit', common.mustCall((code, killSignal) => {
156-
assert.strictEqual(code, null);
157+
assert.strictEqual(code, convertProcessSignalToExitCode('SIGKILL'));
157158
assert.strictEqual(killSignal, 'SIGKILL');
158159
}));
159160

0 commit comments

Comments
 (0)