├── .editorconfig ├── .gitattributes ├── .gitignore ├── .jshintrc ├── .travis.yml ├── CHANGELOG.md ├── index.js ├── license ├── package-lock.json ├── package.json ├── readme.md └── test ├── cases ├── async-err.js ├── async-exit-timeout.js ├── async.js ├── stub.js ├── sync.js └── unhandled-promise.js └── tests.js /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = tab 5 | end_of_line = lf 6 | charset = utf-8 7 | trim_trailing_whitespace = true 8 | insert_final_newline = true 9 | 10 | [package.json] 11 | indent_style = space 12 | indent_size = 2 13 | 14 | [*.md] 15 | trim_trailing_whitespace = false 16 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | 3 | /.idea 4 | 5 | coverage 6 | .nyc_output 7 | 8 | npm-debug.log 9 | -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "node": true, 3 | "esnext": true, 4 | "bitwise": true, 5 | "camelcase": true, 6 | "curly": true, 7 | "immed": true, 8 | "newcap": true, 9 | "noarg": true, 10 | "undef": true, 11 | "unused": "vars", 12 | "strict": true 13 | } 14 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | language: node_js 3 | node_js: 4 | - '8' 5 | - '6' 6 | - '4' 7 | before_install: 8 | - 'npm install -g npm@latest' 9 | after_success: 10 | - 'npm test && ./node_modules/.bin/nyc report --reporter=text-lcov | ./node_modules/.bin/coveralls' 11 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines. 4 | 5 | 6 | ## [2.0.1](https://github.com/tapppi/async-exit-hook/compare/v2.0.0...v2.0.1) (2017-08-03) 7 | 8 | 9 | 10 | 11 | # [2.0.0](https://github.com/tapppi/async-exit-hook/compare/v1.1.2...v2.0.0) (2017-08-03) 12 | 13 | 14 | ### Features 15 | 16 | * add unhandledRejectionHandler ([#3](https://github.com/tapppi/async-exit-hook/issues/3)) ([96a194f](https://github.com/tapppi/async-exit-hook/commit/96a194f)) 17 | 18 | 19 | ### BREAKING CHANGES 20 | 21 | * unhandledExceptionHandler no longer 22 | catches rejections. 23 | 24 | 25 | 26 | 27 | ## [1.1.2](https://github.com/tapppi/async-exit-hook/compare/v1.1.1...v1.1.2) (2017-03-29) 28 | 29 | 30 | ### Bug Fixes 31 | 32 | * filters are used individually for events [#1](https://github.com/tapppi/async-exit-hook/issues/1) ([03235c8](https://github.com/tapppi/async-exit-hook/commit/03235c8)) 33 | 34 | 35 | 36 | 37 | ## [1.1.1](https://github.com/tapppi/async-exit-hook/compare/v1.1.0...v1.1.1) (2016-11-04) 38 | 39 | 40 | ### Bug Fixes 41 | 42 | * unhandled rejections now handled ([4302b9e](https://github.com/tapppi/async-exit-hook/commit/4302b9e)) 43 | 44 | 45 | ### Chores 46 | 47 | * drop support for node 0.12 ([2830391](https://github.com/tapppi/async-exit-hook/commit/2830391)) 48 | 49 | 50 | ### BREAKING CHANGES 51 | 52 | * node 0.12 not tested anymore 53 | 54 | 55 | 56 | 57 | # [1.1.0](https://github.com/tapppi/async-exit-hook/compare/v1.0.0...v1.1.0) (2016-10-13) 58 | 59 | 60 | ### Features 61 | 62 | * support uncaughtRejectionHandler ([9098e3c](https://github.com/tapppi/async-exit-hook/commit/9098e3c)) 63 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const hooks = []; 4 | const errHooks = []; 5 | let called = false; 6 | let waitingFor = 0; 7 | let asyncTimeoutMs = 10000; 8 | 9 | const events = {}; 10 | const filters = {}; 11 | 12 | function exit(exit, code, err) { 13 | // Helper functions 14 | let doExitDone = false; 15 | 16 | function doExit() { 17 | if (doExitDone) { 18 | return; 19 | } 20 | doExitDone = true; 21 | 22 | if (exit === true) { 23 | // All handlers should be called even if the exit-hook handler was registered first 24 | process.nextTick(process.exit.bind(null, code)); 25 | } 26 | } 27 | 28 | // Async hook callback, decrements waiting counter 29 | function stepTowardExit() { 30 | process.nextTick(() => { 31 | if (--waitingFor === 0) { 32 | doExit(); 33 | } 34 | }); 35 | } 36 | 37 | // Runs a single hook 38 | function runHook(syncArgCount, err, hook) { 39 | // Cannot perform async hooks in `exit` event 40 | if (exit && hook.length > syncArgCount) { 41 | // Hook is async, expects a finish callback 42 | waitingFor++; 43 | 44 | if (err) { 45 | // Pass error, calling uncaught exception handlers 46 | return hook(err, stepTowardExit); 47 | } 48 | return hook(stepTowardExit); 49 | } 50 | 51 | // Hook is synchronous 52 | if (err) { 53 | // Pass error, calling uncaught exception handlers 54 | return hook(err); 55 | } 56 | return hook(); 57 | } 58 | 59 | // Only execute hooks once 60 | if (called) { 61 | return; 62 | } 63 | 64 | called = true; 65 | 66 | // Run hooks 67 | if (err) { 68 | // Uncaught exception, run error hooks 69 | errHooks.map(runHook.bind(null, 1, err)); 70 | } 71 | hooks.map(runHook.bind(null, 0, null)); 72 | 73 | if (waitingFor) { 74 | // Force exit after x ms (10000 by default), even if async hooks in progress 75 | setTimeout(() => { 76 | doExit(); 77 | }, asyncTimeoutMs); 78 | } else { 79 | // No asynchronous hooks, exit immediately 80 | doExit(); 81 | } 82 | } 83 | 84 | // Add a hook 85 | function add(hook) { 86 | hooks.push(hook); 87 | 88 | if (hooks.length === 1) { 89 | add.hookEvent('exit'); 90 | add.hookEvent('beforeExit', 0); 91 | add.hookEvent('SIGHUP', 128 + 1); 92 | add.hookEvent('SIGINT', 128 + 2); 93 | add.hookEvent('SIGTERM', 128 + 15); 94 | add.hookEvent('SIGBREAK', 128 + 21); 95 | 96 | // PM2 Cluster shutdown message. Caught to support async handlers with pm2, needed because 97 | // explicitly calling process.exit() doesn't trigger the beforeExit event, and the exit 98 | // event cannot support async handlers, since the event loop is never called after it. 99 | add.hookEvent('message', 0, function (msg) { // eslint-disable-line prefer-arrow-callback 100 | if (msg !== 'shutdown') { 101 | return true; 102 | } 103 | }); 104 | } 105 | } 106 | 107 | // New signal / event to hook 108 | add.hookEvent = function (event, code, filter) { 109 | events[event] = function () { 110 | const eventFilters = filters[event]; 111 | for (let i = 0; i < eventFilters.length; i++) { 112 | if (eventFilters[i].apply(this, arguments)) { 113 | return; 114 | } 115 | } 116 | exit(code !== undefined && code !== null, code); 117 | }; 118 | 119 | if (!filters[event]) { 120 | filters[event] = []; 121 | } 122 | 123 | if (filter) { 124 | filters[event].push(filter); 125 | } 126 | process.on(event, events[event]); 127 | }; 128 | 129 | // Unhook signal / event 130 | add.unhookEvent = function (event) { 131 | process.removeListener(event, events[event]); 132 | delete events[event]; 133 | delete filters[event]; 134 | }; 135 | 136 | // List hooked events 137 | add.hookedEvents = function () { 138 | const ret = []; 139 | for (const name in events) { 140 | if ({}.hasOwnProperty.call(events, name)) { 141 | ret.push(name); 142 | } 143 | } 144 | return ret; 145 | }; 146 | 147 | // Add an uncaught exception handler 148 | add.uncaughtExceptionHandler = function (hook) { 149 | errHooks.push(hook); 150 | 151 | if (errHooks.length === 1) { 152 | process.once('uncaughtException', exit.bind(null, true, 1)); 153 | } 154 | }; 155 | 156 | // Add an unhandled rejection handler 157 | add.unhandledRejectionHandler = function (hook) { 158 | errHooks.push(hook); 159 | 160 | if (errHooks.length === 1) { 161 | process.once('unhandledRejection', exit.bind(null, true, 1)); 162 | } 163 | }; 164 | 165 | // Configure async force exit timeout 166 | add.forceExitTimeout = function (ms) { 167 | asyncTimeoutMs = ms; 168 | }; 169 | 170 | // Export 171 | module.exports = add; 172 | -------------------------------------------------------------------------------- /license: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) Sindre Sorhus (sindresorhus.com) 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "async-exit-hook", 3 | "version": "2.0.1", 4 | "description": "Run some code when the process exits (supports async hooks and pm2 clustering)", 5 | "license": "MIT", 6 | "repository": { 7 | "type": "git", 8 | "url": "https://github.com/tapppi/async-exit-hook.git" 9 | }, 10 | "author": { 11 | "name": "Tapani Moilanen", 12 | "email": "moilanen.tapani@gmail.com", 13 | "url": "https://github.com/tapppi" 14 | }, 15 | "contributors": [ 16 | { 17 | "name": "Sindre Sorhus", 18 | "email": "sindresorhus@gmail.com", 19 | "url": "http://sindresorhus.com" 20 | } 21 | ], 22 | "engines": { 23 | "node": ">=0.12.0" 24 | }, 25 | "scripts": { 26 | "test": "xo && nyc ava", 27 | "release": "standard-version" 28 | }, 29 | "files": [ 30 | "index.js" 31 | ], 32 | "keywords": [ 33 | "exit", 34 | "quit", 35 | "process", 36 | "hook", 37 | "graceful", 38 | "handler", 39 | "shutdown", 40 | "sigterm", 41 | "sigint", 42 | "sighup", 43 | "pm2", 44 | "cluster", 45 | "child", 46 | "reload", 47 | "async", 48 | "terminate", 49 | "kill", 50 | "stop", 51 | "event" 52 | ], 53 | "devDependencies": { 54 | "ava": "^0.21.0", 55 | "coveralls": "^2.11.14", 56 | "nyc": "^10.3.2", 57 | "standard-version": "^4.2.0", 58 | "xo": "^0.18.2" 59 | }, 60 | "ava": { 61 | "files": [ 62 | "test/*.js", 63 | "!tests/cases/*" 64 | ] 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # async-exit-hook 2 | [![Build Status](https://api.travis-ci.org/Tapppi/async-exit-hook.svg)](https://travis-ci.org/Tapppi/async-exit-hook) 3 | [![Coverage Status](https://coveralls.io/repos/github/Tapppi/async-exit-hook/badge.svg?branch=master)](https://coveralls.io/github/Tapppi/async-exit-hook?branch=master) 4 | 5 | > Run some code when the process exits 6 | 7 | The `process.on('exit')` event doesn't catch all the ways a process can exit. This module catches: 8 | 9 | * process SIGINT, SIGTERM and SIGHUP, SIGBREAK signals 10 | * process beforeExit and exit events 11 | * PM2 clustering process shutdown message ([PM2 graceful reload](http://pm2.keymetrics.io/docs/usage/cluster-mode/#graceful-reload)) 12 | 13 | Useful for cleaning up. You can also include async handlers, and add custom events to hook and exit on. 14 | 15 | Forked and pretty much rewritten from [exit-hook](https://npmjs.com/package/exit-hook). 16 | 17 | 18 | ## Install 19 | 20 | ``` 21 | $ npm install --save async-exit-hook 22 | ``` 23 | 24 | ## Usage 25 | 26 | ### Considerations and warning 27 | #### On `process.exit()` and asynchronous code 28 | **If you use asynchronous exit hooks, DO NOT use `process.exit()` to exit. 29 | The `exit` event DOES NOT support asynchronous code.** 30 | >['beforeExit' is not emitted for conditions causing explicit termination, such as process.exit()] 31 | (https://nodejs.org/api/process.html#process_event_beforeexit) 32 | 33 | #### Windows and `process.kill(signal)` 34 | On windows `process.kill(signal)` immediately kills the process, and does not fire signal events, 35 | and as such, cannot be used to gracefully exit. See *Clustering and child processes* for a 36 | workaround when killing child processes. I'm planning to support gracefully exiting 37 | with async support on windows soon. 38 | 39 | ### Clustering and child processes 40 | If you use custom clustering / child processes, you can gracefully shutdown your child process 41 | by sending a shutdown message (`childProc.send('shutdown')`). 42 | 43 | ### Example 44 | ```js 45 | const exitHook = require('async-exit-hook'); 46 | 47 | exitHook(() => { 48 | console.log('exiting'); 49 | }); 50 | 51 | // you can add multiple hooks, even across files 52 | exitHook(() => { 53 | console.log('exiting 2'); 54 | }); 55 | 56 | // you can add async hooks by accepting a callback 57 | exitHook(callback => { 58 | setTimeout(() => { 59 | console.log('exiting 3'); 60 | callback(); 61 | }, 1000); 62 | }); 63 | 64 | // You can hook uncaught errors with uncaughtExceptionHandler(), consequently adding 65 | // async support to uncaught errors (normally uncaught errors result in a synchronous exit). 66 | exitHook.uncaughtExceptionHandler(err => { 67 | console.error(err); 68 | }); 69 | 70 | // You can hook unhandled rejections with unhandledRejectionHandler() 71 | exitHook.unhandledRejectionHandler(err => { 72 | console.error(err); 73 | }); 74 | 75 | // You can add multiple uncaught error handlers 76 | // Add the second parameter (callback) to indicate async hooks 77 | exitHook.uncaughtExceptionHandler((err, callback) => { 78 | sendErrorToCloudOrWhatever(err) // Returns promise 79 | .then(() => { 80 | console.log('Sent err to cloud'); 81 | }) 82 | .catch(sendError => { 83 | console.error('Error sending to cloud: ', err.stack); 84 | }) 85 | .then(() => callback); 86 | }); 87 | 88 | // Add exit hooks for a signal or custom message: 89 | 90 | // Custom signal 91 | // Arguments are `signal, exitCode` (SIGBREAK is already handled, this is an example) 92 | exitHook.hookEvent('SIGBREAK', 21); 93 | 94 | // process event: `message` with a filter 95 | // filter gets all arguments passed to *handler*: `process.on(message, *handler*)` 96 | // Exits on process event `message` with msg `customShutdownMessage` only 97 | exitHook.hookEvent('message', 0, msg => msg !== 'customShutdownMessage'); 98 | 99 | // All async hooks will work with uncaught errors when you have specified an uncaughtExceptionHandler 100 | throw new Error('awesome'); 101 | 102 | //=> // Sync uncaughtExcpetion hooks called and retun 103 | //=> '[Error: awesome]' 104 | //=> // Sync hooks called and retun 105 | //=> 'exiting' 106 | //=> 'exiting 2' 107 | //=> // Async uncaughtException hooks return 108 | //=> 'Sent error to cloud' 109 | //=> // Sync uncaughtException hooks return 110 | //=> 'exiting 3' 111 | ``` 112 | 113 | 114 | ## License 115 | 116 | MIT © Tapani Moilanen 117 | MIT © [Sindre Sorhus](http://sindresorhus.com) 118 | -------------------------------------------------------------------------------- /test/cases/async-err.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const exitHook = require('./../../index'); 3 | const stub = require('./stub'); 4 | 5 | exitHook(cb => { 6 | setTimeout(() => { 7 | stub.called(); 8 | cb(); 9 | }, 50); 10 | stub.called(); 11 | }); 12 | 13 | exitHook(() => { 14 | stub.called(); 15 | }); 16 | 17 | exitHook.uncaughtExceptionHandler((err, cb) => { 18 | setTimeout(() => { 19 | stub.called(); 20 | cb(); 21 | }, 50); 22 | if (!err || err.message !== 'test') { 23 | stub.reject('No error passed to uncaughtExceptionHandler, or message not test - '); 24 | } 25 | stub.called(); 26 | }); 27 | 28 | process.on('uncaughtException', () => { 29 | // All uncaught exception handlers should be called even though the exit hook handler was registered 30 | stub.called(); 31 | }); 32 | 33 | stub.addCheck(6); 34 | 35 | throw new Error('test'); 36 | -------------------------------------------------------------------------------- /test/cases/async-exit-timeout.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const exitHook = require('./../../index'); 3 | const stub = require('./stub'); 4 | 5 | exitHook(cb => { 6 | setTimeout(() => { 7 | stub.called(); 8 | cb(); 9 | }, 2000); 10 | stub.called(); 11 | }); 12 | 13 | exitHook(() => { 14 | stub.called(); 15 | }); 16 | 17 | // eslint-disable-next-line handle-callback-err 18 | exitHook.uncaughtExceptionHandler((err, cb) => { 19 | setTimeout(() => { 20 | stub.called(); 21 | cb(); 22 | }, 2000); 23 | stub.called(); 24 | }); 25 | 26 | exitHook.forceExitTimeout(500); 27 | stub.addCheck(3); 28 | 29 | throw new Error('test'); 30 | -------------------------------------------------------------------------------- /test/cases/async.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const exitHook = require('./../../index'); 3 | const stub = require('./stub'); 4 | 5 | exitHook(cb => { 6 | setTimeout(() => { 7 | stub.called(); 8 | cb(); 9 | }, 50); 10 | stub.called(); 11 | }); 12 | 13 | exitHook(() => { 14 | stub.called(); 15 | }); 16 | 17 | stub.addCheck(3); 18 | -------------------------------------------------------------------------------- /test/cases/stub.js: -------------------------------------------------------------------------------- 1 | // Stub to make sure that the required callbacks are called by exit-hook 2 | 'use strict'; 3 | let c = 0; 4 | let noCallback = true; 5 | 6 | // Increment the called count 7 | exports.called = () => { 8 | c++; 9 | }; 10 | 11 | // Exit with error 12 | exports.reject = (s, code) => { 13 | process.stdout.write('FAILURE: ' + s); 14 | // eslint-disable-next-line unicorn/no-process-exit 15 | process.exit(code === null || code === undefined ? 1 : code); 16 | }; 17 | 18 | // Exit with success 19 | exports.done = () => { 20 | process.stdout.write('SUCCESS'); 21 | // eslint-disable-next-line unicorn/no-process-exit 22 | process.exit(0); 23 | }; 24 | 25 | // Add the exit check with a specific expected called count 26 | exports.addCheck = num => { 27 | noCallback = false; 28 | 29 | // Only call exit once, and save uncaught errors 30 | let called = false; 31 | let ucErrStr; 32 | 33 | // Save errors that do not start with 'test' 34 | process.on('uncaughtException', err => { 35 | if (err.message.indexOf('test') !== 0) { 36 | ucErrStr = err.stack; 37 | } 38 | }); 39 | // Save rejections that do not start with 'test' 40 | process.on('unhandledRejection', reason => { 41 | if ((reason.message || reason).indexOf('test') !== 0) { 42 | ucErrStr = reason.message || reason; 43 | } 44 | }); 45 | 46 | // Check that there were no unexpected errors and all callbacks were called 47 | function onExitCheck(timeout) { 48 | if (called) { 49 | return; 50 | } 51 | called = true; 52 | 53 | if (timeout) { 54 | exports.reject('Test timed out'); 55 | } else if (ucErrStr) { 56 | exports.reject(ucErrStr); 57 | } else if (c === num) { 58 | exports.done(); 59 | } else { 60 | exports.reject('Expected ' + num + ' callback calls, but ' + c + ' received'); 61 | } 62 | } 63 | 64 | process.once('exit', onExitCheck.bind(null, null)); 65 | setTimeout(onExitCheck.bind(null, true), 10000); 66 | }; 67 | 68 | // If the check isn't added, throw on exit 69 | process.once('exit', () => { 70 | if (noCallback) { 71 | exports.reject('FAILURE, CHECK NOT ADDED'); 72 | } 73 | }); 74 | -------------------------------------------------------------------------------- /test/cases/sync.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const stub = require('./stub'); 3 | const exitHook = require('./../../index'); 4 | 5 | exitHook(() => { 6 | stub.called(); 7 | }); 8 | 9 | exitHook(() => { 10 | stub.called(); 11 | }); 12 | 13 | process.on('exit', () => { 14 | stub.called(); 15 | }); 16 | 17 | stub.addCheck(3); 18 | -------------------------------------------------------------------------------- /test/cases/unhandled-promise.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const exitHook = require('./../../index'); 3 | const stub = require('./stub'); 4 | 5 | exitHook(cb => { 6 | setTimeout(() => { 7 | stub.called(); 8 | cb(); 9 | }, 50); 10 | stub.called(); 11 | }); 12 | 13 | exitHook(() => { 14 | stub.called(); 15 | }); 16 | 17 | exitHook.unhandledRejectionHandler((err, cb) => { 18 | setTimeout(() => { 19 | stub.called(); 20 | cb(); 21 | }, 50); 22 | if (!err || err.message !== 'test-promise') { 23 | stub.reject(`No error passed to unhandledRejectionHandler, or message not test-promise - ${err.message}`); 24 | } 25 | stub.called(); 26 | }); 27 | 28 | process.on('unhandledRejection', () => { 29 | // All uncaught rejection handlers should be called even though the exit hook handler was registered 30 | stub.called(); 31 | }); 32 | 33 | stub.addCheck(6); 34 | 35 | (() => { 36 | return Promise.reject(new Error('test-promise')); 37 | })(); 38 | -------------------------------------------------------------------------------- /test/tests.js: -------------------------------------------------------------------------------- 1 | // Tests have to happen in a subprocess to test the exit functionality 2 | 'use strict'; 3 | 4 | const fork = require('child_process').fork; 5 | const path = require('path'); 6 | 7 | const test = require('ava'); 8 | 9 | /** 10 | * Starts a test file in a subprocess, returns a promise that resolves with the subprocess 11 | * exit code and output in an array ([code, output]) 12 | * 13 | * @async 14 | * @param {String} test Filename without path or extension 15 | * @param {String} signal Signal (or 'shutdown' for message) to send to the process 16 | * @return {Promise.<[Number, String]>} Array with the exit code and output of the subprocess 17 | */ 18 | function testInSub(test, signal) { 19 | return new Promise(resolve => { 20 | const proc = fork( 21 | path.resolve(__dirname, './cases/' + test + '.js'), 22 | { 23 | env: process.env, 24 | silent: true 25 | } 26 | ); 27 | 28 | let output = ''; 29 | 30 | proc.stdout.on('data', data => { 31 | output += data.toString(); 32 | }); 33 | 34 | proc.stderr.on('data', data => { 35 | output += data.toString(); 36 | }); 37 | 38 | proc.on('exit', code => { 39 | resolve([code, output]); 40 | }); 41 | 42 | if (signal === 'shutdown') { 43 | proc.send(signal); 44 | } else if (signal) { 45 | proc.kill(signal); 46 | } 47 | }); 48 | } 49 | 50 | test('API: test adding and removing and listing hooks', t => { 51 | const exitHook = require('./../'); 52 | 53 | t.plan(3); 54 | 55 | // Enable hooks 56 | exitHook(() => {}); 57 | 58 | // Ensure SIGBREAK hook 59 | exitHook.hookEvent('SIGBREAK', 128 + 21); 60 | t.not(exitHook.hookedEvents().indexOf('SIGBREAK'), -1); 61 | 62 | // Unhook SIGBREAK 63 | exitHook.unhookEvent('SIGBREAK'); 64 | t.is(exitHook.hookedEvents().indexOf('SIGBREAK'), -1); 65 | 66 | // Rehook SIGBREAK 67 | exitHook.hookEvent('SIGBREAK', 128 + 21); 68 | t.not(exitHook.hookedEvents().indexOf('SIGBREAK'), -1); 69 | }); 70 | 71 | test('sync handlers', async t => { 72 | t.plan(2); 73 | const [code, output] = await testInSub('sync', 'shutdown'); 74 | 75 | t.is(output, 'SUCCESS'); 76 | t.is(code, 0); 77 | }); 78 | 79 | test('async handlers', async t => { 80 | t.plan(2); 81 | const [code, output] = await testInSub('async', 'shutdown'); 82 | 83 | t.is(output, 'SUCCESS'); 84 | t.is(code, 0); 85 | }); 86 | 87 | test('async uncaught exception handler', async t => { 88 | t.plan(2); 89 | const [code, output] = await testInSub('async-err'); 90 | 91 | t.is(output, 'SUCCESS'); 92 | t.is(code, 0); 93 | }); 94 | 95 | test('async exit timeout', async t => { 96 | t.plan(2); 97 | const [code, output] = await testInSub('async-exit-timeout'); 98 | 99 | t.is(output, 'SUCCESS'); 100 | t.is(code, 0); 101 | }); 102 | 103 | test('unhandled promise rejection', async t => { 104 | t.plan(2); 105 | const [code, output] = await testInSub('unhandled-promise'); 106 | 107 | t.is(output, 'SUCCESS'); 108 | t.is(code, 0); 109 | }); 110 | --------------------------------------------------------------------------------