diff --git a/configure.py b/configure.py index 232806170a0c3b..0572471a406f62 100755 --- a/configure.py +++ b/configure.py @@ -493,6 +493,11 @@ dest='without_npm', help='do not install the bundled npm (package manager)') +parser.add_option('--without-report', + action='store_true', + dest='without_report', + help='build without report') + # Dummy option for backwards compatibility parser.add_option('--with-snapshot', action='store_true', @@ -938,6 +943,7 @@ def configure_node(o): o['variables']['OS'] = 'android' o['variables']['node_prefix'] = options.prefix o['variables']['node_install_npm'] = b(not options.without_npm) + o['variables']['node_report'] = b(not options.without_report) o['default_configuration'] = 'Debug' if options.debug else 'Release' host_arch = host_arch_win() if os.name == 'nt' else host_arch_cc() diff --git a/lib/internal/bootstrap/node.js b/lib/internal/bootstrap/node.js index d8313210fd5248..ae14e1a79660f1 100644 --- a/lib/internal/bootstrap/node.js +++ b/lib/internal/bootstrap/node.js @@ -352,6 +352,10 @@ function startup() { } = perf.constants; perf.markMilestone(NODE_PERFORMANCE_MILESTONE_BOOTSTRAP_COMPLETE); + if (getOptionValue('--experimental-report')) { + NativeModule.require('internal/process/report').setup(); + } + if (isMainThread) { return startMainThreadExecution; } else { diff --git a/lib/internal/errors.js b/lib/internal/errors.js index b33cd2710960b6..d5cb86c3a5a4d8 100644 --- a/lib/internal/errors.js +++ b/lib/internal/errors.js @@ -709,6 +709,7 @@ E('ERR_INSPECTOR_CLOSED', 'Session was closed', Error); E('ERR_INSPECTOR_NOT_AVAILABLE', 'Inspector is not available', Error); E('ERR_INSPECTOR_NOT_CONNECTED', 'Session is not connected', Error); E('ERR_INVALID_ADDRESS_FAMILY', 'Invalid address family: %s', RangeError); +E('ERR_SYNTHETIC', 'JavaScript Callstack: %s', Error); E('ERR_INVALID_ARG_TYPE', (name, expected, actual) => { assert(typeof name === 'string', "'name' must be a string"); diff --git a/lib/internal/process/execution.js b/lib/internal/process/execution.js index 607829544a9115..b11f54b35d6253 100644 --- a/lib/internal/process/execution.js +++ b/lib/internal/process/execution.js @@ -98,6 +98,31 @@ function createFatalException() { // call that threw and was never cleared. So clear it now. clearDefaultTriggerAsyncId(); + // If node-report is enabled, call into its handler to see + // whether it is interested in handling the situation. + // Ignore if the error is scoped inside a domain. + // use == in the checks as we want to allow for null and undefined + if (er == null || er.domain == null) { + try { + const report = internalBinding('report'); + if (report != null) { + if (require('internal/options').getOptionValue( + '--experimental-report')) { + const config = {}; + report.syncConfig(config, false); + if (Array.isArray(config.events) && + config.events.includes('exception')) { + if (er) { + report.onUnCaughtException(er.stack); + } else { + report.onUnCaughtException(undefined); + } + } + } + } + } catch {} // NOOP, node_report unavailable. + } + if (exceptionHandlerState.captureFn !== null) { exceptionHandlerState.captureFn(er); } else if (!process.emit('uncaughtException', er)) { diff --git a/lib/internal/process/report.js b/lib/internal/process/report.js new file mode 100644 index 00000000000000..2d0d3f39214793 --- /dev/null +++ b/lib/internal/process/report.js @@ -0,0 +1,163 @@ +'use strict'; + +const { emitExperimentalWarning } = require('internal/util'); +const { + ERR_INVALID_ARG_TYPE, + ERR_SYNTHETIC } = require('internal/errors').codes; + +exports.setup = function() { + const REPORTEVENTS = 1; + const REPORTSIGNAL = 2; + const REPORTFILENAME = 3; + const REPORTPATH = 4; + const REPORTVERBOSE = 5; + if (internalBinding('config').hasReport) { + // If report is enabled, extract the binding and + // wrap the APIs with thin layers, with some error checks. + // user options can come in from CLI / ENV / API. + // CLI and ENV is intercepted in C++ and the API call here (JS). + // So sync up with both sides as appropriate - initially from + // C++ to JS and from JS to C++ whenever the API is called. + // Some events are controlled purely from JS (signal | exception) + // and some from C++ (fatalerror) so this sync-up is essential for + // correct behavior and alignment with the supplied tunables. + const nr = internalBinding('report'); + + // Keep it un-exposed; lest programs play with it + // leaving us with a lot of unwanted sanity checks. + let config = { + events: [], + signal: 'SIGUSR2', + filename: '', + path: '', + verbose: false + }; + const report = { + setDiagnosticReportOptions(options) { + emitExperimentalWarning('report'); + // Reuse the null and undefined checks. Save + // space when dealing with large number of arguments. + const list = parseOptions(options); + + // Flush the stale entries from report, as + // we are refreshing it, items that the users did not + // touch may be hanging around stale otherwise. + config = {}; + + // The parseOption method returns an array that include + // the indices at which valid params are present. + list.forEach((i) => { + switch (i) { + case REPORTEVENTS: + if (Array.isArray(options.events)) + config.events = options.events; + else + throw new ERR_INVALID_ARG_TYPE('events', + 'Array', + options.events); + break; + case REPORTSIGNAL: + if (typeof options.signal !== 'string') { + throw new ERR_INVALID_ARG_TYPE('signal', + 'String', + options.signal); + } + process.removeListener(config.signal, handleSignal); + if (config.events.includes('signal')) + process.on(options.signal, handleSignal); + config.signal = options.signal; + break; + case REPORTFILENAME: + if (typeof options.filename !== 'string') { + throw new ERR_INVALID_ARG_TYPE('filename', + 'String', + options.filename); + } + config.filename = options.filename; + break; + case REPORTPATH: + if (typeof options.path !== 'string') + throw new ERR_INVALID_ARG_TYPE('path', 'String', options.path); + config.path = options.path; + break; + case REPORTVERBOSE: + if (typeof options.verbose !== 'string' && + typeof options.verbose !== 'boolean') { + throw new ERR_INVALID_ARG_TYPE('verbose', + 'Booelan | String' + + ' (true|false|yes|no)', + options.verbose); + } + config.verbose = options.verbose; + break; + } + }); + // Upload this new config to C++ land + nr.syncConfig(config, true); + }, + + + triggerReport(file, err) { + emitExperimentalWarning('report'); + if (err == null) { + if (file == null) { + return nr.triggerReport(new ERR_SYNTHETIC( + 'JavaScript Callstack').stack); + } + if (typeof file !== 'string') + throw new ERR_INVALID_ARG_TYPE('file', 'String', file); + return nr.triggerReport(file, new ERR_SYNTHETIC( + 'JavaScript Callstack').stack); + } + if (typeof err !== 'object') + throw new ERR_INVALID_ARG_TYPE('err', 'Object', err); + if (file == null) + return nr.triggerReport(err.stack); + if (typeof file !== 'string') + throw new ERR_INVALID_ARG_TYPE('file', 'String', file); + return nr.triggerReport(file, err.stack); + }, + getReport(err) { + emitExperimentalWarning('report'); + if (err == null) { + return nr.getReport(new ERR_SYNTHETIC('JavaScript Callstack').stack); + } else if (typeof err !== 'object') { + throw new ERR_INVALID_ARG_TYPE('err', 'Objct', err); + } else { + return nr.getReport(err.stack); + } + } + }; + + // Download the CLI / ENV config into JS land. + nr.syncConfig(config, false); + + function handleSignal(signo) { + if (typeof signo !== 'string') + signo = config.signal; + nr.onUserSignal(signo); + } + + if (config.events.includes('signal')) { + process.on(config.signal, handleSignal); + } + + function parseOptions(obj) { + const list = []; + if (obj == null) + return list; + if (obj.events != null) + list.push(REPORTEVENTS); + if (obj.signal != null) + list.push(REPORTSIGNAL); + if (obj.filename != null) + list.push(REPORTFILENAME); + if (obj.path != null) + list.push(REPORTPATH); + if (obj.verbose != null) + list.push(REPORTVERBOSE); + return list; + } + process.report = report; + } +}; diff --git a/node.gyp b/node.gyp index d5ca93b4b7ef90..0d99551420743a 100644 --- a/node.gyp +++ b/node.gyp @@ -155,6 +155,7 @@ 'lib/internal/process/stdio.js', 'lib/internal/process/warning.js', 'lib/internal/process/worker_thread_only.js', + 'lib/internal/process/report.js', 'lib/internal/querystring.js', 'lib/internal/queue_microtask.js', 'lib/internal/readline.js', @@ -313,6 +314,29 @@ # the executable and rename it back to node.exe later 'product_name': '<(node_core_target_name)-win', }], + [ 'node_report=="true"', { + 'defines': [ + 'NODE_REPORT', + 'NODE_ARCH="<(target_arch)"', + 'NODE_PLATFORM="<(OS)"', + ], + 'conditions': [ + ['OS=="win"', { + 'libraries': [ + 'dbghelp.lib', + 'Netapi32.lib', + 'PsApi.lib', + 'Ws2_32.lib', + ], + 'dll_files': [ + 'dbghelp.dll', + 'Netapi32.dll', + 'PsApi.dll', + 'Ws2_32.dll', + ], + }], + ], + }], ], }, # node_core_target_name { @@ -622,6 +646,34 @@ 'src/tls_wrap.h' ], }], + [ 'node_report=="true"', { + 'sources': [ + 'src/node_report.cc', + 'src/node_report_module.cc', + 'src/node_report_utils.cc', + ], + 'defines': [ + 'NODE_REPORT', + 'NODE_ARCH="<(target_arch)"', + 'NODE_PLATFORM="<(OS)"', + ], + 'conditions': [ + ['OS=="win"', { + 'libraries': [ + 'dbghelp.lib', + 'Netapi32.lib', + 'PsApi.lib', + 'Ws2_32.lib', + ], + 'dll_files': [ + 'dbghelp.dll', + 'Netapi32.dll', + 'PsApi.dll', + 'Ws2_32.dll', + ], + }], + ], + }], [ 'node_use_large_pages=="true" and OS=="linux"', { 'defines': [ 'NODE_ENABLE_LARGE_CODE_PAGES=1' ], # The current implementation of Large Pages is under Linux. @@ -963,6 +1015,29 @@ 'OTHER_LDFLAGS': [ '-Wl,-rpath,@loader_path', ], }, }], + [ 'node_report=="true"', { + 'defines': [ + 'NODE_REPORT', + 'NODE_ARCH="<(target_arch)"', + 'NODE_PLATFORM="<(OS)"', + ], + 'conditions': [ + ['OS=="win"', { + 'libraries': [ + 'dbghelp.lib', + 'Netapi32.lib', + 'PsApi.lib', + 'Ws2_32.lib', + ], + 'dll_files': [ + 'dbghelp.dll', + 'Netapi32.dll', + 'PsApi.dll', + 'Ws2_32.dll', + ], + }], + ], + }], ], }, # cctest ], # end targets diff --git a/src/node.cc b/src/node.cc index ff9ac8494fbb83..148060d7c78254 100644 --- a/src/node.cc +++ b/src/node.cc @@ -89,6 +89,10 @@ #include #endif +#ifdef NODE_REPORT +#include "node_report.h" +#endif + #if defined(LEAK_SANITIZER) #include #endif @@ -724,6 +728,12 @@ void RunBootstrapping(Environment* env) { return; } +#ifdef NODE_REPORT + if (env->options()->experimental_report) { + report::InitializeReport(env->isolate(), env); + } +#endif // NODE_REPORT + // process, loaderExports, isMainThread std::vector> node_params = { env->process_string(), @@ -960,6 +970,12 @@ int Init(std::vector* argv, // Make inherited handles noninheritable. uv_disable_stdio_inheritance(); +#ifdef NODE_REPORT + // Cache the original command line to be + // used in diagnostic reports. + per_process::cli_options->cmdline = *argv; +#endif // NODE_REPORT + #if defined(NODE_V8_OPTIONS) // Should come before the call to V8::SetFlagsFromCommandLine() // so the user can disable a flag --foo at run-time by passing diff --git a/src/node_binding.cc b/src/node_binding.cc index d15c849129343c..85f3c19e690182 100644 --- a/src/node_binding.cc +++ b/src/node_binding.cc @@ -15,6 +15,12 @@ #define NODE_BUILTIN_ICU_MODULES(V) #endif +#if NODE_REPORT +#define NODE_BUILTIN_REPORT_MODULES(V) V(report) +#else +#define NODE_BUILTIN_REPORT_MODULES(V) +#endif + // A list of built-in modules. In order to do module registration // in node::Init(), need to add built-in modules in the following list. // Then in binding::RegisterBuiltinModules(), it calls modules' registration @@ -70,7 +76,8 @@ #define NODE_BUILTIN_MODULES(V) \ NODE_BUILTIN_STANDARD_MODULES(V) \ NODE_BUILTIN_OPENSSL_MODULES(V) \ - NODE_BUILTIN_ICU_MODULES(V) + NODE_BUILTIN_ICU_MODULES(V) \ + NODE_BUILTIN_REPORT_MODULES(V) // This is used to load built-in modules. Instead of using // __attribute__((constructor)), we call the _register_ diff --git a/src/node_config.cc b/src/node_config.cc index 384872cc0092b7..e7b469d00d92b2 100644 --- a/src/node_config.cc +++ b/src/node_config.cc @@ -1,7 +1,7 @@ +#include "env-inl.h" #include "node.h" #include "node_i18n.h" #include "node_options-inl.h" -#include "env-inl.h" #include "util-inl.h" namespace node { @@ -73,6 +73,10 @@ static void Initialize(Local target, #endif // NODE_HAVE_I18N_SUPPORT +#if defined(NODE_REPORT) + READONLY_TRUE_PROPERTY(target, "hasReport"); +#endif // NODE_REPORT + if (env->options()->preserve_symlinks) READONLY_TRUE_PROPERTY(target, "preserveSymlinks"); if (env->options()->preserve_symlinks_main) diff --git a/src/node_errors.cc b/src/node_errors.cc index 9b55a0b92187ac..3d607db2a88712 100644 --- a/src/node_errors.cc +++ b/src/node_errors.cc @@ -3,6 +3,9 @@ #include "node_errors.h" #include "node_internals.h" +#ifdef NODE_REPORT +#include "node_report.h" +#endif namespace node { @@ -314,6 +317,21 @@ void OnFatalError(const char* location, const char* message) { } else { PrintErrorString("FATAL ERROR: %s\n", message); } +#ifdef NODE_REPORT + Isolate* isolate = Isolate::GetCurrent(); + std::string filename; + Environment* env = Environment::GetCurrent(isolate); + if (env != nullptr) { + std::shared_ptr options = env->isolate_data()->options(); + if (options->report_on_fatalerror) { + report::TriggerNodeReport( + isolate, env, message, __func__, filename, Local()); + } + } else { + report::TriggerNodeReport( + isolate, nullptr, message, __func__, filename, Local()); + } +#endif // NODE_REPORT fflush(stderr); ABORT(); } diff --git a/src/node_options.cc b/src/node_options.cc index dcbeac97f689a8..736edb5d4425ae 100644 --- a/src/node_options.cc +++ b/src/node_options.cc @@ -35,6 +35,31 @@ void PerProcessOptions::CheckOptions(std::vector* errors) { void PerIsolateOptions::CheckOptions(std::vector* errors) { per_env->CheckOptions(errors); +#ifdef NODE_REPORT + if (!report_directory.empty() && !per_env->experimental_report) + errors->push_back("--diagnostic-report-directory option is valid only when " + "--experimental-report is set"); + if (!report_filename.empty() && !per_env->experimental_report) + errors->push_back("--diagnostic-report-filename option is valid only when " + "--experimental-report is set"); + if (!report_signal.empty() && !per_env->experimental_report) + errors->push_back("--diagnostic-report-signal option is valid only when " + "--experimental-report is set"); + if (report_on_fatalerror && !per_env->experimental_report) + errors->push_back( + "--diagnostic-report-on-fatalerror option is valid only when " + "--experimental-report is set"); + if (report_on_signal && !per_env->experimental_report) + errors->push_back("--diagnostic-report-on-signal option is valid only when " + "--experimental-report is set"); + if (report_uncaught_exception && !per_env->experimental_report) + errors->push_back( + "--diagnostic-report-uncaught-exception option is valid only when " + "--experimental-report is set"); + if (report_verbose && !per_env->experimental_report) + errors->push_back("--diagnostic-report-verbose option is valid only when " + "--experimental-report is set"); +#endif // NODE_REPORT } void EnvironmentOptions::CheckOptions(std::vector* errors) { @@ -120,6 +145,12 @@ EnvironmentOptionsParser::EnvironmentOptionsParser() { &EnvironmentOptions::experimental_vm_modules, kAllowedInEnvironment); AddOption("--experimental-worker", "", NoOp{}, kAllowedInEnvironment); +#ifdef NODE_REPORT + AddOption("--experimental-report", + "enable report generation", + &EnvironmentOptions::experimental_report, + kAllowedInEnvironment); +#endif // NODE_REPORT AddOption("--expose-internals", "", &EnvironmentOptions::expose_internals); AddOption("--http-parser", "Select which HTTP parser to use; either 'legacy' or 'llhttp' " @@ -242,6 +273,42 @@ PerIsolateOptionsParser::PerIsolateOptionsParser() { AddOption("--perf-prof", "", V8Option{}, kAllowedInEnvironment); AddOption("--stack-trace-limit", "", V8Option{}, kAllowedInEnvironment); +#ifdef NODE_REPORT + AddOption("--diagnostic-report-uncaught-exception", + "generate diagnostic report on uncaught exceptions", + &PerIsolateOptions::report_uncaught_exception, + kAllowedInEnvironment); + AddOption("--diagnostic-report-on-signal", + "generate diagnostic report upon receiving signals", + &PerIsolateOptions::report_on_signal, + kAllowedInEnvironment); + AddOption("--diagnostic-report-on-fatalerror", + "generate diagnostic report on fatal (internal) errors", + &PerIsolateOptions::report_on_fatalerror, + kAllowedInEnvironment); + AddOption("--diagnostic-report-signal", + "causes diagnostic report to be produced on provided signal," + " unsupported in Windows. (default: SIGUSR2)", + &PerIsolateOptions::report_signal, + kAllowedInEnvironment); + Implies("--diagnostic-report-signal", "--diagnostic-report-on-signal"); + AddOption("--diagnostic-report-filename", + "define custom report file name." + " (default: YYYYMMDD.HHMMSS.PID.SEQUENCE#.txt)", + &PerIsolateOptions::report_filename, + kAllowedInEnvironment); + AddOption("--diagnostic-report-directory", + "define custom report pathname." + " (default: current working directory of Node.js process)", + &PerIsolateOptions::report_directory, + kAllowedInEnvironment); + AddOption("--diagnostic-report-verbose", + "verbose option for report generation(true|false)." + " (default: false)", + &PerIsolateOptions::report_verbose, + kAllowedInEnvironment); +#endif // NODE_REPORT + Insert(&EnvironmentOptionsParser::instance, &PerIsolateOptions::get_per_env_options); } diff --git a/src/node_options.h b/src/node_options.h index 47e300e8916ddc..aa221be348912a 100644 --- a/src/node_options.h +++ b/src/node_options.h @@ -121,6 +121,9 @@ class EnvironmentOptions : public Options { bool syntax_check_only = false; bool has_eval_string = false; +#ifdef NODE_REPORT + bool experimental_report = false; +#endif // NODE_REPORT std::string eval_string; bool print_eval = false; bool force_repl = false; @@ -142,6 +145,15 @@ class PerIsolateOptions : public Options { std::shared_ptr per_env { new EnvironmentOptions() }; bool track_heap_objects = false; +#ifdef NODE_REPORT + bool report_uncaught_exception = false; + bool report_on_signal = false; + bool report_on_fatalerror = false; + std::string report_signal; + std::string report_filename; + std::string report_directory; + bool report_verbose; +#endif // NODE_REPORT inline EnvironmentOptions* get_per_env_options(); void CheckOptions(std::vector* errors); }; @@ -184,6 +196,10 @@ class PerProcessOptions : public Options { #endif #endif +#ifdef NODE_REPORT + std::vector cmdline; +#endif // NODE_REPORT + inline PerIsolateOptions* get_per_isolate_options(); void CheckOptions(std::vector* errors); }; diff --git a/src/node_report.cc b/src/node_report.cc new file mode 100644 index 00000000000000..276ce93095029d --- /dev/null +++ b/src/node_report.cc @@ -0,0 +1,833 @@ + +#include "node_report.h" +#include "ares.h" +#include "debug_utils.h" +#include "http_parser.h" +#include "nghttp2/nghttp2ver.h" +#include "node_internals.h" +#include "node_metadata.h" +#include "zlib.h" + +#include +#include + +#ifdef _WIN32 +#include +#include +#include +#include +#include +#include +#include +#else +#include +// Get the standard printf format macros for C99 stdint types. +#ifndef __STDC_FORMAT_MACROS +#define __STDC_FORMAT_MACROS +#endif +#include +#include +#include +#include +#endif + +#include +#include +#include +#include + +#ifndef _MSC_VER +#include +#endif + +#ifdef __APPLE__ +#include +#endif + +#ifndef _WIN32 +extern char** environ; +#endif + +namespace report { +using node::arraysize; +using node::Environment; +using node::Mutex; +using node::NativeSymbolDebuggingContext; +using node::PerIsolateOptions; +using v8::HeapSpaceStatistics; +using v8::HeapStatistics; +using v8::Isolate; +using v8::Local; +using v8::Number; +using v8::StackTrace; +using v8::String; +using v8::V8; +using v8::Value; + +// Internal/static function declarations +static void WriteNodeReport(Isolate* isolate, + Environment* env, + const char* message, + const char* location, + const std::string& filename, + std::ostream& out, + Local stackstr, + TIME_TYPE* time); +static void PrintVersionInformation(JSONWriter* writer); +static void PrintJavaScriptStack(JSONWriter* writer, + Isolate* isolate, + Local stackstr, + const char* location); +static void PrintNativeStack(JSONWriter* writer); +#ifndef _WIN32 +static void PrintResourceUsage(JSONWriter* writer); +#endif +static void PrintGCStatistics(JSONWriter* writer, Isolate* isolate); +static void PrintSystemInformation(JSONWriter* writer); +static void PrintLoadedLibraries(JSONWriter* writer); +static void PrintComponentVersions(JSONWriter* writer); +static void LocalTime(TIME_TYPE* tm_struct); + +// Global variables +static std::atomic_int seq = {0}; // sequence number for report filenames + +// External function to trigger a report, writing to file. +// The 'name' parameter is in/out: an input filename is used +// if supplied, and the actual filename is returned. +std::string TriggerNodeReport(Isolate* isolate, + Environment* env, + const char* message, + const char* location, + std::string name, + Local stackstr) { + std::ostringstream oss; + std::string filename; + std::shared_ptr options; + if (env != nullptr) options = env->isolate_data()->options(); + + // Obtain the current time and the pid (platform dependent) + TIME_TYPE tm_struct; + PID_TYPE pid; + LocalTime(&tm_struct); + pid = uv_os_getpid(); + // Determine the required report filename. In order of priority: + // 1) supplied on API 2) configured on startup 3) default generated + if (!name.empty()) { + // Filename was specified as API parameter, use that + oss << name; + } else if (env != nullptr && options->report_filename.length() > 0) { + // File name was supplied via start-up option, use that + oss << options->report_filename; + } else { + // Construct the report filename, with timestamp, pid and sequence number + oss << "report"; + seq++; +#ifdef _WIN32 + oss << "." << std::setfill('0') << std::setw(4) << tm_struct.wYear; + oss << std::setfill('0') << std::setw(2) << tm_struct.wMonth; + oss << std::setfill('0') << std::setw(2) << tm_struct.wDay; + oss << "." << std::setfill('0') << std::setw(2) << tm_struct.wHour; + oss << std::setfill('0') << std::setw(2) << tm_struct.wMinute; + oss << std::setfill('0') << std::setw(2) << tm_struct.wSecond; + oss << "." << pid; + oss << "." << std::setfill('0') << std::setw(3) << seq.load(); +#else // UNIX, OSX + oss << "." << std::setfill('0') << std::setw(4) << tm_struct.tm_year + 1900; + oss << std::setfill('0') << std::setw(2) << tm_struct.tm_mon + 1; + oss << std::setfill('0') << std::setw(2) << tm_struct.tm_mday; + oss << "." << std::setfill('0') << std::setw(2) << tm_struct.tm_hour; + oss << std::setfill('0') << std::setw(2) << tm_struct.tm_min; + oss << std::setfill('0') << std::setw(2) << tm_struct.tm_sec; + oss << "." << pid; + oss << "." << std::setfill('0') << std::setw(3) << seq.load(); +#endif + oss << ".json"; + } + + filename = oss.str(); + // Open the report file stream for writing. Supports stdout/err, + // user-specified or (default) generated name + std::ofstream outfile; + std::ostream* outstream = &std::cout; + if (filename == "stdout") { + outstream = &std::cout; + } else if (filename == "stderr") { + outstream = &std::cerr; + } else { + // Regular file. Append filename to directory path if one was specified + if (env != nullptr && options->report_directory.length() > 0) { + std::string pathname = options->report_directory; + pathname += PATHSEP; + pathname += filename; + outfile.open(pathname, std::ios::out | std::ios::binary); + } else { + outfile.open(filename, std::ios::out | std::ios::binary); + } + // Check for errors on the file open + if (!outfile.is_open()) { + if (env != nullptr && options->report_directory.length() > 0) { + std::cerr << std::endl + << "Failed to open Node.js report file: " << filename + << " directory: " << options->report_directory + << " (errno: " << errno << ")" << std::endl; + } else { + std::cerr << std::endl + << "Failed to open Node.js report file: " << filename + << " (errno: " << errno << ")" << std::endl; + } + return ""; + } else { + std::cerr << std::endl + << "Writing Node.js report to file: " << filename << std::endl; + } + } + + // Pass our stream about by reference, not by copying it. + std::ostream& out = outfile.is_open() ? outfile : *outstream; + + WriteNodeReport( + isolate, env, message, location, filename, out, stackstr, &tm_struct); + + // Do not close stdout/stderr, only close files we opened. + if (outfile.is_open()) { + outfile.close(); + } + + std::cerr << "Node.js report completed" << std::endl; + if (name.empty()) return filename; + return name; +} + +// External function to trigger a report, writing to a supplied stream. +void GetNodeReport(Isolate* isolate, + Environment* env, + const char* message, + const char* location, + Local stackstr, + std::ostream& out) { + // Obtain the current time and the pid (platform dependent) + TIME_TYPE tm_struct; + LocalTime(&tm_struct); + std::string str = "NA"; + WriteNodeReport( + isolate, env, message, location, str, out, stackstr, &tm_struct); +} + +// Internal function to coordinate and write the various +// sections of the report to the supplied stream +static void WriteNodeReport(Isolate* isolate, + Environment* env, + const char* message, + const char* location, + const std::string& filename, + std::ostream& out, + Local stackstr, + TIME_TYPE* tm_struct) { + std::ostringstream buf; + PID_TYPE pid = uv_os_getpid(); + + // Save formatting for output stream. + std::ios old_state(nullptr); + old_state.copyfmt(out); + + // File stream opened OK, now start printing the report content: + // the title and header information (event, filename, timestamp and pid) + + JSONWriter writer(out); + writer.json_start(); + writer.json_objectstart("header"); + + writer.json_keyvalue("event", message); + writer.json_keyvalue("location", location); + if (!filename.empty()) + writer.json_keyvalue("filename", filename); + else + writer.json_keyvalue("filename", std::string("''")); + + // Report dump event and module load date/time stamps + char timebuf[64]; +#ifdef _WIN32 + snprintf(timebuf, + sizeof(timebuf), + "%4d/%02d/%02d %02d:%02d:%02d", + tm_struct->wYear, + tm_struct->wMonth, + tm_struct->wDay, + tm_struct->wHour, + tm_struct->wMinute, + tm_struct->wSecond); + writer.json_keyvalue("dumpEventTime", timebuf); +#else // UNIX, OSX + snprintf(timebuf, + sizeof(timebuf), + "%4d-%02d-%02dT%02d:%02d:%02dZ", + tm_struct->tm_year + 1900, + tm_struct->tm_mon + 1, + tm_struct->tm_mday, + tm_struct->tm_hour, + tm_struct->tm_min, + tm_struct->tm_sec); + writer.json_keyvalue("dumpEventTime", timebuf); + struct timeval ts; + gettimeofday(&ts, nullptr); + writer.json_keyvalue("dumpEventTimeStamp", + std::to_string(ts.tv_sec * 1000 + ts.tv_usec / 1000)); +#endif + // Report native process ID + buf << pid; + writer.json_keyvalue("processId", buf.str()); + buf.flush(); + + // Report out the command line. + if (!node::per_process::cli_options->cmdline.empty()) { + writer.json_arraystart("commandLine"); + for (std::string arg : node::per_process::cli_options->cmdline) { + writer.json_element(arg); + } + writer.json_arrayend(); + } + + // Report Node.js and OS version information + PrintVersionInformation(&writer); + writer.json_objectend(); + + // Report summary JavaScript stack backtrace + PrintJavaScriptStack(&writer, isolate, stackstr, location); + + // Report native stack backtrace + PrintNativeStack(&writer); + + // Report V8 Heap and Garbage Collector information + PrintGCStatistics(&writer, isolate); + + // Report OS and current thread resource usage +#ifndef _WIN32 + PrintResourceUsage(&writer); +#endif + + writer.json_arraystart("libuv"); + if (env != nullptr) + uv_walk(env->event_loop(), WalkHandle, static_cast(&writer)); + else + uv_walk(uv_default_loop(), WalkHandle, static_cast(&writer)); + + writer.json_arrayend(); + + // Report operating system information + PrintSystemInformation(&writer); + + writer.json_objectend(); + + // Restore output stream formatting. + out.copyfmt(old_state); +} + +// Report Node.js version, OS version and machine information. +static void PrintVersionInformation(JSONWriter* writer) { + std::ostringstream buf; + // Report Node version + buf << "v" << NODE_VERSION_STRING; + writer->json_keyvalue("nodejsVersion", buf.str()); + buf.str(""); +#ifdef __GLIBC__ + buf << __GLIBC__ << "." << __GLIBC_MINOR__; + writer->json_keyvalue("glibcVersion", buf.str()); + buf.str(""); +#endif + // Report Process word size + buf << sizeof(void*) * 8 << " bit"; + writer->json_keyvalue("wordSize", buf.str()); + buf.str(""); + + // Report deps component versions + PrintComponentVersions(writer); + + // Report operating system and machine information (Windows) +#ifdef _WIN32 + { + // Level 101 to obtain the server name, type, and associated details. + // ref: https://docs.microsoft.com/en-us/windows/desktop/ + // api/lmserver/nf-lmserver-netservergetinfo + const DWORD level = 101; + LPSERVER_INFO_101 os_info = nullptr; + NET_API_STATUS nStatus = + NetServerGetInfo(nullptr, level, reinterpret_cast(&os_info)); + if (nStatus == NERR_Success) { + LPSTR os_name = "Windows"; + const DWORD major = os_info->sv101_version_major & MAJOR_VERSION_MASK; + const DWORD type = os_info->sv101_type; + const bool isServer = (type & SV_TYPE_DOMAIN_CTRL) || + (type & SV_TYPE_DOMAIN_BAKCTRL) || + (type & SV_TYPE_SERVER_NT); + switch (major) { + case 5: + switch (os_info->sv101_version_minor) { + case 0: + os_name = "Windows 2000"; + break; + default: + os_name = (isServer ? "Windows Server 2003" : "Windows XP"); + } + break; + case 6: + switch (os_info->sv101_version_minor) { + case 0: + os_name = (isServer ? "Windows Server 2008" : "Windows Vista"); + break; + case 1: + os_name = (isServer ? "Windows Server 2008 R2" : "Windows 7"); + break; + case 2: + os_name = (isServer ? "Windows Server 2012" : "Windows 8"); + break; + case 3: + os_name = (isServer ? "Windows Server 2012 R2" : "Windows 8.1"); + break; + default: + os_name = (isServer ? "Windows Server" : "Windows Client"); + } + break; + case 10: + os_name = (isServer ? "Windows Server 2016" : "Windows 10"); + break; + default: + os_name = (isServer ? "Windows Server" : "Windows Client"); + } + writer->json_keyvalue("osVersion", os_name); + + // Convert and report the machine name and comment fields + // (these are LPWSTR types) + size_t count; + char name_buf[256]; + wcstombs_s( + &count, name_buf, sizeof(name_buf), os_info->sv101_name, _TRUNCATE); + if (os_info->sv101_comment != nullptr) { + char comment_buf[256]; + wcstombs_s(&count, + comment_buf, + sizeof(comment_buf), + os_info->sv101_comment, + _TRUNCATE); + buf << name_buf << " " << comment_buf; + writer->json_keyvalue("machine", buf.str()); + buf.flush(); + } else { + writer->json_keyvalue("machine", name_buf); + } + + if (os_info != nullptr) { + NetApiBufferFree(os_info); + } + } else { + // NetServerGetInfo() failed, fallback to use GetComputerName() instead + TCHAR machine_name[256]; + DWORD machine_name_size = 256; + writer->json_keyvalue("osVersion", "Windows"); + if (GetComputerName(machine_name, &machine_name_size)) { + writer->json_keyvalue("machine", machine_name); + } + } + } +#else + // Report operating system and machine information (Unix/OSX) + struct utsname os_info; + if (uname(&os_info) >= 0) { +#ifdef _AIX + buf << os_info.sysname << " " << os_info.version << "." << os_info.release; + writer->json_keyvalue("osVersion", buf.str()); + buf.flush(); +#else + buf << os_info.sysname << " " << os_info.release << " " << os_info.version; + writer->json_keyvalue("osVersion", buf.str()); + buf.flush(); +#endif + const char* (*libc_version)(); + *(reinterpret_cast(&libc_version)) = + dlsym(RTLD_DEFAULT, "gnu_get_libc_version"); + if (libc_version != nullptr) { + writer->json_keyvalue("glibc", (*libc_version)()); + } + buf << os_info.nodename << " " << os_info.machine; + writer->json_keyvalue("machine", buf.str()); + buf.flush(); + } +#endif +} + +// Report the JavaScript stack. +static void PrintJavaScriptStack(JSONWriter* writer, + Isolate* isolate, + Local stackstr, + const char* location) { + writer->json_objectstart("javascriptStack"); + + std::string ss; + if ((!strcmp(location, "OnFatalError")) || + (!strcmp(location, "OnUserSignal"))) { + ss = "No stack.\nUnavailable.\n"; + } else { + String::Utf8Value sv(isolate, stackstr); + ss = std::string(*sv, sv.length()); + } + int line = ss.find("\n"); + if (line == -1) { + writer->json_keyvalue("message", ss.c_str()); + writer->json_objectend(); + } else { + std::string l = ss.substr(0, line); + writer->json_keyvalue("message", l); + writer->json_arraystart("stack"); + ss = ss.substr(line + 1); + line = ss.find("\n"); + while (line != -1) { + l = ss.substr(0, line); + l.erase(l.begin(), std::find_if(l.begin(), l.end(), [](int ch) { + return !std::iswspace(ch); + })); + writer->json_element(l); + ss = ss.substr(line + 1); + line = ss.find("\n"); + } + } + writer->json_arrayend(); + writer->json_objectend(); +} + +// Report a native stack backtrace +static void PrintNativeStack(JSONWriter* writer) { + auto sym_ctx = NativeSymbolDebuggingContext::New(); + void* frames[256]; + const int size = sym_ctx->GetStackTrace(frames, arraysize(frames)); + writer->json_arraystart("nativeStack"); + int i; + std::ostringstream buf; + for (i = 1; i < size - 1; i += 1) { + void* frame = frames[i]; + buf.str(""); + buf << " [pc=" << frame << "] "; + buf << sym_ctx->LookupSymbol(frame).Display().c_str(); + writer->json_element(buf.str()); + } + buf.str(""); + buf << " [pc=" << frames[i] << "] "; + buf << sym_ctx->LookupSymbol(frames[i]).Display().c_str(); + writer->json_element(buf.str()); + writer->json_arrayend(); +} + +// Report V8 JavaScript heap information. +// This uses the existing V8 HeapStatistics and HeapSpaceStatistics APIs. +// The isolate->GetGCStatistics(&heap_stats) internal V8 API could potentially +// provide some more useful information - the GC history and the handle counts +static void PrintGCStatistics(JSONWriter* writer, Isolate* isolate) { + HeapStatistics v8_heap_stats; + isolate->GetHeapStatistics(&v8_heap_stats); + HeapSpaceStatistics v8_heap_space_stats; + + writer->json_objectstart("javascriptHeap"); + writer->json_keyvalue("totalMemory", + std::to_string(v8_heap_stats.total_heap_size())); + writer->json_keyvalue("totalCommittedMemory", + std::to_string(v8_heap_stats.total_physical_size())); + writer->json_keyvalue("usedMemory", + std::to_string(v8_heap_stats.used_heap_size())); + writer->json_keyvalue("availableMemory", + std::to_string(v8_heap_stats.total_available_size())); + writer->json_keyvalue("memoryLimit", + std::to_string(v8_heap_stats.heap_size_limit())); + + writer->json_objectstart("heapSpaces"); + // Loop through heap spaces + size_t i; + for (i = 0; i < isolate->NumberOfHeapSpaces() - 1; i++) { + isolate->GetHeapSpaceStatistics(&v8_heap_space_stats, i); + writer->json_objectstart(v8_heap_space_stats.space_name()); + writer->json_keyvalue("memorySize", + std::to_string(v8_heap_space_stats.space_size())); + writer->json_keyvalue( + "committedMemory", + std::to_string(v8_heap_space_stats.physical_space_size())); + writer->json_keyvalue( + "capacity", + std::to_string(v8_heap_space_stats.space_used_size() + + v8_heap_space_stats.space_available_size())); + writer->json_keyvalue( + "used", std::to_string(v8_heap_space_stats.space_used_size())); + writer->json_keyvalue( + "available", + std::to_string(v8_heap_space_stats.space_available_size())); + writer->json_objectend(); + } + isolate->GetHeapSpaceStatistics(&v8_heap_space_stats, i); + writer->json_objectstart(v8_heap_space_stats.space_name()); + writer->json_keyvalue("memorySize", + std::to_string(v8_heap_space_stats.space_size())); + writer->json_keyvalue( + "committedMemory", + std::to_string(v8_heap_space_stats.physical_space_size())); + writer->json_keyvalue( + "capacity", + std::to_string(v8_heap_space_stats.space_used_size() + + v8_heap_space_stats.space_available_size())); + writer->json_keyvalue("used", + std::to_string(v8_heap_space_stats.space_used_size())); + writer->json_keyvalue( + "available", std::to_string(v8_heap_space_stats.space_available_size())); + writer->json_objectend(); + writer->json_objectend(); + writer->json_objectend(); +} + +#ifndef _WIN32 +// Report resource usage (Linux/OSX only). +static void PrintResourceUsage(JSONWriter* writer) { + char buf[64]; + double cpu_abs; + double cpu_percentage; + time_t current_time; // current time absolute + time(¤t_time); + size_t boot_time = static_cast(node::per_process::prog_start_time / + (1000 * 1000 * 1000)); + auto uptime = difftime(current_time, boot_time); + if (uptime == 0) uptime = 1; // avoid division by zero. + + // Process and current thread usage statistics + struct rusage stats; + writer->json_objectstart("resourceUsage"); + if (getrusage(RUSAGE_SELF, &stats) == 0) { +#if defined(__APPLE__) || defined(_AIX) + snprintf(buf, + sizeof(buf), + "%ld.%06d", + stats.ru_utime.tv_sec, + stats.ru_utime.tv_usec); + writer->json_keyvalue("userCpuSeconds", buf); + snprintf(buf, + sizeof(buf), + "%ld.%06d", + stats.ru_stime.tv_sec, + stats.ru_stime.tv_usec); + writer->json_keyvalue("kernelCpuSeconds", buf); +#else + snprintf(buf, + sizeof(buf), + "%ld.%06ld", + stats.ru_utime.tv_sec, + stats.ru_utime.tv_usec); + writer->json_keyvalue("userCpuSeconds", buf); + snprintf(buf, + sizeof(buf), + "%ld.%06ld", + stats.ru_stime.tv_sec, + stats.ru_stime.tv_usec); + writer->json_keyvalue("kernelCpuSeconds", buf); +#endif + cpu_abs = stats.ru_utime.tv_sec + 0.000001 * stats.ru_utime.tv_usec + + stats.ru_stime.tv_sec + 0.000001 * stats.ru_stime.tv_usec; + cpu_percentage = (cpu_abs / uptime) * 100.0; + writer->json_keyvalue("cpuConsumptionPercent", + std::to_string(cpu_percentage)); + writer->json_keyvalue("maxRss", std::to_string(stats.ru_maxrss * 1024)); + writer->json_objectstart("pageFaults"); + writer->json_keyvalue("IORequired", std::to_string(stats.ru_majflt)); + writer->json_keyvalue("IONotRequired", std::to_string(stats.ru_minflt)); + writer->json_objectend(); + writer->json_objectstart("fsActivity"); + writer->json_keyvalue("reads", std::to_string(stats.ru_inblock)); + writer->json_keyvalue("writes", std::to_string(stats.ru_oublock)); + writer->json_objectend(); + } + writer->json_objectend(); +#ifdef RUSAGE_THREAD + if (getrusage(RUSAGE_THREAD, &stats) == 0) { + writer->json_objectstart("uvthreadResourceUsage"); +#if defined(__APPLE__) || defined(_AIX) + snprintf(buf, + sizeof(buf), + "%ld.%06d", + stats.ru_utime.tv_sec, + stats.ru_utime.tv_usec); + writer->json_keyvalue("userCpuSeconds", buf); + snprintf(buf, + sizeof(buf), + "%ld.%06d", + stats.ru_stime.tv_sec, + stats.ru_stime.tv_usec); + writer->json_keyvalue("kernelCpuSeconds", buf); +#else + snprintf(buf, + sizeof(buf), + "%ld.%06ld", + stats.ru_utime.tv_sec, + stats.ru_utime.tv_usec); + writer->json_keyvalue("userCpuSeconds", buf); + snprintf(buf, + sizeof(buf), + "%ld.%06ld", + stats.ru_stime.tv_sec, + stats.ru_stime.tv_usec); + writer->json_keyvalue("kernelCpuSeconds", buf); +#endif + cpu_abs = stats.ru_utime.tv_sec + 0.000001 * stats.ru_utime.tv_usec + + stats.ru_stime.tv_sec + 0.000001 * stats.ru_stime.tv_usec; + cpu_percentage = (cpu_abs / uptime) * 100.0; + writer->json_keyvalue("cpuConsumptionPercent", + std::to_string(cpu_percentage)); + writer->json_objectstart("fsActivity"); + writer->json_keyvalue("reads", std::to_string(stats.ru_inblock)); + writer->json_keyvalue("writes", std::to_string(stats.ru_oublock)); + writer->json_objectend(); + writer->json_objectend(); + } +#endif +} +#endif + +// Report operating system information. +static void PrintSystemInformation(JSONWriter* writer) { +#ifndef _WIN32 + static struct { + const char* description; + int id; + } rlimit_strings[] = { + {"core_file_size_blocks", RLIMIT_CORE}, + {"data_seg_size_kbytes", RLIMIT_DATA}, + {"file_size_blocks", RLIMIT_FSIZE}, +#if !(defined(_AIX) || defined(__sun)) + {"max_locked_memory_bytes", RLIMIT_MEMLOCK}, +#endif +#ifndef __sun + {"max_memory_size_kbytes", RLIMIT_RSS}, +#endif + {"open_files", RLIMIT_NOFILE}, + {"stack_size_bytes", RLIMIT_STACK}, + {"cpu_time_seconds", RLIMIT_CPU}, +#ifndef __sun + {"max_user_processes", RLIMIT_NPROC}, +#endif + {"virtual_memory_kbytes", RLIMIT_AS} + }; +#endif // _WIN32 + writer->json_objectstart("environmentVariables"); + Mutex::ScopedLock lock(node::per_process::env_var_mutex); +#ifdef _WIN32 + LPWSTR lpszVariable; + LPWCH lpvEnv; + + // Get pointer to the environment block + lpvEnv = GetEnvironmentStringsW(); + if (lpvEnv != nullptr) { + // Variable strings are separated by null bytes, + // and the block is terminated by a null byte. + lpszVariable = reinterpret_cast(lpvEnv); + while (*lpszVariable) { + DWORD size = WideCharToMultiByte( + CP_UTF8, 0, lpszVariable, -1, nullptr, 0, nullptr, nullptr); + char* str = new char[size]; + WideCharToMultiByte( + CP_UTF8, 0, lpszVariable, -1, str, size, nullptr, nullptr); + std::string env(str); + int sep = env.rfind("="); + std::string key = env.substr(0, sep); + std::string value = env.substr(sep + 1); + writer->json_keyvalue(key, value); + lpszVariable += lstrlenW(lpszVariable) + 1; + } + FreeEnvironmentStringsW(lpvEnv); + } + writer->json_objectend(); +#else + std::string pair; + for (char** env = environ; *env != nullptr; ++env) { + std::string pair(*env); + int separator = pair.find('='); + std::string key = pair.substr(0, separator); + std::string str = pair.substr(separator + 1); + writer->json_keyvalue(key, str); + } + writer->json_objectend(); + + writer->json_objectstart("userLimits"); + struct rlimit limit; + char buf[64]; + std::string soft, hard; + + for (size_t i = 0; i < arraysize(rlimit_strings); i++) { + if (getrlimit(rlimit_strings[i].id, &limit) == 0) { + if (limit.rlim_cur == RLIM_INFINITY) { + soft = std::string("unlimited"); + } else { +#if defined(_AIX) || defined(__sun) + snprintf(buf, sizeof(buf), "%ld", limit.rlim_cur); + soft = std::string(buf); +#elif defined(__linux__) && !defined(__GLIBC__) + snprintf(buf, sizeof(buf), "%ld", limit.rlim_cur); + soft = std::string(buf); +#else + snprintf(buf, sizeof(buf), "%16" PRIu64, limit.rlim_cur); + soft = std::string(soft); +#endif + } + if (limit.rlim_max == RLIM_INFINITY) { + hard = std::string("unlimited"); + } else { +#ifdef _AIX + snprintf(buf, sizeof(buf), "%lu", limit.rlim_max); + hard = std::string(buf); +#else + snprintf(buf, sizeof(buf), "%lu", limit.rlim_max); + hard = std::string(buf); +#endif + } + writer->json_objectstart(rlimit_strings[i].description); + writer->json_keyvalue("soft", soft); + writer->json_keyvalue("hard", hard); + writer->json_objectend(); + } + } + writer->json_objectend(); +#endif + + PrintLoadedLibraries(writer); +} + +// Report a list of loaded native libraries. +static void PrintLoadedLibraries(JSONWriter* writer) { + writer->json_arraystart("sharedObjects"); + std::vector modules = + NativeSymbolDebuggingContext::GetLoadedLibraries(); + for (auto const& module_name : modules) writer->json_element(module_name); + writer->json_arrayend(); +} + +// Obtain and report the node and subcomponent version strings. +static void PrintComponentVersions(JSONWriter* writer) { + std::stringstream buf; + + writer->json_objectstart("componentVersions"); + +#define V(key) \ + writer->json_keyvalue(#key, node::per_process::metadata.versions.key.c_str()); + NODE_VERSIONS_KEYS(V) +#undef V + + // Some extra information that is not present in node_metadata. + writer->json_keyvalue("arch", NODE_ARCH); + writer->json_keyvalue("platform", NODE_PLATFORM); + writer->json_keyvalue("release", NODE_RELEASE); + if (NODE_VERSION_IS_LTS != 0) + writer->json_keyvalue("lts", NODE_VERSION_LTS_CODENAME); + writer->json_objectend(); +} + +static void LocalTime(TIME_TYPE* tm_struct) { +#ifdef _WIN32 + GetLocalTime(tm_struct); +#else // UNIX, OSX + struct timeval time_val; + gettimeofday(&time_val, nullptr); + localtime_r(&time_val.tv_sec, tm_struct); +#endif +} + +} // namespace report diff --git a/src/node_report.h b/src/node_report.h new file mode 100644 index 00000000000000..c64b9c9a20c116 --- /dev/null +++ b/src/node_report.h @@ -0,0 +1,165 @@ +#ifndef SRC_NODE_REPORT_H_ +#define SRC_NODE_REPORT_H_ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "v8.h" + +#include +#include +#include +#include +#include + +#ifdef _WIN32 +#include +#else +#include +#include +#include +#endif + +namespace report { + +#ifdef _WIN32 +typedef SYSTEMTIME TIME_TYPE; +typedef DWORD PID_TYPE; +#define PATHSEP "\\" +#else // UNIX, OSX +typedef struct tm TIME_TYPE; +typedef pid_t PID_TYPE; +#define PATHSEP "/" +#endif + +void InitializeReport(v8::Isolate* isolate, node::Environment* env); + +// Function declarations - functions in src/node_report.cc +std::string TriggerNodeReport(v8::Isolate* isolate, + node::Environment* env, + const char* message, + const char* location, + std::string name, + v8::Local stackstr); +void GetNodeReport(v8::Isolate* isolate, + node::Environment* env, + const char* message, + const char* location, + v8::Local stackstr, + std::ostream& out); + +// Function declarations - utility functions in src/utilities.cc +void ReportEndpoints(uv_handle_t* h, std::ostringstream& out); +void WalkHandle(uv_handle_t* h, void* arg); +std::string EscapeJsonChars(const std::string& str); + +// Function declarations - export functions in src/node_report_module.cc +void TriggerReport(const v8::FunctionCallbackInfo& info); +void GetReport(const v8::FunctionCallbackInfo& info); + +// Node.js boot time - defined in src/node.cc +extern double prog_start_time; + +// JSON compiler definitions. +class JSONWriter { + public: + explicit JSONWriter(std::ostream& out) + : out_(out), indent_(0), state_(JSONOBJECT) {} + + inline void indent() { indent_ += 2; } + inline void deindent() { indent_ -= 2; } + inline void advance() { + for (int i = 0; i < indent_; i++) out_ << " "; + } + + inline void json_start() { + if (state_ == JSONVALUE) out_ << ","; + out_ << "\n"; + advance(); + out_ << "{"; + indent(); + state_ = JSONOBJECT; + } + + inline void json_end() { + out_ << "\n"; + deindent(); + advance(); + out_ << "}"; + state_ = JSONVALUE; + } + template + inline void json_objectstart(T key) { + if (state_ == JSONVALUE) out_ << ","; + out_ << "\n"; + advance(); + out_ << "\"" << key << "\"" + << ": {"; + indent(); + state_ = JSONOBJECT; + } + + template + inline void json_arraystart(T key) { + if (state_ == JSONVALUE) out_ << ","; + out_ << "\n"; + advance(); + out_ << "\"" << key << "\"" + << ": ["; + indent(); + state_ = JSONOBJECT; + } + inline void json_objectend() { + out_ << "\n"; + deindent(); + advance(); + out_ << "}"; + state_ = JSONVALUE; + } + + inline void json_arrayend() { + out_ << "\n"; + deindent(); + advance(); + out_ << "]"; + state_ = JSONVALUE; + } + template + inline void json_keyvalue(T key, U value) { + if (state_ == JSONVALUE) out_ << ","; + out_ << "\n"; + advance(); + out_ << "\"" << key << "\"" + << ": " + << "\""; + out_ << EscapeJsonChars(value) << "\""; + state_ = JSONVALUE; + } + + template + inline void json_element(U value) { + if (state_ == JSONVALUE) out_ << ","; + out_ << "\n"; + advance(); + out_ << "\"" << EscapeJsonChars(value) << "\""; + state_ = JSONVALUE; + } + + private: + enum JSONState { JSONOBJECT, JSONVALUE }; + std::ostream& out_; + int indent_; + int state_; +}; + +} // namespace report + +#endif // SRC_NODE_REPORT_H_ diff --git a/src/node_report_module.cc b/src/node_report_module.cc new file mode 100644 index 00000000000000..a9ce1cbddfbe46 --- /dev/null +++ b/src/node_report_module.cc @@ -0,0 +1,294 @@ +#include "env.h" +#include "node_errors.h" +#include "node_internals.h" +#include "node_options.h" +#include "node_report.h" +#include "util.h" + +#include "env-inl.h" +#include "handle_wrap.h" +#include "node_buffer.h" +#include "stream_base-inl.h" +#include "stream_wrap.h" +#include "util-inl.h" + +#include +#include +#include + +namespace report { +using node::Environment; +using node::FIXED_ONE_BYTE_STRING; +using node::PerIsolateOptions; +using node::Utf8Value; +using v8::Array; +using v8::Boolean; +using v8::Context; +using v8::Function; +using v8::FunctionCallbackInfo; +using v8::HandleScope; +using v8::Isolate; +using v8::Local; +using v8::Object; +using v8::String; +using v8::V8; +using v8::Value; + +// Internal/static function declarations +void OnUncaughtException(const FunctionCallbackInfo& info); +static void Initialize(Local exports, + Local unused, + Local context); + +// External JavaScript API for triggering a report +void TriggerReport(const FunctionCallbackInfo& info) { + Environment* env = Environment::GetCurrent(info); + Isolate* isolate = env->isolate(); + HandleScope scope(isolate); + std::string filename; + Local stackstr; + + if (info.Length() == 1) { + stackstr = info[0].As(); + } else { + filename = *String::Utf8Value(isolate, info[0]); + stackstr = info[1].As(); + } + + filename = TriggerNodeReport( + isolate, env, "JavaScript API", __func__, filename, stackstr); + // Return value is the report filename + info.GetReturnValue().Set( + String::NewFromUtf8(isolate, filename.c_str(), v8::NewStringType::kNormal) + .ToLocalChecked()); +} + +// External JavaScript API for returning a report +void GetReport(const FunctionCallbackInfo& info) { + Environment* env = Environment::GetCurrent(info); + Isolate* isolate = env->isolate(); + HandleScope scope(isolate); + std::ostringstream out; + + GetNodeReport( + isolate, env, "JavaScript API", __func__, info[0].As(), out); + + // Return value is the contents of a report as a string. + info.GetReturnValue().Set(String::NewFromUtf8(isolate, + out.str().c_str(), + v8::NewStringType::kNormal) + .ToLocalChecked()); +} + +// Callbacks for triggering report on uncaught exception. +// Calls triggered from JS land. +void OnUncaughtException(const FunctionCallbackInfo& info) { + Environment* env = Environment::GetCurrent(info); + Isolate* isolate = env->isolate(); + HandleScope scope(isolate); + std::string filename; + std::shared_ptr options = env->isolate_data()->options(); + + // Trigger report if requested + if (options->report_uncaught_exception) { + TriggerNodeReport( + isolate, env, "exception", __func__, filename, info[0].As()); + } +} + +// Signal handler for report action, called from JS land (util.js) +void OnUserSignal(const FunctionCallbackInfo& info) { + Environment* env = Environment::GetCurrent(info); + Isolate* isolate = env->isolate(); + CHECK(info[0]->IsString()); + Local str = info[0].As(); + String::Utf8Value value(isolate, str); + std::string filename; + TriggerNodeReport( + isolate, env, *value, __func__, filename, info[0].As()); +} + +// Native module initializer function, called when the module is require'd +void InitializeReport(Isolate* isolate, Environment* env) { + // Register the boot time of the process, for + // computing resource consumption average etc. + std::shared_ptr options = env->isolate_data()->options(); + + if (options->report_signal == "") options->report_signal = "SIGUSR2"; +} + +// A method to sync up data elements in the JS land with its +// corresponding elements in the C++ world. Required because +// (i) the tunables are first intercepted through the CLI but +// later modified via APIs. (ii) the report generation events +// are controlled partly from C++ and partly from JS. +void SyncConfig(const FunctionCallbackInfo& info) { + Environment* env = Environment::GetCurrent(info); + Local context = env->context(); + std::shared_ptr options = env->isolate_data()->options(); + + CHECK_EQ(info.Length(), 2); + Local obj; + if (!info[0]->ToObject(context).ToLocal(&obj)) return; + bool sync = info[1].As()->Value(); + + // Events array + Local eventskey = FIXED_ONE_BYTE_STRING(env->isolate(), "events"); + Local events_unchecked; + if (!obj->Get(context, eventskey).ToLocal(&events_unchecked)) return; + Local events; + if (events_unchecked->IsUndefined() || events_unchecked->IsNull()) { + events_unchecked = Array::New(env->isolate(), 0); + if (obj->Set(context, eventskey, events_unchecked).IsNothing()) return; + } + events = events_unchecked.As(); + + // Signal + Local signalkey = env->signal_string(); + Local signal_unchecked; + if (!obj->Get(context, signalkey).ToLocal(&signal_unchecked)) return; + Local signal; + if (signal_unchecked->IsUndefined() || signal_unchecked->IsNull()) + signal_unchecked = signalkey; + signal = signal_unchecked.As(); + + Utf8Value signalstr(env->isolate(), signal); + + // Report file + Local filekey = FIXED_ONE_BYTE_STRING(env->isolate(), "filename"); + Local file_unchecked; + if (!obj->Get(context, filekey).ToLocal(&file_unchecked)) return; + Local file; + if (file_unchecked->IsUndefined() || file_unchecked->IsNull()) + file_unchecked = filekey; + file = file_unchecked.As(); + + Utf8Value filestr(env->isolate(), file); + + // Report file path + Local pathkey = FIXED_ONE_BYTE_STRING(env->isolate(), "path"); + Local path_unchecked; + if (!obj->Get(context, pathkey).ToLocal(&path_unchecked)) return; + Local path; + if (path_unchecked->IsUndefined() || path_unchecked->IsNull()) + path_unchecked = pathkey; + path = path_unchecked.As(); + + Utf8Value pathstr(env->isolate(), path); + + // Report verbosity + Local verbosekey = FIXED_ONE_BYTE_STRING(env->isolate(), "verbose"); + Local verbose_unchecked; + if (!obj->Get(context, verbosekey).ToLocal(&verbose_unchecked)) return; + Local verbose; + if (verbose_unchecked->IsUndefined() || verbose_unchecked->IsNull()) + verbose_unchecked = Boolean::New(env->isolate(), "verbose"); + verbose = verbose_unchecked.As(); + + bool verb = verbose->BooleanValue(context).FromJust(); + + if (sync) { + static const std::string e = "exception"; + static const std::string s = "signal"; + static const std::string f = "fatalerror"; + for (uint32_t i = 0; i < events->Length(); i++) { + Local v; + if (!events->Get(context, i).ToLocal(&v)) return; + Local elem; + if (!v->ToString(context).ToLocal(&elem)) return; + String::Utf8Value buf(env->isolate(), elem); + if (*buf == e) { + options->report_uncaught_exception = true; + } else if (*buf == s) { + options->report_on_signal = true; + } else if (*buf == f) { + options->report_on_fatalerror = true; + } + } + CHECK_NOT_NULL(*signalstr); + options->report_signal = *signalstr; + CHECK_NOT_NULL(*filestr); + options->report_filename = *filestr; + CHECK_NOT_NULL(*pathstr); + options->report_directory = *pathstr; + options->report_verbose = verb; + } else { + int i = 0; + if (options->report_uncaught_exception && + events + ->Set(context, + i++, + FIXED_ONE_BYTE_STRING(env->isolate(), "exception")) + .IsNothing()) + return; + if (options->report_on_signal && + events + ->Set(context, i++, FIXED_ONE_BYTE_STRING(env->isolate(), "signal")) + .IsNothing()) + return; + if (options->report_on_fatalerror && + events + ->Set( + context, i, FIXED_ONE_BYTE_STRING(env->isolate(), "fatalerror")) + .IsNothing()) + return; + + Local signal_value; + Local file_value; + Local path_value; + if (!node::ToV8Value(context, options->report_signal) + .ToLocal(&signal_value)) + return; + if (!obj->Set(context, signalkey, signal_value).FromJust()) return; + + if (!node::ToV8Value(context, options->report_filename) + .ToLocal(&file_value)) + return; + if (!obj->Set(context, filekey, file_value).FromJust()) return; + + if (!node::ToV8Value(context, options->report_directory) + .ToLocal(&path_value)) + return; + if (!obj->Set(context, pathkey, path_value).FromJust()) return; + + if (!obj->Set(context, + verbosekey, + Boolean::New(env->isolate(), options->report_verbose)) + .FromJust()) + return; + } +} + +static void Initialize(Local exports, + Local unused, + Local context) { + Environment* env = Environment::GetCurrent(context); + std::shared_ptr options = env->isolate_data()->options(); + Isolate* isolate = env->isolate(); + InitializeReport(isolate, env); + env->SetMethod(exports, "triggerReport", TriggerReport); + env->SetMethod(exports, "getReport", GetReport); + env->SetMethod(exports, "onUnCaughtException", OnUncaughtException); + env->SetMethod(exports, "onUserSignal", OnUserSignal); + env->SetMethod(exports, "syncConfig", SyncConfig); + + // TODO(gireeshpunathil) if we are retaining this flag, + // insert more verbose information at vital control flow + // points. Right now, it is only this one. + if (options->report_verbose) { + std::cerr << "report: initialization complete, event flags:" << std::endl; + std::cerr << "report_uncaught_exception: " + << options->report_uncaught_exception << std::endl; + std::cerr << "report_on_signal: " << options->report_on_signal << std::endl; + std::cerr << "report_on_fatalerror: " << options->report_on_fatalerror + << std::endl; + std::cerr << "report_signal: " << options->report_signal << std::endl; + std::cerr << "report_filename: " << options->report_filename << std::endl; + std::cerr << "report_directory: " << options->report_directory << std::endl; + std::cerr << "report_verbose: " << options->report_verbose << std::endl; + } +} + +} // namespace report + +NODE_MODULE_CONTEXT_AWARE_INTERNAL(report, report::Initialize) diff --git a/src/node_report_utils.cc b/src/node_report_utils.cc new file mode 100644 index 00000000000000..e93f230d318a6d --- /dev/null +++ b/src/node_report_utils.cc @@ -0,0 +1,299 @@ +#include +#include "env.h" +#include "node_internals.h" +#include "node_options.h" +#include "node_report.h" +#include "util.h" +#include "v8.h" + +namespace report { + +using node::MallocedBuffer; + +// Utility function to format libuv socket information. +void ReportEndpoints(uv_handle_t* h, std::ostringstream& out) { + struct sockaddr_storage addr_storage; + struct sockaddr* addr = reinterpret_cast(&addr_storage); + char hostbuf[NI_MAXHOST]; + char portbuf[NI_MAXSERV]; + uv_any_handle* handle = reinterpret_cast(h); + int addr_size = sizeof(addr_storage); + int rc = -1; + + switch (h->type) { + case UV_UDP: { + rc = uv_udp_getsockname(&(handle->udp), addr, &addr_size); + break; + } + case UV_TCP: { + rc = uv_tcp_getsockname(&(handle->tcp), addr, &addr_size); + break; + } + default: + break; + } + if (rc == 0) { + // getnameinfo will format host and port and handle IPv4/IPv6. + rc = getnameinfo(addr, + addr_size, + hostbuf, + sizeof(hostbuf), + portbuf, + sizeof(portbuf), + NI_NUMERICSERV); + if (rc == 0) { + out << std::string(hostbuf) << ":" << std::string(portbuf); + } + + if (h->type == UV_TCP) { + // Get the remote end of the connection. + rc = uv_tcp_getpeername(&(handle->tcp), addr, &addr_size); + if (rc == 0) { + rc = getnameinfo(addr, + addr_size, + hostbuf, + sizeof(hostbuf), + portbuf, + sizeof(portbuf), + NI_NUMERICSERV); + if (rc == 0) { + out << " connected to "; + out << std::string(hostbuf) << ":" << std::string(portbuf); + } + } else if (rc == UV_ENOTCONN) { + out << " (not connected)"; + } + } + } +} + +// Utility function to format libuv path information. +void ReportPath(uv_handle_t* h, std::ostringstream& out) { + MallocedBuffer buffer(0); + int rc = -1; + size_t size = 0; + uv_any_handle* handle = reinterpret_cast(h); + // First call to get required buffer size. + switch (h->type) { + case UV_FS_EVENT: { + rc = uv_fs_event_getpath(&(handle->fs_event), buffer.data, &size); + break; + } + case UV_FS_POLL: { + rc = uv_fs_poll_getpath(&(handle->fs_poll), buffer.data, &size); + break; + } + default: + break; + } + if (rc == UV_ENOBUFS) { + buffer = MallocedBuffer(size); + switch (h->type) { + case UV_FS_EVENT: { + rc = uv_fs_event_getpath(&(handle->fs_event), buffer.data, &size); + break; + } + case UV_FS_POLL: { + rc = uv_fs_poll_getpath(&(handle->fs_poll), buffer.data, &size); + break; + } + default: + break; + } + if (rc == 0) { + // buffer is not null terminated. + std::string name(buffer.data, size); + out << "filename: " << name; + } + } +} + +// Utility function to walk libuv handles. +void WalkHandle(uv_handle_t* h, void* arg) { + std::string type; + std::ostringstream data; + JSONWriter* writer = reinterpret_cast(arg); + uv_any_handle* handle = reinterpret_cast(h); + + // List all the types so we get a compile warning if we've missed one, + // (using default: supresses the compiler warning). + switch (h->type) { + case UV_UNKNOWN_HANDLE: + type = "unknown"; + break; + case UV_ASYNC: + type = "async"; + break; + case UV_CHECK: + type = "check"; + break; + case UV_FS_EVENT: { + type = "fs_event"; + ReportPath(h, data); + break; + } + case UV_FS_POLL: { + type = "fs_poll"; + ReportPath(h, data); + break; + } + case UV_HANDLE: + type = "handle"; + break; + case UV_IDLE: + type = "idle"; + break; + case UV_NAMED_PIPE: + type = "pipe"; + break; + case UV_POLL: + type = "poll"; + break; + case UV_PREPARE: + type = "prepare"; + break; + case UV_PROCESS: { + type = "process"; + data << "pid: " << handle->process.pid; + break; + } + case UV_STREAM: + type = "stream"; + break; + case UV_TCP: { + type = "tcp"; + ReportEndpoints(h, data); + break; + } + case UV_TIMER: { + uint64_t due = handle->timer.timeout; + uint64_t now = uv_now(handle->timer.loop); + type = "timer"; + data << "repeat: " << uv_timer_get_repeat(&(handle->timer)); + if (due > now) { + data << ", timeout in: " << (due - now) << " ms"; + } else { + data << ", timeout expired: " << (now - due) << " ms ago"; + } + break; + } + case UV_TTY: { + int height, width, rc; + type = "tty"; + rc = uv_tty_get_winsize(&(handle->tty), &width, &height); + if (rc == 0) { + data << "width: " << width << ", height: " << height; + } + break; + } + case UV_UDP: { + type = "udp"; + ReportEndpoints(h, data); + break; + } + case UV_SIGNAL: { + // SIGWINCH is used by libuv so always appears. + // See http://docs.libuv.org/en/v1.x/signal.html + type = "signal"; + data << "signum: " << handle->signal.signum +#ifndef _WIN32 + << " (" << node::signo_string(handle->signal.signum) << ")" +#endif + << ""; + break; + } + case UV_FILE: + type = "file"; + break; + // We shouldn't see "max" type + case UV_HANDLE_TYPE_MAX: + type = "max"; + break; + } + + if (h->type == UV_TCP || h->type == UV_UDP +#ifndef _WIN32 + || h->type == UV_NAMED_PIPE +#endif + ) { + // These *must* be 0 or libuv will set the buffer sizes to the non-zero + // values they contain. + int send_size = 0; + int recv_size = 0; + if (h->type == UV_TCP || h->type == UV_UDP) { + data << ", "; + } + uv_send_buffer_size(h, &send_size); + uv_recv_buffer_size(h, &recv_size); + data << "send buffer size: " << send_size + << ", recv buffer size: " << recv_size; + } + + if (h->type == UV_TCP || h->type == UV_NAMED_PIPE || h->type == UV_TTY || + h->type == UV_UDP || h->type == UV_POLL) { + uv_os_fd_t fd_v; + uv_os_fd_t* fd = &fd_v; + int rc = uv_fileno(h, fd); + // uv_os_fd_t is an int on Unix and HANDLE on Windows. +#ifndef _WIN32 + if (rc == 0) { + switch (fd_v) { + case 0: + data << ", stdin"; + break; + case 1: + data << ", stdout"; + break; + case 2: + data << ", stderr"; + break; + default: + data << ", file descriptor: " << static_cast(fd_v); + break; + } + } +#endif + } + + if (h->type == UV_TCP || h->type == UV_NAMED_PIPE || h->type == UV_TTY) { + data << ", write queue size: " << handle->stream.write_queue_size; + data << (uv_is_readable(&handle->stream) ? ", readable" : "") + << (uv_is_writable(&handle->stream) ? ", writable" : ""); + } + + writer->json_start(); + writer->json_keyvalue("type", type); + writer->json_keyvalue("is_active", std::to_string(uv_is_active(h))); + writer->json_keyvalue("is_referenced", std::to_string(uv_has_ref(h))); + writer->json_keyvalue("address", + std::to_string(reinterpret_cast(h))); + writer->json_keyvalue("details", data.str()); + writer->json_end(); +} + +static std::string findAndReplace(const std::string& str, + const std::string& old, + const std::string& neu) { + std::string ret = str; + size_t pos = 0; + while ((pos = ret.find(old, pos)) != std::string::npos) { + ret.replace(pos, old.length(), neu); + pos += neu.length(); + } + return ret; +} + +std::string EscapeJsonChars(const std::string& str) { + std::string ret = str; + ret = findAndReplace(ret, "\\", "\\\\"); + ret = findAndReplace(ret, "\\u", "\\u"); + ret = findAndReplace(ret, "\n", "\\n"); + ret = findAndReplace(ret, "\f", "\\f"); + ret = findAndReplace(ret, "\r", "\\r"); + ret = findAndReplace(ret, "\b", "\\b"); + ret = findAndReplace(ret, "\t", "\\t"); + ret = findAndReplace(ret, "\"", "\\\""); + return ret; +} + +} // namespace report