diff --git a/node.gyp b/node.gyp index 7971fad478f5e5..1570d27d26bba6 100644 --- a/node.gyp +++ b/node.gyp @@ -993,6 +993,7 @@ 'test/cctest/test_node_api.cc', 'test/cctest/test_per_process.cc', 'test/cctest/test_platform.cc', + 'test/cctest/test_report.cc', 'test/cctest/test_json_utils.cc', 'test/cctest/test_sockaddr.cc', 'test/cctest/test_traced_value.cc', diff --git a/src/node.h b/src/node.h index f8afff5d357639..d6f6509f886959 100644 --- a/src/node.h +++ b/src/node.h @@ -75,8 +75,9 @@ #include "v8-platform.h" // NOLINT(build/include_order) #include "node_version.h" // NODE_MODULE_VERSION -#include #include +#include +#include // We cannot use __POSIX__ in this header because that's only defined when // building Node.js. @@ -617,6 +618,33 @@ NODE_EXTERN v8::MaybeLocal PrepareStackTraceCallback( v8::Local exception, v8::Local trace); +// Writes a diagnostic report to a file. If filename is not provided, the +// default filename includes the date, time, PID, and a sequence number. +// The report's JavaScript stack trace is taken from err, if present. +// If isolate is nullptr, no information about the JavaScript environment +// is included in the report. +// Returns the filename of the written report. +NODE_EXTERN std::string TriggerNodeReport(v8::Isolate* isolate, + const char* message, + const char* trigger, + const std::string& filename, + v8::Local error); +NODE_EXTERN std::string TriggerNodeReport(Environment* env, + const char* message, + const char* trigger, + const std::string& filename, + v8::Local error); +NODE_EXTERN void GetNodeReport(v8::Isolate* isolate, + const char* message, + const char* trigger, + v8::Local error, + std::ostream& out); +NODE_EXTERN void GetNodeReport(Environment* env, + const char* message, + const char* trigger, + v8::Local error, + std::ostream& out); + // This returns the MultiIsolatePlatform used for an Environment or IsolateData // instance, if one exists. NODE_EXTERN MultiIsolatePlatform* GetMultiIsolatePlatform(Environment* env); diff --git a/src/node_errors.cc b/src/node_errors.cc index 0cfb95e67b244e..aa54bc2fe7cc4e 100644 --- a/src/node_errors.cc +++ b/src/node_errors.cc @@ -449,8 +449,7 @@ static void ReportFatalException(Environment* env, } if (env->isolate_data()->options()->report_uncaught_exception) { - report::TriggerNodeReport( - isolate, env, report_message.c_str(), "Exception", "", error); + TriggerNodeReport(env, report_message.c_str(), "Exception", "", error); } if (env->options()->trace_uncaught) { @@ -482,10 +481,6 @@ void OnFatalError(const char* location, const char* message) { } Isolate* isolate = Isolate::TryGetCurrent(); - Environment* env = nullptr; - if (isolate != nullptr) { - env = Environment::GetCurrent(isolate); - } bool report_on_fatalerror; { Mutex::ScopedLock lock(node::per_process::cli_options_mutex); @@ -493,8 +488,7 @@ void OnFatalError(const char* location, const char* message) { } if (report_on_fatalerror) { - report::TriggerNodeReport( - isolate, env, message, "FatalError", "", Local()); + TriggerNodeReport(isolate, message, "FatalError", "", Local()); } fflush(stderr); @@ -512,10 +506,6 @@ void OOMErrorHandler(const char* location, bool is_heap_oom) { } Isolate* isolate = Isolate::TryGetCurrent(); - Environment* env = nullptr; - if (isolate != nullptr) { - env = Environment::GetCurrent(isolate); - } bool report_on_fatalerror; { Mutex::ScopedLock lock(node::per_process::cli_options_mutex); @@ -523,8 +513,7 @@ void OOMErrorHandler(const char* location, bool is_heap_oom) { } if (report_on_fatalerror) { - report::TriggerNodeReport( - isolate, env, message, "OOMError", "", Local()); + TriggerNodeReport(isolate, message, "OOMError", "", Local()); } fflush(stderr); diff --git a/src/node_report.cc b/src/node_report.cc index 446b88303d82a9..3970f4ec53127e 100644 --- a/src/node_report.cc +++ b/src/node_report.cc @@ -1,8 +1,8 @@ -#include "env-inl.h" -#include "json_utils.h" #include "node_report.h" #include "debug_utils-inl.h" #include "diagnosticfilename-inl.h" +#include "env-inl.h" +#include "json_utils.h" #include "node_internals.h" #include "node_metadata.h" #include "node_mutex.h" @@ -29,8 +29,6 @@ constexpr double SEC_PER_MICROS = 1e-6; constexpr int MAX_FRAME_COUNT = 10; namespace node { -namespace report { - using node::worker::Worker; using v8::Array; using v8::Context; @@ -53,6 +51,7 @@ using v8::TryCatch; using v8::V8; using v8::Value; +namespace report { // Internal/static function declarations static void WriteNodeReport(Isolate* isolate, Environment* env, @@ -83,102 +82,6 @@ static void PrintRelease(JSONWriter* writer); static void PrintCpuInfo(JSONWriter* writer); static void PrintNetworkInterfaceInfo(JSONWriter* writer); -// External function to trigger a report, writing to file. -std::string TriggerNodeReport(Isolate* isolate, - Environment* env, - const char* message, - const char* trigger, - const std::string& name, - Local error) { - std::string filename; - - // Determine the required report filename. In order of priority: - // 1) supplied on API 2) configured on startup 3) default generated - if (!name.empty()) { - // Filename was specified as API parameter. - filename = name; - } else { - std::string report_filename; - { - Mutex::ScopedLock lock(per_process::cli_options_mutex); - report_filename = per_process::cli_options->report_filename; - } - if (report_filename.length() > 0) { - // File name was supplied via start-up option. - filename = report_filename; - } else { - filename = *DiagnosticFilename(env != nullptr ? env->thread_id() : 0, - "report", "json"); - } - } - - // Open the report file stream for writing. Supports stdout/err, - // user-specified or (default) generated name - std::ofstream outfile; - std::ostream* outstream; - if (filename == "stdout") { - outstream = &std::cout; - } else if (filename == "stderr") { - outstream = &std::cerr; - } else { - std::string report_directory; - { - Mutex::ScopedLock lock(per_process::cli_options_mutex); - report_directory = per_process::cli_options->report_directory; - } - // Regular file. Append filename to directory path if one was specified - if (report_directory.length() > 0) { - std::string pathname = report_directory; - pathname += kPathSeparator; - pathname += filename; - outfile.open(pathname, std::ios::out | std::ios::binary); - } else { - outfile.open(filename, std::ios::out | std::ios::binary); - } - // Check for errors on the file open - if (!outfile.is_open()) { - std::cerr << "\nFailed to open Node.js report file: " << filename; - - if (report_directory.length() > 0) - std::cerr << " directory: " << report_directory; - - std::cerr << " (errno: " << errno << ")" << std::endl; - return ""; - } - outstream = &outfile; - std::cerr << "\nWriting Node.js report to file: " << filename; - } - - bool compact; - { - Mutex::ScopedLock lock(per_process::cli_options_mutex); - compact = per_process::cli_options->report_compact; - } - WriteNodeReport(isolate, env, message, trigger, filename, *outstream, - error, compact); - - // Do not close stdout/stderr, only close files we opened. - if (outfile.is_open()) { - outfile.close(); - } - - // Do not mix JSON and free-form text on stderr. - if (filename != "stderr") { - std::cerr << "\nNode.js report completed" << std::endl; - } - return filename; -} - -// External function to trigger a report, writing to a supplied stream. -void GetNodeReport(Isolate* isolate, - Environment* env, - const char* message, - const char* trigger, - Local error, - std::ostream& out) { - WriteNodeReport(isolate, env, message, trigger, "", out, error, false); -} - // Internal function to coordinate and write the various // sections of the report to the supplied stream static void WriteNodeReport(Isolate* isolate, @@ -319,12 +222,8 @@ static void WriteNodeReport(Isolate* isolate, expected_results += w->RequestInterrupt([&](Environment* env) { std::ostringstream os; - GetNodeReport(env->isolate(), - env, - "Worker thread subreport", - trigger, - Local(), - os); + GetNodeReport( + env, "Worker thread subreport", trigger, Local(), os); Mutex::ScopedLock lock(workers_mutex); worker_infos.emplace_back(os.str()); @@ -884,4 +783,136 @@ static void PrintRelease(JSONWriter* writer) { } } // namespace report + +// External function to trigger a report, writing to file. +std::string TriggerNodeReport(Isolate* isolate, + const char* message, + const char* trigger, + const std::string& name, + Local error) { + Environment* env = nullptr; + if (isolate != nullptr) { + env = Environment::GetCurrent(isolate); + } + return TriggerNodeReport(env, message, trigger, name, error); +} + +// External function to trigger a report, writing to file. +std::string TriggerNodeReport(Environment* env, + const char* message, + const char* trigger, + const std::string& name, + Local error) { + std::string filename; + + // Determine the required report filename. In order of priority: + // 1) supplied on API 2) configured on startup 3) default generated + if (!name.empty()) { + // Filename was specified as API parameter. + filename = name; + } else { + std::string report_filename; + { + Mutex::ScopedLock lock(per_process::cli_options_mutex); + report_filename = per_process::cli_options->report_filename; + } + if (report_filename.length() > 0) { + // File name was supplied via start-up option. + filename = report_filename; + } else { + filename = *DiagnosticFilename( + env != nullptr ? env->thread_id() : 0, "report", "json"); + } + } + + // Open the report file stream for writing. Supports stdout/err, + // user-specified or (default) generated name + std::ofstream outfile; + std::ostream* outstream; + if (filename == "stdout") { + outstream = &std::cout; + } else if (filename == "stderr") { + outstream = &std::cerr; + } else { + std::string report_directory; + { + Mutex::ScopedLock lock(per_process::cli_options_mutex); + report_directory = per_process::cli_options->report_directory; + } + // Regular file. Append filename to directory path if one was specified + if (report_directory.length() > 0) { + std::string pathname = report_directory; + pathname += kPathSeparator; + pathname += filename; + outfile.open(pathname, std::ios::out | std::ios::binary); + } else { + outfile.open(filename, std::ios::out | std::ios::binary); + } + // Check for errors on the file open + if (!outfile.is_open()) { + std::cerr << "\nFailed to open Node.js report file: " << filename; + + if (report_directory.length() > 0) + std::cerr << " directory: " << report_directory; + + std::cerr << " (errno: " << errno << ")" << std::endl; + return ""; + } + outstream = &outfile; + std::cerr << "\nWriting Node.js report to file: " << filename; + } + + bool compact; + { + Mutex::ScopedLock lock(per_process::cli_options_mutex); + compact = per_process::cli_options->report_compact; + } + + Isolate* isolate = nullptr; + if (env != nullptr) { + isolate = env->isolate(); + } + report::WriteNodeReport( + isolate, env, message, trigger, filename, *outstream, error, compact); + + // Do not close stdout/stderr, only close files we opened. + if (outfile.is_open()) { + outfile.close(); + } + + // Do not mix JSON and free-form text on stderr. + if (filename != "stderr") { + std::cerr << "\nNode.js report completed" << std::endl; + } + return filename; +} + +// External function to trigger a report, writing to a supplied stream. +void GetNodeReport(Isolate* isolate, + const char* message, + const char* trigger, + Local error, + std::ostream& out) { + Environment* env = nullptr; + if (isolate != nullptr) { + env = Environment::GetCurrent(isolate); + } + report::WriteNodeReport( + isolate, env, message, trigger, "", out, error, false); +} + +// External function to trigger a report, writing to a supplied stream. +void GetNodeReport(Environment* env, + const char* message, + const char* trigger, + Local error, + std::ostream& out) { + Isolate* isolate = nullptr; + if (env != nullptr) { + isolate = env->isolate(); + } + report::WriteNodeReport( + isolate, env, message, trigger, "", out, error, false); +} + } // namespace node diff --git a/src/node_report.h b/src/node_report.h index dde48f14ec0f43..7a2e817ac82f6b 100644 --- a/src/node_report.h +++ b/src/node_report.h @@ -14,24 +14,10 @@ #endif #include +#include namespace node { namespace report { - -// Function declarations - functions in src/node_report.cc -std::string TriggerNodeReport(v8::Isolate* isolate, - Environment* env, - const char* message, - const char* trigger, - const std::string& name, - v8::Local error); -void GetNodeReport(v8::Isolate* isolate, - Environment* env, - const char* message, - const char* trigger, - v8::Local error, - std::ostream& out); - // Function declarations - utility functions in src/node_report_utils.cc void WalkHandle(uv_handle_t* h, void* arg); diff --git a/src/node_report_module.cc b/src/node_report_module.cc index 29da71b9e2a71f..884f9ec325979a 100644 --- a/src/node_report_module.cc +++ b/src/node_report_module.cc @@ -44,8 +44,7 @@ void WriteReport(const FunctionCallbackInfo& info) { else error = Local(); - filename = TriggerNodeReport( - isolate, env, *message, *trigger, filename, error); + filename = TriggerNodeReport(env, *message, *trigger, filename, error); // Return value is the report filename info.GetReturnValue().Set( String::NewFromUtf8(isolate, filename.c_str()).ToLocalChecked()); @@ -65,8 +64,7 @@ void GetReport(const FunctionCallbackInfo& info) { else error = Local(); - GetNodeReport( - isolate, env, "JavaScript API", __func__, error, out); + GetNodeReport(env, "JavaScript API", __func__, error, out); // Return value is the contents of a report as a string. info.GetReturnValue().Set( diff --git a/test/addons/report-api/binding.cc b/test/addons/report-api/binding.cc new file mode 100644 index 00000000000000..f52da3c765d7df --- /dev/null +++ b/test/addons/report-api/binding.cc @@ -0,0 +1,53 @@ +#include +#include + +using v8::FunctionCallbackInfo; +using v8::Isolate; +using v8::Local; +using v8::Object; +using v8::Value; + +void TriggerReport(const FunctionCallbackInfo& args) { + Isolate* isolate = args.GetIsolate(); + + node::TriggerNodeReport( + isolate, "FooMessage", "BarTrigger", std::string(), Local()); +} + +void TriggerReportNoIsolate(const FunctionCallbackInfo& args) { + node::TriggerNodeReport(static_cast(nullptr), + "FooMessage", + "BarTrigger", + std::string(), + Local()); +} + +void TriggerReportEnv(const FunctionCallbackInfo& args) { + Isolate* isolate = args.GetIsolate(); + + node::TriggerNodeReport( + node::GetCurrentEnvironment(isolate->GetCurrentContext()), + "FooMessage", + "BarTrigger", + std::string(), + Local()); +} + +void TriggerReportNoEnv(const FunctionCallbackInfo& args) { + Isolate* isolate = args.GetIsolate(); + + node::TriggerNodeReport(static_cast(nullptr), + "FooMessage", + "BarTrigger", + std::string(), + Local()); +} + +void init(Local exports) { + NODE_SET_METHOD(exports, "triggerReport", TriggerReport); + NODE_SET_METHOD(exports, "triggerReportNoIsolate", TriggerReportNoIsolate); + NODE_SET_METHOD(exports, "triggerReportEnv", TriggerReportEnv); + NODE_SET_METHOD(exports, "triggerReportNoEnv", TriggerReportNoEnv); +} + +NODE_MODULE(NODE_GYP_MODULE_NAME, init) diff --git a/test/addons/report-api/binding.gyp b/test/addons/report-api/binding.gyp new file mode 100644 index 00000000000000..55fbe7050f18e4 --- /dev/null +++ b/test/addons/report-api/binding.gyp @@ -0,0 +1,9 @@ +{ + 'targets': [ + { + 'target_name': 'binding', + 'sources': [ 'binding.cc' ], + 'includes': ['../common.gypi'], + } + ] +} diff --git a/test/addons/report-api/test.js b/test/addons/report-api/test.js new file mode 100644 index 00000000000000..e5000f56a584e1 --- /dev/null +++ b/test/addons/report-api/test.js @@ -0,0 +1,44 @@ +'use strict'; + +const common = require('../../common'); +const assert = require('assert'); +const path = require('path'); +const helper = require('../../common/report.js'); +const tmpdir = require('../../common/tmpdir'); + +const binding = path.resolve(__dirname, `./build/${common.buildType}/binding`); +const addon = require(binding); + +function myAddonMain(method, hasJavaScriptFrames) { + tmpdir.refresh(); + process.report.directory = tmpdir.path; + + addon[method](); + + const reports = helper.findReports(process.pid, tmpdir.path); + assert.strictEqual(reports.length, 1); + + const report = reports[0]; + helper.validate(report); + + const content = require(report); + assert.strictEqual(content.header.event, 'FooMessage'); + assert.strictEqual(content.header.trigger, 'BarTrigger'); + + // Check that the javascript stack is present. + if (hasJavaScriptFrames) { + assert.strictEqual(content.javascriptStack.stack.findIndex((frame) => frame.match('myAddonMain')), 0); + } else { + assert.strictEqual(content.javascriptStack, undefined); + } +} + +const methods = [ + ['triggerReport', true], + ['triggerReportNoIsolate', false], + ['triggerReportEnv', true], + ['triggerReportNoEnv', false], +]; +for (const [method, hasJavaScriptFrames] of methods) { + myAddonMain(method, hasJavaScriptFrames); +} diff --git a/test/cctest/test_report.cc b/test/cctest/test_report.cc new file mode 100644 index 00000000000000..861fa40385e206 --- /dev/null +++ b/test/cctest/test_report.cc @@ -0,0 +1,125 @@ +#include "node.h" + +#include +#include "gtest/gtest.h" +#include "node_test_fixture.h" + +using node::Environment; +using v8::Context; +using v8::Function; +using v8::FunctionCallbackInfo; +using v8::HandleScope; +using v8::Isolate; +using v8::Local; +using v8::SealHandleScope; +using v8::String; +using v8::Value; + +bool report_callback_called = false; + +class ReportTest : public EnvironmentTestFixture { + private: + void TearDown() override { + NodeTestFixture::TearDown(); + report_callback_called = false; + } +}; + +TEST_F(ReportTest, ReportWithNoIsolate) { + SealHandleScope handle_scope(isolate_); + + std::ostringstream oss; + node::GetNodeReport(static_cast(nullptr), + "FooMessage", + "BarTrigger", + Local(), + oss); + + // Simple checks on the output string contains the message and trigger. + std::string actual = oss.str(); + EXPECT_NE(actual.find("FooMessage"), std::string::npos); + EXPECT_NE(actual.find("BarTrigger"), std::string::npos); +} + +TEST_F(ReportTest, ReportWithNoEnv) { + SealHandleScope handle_scope(isolate_); + + std::ostringstream oss; + node::GetNodeReport(static_cast(nullptr), + "FooMessage", + "BarTrigger", + Local(), + oss); + + // Simple checks on the output string contains the message and trigger. + std::string actual = oss.str(); + EXPECT_NE(actual.find("FooMessage"), std::string::npos); + EXPECT_NE(actual.find("BarTrigger"), std::string::npos); +} + +TEST_F(ReportTest, ReportWithIsolate) { + const HandleScope handle_scope(isolate_); + const Argv argv; + Env env{handle_scope, argv}; + + Local context = isolate_->GetCurrentContext(); + Local fn = + Function::New(context, [](const FunctionCallbackInfo& args) { + Isolate* isolate = args.GetIsolate(); + HandleScope scope(isolate); + + std::ostringstream oss; + node::GetNodeReport(isolate, "FooMessage", "BarTrigger", args[0], oss); + + // Simple checks on the output string contains the message and trigger. + std::string actual = oss.str(); + EXPECT_NE(actual.find("FooMessage"), std::string::npos); + EXPECT_NE(actual.find("BarTrigger"), std::string::npos); + + report_callback_called = true; + }).ToLocalChecked(); + + context->Global() + ->Set(context, String::NewFromUtf8(isolate_, "foo").ToLocalChecked(), fn) + .FromJust(); + + node::LoadEnvironment(*env, "foo()").ToLocalChecked(); + + EXPECT_TRUE(report_callback_called); +} + +TEST_F(ReportTest, ReportWithEnv) { + const HandleScope handle_scope(isolate_); + const Argv argv; + Env env{handle_scope, argv}; + + Local context = isolate_->GetCurrentContext(); + Local fn = + Function::New(context, [](const FunctionCallbackInfo& args) { + Isolate* isolate = args.GetIsolate(); + HandleScope scope(isolate); + + std::ostringstream oss; + node::GetNodeReport( + node::GetCurrentEnvironment(isolate->GetCurrentContext()), + "FooMessage", + "BarTrigger", + args[0], + oss); + + // Simple checks on the output string contains the message and trigger. + std::string actual = oss.str(); + EXPECT_NE(actual.find("FooMessage"), std::string::npos); + EXPECT_NE(actual.find("BarTrigger"), std::string::npos); + + report_callback_called = true; + }).ToLocalChecked(); + + context->Global() + ->Set(context, String::NewFromUtf8(isolate_, "foo").ToLocalChecked(), fn) + .FromJust(); + + node::LoadEnvironment(*env, "foo()").ToLocalChecked(); + + EXPECT_TRUE(report_callback_called); +}