Correct some bunyan
CLI exit handling.
This cleans up exit handling and stdout flushing on exit after some lessons learned with https://github.com/joyent/node-exeunt This also improves internal bunyan dev debugging via the `BUNYAN_SELF_TRACE=1` envvar to help trace what was happening with the issues described in #499. Fixes #499
This commit is contained in:
parent
2a5bdd231a
commit
0d62f8a890
3 changed files with 79 additions and 67 deletions
|
@ -5,9 +5,9 @@ Known issues:
|
||||||
- [issue #58] Can't install to a dir with spaces. This is [this node-gyp
|
- [issue #58] Can't install to a dir with spaces. This is [this node-gyp
|
||||||
bug](https://github.com/TooTallNate/node-gyp/issues/65).
|
bug](https://github.com/TooTallNate/node-gyp/issues/65).
|
||||||
|
|
||||||
## not yet released
|
## not yet released (2.x)
|
||||||
|
|
||||||
(nothing yet)
|
- [issue #499] Fix some `bunyan` CLI exit handling problems.
|
||||||
|
|
||||||
|
|
||||||
## 1.8.10
|
## 1.8.10
|
||||||
|
|
137
bin/bunyan
137
bin/bunyan
|
@ -26,6 +26,8 @@ var child_process = require('child_process'),
|
||||||
execFile = child_process.execFile;
|
execFile = child_process.execFile;
|
||||||
var assert = require('assert');
|
var assert = require('assert');
|
||||||
|
|
||||||
|
var exeunt = require('exeunt');
|
||||||
|
|
||||||
try {
|
try {
|
||||||
var moment = require('moment');
|
var moment = require('moment');
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
@ -39,7 +41,13 @@ var nodeVer = process.versions.node.split('.').map(Number);
|
||||||
var nodeSpawnSupportsStdio = (nodeVer[0] > 0 || nodeVer[1] >= 8);
|
var nodeSpawnSupportsStdio = (nodeVer[0] > 0 || nodeVer[1] >= 8);
|
||||||
|
|
||||||
// Internal debug logging via `console.warn`.
|
// Internal debug logging via `console.warn`.
|
||||||
var _DEBUG = false;
|
var _selfTrace = function selfTraceNoop() {};
|
||||||
|
if (process.env.BUNYAN_SELF_TRACE === "1") {
|
||||||
|
_selfTrace = function selfTrace() {
|
||||||
|
process.stderr.write('[bunyan self-trace] ');
|
||||||
|
console.warn.apply(null, arguments);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Output modes.
|
// Output modes.
|
||||||
var OM_LONG = 1;
|
var OM_LONG = 1;
|
||||||
|
@ -102,6 +110,10 @@ var TIMEZONE_LOCAL_FORMATS = {
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
// Boolean set to true when we are in the process of exiting. We don't always
|
||||||
|
// hard-exit (e.g. when staying alive while the pager completes).
|
||||||
|
var exiting = false;
|
||||||
|
|
||||||
// The current raw input line being processed. Used for `uncaughtException`.
|
// The current raw input line being processed. Used for `uncaughtException`.
|
||||||
var currLine = null;
|
var currLine = null;
|
||||||
|
|
||||||
|
@ -710,6 +722,11 @@ var minValidRecord = {
|
||||||
* records) or enqueues it for emitting later when it's the next line to show.
|
* records) or enqueues it for emitting later when it's the next line to show.
|
||||||
*/
|
*/
|
||||||
function handleLogLine(file, line, opts, stylize) {
|
function handleLogLine(file, line, opts, stylize) {
|
||||||
|
if (exiting) {
|
||||||
|
_selfTrace('warn: handleLogLine called while exiting');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
currLine = line; // intentionally global
|
currLine = line; // intentionally global
|
||||||
|
|
||||||
// Emit non-JSON lines immediately.
|
// Emit non-JSON lines immediately.
|
||||||
|
@ -1091,37 +1108,16 @@ function emitRecord(rec, line, opts, stylize) {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
var stdoutFlushed = true;
|
|
||||||
function emit(s) {
|
function emit(s) {
|
||||||
try {
|
try {
|
||||||
stdoutFlushed = stdout.write(s);
|
stdout.write(s);
|
||||||
} catch (e) {
|
} catch (writeErr) {
|
||||||
|
_selfTrace('exception from stdout.write:', writeErr)
|
||||||
// Handle any exceptions in stdout writing in `stdout.on('error', ...)`.
|
// Handle any exceptions in stdout writing in `stdout.on('error', ...)`.
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A hacked up version of 'process.exit' that will first drain stdout
|
|
||||||
* before exiting. *WARNING: This doesn't stop event processing.* IOW,
|
|
||||||
* callers have to be careful that code following this call isn't
|
|
||||||
* accidentally executed.
|
|
||||||
*
|
|
||||||
* In node v0.6 "process.stdout and process.stderr are blocking when they
|
|
||||||
* refer to regular files or TTY file descriptors." However, this hack might
|
|
||||||
* still be necessary in a shell pipeline.
|
|
||||||
*/
|
|
||||||
function drainStdoutAndExit(code) {
|
|
||||||
if (_DEBUG) warn('(drainStdoutAndExit(%d))', code);
|
|
||||||
stdout.on('drain', function () {
|
|
||||||
cleanupAndExit(code);
|
|
||||||
});
|
|
||||||
if (stdoutFlushed) {
|
|
||||||
cleanupAndExit(code);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Process all input from stdin.
|
* Process all input from stdin.
|
||||||
*
|
*
|
||||||
|
@ -1256,7 +1252,7 @@ function processPids(opts, stylize, callback) {
|
||||||
|
|
||||||
warn('bunyan: error: level (%d) exceeds maximum logging level',
|
warn('bunyan: error: level (%d) exceeds maximum logging level',
|
||||||
opts.level);
|
opts.level);
|
||||||
return drainStdoutAndExit(1);
|
cleanupAndExit(1);
|
||||||
}).join(',');
|
}).join(',');
|
||||||
var argv = ['dtrace', '-Z', '-x', 'strsize=4k',
|
var argv = ['dtrace', '-Z', '-x', 'strsize=4k',
|
||||||
'-x', 'switchrate=10hz', '-qn',
|
'-x', 'switchrate=10hz', '-qn',
|
||||||
|
@ -1353,6 +1349,12 @@ function processFile(file, opts, stylize, callback) {
|
||||||
|
|
||||||
var leftover = ''; // Left-over partial line from last chunk.
|
var leftover = ''; // Left-over partial line from last chunk.
|
||||||
stream.on('data', function (data) {
|
stream.on('data', function (data) {
|
||||||
|
if (exiting) {
|
||||||
|
_selfTrace('stop reading file "%s" because exiting', file);
|
||||||
|
stream.destroy();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
var chunk = decoder.write(data);
|
var chunk = decoder.write(data);
|
||||||
if (!chunk.length) {
|
if (!chunk.length) {
|
||||||
return;
|
return;
|
||||||
|
@ -1419,22 +1421,21 @@ function asyncForEach(arr, iterator, callback) {
|
||||||
/**
|
/**
|
||||||
* Cleanup and exit properly.
|
* Cleanup and exit properly.
|
||||||
*
|
*
|
||||||
* Warning: this doesn't stop processing, i.e. process exit might be delayed.
|
* Warning: this doesn't necessarily stop processing, i.e. process exit
|
||||||
* It is up to the caller to ensure that no subsequent bunyan processing
|
* might be delayed. It is up to the caller to ensure that no subsequent
|
||||||
* is done after calling this.
|
* bunyan processing is done after calling this.
|
||||||
*
|
*
|
||||||
* @param code {Number} exit code.
|
* @param code {Number} exit code.
|
||||||
* @param signal {String} Optional signal name, if this was exitting because
|
* @param signal {String} Optional signal name, if this was exitting because
|
||||||
* of a signal.
|
* of a signal.
|
||||||
*/
|
*/
|
||||||
var cleanedUp = false;
|
|
||||||
function cleanupAndExit(code, signal) {
|
function cleanupAndExit(code, signal) {
|
||||||
// Guard one call.
|
// Guard one call.
|
||||||
if (cleanedUp) {
|
if (exiting) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
cleanedUp = true;
|
exiting = true;
|
||||||
if (_DEBUG) warn('(bunyan: cleanupAndExit)');
|
_selfTrace('cleanupAndExit(%s, %s)', code, signal);
|
||||||
|
|
||||||
// Clear possibly interrupted ANSI code (issue #59).
|
// Clear possibly interrupted ANSI code (issue #59).
|
||||||
if (usingAnsiCodes) {
|
if (usingAnsiCodes) {
|
||||||
|
@ -1448,16 +1449,22 @@ function cleanupAndExit(code, signal) {
|
||||||
|
|
||||||
if (pager) {
|
if (pager) {
|
||||||
// Let pager know that output is done, then wait for pager to exit.
|
// Let pager know that output is done, then wait for pager to exit.
|
||||||
stdout.end();
|
pager.removeListener('exit', onPrematurePagerExit);
|
||||||
pager.on('exit', function (pagerCode) {
|
pager.on('exit', function onPagerExit(pagerCode) {
|
||||||
if (_DEBUG)
|
_selfTrace('pager exit -> process.exit(%s)', pagerCode || code);
|
||||||
warn('(bunyan: pager exit -> process.exit(%s))',
|
|
||||||
pagerCode || code);
|
|
||||||
process.exit(pagerCode || code);
|
process.exit(pagerCode || code);
|
||||||
});
|
});
|
||||||
} else {
|
stdout.end();
|
||||||
if (_DEBUG) warn('(bunyan: process.exit(%s))', code);
|
} else if (code) {
|
||||||
|
// Non-zero exit: Something is wrong. We are very likely still
|
||||||
|
// processing log records -- i.e. we have open handles -- so we need
|
||||||
|
// a hard stop (aka `process.exit`).
|
||||||
|
_selfTrace('process.exit(%s)', code);
|
||||||
process.exit(code);
|
process.exit(code);
|
||||||
|
} else {
|
||||||
|
// Zero exit: This should be a "normal" exit, for which we want to
|
||||||
|
// flush stdout/stderr.
|
||||||
|
exeunt.softExit(code);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1510,12 +1517,24 @@ process.on('uncaughtException', function (err) {
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
|
// Early termination of the pager: just stop.
|
||||||
|
function onPrematurePagerExit(pagerCode) {
|
||||||
|
_selfTrace('premature pager exit');
|
||||||
|
// 'pager' and 'stdout' are intentionally global.
|
||||||
|
pager = null;
|
||||||
|
stdout.end()
|
||||||
|
stdout = process.stdout;
|
||||||
|
cleanupAndExit(pagerCode);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
function main(argv) {
|
function main(argv) {
|
||||||
try {
|
try {
|
||||||
var opts = parseArgv(argv);
|
var opts = parseArgv(argv);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
warn('bunyan: error: %s', e.message);
|
warn('bunyan: error: %s', e.message);
|
||||||
return drainStdoutAndExit(1);
|
cleanupAndExit(1);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
if (opts.help) {
|
if (opts.help) {
|
||||||
printHelp();
|
printHelp();
|
||||||
|
@ -1528,7 +1547,8 @@ function main(argv) {
|
||||||
if (opts.pids && opts.args.length > 0) {
|
if (opts.pids && opts.args.length > 0) {
|
||||||
warn('bunyan: error: can\'t use both "-p PID" (%s) and file (%s) args',
|
warn('bunyan: error: can\'t use both "-p PID" (%s) and file (%s) args',
|
||||||
opts.pids, opts.args.join(' '));
|
opts.pids, opts.args.join(' '));
|
||||||
return drainStdoutAndExit(1);
|
cleanupAndExit(1);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
if (opts.color === null) {
|
if (opts.color === null) {
|
||||||
if (process.env.BUNYAN_NO_COLOR &&
|
if (process.env.BUNYAN_NO_COLOR &&
|
||||||
|
@ -1568,36 +1588,26 @@ function main(argv) {
|
||||||
// I'll remove 'F' from here.
|
// I'll remove 'F' from here.
|
||||||
env.LESS = 'FRX';
|
env.LESS = 'FRX';
|
||||||
}
|
}
|
||||||
if (_DEBUG) warn('(pager: argv=%j, env.LESS=%j)', argv, env.LESS);
|
_selfTrace('pager: argv=%j, env.LESS=%j', argv, env.LESS);
|
||||||
// `pager` and `stdout` intentionally global.
|
// `pager` and `stdout` intentionally global.
|
||||||
pager = spawn(argv[0], argv.slice(1),
|
pager = spawn(argv[0], argv.slice(1),
|
||||||
// Share the stderr handle to have error output come
|
// Share the stderr handle to have error output come
|
||||||
// straight through. Only supported in v0.8+.
|
// straight through. Only supported in v0.8+.
|
||||||
{env: env, stdio: ['pipe', 1, 2]});
|
{env: env, stdio: ['pipe', 1, 2]});
|
||||||
stdout = pager.stdin;
|
stdout = pager.stdin;
|
||||||
|
pager.on('exit', onPrematurePagerExit);
|
||||||
// Early termination of the pager: just stop.
|
|
||||||
pager.on('exit', function (pagerCode) {
|
|
||||||
if (_DEBUG) warn('(bunyan: pager exit)');
|
|
||||||
pager = null;
|
|
||||||
stdout.end()
|
|
||||||
stdout = process.stdout;
|
|
||||||
cleanupAndExit(pagerCode);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Stdout error handling. (Couldn't setup until `stdout` was determined.)
|
// Stdout error handling. (Couldn't setup until `stdout` was determined.)
|
||||||
stdout.on('error', function (err) {
|
stdout.on('error', function (err) {
|
||||||
if (_DEBUG) warn('(stdout error event: %s)', err);
|
_selfTrace('stdout error event: %s, exiting=%s', err, exiting);
|
||||||
if (err.code === 'EPIPE') {
|
if (exiting) {
|
||||||
drainStdoutAndExit(0);
|
return;
|
||||||
} else if (err.toString() === 'Error: This socket is closed.') {
|
} else if (err.code === 'EPIPE') {
|
||||||
// Could get this if the pager closes its stdin, but hasn't
|
cleanupAndExit(0);
|
||||||
// exited yet.
|
|
||||||
drainStdoutAndExit(1);
|
|
||||||
} else {
|
} else {
|
||||||
warn(err);
|
warn('bunyan: error on output stream: %s', err);
|
||||||
drainStdoutAndExit(1);
|
cleanupAndExit(1);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -1624,9 +1634,10 @@ function main(argv) {
|
||||||
function (err) {
|
function (err) {
|
||||||
if (err) {
|
if (err) {
|
||||||
warn('bunyan: unexpected error: %s', err.stack || err);
|
warn('bunyan: unexpected error: %s', err.stack || err);
|
||||||
return drainStdoutAndExit(1);
|
cleanupAndExit(1);
|
||||||
|
} else {
|
||||||
|
cleanupAndExit(retval);
|
||||||
}
|
}
|
||||||
cleanupAndExit(retval);
|
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -16,6 +16,9 @@
|
||||||
"keywords": ["log", "logging", "log4j", "json", "bunyan"],
|
"keywords": ["log", "logging", "log4j", "json", "bunyan"],
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
|
||||||
|
"dependencies": {
|
||||||
|
"exeunt": "1.1.0"
|
||||||
|
},
|
||||||
"// dtrace-provider": "required for dtrace features",
|
"// dtrace-provider": "required for dtrace features",
|
||||||
"// mv": "required for RotatingFileStream",
|
"// mv": "required for RotatingFileStream",
|
||||||
"// moment": "required for local time with CLI",
|
"// moment": "required for local time with CLI",
|
||||||
|
@ -35,7 +38,5 @@
|
||||||
|
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"test": "make test"
|
"test": "make test"
|
||||||
},
|
|
||||||
"dependencies": {
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue