From 7c675904b9a195220e5918c6ded6f993759ff7e4 Mon Sep 17 00:00:00 2001 From: NgoQuocViet2001 Date: Fri, 30 Oct 2026 02:22:26 +0700 Subject: [PATCH] fix(cli): exit when the command dies from SIGPIPE --- cli.js | 4 +++- tests/test-cli-signals.js | 13 +++++++++++++ 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/cli.js b/cli.js index 12e8a899..84154227 100755 --- a/cli.js +++ b/cli.js @@ -286,7 +286,9 @@ function run (argv) { } else { // Keep the event loop alive until the re-raised signal terminates us. // Otherwise Node can finish normally before delivery and report exit 0. - setInterval(() => {}, 1000) + // Node ignores some signals (SIGPIPE), so fall back to the shell's + // 128 + signal number status instead of waiting forever. + setTimeout(() => process.exit(128 + os.constants.signals[signal]), 1000) process.kill(process.pid, signal) } }) diff --git a/tests/test-cli-signals.js b/tests/test-cli-signals.js index 5faadb7d..b253554c 100644 --- a/tests/test-cli-signals.js +++ b/tests/test-cli-signals.js @@ -75,6 +75,19 @@ t.test('POSIX signal forwarding', { skip: process.platform === 'win32' }, async st.same(await command.exited, { code: null, signal: 'SIGTERM' }) }) + await ct.test('exits when the command dies from a signal Node ignores', async st => { + // Node ignores SIGPIPE, so re-raising it does not stop dotenv (`dotenv run -- cmd | head`). + const result = cp.spawnSync(process.execPath, ['dist/index.cjs', 'run', '-q', '--', 'sh', '-c', 'kill -PIPE $$'], { + cwd: path.resolve(__dirname, '..'), + encoding: 'utf8', + timeout: 5000, + env: { ...process.env, NODE_OPTIONS: '' } + }) + st.equal(result.error, undefined) + st.equal(result.signal, null) + st.equal(result.status, 128 + os.constants.signals.SIGPIPE) + }) + await ct.test('forwards termination to subprocesses too', async st => { const grandchild = "process.once('SIGTERM', () => { console.log('grandchild stopped'); process.exit(0) }); setInterval(() => {}, 1000); process.send('ready')" const command = await launch(st, `