Skip to content

Commit 545f3be

Browse files
authored
fix: handle signals more correctly (#142)
previously we were assuming that npm received the signal first and was required to forward the signal to child processes, however that seems to no longer be the case. instead npm and the child process both receive the signal at the same time. the previous logic has been modified such that it places a no-op function as the signal handler. this is strictly to prevent the default behavior of exiting node from happening. once all child process have exited, the handlers are all removed and we exit appropriately
1 parent 1cbd286 commit 545f3be

File tree

3 files changed

+12
-27
lines changed

3 files changed

+12
-27
lines changed

lib/run-script-pkg.js

+4
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,11 @@ const runScriptPkg = async options => {
9494
return p.catch(er => {
9595
const { signal } = er
9696
if (stdio === 'inherit' && signal) {
97+
// by the time we reach here, the child has already exited. we send the
98+
// signal back to ourselves again so that npm will exit with the same
99+
// status as the child
97100
process.kill(process.pid, signal)
101+
98102
// just in case we don't die, reject after 500ms
99103
// this also keeps the node process open long enough to actually
100104
// get the signal, rather than terminating gracefully.

lib/signal-manager.js

+8-6
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,19 @@
11
const runningProcs = new Set()
22
let handlersInstalled = false
33

4+
// NOTE: these signals aren't actually forwarded anywhere. they're trapped and
5+
// ignored until all child processes have exited. in our next breaking change
6+
// we should rename this
47
const forwardedSignals = [
58
'SIGINT',
69
'SIGTERM',
710
]
811

9-
const handleSignal = signal => {
10-
for (const proc of runningProcs) {
11-
proc.kill(signal)
12-
}
13-
}
14-
12+
// no-op, this is so receiving the signal doesn't cause us to exit immediately
13+
// instead, we exit after all children have exited when we re-send the signal
14+
// to ourselves. see the catch handler at the bottom of run-script-pkg.js
15+
// istanbul ignore next - this function does nothing
16+
const handleSignal = () => {}
1517
const setupListeners = () => {
1618
for (const signal of forwardedSignals) {
1719
process.on(signal, handleSignal)

test/signal-manager.js

-21
Original file line numberDiff line numberDiff line change
@@ -44,24 +44,3 @@ test('adds only one handler for each signal, removes handlers when children have
4444

4545
t.end()
4646
})
47-
48-
test('forwards signals to child process', t => {
49-
const proc = new EventEmitter()
50-
proc.kill = (signal) => {
51-
t.equal(signal, signalManager.forwardedSignals[0], 'child receives correct signal')
52-
proc.emit('exit', 0)
53-
for (const forwarded of signalManager.forwardedSignals) {
54-
t.equal(
55-
process.listeners(forwarded).includes(signalManager.handleSignal),
56-
false, 'listener has been removed')
57-
}
58-
t.end()
59-
}
60-
61-
signalManager.add(proc)
62-
// passing the signal name here is necessary to fake the effects of actually
63-
// receiving the signal per nodejs documentation signal handlers receive the
64-
// name of the signal as their first parameter
65-
// https://nodejs.org/api/process.html#process_signal_events
66-
process.emit(signalManager.forwardedSignals[0], signalManager.forwardedSignals[0])
67-
})

0 commit comments

Comments
 (0)