节点子进程:如何拦截SIGINT之类的信号

时间:2017-06-27 19:06:53

标签: node.js signals child-process

在我的节点应用中,我挂钩SIGINT信号以便优雅地停止(使用pm2,但这与此无关。)

我的应用程序也会执行/生成几个子进程。

我可以挂钩SIGINT拦截它并执行优雅停止,然而我的子进程通过相同的信号传递,因此立即被杀死。

如何拦截我子进程的SIGINT信号?

我正在做的一个例子:

const child = child_process.spawn('sleep', ['10000000']);
console.log(`Child pid: ${child.pid}`);

child.on('exit', (code, signal) => { console.log('Exit', code, signal); });

process.on('SIGINT', () => {
    console.log("Intercepting SIGINT");
});

2 个答案:

答案 0 :(得分:6)

默认情况下,child_process.spawn()创建的子进程与父进程具有相同的process group,除非使用{detached:true} option进行调用。

结果是这个脚本在不同环境中的表现会有所不同:

// spawn-test.js
const { spawn } = require('child_process');
const one = spawn('sleep', ['101']);
const two = spawn('sleep', ['102'], {detached: true});
two.unref();
process.on('SIGINT', function () {
  console.log('just ignore SIGINT');
});

在交互式shell上,默认情况下来自Ctl-C的SIGINT被发送到整个组,因此未分离的子进程将获得SIGINT并退出:

you@bash $ node spawn-test.js
^Cjust ignore SIGINT
you@bash [another-terminal-window] $ ps aux | grep sleep
... sleep 102
# note that sleep 101 is not running anymore
# because it recieved the SIGINT from the Ctl-C

...但是对kill(2)的调用只能表示你的父进程,所以孩子们还活着:

you@bash $ node spawn-test.js & echo $?
[2] 1234
you@bash [another-terminal-window] $ kill -SIGINT 1234
you@bash [another-terminal-window] $ ps aux | grep sleep
... sleep 101
... sleep 102
# both are still running

然而,pm2是另一头野兽。即使你尝试了上述技术,它也会杀死整个流程树,包括你的分离流程,即使是很长的--kill-timeout

# Test pm2 stop
you@bash $ pm2 start spawn-test.js --kill-timeout 3600
you@bash $ pm2 stop spawn-test
you@bash $ ps aux | grep sleep
# both are dead

# Test pm3 reload
you@bash $ pm2 start spawn-test.js --kill-timeout 3600
you@bash $ pm2 reload spawn-test
you@bash $ ps aux | grep sleep
# both have different PIDs and were therefore killed and restarted

这似乎是pm2中的一个错误。

我通过使用init系统(在我的情况下是systemd)而不是pm2来解决类似的问题,因为这样可以更好地控制信号处理。

在systemd上,默认情况下会将信号发送到整个组,但是您可以使用KillMode=mixed将信号仅发送到父进程,但如果超出超时时间仍然会运行SIGKILL子进程。

我的systemd单元文件如下所示:

[Unit]
Description=node server with long-running children example

[Service]
Type=simple
Restart=always
RestartSec=30
TimeoutStopSec=3600
KillMode=mixed
ExecStart=/usr/local/bin/node /path/to/your/server.js

[Install]
WantedBy=multi-user.target

答案 1 :(得分:3)

通常在C中,您可以通过忽略子节点中的信号(或者通过在新的进程组中生成信号来解决这个问题,以便终端为前台进程组生成的信号无法到达它)

https://nodejs.org/api/child_process.html#child_process_child_process_spawn_command_args_options看,它看起来并不像NodeJ为此公开API,但它确实有一个通过shell生成子进程的选项,所以你可以做的就是转它打开并忽略shell中的信号,这将导致其被忽略的状态被继承到shell的子项。

const child_process = require('child_process')
//const child = child_process.spawn('sleep', ['10000000']);
const child = child_process.spawn("trap '' INT; sleep 10000000", [], {shell: true });
console.log(`Child pid: ${child.pid}`);

child.on('exit', (code, signal) => { console.log('Exit', code, signal); });

process.on('SIGINT', () => {
    console.log("Intercepting SIGINT");
});


//emulate cat to keep the process alive
process.stdin.pipe(process.stdout);

现在,当您按下Ctrl-C时,Node进程会处理它并且睡眠过程继续存在。 (如果您不熟悉其他终端生成的信号,如果您不介意coredump,可以通过按Ctrl- \(将SIGQUIT发送到组)轻松杀死该组。