diff --git a/README.md b/README.md index 4e7b3f5..aa817bc 100644 --- a/README.md +++ b/README.md @@ -5,11 +5,10 @@ Delivers a human-readable diagnostic summary, 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 can be triggered on unhandled exceptions, fatal errors, signals -and calls to a JavaScript API. +platform information and resource usage etc. Reports can be triggered on +unhandled exceptions, fatal errors, signals and calls to JavaScript APIs. -Supports Node.js v4, v6 and v7 on AIX, Linux, MacOS, SmartOS and Windows. +Supports Node.js version 4, 6 and 8 on AIX, Linux, MacOS, SmartOS and Windows. ## Usage @@ -17,9 +16,9 @@ Supports Node.js v4, v6 and v7 on AIX, Linux, MacOS, SmartOS and Windows. npm install node-report node -r node-report app.js ``` -A report will be triggered automatically on unhandled exceptions and fatal -error events (for example out of memory errors), and can also be triggered -by sending a USR2 signal to a Node.js process (not supported on Windows). +A report will be triggered automatically on unhandled exceptions, fatal errors +(for example out of memory errors) and crashes in native code. A report can also be +triggered by sending a USR2 signal to a Node.js process (not supported on Windows). A report can also be triggered via an API call from a JavaScript application. @@ -37,7 +36,7 @@ var report_str = nodereport.getReport(); console.log(report_str); ``` The API can be used without adding the automatic exception and fatal error -hooks and the signal handler, as follows: +hooks and the signal handlers, as follows: ```js var nodereport = require('node-report/api'); @@ -95,7 +94,7 @@ Additional configuration is available using the following APIs: ```js var nodereport = require('node-report/api'); -nodereport.setEvents("exception+fatalerror+signal+apicall"); +nodereport.setEvents("exception+fatalerror+signal+apicall+crash"); nodereport.setSignal("SIGUSR2|SIGQUIT"); nodereport.setFileName("stdout|stderr|"); nodereport.setDirectory(""); @@ -105,7 +104,7 @@ nodereport.setVerbose("yes|no"); Configuration on module initialization is also available via environment variables: ```bash -export NODEREPORT_EVENTS=exception+fatalerror+signal+apicall +export NODEREPORT_EVENTS=exception+fatalerror+signal+apicall+crash export NODEREPORT_SIGNAL=SIGUSR2|SIGQUIT export NODEREPORT_FILENAME=stdout|stderr| export NODEREPORT_DIRECTORY= diff --git a/index.js b/index.js index 94ba556..e2fce74 100644 --- a/index.js +++ b/index.js @@ -3,7 +3,7 @@ const api = require('./api'); // NODEREPORT_EVENTS env var overrides the defaults -const options = process.env.NODEREPORT_EVENTS || 'exception+fatalerror+signal+apicall'; +const options = process.env.NODEREPORT_EVENTS || 'exception+fatalerror+signal+apicall+crash'; api.setEvents(options); exports.triggerReport = api.triggerReport; diff --git a/src/module.cc b/src/module.cc index 5c2f76d..d329f8f 100644 --- a/src/module.cc +++ b/src/module.cc @@ -13,10 +13,11 @@ static void PrintStackFromStackTrace(Isolate* isolate, FILE* fp); 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), +static void RegisterSignalHandler(int signo, void (*handler)(int, siginfo_t*, void*), struct sigaction* saved_sa); static void RestoreSignalHandler(int signo, struct sigaction* saved_sa); -static void SignalDump(int signo); +static void SignalHandler(int signo, siginfo_t* info, void* void_context); +static void CrashHandler(int signo, siginfo_t* info, void* void_context); static void SetupSignalHandler(); #endif @@ -31,7 +32,10 @@ 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 watchdog thread -static struct sigaction saved_sa; // saved signal action +static struct sigaction saved_user_sa; // saved user signal action +static struct sigaction saved_sigsegv_sa; // saved crash signal action +static struct sigaction saved_sigill_sa; // saved crash signal action +static struct sigaction saved_sigfpe_sa; // saved crash signal action #endif // State variables for v8 hooks and signal initialisation @@ -128,7 +132,20 @@ NAN_METHOD(SetEvents) { } // If report 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); + RestoreSignalHandler(nodereport_signal, &saved_user_sa); + } + + // If report newly requested on native crash, set up signal handlers + if ((nodereport_events & NR_CRASH) && !(previous_events & NR_CRASH)) { + RegisterSignalHandler(SIGSEGV, CrashHandler, &saved_sigsegv_sa); + RegisterSignalHandler(SIGILL, CrashHandler, &saved_sigill_sa); + RegisterSignalHandler(SIGFPE, CrashHandler, &saved_sigfpe_sa); + } + // If report no longer required on native crash, reset the signal handlers + if (!(nodereport_events & NR_CRASH) && (previous_events & NR_CRASH)) { + RestoreSignalHandler(SIGSEGV, &saved_sigsegv_sa); + RestoreSignalHandler(SIGILL, &saved_sigill_sa); + RestoreSignalHandler(SIGFPE, &saved_sigfpe_sa); } #endif } @@ -140,8 +157,8 @@ NAN_METHOD(SetSignal) { // 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); + RestoreSignalHandler(previous_signal, &saved_user_sa); + RegisterSignalHandler(nodereport_signal, SignalHandler, &saved_user_sa); } #endif } @@ -268,18 +285,20 @@ static void SignalDumpAsyncCallback(uv_async_t* handle) { /******************************************************************************* * Utility functions for signal handling support (platforms except Windows) - * - RegisterSignalHandler() - register a raw OS signal handler - * - SignalDump() - implementation of raw OS signal handler + * - RegisterSignalHandler() - register an OS signal handler + * - SignalHandler() - implementation of OS signal handler for external signals + * - CrashHandler() - implementation of OS signal handler for crash signals * - 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), +static void RegisterSignalHandler(int signo, void (*handler)(int, siginfo_t*, void*), struct sigaction* saved_sa) { struct sigaction sa; memset(&sa, 0, sizeof(sa)); - sa.sa_handler = handler; + sa.sa_flags = SA_SIGINFO; + sa.sa_sigaction = handler; sigfillset(&sa.sa_mask); // mask all signals while in the handler sigaction(signo, &sa, saved_sa); } @@ -289,14 +308,37 @@ static void RestoreSignalHandler(int signo, struct sigaction* saved_sa) { sigaction(signo, saved_sa, nullptr); } -// Raw signal handler for triggering a report - runs on an arbitrary thread -static void SignalDump(int signo) { +// Native signal handler for triggering a report on user signal - runs on an arbitrary thread +static void SignalHandler(int signo, siginfo_t* info, void* void_context) { // Check atomic for report 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 } } +// Native signal handler for triggering a report on a crash, runs on crashing thread +static void CrashHandler(int signo, siginfo_t* info, void* void_context) { + // Remove node-report crash handlers in case we get a secondary crash + RestoreSignalHandler(signo, &saved_sigsegv_sa); + RestoreSignalHandler(signo, &saved_sigill_sa); + RestoreSignalHandler(signo, &saved_sigfpe_sa); + // Also remove the node-report user signal handler, if set + if (nodereport_events & NR_SIGNAL) { + RestoreSignalHandler(nodereport_signal, &saved_user_sa); + } + if ((info->si_pid == 0) || (info->si_pid == getpid())) { + // Real crash, or a signal raised internally by this process + TriggerNodeReport(Isolate::GetCurrent(), kCrashSignal, node::signo_string(signo), + __func__, nullptr, MaybeLocal()); + } else { + // External crash signal sent by some other process + TriggerNodeReport(Isolate::GetCurrent(), kKillSignal, node::signo_string(signo), + __func__, nullptr, MaybeLocal()); + } + // Propagate the signal + raise(signo); +} + // Utility function to start a watchdog thread - used for processing signals static int StartWatchdogThread(void* (*thread_main)(void* unused)) { pthread_attr_t attr; @@ -361,7 +403,7 @@ static void SetupSignalHandler() { Nan::ThrowError("node-report: initialization failed, uv_async_init() returned error\n"); } uv_unref(reinterpret_cast(&nodereport_trigger_async)); - RegisterSignalHandler(nodereport_signal, SignalDump, &saved_sa); + RegisterSignalHandler(nodereport_signal, SignalHandler, &saved_user_sa); signal_thread_initialised = true; } } @@ -420,6 +462,12 @@ void Initialize(v8::Local exports) { if (nodereport_events & NR_SIGNAL) { SetupSignalHandler(); } + // If report requested on crash signal set up crash handler + if (nodereport_events & NR_CRASH) { + RegisterSignalHandler(SIGSEGV, CrashHandler, &saved_sigsegv_sa); + RegisterSignalHandler(SIGILL, CrashHandler, &saved_sigill_sa); + RegisterSignalHandler(SIGFPE, CrashHandler, &saved_sigfpe_sa); + } #endif exports->Set(Nan::New("triggerReport").ToLocalChecked(), diff --git a/src/node_report.cc b/src/node_report.cc index f6376a6..a7028ff 100644 --- a/src/node_report.cc +++ b/src/node_report.cc @@ -140,6 +140,9 @@ unsigned int ProcessNodeReportEvents(const char* args) { } else if (!strncmp(cursor, "apicall", sizeof("apicall") - 1)) { event_flags |= NR_APICALL; cursor += sizeof("apicall") - 1; + } else if (!strncmp(cursor, "crash", sizeof("crash") - 1)) { + event_flags |= NR_CRASH; + cursor += sizeof("crash") - 1; } else { std::cerr << "Unrecognised argument for node-report events option: " << cursor << "\n"; return 0; @@ -460,7 +463,7 @@ void TriggerNodeReport(Isolate* isolate, DumpEvent event, const char* message, c } return; } else { - std::cerr << "\nWriting Node.js report to file: " << filename << "\n"; + std::cerr << "Writing Node.js report to file: " << filename << "\n"; } } @@ -993,9 +996,13 @@ static void PrintJavaScriptStack(std::ostream& out, Isolate* isolate, DumpEvent break; case kSignal_JS: case kSignal_UV: + case kCrashSignal: // Print the stack using StackTrace::StackTrace() and GetStackSample() APIs PrintStackFromStackTrace(out, isolate, event); break; + case kKillSignal: + out << "Crash signal received from external process, no stack trace available\n"; + break; } // end switch(event) #endif } diff --git a/src/node_report.h b/src/node_report.h index b597303..27d5d50 100644 --- a/src/node_report.h +++ b/src/node_report.h @@ -26,12 +26,13 @@ using v8::MaybeLocal; #define NR_FATALERROR 0x02 #define NR_SIGNAL 0x04 #define NR_APICALL 0x08 +#define NR_CRASH 0x10 // Maximum file and path name lengths #define NR_MAXNAME 64 #define NR_MAXPATH 1024 -enum DumpEvent {kException, kFatalError, kSignal_JS, kSignal_UV, kJavaScript}; +enum DumpEvent {kException, kFatalError, kSignal_JS, kSignal_UV, kJavaScript, kCrashSignal, kKillSignal}; void TriggerNodeReport(Isolate* isolate, DumpEvent event, const char* message, const char* location, char* name, v8::MaybeLocal error); void GetNodeReport(Isolate* isolate, DumpEvent event, const char* message, const char* location, v8::MaybeLocal error, std::ostream& out); diff --git a/test/test-sigfpe.js b/test/test-sigfpe.js new file mode 100644 index 0000000..a41c7a2 --- /dev/null +++ b/test/test-sigfpe.js @@ -0,0 +1,50 @@ +'use strict'; + +// Testcase to produce report on native crash (sigfpe) + +// This testcase uses process.kill() to raise the sigfpe signal rather than +// producing a real arithmetic exception (which would require native code). The +// code exercised in node-report is the same. + +if (process.argv[2] === 'child') { + // Child process implementation + require('../'); + process.kill(process.pid, 'SIGFPE'); + +} else { + // Parent process implementation + const common = require('./common.js'); + const spawn = require('child_process').spawn; + const tap = require('tap'); + const fs = require('fs'); + + if (common.isWindows()) { + tap.fail('Native crash support not available on Windows', { skip: true }); + return; + } + + const child = spawn(process.execPath, [__filename, 'child']); + + // Capture stderr output from the child process + var stderr = ''; + child.stderr.on('data', (chunk) => {stderr += chunk;}); + + // Validation on child exit + child.on('exit', (code) => { + tap.plan(4); + tap.notEqual(code, 1, 'Check for expected non-zero exit code'); + const reports = common.findReports(child.pid); + tap.equal(reports.length, 1, 'Found reports ' + reports); + const report = reports[0]; + + // Testcase-specific report validation + fs.readFile(report, (err, data) => { + const headerSection = common.getSection(data, 'Node Report'); + tap.match(headerSection, /SIGFPE/, + 'Checking that header section contains crash signal name'); + }); + // Common report validation + const options = {pid: child.pid, commandline: child.spawnargs.join(' ')}; + common.validate(tap, report, options); + }); +} diff --git a/test/test-sigill.js b/test/test-sigill.js new file mode 100644 index 0000000..69d5672 --- /dev/null +++ b/test/test-sigill.js @@ -0,0 +1,50 @@ +'use strict'; + +// Testcase to produce report on native crash (sigill) + +// This testcase uses process.kill() to raise the sigill signal rather than +// executing a real illegal instruction (which would require native code). The +// code exercised in node-report is the same. + +if (process.argv[2] === 'child') { + // Child process implementation + require('../'); + process.kill(process.pid, 'SIGILL'); + +} else { + // Parent process implementation + const common = require('./common.js'); + const spawn = require('child_process').spawn; + const tap = require('tap'); + const fs = require('fs'); + + if (common.isWindows()) { + tap.fail('Native crash support not available on Windows', { skip: true }); + return; + } + + const child = spawn(process.execPath, [__filename, 'child']); + + // Capture stderr output from the child process + var stderr = ''; + child.stderr.on('data', (chunk) => {stderr += chunk;}); + + // Validation on child exit + child.on('exit', (code) => { + tap.plan(4); + tap.notEqual(code, 1, 'Check for expected non-zero exit code'); + const reports = common.findReports(child.pid); + tap.equal(reports.length, 1, 'Found reports ' + reports); + const report = reports[0]; + + // Testcase-specific report validation + fs.readFile(report, (err, data) => { + const headerSection = common.getSection(data, 'Node Report'); + tap.match(headerSection, /SIGILL/, + 'Checking that header section contains crash signal name'); + }); + // Common report validation + const options = {pid: child.pid, commandline: child.spawnargs.join(' ')}; + common.validate(tap, report, options); + }); +} diff --git a/test/test-sigsegv.js b/test/test-sigsegv.js new file mode 100644 index 0000000..0707cfa --- /dev/null +++ b/test/test-sigsegv.js @@ -0,0 +1,50 @@ +'use strict'; + +// Testcase to produce report on native crash (sigsegv) + +// This testcase uses process.kill() to raise the sigsegv signal rather than +// producing a real segmentation fault (which would require native code). The +// code exercised in node-report is the same. + +if (process.argv[2] === 'child') { + // Child process implementation + require('../'); + process.kill(process.pid, 'SIGSEGV'); + +} else { + // Parent process implementation + const common = require('./common.js'); + const spawn = require('child_process').spawn; + const tap = require('tap'); + const fs = require('fs'); + + if (common.isWindows()) { + tap.fail('Native crash support not available on Windows', { skip: true }); + return; + } + + const child = spawn(process.execPath, [__filename, 'child']); + + // Capture stderr output from the child process + var stderr = ''; + child.stderr.on('data', (chunk) => {stderr += chunk;}); + + // Validation when child process exits + child.on('exit', (code) => { + tap.plan(4); + tap.notEqual(code, 1, 'Check for expected non-zero exit code'); + const reports = common.findReports(child.pid); + tap.equal(reports.length, 1, 'Found reports ' + reports); + const report = reports[0]; + + // Testcase-specific report validation + fs.readFile(report, (err, data) => { + const headerSection = common.getSection(data, 'Node Report'); + tap.match(headerSection, /SIGSEGV/, + 'Checking that header section contains crash signal name'); + }); + // Common report validation + const options = {pid: child.pid, commandline: child.spawnargs.join(' ')}; + common.validate(tap, report, options); + }); +}