在Node.js退出之前执行清理操作

446

我希望告诉Node.js在退出之前始终执行某些操作-无论是 Ctrl + C ,异常还是其他任何原因。

我尝试过这样做:

process.on('exit', function (){
    console.log('Goodbye!');
});

我开始了这个过程,然后停止了它,但是没有任何反应。我又开始了它,按下了Ctrl+C,但仍然没有任何反应...


1
请查看 https://github.com/nodejs/node/issues/20804。 - pravdomil
13个回答

684

更新:

你可以为 `process.on('exit')` 注册一个处理程序,并在任何其他情况(`SIGINT` 或未处理的异常)下调用 `process.exit()`。
process.stdin.resume(); // so the program will not close instantly

function exitHandler(options, exitCode) {
    if (options.cleanup) console.log('clean');
    if (exitCode || exitCode === 0) console.log(exitCode);
    if (options.exit) process.exit();
}

// do something when app is closing
process.on('exit', exitHandler.bind(null,{cleanup:true}));

// catches ctrl+c event
process.on('SIGINT', exitHandler.bind(null, {exit:true}));

// catches "kill pid" (for example: nodemon restart)
process.on('SIGUSR1', exitHandler.bind(null, {exit:true}));
process.on('SIGUSR2', exitHandler.bind(null, {exit:true}));

// catches uncaught exceptions
process.on('uncaughtException', exitHandler.bind(null, {exit:true}));

这只有在处理程序内调用同步代码时才起作用,否则它将无限调用处理程序。

6
有没有一种方法可以在同一个位置处理Ctrl+C和通常的退出,还是我必须编写两个不同的处理程序?其他类型的退出怎么样,例如未处理的异常 - 针对该情况有一个特定的处理程序,但我是否应该使用第三个相同处理程序来处理这种情况? - Erel Segal-Halevi
106
请注意,在退出处理程序中,您必须只执行同步操作。 - Lewis
4
我认为你应该使用beforeExit事件,而不是其他事件。 - Lewis
42
这个解决方案存在很多问题。(1)它不会向父进程报告信号。(2)它不会将退出代码传达给父进程。(3)它不允许像Emacs那样忽略Ctrl-C SIGINT的子进程。(4)它不允许异步清理。(5)它不能协调在多个清理处理程序中跨越单个stderr消息。我编写了一个模块,https://github.com/jtlapp/node-cleanup,可以解决所有这些问题,最初基于下面的cleanup.js解决方案,但根据反馈进行了大量修订。希望它能对您有所帮助。 - Joe Lapp
3
on("exit", ...)处理程序中,不能调用process.exit。该处理程序应在process.exit调用后调用。https://nodejs.org/api/process.html#process_event_exit 否则后果将非常严重,例如,所有错误都将被消音。 - Ivan Kleshnin
显示剩余16条评论

205
以下脚本允许针对所有退出条件使用单个处理程序。它使用应用程序特定的回调函数执行自定义清理代码。 cleanup.js
// Object to capture process exits and call app specific cleanup function

function noOp() {};

exports.Cleanup = function Cleanup(callback) {

  // attach user callback to the process event emitter
  // if no callback, it will still exit gracefully on Ctrl-C
  callback = callback || noOp;
  process.on('cleanup',callback);

  // do app specific cleaning before exiting
  process.on('exit', function () {
    process.emit('cleanup');
  });

  // catch ctrl+c event and exit normally
  process.on('SIGINT', function () {
    console.log('Ctrl-C...');
    process.exit(2);
  });

  //catch uncaught exceptions, trace, then exit normally
  process.on('uncaughtException', function(e) {
    console.log('Uncaught Exception...');
    console.log(e.stack);
    process.exit(99);
  });
};

这段代码截取未捕获的异常、Ctrl+C和正常退出事件。然后在退出前调用一个可选的用户清理回调函数,在一个对象中处理所有退出条件。

该模块只是扩展了进程对象,而不是定义另一个事件发射器。如果没有特定于应用程序的回调,则清除默认为无操作函数。在我使用Ctrl+C退出时,父进程仍在运行时,这已经足够了。

您可以根据需要轻松添加其他退出事件,如SIGHUP。注意:根据NodeJS手册,SIGKILL无法有监听器。下面的测试代码演示了使用cleanup.js的各种方式。

// test cleanup.js on version 0.10.21

// loads module and registers app specific cleanup callback...
var cleanup = require('./cleanup').Cleanup(myCleanup);
//var cleanup = require('./cleanup').Cleanup(); // will call noOp

// defines app specific callback...
function myCleanup() {
  console.log('App specific cleanup code...');
};

// All of the following code is only needed for test demo

// Prevents the program from closing instantly
process.stdin.resume();

// Emits an uncaught exception when called because module does not exist
function error() {
  console.log('error');
  var x = require('');
};

// Try each of the following one at a time:

// Uncomment the next line to test exiting on an uncaught exception
//setTimeout(error,2000);

// Uncomment the next line to test exiting normally
//setTimeout(function(){process.exit(3)}, 2000);

// Type Ctrl-C to test forced exit 

18
我已经发现这段代码非常重要,并创建了一个Node包,对其进行了修改并注明了你和这个SO回答的贡献。希望没问题,@CanyonCasa。谢谢!https://www.npmjs.com/package/node-cleanup - Joe Lapp
4
我喜欢清理工作,但我不喜欢process.exit(0);。我认为你应该让内核处理销毁。你不是以和SIGINT相同的方式退出。SIGINT并不会以2退出。你误将SIGINT与错误代码混淆了。它们并不相同。实际上,Ctrl+C以130结束。而不是2。http://www.tldp.org/LDP/abs/html/exitcodes.html - Banjocat
8
我已经重写了 https://www.npmjs.com/package/node-cleanup,使它能够与其他进程良好地处理 SIGINT 信号,根据 @Banjocat 的链接。现在,它正确地将信号传递给父进程,而不是调用 process.exit()。清理处理程序现在具有依据退出码或信号行为的灵活性,并且可以根据需要卸载清理处理程序,以支持异步清理或防止循环清理。现在它与上面的代码几乎没有相似之处。 - Joe Lapp
4
我忘了提到我还编写了(希望)全面的测试套件。 - Joe Lapp
1
不确定在退出回调中是否可以安全地执行 process.emit('cleanup');。Node.js文档中指出:"监听器函数只能执行同步操作。在调用'exit'事件监听器后,Node.js进程将立即退出,导致事件循环中仍排队的任何其他工作都将被放弃"。 - aromero
显示剩余3条评论

83

我找到了可以处理的每个退出事件。目前看来相当可靠和干净。

[`exit`, `SIGINT`, `SIGUSR1`, `SIGUSR2`, `uncaughtException`, `SIGTERM`].forEach((eventType) => {
  process.on(eventType, cleanUpServer.bind(null, eventType));
})

3
太棒了! - Andranik Hovesyan
1
非常棒的工作,我现在正在生产中使用它!非常感谢! - randy
希望能够获取这些事件的一些信息或资源链接。 - Sagiv b.g
@Sagivb.g 进程信号在每个*nix发行版中都有文档记录。请执行man kill - oligofren
3
请记住,在您的清理函数中,最后要调用process.exit()。原因是,您的函数将覆盖默认行为,例如杀死端口(请参阅Node文档:https://nodejs.org/api/process.html#process_signal_events)。 - Liran H
注意:'SIGUSR1' 是由 Node.js 保留用于启动调试器的信号。虽然可以安装一个监听器,但这样做可能会干扰调试器的正常工作。来源:https://nodejs.org/api/process.html#signal-events - undefined

24

"exit"事件是指节点在内部完成事件循环时触发的事件,而不是在您从外部终止进程时触发的事件。

您要寻找的是在SIGINT上执行某些操作。

文档中的http://nodejs.org/api/process.html#process_signal_events给出了一个例子:

监听SIGINT的示例:

// Start reading from stdin so we don't exit.
process.stdin.resume();

process.on('SIGINT', function () {
  console.log('Got SIGINT.  Press Control-D to exit.');
});

注意:这似乎会中断 SIGINT,当您完成代码时,您需要调用 process.exit()。

2
有没有一种方法可以在同一个地方处理Ctrl+C和通常的退出?还是我必须编写两个相同的处理程序? - Erel Segal-Halevi
只是提醒一下,如果您必须通过kill命令结束节点,则执行kill -2将传递SIGINT代码。我们必须以这种方式执行此操作,因为我们将节点记录到txt文件中,因此无法使用Ctrl + C。 - Aust

13
function fnAsyncTest(callback) {
    require('fs').writeFile('async.txt', 'bye!', callback);
}

function fnSyncTest() {
    for (var i = 0; i < 10; i++) {}
}

function killProcess() {

    if (process.exitTimeoutId) {
        return;
    }

    process.exitTimeoutId = setTimeout(() => process.exit, 5000);
    console.log('process will exit in 5 seconds');

    fnAsyncTest(function() {
        console.log('async op. done', arguments);
    });

    if (!fnSyncTest()) {
        console.log('sync op. done');
    }
}

// https://nodejs.org/api/process.html#process_signal_events
process.on('SIGTERM', killProcess);
process.on('SIGINT', killProcess);

process.on('uncaughtException', function(e) {

    console.log('[uncaughtException] app will be terminated: ', e.stack);

    killProcess();
    /**
     * @https://nodejs.org/api/process.html#process_event_uncaughtexception
     *  
     * 'uncaughtException' should be used to perform synchronous cleanup before shutting down the process. 
     * It is not safe to resume normal operation after 'uncaughtException'. 
     * If you do use it, restart your application after every unhandled exception!
     * 
     * You have been warned.
     */
});

console.log('App is running...');
console.log('Try to press CTRL+C or SIGNAL the process with PID: ', process.pid);

process.stdin.resume();
// just for testing

7
这个答案值得赞扬,但由于没有解释,也没有得到点赞。这个答案的显著之处在于,文档说,“监听函数必须只执行同步操作。Node.js进程在调用“exit”事件监听器后将立即退出,导致仍在事件循环中排队的任何额外工作都被放弃。”,而这个答案克服了这个限制! - xpt
如果我理解正确的话,退出将始终需要固定的时间(五秒钟),这可能太长或更糟,如果异步操作需要更多时间。 - Sebastian

10

async-exit-hook 看起来是处理这个问题最新的解决方案。它是 exit-hook 的一个分支/重写版本,在退出之前支持异步代码。


1
我最终使用了这个库,因为它允许我使用超时,这正是我需要优雅地停止的。 - Lorenzo Lapucci

9

我想提一下这里的death包:https://github.com/jprichardson/node-death

示例:

var ON_DEATH = require('death')({uncaughtException: true}); //this is intentionally ugly

ON_DEATH(function(signal, err) {
  //clean up code here
})

似乎你必须在回调函数中显式地使用 process.exit() 退出程序。这让我有些困惑。 - Nick Manning

5

我需要在退出时执行异步清理操作,但是这个问题中的所有答案都对我没用。

所以我自己尝试了一下,最终找到了这个:

process.once('uncaughtException', async () => {
  await cleanup()

  process.exit(0)
})

process.once('SIGINT', () => { throw new Error() })

1

在尝试其他答案后,这是我对此任务的解决方案。以这种方式实现可以帮助我将清理集中在一个地方,防止重复处理清理。

  1. 我想将所有其他退出代码路由到“退出”代码。
const others = [`SIGINT`, `SIGUSR1`, `SIGUSR2`, `uncaughtException`, `SIGTERM`]
others.forEach((eventType) => {
    process.on(eventType, exitRouter.bind(null, { exit: true }));
})
  1. exitRouter 的作用是调用 process.exit()。
function exitRouter(options, exitCode) {
   if (exitCode || exitCode === 0) console.log(`ExitCode ${exitCode}`);
   if (options.exit) process.exit();
}

在“退出”时,使用一个新函数来处理清理工作。
function exitHandler(exitCode) {
  console.log(`ExitCode ${exitCode}`);
  console.log('Exiting finally...')
}

process.on('exit', exitHandler)

为了演示目的,这是链接到我的gist。在文件中,我添加了一个setTimeout来模拟进程运行。
如果你运行node node-exit-demo.js并且什么都不做,那么2秒后,你会看到日志:
The service is finish after a while.
ExitCode 0
Exiting finally...

如果在服务完成之前终止,使用ctrl+C,你会看到:
^CExitCode SIGINT
ExitCode 0
Exiting finally...

发生的情况是Node进程最初以SIGINT代码退出,然后路由到process.exit()并最终以退出代码0退出。

0
对于那些运行npm脚本并希望在进程通过脚本行定义完成之前执行某个操作而不需要额外的文件定义的人,您可以只需添加一个分号即可(在Mac上进行了检查)。
例如:
"start": "npm run add-hosts-to-hosts-file && npm run start ; npm run clear-hosts-from-hosts file",

这涵盖了 Ctrl + C 的情况,也可能包括其他情况。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接