pax_global_header00006660000000000000000000000064141403115320014504gustar00rootroot0000000000000052 comment=6e714395abb3f63b14b06e25992489b567ceb75e node-exeunt-1.1.2/000077500000000000000000000000001414031153200137405ustar00rootroot00000000000000node-exeunt-1.1.2/.eslintignore000066400000000000000000000000511414031153200164370ustar00rootroot00000000000000deps/* build/* node_modules/* examples/* node-exeunt-1.1.2/.eslintrc000066400000000000000000000100331414031153200155610ustar00rootroot00000000000000{ // 0 = off, 1 = warn, 2 = err "rules": { "array-bracket-spacing": [2, "never"], "block-scoped-var": 2, "brace-style": 2, "callback-return": [2, ["callback", "cb", "next"]], "comma-dangle": [2, "never"], "comma-spacing": [2, {"before": false, "after": true}], "comma-style": [2, "last"], "complexity": [1, 6], "computed-property-spacing": [2, "never"], "consistent-return": 2, "consistent-this": [2, "self"], "curly": [2, "all"], "default-case": 2, "dot-location": [2, "property"], "dot-notation": [2, {"allowKeywords": true, "allowPattern": ""}], "eqeqeq": [2, "smart"], "eol-last": 2, "func-names": 2, "func-style": [2, "declaration"], "global-require": 2, "guard-for-in": 2, "handle-callback-err": [2, "^.*(e|E)rr" ], "indent": [2, 4, {"SwitchCase": 1}], "key-spacing": [2, {"beforeColon": false, "afterColon": true}], "linebreak-style": [2, "unix"], "lines-around-comment": [2, { "beforeBlockComment": true, "allowBlockStart": true }], "max-depth": [2, 5], "max-nested-callbacks": [2, 5], "max-len": [2, 80, 8, {"ignoreUrls": true}], "new-parens": 2, "newline-after-var": [2, "always"], "no-array-constructor": 2, "no-case-declarations": 2, "no-cond-assign": [2, "always"], "no-console": 1, "no-constant-condition": 2, "no-dupe-args": 2, "no-dupe-keys": 2, "no-duplicate-case": 2, "no-else-return": 2, "no-empty": 2, "no-empty-pattern": 2, "no-eq-null": 2, "no-eval": 2, "no-extend-native":2, "no-extra-bind": 2, "no-extra-semi": 2, "no-floating-decimal": 2, "no-implicit-coercion": 2, "no-inner-declarations": 2, "no-irregular-whitespace": 2, "no-labels": 2, "no-label-var": 2, "no-loop-func": 2, "no-lone-blocks": 2, "no-lonely-if": 2, "no-magic-numbers": [2, {ignore: [-1, 0, 1]}], "no-mixed-requires": 2, "no-mixed-spaces-and-tabs": 2, "no-multi-spaces": [2, { exceptions: { "Property": false } }], "no-multiple-empty-lines": [2, {"max": 2}], "no-multi-str": 2, "no-negated-condition": 2, "no-nested-ternary": 2, "no-new": 2, "no-new-func": 2, "no-new-object": 2, "no-new-require": 2, "no-new-wrappers": 2, "no-param-reassign": 2, "no-proto": 2, "no-return-assign": [2, "always"], "no-self-compare": 2, "no-sequences": 2, "no-shadow": 2, "no-shadow-restricted-names": 2, "no-spaced-func": 2, "no-throw-literal": 2, "no-trailing-spaces": 2, "no-undef-init": 2, "no-undefined": 2, "no-unneeded-ternary": 2, "no-unused-expressions": 2, "no-unused-vars": 2, "no-use-before-define": 2, "no-useless-call": 2, "no-useless-concat": 2, "no-warning-comments": [1, { "terms": ["todo", "fixme", "xxx"], "location": "start" }], "no-with": 2, "object-curly-spacing": [2, "never"], "one-var": [2, "never"], "operator-linebreak": [2, "before"], "padded-blocks": [2, "never"], "radix": 2, "quote-props": [2, "as-needed"], "quotes": [2, "single"], "semi": [2, "always"], "semi-spacing": 2, "sort-vars": [2, {"ignoreCase": true}], "keyword-spacing": [2, {"after": true, "before": true}], "space-before-blocks": 2, "space-before-function-paren": [2, { "anonymous": "always", "named": "never" }], "space-in-parens": [2, "never"], "space-infix-ops": 2, "space-unary-ops": 2, "spaced-comment": [2, "always"], "vars-on-top": 2, "wrap-iife": [2, "inside"], "wrap-regex": 2 }, "env": { "node": true }, "extends": "eslint:recommended" } node-exeunt-1.1.2/.gitignore000066400000000000000000000000601414031153200157240ustar00rootroot00000000000000/node_modules /tmp /npm-debug.log /exeunt-*.tgz node-exeunt-1.1.2/.npmignore000066400000000000000000000001201414031153200157300ustar00rootroot00000000000000/node_modules /tmp /test /examples /npm-debug.log /deps /Makefile /exeunt-*.tgz node-exeunt-1.1.2/CHANGES.md000066400000000000000000000004051414031153200153310ustar00rootroot00000000000000# node-exeunt changelog ## not yet released (nothing yet) ## 1.1.1 - Exclude "examples/" dir from the npm-installed package. ## 1.1.0 - Export `softExit()` function for those that might need it. - Improve discussion in readme. ## 1.0.0 - First release. node-exeunt-1.1.2/Makefile000066400000000000000000000026101414031153200153770ustar00rootroot00000000000000# # Copyright (c) 2017, Joyent, Inc. # # Makefile for node-exeunt # ESLINT = ./node_modules/.bin/eslint # # Targets # .PHONY: all all: npm install check:: check-version check-eslint # Ensure CHANGES.md and package.json have the same version. .PHONY: check-version check-version: @echo version is: $(shell cat package.json | json version) [[ `cat package.json | json version` == `grep '^## ' CHANGES.md | head -2 | tail -1 | awk '{print $$2}'` ]] $(ESLINT): npm install .PHONY: check-eslint check-eslint: $(ESLINT) @$< ./ .PHONY: cutarelease cutarelease: check-version [[ -z `git status --short` ]] # If this fails, the working dir is dirty. @which json 2>/dev/null 1>/dev/null && \ ver=$(shell json -f package.json version) && \ name=$(shell json -f package.json name) && \ publishedVer=$(shell npm view -j $(shell json -f package.json name)@$(shell json -f package.json version) version 2>/dev/null) && \ if [[ -n "$$publishedVer" ]]; then \ echo "error: $$name@$$ver is already published to npm"; \ exit 1; \ fi && \ echo "** Are you sure you want to tag and publish $$name@$$ver to npm?" && \ echo "** Enter to continue, Ctrl+C to abort." && \ read ver=$(shell cat package.json | json version) && \ date=$(shell date -u "+%Y-%m-%d") && \ git tag -a "v$$ver" -m "version $$ver ($$date)" && \ git push --tags origin && \ npm publish node-exeunt-1.1.2/README.md000066400000000000000000000237431414031153200152300ustar00rootroot00000000000000# node-exeunt A module for (and discussion on) exiting a node.js process *and flushing stdout and stderr*. Somewhere in the node.js 0.10 or 0.12 version range, and at least on certain platforms including macOS and SmartOS, stdout and stderr stopped being blocking. That means that where with node.js 0.10 or before your script might write output and exit with `process.exit([CODE])`, with newer versions of node.js your output to stdout and/or stderr *would sometimes not all get written* before the process exited. This is most commonly an annoyance for command-line tools written in node.js, especially when used in a pipeline where the problem more often manifests itself. The issue is surprisingly (at least to me) complex. This repo will attempt to explain the tradeoffs with different solutions and provide advice and one or more functions to use for exiting. ## Usage ```javascript var exeunt = require('exeunt'); function main() { // ... exeunt(code); // flush stdout/stderr and exit return; // `exeunt` returns, unlike `process.exit` } ``` See the [Solution 4](#solution-4-exeunt) section below for details. Note: `exeunt()` is a small function. If you don't want yet another node dependency, then feel free to just copy it to your repo. ## The problem A [node.js script](https://github.com/joyent/node-exeunt/blob/master/examples/write-65k-and-exit.js) writes a lot of output (such that buffering occurs), [and then exits](https://github.com/joyent/node-exeunt/blob/master/examples/write-65k-and-exit.js#L15). Not all output will be written before the process terminates. E.g.: ``` $ node examples/write-65k-and-exit.js | grep meta [meta] start: writing 66560 bytes... # 65k of output elided by the `grep` [meta] done # all output was emitted this time $ node examples/write-65k-and-exit.js | grep meta [meta] start: writing 66560 bytes... # the final 'done' line is missing ``` This example writes 65k to be more than the buffer size for a pipe (which is 64k, at least on macOS, IIUC). If we increase that to ~1MB, it is more frequent that output is truncated: ``` $ node examples/write-65k-and-exit.js 1000000 | grep meta [meta] start: writing 1000000 bytes... ``` ## Solution 1: avoid process.exit Summary: Use `process.exitCode = code;` (added in node.js 0.12), do *not* use `process.exit([code])`, and ensure you have no active handles (`process._getActiveHandles()`). Pros: - All stdout and stderr content will be written before the node.js process exits. AFAIK this is the only solution that guarantees this. Cons: - You need to be diligent about closing active handles (from `setTimeout`, `setInterval`, open sockets, etc.) otherwise your script will hang on exit. - In node 0.10 (if you need to support it), there is no way to exit with a non-zero exit code without `process.exit(code)`. [Example](https://github.com/joyent/node-exeunt/blob/master/examples/hang-because-active-handle.js) showing an accidental hang on exit: ``` $ node examples/hang-because-active-handle.js | grep meta [meta] start: writing 66560 bytes... [meta] done [meta] this interval is still running [meta] this interval is still running [meta] this interval is still running ^C ``` If you need to support node 0.10, [here is a `softExit()` function](https://github.com/joyent/node-exeunt/blob/master/lib/exeunt.js#L26-L56) that will use `process.exitCode` if the node version supports it, else fallback to `process.exit` if necessary (with the potential for truncation). ## Solution 2: give it a few seconds, then play hardball Summary: Attempt to avoid process.exit, but set a timer to use it after a short while if it looks like we are hanging. Pros: - In correct operation, your script will write out all stdout/stderr before exiting. Cons: - If stdout/stderr takes more than 2s (or whatever timeout you choose) to flush, then output will still be truncated. This is the main tradeoff to avoid a hang. - This technique involves calling a function that doesn't synchronously exit the process like `process.exit()` does. That means you need to handle it returning and code still executing. That might be as simple as calling `return;`, or it might be more difficult. It depends on your application's code. [Example](https://github.com/joyent/node-exeunt/blob/master/examples/hardball-after-2s.js#L25-L33): ``` $ node examples/hardball-after-2s.js | grep meta [meta] start: writing 66560 bytes... [meta] done [meta] this interval is still running [meta] this interval is still running [meta] hardball exit, you had your chance ``` ## Solution 3: set stdout/stderr to be blocking This all started because stdout/stderr weren't blocking. Let's just set them to be blocking again. Pros: - Stdout and stderr will be flushed as soon as your script writes to them. Cons: - The *node event loop can block* if the other end of those pipes isn't reading! This was a subtlety that surprised me. See for an example showing this. (TODO: include those scripts in examples/ here.) [Example](https://github.com/joyent/node-exeunt/blob/master/examples/set-blocking-write-65k-and-exit.js#L8-L10): ``` $ node examples/set-blocking-write-65k-and-exit.js 1000000 | grep meta [meta] start: writing 1000000 bytes... [meta] done ``` ## Solution 4: exeunt Set stdout/stderr to be blocking, but *only when about to exit*. Usage: ```javascript var exeunt = require('exeunt'); function main() { // ... exeunt(code); // flush stdout/stderr and exit return; // `exeunt` returns, unlike `process.exit` } ``` Pros: - Stdout and stderr will *most likely* (see below) be flushed before exiting. - Because `exeunt()` is calling `process.exit()`, there is no special issue with the node event loop blocking. Cons: - `exeunt()` calls `process.exit()` *asynchronously* (in `setImmediate`), which means you need to handle code still executing. Depending on how your code is structured, that might just require calling `return;`. - `process.exit` is called in `setImmediate` to ensure that one more pass through the event loop will flush stdout/stderr. That event loop pass will also run timers (as part of `uv__run_timers()` in `uv_run()`). I.e. current `setTimeout`s and `setIntervals` may run one more time. My expectation is that this shouldn't be a practical concern for most programs, but it might be for yours. [Example](https://github.com/joyent/node-exeunt/blob/master/examples/write-65k-and-exeunt.js#L17): ``` $ node examples/write-65k-and-exeunt.js 1000000 | grep meta [meta] start: writing 1000000 bytes... [meta] done ``` The code, to show what is happening, is here: . There are some subtleties. First, we can't just exit synchronously: ```javascript setBlocking(); process.exit(code); ``` because that will synchronously call the exit syscall, and the process will terminate, before any IO handling to write buffered stdout/stderr. Instead we use `setImmediate` to ensure that there is one more run through the node event loop which [calls `uv__io_poll`](https://github.com/nodejs/node/blob/v4.8.0/deps/uv/src/unix/core.c#L354) to service IO requests before [calling our `setImmediate` handler](https://github.com/nodejs/node/blob/v4.8.0/deps/uv/src/unix/core.c#L355). Second, we said that stdout/stderr will "most likely be flushed" above, because it appears that [`uv__io_poll` is tuned](https://github.com/nodejs/node/blob/v4.8.0/deps/uv/src/unix/kqueue.c#L150) to limit the amount of events if will handle in a single event loop pass. I haven't yet come up with example code that hits this threshold, however. ## Open Questions We haven't verified all our observations yet. This section includes Rumsfeldian known unknowns. - We need to verify the observations I've made above. At time of writing I was testing out the above examples with node v4.8.0 on macOS 10.11.6. - What are the conditions in libuv's `uv__io_poll` (which is called once for each pass through the node event loop) such that the `count = 48` guard is triggered, such that not all IO is handled in that last pass? https://github.com/nodejs/node/blob/v4.8.0/deps/uv/src/unix/kqueue.c#L291 https://github.com/nodejs/node/blob/v4.8.0/deps/uv/src/unix/sunos.c#L287 Understanding this would be useful to know if and what limitation there is on solution 4. - Test yargs' cases using setBlocking, e.g. to see if they work. ## See Also - [nodejs/node#6980](https://github.com/nodejs/node/issues/6980) "Tracking issue: stdio problems". The node.js core issue that aims to be the tracker for issues related to this. Aside: One of the [linked issues](https://github.com/nodejs/node/issues/6456) includes this: > If this is currently breaking your program, please use this temporary fix: > > [process.stdout, process.stderr].forEach((s) => { > s && s.isTTY && s._handle && s._handle.setBlocking && > s._handle.setBlocking(true) > }) I believe the `s.isTTY` guard needs to be dropped. - [nodejs/node@ab3306a](https://github.com/nodejs/node/commit/ab3306ad51d8136014aa0fa9278b57bb77105320) is the commit where a *TTY* is set to blocking. This is why (at least for node releases with this commit), stdout/stderr flushing is not an issue for a node app called interactively and without piping into another program. - is a small module related to the same problem. It states: "In yargs we only call setBlocking(true) once we already know we are about to call process.exit(code)." This is therefore similar to "Solution 4" described here, and the provided `exeunt()` function. It isn't clear to me all of yargs' usages of this pattern call `process.exit` in a separate tick, which is necessary to actually flush output. ## License MPL 2.0 node-exeunt-1.1.2/examples/000077500000000000000000000000001414031153200155565ustar00rootroot00000000000000node-exeunt-1.1.2/examples/hang-because-active-handle.js000066400000000000000000000011621414031153200231400ustar00rootroot00000000000000/* * Here we attempt to avoid truncation by avoiding process.exit(). * However we have an active handle (the running interval), that will result * in our script hanging. */ function main() { var interval = setInterval(function () { process.stderr.write('[meta] this interval is still running\n'); }, 1000); var size = Number(process.argv[2]) || 65 * 1024; var buff = new Buffer(size); buff.fill('a'); process.stdout.write('[meta] start: writing ' + size + ' bytes...\n'); process.stdout.write(buff); process.stdout.write('\n[meta] done\n'); process.exitCode = 42; } main();node-exeunt-1.1.2/examples/hardball-after-2s.js000066400000000000000000000017341414031153200213130ustar00rootroot00000000000000/* * Here we attempt to avoid truncation by avoiding process.exit(). * However, we have a fallback that will report to `process.exit` after two * seconds -- assuming that if that is reached that the script has hung. */ var exeunt = require('../'); function hardballExit(code) { exeunt.softExit(code); var timeout = setTimeout(function () { process.stderr.write('[meta] hardball exit, you had your chance\n'); process.exit(code); }, 2000); timeout.unref(); // don't be another active handle } function main() { var interval = setInterval(function () { process.stderr.write('[meta] this interval is still running\n'); }, 1000); var size = Number(process.argv[2]) || 65 * 1024; var buff = new Buffer(size); buff.fill('a'); process.stdout.write('[meta] start: writing ' + size + ' bytes...\n'); process.stdout.write(buff); process.stdout.write('\n[meta] done\n'); hardballExit(42); return; } main(); node-exeunt-1.1.2/examples/set-blocking-write-65k-and-exit.js000066400000000000000000000012571414031153200237440ustar00rootroot00000000000000/* * Set stdout/stderr to be blocking so output is always flushed. * Write a large blob and `process.exit`. By default we write ~65k to be more * than the buffer size for a pipe (which is 64k, at least on macOS, IIUC). */ function main() { [process.stdout, process.stderr].forEach(function (s) { s && s._handle && s._handle.setBlocking && s._handle.setBlocking(true); }); var size = Number(process.argv[2]) || 65 * 1024; var buff = new Buffer(size); buff.fill('a'); process.stdout.write('[meta] start: writing ' + size + ' bytes...\n'); process.stdout.write(buff); process.stdout.write('\n[meta] done\n'); process.exit(42); } main(); node-exeunt-1.1.2/examples/write-65k-and-exeunt.js000066400000000000000000000007551414031153200217260ustar00rootroot00000000000000/* * Write a large blob and `exeunt()`. By default we write ~65k to be more * than the buffer size for a pipe (which is 64k, at least on macOS, IIUC). */ var exeunt = require('../'); function main() { var size = Number(process.argv[2]) || 65 * 1024; var buff = new Buffer(size); buff.fill('a'); process.stdout.write('[meta] start: writing ' + size + ' bytes...\n'); process.stdout.write(buff); process.stdout.write('\n[meta] done\n'); exeunt(42); } main(); node-exeunt-1.1.2/examples/write-65k-and-exit.js000066400000000000000000000007311414031153200213610ustar00rootroot00000000000000/* * Write a large blob and `process.exit`. By default we write ~65k to be more * than the buffer size for a pipe (which is 64k, at least on macOS, IIUC). */ function main() { var size = Number(process.argv[2]) || 65 * 1024; var buff = new Buffer(size); buff.fill('a'); process.stdout.write('[meta] start: writing ' + size + ' bytes...\n'); process.stdout.write(buff); process.stdout.write('\n[meta] done\n'); process.exit(42); } main(); node-exeunt-1.1.2/examples/write-65k-setblocking-write-and-exit.js000066400000000000000000000023701414031153200250140ustar00rootroot00000000000000/* * See * * This is an attempt to see if we can squeeze out some "blocking writes" * just before program exit by writing *after* setting output streams blocking * but before calling `process.exit`. * * Usage: * node write-65k-setblocking-write-and-exit.js | grep meta * * Conclusion: If the stdout buffer is full (which it is if we have written * >65k), then writes after `setBlocking` do NOT immediately get written. We * still need another pass through the event loop (which is what exeunt() * does). */ function setBlocking() { [process.stdout, process.stderr].forEach(function setStreamBlocking(s) { if (s && s._handle && s._handle.setBlocking) { s._handle.setBlocking(true); } }); } function main() { var size = Number(process.argv[2]) || 65 * 1024; var buff = new Buffer(size); buff.fill('a'); process.stdout.write('[meta] start: writing ' + size + ' bytes...\n'); process.stdout.write(buff); process.stdout.write('\n[meta] done\n'); setBlocking(); for (var i = 0; i < 65*1024; i++) { process.stdout.write('[meta] more\n'); } process.stdout.write('[meta] done with more\n'); process.exit(42); } main(); node-exeunt-1.1.2/lib/000077500000000000000000000000001414031153200145065ustar00rootroot00000000000000node-exeunt-1.1.2/lib/exeunt.js000066400000000000000000000046231414031153200163610ustar00rootroot00000000000000/* * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ /* * Copyright 2017 Joyent, Inc. */ 'use strict'; var NODE_MINOR_VER_WITHOUT_PROCESS_EXIT_CODE = 10; function setBlocking() { [process.stdout, process.stderr].forEach(function setStreamBlocking(s) { if (s && s._handle && s._handle.setBlocking) { s._handle.setBlocking(true); } }); } /* * A "soft" version of `process.exit([code])` that will avoid actually using * `process.exit` if possible -- only if exiting non-zero and with an older * version of node (<= 0.10) that doesn't yet support `process.exitCode`. * * See the discussion in "Solution 1" of the README.md. * * Usage: * var exeunt = require('exeunt'); * // ... * exeunt.softExit(code); * return; * * @param {Number} code - Optional exit code. Defaults to 0. */ function softExit(code) { var exitCode = code || 0; var nodeVer = process.versions.node.split('.').map(Number); var supportsProcessExitCode = true; if (nodeVer[0] === 0 && nodeVer[1] <= NODE_MINOR_VER_WITHOUT_PROCESS_EXIT_CODE) { supportsProcessExitCode = false; } if (supportsProcessExitCode) { process.exitCode = exitCode; } else if (exitCode !== 0) { process.exit(exitCode); } } /* * Set stdout and stderr blocking and then `process.exit()` asynchronously to * allow stdout and stderr to flush before process termination. * * Call this function as follows: * exeunt([code]); * return; * instead of: * process.exit([code]); * to attempt to ensure stdout/stderr are flushed before exiting. * * Note that this isn't perfect. See the README.md for considerations. This * function corresponds to "Solution 4" described there. * * @param {Number} code - Optional exit code. Defaults to 0. */ function exeunt(code) { var exitCode = code || 0; // Set stdout and stderr to be blocking *before* we exit... setBlocking(); // ...then exit. However, we must do so in a way that node (libuv) will // do another pass through the event loop to handle async IO (in // `uv__io_poll`). setImmediate(function processExit() { process.exit(exitCode); }); } module.exports = exeunt; module.exports.softExit = softExit; node-exeunt-1.1.2/package.json000066400000000000000000000010311414031153200162210ustar00rootroot00000000000000{ "name": "exeunt", "description": "exiting a node.js process *and flushing stdout and stderr*", "version": "1.1.2", "author": "Joyent (joyent.com)", "homepage": "https://github.com/joyent/node-exeunt", "keywords": [ "exit", "flush", "stdout", "stderr", "cli" ], "main": "./lib/exeunt.js", "repository": { "type": "git", "url": "https://github.com/joyent/node-exeunt.git" }, "engines": { "node": ">=0.10" }, "license": "MPL-2.0", "devDependencies": { "eslint": "3.x" } }