diff --git a/README.md b/README.md index 427915a..cfb18d2 100644 --- a/README.md +++ b/README.md @@ -42,13 +42,13 @@ nodeCleanup(function (exitCode, signal) { If you only want to install your own messages for *Ctrl-C* and uncaught exception (either or both), you can do this: ```js -nodeCleanup(null, { +nodeCleanup({ ctrl_C: "{^C}", uncaughtException: "Uh oh. Look what happened:" }); ``` -To get just the default `stderr` messages, without installing a cleanup handler: +To get the default `stderr` messages, without installing a cleanup handler: ```js nodeCleanup(); @@ -104,19 +104,22 @@ nodeCleanup(function (exitCode, signal) { ### `nodeCleanup()` -`nodeCleanup()` has the following ([FlowType](https://flowtype.org/docs/getting-started.html#_)) signature: +`nodeCleanup()` has the following available ([FlowType](https://flowtype.org/docs/getting-started.html#_)) signatures: ```js -function nodeCleanup(cleanupHandler?: Function, messages?: object): void +function nodeCleanup(cleanupHandler: Function): void +function nodeCleanup(cleanupHandler: Function, stderrMessages: object): void +function nodeCleanup(stderrMessages: object): void +function nodeCleanup(): void ``` -`nodeCleanup()` installs a cleanup handler. It may also assign messages to write to `stderr` on SIGINT or an uncaught exception. Both parameters are optional. If not `cleanupHandler` is provided, the `stderr` messages are still written. If no `messages` are provided, default `stderr` messages are written. Calling `nodeCleanup()` with no parameters just installs these default messages. +The 1st form installs a cleanup handler. The 2nd form also assigns messages to write to `stderr` on SIGINT or an uncaught exception. The 3rd and 4th forms only assign messages to write to `stderr`, without installing a cleanup handler. The 4th form assigns default `stderr` messages. -`cleanupHandler` is a cleanup handler callback and is described in its own section below. When null or undefined, termination events all result in the process terminating, including signals. +`cleanupHandler` is a cleanup handler callback and is described in its own section below. When no cleanup handlers are installed, termination events all result in the process terminating, including signal events. -`messages` is an object mapping any of the keys `ctrl_C` and `uncaughtException` to message strings that output to `stderr`. Default messages are provided for omitted messages. Set a message to the empty string `''` inhibit the message. +`stderrMessages` is an object mapping any of the keys `ctrl_C` and `uncaughtException` to message strings that output to `stderr`. Set a message to the empty string `''` inhibit a previously-assigned message. -`nodeCleanup()` may be called multiple times to install multiple cleanup handlers. Each of these handlers runs for each signal or termination condition. The first call to `nodeCleanup()` establishes the `stderr` messages; messages passed to subsequent calls are ignored. +`nodeCleanup()` may be called multiple times to install multiple cleanup handlers or override previous messages. Each handler gets called on each signal or termination condition. The most recently assigned messages apply. ### `nodeCleanup.uninstall()` @@ -162,12 +165,11 @@ subtap ## Incompatibilities with v1.0.x -TBD +`node-cleanup` v2+ is not fully compatible with v1.x. You may need to change your usage to upgrade. These are the potential incompatibilities: -- default messages -- catches SIGHUP, SIGQUIT, and SIGTERM +- The cleanup handlers now also run on SIGHUP, SIGQUIT, and SIGTERM, which were not getting cleanup processing before. +- `stderr` messages are handled quite differently. Previously, there were defaults that you had to override, and only your first message assignments applied. Now, the defaults **only** install with the parameterless call `nodeCleanup()`. Otherwise there are no messages unless you provide them. Moreover, the most recent message assignments are the ones that get used. ## Credit This module began by borrowing and modifying code from CanyonCasa's [answer to a stackoverflow question](http://stackoverflow.com/a/21947851/650894). I had found the code necessary for all my node projects. @Banjocat piped in with a [comment](http://stackoverflow.com/questions/14031763/doing-a-cleanup-action-just-before-node-js-exits/21947851#comment68567869_21947851) about how the solution didn't properly handle SIGINT. (See [this detailed explanation](https://www.cons.org/cracauer/sigint.html) of the SIGINT problem). I have completely rewritten the module to properly deal with SIGINT and other signals (I hope!). The rewrite also provides some additional flexibility that @zixia and I found ourselves needing for our respective projects. - diff --git a/node-cleanup.js b/node-cleanup.js index 9d9f032..b6797d3 100644 --- a/node-cleanup.js +++ b/node-cleanup.js @@ -12,9 +12,17 @@ The process terminates after cleanup, except possibly on signals. If any cleanup Install a cleanup handler as follows: var nodeCleanup = require('node-cleanup'); - nodeCleanup(cleanupHandler, terminationMessages); + nodeCleanup(cleanupHandler, stderrMessages); + +Or to only install stderr messages: + + nodeCleanup(stderrMessages); + +Or to install the default stderr messages: -nodeCleanup() may be called multiple times to install multiple cleanup handlers. However, only the termination messages established by the first call get used. + nodeCleanup(); + +nodeCleanup() may be called multiple times to install multiple cleanup handlers. However, only the most recently installed stderr messages get used. The messages available are ctrl_C and uncaughtException. The following uninstalls all cleanup handlers and may be called multiple times in succession: @@ -25,13 +33,15 @@ This module has its origin in code by @CanyonCasa at http://stackoverflow.com/ //// CONSTANTS //////////////////////////////////////////////////////////////// -var DEFAULT_SIGINT_MSG = '[ctrl-C]'; -var DEFAULT_EXCEPTION_MSG = 'Uncaught exception...'; +var DEFAULT_MESSAGES = { + ctrl_C: '[ctrl-C]', + uncaughtException: 'Uncaught exception...' +}; //// CONFIGURATION //////////////////////////////////////////////////////////// var cleanupHandlers = null; // array of cleanup handlers to call -var exceptionMessage = null; // stderr message for uncaught exceptions +var messages = null; // messages to write to stderr var sigintHandler; // POSIX signal handlers var sighupHandler; @@ -40,7 +50,7 @@ var sigtermHandler; //// HANDLERS ///////////////////////////////////////////////////////////////// -function signalHandler(signal, message) +function signalHandler(signal) { var exit = true; cleanupHandlers.forEach(function (cleanup) { @@ -48,8 +58,8 @@ function signalHandler(signal, message) exit = false; }); if (exit) { - if (message !== '') - process.stderr.write(message + "\n"); + if (signal === 'SIGINT' && messages && messages.ctrl_C !== '') + process.stderr.write(messages.ctrl_C + "\n"); uninstall(); // don't cleanup again // necessary to communicate the signal to the parent process process.kill(process.pid, signal); @@ -58,8 +68,8 @@ function signalHandler(signal, message) function exceptionHandler(e) { - if (exceptionMessage !== '') - process.stderr.write(exceptionMessage + "\n"); + if (messages && messages.uncaughtException !== '') + process.stderr.write(messages.uncaughtException + "\n"); process.stderr.write(e.stack + "\n"); process.exit(1); // will call exitHandler() for cleanup } @@ -73,22 +83,33 @@ function exitHandler(exitCode, signal) //// MAIN ///////////////////////////////////////////////////////////////////// -function install(cleanupHandler, messages) +function install(cleanupHandler, stderrMessages) { + if (cleanupHandler) { + if (typeof cleanupHandler === 'object') { + stderrMessages = cleanupHandler; + cleanupHandler = null; + } + } + else if (!stderrMessages) + stderrMessages = DEFAULT_MESSAGES; + + if (stderrMessages) { + if (messages === null) + messages = { ctrl_C: '', uncaughtException: '' }; + if (typeof stderrMessages.ctrl_C === 'string') + messages.ctrl_C = stderrMessages.ctrl_C; + if (typeof stderrMessages.uncaughtException === 'string') + messages.uncaughtException = stderrMessages.uncaughtException; + } + if (cleanupHandlers === null) { cleanupHandlers = []; // establish before installing handlers - messages = messages || {}; - if (typeof messages.ctrl_C !== 'string') - messages.ctrl_C = DEFAULT_SIGINT_MSG; - if (typeof messages.uncaughtException !== 'string') - messages.uncaughtException = DEFAULT_EXCEPTION_MSG; - exceptionMessage = messages.uncaughtException; - - sigintHandler = signalHandler.bind(this, 'SIGINT', messages.ctrl_C); - sighupHandler = signalHandler.bind(this, 'SIGHUP', ''); - sigquitHandler = signalHandler.bind(this, 'SIGQUIT', ''); - sigtermHandler = signalHandler.bind(this, 'SIGTERM', ''); + sigintHandler = signalHandler.bind(this, 'SIGINT'); + sighupHandler = signalHandler.bind(this, 'SIGHUP'); + sigquitHandler = signalHandler.bind(this, 'SIGQUIT'); + sigtermHandler = signalHandler.bind(this, 'SIGTERM'); process.on('SIGINT', sigintHandler); process.on('SIGHUP', sighupHandler); diff --git a/package.json b/package.json index c1b8e4e..49e7560 100644 --- a/package.json +++ b/package.json @@ -1,10 +1,10 @@ { "name": "node-cleanup", - "version": "2.0.0", + "version": "2.1.0", "description": "installs cleanup handlers that always run on exiting node", "main": "node-cleanup.js", "scripts": { - "test": "echo \"Error: no test specified\" && exit 1" + "test": "tap tests/*.js" }, "repository": { "type": "git", diff --git a/tests/bin/stackable.js b/tests/bin/stackable.js index fb8d466..887aead 100755 --- a/tests/bin/stackable.js +++ b/tests/bin/stackable.js @@ -5,6 +5,7 @@ Child process that installs node-cleanup for testing zero, one, or multiple (two { handlers; number; // 0, 1, or 2 concurrent cleanup handlers + messages0: object|null; // messages argument for no-cleanup call, if any messages1: object|null; // messages argument for 1st nodeCleanup() call messages2: object|null; // messages argument for 2nd nodeCleanup() call return1: boolean; // return value of 1st cleanup handler @@ -47,8 +48,12 @@ function cleanup2(exitCode, signal) { //// MAIN ///////////////////////////////////////////////////////////////////// -if (config.handlers === 0) - nodeCleanup(); +if (config.handlers === 0) { + if (config.messages0) + nodeCleanup(config.messages0); + else + nodeCleanup(); +} else { nodeCleanup(cleanup1, config.messages1); if (config.handlers > 1) diff --git a/tests/multiple.js b/tests/multiple.js index e1bf796..74e1480 100644 --- a/tests/multiple.js +++ b/tests/multiple.js @@ -22,34 +22,15 @@ t.test("multiple handlers: normal exit", function (t) { }); }); -t.test("multiple handlers: uncaught exception - default message", function (t) { - lib.test(t, { - child: 'stackable', - handlers: 2, - messages1: null, - messages2: null, - return1: true, - return2: true, - exception: true, - uninstall: false - }, function (childPID) { - // no signal - }, { - exitReason: 1, - stdout: "cleanup1 cleanup2", - stderr: lib.DEFAULT_EXCEPTION_OUT - }); -}); - -t.test("multiple handlers: uncaught exception - custom message", function (t) { +t.test("multiple handlers: uncaught exception - custom messages", function (t) { lib.test(t, { child: 'stackable', handlers: 2, messages1: { - uncaughtException: "Look! A surprise!" + uncaughtException: "Not the surprise you're looking for." }, messages2: { - uncaughtException: "Not the surprise you're looking for." + uncaughtException: "Look! A surprise!" }, return1: true, return2: true, @@ -64,7 +45,57 @@ t.test("multiple handlers: uncaught exception - custom message", function (t) { }); }); -t.test("multiple handlers: child SIGINT - both heeded", function (t) { +t.test("multiple handlers: uncaught exception - removed message", + function (t) { + lib.test(t, { + child: 'stackable', + handlers: 2, + messages1: { + uncaughtException: "Not the surprise you're looking for." + }, + messages2: { + uncaughtException: "" + }, + return1: true, + return2: true, + exception: true, + uninstall: false + }, function (childPID) { + // no signal + }, { + exitReason: 1, + stdout: "cleanup1 cleanup2", + stderr: /tests[\/\\]bin[\/\\]stackable.js/ + }); + } +); + +t.test("multiple handlers: uncaught exception - added message", + function (t) { + lib.test(t, { + child: 'stackable', + handlers: 2, + messages1: { + ctrl_C: "{^C}}" + }, + messages2: { + uncaughtException: "Oops!" + }, + return1: true, + return2: true, + exception: true, + uninstall: false + }, function (childPID) { + // no signal + }, { + exitReason: 1, + stdout: "cleanup1 cleanup2", + stderr: /^Oops!/ + }); + } +); + +t.test("multiple handlers: uncaught exception - no message", function (t) { lib.test(t, { child: 'stackable', handlers: 2, @@ -72,64 +103,124 @@ t.test("multiple handlers: child SIGINT - both heeded", function (t) { messages2: null, return1: true, return2: true, - exception: false, + exception: true, uninstall: false }, function (childPID) { - process.kill(childPID, 'SIGINT'); + // no signal }, { - exitReason: 'SIGINT', + exitReason: 1, stdout: "cleanup1 cleanup2", - stderr: lib.DEFAULT_SIGINT_OUT + stderr: /tests[\/\\]bin[\/\\]stackable.js/ }); }); -t.test("multiple handlers: child SIGINT - first heeded", function (t) { - lib.test(t, { - child: 'stackable', - handlers: 2, - messages1: null, - messages2: null, - return1: true, - return2: false, - exception: false, - uninstall: false - }, function (childPID) { - process.kill(childPID, 'SIGINT'); - }, { - exitReason: 0, - stdout: "cleanup1 cleanup2", - stderr: "" - }); -}); +t.test("multiple handlers: child SIGINT - both heeded, custom messages", + function (t) { + lib.test(t, { + child: 'stackable', + handlers: 2, + messages1: { + ctrl_C: "{^C1}" + }, + messages2: { + ctrl_C: "{^C2}" + }, + return1: true, + return2: true, + exception: false, + uninstall: false + }, function (childPID) { + process.kill(childPID, 'SIGINT'); + }, { + exitReason: 'SIGINT', + stdout: "cleanup1 cleanup2", + stderr: "{^C2}\n" + }); + } +); + +t.test("multiple handlers: child SIGINT - first heeded, custom messages", + function (t) { + lib.test(t, { + child: 'stackable', + handlers: 2, + messages1: { + ctrl_C: "{^C1}" + }, + messages2: { + ctrl_C: "{^C2}" + }, + return1: true, + return2: false, + exception: false, + uninstall: false + }, function (childPID) { + process.kill(childPID, 'SIGINT'); + }, { + exitReason: 0, + stdout: "cleanup1 cleanup2", + stderr: "" + }); + } +); + +t.test("multiple handlers: child SIGINT - second heeded, custom messages", + function (t) { + lib.test(t, { + child: 'stackable', + handlers: 2, + messages1: { + ctrl_C: "{^C1}" + }, + messages2: { + ctrl_C: "{^C2}" + }, + return1: false, + return2: true, + exception: false, + uninstall: false + }, function (childPID) { + process.kill(childPID, 'SIGINT'); + }, { + exitReason: 0, + stdout: "cleanup1 cleanup2", + stderr: "" + }); + } +); -t.test("multiple handlers: child SIGINT - second heeded", function (t) { +t.test("multiple handlers: child SIGINT - removed message", function (t) { lib.test(t, { child: 'stackable', handlers: 2, - messages1: null, - messages2: null, - return1: false, + messages1: { + ctrl_C: "{^C1}" + }, + messages2: { + ctrl_C: "" + }, + return1: true, return2: true, exception: false, uninstall: false }, function (childPID) { process.kill(childPID, 'SIGINT'); }, { - exitReason: 0, + exitReason: 'SIGINT', stdout: "cleanup1 cleanup2", stderr: "" }); }); -t.test("multiple handlers: child SIGINT - custom message", function (t) { +t.test("multiple handlers: child SIGINT - added message", function (t) { lib.test(t, { child: 'stackable', handlers: 2, messages1: { - ctrl_C: "{^C1}" + uncaughtException: "Oops!" }, messages2: { - ctrl_C: "{^C2}" + ctrl_C: "{^C1}" }, return1: true, return2: true, @@ -224,8 +315,12 @@ t.test("multiple handlers/uninstall: uncaught exception", function (t) { lib.test(t, { child: 'stackable', handlers: 2, - messages1: null, - messages2: null, + messages1: { + uncaughtException: "Shouldn't show." + }, + messages2: { + uncaughtException: "Also shouldn't show." + }, return1: true, return2: true, exception: true, @@ -243,8 +338,12 @@ t.test("multiple handlers/uninstall: child SIGINT", function (t) { lib.test(t, { child: 'stackable', handlers: 2, - messages1: null, - messages2: null, + messages1: { + ctrl_C: "{^C1}" + }, + messages2: { + ctrl_C: "{^C2}" + }, return1: true, return2: true, exception: false, diff --git a/tests/nocleanup.js b/tests/nocleanup.js index 6f85f4c..61a4031 100644 --- a/tests/nocleanup.js +++ b/tests/nocleanup.js @@ -18,7 +18,7 @@ t.test("nocleanup: normal child exit", function (t) { }); }); -t.test("nocleanup: uncaught exception", function (t) { +t.test("nocleanup: uncaught exception - default message", function (t) { lib.test(t, { child: 'stackable', handlers: 0, @@ -33,7 +33,25 @@ t.test("nocleanup: uncaught exception", function (t) { }); }); -t.test("nocleanup: child SIGINT", function (t) { +t.test("nocleanup: uncaught exception - custom message", function (t) { + lib.test(t, { + child: 'stackable', + handlers: 0, + messages0: { + uncaughtException: "Yikes!" + }, + exception: true, + uninstall: false + }, function (childPID) { + // no signal + }, { + exitReason: 1, + stdout: "", + stderr: /^Yikes!/ + }); +}); + +t.test("nocleanup: child SIGINT - default message", function (t) { lib.test(t, { child: 'stackable', handlers: 0, @@ -48,6 +66,24 @@ t.test("nocleanup: child SIGINT", function (t) { }); }); +t.test("nocleanup: child SIGINT - custom message", function (t) { + lib.test(t, { + child: 'stackable', + handlers: 0, + messages0: { + ctrl_C: "{^C}" + }, + exception: false, + uninstall: false + }, function (childPID) { + process.kill(childPID, 'SIGINT'); + }, { + exitReason: 'SIGINT', + stdout: "", + stderr: "{^C}\n" + }); +}); + t.test("nocleanup: child SIGQUIT", function (t) { lib.test(t, { child: 'stackable', diff --git a/tests/single.js b/tests/single.js index 3d06e2f..dc8a53f 100644 --- a/tests/single.js +++ b/tests/single.js @@ -57,24 +57,6 @@ t.test("single: normal grandchild exit", function (t) { }); }); -t.test("single: uncaught exception - default message", function (t) { - lib.test(t, { - child: 'groupable', - grandchild: false, - grandchildHeedsSIGINT: false, - messages: null, - exception: true, - skipTermination: false, - exitReturn: 'true' - }, function (childPID) { - // no signal - }, { - exitReason: 1, - stdout: "cleanup", - stderr: lib.DEFAULT_EXCEPTION_OUT - }); -}); - t.test("single: uncaught exception - custom message", function (t) { lib.test(t, { child: 'groupable', @@ -115,12 +97,14 @@ t.test("single: uncaught exception - no message", function (t) { }); }); -t.test("single: child SIGINT - true return, default message", function (t) { +t.test("single: child SIGINT - true return, custom message", function (t) { lib.test(t, { child: 'groupable', grandchild: false, grandchildHeedsSIGINT: false, - messages: null, + messages: { + ctrl_C: "{^C}" + }, exception: false, skipTermination: false, exitReturn: 'true' @@ -129,17 +113,19 @@ t.test("single: child SIGINT - true return, default message", function (t) { }, { exitReason: 'SIGINT', stdout: "cleanup", - stderr: lib.DEFAULT_SIGINT_OUT + stderr: "{^C}\n" }); }); -t.test("single: child SIGINT - undefined return, default message", +t.test("single: child SIGINT - undefined return, custom message", function (t) { lib.test(t, { child: 'groupable', grandchild: false, grandchildHeedsSIGINT: false, - messages: null, + messages: { + ctrl_C: "{^C}" + }, exception: false, skipTermination: false, exitReturn: 'undefined' @@ -148,39 +134,17 @@ t.test("single: child SIGINT - undefined return, default message", }, { exitReason: 'SIGINT', stdout: "cleanup", - stderr: lib.DEFAULT_SIGINT_OUT + stderr: "{^C}\n" }); } ); -t.test("single: child SIGINT - custom message", function (t) { - lib.test(t, { - child: 'groupable', - grandchild: false, - grandchildHeedsSIGINT: false, - messages: { - ctrl_C: "{^C}" - }, - exception: false, - skipTermination: false, - exitReturn: 'true' - }, function (childPID) { - process.kill(childPID, 'SIGINT'); - }, { - exitReason: 'SIGINT', - stdout: "cleanup", - stderr: "{^C}\n" - }); -}); - t.test("single: child SIGINT - no message", function (t) { lib.test(t, { child: 'groupable', grandchild: false, grandchildHeedsSIGINT: false, - messages: { - ctrl_C: "" - }, + messages: null, exception: false, skipTermination: false, exitReturn: 'true' @@ -198,7 +162,9 @@ t.test("single: group SIGINT - grandchild ignores", function (t) { child: 'groupable', grandchild: true, grandchildHeedsSIGINT: false, - messages: null, + messages: { + ctrl_C: "{^C}" + }, exception: false, skipTermination: false, exitReturn: 'true' @@ -216,7 +182,9 @@ t.test("single: group SIGINT - grandchild heeds", function (t) { child: 'groupable', grandchild: true, grandchildHeedsSIGINT: true, - messages: null, + messages: { + ctrl_C: "{^C}" + }, exception: false, skipTermination: false, exitReturn: 'true' @@ -225,7 +193,7 @@ t.test("single: group SIGINT - grandchild heeds", function (t) { }, { exitReason: 'SIGINT', stdout: "skipped_cleanup grandchild=SIGINT cleanup", - stderr: lib.DEFAULT_SIGINT_OUT + stderr: "{^C}\n" }); });