diff --git a/doc/api/cli.md b/doc/api/cli.md index 61e1ba18948312..0c83aa12b485a1 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 a79cbcaef65933..fe1b0e7fa1aba3 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 @@ -262,6 +266,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 fd9e7e72a40430..bf487f38a417c4 100644 --- a/lib/internal/process/warning.js +++ b/lib/internal/process/warning.js @@ -1,9 +1,80 @@ 'use strict'; +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) => { @@ -14,19 +85,18 @@ function setupProcessWarnings() { (isDeprecation && process.traceDeprecation); if (trace && warning.stack) { if (warning.code) { - console.error(`${prefix}[${warning.code}] ${warning.stack}`); + output(`${prefix}[${warning.code}] ${warning.stack}`); } else { - console.error(`${prefix}${warning.stack}`); + output(`${prefix}${warning.stack}`); } } else { const toString = typeof warning.toString === 'function' ? warning.toString : Error.prototype.toString; if (warning.code) { - console.error( - `${prefix}[${warning.code}] ${toString.apply(warning)}`); + output(`${prefix}[${warning.code}] ${toString.apply(warning)}`); } else { - console.error(`${prefix}${toString.apply(warning)}`); + output(`${prefix}${toString.apply(warning)}`); } } }); @@ -63,6 +133,6 @@ function setupProcessWarnings() { if (process.throwDeprecation) throw warning; } - process.nextTick(() => process.emit('warning', warning)); + process.nextTick(doEmitWarning(warning)); }; } diff --git a/src/node.cc b/src/node.cc index 51f76ecba6b251..90eee0fb9fe289 100644 --- a/src/node.cc +++ b/src/node.cc @@ -188,6 +188,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; + bool v8_initialized = false; // process-relative uptime base, initialized at start-up @@ -3499,6 +3502,9 @@ static void PrintHelp() { " --throw-deprecation throw an exception on deprecations\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\n" + " stderr\n" " --trace-sync-io show stack trace when use of sync IO\n" " is detected after the first tick\n" " --trace-events-enabled track trace events\n" @@ -3564,6 +3570,8 @@ static void PrintHelp() { " prefixed to the module search path\n" "NODE_REPL_HISTORY path to the persistent REPL history\n" " file\n" + "NODE_REDIRECT_WARNINGS write warnings to path instead of\n" + " stderr\n" "Documentation can be found at https://nodejs.org/\n"); } @@ -3664,6 +3672,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) { @@ -4206,6 +4216,10 @@ void Init(int* argc, config_preserve_symlinks = (*preserve_symlinks == '1'); } + 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 c93870968011be..7c1b79d62f09c5 100644 --- a/src/node_internals.h +++ b/src/node_internals.h @@ -42,6 +42,11 @@ extern const char* 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; + // Tells whether it is safe to call v8::Isolate::GetCurrent(). extern bool v8_initialized; 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)); + })); + }));