Skip to content

Commit 010d858

Browse files
committed
fix(cli): interrupt running commands cleanly on Ctrl+C
The launcher (bin/cli.js) spawns the real CLI as a child with inherited stdio. Previously it was torn down by SIGINT before that child finished, so the child's final status printed after the shell prompt had already returned (and the child could be left running in the background). Wait briefly for the child to handle the interrupt itself and mirror its exit, so output stays ordered. The wait is bounded: if the child outlives a short grace period, or on a second Ctrl+C, SIGKILL it and exit with the conventional 128+signum code. The grace timer is unref'd, so a child that exits on its own is mirrored with no added latency.
1 parent c78c6c6 commit 010d858

1 file changed

Lines changed: 30 additions & 0 deletions

File tree

bin/cli.js

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,9 +38,39 @@ void (async () => {
3838
},
3939
)
4040

41+
// The child shares our process group and handles the signal itself; wait briefly for it
42+
// to exit (so its final output isn't printed after the prompt returns) and mirror its
43+
// exit below. SIGKILL and leave if it outlasts the grace, or on a second signal.
44+
const SHUTDOWN_GRACE_MS = 3_000
45+
const hardAbort = signalName => {
46+
const child = spawnPromise.process
47+
if (child.exitCode === null && child.signalCode === null) {
48+
child.kill('SIGKILL')
49+
}
50+
// eslint-disable-next-line n/no-process-exit
51+
process.exit(signalName === 'SIGTERM' ? 143 : 130)
52+
}
53+
let sawSignal = false
54+
const onSignal = signalName => {
55+
if (sawSignal) {
56+
hardAbort(signalName)
57+
return
58+
}
59+
sawSignal = true
60+
setTimeout(() => hardAbort(signalName), SHUTDOWN_GRACE_MS).unref?.()
61+
}
62+
const onSigint = () => onSignal('SIGINT')
63+
const onSigterm = () => onSignal('SIGTERM')
64+
process.on('SIGINT', onSigint)
65+
process.on('SIGTERM', onSigterm)
66+
4167
// See https://nodejs.org/api/child_process.html#event-exit.
4268
spawnPromise.process.on('exit', (code, signalName) => {
4369
if (signalName) {
70+
// Mirror a signal death. Drop our own handlers first so the re-raise actually
71+
// terminates us instead of being swallowed by onSigint/onSigterm above.
72+
process.removeListener('SIGINT', onSigint)
73+
process.removeListener('SIGTERM', onSigterm)
4474
process.kill(process.pid, signalName)
4575
} else if (typeof code === 'number') {
4676
// eslint-disable-next-line n/no-process-exit

0 commit comments

Comments
 (0)