Comparing version 6.0.0 to 6.0.1
427
lib/index.js
@@ -9,8 +9,9 @@ 'use strict'; | ||
manager: null, | ||
signals: { | ||
SIGINT: true, | ||
SIGQUIT: true, | ||
SIGTERM: true, | ||
SIGHUP: false | ||
}, | ||
signals: new Map([ | ||
['SIGINT', true], | ||
['SIGQUIT', true], | ||
['SIGTERM', true], | ||
['SIGHUP', false] | ||
]), | ||
listeners: new Map(), | ||
processExit: null | ||
@@ -20,301 +21,305 @@ }; | ||
internals.exit = async function (code) { | ||
internals.addExitHook = function (event, handler, prepend = false) { | ||
const manager = internals.manager; | ||
prepend ? process.prependListener(event, handler) : process.on(event, handler); | ||
internals.listeners.set(event, handler); | ||
}; | ||
if (!manager) { | ||
return; // exit processing was disabled | ||
} | ||
if (typeof code === 'number' && code > manager.exitCode) { | ||
manager.exitCode = code; | ||
} | ||
internals.teardownExitHooks = function () { | ||
if (!manager.exitTimer) { | ||
manager.exitTimer = setTimeout(() => { | ||
process.exit = internals.processExit; | ||
manager.state = 'timeout'; | ||
return internals.exit(255); | ||
}, manager.exitTimeout); | ||
for (const listener of internals.listeners) { | ||
process.removeListener(...listener); | ||
} | ||
if (manager.state === 'starting') { | ||
manager.state = 'startAborted'; | ||
return; | ||
} | ||
internals.listeners.clear(); | ||
internals.processExit = null; | ||
}; | ||
if (manager.state === 'startAborted') { // wait until started | ||
return; | ||
} | ||
if (manager.state === 'started') { | ||
exports.Manager = class { | ||
// change state to prestopped as soon as the first server is stopping | ||
for (const server of internals.manager.servers) { | ||
server.ext('onPreStop', internals.listenerStopHandler); | ||
} | ||
exitTimeout = 5000; | ||
servers; | ||
state; // 'starting', 'started', 'stopping', 'prestopped', 'stopped', 'startAborted', 'errored', 'timeout' | ||
exitTimer; | ||
exitCode = 0; | ||
active = true; | ||
try { | ||
await internals.stop({ timeout: manager.exitTimeout - 500 }); | ||
} | ||
catch (err) { | ||
exports.log('Server stop failed:', err.stack); | ||
} | ||
constructor(servers, options = {}) { | ||
return internals.exit(); | ||
} | ||
Hoek.assert(!internals.manager, 'Only one manager can be created'); | ||
if (manager.state === 'stopping') { // wait until stopped | ||
return; | ||
this.exitTimeout = options.exitTimeout || this.exitTimeout; | ||
this.servers = typeof servers[Symbol.iterator] === 'function' ? [...servers] : [servers]; | ||
internals.manager = this; | ||
} | ||
if (manager.state === 'prestopped') { | ||
if (manager.exitCode === 0) { | ||
return; // defer to prestop logic | ||
async start() { | ||
if (!this.state) { | ||
this._setupExitHooks(); | ||
} | ||
manager.state = 'errored'; | ||
} | ||
this.state = 'starting'; | ||
// Perform actual exit | ||
let startError = null; | ||
let active = []; | ||
internals.processExit(manager.exitCode); | ||
}; | ||
const safeStop = async (server) => { | ||
try { | ||
await server.stop(); | ||
} | ||
catch (err) { | ||
Bounce.rethrow(err, 'system'); | ||
} | ||
}; | ||
internals.abortHandler = function (event) { | ||
const safeStart = async (server) => { | ||
if (this.listenerCount(event) === 1) { | ||
return internals.exit(1); | ||
} | ||
}; | ||
// "atomic" start, which immediately stops servers on errors | ||
try { | ||
await server.start(); | ||
if (startError) { | ||
throw new Error('Start aborted'); | ||
} | ||
active.push(server); | ||
} | ||
catch (err) { | ||
Bounce.rethrow(err, 'system'); | ||
internals.gracefulHandler = function (event) { | ||
if (!startError) { | ||
startError = err; | ||
} | ||
if (this.listenerCount(event) === 1) { | ||
return internals.exit(0); | ||
} | ||
}; | ||
const stopping = active.concat(server); | ||
active = []; | ||
await Promise.all(stopping.map(safeStop)); | ||
} | ||
}; | ||
try { | ||
await Promise.all(this.servers.map(safeStart)); | ||
} | ||
finally { | ||
const aborted = (this.state === 'startAborted'); | ||
this.state = startError ? 'errored' : 'started'; | ||
internals.unhandledError = function (type, err) { | ||
if (aborted) { // Note that throw is not returned when aborted | ||
return this._exit(); // eslint-disable-line no-unsafe-finally | ||
} | ||
} | ||
if (err instanceof exports.ProcessExitError) { // Ignore ProcessExitError, since we are already handling it | ||
return; | ||
} | ||
if (startError) { | ||
throw startError; | ||
} | ||
exports.log(`Fatal ${type}:`, (err || {}).stack || err); | ||
// Attach close listeners to catch spurious closes | ||
if (internals.manager.state === 'stopping') { // Exceptions while stopping advance to error state immediately | ||
for (const server of this.servers) { | ||
server.listener.once('close', this._listenerClosedHandler.bind(this)); | ||
} | ||
internals.manager.state = 'errored'; | ||
return this; | ||
} | ||
return internals.exit(1); | ||
}; | ||
stop(options = {}) { | ||
Hoek.assert(this.state === 'started', 'Stop requires that server is started'); | ||
internals.uncaughtExceptionHandler = function (err) { | ||
return this._stop(options); | ||
} | ||
return internals.unhandledError('exception', err); | ||
}; | ||
deactivate() { | ||
if (this.active) { | ||
if (internals.processExit) { | ||
internals.teardownExitHooks(); | ||
} | ||
internals.unhandledRejectionHandler = function (err) { | ||
clearTimeout(this.exitTimer); | ||
internals.manager = undefined; | ||
return internals.unhandledError('rejection', err); | ||
}; | ||
this.active = false; | ||
} | ||
} | ||
// Private | ||
internals.listenerClosedHandler = function () { | ||
async _exit(code) { | ||
// If server is closed without stopping, exit with error | ||
if (!this.active) { | ||
return; | ||
} | ||
if (internals.manager && internals.manager.state === 'started') { | ||
return internals.exit(255); | ||
} | ||
}; | ||
if (typeof code === 'number' && code > this.exitCode) { | ||
this.exitCode = code; | ||
} | ||
if (!this.exitTimer) { | ||
this.exitTimer = setTimeout(() => { | ||
internals.listenerStopHandler = function (server) { | ||
this.state = 'timeout'; | ||
return this._exit(255); | ||
}, this.exitTimeout); | ||
} | ||
internals.manager.state = 'prestopped'; | ||
if (this.state === 'starting') { | ||
this.state = 'startAborted'; | ||
return; | ||
} | ||
if (internals.manager.exitCode !== 0) { | ||
throw new Error('Process aborted'); | ||
} | ||
}; | ||
if (this.state === 'startAborted') { // wait until started | ||
return; | ||
} | ||
if (this.state === 'started') { | ||
internals.stop = async function (options) { | ||
// change state to prestopped as soon as the first server is stopping | ||
for (const server of this.servers) { | ||
server.ext('onPreStop', this._listenerStopHandler.bind(this)); | ||
} | ||
const manager = internals.manager; | ||
try { | ||
await this._stop({ timeout: this.exitTimeout - 500 }); | ||
} | ||
catch (err) { | ||
this._log('Server stop failed:', err.stack); | ||
} | ||
try { | ||
manager.state = 'stopping'; | ||
await Promise.all(manager.servers.map((server) => server.stop(options))); | ||
manager.state = 'stopped'; | ||
} | ||
catch (err) { | ||
manager.state = 'errored'; | ||
throw err; | ||
} | ||
}; | ||
return this._exit(); | ||
} | ||
if (this.state === 'stopping') { // wait until stopped | ||
return; | ||
} | ||
internals.badExitCheck = function () { | ||
if (this.state === 'prestopped') { | ||
if (this.exitCode === 0) { | ||
return; // defer to prestop logic | ||
} | ||
const state = internals.manager.state; | ||
if (state !== 'stopped' && state !== 'errored' && state !== 'timeout') { | ||
exports.log('Process exiting without stopping server (state == ' + state + ')'); | ||
} | ||
}; | ||
this.state = 'errored'; | ||
} | ||
// Perform actual exit | ||
internals.setupExitHooks = function () { | ||
internals.processExit(this.exitCode); | ||
} | ||
process.on('uncaughtException', internals.uncaughtExceptionHandler); | ||
process.on('unhandledRejection', internals.unhandledRejectionHandler); | ||
_abortHandler(event) { | ||
for (const event in internals.signals) { | ||
let handler = internals.signals[event]; | ||
if (handler === true) { | ||
handler = internals.signals[event] = internals.gracefulHandler.bind(process, event); | ||
if (process.listenerCount(event) === 1) { | ||
return this._exit(1); | ||
} | ||
else if (handler === false) { | ||
handler = internals.signals[event] = internals.abortHandler.bind(process, event); | ||
} | ||
process.prependListener(event, handler); | ||
} | ||
process.on('beforeExit', internals.exit); | ||
process.on('exit', internals.badExitCheck); | ||
_gracefulHandler(event) { | ||
// Monkey patch process.exit() | ||
if (process.listenerCount(event) === 1) { | ||
return this._exit(0); | ||
} | ||
} | ||
internals.processExit = process.exit; | ||
process.exit = (code) => { | ||
_unhandledError(type, err) { | ||
internals.exit(code); | ||
if (err instanceof exports.ProcessExitError) { // Ignore ProcessExitError, since we are already handling it | ||
return; | ||
} | ||
// Since we didn't actually exit, throw an error to escape the current scope | ||
this._log(`Fatal ${type}:`, (err || {}).stack || err); | ||
throw new exports.ProcessExitError(); | ||
}; | ||
}; | ||
if (this.state === 'stopping') { // Exceptions while stopping advance to error state immediately | ||
this.state = 'errored'; | ||
} | ||
return this._exit(1); | ||
} | ||
internals.teardownExitHooks = function () { | ||
_uncaughtExceptionHandler(err) { | ||
process.exit = internals.processExit; | ||
for (const event in internals.signals) { | ||
process.removeListener(event, internals.signals[event]); | ||
return this._unhandledError('exception', err); | ||
} | ||
process.removeListener('beforeExit', internals.exit); | ||
process.removeListener('exit', internals.badExitCheck); | ||
process.removeListener('unhandledRejection', internals.unhandledRejectionHandler); | ||
process.removeListener('uncaughtException', internals.uncaughtExceptionHandler); | ||
_unhandledRejectionHandler(err) { | ||
internals.processExit = null; | ||
}; | ||
return this._unhandledError('rejection', err); | ||
} | ||
_listenerClosedHandler() { | ||
exports.Manager = class { | ||
// If server is closed without stopping, exit with error | ||
constructor(servers, options = {}) { | ||
if (this.state === 'started') { | ||
return this._exit(255); | ||
} | ||
} | ||
Hoek.assert(!internals.manager, 'Only one manager can be created'); | ||
_listenerStopHandler(/*server*/) { | ||
this.exitTimeout = options.exitTimeout || 5000; | ||
this.state = 'prestopped'; | ||
this.servers = Array.isArray(servers) ? servers : [servers]; | ||
if (this.exitCode !== 0) { | ||
throw new Error('Process aborted'); | ||
} | ||
} | ||
this.state = null; // ['starting', 'started', 'stopping', 'prestopped', 'stopped', 'startAborted', 'errored', 'timeout'] | ||
this.exitTimer = null; | ||
this.exitCode = 0; | ||
async _stop(options) { | ||
internals.manager = this; | ||
try { | ||
this.state = 'stopping'; | ||
await Promise.all(this.servers.map((server) => server.stop(options))); | ||
this.state = 'stopped'; | ||
} | ||
catch (err) { | ||
this.state = 'errored'; | ||
throw err; | ||
} | ||
} | ||
async start() { | ||
_badExitCheck() { | ||
if (!this.state) { | ||
internals.setupExitHooks(); | ||
if (this.state !== 'stopped' && this.state !== 'errored' && this.state !== 'timeout') { | ||
this._log('Process exiting without stopping server (state == ' + this.state + ')'); | ||
} | ||
} | ||
this.state = 'starting'; | ||
_setupExitHooks() { | ||
let startError = null; | ||
let active = []; | ||
internals.addExitHook('uncaughtException', this._uncaughtExceptionHandler.bind(this)); | ||
internals.addExitHook('unhandledRejection', this._unhandledRejectionHandler.bind(this)); | ||
const safeStop = async (server) => { | ||
for (const [event, graceful] of internals.signals) { | ||
const handler = graceful ? this._gracefulHandler.bind(this, event) : this._abortHandler.bind(this, event); | ||
internals.addExitHook(event, handler, true); | ||
} | ||
try { | ||
await server.stop(); | ||
} | ||
catch (err) { | ||
Bounce.rethrow(err, 'system'); | ||
} | ||
}; | ||
internals.addExitHook('beforeExit', this._exit.bind(this)); | ||
internals.addExitHook('exit', this._badExitCheck.bind(this)); | ||
const safeStart = async (server) => { | ||
// Monkey patch process.exit() | ||
// "atomic" start, which immediately stops servers on errors | ||
try { | ||
await server.start(); | ||
if (startError) { | ||
throw new Error('Start aborted'); | ||
} | ||
internals.processExit = process.exit; | ||
process.exit = (code) => { | ||
active.push(server); | ||
} | ||
catch (err) { | ||
Bounce.rethrow(err, 'system'); | ||
this._exit(code); | ||
if (!startError) { | ||
startError = err; | ||
} | ||
// Since we didn't actually exit, throw an error to escape the current scope | ||
const stopping = active.concat(server); | ||
active = []; | ||
await Promise.all(stopping.map(safeStop)); | ||
} | ||
throw new exports.ProcessExitError(); | ||
}; | ||
} | ||
_log(...args) { | ||
try { | ||
await Promise.all(this.servers.map(safeStart)); | ||
return exports.log(...args); | ||
} | ||
finally { | ||
const aborted = (this.state === 'startAborted'); | ||
this.state = startError ? 'errored' : 'started'; | ||
if (aborted) { // Note that throw is not returned when aborted | ||
return internals.exit(); // eslint-disable-line no-unsafe-finally | ||
} | ||
} | ||
if (startError) { | ||
throw startError; | ||
} | ||
// Attach close listeners to catch spurious closes | ||
this.servers.forEach((server) => { | ||
server.listener.once('close', internals.listenerClosedHandler); | ||
}); | ||
return this; | ||
catch {} | ||
} | ||
stop(options = {}) { | ||
Hoek.assert(this.state === 'started', 'Stop requires that server is started'); | ||
return internals.stop(options); | ||
} | ||
}; | ||
@@ -337,11 +342,5 @@ | ||
if (internals.processExit) { | ||
internals.teardownExitHooks(); | ||
} | ||
if (internals.manager) { | ||
clearTimeout(internals.manager.exitTimer); | ||
internals.manager.deactivate(); | ||
} | ||
internals.manager = null; | ||
}; | ||
@@ -348,0 +347,0 @@ |
{ | ||
"name": "exiting", | ||
"version": "6.0.0", | ||
"version": "6.0.1", | ||
"description": "Gracefully stop hapi.js servers", | ||
@@ -5,0 +5,0 @@ "main": "lib/index.js", |
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
15496
246
1