-
Notifications
You must be signed in to change notification settings - Fork 45
Allow Error object to be passed to node-report #82
Changes from 5 commits
15fd45a
c73968a
5b75c5e
e5edd5f
cee0a31
7130888
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -51,6 +51,8 @@ NAN_METHOD(TriggerReport) { | |
Nan::HandleScope scope; | ||
v8::Isolate* isolate = info.GetIsolate(); | ||
char filename[NR_MAXNAME + 1] = ""; | ||
v8::MaybeLocal<v8::Value> error; | ||
int err_index = 0; | ||
|
||
if (info[0]->IsString()) { | ||
// Filename parameter supplied | ||
|
@@ -60,9 +62,16 @@ NAN_METHOD(TriggerReport) { | |
} else { | ||
Nan::ThrowError("node-report: filename parameter is too long"); | ||
} | ||
err_index++; | ||
} | ||
|
||
// We need to pass the javascript object so we can query if for a stack trace. | ||
if (info[err_index]->IsNativeError()) { | ||
error = info[err_index]; | ||
} | ||
|
||
if (nodereport_events & NR_APICALL) { | ||
TriggerNodeReport(isolate, kJavaScript, "JavaScript API", __func__, filename); | ||
TriggerNodeReport(isolate, kJavaScript, "JavaScript API", __func__, filename, error); | ||
// Return value is the report filename | ||
info.GetReturnValue().Set(Nan::New(filename).ToLocalChecked()); | ||
} | ||
|
@@ -77,7 +86,12 @@ NAN_METHOD(GetReport) { | |
v8::Isolate* isolate = info.GetIsolate(); | ||
std::ostringstream out; | ||
|
||
GetNodeReport(isolate, kJavaScript, "JavaScript API", __func__, out); | ||
v8::MaybeLocal<v8::Value> error; | ||
if (info[0]->IsNativeError()) { | ||
error = info[0]; | ||
} | ||
|
||
GetNodeReport(isolate, kJavaScript, "JavaScript API", __func__, error, out); | ||
// Return value is the contents of a report as a string. | ||
info.GetReturnValue().Set(Nan::New(out.str()).ToLocalChecked()); | ||
} | ||
|
@@ -156,7 +170,7 @@ static void OnFatalError(const char* location, const char* message) { | |
} | ||
// Trigger report if requested | ||
if (nodereport_events & NR_FATALERROR) { | ||
TriggerNodeReport(Isolate::GetCurrent(), kFatalError, message, location, nullptr); | ||
TriggerNodeReport(Isolate::GetCurrent(), kFatalError, message, location, nullptr, v8::MaybeLocal<v8::Value>()); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Does this project wrap lines at 80 characters? If so, there are a number of long lines in this PR. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Of our six outstanding PR's four of them concern code formatting and the conversations got a bit bogged down. After the two outstanding functional PR's are merged I should probably do something like just run clang-format over the C source to standardise it a bit. At the moment we are well over 80 in node_report.cc as it writes strings of 80 "=" char strings as section dividers in several places. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @cjihrig - Do you want me to push a fix to this or would you rather I dealt with long lines throughout the project in a separate PR? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If the project doesn't enforce 80 character lines, or there are a bunch of other offenders, it's probably simpler to not hold up this PR and deal with them all in another PR. Your call. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. There's other places with the same problem so I should probably get the various PRs we have for linting C/C++ and JavaScript tidied up. That way we can do the all code formatting in one patch without muddling them up with functional changes. |
||
} | ||
fflush(stderr); | ||
raise(SIGABRT); | ||
|
@@ -165,7 +179,7 @@ static void OnFatalError(const char* location, const char* message) { | |
bool OnUncaughtException(v8::Isolate* isolate) { | ||
// Trigger report if requested | ||
if (nodereport_events & NR_EXCEPTION) { | ||
TriggerNodeReport(isolate, kException, "exception", __func__, nullptr); | ||
TriggerNodeReport(isolate, kException, "exception", __func__, nullptr, v8::MaybeLocal<v8::Value>()); | ||
} | ||
if ((commandline_string.find("abort-on-uncaught-exception") != std::string::npos) || | ||
(commandline_string.find("abort_on_uncaught_exception") != std::string::npos)) { | ||
|
@@ -231,7 +245,7 @@ static void SignalDumpInterruptCallback(Isolate* isolate, void* data) { | |
fprintf(stdout, "node-report: SignalDumpInterruptCallback triggering report\n"); | ||
} | ||
TriggerNodeReport(isolate, kSignal_JS, | ||
node::signo_string(report_signal), __func__, nullptr); | ||
node::signo_string(report_signal), __func__, nullptr, v8::MaybeLocal<v8::Value>()); | ||
} | ||
report_signal = 0; | ||
} | ||
|
@@ -246,7 +260,7 @@ static void SignalDumpAsyncCallback(uv_async_t* handle) { | |
fprintf(stdout, "node-report: SignalDumpAsyncCallback triggering NodeReport\n"); | ||
} | ||
TriggerNodeReport(Isolate::GetCurrent(), kSignal_UV, | ||
node::signo_string(report_signal), __func__, nullptr); | ||
node::signo_string(report_signal), __func__, nullptr, v8::MaybeLocal<v8::Value>()); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It would be nice to not have all of the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I've added a I checked the Node.js source code to see if there was a better solution but that passes There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What about passing There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The argument is pass-by-value not a pointer. IIRC You can't create an empty There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Aren't you introducing the argument in this PR though? If so, you could make it a pointer. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I believe from the documentation for Local (and MaybeLocal) in deps/v8.h that Locals should be passed by value. The local is managing the reference to an object owned by V8's garbage collector, I'll admit I haven't dug that far into the implementation details but I've not seen any examples in the Node.js code or other modules where Locals are passed by reference. In my head I have them in a similar category to std::shared_ptr (and other _ptr's) in C++ where you pass by value something which manages does the actual pointer management for you. I don't believe that they are actually large objects to pass by value, I don't think they really contain fields other than the reference itself. If I'm wrong I'm happy to change it but I'm a bit wary that passing these by reference is really just a good way to introduce bugs and I'd like to be sure I'm handling these the right way. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I haven't heard that about Locals, but I could be wrong. I did find this though - https://github.com/nodejs/node/blob/6bcf65d4a788a73b3c3f27d75796609f948f7885/src/async-wrap-inl.h#L57 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. My reference is here: I think pass by value is correct and I don't think briefly stack allocating an empty MaybeLocal is remotely expensive so I'm happy with it but also happy to learn the "correct" answer. (I'm wary of following async-wrap as an example, I suspect it may be special as it probably is quite performance critical which isn't true of node-report code.) There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. OK, I just talked with @fhinkel from V8 about it and they agreed to not use a reference. |
||
} | ||
report_signal = 0; | ||
} | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -82,10 +82,11 @@ typedef struct tm TIME_TYPE; | |
#endif | ||
|
||
// Internal/static function declarations | ||
static void WriteNodeReport(Isolate* isolate, DumpEvent event, const char* message, const char* location, char* filename, std::ostream &out, TIME_TYPE* time); | ||
static void WriteNodeReport(Isolate* isolate, DumpEvent event, const char* message, const char* location, char* filename, std::ostream &out, v8::MaybeLocal<v8::Value> error, TIME_TYPE* time); | ||
static void PrintCommandLine(std::ostream& out); | ||
static void PrintVersionInformation(std::ostream& out); | ||
static void PrintJavaScriptStack(std::ostream& out, Isolate* isolate, DumpEvent event, const char* location); | ||
static void PrintJavaScriptErrorStack(std::ostream& out, Isolate* isolate, v8::MaybeLocal<v8::Value> error); | ||
static void PrintStackFromStackTrace(std::ostream& out, Isolate* isolate, DumpEvent event); | ||
static void PrintStackFrame(std::ostream& out, Isolate* isolate, Local<StackFrame> frame, int index, void* pc); | ||
static void PrintNativeStack(std::ostream& out); | ||
|
@@ -379,7 +380,7 @@ void SetCommandLine() { | |
* const char* location | ||
* char* name - in/out - returns the report filename | ||
******************************************************************************/ | ||
void TriggerNodeReport(Isolate* isolate, DumpEvent event, const char* message, const char* location, char* name) { | ||
void TriggerNodeReport(Isolate* isolate, DumpEvent event, const char* message, const char* location, char* name, v8::MaybeLocal<v8::Value> error) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The comment before this line documents the parameters. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Good spot. |
||
// Recursion check for report in progress, bail out | ||
if (report_active) return; | ||
report_active = true; | ||
|
@@ -460,7 +461,7 @@ void TriggerNodeReport(Isolate* isolate, DumpEvent event, const char* message, c | |
// Pass our stream about by reference, not by copying it. | ||
std::ostream &out = outfile.is_open() ? outfile : *outstream; | ||
|
||
WriteNodeReport(isolate, event, message, location, filename, out, &tm_struct); | ||
WriteNodeReport(isolate, event, message, location, filename, out, error, &tm_struct); | ||
|
||
// Do not close stdout/stderr, only close files we opened. | ||
if(outfile.is_open()) { | ||
|
@@ -474,7 +475,7 @@ void TriggerNodeReport(Isolate* isolate, DumpEvent event, const char* message, c | |
|
||
} | ||
|
||
void GetNodeReport(Isolate* isolate, DumpEvent event, const char* message, const char* location, std::ostream& out) { | ||
void GetNodeReport(Isolate* isolate, DumpEvent event, const char* message, const char* location, v8::MaybeLocal<v8::Value> error, std::ostream& out) { | ||
// Obtain the current time and the pid (platform dependent) | ||
TIME_TYPE tm_struct; | ||
#ifdef _WIN32 | ||
|
@@ -484,7 +485,7 @@ void GetNodeReport(Isolate* isolate, DumpEvent event, const char* message, const | |
gettimeofday(&time_val, nullptr); | ||
localtime_r(&time_val.tv_sec, &tm_struct); | ||
#endif | ||
WriteNodeReport(isolate, event, message, location, nullptr, out, &tm_struct); | ||
WriteNodeReport(isolate, event, message, location, nullptr, out, error, &tm_struct); | ||
} | ||
|
||
static void walkHandle(uv_handle_t* h, void* arg) { | ||
|
@@ -531,7 +532,7 @@ static void walkHandle(uv_handle_t* h, void* arg) { | |
*out << buf; | ||
} | ||
|
||
static void WriteNodeReport(Isolate* isolate, DumpEvent event, const char* message, const char* location, char* filename, std::ostream &out, TIME_TYPE* tm_struct) { | ||
static void WriteNodeReport(Isolate* isolate, DumpEvent event, const char* message, const char* location, char* filename, std::ostream &out, v8::MaybeLocal<v8::Value> error, TIME_TYPE* tm_struct) { | ||
|
||
#ifdef _WIN32 | ||
DWORD pid = GetCurrentProcessId(); | ||
|
@@ -589,6 +590,11 @@ static void WriteNodeReport(Isolate* isolate, DumpEvent event, const char* messa | |
PrintNativeStack(out); | ||
out << std::flush; | ||
|
||
// Print the stack trace and message from the Error object. | ||
// (If one was provided.) | ||
PrintJavaScriptErrorStack(out, isolate, error); | ||
out << std::flush; | ||
|
||
// Print V8 Heap and Garbage Collector information | ||
PrintGCStatistics(out, isolate); | ||
out << std::flush; | ||
|
@@ -791,6 +797,29 @@ static void PrintJavaScriptStack(std::ostream& out, Isolate* isolate, DumpEvent | |
#endif | ||
} | ||
|
||
static void PrintJavaScriptErrorStack(std::ostream& out, Isolate* isolate, v8::MaybeLocal<v8::Value> error) { | ||
if (error.IsEmpty() || !error.ToLocalChecked()->IsNativeError()) { | ||
return; | ||
} | ||
|
||
out << "\n================================================================================"; | ||
out << "\n==== JavaScript Exception Details ==============================================\n\n"; | ||
Local<Message> message = v8::Exception::CreateMessage(isolate, error.ToLocalChecked()); | ||
Nan::Utf8String message_str(message->Get()); | ||
|
||
out << *message_str << "\n\n"; | ||
|
||
Local<StackTrace> stack = v8::Exception::GetStackTrace(error.ToLocalChecked()); | ||
if (stack.IsEmpty()) { | ||
out << "No stack trace available from Exception::GetStackTrace()\n"; | ||
return; | ||
} | ||
// Print the stack trace, samples are not available as the exception isn't from the current stack. | ||
for (int i = 0; i < stack->GetFrameCount(); i++) { | ||
PrintStackFrame(out, isolate, stack->GetFrame(i), i, nullptr); | ||
} | ||
} | ||
|
||
/******************************************************************************* | ||
* Function to print stack using GetStackSample() and StackTrace::StackTrace() | ||
* | ||
|
@@ -842,13 +871,14 @@ static void PrintStackFrame(std::ostream& out, Isolate* isolate, Local<StackFram | |
char buf[64]; | ||
|
||
// First print the frame index and the instruction address | ||
if (pc != nullptr) { | ||
#ifdef _WIN32 | ||
snprintf( buf, sizeof(buf), "%2d: [pc=0x%p] ", i, pc); | ||
out << buf; | ||
snprintf( buf, sizeof(buf), "%2d: [pc=0x%p] ", i, pc); | ||
#else | ||
snprintf( buf, sizeof(buf), "%2d: [pc=%p] ", i, pc); | ||
out << buf; | ||
snprintf( buf, sizeof(buf), "%2d: [pc=%p] ", i, pc); | ||
#endif | ||
out << buf; | ||
} | ||
|
||
// Now print the JavaScript function name and source information | ||
if (frame->IsEval()) { | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,29 @@ | ||
'use strict'; | ||
|
||
// Testcase for passing an error object to the API call. | ||
|
||
if (process.argv[2] === 'child') { | ||
const nodereport = require('../'); | ||
try { | ||
throw new Error("Testing error handling"); | ||
} catch (err) { | ||
nodereport.triggerReport(err); | ||
} | ||
} else { | ||
const common = require('./common.js'); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think this can just be There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I tested, it looks like it can. @sam-github had other comments about the test structure. At the moment it matches all the other tests and if we want to fix one up we should probably do all of them under a separate PR so I'm going to leave this for now. |
||
const spawn = require('child_process').spawn; | ||
const tap = require('tap'); | ||
|
||
const child = spawn(process.execPath, [__filename, 'child']); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't see why the test is spawning a child to call triggerReport(), why not just call it directly in the test? It looks like boilerplate copied from a test that must exist in order to get the report that doesn't apply here. If it is needed, probably needs a comment explaining why. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I believe most of the tests use spawn so the standard validation checks in common.js can check a known set of process arguments. I think @richardlau would be the person to ask if you want the history. The test is a direct copy of test-api.js with a small change to how the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes, part of the standard validation checks in There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Although the command line check can be skipped by not setting There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @sam-github Do you want this changed before you approve? I'm happy to raise a separate PR about changing the tests. |
||
child.on('exit', (code) => { | ||
tap.plan(3); | ||
tap.equal(code, 0, 'Process exited cleanly'); | ||
const reports = common.findReports(child.pid); | ||
tap.equal(reports.length, 1, 'Found reports ' + reports); | ||
const report = reports[0]; | ||
common.validate(tap, report, {pid: child.pid, | ||
commandline: child.spawnargs.join(' '), | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I was half convinced by the argument for spawning above, then I read test/common.js, and now I'm not. The command line is just checked against what is passed in here, which could just as easily be There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. One of the problems we had with But as I pointed out elsewhere the command line check is optional -- We can leave out |
||
expectedException: "Testing error handling", | ||
}); | ||
}); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
javascript -> JavaScript and if -> it
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done.