diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..6fa3448 --- /dev/null +++ b/.gitignore @@ -0,0 +1,38 @@ +# Compiled Object files +*.slo +*.lo +*.o +*.obj + +# Precompiled Headers +*.gch +*.pch + +# Compiled Dynamic libraries +*.so +*.dylib +*.dll + +# Fortran module files +*.mod +*.smod + +# Compiled Static libraries +*.lai +*.la +*.a +*.lib + +# Executables +*.exe +*.out +*.app +*.node + +# Project files +*.project +*.cproject + +# Build directories +/build/ +/node_modules/ diff --git a/README.md b/README.md index 8c77476..9728867 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,65 @@ # nodereport -nodereport is an add-on for Node.js, delivered as an NPM module, +nodereport is an add-on for Node.js, delivered as an NPM native module, which provides a human-readable diagnostic summary report, written to file. The report is intended for development, test and production use, to capture and preserve information for problem determination. It includes Javascript and native stack traces, heap statistics, platform information and resource usage etc. With the report enabled, -reports are triggered on unhandled exceptions, fatal errors, signals +reports can be triggered on unhandled exceptions, fatal errors, signals and calls to a Javascript API. + +Usage: + + npm install nodejs/nodereport + + var nodereport = require('nodereport'); + +By default, this will allow a NodeReport to be triggered via an API +call from a JavaScript application. The filename of the NodeReport is +returned. The default filename includes the date, time, PID and a +sequence number. Alternatively a filename can be specified on the API call. + + nodereport.triggerReport(); + + var filename = nodereport.triggerReport(); + + nodereport.triggerReport("myReportName"); + +Content of the NodeReport in the initial implementation consists of a +header section containing the event type, date, time, PID and Node version, +sections containing Javascript and native stack traces, a section containing +V8 heap information, a section containing libuv handle information and an OS +platform information section showing CPU and memory usage and system limits. +The following messages are issued to stderr when a NodeReport is triggered: + + Writing Node.js error report to file: NodeReport.201605113.145311.26249.001.txt + Node.js error report completed + +A NodeReport can also be triggered on unhandled exception and fatal error +events, and/or signals (Linux/OSX only). These and other options can be +enabled or disabled using the following APIs: + + nodereport.setEvents("exception+fatalerror+signal+apicall"); + nodereport.setSignal("SIGUSR2|SIGQUIT"); + nodereport.setFileName("stdout|stderr|"); + nodereport.setDirectory(""); + nodereport.setCoreDump("yes|no"); + nodereport.setVerbose("yes|no"); + +Configuration on module initialisation is also available via environment variables: + + export NODEREPORT_EVENTS=exception+fatalerror+signal+apicall + export NODEREPORT_SIGNAL=SIGUSR2|SIGQUIT + export NODEREPORT_FILENAME=stdout|stderr| + export NODEREPORT_DIRECTORY= + export NODEREPORT_COREDUMP=yes|no + export NODEREPORT_VERBOSE=yes|no + +Sample programs for triggering NodeReports are provided in the +node_modules/nodereport/demo directory: + + api.js - NodeReport triggered by Javascript API call + exception.js - NodeReport triggered by unhandled exception + fatalerror.js - NodeReport triggered by fatal error on Javascript heap out of memory + loop.js - looping application, NodeReport triggered using kill -USR2 diff --git a/binding.gyp b/binding.gyp new file mode 100644 index 0000000..9e4cefd --- /dev/null +++ b/binding.gyp @@ -0,0 +1,30 @@ +{ + "targets": [ + { + "target_name": "nodereport", + "sources": [ "src/node_report.cc", "src/module.cc" ], + "include_dirs": [ ':8080/ or http://localhost:8080/'); + +setTimeout(function(){ + console.log('api_call.js: test timeout expired, exiting.'); + process.exit(0); +}, 60000); diff --git a/demo/exception.js b/demo/exception.js new file mode 100644 index 0000000..e677acf --- /dev/null +++ b/demo/exception.js @@ -0,0 +1,33 @@ +// Example - generation of NodeReport on uncaught exception +require('nodereport').setEvents("exception"); +var http = require("http"); + +var count = 0; + +function my_listener(request, response) { + switch(count++) { + case 0: + response.writeHead(200,{"Content-Type": "text/plain"}); + response.write("\nRunning NodeReport exception demo... refresh page to cause exception (application will terminate)"); + response.end(); + break; + default: + throw new UserException('*** exception.js: exception thrown from my_listener()'); + } +} + +function UserException(message) { + this.message = message; + this.name = "UserException"; +} + +var http_server = http.createServer(my_listener); +http_server.listen(8080); + +console.log('exception.js: Node running'); +console.log('exception.js: Go to http://:8080/ or http://localhost:8080/'); + +setTimeout(function() { + console.log('exception.js: test timeout expired, exiting.'); + process.exit(0); +}, 60000); diff --git a/demo/fatalerror.js b/demo/fatalerror.js new file mode 100644 index 0000000..0abba5c --- /dev/null +++ b/demo/fatalerror.js @@ -0,0 +1,41 @@ +// Example - generation of Nodereport on fatal error (Javascript heap OOM) +require('nodereport').setEvents("fatalerror"); +var http = require('http'); + +var count = 0; + +function my_listener(request, response) { + switch(count++) { + case 0: + response.writeHead(200,{"Content-Type": "text/plain"}); + response.write("\nRunning NodeReport fatal error demo... refresh page to trigger excessive memory usage (application will terminate)"); + response.end(); + break; + case 1: + console.log('heap_oom.js: allocating excessive Javascript heap memory....'); + var list = []; + while (true) { + list.push(new MyRecord()); + } + response.end(); + break; + } +} + +function MyRecord() { + this.name = 'foo'; + this.id = 128; + this.account = 98454324; +} + +var http_server = http.createServer(my_listener); +http_server.listen(8080); + +console.log('fatalerror.js: Node running'); +console.log('fatalerror.js: Note: heap default is 1.4Gb, use --max-old-space-size= to change'); +console.log('fatalerror.js: Go to http://:8080/ or http://localhost:8080/'); + +setTimeout(function(){ + console.log('fatalerror.js: timeout expired, exiting.'); + process.exit(0); +}, 60000); diff --git a/demo/loop.js b/demo/loop.js new file mode 100644 index 0000000..b50e44b --- /dev/null +++ b/demo/loop.js @@ -0,0 +1,53 @@ +// Example - geneation of Nodereport via signal for a looping application +require('nodereport').setEvents("signal"); +var http = require("http"); + +var count = 0; + +function my_listener(request, response) { + switch(count++) { + case 0: + response.writeHead(200,{"Content-Type": "text/plain"}); + response.write("\nRunning NodeReport looping application demo. Node process ID = " + process.pid); + response.write("\n\nRefresh page to enter loop, then use 'kill -USR2 " + process.pid + "' to trigger NodeReport"); + response.end(); + break; + case 1: + console.log("loop.js: going to loop now, use 'kill -USR2 " + process.pid + "' to trigger NodeReport"); + var list = []; + for (var i=0; i<10000000000; i++) { + for (var j=0; i<1000; i++) { + list.push(new MyRecord()); + } + for (var j=0; i<1000; i++) { + list[j].id += 1; + list[j].account += 2; + } + for (var j=0; i<1000; i++) { + list.pop(); + } + } + response.writeHead(200,{"Content-Type": "text/plain"}); + response.write("\nNodeReport demo.... finished looping"); + response.end(); + break; + default: + } +} + +function MyRecord() { + this.name = 'foo'; + this.id = 128; + this.account = 98454324; +} + +var http_server = http.createServer(my_listener); +http_server.listen(8080); + +console.log('loop.js: Node running'); +console.log('loop.js: Go to http://:8080/ or http://localhost:8080/'); + +setTimeout(function() { + console.log('loop.js: timeout expired, exiting.'); + process.exit(0); +}, 60000); diff --git a/package.json b/package.json new file mode 100644 index 0000000..9e33249 --- /dev/null +++ b/package.json @@ -0,0 +1,21 @@ +{ + "name": "nodereport", + "main": "build/Release/nodereport.node", + "version": "0.1.0", + "description": "Diagnostic NodeReport", + "homepage": "https://github.com/nodejs/nodereport", + "repository": { + "type": "git", + "url": "https://github.com/nodejs/nodereport.git" + }, + "engines": { + "node": ">=4.0.0" + }, + "dependencies": { + "nan": "^2.3.5" + }, + "license": "MIT", + "scripts": { + "test": "node test/autorun.js" + } +} diff --git a/src/module.cc b/src/module.cc new file mode 100644 index 0000000..984a86b --- /dev/null +++ b/src/module.cc @@ -0,0 +1,382 @@ +#include "node_report.h" +#include + +namespace nodereport { + +// Internal/static function declarations +static void OnFatalError(const char* location, const char* message); +bool OnUncaughtException(v8::Isolate* isolate); +#ifndef _WIN32 +static void SignalDumpAsyncCallback(uv_async_t* handle); +inline void* ReportSignalThreadMain(void* unused); +static int StartWatchdogThread(void* (*thread_main)(void* unused)); +static void RegisterSignalHandler(int signo, void (*handler)(int), + struct sigaction* saved_sa); +static void RestoreSignalHandler(int signo, struct sigaction* saved_sa); +static void SignalDump(int signo); +static void SetupSignalHandler(); +#endif + +// Default nodereport option settings +static unsigned int nodereport_events = NR_APICALL; +static unsigned int nodereport_core = 1; +static unsigned int nodereport_verbose = 0; +#ifdef _WIN32 // signal trigger not supported on Windows +static unsigned int nodereport_signal = 0; +#else // trigger signal supported on Unix platforms and OSX +static unsigned int nodereport_signal = SIGUSR2; // default signal is SIGUSR2 +static int report_signal = 0; // atomic for signal handling in progress +static uv_sem_t report_semaphore; // semaphore for hand-off to watchdog +static uv_async_t nodereport_trigger_async; // async handle for event loop +static uv_mutex_t node_isolate_mutex; // mutex for wachdog thread +static struct sigaction saved_sa; // saved signal action +#endif + +// State variables for v8 hooks and signal initialisation +static bool exception_hook_initialised = false; +static bool error_hook_initialised = false; +static bool signal_thread_initialised = false; + +static v8::Isolate* node_isolate; + +/******************************************************************************* + * External JavaScript API for triggering a NodeReport + * + ******************************************************************************/ +NAN_METHOD(TriggerReport) { + Nan::HandleScope scope; + v8::Isolate* isolate = info.GetIsolate(); + char filename[NR_MAXNAME + 1] = ""; + + if (info[0]->IsString()) { + // Filename parameter supplied + Nan::Utf8String filename_parameter(info[0]); + if (filename_parameter.length() < NR_MAXNAME) { + snprintf(filename, sizeof(filename), "%s", *filename_parameter); + } else { + Nan::ThrowError("nodereport: filename parameter is too long"); + } + } + if (nodereport_events & NR_APICALL) { + TriggerNodeReport(isolate, kJavaScript, "JavaScript API", __func__, filename); + // Return value is the NodeReport filename + info.GetReturnValue().Set(Nan::New(filename).ToLocalChecked()); + } +} + +/******************************************************************************* + * External JavaScript APIs for nodereport configuration + * + ******************************************************************************/ +NAN_METHOD(SetEvents) { + Nan::Utf8String parameter(info[0]); + v8::Isolate* isolate = info.GetIsolate(); + unsigned int previous_events = nodereport_events; // save previous settings + nodereport_events = ProcessNodeReportEvents(*parameter); + + // If NodeReport newly requested for fatalerror, set up the V8 call-back + if ((nodereport_events & NR_FATALERROR) && (error_hook_initialised == false)) { + isolate->SetFatalErrorHandler(OnFatalError); + error_hook_initialised = true; + } + + // If NodeReport newly requested for exceptions, tell V8 to capture stack trace and set up the callback + if ((nodereport_events & NR_EXCEPTION) && (exception_hook_initialised == false)) { + isolate->SetCaptureStackTraceForUncaughtExceptions(true, 32, v8::StackTrace::kDetailed); + // The hook for uncaught exception won't get called unless the --abort_on_uncaught_exception option is set + v8::V8::SetFlagsFromString("--abort_on_uncaught_exception", sizeof("--abort_on_uncaught_exception")-1); + isolate->SetAbortOnUncaughtExceptionCallback(OnUncaughtException); + exception_hook_initialised = true; + } + +#ifndef _WIN32 + // If NodeReport newly requested on external user signal set up watchdog thread and handler + if ((nodereport_events & NR_SIGNAL) && (signal_thread_initialised == false)) { + SetupSignalHandler(); + } + // If NodeReport no longer required on external user signal, reset the OS signal handler + if (!(nodereport_events & NR_SIGNAL) && (previous_events & NR_SIGNAL)) { + RestoreSignalHandler(nodereport_signal, &saved_sa); + } +#endif +} +NAN_METHOD(SetCoreDump) { + Nan::Utf8String parameter(info[0]); + nodereport_core = ProcessNodeReportCoreSwitch(*parameter); +} +NAN_METHOD(SetSignal) { +#ifndef _WIN32 + Nan::Utf8String parameter(info[0]); + unsigned int previous_signal = nodereport_signal; // save previous setting + nodereport_signal = ProcessNodeReportSignal(*parameter); + + // If signal event active and selected signal has changed, switch the OS signal handler + if ((nodereport_events & NR_SIGNAL) && (nodereport_signal != previous_signal)) { + RestoreSignalHandler(previous_signal, &saved_sa); + RegisterSignalHandler(nodereport_signal, SignalDump, &saved_sa); + } +#endif +} +NAN_METHOD(SetFileName) { + Nan::Utf8String parameter(info[0]); + ProcessNodeReportFileName(*parameter); +} +NAN_METHOD(SetDirectory) { + Nan::Utf8String parameter(info[0]); + ProcessNodeReportDirectory(*parameter); +} +NAN_METHOD(SetVerbose) { + Nan::Utf8String parameter(info[0]); + nodereport_verbose = ProcessNodeReportVerboseSwitch(*parameter); +} + +/******************************************************************************* + * Callbacks for triggering NodeReport on fatal error, uncaught exception and + * external signals + ******************************************************************************/ +static void OnFatalError(const char* location, const char* message) { + if (location) { + fprintf(stderr, "FATAL ERROR: %s %s\n", location, message); + } else { + fprintf(stderr, "FATAL ERROR: %s\n", message); + } + // Trigger NodeReport if requested + if (nodereport_events & NR_FATALERROR) { + TriggerNodeReport(Isolate::GetCurrent(), kFatalError, message, location, nullptr); + } + fflush(stderr); + if (nodereport_core) { + raise(SIGABRT); // core dump requested (default) + } else { + exit(1); // user specified that no core dump is wanted, just exit + } +} + +bool OnUncaughtException(v8::Isolate* isolate) { + // Trigger NodeReport if required + if (nodereport_events & NR_EXCEPTION) { + TriggerNodeReport(isolate, kException, "exception", __func__, nullptr); + } + return nodereport_core; +} + +#ifndef _WIN32 +static void SignalDumpInterruptCallback(Isolate* isolate, void* data) { + if (report_signal != 0) { + if (nodereport_verbose) { + fprintf(stdout, "nodereport: SignalDumpInterruptCallback handling signal\n"); + } + if (nodereport_events & NR_SIGNAL) { + if (nodereport_verbose) { + fprintf(stdout, "nodereport: SignalDumpInterruptCallback triggering NodeReport\n"); + } + TriggerNodeReport(isolate, kSignal_JS, + node::signo_string(report_signal), __func__, nullptr); + } + report_signal = 0; + } +} +static void SignalDumpAsyncCallback(uv_async_t* handle) { + if (report_signal != 0) { + if (nodereport_verbose) { + fprintf(stdout, "nodereport: SignalDumpAsyncCallback handling signal\n"); + } + if (nodereport_events & NR_SIGNAL) { + if (nodereport_verbose) { + fprintf(stdout, "nodereport: SignalDumpAsyncCallback triggering NodeReport\n"); + } + TriggerNodeReport(Isolate::GetCurrent(), kSignal_UV, + node::signo_string(report_signal), __func__, nullptr); + } + report_signal = 0; + } +} + +/******************************************************************************* + * Utility functions for signal handling support (platforms except Windows) + * - RegisterSignalHandler() - register a raw OS signal handler + * - SignalDump() - implementation of raw OS signal handler + * - StartWatchdogThread() - create a watchdog thread + * - ReportSignalThreadMain() - implementation of watchdog thread + * - SetupSignalHandler() - initialisation of signal handlers and threads + ******************************************************************************/ +// Utility function to register an OS signal handler +static void RegisterSignalHandler(int signo, void (*handler)(int), + struct sigaction* saved_sa) { + struct sigaction sa; + memset(&sa, 0, sizeof(sa)); + sa.sa_handler = handler; + sigfillset(&sa.sa_mask); // mask all signals while in the handler + sigaction(signo, &sa, saved_sa); +} + +// Utility function to restore an OS signal handler to its previous setting +static void RestoreSignalHandler(int signo, struct sigaction* saved_sa) { + sigaction(signo, saved_sa, nullptr); +} + +// Raw signal handler for triggering a NodeReport - runs on an arbitrary thread +static void SignalDump(int signo) { + // Check atomic for NodeReport already pending, storing the signal number + if (__sync_val_compare_and_swap(&report_signal, 0, signo) == 0) { + uv_sem_post(&report_semaphore); // Hand-off to watchdog thread + } +} + +// Utility function to start a watchdog thread - used for processing signals +static int StartWatchdogThread(void* (*thread_main)(void* unused)) { + pthread_attr_t attr; + pthread_attr_init(&attr); + // Minimise the stack size, except on FreeBSD where the minimum is too low +#ifndef __FreeBSD__ + pthread_attr_setstacksize(&attr, PTHREAD_STACK_MIN); +#endif // __FreeBSD__ + pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED); + + sigset_t sigmask; + sigfillset(&sigmask); + pthread_sigmask(SIG_SETMASK, &sigmask, &sigmask); + pthread_t thread; + const int err = pthread_create(&thread, &attr, thread_main, nullptr); + pthread_sigmask(SIG_SETMASK, &sigmask, nullptr); + pthread_attr_destroy(&attr); + if (err != 0) { + fprintf(stderr, "nodereport: StartWatchdogThread pthread_create() failed: %s\n", strerror(err)); + fflush(stderr); + return -err; + } + return 0; +} + +// Watchdog thread implementation for signal-triggered NodeReport +inline void* ReportSignalThreadMain(void* unused) { + for (;;) { + uv_sem_wait(&report_semaphore); + if (nodereport_verbose) { + fprintf(stdout, "nodereport: signal %s received\n", node::signo_string(report_signal)); + } + uv_mutex_lock(&node_isolate_mutex); + if (auto isolate = node_isolate) { + // Request interrupt callback for running JavaScript code + isolate->RequestInterrupt(SignalDumpInterruptCallback, nullptr); + // Event loop may be idle, so also request an async callback + uv_async_send(&nodereport_trigger_async); + } + uv_mutex_unlock(&node_isolate_mutex); + } + return nullptr; +} + +// Utility function to initialise signal handlers and threads +static void SetupSignalHandler() { + int rc = uv_sem_init(&report_semaphore, 0); + if (rc != 0) { + fprintf(stderr, "nodereport: initialization failed, uv_sem_init() returned %d\n", rc); + Nan::ThrowError("nodereport: initialization failed, uv_sem_init() returned error\n"); + } + rc = uv_mutex_init(&node_isolate_mutex); + if (rc != 0) { + fprintf(stderr, "nodereport: initialization failed, uv_mutex_init() returned %d\n", rc); + Nan::ThrowError("nodereport: initialization failed, uv_mutex_init() returned error\n"); + } + + if (StartWatchdogThread(ReportSignalThreadMain) == 0) { + rc = uv_async_init(uv_default_loop(), &nodereport_trigger_async, SignalDumpAsyncCallback); + if (rc != 0) { + fprintf(stderr, "nodereport: initialization failed, uv_async_init() returned %d\n", rc); + Nan::ThrowError("nodereport: initialization failed, uv_async_init() returned error\n"); + } + uv_unref(reinterpret_cast(&nodereport_trigger_async)); + RegisterSignalHandler(nodereport_signal, SignalDump, &saved_sa); + signal_thread_initialised = true; + } +} +#endif + +/******************************************************************************* + * Native module initializer function, called when the module is require'd + * + ******************************************************************************/ +void Initialize(v8::Local exports) { + v8::Isolate* isolate = Isolate::GetCurrent(); + node_isolate = isolate; + + SetLoadTime(); + + const char* verbose_switch = secure_getenv("NODEREPORT_VERBOSE"); + if (verbose_switch != nullptr) { + nodereport_verbose = ProcessNodeReportVerboseSwitch(verbose_switch); + } + const char* trigger_events = secure_getenv("NODEREPORT_EVENTS"); + if (trigger_events != nullptr) { + nodereport_events = ProcessNodeReportEvents(trigger_events); + } + const char* core_dump_switch = secure_getenv("NODEREPORT_COREDUMP"); + if (core_dump_switch != nullptr) { + nodereport_core = ProcessNodeReportCoreSwitch(core_dump_switch); + } + const char* trigger_signal = secure_getenv("NODEREPORT_SIGNAL"); + if (trigger_signal != nullptr) { + nodereport_signal = ProcessNodeReportSignal(trigger_signal); + } + const char* report_name = secure_getenv("NODEREPORT_FILENAME"); + if (report_name != nullptr) { + ProcessNodeReportFileName(report_name); + } + const char* directory_name = secure_getenv("NODEREPORT_DIRECTORY"); + if (directory_name != nullptr) { + ProcessNodeReportDirectory(directory_name); + } + + // If NodeReport requested for fatalerror, set up the V8 call-back + if (nodereport_events & NR_FATALERROR) { + isolate->SetFatalErrorHandler(OnFatalError); + error_hook_initialised = true; + } + + // If NodeReport requested for exceptions, tell V8 to capture stack trace and set up the callback + if (nodereport_events & NR_EXCEPTION) { + isolate->SetCaptureStackTraceForUncaughtExceptions(true, 32, v8::StackTrace::kDetailed); + // The hook for uncaught exception won't get called unless the --abort_on_uncaught_exception option is set + v8::V8::SetFlagsFromString("--abort_on_uncaught_exception", sizeof("--abort_on_uncaught_exception")-1); + isolate->SetAbortOnUncaughtExceptionCallback(OnUncaughtException); + exception_hook_initialised = true; + } + +#ifndef _WIN32 + // If NodeReport requested on external user signal set up watchdog thread and callbacks + if (nodereport_events & NR_SIGNAL) { + SetupSignalHandler(); + } +#endif + + exports->Set(Nan::New("triggerReport").ToLocalChecked(), + Nan::New(TriggerReport)->GetFunction()); + exports->Set(Nan::New("setEvents").ToLocalChecked(), + Nan::New(SetEvents)->GetFunction()); + exports->Set(Nan::New("setCoreDump").ToLocalChecked(), + Nan::New(SetCoreDump)->GetFunction()); + exports->Set(Nan::New("setSignal").ToLocalChecked(), + Nan::New(SetSignal)->GetFunction()); + exports->Set(Nan::New("setFileName").ToLocalChecked(), + Nan::New(SetFileName)->GetFunction()); + exports->Set(Nan::New("setDirectory").ToLocalChecked(), + Nan::New(SetDirectory)->GetFunction()); + exports->Set(Nan::New("setVerbose").ToLocalChecked(), + Nan::New(SetVerbose)->GetFunction()); + + if (nodereport_verbose) { +#ifdef _WIN32 + fprintf(stdout, "nodereport: initialization complete, event flags: %#x core flag: %#x\n", + nodereport_events, nodereport_core); +#else + fprintf(stdout, "nodereport: initialization complete, event flags: %#x core flag: %#x signal flag: %#x\n", + nodereport_events, nodereport_core, nodereport_signal); +#endif + } +} + +NODE_MODULE(nodereport, Initialize) + +} // namespace nodereport + diff --git a/src/node_report.cc b/src/node_report.cc new file mode 100644 index 0000000..35aa4ec --- /dev/null +++ b/src/node_report.cc @@ -0,0 +1,804 @@ +#include "node_report.h" +#include "node_version.h" +#include "v8.h" +#include "time.h" +#include "zlib.h" +#include "ares.h" + +#include +#include +#include +#include + +#if !defined(_MSC_VER) +#include +#endif + +#ifdef _WIN32 +#include +#include +#include +#include +#else +#include +#include +#include +#include +#include +#include +#include +#endif + +#ifndef _WIN32 +extern char** environ; +#endif + +namespace nodereport { + +using v8::HeapSpaceStatistics; +using v8::HeapStatistics; +using v8::Isolate; +using v8::Local; +using v8::Message; +using v8::StackFrame; +using v8::StackTrace; +using v8::String; +using v8::V8; + +// Internal/static function declarations +static void PrintVersionInformation(FILE* fp); +static void PrintJavaScriptStack(FILE* fp, Isolate* isolate, DumpEvent event, const char* location); +static void PrintStackFromStackTrace(FILE* fp, Isolate* isolate, DumpEvent event); +static void PrintStackFrame(FILE* fp, Isolate* isolate, Local frame, int index, void* pc); +static void PrintNativeStack(FILE* fp); +#ifndef _WIN32 +static void PrintResourceUsage(FILE* fp); +#endif +static void PrintGCStatistics(FILE* fp, Isolate* isolate); +static void PrintSystemInformation(FILE* fp, Isolate* isolate); +static void WriteInteger(FILE* fp, size_t value); + +// Global variables +static int seq = 0; // sequence number for NodeReport filenames +const char* v8_states[] = {"JS", "GC", "COMPILER", "OTHER", "EXTERNAL", "IDLE"}; +static bool report_active = false; // recursion protection +static char report_filename[NR_MAXNAME + 1] = ""; +static char report_directory[NR_MAXPATH + 1] = ""; // defaults to current working directory +#ifdef _WIN32 +static SYSTEMTIME loadtime_tm_struct; // module load time +#else // UNIX, OSX +static struct tm loadtime_tm_struct; // module load time +#endif + +/******************************************************************************* + * Functions to process nodereport configuration options: + * Trigger event selection + * Core dump yes/no switch + * Trigger signal selection + * NodeReport filename + * NodeReport directory + * Verbose mode + ******************************************************************************/ +unsigned int ProcessNodeReportEvents(const char* args) { + // Parse the supplied event types + unsigned int event_flags = 0; + const char* cursor = args; + while (*cursor != '\0') { + if (!strncmp(cursor, "exception", sizeof("exception") - 1)) { + event_flags |= NR_EXCEPTION; + cursor += sizeof("exception") - 1; + } else if (!strncmp(cursor, "fatalerror", sizeof("fatalerror") - 1)) { + event_flags |= NR_FATALERROR; + cursor += sizeof("fatalerror") - 1; + } else if (!strncmp(cursor, "signal", sizeof("signal") - 1)) { + event_flags |= NR_SIGNAL; + cursor += sizeof("signal") - 1; + } else if (!strncmp(cursor, "apicall", sizeof("apicall") - 1)) { + event_flags |= NR_APICALL; + cursor += sizeof("apicall") - 1; + } else { + fprintf(stderr, "Unrecognised argument for nodereport events option: %s\n", cursor); + return 0; + } + if (*cursor == '+') { + cursor++; // Hop over the '+' separator + } + } + return event_flags; +} + +unsigned int ProcessNodeReportCoreSwitch(const char* args) { + if (strlen(args) == 0) { + fprintf(stderr, "Missing argument for nodereport core switch option\n"); + } else { + // Parse the supplied switch + if (!strncmp(args, "yes", sizeof("yes") - 1) || !strncmp(args, "true", sizeof("true") - 1)) { + return 1; + } else if (!strncmp(args, "no", sizeof("no") - 1) || !strncmp(args, "false", sizeof("false") - 1)) { + return 0; + } else { + fprintf(stderr, "Unrecognised argument for nodereport core switch option: %s\n", args); + } + } + return 1; // Default is to produce core dumps +} + +unsigned int ProcessNodeReportSignal(const char* args) { +#ifdef _WIN32 + return 0; // no-op on Windows +#else + if (strlen(args) == 0) { + fprintf(stderr, "Missing argument for nodereport signal option\n"); + } else { + // Parse the supplied switch + if (!strncmp(args, "SIGUSR2", sizeof("SIGUSR2") - 1)) { + return SIGUSR2; + } else if (!strncmp(args, "SIGQUIT", sizeof("SIGQUIT") - 1)) { + return SIGQUIT; + } else { + fprintf(stderr, "Unrecognised argument for nodereport signal option: %s\n", args); + } + } + return SIGUSR2; // Default signal is SIGUSR2 +#endif +} + +void ProcessNodeReportFileName(const char* args) { + if (strlen(args) == 0) { + fprintf(stderr, "Missing argument for nodereport filename option\n"); + return; + } + if (strlen(args) > NR_MAXNAME) { + fprintf(stderr, "Supplied nodereport filename too long (max %d characters)\n", NR_MAXNAME); + return; + } + snprintf(report_filename, sizeof(report_filename), "%s", args); +} + +void ProcessNodeReportDirectory(const char* args) { + if (strlen(args) == 0) { + fprintf(stderr, "Missing argument for nodereport directory option\n"); + return; + } + if (strlen(args) > NR_MAXPATH) { + fprintf(stderr, "Supplied nodereport directory path too long (max %d characters)\n", NR_MAXPATH); + return; + } + snprintf(report_directory, sizeof(report_directory), "%s", args); +} + +unsigned int ProcessNodeReportVerboseSwitch(const char* args) { + if (strlen(args) == 0) { + fprintf(stderr, "Missing argument for nodereport verbose switch option\n"); + return 0; + } + // Parse the supplied switch + if (!strncmp(args, "yes", sizeof("yes") - 1) || !strncmp(args, "true", sizeof("true") - 1)) { + return 1; + } else if (!strncmp(args, "no", sizeof("no") - 1) || !strncmp(args, "false", sizeof("false") - 1)) { + return 0; + } else { + fprintf(stderr, "Unrecognised argument for nodereport verbose switch option: %s\n", args); + } + return 0; // Default is verbose mode off +} + +/******************************************************************************* + * Function to save the nodereport module load time value + *******************************************************************************/ +void SetLoadTime() { +#ifdef _WIN32 + GetLocalTime(&loadtime_tm_struct); +#else // UNIX, OSX + struct timeval time_val; + gettimeofday(&time_val, nullptr); + localtime_r(&time_val.tv_sec, &loadtime_tm_struct); +#endif +} +/******************************************************************************* + * Main API function to write a NodeReport to file. + * + * Parameters: + * Isolate* isolate + * DumpEvent event + * const char* message + * const char* location + * char* name - in/out - returns the NodeReport filename + ******************************************************************************/ +void TriggerNodeReport(Isolate* isolate, DumpEvent event, const char* message, const char* location, char* name) { + // Recursion check for NodeReport in progress, bail out + if (report_active) return; + report_active = true; + + // Obtain the current time and the pid (platform dependent) +#ifdef _WIN32 + SYSTEMTIME tm_struct; + GetLocalTime(&tm_struct); + DWORD pid = GetCurrentProcessId(); +#else // UNIX, OSX + struct timeval time_val; + struct tm tm_struct; + gettimeofday(&time_val, nullptr); + localtime_r(&time_val.tv_sec, &tm_struct); + pid_t pid = getpid(); +#endif + + // Determine the required NodeReport filename. In order of priority: + // 1) supplied on API 2) configured on startup 3) default generated + char filename[NR_MAXNAME + 1] = ""; + if (name != nullptr && strlen(name) > 0) { + // Filename was specified as API parameter, use that + snprintf(filename, sizeof(filename), "%s", name); + } else if (strlen(report_filename) > 0) { + // File name was supplied via start-up option, use that + snprintf(filename, sizeof(filename), "%s", report_filename); + } else { + // Construct the NodeReport filename, with timestamp, pid and sequence number + snprintf(filename, sizeof(filename), "%s", "NodeReport"); + seq++; +#ifdef _WIN32 + snprintf(&filename[strlen(filename)], sizeof(filename) - strlen(filename), + ".%4d%02d%02d.%02d%02d%02d.%d.%03d.txt", + tm_struct.wYear, tm_struct.wMonth, tm_struct.wDay, + tm_struct.wHour, tm_struct.wMinute, tm_struct.wSecond, + pid, seq); +#else // UNIX, OSX + snprintf(&filename[strlen(filename)], sizeof(filename) - strlen(filename), + ".%4d%02d%02d.%02d%02d%02d.%d.%03d.txt", + 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, + pid, seq); +#endif + } + + // Open the NodeReport file stream for writing. Supports stdout/err, user-specified or (default) generated name + FILE* fp = nullptr; + if (!strncmp(filename, "stdout", sizeof("stdout") - 1)) { + fp = stdout; + } else if (!strncmp(filename, "stderr", sizeof("stderr") - 1)) { + fp = stderr; + } else { + // Regular file. Append filename to directory path if one was specified + if (strlen(report_directory) > 0) { + char pathname[NR_MAXPATH + NR_MAXNAME + 1] = ""; +#ifdef _WIN32 + snprintf(pathname, sizeof(pathname), "%s%s%s", report_directory, "\\", filename); +#else + snprintf(pathname, sizeof(pathname), "%s%s%s", report_directory, "/", filename); +#endif + fp = fopen(pathname, "w"); + } else { + fp = fopen(filename, "w"); + } + // Check for errors on the file open + if (fp == nullptr) { + if (strlen(report_directory) > 0) { + fprintf(stderr, "\nFailed to open Node.js report file: %s directory: %s (errno: %d)\n", filename, report_directory, errno); + } else { + fprintf(stderr, "\nFailed to open Node.js report file: %s (errno: %d)\n", filename, errno); + } + return; + } else { + fprintf(stderr, "\nWriting Node.js report to file: %s\n", filename); + } + } + + // File stream opened OK, now start printing the NodeReport content, starting with the title + // and header information (event, filename, timestamp and pid) + fprintf(fp, "================================================================================\n"); + fprintf(fp, "==== NodeReport ================================================================\n"); + fprintf(fp, "\nEvent: %s, location: \"%s\"\n", message, location); + fprintf(fp, "Filename: %s\n", filename); + + // Print dump event and module load date/time stamps +#ifdef _WIN32 + fprintf(fp, "Dump event time: %4d/%02d/%02d %02d:%02d:%02d\n", + tm_struct.wYear, tm_struct.wMonth, tm_struct.wDay, + tm_struct.wHour, tm_struct.wMinute, tm_struct.wSecond); + fprintf(fp, "Module load time: %4d/%02d/%02d %02d:%02d:%02d\n", + loadtime_tm_struct.wYear, loadtime_tm_struct.wMonth, loadtime_tm_struct.wDay, + loadtime_tm_struct.wHour, loadtime_tm_struct.wMinute, loadtime_tm_struct.wSecond); +#else // UNIX, OSX + fprintf(fp, "Dump event time: %4d/%02d/%02d %02d:%02d:%02d\n", + 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); + fprintf(fp, "Module load time: %4d/%02d/%02d %02d:%02d:%02d\n", + loadtime_tm_struct.tm_year+1900, loadtime_tm_struct.tm_mon+1, loadtime_tm_struct.tm_mday, + loadtime_tm_struct.tm_hour, loadtime_tm_struct.tm_min, loadtime_tm_struct.tm_sec); +#endif + // Print native process ID + fprintf(fp, "Process ID: %d\n", pid); + fflush(fp); + + // Print Node.js and OS version information + PrintVersionInformation(fp); + fflush(fp); + +// Print summary JavaScript stack backtrace + PrintJavaScriptStack(fp, isolate, event, location); + fflush(fp); + + // Print native stack backtrace + PrintNativeStack(fp); + fflush(fp); + + // Print V8 Heap and Garbage Collector information + PrintGCStatistics(fp, isolate); + fflush(fp); + + // Print OS and current thread resource usage +#ifndef _WIN32 + PrintResourceUsage(fp); + fflush(fp); +#endif + + // Print libuv handle summary (TODO: investigate failure on Windows) + // Note: documentation of the uv_print_all_handles() API says "This function + // is meant for ad hoc debugging, there is no API/ABI stability guarantee" + // http://docs.libuv.org/en/v1.x/misc.html +#ifndef _WIN32 + fprintf(fp, "\n================================================================================"); + fprintf(fp, "\n==== Node.js libuv Handle Summary ==============================================\n"); + fprintf(fp, "\n(Flags: R=Ref, A=Active, I=Internal)\n"); + fprintf(fp, "\nFlags Type Address\n"); + uv_print_all_handles(nullptr, fp); + fflush(fp); +#endif + + // Print operating system information + PrintSystemInformation(fp, isolate); + + fprintf(fp, "\n================================================================================\n"); + fflush(fp); + fclose(fp); + + fprintf(stderr, "Node.js report completed\n"); + if (name != nullptr) { + snprintf(name, NR_MAXNAME + 1, "%s", filename); // return the NodeReport file name + } + report_active = false; +} + +/******************************************************************************* + * Function to print Node.js version, OS version and machine information + * + ******************************************************************************/ +static void PrintVersionInformation(FILE* fp) { + + // Print Node.js and deps component versions + fprintf(fp, "\nNode.js version: %s\n", NODE_VERSION); + fprintf(fp, "(v8: %s, libuv: %s, zlib: %s, ares: %s)\n", + V8::GetVersion(), uv_version_string(), ZLIB_VERSION, ARES_VERSION_STR); + + // Print operating system and machine information (Windows) +#ifdef _WIN32 + fprintf(fp, "\nOS version: Windows "); +#if defined(_MSC_VER) && (_MSC_VER >= 1900) + if (IsWindows1OrGreater()) { + fprintf(fp, "10 "); + } else +#endif + if (IsWindows8OrGreater()) { + fprintf(fp, "8 "); + } else if (IsWindows7OrGreater()) { + fprintf(fp, "7 "); + } else if (IsWindowsXPOrGreater()) { + fprintf(fp, "XP "); + } + if (IsWindowsServer()) { + fprintf(fp, "Server\n"); + } else { + fprintf(fp, "Client\n"); + } + TCHAR machine_name[256]; + DWORD machine_name_size = 256; + if (GetComputerName(machine_name, &machine_name_size)) { + fprintf(fp, "\nMachine: %s %s\n", machine_name); + } +#else + // Print operating system and machine information (Unix/OSX) + struct utsname os_info; + if (uname(&os_info) == 0) { + fprintf(fp, "\nOS version: %s %s %s\n", os_info.sysname, os_info.release, os_info.version); +#if defined(__GLIBC__) + fprintf(fp, "(glibc: %d.%d)\n", __GLIBC__, __GLIBC_MINOR__); +#endif + fprintf(fp, "\nMachine: %s %s\n", os_info.nodename, os_info.machine); + } +#endif +} + +/******************************************************************************* + * Function to print the JavaScript stack, if available + * + ******************************************************************************/ +static void PrintJavaScriptStack(FILE* fp, Isolate* isolate, DumpEvent event, const char* location) { + fprintf(fp, "\n================================================================================"); + fprintf(fp, "\n==== JavaScript Stack Trace ====================================================\n\n"); + +#ifdef _WIN32 + switch (event) { + case kFatalError: + // Stack trace on fatal error not supported on Windows + fprintf(fp, "No stack trace available\n"); + break; + default: + // All other events, print the stack using StackTrace::StackTrace() and GetStackSample() APIs + PrintStackFromStackTrace(fp, isolate, event); + break; + } // end switch(event) +#else // Unix, OSX + switch (event) { + case kException: + case kJavaScript: + // Print the stack using Message::PrintCurrentStackTrace() API + Message::PrintCurrentStackTrace(isolate, fp); + break; + case kFatalError: +#if NODE_VERSION_AT_LEAST(6, 0, 0) + if (!strncmp(location, "MarkCompactCollector", sizeof("MarkCompactCollector") - 1)) { + fprintf(fp, "V8 running in GC - no stack trace available\n"); + } else { + Message::PrintCurrentStackTrace(isolate, fp); + } +#else + fprintf(fp, "No stack trace available\n"); +#endif + break; + case kSignal_JS: + case kSignal_UV: + // Print the stack using StackTrace::StackTrace() and GetStackSample() APIs + PrintStackFromStackTrace(fp, isolate, event); + break; + } // end switch(event) +#endif +} + +/******************************************************************************* + * Function to print stack using GetStackSample() and StackTrace::StackTrace() + * + ******************************************************************************/ +static void PrintStackFromStackTrace(FILE* fp, Isolate* isolate, DumpEvent event) { + v8::RegisterState state; + v8::SampleInfo info; + void* samples[255]; + + // Initialise the register state + state.pc = nullptr; + state.fp = &state; + state.sp = &state; + + isolate->GetStackSample(state, samples, arraysize(samples), &info); + if (static_cast(info.vm_state) < arraysize(v8_states)) { + fprintf(fp, "JavaScript VM state: %s\n\n", v8_states[info.vm_state]); + } else { + fprintf(fp, "JavaScript VM state: \n\n"); + } + if (event == kSignal_UV) { + fprintf(fp, "Signal received when event loop idle, no stack trace available\n"); + return; + } + Local stack = StackTrace::CurrentStackTrace(isolate, 255, StackTrace::kDetailed); + if (stack.IsEmpty()) { + fprintf(fp, "\nNo stack trace available from StackTrace::CurrentStackTrace()\n"); + return; + } + // Print the stack trace, adding in the pc values from GetStackSample() if available + for (int i = 0; i < stack->GetFrameCount(); i++) { + if (static_cast(i) < info.frames_count) { + PrintStackFrame(fp, isolate, stack->GetFrame(i), i, samples[i]); + } else { + PrintStackFrame(fp, isolate, stack->GetFrame(i), i, nullptr); + } + } +} + +/******************************************************************************* + * Function to print a JavaScript stack frame from a V8 StackFrame object + * + ******************************************************************************/ +static void PrintStackFrame(FILE* fp, Isolate* isolate, Local frame, int i, void* pc) { + Nan::Utf8String fn_name_s(frame->GetFunctionName()); + Nan::Utf8String script_name(frame->GetScriptName()); + const int line_number = frame->GetLineNumber(); + const int column = frame->GetColumn(); + + // First print the frame index and the instruction address +#ifdef _WIN32 + fprintf(fp, "%2d: [pc=0x%p] ", i, pc); +#else + fprintf(fp, "%2d: [pc=%p] ", i, pc); +#endif + + // Now print the JavaScript function name and source information + if (frame->IsEval()) { + if (frame->GetScriptId() == Message::kNoScriptIdInfo) { + fprintf(fp, "at [eval]:%i:%i\n", line_number, column); + } else { + fprintf(fp, "at [eval] (%s:%i:%i)\n", *script_name, line_number, column); + } + return; + } + + if (fn_name_s.length() == 0) { + fprintf(fp, "%s:%i:%i\n", *script_name, line_number, column); + } else { + if (frame->IsConstructor()) { + fprintf(fp, "%s [constructor] (%s:%i:%i)\n", *fn_name_s, *script_name, line_number, column); + } else { + fprintf(fp, "%s (%s:%i:%i)\n", *fn_name_s, *script_name, line_number, column); + } + } +} + + +#ifdef _WIN32 +/******************************************************************************* + * Function to print a native stack backtrace + * + ******************************************************************************/ +void PrintNativeStack(FILE* fp) { + void* frames[64]; + fprintf(fp, "\n================================================================================"); + fprintf(fp, "\n==== Native Stack Trace ========================================================\n\n"); + + HANDLE hProcess = GetCurrentProcess(); + SymSetOptions(SYMOPT_LOAD_LINES | SYMOPT_UNDNAME | SYMOPT_DEFERRED_LOADS); + SymInitialize(hProcess, nullptr, TRUE); + + WORD numberOfFrames = CaptureStackBackTrace(2, 64, frames, nullptr); + + // Walk the frames printing symbolic information if available + for (int i = 0; i < numberOfFrames; i++) { + DWORD64 dwOffset64 = 0; + DWORD64 dwAddress = reinterpret_cast(frames[i]); + char buffer[sizeof(SYMBOL_INFO) + MAX_SYM_NAME * sizeof(TCHAR)]; + PSYMBOL_INFO pSymbol = reinterpret_cast(buffer); + pSymbol->SizeOfStruct = sizeof(SYMBOL_INFO); + pSymbol->MaxNameLen = MAX_SYM_NAME; + + if (SymFromAddr(hProcess, dwAddress, &dwOffset64, pSymbol)) { + DWORD dwOffset = 0; + IMAGEHLP_LINE64 line; + line.SizeOfStruct = sizeof(line); + if (SymGetLineFromAddr64(hProcess, dwAddress, &dwOffset, &line)) { + fprintf(fp, "%2d: [pc=0x%p] %s [+%d] in %s: line: %lu\n", i, pSymbol->Address, pSymbol->Name, dwOffset, line.FileName, line.LineNumber); + } else { + // SymGetLineFromAddr64() failed, just print the address and symbol + if (dwOffset64 <= 32) { // sanity check + fprintf(fp, "%2d: [pc=0x%p] %s [+%d]\n", i, pSymbol->Address, pSymbol->Name, dwOffset64); + } else { + fprintf(fp, "%2d: [pc=0x%p]\n", i, pSymbol->Address); + } + } + } else { // SymFromAddr() failed, just print the address + fprintf(fp, "%2d: [pc=0x%p]\n", i, pSymbol->Address); + } + } +} +#else +/******************************************************************************* + * Function to print a native stack backtrace - Linux/OSX + * + ******************************************************************************/ +void PrintNativeStack(FILE* fp) { + void* frames[256]; + fprintf(fp, "\n================================================================================"); + fprintf(fp, "\n==== Native Stack Trace ========================================================\n\n"); + + // Get the native backtrace (array of instruction addresses) + const int size = backtrace(frames, arraysize(frames)); + if (size <= 0) { + fprintf(fp, "Native backtrace failed, error %d\n", size); + return; + } else if (size <=2) { + fprintf(fp, "No frames to print\n"); + return; + } + + // Print the native frames, omitting the top 3 frames as they are in nodereport code + // backtrace_symbols_fd(frames, size, fileno(fp)); + for (int i = 2; i < size; i++) { + // print frame index and instruction address + fprintf(fp, "%2d: [pc=%p] ", i-2, frames[i]); + // If we can translate the address using dladdr() print additional symbolic information + Dl_info info; + if (dladdr(frames[i], &info)) { + if (info.dli_sname != nullptr) { + if (char* demangled = abi::__cxa_demangle(info.dli_sname, 0, 0, 0)) { + fprintf(fp, "%s", demangled); // print demangled symbol name + free(demangled); + } else { + fprintf(fp, "%s", info.dli_sname); // just print the symbol name + } + } + if (info.dli_fname != nullptr) { + fprintf(fp, " [%s]", info.dli_fname); // print shared object name + } + } + fprintf(fp, "\n"); + } +} +#endif + +/******************************************************************************* + * Function to print 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(FILE* fp, Isolate* isolate) { + HeapStatistics v8_heap_stats; + isolate->GetHeapStatistics(&v8_heap_stats); + + fprintf(fp, "\n================================================================================"); + fprintf(fp, "\n==== JavaScript Heap and Garbage Collector =====================================\n"); + HeapSpaceStatistics v8_heap_space_stats; + // Loop through heap spaces + for (size_t i = 0; i < isolate->NumberOfHeapSpaces(); i++) { + isolate->GetHeapSpaceStatistics(&v8_heap_space_stats, i); + fprintf(fp, "\nHeap space name: %s", v8_heap_space_stats.space_name()); + fprintf(fp, "\n Memory size: "); + WriteInteger(fp, v8_heap_space_stats.space_size()); + fprintf(fp, " bytes, committed memory: "); + WriteInteger(fp, v8_heap_space_stats.physical_space_size()); + fprintf(fp, " bytes\n Capacity: "); + WriteInteger(fp, v8_heap_space_stats.space_used_size() + + v8_heap_space_stats.space_available_size()); + fprintf(fp, " bytes, used: "); + WriteInteger(fp, v8_heap_space_stats.space_used_size()); + fprintf(fp, " bytes, available: "); + WriteInteger(fp, v8_heap_space_stats.space_available_size()); + fprintf(fp, " bytes"); + } + + fprintf(fp, "\n\nTotal heap memory size: "); + WriteInteger(fp, v8_heap_stats.total_heap_size()); + fprintf(fp, " bytes\nTotal heap committed memory: "); + WriteInteger(fp, v8_heap_stats.total_physical_size()); + fprintf(fp, " bytes\nTotal used heap memory: "); + WriteInteger(fp, v8_heap_stats.used_heap_size()); + fprintf(fp, " bytes\nTotal available heap memory: "); + WriteInteger(fp, v8_heap_stats.total_available_size()); + fprintf(fp, " bytes\n\nHeap memory limit: "); + WriteInteger(fp, v8_heap_stats.heap_size_limit()); + fprintf(fp, "\n"); +} + +#ifndef _WIN32 +/******************************************************************************* + * Function to print resource usage (Linux/OSX only). + * + ******************************************************************************/ +static void PrintResourceUsage(FILE* fp) { + fprintf(fp, "\n================================================================================"); + fprintf(fp, "\n==== Resource Usage ============================================================\n"); + + // Process and current thread usage statistics + struct rusage stats; + fprintf(fp, "\nProcess total resource usage:"); + if (getrusage(RUSAGE_SELF, &stats) == 0) { +#ifdef __APPLE__ + fprintf(fp, "\n User mode CPU: %ld.%06d secs", stats.ru_utime.tv_sec, stats.ru_utime.tv_usec); + fprintf(fp, "\n Kernel mode CPU: %ld.%06d secs", stats.ru_stime.tv_sec, stats.ru_stime.tv_usec); +#else + fprintf(fp, "\n User mode CPU: %ld.%06ld secs", stats.ru_utime.tv_sec, stats.ru_utime.tv_usec); + fprintf(fp, "\n Kernel mode CPU: %ld.%06ld secs", stats.ru_stime.tv_sec, stats.ru_stime.tv_usec); +#endif + fprintf(fp, "\n Maximum resident set size: "); + WriteInteger(fp, stats.ru_maxrss * 1024); + fprintf(fp, " bytes\n Page faults: %ld (I/O required) %ld (no I/O required)", stats.ru_majflt, stats.ru_minflt); + fprintf(fp, "\n Filesystem activity: %ld reads %ld writes", stats.ru_inblock, stats.ru_oublock); + } +#ifdef RUSAGE_THREAD + fprintf(fp, "\n\nEvent loop thread resource usage:"); + if (getrusage(RUSAGE_THREAD, &stats) == 0) { + fprintf(fp, "\n User mode CPU: %ld.%06ld secs", stats.ru_utime.tv_sec, stats.ru_utime.tv_usec); + fprintf(fp, "\n Kernel mode CPU: %ld.%06ld secs", stats.ru_stime.tv_sec, stats.ru_stime.tv_usec); + fprintf(fp, "\n Filesystem activity: %ld reads %ld writes", stats.ru_inblock, stats.ru_oublock); + } +#endif + fprintf(fp, "\n"); +} +#endif + +/******************************************************************************* + * Function to print operating system information. + * + ******************************************************************************/ +static void PrintSystemInformation(FILE* fp, Isolate* isolate) { + fprintf(fp, "\n================================================================================"); + fprintf(fp, "\n==== System Information ========================================================\n"); + +#ifdef _WIN32 + fprintf(fp, "\nEnvironment variables\n"); + LPTSTR lpszVariable; + LPTCH lpvEnv; + + // Get pointer to the environment block + lpvEnv = GetEnvironmentStrings(); + 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) { + fprintf(fp, " %s\n", lpszVariable); + lpszVariable += lstrlen(lpszVariable) + 1; + } + FreeEnvironmentStrings(lpvEnv); + } +#else + fprintf(fp, "\nEnvironment variables\n"); + int index = 1; + char* env_var = *environ; + + while (env_var != nullptr) { + fprintf(fp, " %s\n", env_var); + env_var = *(environ + index++); + } + +const 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}, + {"max locked memory (bytes) ", RLIMIT_MEMLOCK}, + {"max memory size (kbytes) ", RLIMIT_RSS}, + {"open files ", RLIMIT_NOFILE}, + {"stack size (bytes) ", RLIMIT_STACK}, + {"cpu time (seconds) ", RLIMIT_CPU}, + {"max user processes ", RLIMIT_NPROC}, + {"virtual memory (kbytes) ", RLIMIT_AS} +}; + + fprintf(fp, "\nResource limits soft limit hard limit\n"); + struct rlimit limit; + + for (size_t i = 0; i < arraysize(rlimit_strings); i++) { + if (getrlimit(rlimit_strings[i].id, &limit) == 0) { + fprintf(fp, " %s ", rlimit_strings[i].description); + if (limit.rlim_cur == RLIM_INFINITY) { + fprintf(fp, " unlimited"); + } else { + fprintf(fp, "%16" PRIu64, limit.rlim_cur); + } + if (limit.rlim_max == RLIM_INFINITY) { + fprintf(fp, " unlimited\n"); + } else { + fprintf(fp, "%16" PRIu64 "\n", limit.rlim_max); + } + } + } +#endif +} + +/******************************************************************************* + * Utility function to print out integer values with commas for readability. + * + ******************************************************************************/ +static void WriteInteger(FILE* fp, size_t value) { + int thousandsStack[8]; // Sufficient for max 64-bit number + int stackTop = 0; + int i; + size_t workingValue = value; + + do { + thousandsStack[stackTop++] = workingValue % 1000; + workingValue /= 1000; + } while (workingValue != 0); + + for (i = stackTop-1; i >= 0; i--) { + if (i == (stackTop-1)) { + fprintf(fp, "%u", thousandsStack[i]); + } else { + fprintf(fp, "%03u", thousandsStack[i]); + } + if (i > 0) { + fprintf(fp, ","); + } + } +} + +} // namespace nodereport diff --git a/src/node_report.h b/src/node_report.h new file mode 100644 index 0000000..d952a48 --- /dev/null +++ b/src/node_report.h @@ -0,0 +1,81 @@ +#ifndef SRC_NODE_REPORT_H_ +#define SRC_NODE_REPORT_H_ + +#include "nan.h" +#ifndef _WIN32 +#include +#include +#ifndef __APPLE__ +#include +#endif +#endif + +namespace nodereport { + +using v8::Isolate; +using v8::Local; +using v8::Message; +using v8::Function; +using v8::Object; +using v8::Number; +using v8::String; +using v8::Value; + +// Bit-flags for NodeReport trigger options +#define NR_EXCEPTION 0x01 +#define NR_FATALERROR 0x02 +#define NR_SIGNAL 0x04 +#define NR_APICALL 0x08 + +// Maximum file and path name lengths +#define NR_MAXNAME 64 +#define NR_MAXPATH 1024 + +enum DumpEvent {kException, kFatalError, kSignal_JS, kSignal_UV, kJavaScript}; + +void TriggerNodeReport(Isolate* isolate, DumpEvent event, const char* message, const char* location, char* name); + +unsigned int ProcessNodeReportEvents(const char* args); +unsigned int ProcessNodeReportCoreSwitch(const char* args); +unsigned int ProcessNodeReportSignal(const char* args); +void ProcessNodeReportFileName(const char* args); +void ProcessNodeReportDirectory(const char* args); +unsigned int ProcessNodeReportVerboseSwitch(const char* args); + +void SetLoadTime(); + +// Local implementation of secure_getenv() +inline const char* secure_getenv(const char* key) { +#ifndef _WIN32 + if (getuid() != geteuid() || getgid() != getegid()) + return nullptr; +#endif + return getenv(key); +} + +// Emulate arraysize() on Windows pre Visual Studio 2015 +#if defined(_MSC_VER) && _MSC_VER < 1900 +#define arraysize(a) (sizeof(a) / sizeof(*a)) +#else +template +constexpr size_t arraysize(const T(&)[N]) { return N; } +#endif // defined( _MSC_VER ) && (_MSC_VER < 1900) + +// Emulate snprintf() on Windows pre Visual Studio 2015 +#if defined( _MSC_VER ) && (_MSC_VER < 1900) +#include +inline static int snprintf(char* buffer, size_t n, const char* format, ...) { + va_list argp; + va_start(argp, format); + int ret = _vscprintf(format, argp); + vsnprintf_s(buffer, n, _TRUNCATE, format, argp); + va_end(argp); + return ret; +} + +#define __func__ __FUNCTION__ +#endif // defined( _MSC_VER ) && (_MSC_VER < 1900) + +} // namespace nodereport + +#endif // SRC_NODE_REPORT_H_ diff --git a/test/autorun.js b/test/autorun.js new file mode 100644 index 0000000..63a1bea --- /dev/null +++ b/test/autorun.js @@ -0,0 +1,129 @@ +// Testcase for nodereport. Triggers and validates NodeReport files +// +// Parameter(s): --save_log (optional) preserves verbose log file +// +'use strict'; + +const assert = require('assert'); +const spawn = require('child_process').spawn; +const readline = require('readline'); +const fs = require('fs'); +const path = require('path'); + +var results; +if (process.platform === 'win32') { + results = ['fail','fail','fail']; +} else { + results = ['fail','fail','fail','fail']; +} + +// Open a log file to record verbose test output +const stdio_log = fs.openSync('./nodereport_test.log', 'w'); + +// Test #1: Run child process to call API to generate a NodeReport +console.log('autorun.js: running child process #1 to produce NodeReport on API call'); +var child1 = spawn(process.execPath, [path.resolve(__dirname, 'test_1.js')], {stdio: ['pipe', stdio_log, stdio_log]}); +child1.on('exit', function(code, signal) { + assert.ok(code == 0); + // Locate and validate the NodeReport + var report = locate(child1.pid); + if (report) { + validate(report, 1); + fs.unlink(report); // delete the file when we are done + } +}); + +// Test #2: Run child process to produce unhandled exception and generate a NodeReport +console.log('autorun.js: running child process #2 to produce NodeReport on exception'); +var child2 = spawn(process.execPath, [path.resolve(__dirname, 'test_2.js')], {stdio: ['pipe', stdio_log, stdio_log]}); +child2.on('exit', function(code, signal) { + assert.ok(code !== 0); + // Locate and validate the NodeReport + var report = locate(child2.pid); + if (report) { + validate(report, 2); + fs.unlink(report); // delete the file when we are done + } +}); + +// Test #3: Run child process to produce fatal error (heap OOM) and generate a NodeReport +console.log('autorun.js: running child process #3 to produce NodeReport on fatal error'); +var child3 = spawn(process.execPath, ['--max-old-space-size=20', path.resolve(__dirname, 'test_3.js')], {stdio: ['pipe', stdio_log, stdio_log]}); +child3.on('exit', function(code, signal) { + assert.ok(code !== 0); + // Locate and validate the NodeReport + var report = locate(child3.pid); + if (report) { + validate(report, 3); + fs.unlink(report); // delete the file when we are done + } +}); + +// Test #4: Run child process to loop, then send SIGUSR2 to generate a NodeReport +if (process.platform === 'win32') { + // Not supported on Windows +} else { + console.log('autorun.js: running child process #4 to produce NodeReport on SIGUSR2'); + var child4 = spawn(process.execPath, [path.resolve(__dirname, 'test_4.js')], {stdio: ['pipe', stdio_log, stdio_log]}); + setTimeout(function() { + child4.kill('SIGUSR2'); + setTimeout(function() { + child4.kill('SIGTERM'); + }, 1000); + }, 1000); + child4.on('exit', function(code, signal) { + // Locate and validate the NodeReport + var report = locate(child4.pid); + if (report) { + validate(report, 4); + fs.unlink(report); // delete the file when we are done + } + }); +} + +// Print out results of all the tests on exit +process.on('exit', function() { + console.log('autorun.js: test results: '); + for (var i = 0; i < results.length; i++) { + console.log('\ttest', i+1, ': ', results[i]); + } + // Close the verbose output log file, and delete it unless --save_log was specified + fs.close(stdio_log); + if (!(process.argv[2] === '--save_log')) { + fs.unlink('./nodereport_test.log'); + } +}); + +// Utility function - locate a NodeReport produced by a process with the given PID +function locate(pid) { + var files = fs.readdirSync("."); + for (var i = 0; i < files.length; i++) { + if (files[i].substring(0, 10) == 'NodeReport' && files[i].indexOf(pid) != -1) { + console.log('autorun.js: located NodeReport: ', files[i]); + return files[i]; + } + } + return; +} + +// Utility function - validate a NodeReport (check all the sections are there) - and save result +function validate(report, index) { + var validationCount = 0; + const reader = readline.createInterface({ + input: fs.createReadStream(report) + }); + reader.on('line', (line) => { + //console.log('Line from file:', line); + if ((line.indexOf('==== NodeReport') > -1) || + (line.indexOf('==== JavaScript Stack Trace') > -1) || + (line.indexOf('==== JavaScript Heap') > -1) || + (line.indexOf('==== System Information') > -1)) { + validationCount++; // found expected section in NodeReport + } + }); + reader.on('close', () => { + if (validationCount == 4) { + results[index-1] = 'pass'; + } + }); +} diff --git a/test/test_1.js b/test/test_1.js new file mode 100644 index 0000000..d69c566 --- /dev/null +++ b/test/test_1.js @@ -0,0 +1,10 @@ +// NodeReport API example +var nodereport = require('nodereport'); + +console.log('api_call.js: triggering a NodeReport via API call...'); + +function myReport() { + nodereport.triggerReport(); +} + +myReport(); diff --git a/test/test_2.js b/test/test_2.js new file mode 100644 index 0000000..e182b78 --- /dev/null +++ b/test/test_2.js @@ -0,0 +1,15 @@ +// Testcase to produce an uncaught exception +require('nodereport').setEvents("exception"); + +console.log('exception.js: throwing an uncaught user exception....'); + +function myException(request, response) { + throw new UserException('*** exception.js: testcase exception thrown from myException()'); +} + +function UserException(message) { + this.message = message; + this.name = "UserException"; +} + +myException(); diff --git a/test/test_3.js b/test/test_3.js new file mode 100644 index 0000000..486f730 --- /dev/null +++ b/test/test_3.js @@ -0,0 +1,16 @@ +// Testcase to produce a fatal error (javascript heap OOM) +require('nodereport').setEvents("fatalerror"); + +console.log('fatalerror.js: allocating excessive javascript heap memory....'); +var list = []; +while (true) { + var record = new MyRecord(); + list.push(record); +} + + +function MyRecord() { + this.name = 'foo'; + this.id = 128; + this.account = 98454324; +} diff --git a/test/test_4.js b/test/test_4.js new file mode 100644 index 0000000..3858d91 --- /dev/null +++ b/test/test_4.js @@ -0,0 +1,28 @@ +// Testcase to loop in Javascript code +require('nodereport').setEvents("signal"); + +console.log('loop.js: going into loop now.... use kill -USR2 to trigger NodeReport'); + +function myLoop() { + var list = []; + for (var i=0; i<10000000000; i++) { + for (var j=0; i<1000; i++) { + list.push(new MyRecord()); + } + for (var j=0; i<1000; i++) { + list[j].id += 1; + list[j].account += 2; + } + for (var j=0; i<1000; i++) { + list.pop(); + } + } +} + +function MyRecord() { + this.name = 'foo'; + this.id = 128; + this.account = 98454324; +} + +myLoop();