节点subprocess:如何拦截SIGINT等信号

在我的Node应用程序中,为了正常停止(使用pm2 ,但在这里不相关),我正在使用SIGINT信号。

我的应用程序也执行/产生了一些subprocess。

我可以钩住SIGINT拦截它,并执行优雅的停止, 但是我的subprocess通过相同的信号,因此,立即死亡。

我如何在我的subprocess上拦截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"); }); 

默认情况下,由child_process.spawn()创build的subprocess与父进程具有相同的进程组 ,除非使用{detached:true} 选项调用它们。

结果是,这个脚本在不同的环境中会有不同的performance:

 // 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将信号发送给父进程,但如果运行时间超过了,仍然是SIGKILLsubprocess。

我的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 

通常在C中,你可以通过忽略孩子中的信号来解决这个问题(或者通过在一个新的过程组中产生信号,使得前台处理组的terminal产生的信号没有达到它)。

https://nodejs.org/api/child_process.html#child_process_child_process_spawn_command_args_options看 ,它看起来并不像NodeJs公开的API,但是,它有一个通过shell产生subprocess的选项,所以你可以做的就是打开它,忽略shell中的信号,这会导致它的忽略状态被inheritance到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进程会处理它,睡眠进程将继续。 (如果你不熟悉其他terminal产生的信号,你可以通过按Ctrl- \(如果你不介意coredump,发送SIGQUIT到组)来轻易地杀死这个组。