From 17f65f564878e06a4b7dc7daa1c7f846b5a878c2 Mon Sep 17 00:00:00 2001 From: legendecas Date: Wed, 17 Aug 2022 01:41:15 +0800 Subject: [PATCH] report: expose report public native apis Exposing the report APIs to the add-on can be helpful for the add-on to generate a diagnostic report when they are in a state of panic and can not recover. Also, it allows APM vendors to generate a diagnostic report without calling into JavaScript. --- node.gyp | 1 + src/node.h | 18 +++ src/node_errors.cc | 6 +- src/node_report.cc | 201 +++++++++++++++-------------- src/node_report.h | 31 ++--- test/addons/report-api/binding.cc | 26 ++++ test/addons/report-api/binding.gyp | 9 ++ test/addons/report-api/test.js | 32 +++++ test/cctest/test_report.cc | 73 +++++++++++ 9 files changed, 278 insertions(+), 119 deletions(-) create mode 100644 test/addons/report-api/binding.cc create mode 100644 test/addons/report-api/binding.gyp create mode 100644 test/addons/report-api/test.js create mode 100644 test/cctest/test_report.cc diff --git a/node.gyp b/node.gyp index 9563073fd6c898..317bc6c84e8ce1 100644 --- a/node.gyp +++ b/node.gyp @@ -992,6 +992,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..e401299f60315e 100644 --- a/src/node.h +++ b/src/node.h @@ -617,6 +617,24 @@ 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 or env is nullptr, no information about the isolate and env +// is included in the report. +NODE_EXTERN std::string TriggerNodeReport(v8::Isolate* isolate, + Environment* env, + const char* message, + const char* trigger, + const std::string& filename, + v8::Local error); +NODE_EXTERN void GetNodeReport(v8::Isolate* isolate, + 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 b196025ae7c34f..9a8a55f689528a 100644 --- a/src/node_errors.cc +++ b/src/node_errors.cc @@ -485,8 +485,7 @@ void OnFatalError(const char* location, const char* message) { } if (report_on_fatalerror) { - report::TriggerNodeReport( - isolate, env, message, "FatalError", "", Local()); + TriggerNodeReport(isolate, env, message, "FatalError", "", Local()); } fflush(stderr); @@ -515,8 +514,7 @@ void OOMErrorHandler(const char* location, bool is_heap_oom) { } if (report_on_fatalerror) { - report::TriggerNodeReport( - isolate, env, message, "OOMError", "", Local()); + TriggerNodeReport(isolate, env, message, "OOMError", "", Local()); } fflush(stderr); diff --git a/src/node_report.cc b/src/node_report.cc index 446b88303d82a9..8c42d192cf1017 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, @@ -884,4 +787,102 @@ static void PrintRelease(JSONWriter* writer) { } } // namespace report + +// 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; + } + 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, + Environment* env, + const char* message, + const char* trigger, + Local error, + std::ostream& out) { + 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..b6f683682c0a5e 100644 --- a/src/node_report.h +++ b/src/node_report.h @@ -13,25 +13,11 @@ #include #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); @@ -49,6 +35,21 @@ void WriteReport(const v8::FunctionCallbackInfo& info); void GetReport(const v8::FunctionCallbackInfo& info); } // 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); + } // namespace node #endif // defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS diff --git a/test/addons/report-api/binding.cc b/test/addons/report-api/binding.cc new file mode 100644 index 00000000000000..476b01512151b2 --- /dev/null +++ b/test/addons/report-api/binding.cc @@ -0,0 +1,26 @@ +#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, + node::GetCurrentEnvironment(isolate->GetCurrentContext()), + "FooMessage", + "BarTrigger", + std::string(), + Local()); +} + +void init(Local exports) { + NODE_SET_METHOD(exports, "triggerReport", TriggerReport); +} + +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..8bd08beb8bfeda --- /dev/null +++ b/test/addons/report-api/test.js @@ -0,0 +1,32 @@ +'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() { + tmpdir.refresh(); + process.report.directory = tmpdir.path; + + addon.triggerReport(); + + 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. + assert.strictEqual(content.javascriptStack.stack.findIndex((frame) => frame.match('myAddonMain')), 0); +} + +myAddonMain(); diff --git a/test/cctest/test_report.cc b/test/cctest/test_report.cc new file mode 100644 index 00000000000000..ab76188a04f27f --- /dev/null +++ b/test/cctest/test_report.cc @@ -0,0 +1,73 @@ +#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, ReportWithNoIsolateAndEnv) { + SealHandleScope handle_scope(isolate_); + + std::ostringstream oss; + node::GetNodeReport( + nullptr, 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, ReportWithIsolateAndEnv) { + 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); + Environment* env = + node::GetCurrentEnvironment(isolate->GetCurrentContext()); + + std::ostringstream oss; + node::GetNodeReport( + isolate, env, "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); +}