diff --git a/doc/api/cli.md b/doc/api/cli.md index 16fabfacf663e1..8ad8c5c7229f3b 100644 --- a/doc/api/cli.md +++ b/doc/api/cli.md @@ -121,6 +121,16 @@ added: v6.0.0 Print stack traces for process warnings (including deprecations). +### `--redirect-warnings=file` + + +Write process warnings to the given file instead of printing to stderr. The +file will be created if it does not exist, and will be appended to if it does. +If an error occurs while attempting to write the warning to the file, the +warning will be written to stderr instead. + ### `--trace-sync-io` + +When set, process warnings will be emitted to the given file instead of +printing to stderr. The file will be created if it does not exist, and will be +appended to if it does. If an error occurs while attempting to write the +warning to the file, the warning will be written to stderr instead. This is +equivalent to using the `--redirect-warnings=file` command-line flag. + [emit_warning]: process.html#process_process_emitwarning_warning_name_ctor [Buffer]: buffer.html#buffer_buffer [debugger]: debugger.html diff --git a/doc/node.1 b/doc/node.1 index 5ce4b7f2daef91..5e63b21eb4ed6f 100644 --- a/doc/node.1 +++ b/doc/node.1 @@ -112,6 +112,10 @@ Silence all process warnings (including deprecations). .BR \-\-trace\-warnings Print stack traces for process warnings (including deprecations). +.TP +.BR \-\-redirect\-warnings=\fIfile\fR +Write process warnings to the given file instead of printing to stderr. + .TP .BR \-\-trace\-sync\-io Print a stack trace whenever synchronous I/O is detected after the first turn @@ -255,6 +259,12 @@ containing trusted certificates. If \fB\-\-use\-openssl\-ca\fR is enabled, this overrides and sets OpenSSL's file containing trusted certificates. +.TP +.BR NODE_REDIRECT_WARNINGS=\fIfile\fR +Write process warnings to the given file instead of printing to stderr. +(equivalent to using the \-\-redirect\-warnings=\fIfile\fR command-line +argument). + .SH BUGS Bugs are tracked in GitHub Issues: .ur https://github.com/nodejs/node/issues diff --git a/lib/internal/process/warning.js b/lib/internal/process/warning.js index bce470cd1c50ac..dd9aa484b7ea21 100644 --- a/lib/internal/process/warning.js +++ b/lib/internal/process/warning.js @@ -4,10 +4,81 @@ const traceWarnings = process.traceProcessWarnings; const noDeprecation = process.noDeprecation; const traceDeprecation = process.traceDeprecation; const throwDeprecation = process.throwDeprecation; +const config = process.binding('config'); const prefix = `(${process.release.name}:${process.pid}) `; exports.setup = setupProcessWarnings; +var fs; +var cachedFd; +var acquiringFd = false; +function nop() {} + +function lazyFs() { + if (!fs) + fs = require('fs'); + return fs; +} + +function writeOut(message) { + if (console && typeof console.error === 'function') + return console.error(message); + process._rawDebug(message); +} + +function onClose(fd) { + return function() { + lazyFs().close(fd, nop); + }; +} + +function onOpen(cb) { + return function(err, fd) { + acquiringFd = false; + if (fd !== undefined) { + cachedFd = fd; + process.on('exit', onClose(fd)); + } + cb(err, fd); + process.emit('_node_warning_fd_acquired', err, fd); + }; +} + +function onAcquired(message) { + // make a best effort attempt at writing the message + // to the fd. Errors are ignored at this point. + return function(err, fd) { + if (err) + return writeOut(message); + lazyFs().appendFile(fd, `${message}\n`, nop); + }; +} + +function acquireFd(cb) { + if (cachedFd === undefined && !acquiringFd) { + acquiringFd = true; + lazyFs().open(config.warningFile, 'a', onOpen(cb)); + } else if (cachedFd !== undefined && !acquiringFd) { + cb(null, cachedFd); + } else { + process.once('_node_warning_fd_acquired', cb); + } +} + +function output(message) { + if (typeof config.warningFile === 'string') { + acquireFd(onAcquired(message)); + return; + } + writeOut(message); +} + +function doEmitWarning(warning) { + return function() { + process.emit('warning', warning); + }; +} + function setupProcessWarnings() { if (!process.noProcessWarnings && process.env.NODE_NO_WARNINGS !== '1') { process.on('warning', (warning) => { @@ -21,7 +92,7 @@ function setupProcessWarnings() { var toString = warning.toString; if (typeof toString !== 'function') toString = Error.prototype.toString; - console.error(`${prefix}${toString.apply(warning)}`); + output(`${prefix}${toString.apply(warning)}`); } }); } @@ -44,6 +115,6 @@ function setupProcessWarnings() { if (throwDeprecation && warning.name === 'DeprecationWarning') throw warning; else - process.nextTick(() => process.emit('warning', warning)); + process.nextTick(doEmitWarning(warning)); }; } diff --git a/src/node.cc b/src/node.cc index 73ed460f361b44..2942139a1fb93b 100644 --- a/src/node.cc +++ b/src/node.cc @@ -199,6 +199,9 @@ bool trace_warnings = false; // that is used by lib/module.js bool config_preserve_symlinks = false; +// Set in node.cc by ParseArgs when --redirect-warnings= is used. +const char* config_warning_file; + // process-relative uptime base, initialized at start-up static double prog_start_time; static bool debugger_running; @@ -3683,6 +3686,8 @@ static void PrintHelp() { "function is used\n" " --no-warnings silence all process warnings\n" " --trace-warnings show stack traces on process warnings\n" + " --redirect-warnings=path\n" + " write warnings to path instead of stderr\n" " --trace-sync-io show stack trace when use of sync IO\n" " is detected after the first tick\n" " --track-heap-objects track heap object allocations for heap " @@ -3747,6 +3752,7 @@ static void PrintHelp() { " prefixed to the module search path\n" "NODE_REPL_HISTORY path to the persistent REPL history file\n" "OPENSSL_CONF load OpenSSL configuration from file\n" + "NODE_REDIRECT_WARNINGS write warnings to path instead of stderr\n" "\n" "Documentation can be found at https://nodejs.org/\n"); } @@ -3848,6 +3854,8 @@ static void ParseArgs(int* argc, no_process_warnings = true; } else if (strcmp(arg, "--trace-warnings") == 0) { trace_warnings = true; + } else if (strncmp(arg, "--redirect-warnings=", 20) == 0) { + config_warning_file = arg + 20; } else if (strcmp(arg, "--trace-deprecation") == 0) { trace_deprecation = true; } else if (strcmp(arg, "--trace-sync-io") == 0) { @@ -4383,6 +4391,10 @@ void Init(int* argc, if (openssl_config.empty()) SafeGetenv("OPENSSL_CONF", &openssl_config); + if (auto redirect_warnings = secure_getenv("NODE_REDIRECT_WARNINGS")) { + config_warning_file = redirect_warnings; + } + // Parse a few arguments which are specific to Node. int v8_argc; const char** v8_argv; diff --git a/src/node_config.cc b/src/node_config.cc index 401345f6a608be..60001207f1851b 100644 --- a/src/node_config.cc +++ b/src/node_config.cc @@ -12,6 +12,7 @@ using v8::Context; using v8::Local; using v8::Object; using v8::ReadOnly; +using v8::String; using v8::Value; // The config binding is used to provide an internal view of compile or runtime @@ -44,6 +45,15 @@ void InitConfig(Local target, if (config_preserve_symlinks) READONLY_BOOLEAN_PROPERTY("preserveSymlinks"); + + if (config_warning_file != nullptr) { + Local name = OneByteString(env->isolate(), "warningFile"); + Local value = String::NewFromUtf8(env->isolate(), + config_warning_file, + v8::NewStringType::kNormal) + .ToLocalChecked(); + target->DefineOwnProperty(env->context(), name, value).FromJust(); + } } // InitConfig } // namespace node diff --git a/src/node_internals.h b/src/node_internals.h index cb71720e8205d9..0135b576d376de 100644 --- a/src/node_internals.h +++ b/src/node_internals.h @@ -43,6 +43,11 @@ extern std::string openssl_config; // that is used by lib/module.js extern bool config_preserve_symlinks; +// Set in node.cc by ParseArgs when --redirect-warnings= is used. +// Used to redirect warning output to a file rather than sending +// it to stderr. +extern const char* config_warning_file; + // Forward declaration class Environment; diff --git a/test/parallel/test-process-redirect-warnings-env.js b/test/parallel/test-process-redirect-warnings-env.js new file mode 100644 index 00000000000000..86942dc9e88e11 --- /dev/null +++ b/test/parallel/test-process-redirect-warnings-env.js @@ -0,0 +1,25 @@ +'use strict'; + +// Tests the NODE_REDIRECT_WARNINGS environment variable by spawning +// a new child node process that emits a warning into a temporary +// warnings file. Once the process completes, the warning file is +// opened and the contents are validated + +const common = require('../common'); +const fs = require('fs'); +const fork = require('child_process').fork; +const path = require('path'); +const assert = require('assert'); + +common.refreshTmpDir(); + +const warnmod = require.resolve(common.fixturesDir + '/warnings.js'); +const warnpath = path.join(common.tmpDir, 'warnings.txt'); + +fork(warnmod, {env: {NODE_REDIRECT_WARNINGS: warnpath}}) + .on('exit', common.mustCall(() => { + fs.readFile(warnpath, 'utf8', common.mustCall((err, data) => { + assert.ifError(err); + assert(/\(node:\d+\) Warning: a bad practice warning/.test(data)); + })); + })); diff --git a/test/parallel/test-process-redirect-warnings.js b/test/parallel/test-process-redirect-warnings.js new file mode 100644 index 00000000000000..b798e41bf6b5e4 --- /dev/null +++ b/test/parallel/test-process-redirect-warnings.js @@ -0,0 +1,25 @@ +'use strict'; + +// Tests the --redirect-warnings command line flag by spawning +// a new child node process that emits a warning into a temporary +// warnings file. Once the process completes, the warning file is +// opened and the contents are validated + +const common = require('../common'); +const fs = require('fs'); +const fork = require('child_process').fork; +const path = require('path'); +const assert = require('assert'); + +common.refreshTmpDir(); + +const warnmod = require.resolve(common.fixturesDir + '/warnings.js'); +const warnpath = path.join(common.tmpDir, 'warnings.txt'); + +fork(warnmod, {execArgv: [`--redirect-warnings=${warnpath}`]}) + .on('exit', common.mustCall(() => { + fs.readFile(warnpath, 'utf8', common.mustCall((err, data) => { + assert.ifError(err); + assert(/\(node:\d+\) Warning: a bad practice warning/.test(data)); + })); + }));