控制npm脚本中的Unix IPC信号



我正在为节点API实现优雅关闭。通常,在开发过程中,这个过程是使用常见的start脚本开始的,但我注意到这会导致一些令人讨厌的行为,我很惊讶,在我作为节点开发人员的3年左右时间里,我从未真正注意到这些行为。

为了在开发过程中开始关闭,我们只需在bash终端中点击ctrl+C,这当然会导致SIGINT被发送到npm进程及其所有子进程。我可以用process.on处理程序来捕捉这一点,从那里启动一个优雅的关闭——关闭新请求,等待现有请求完成,然后终止数据库连接,所有这些都很好。

然而,如果开发人员第二次点击ctrl+C,npm的行为会有所不同。它似乎在向sh进程发送一个SIGTERM,它最初用来调用我的start脚本。这导致sh进程打印出Terminated并退出,在不等待节点进程退出的情况下将终端的控制权返回给用户。

这真的很烦人,因为它给人的印象是节点进程已经停止,但当然没有。它将一直持续到关闭完成,或者它被SIGKILL或SIGQUIT之类的东西强制杀死。如果它碰巧将任何内容打印到控制台,它将直接在开发人员现在可能正在该终端中运行的任何其他内容的中间进行打印。

举个简单的例子,试试这个:

package.json:

{
"private": true,
"scripts": {
"start": "node index.js"
}
}

index.js:

async function waitForever() {
while(true) {
console.log('waiting...');
await new Promise((resolve) => {
setTimeout(resolve, 5000);
});
}
}
process.on('SIGINT', () => {
console.log('SIGINT recieved');
});
process.on('SIGTERM', () => {
console.log('SIGTERM recieved');
})
waitForever();

在终端中运行npm start,然后按ctrl+c一次。您将看到信号通过节点,但它当然不会退出。现在,再做一次。你会看到信号再次通过,但随后你会立即看到"Terminated"和shell提示。在找到节点进程的进程ID并用kill -9杀死它之前,您将每五秒钟看到一次waiting...消息。

我对这个例子做了更多的处理,看起来npm对此负有全部责任。如果将kill -2直接发送到npm进程两次,则shell进程将终止,节点进程将不会接收到SIGINT。

所以我有两个主要问题:

  1. 这里到底发生了什么?我是否遗漏了shell的工作原理,或者这是npm运行脚本中内置的某种功能?如果是,我在哪里可以找到有关这方面的信息?npm help run-script对此一无所知。

  2. 我知道start脚本在这样的项目中很常见,所以看起来其他人应该遇到这个问题。人们通常是如何应对的?我在谷歌上搜索了很多,很难找到一个明确的答案。

当然,这不是什么大不了的事。启动脚本只是为了确保在启动之前运行TS编译。我可以让开发人员在构建后直接在他们的shell中运行构建的应用程序,或者编写一个执行构建并在npm脚本之外启动的脚本。但如果不必这么做,那就太好了。

真的,我只是很困惑,希望能得到一些帮助。谢谢

要回答您的1-查看npm代码,这是处理SIGINT的预期行为。信号的第一次出现被传递给子进程,并为后续SIGINT附加一次性侦听器,这将立即终止npm父进程。你可以在这里看到代码。

我认为这是因为npm start只是开发阶段的简写,在出现信号处理错误的情况下,使用"手刹"立即终止进程是有意义的(不幸的是,正如你所发现的,即使在所有情况下也不起作用(。

我不知道2的答案,但不久前,在关于信号处理和npm start的各种npm问题中,对此进行了长时间的辩论。NPM的官方声明主要是npm start不能取代合适的流程经理(如主管或系统管理员(,不应在这样的生产环境中使用。

编辑:sripberger对2的回答:

我最终做了什么。shutdown是一个执行关机并返回promise的函数。第一个SIGINT将开始关闭,但无论关闭是否完成,第二个都将强制终止进程。这并不能阻止npm终止shell进程,但它确实确保了当这种情况发生时,节点进程会随之消亡:

process.once('SIGINT', () => {
console.log('nShutting down, please wait...');
// Begin the graceful shutdown.
shutdown().catch((err) => {
console.error('Could not shut down gracefully:', err);
});
// Attach a subsequent handler to force kill.
process.on('SIGINT', () => {
console.log('nProcess killed.');
process.exit(0);
});
});

当然,正如blami所指出的,不建议在生产中使用npm脚本来控制服务。这对于开发环境来说只是一种方便。

最新更新