Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions src/node.h
Original file line number Diff line number Diff line change
Expand Up @@ -844,6 +844,16 @@ NODE_EXTERN void SetProcessExitHandler(
std::function<void(Environment*, int)>&& handler);
NODE_EXTERN void DefaultProcessExitHandler(Environment* env, int exit_code);

// Sets a process-global handler invoked when Node.js programmatically aborts
// (the ABORT() macro: failed CHECK/DCHECK, UNREACHABLE(), node::Assert(), and

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These are internal details. I don' think this should be documented in the public api.

// OnFatalError()). The handler may return, in which case Node.js still
// terminates the process (via abort()/_exit), or it may terminate the

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
// terminates the process (via abort()/_exit), or it may terminate the
// terminates the process.

// process itself. The default handler writes the native and JavaScript

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"writes native and JS backtrace" is an best effort behavior. It is not guaranteed and better not be documented as the public API contract.

// backtraces to stderr. Passing nullptr restores the default handler. This is
// process-global and may be invoked before any Isolate or Environment exists.
using AbortHandler = void (*)();

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In order to allow embedders/addons to persist the reason of the abort (like distinguishing multiple ABORT calls in a same function), it would be better to add a const char* message parameter.

NODE_EXTERN void SetAbortHandler(AbortHandler handler);

// This may return nullptr if context is not associated with a Node instance.
NODE_EXTERN Environment* GetCurrentEnvironment(v8::Local<v8::Context> context);
NODE_EXTERN IsolateData* GetEnvironmentIsolateData(Environment* env);
Expand Down
20 changes: 20 additions & 0 deletions src/node_errors.cc
Original file line number Diff line number Diff line change
Expand Up @@ -393,6 +393,26 @@ void AppendExceptionLine(Environment* env,
.FromMaybe(false));
}

namespace {
// Default handler: reproduces previous ABORT() output (native + JS backtrace to

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is weird to say "previous" in the document. Please just document the behavior straight.

// stderr). Termination is NOT done here. The ABORT() macro backstops it.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The API contract is that the abort handler should not return (even if the ABORT() macro ensures that the abort handler must not return). This default handler should also not return.

void DefaultAbortHandler() {
DumpNativeBacktrace(stderr);
DumpJavaScriptBacktrace(stderr);
fflush(stderr);
}
// Constant-initialized, so this is valid from load time, safe even for a
// CHECK() during early startup, before any SetAbortHandler call.
AbortHandler g_abort_handler = DefaultAbortHandler;
} // namespace

void SetAbortHandler(AbortHandler handler) {
g_abort_handler = handler ? handler : DefaultAbortHandler;
}
AbortHandler GetAbortHandler() {
return g_abort_handler;
}

void Assert(const AssertionInfo& info) {
std::string name = GetHumanReadableProcessName();

Expand Down
7 changes: 4 additions & 3 deletions src/util.h
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,9 @@ void NODE_EXTERN_PRIVATE Assert(const AssertionInfo& info);
void DumpNativeBacktrace(FILE* fp);
void DumpJavaScriptBacktrace(FILE* fp);

// Returns the currently installed abort handler which is never null.
AbortHandler GetAbortHandler();

// Windows 8+ does not like abort() in Release mode
#ifdef _WIN32
#define ABORT_NO_BACKTRACE() _exit(static_cast<int>(node::ExitCode::kAbort))
Expand All @@ -142,9 +145,7 @@ void DumpJavaScriptBacktrace(FILE* fp);
// backtrace is correct.
#define ABORT() \
do { \
node::DumpNativeBacktrace(stderr); \
node::DumpJavaScriptBacktrace(stderr); \
fflush(stderr); \
node::GetAbortHandler()(); \
ABORT_NO_BACKTRACE(); \
} while (0)

Expand Down
4 changes: 4 additions & 0 deletions test/abort/test-addon-abort-handler.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
'use strict';
require('../common');
process.env.ALLOW_CRASHES = true;
require('../addons/abort-handler/test');

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This creates a dependency of a test/abort test on test/addons. Tests in test/addons depends on the build-addons target in the Makefile. This test should be integrated into test/addons/abort-handler/test to avoid the dependencies.

18 changes: 18 additions & 0 deletions test/addons/abort-handler/binding.cc
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
#include <node.h>
#include <v8.h>
#include <cstdio>

namespace {
void TestAbortHandler() {
fputs("CUSTOM_ABORT_HANDLER_RAN\n", stderr);
fflush(stderr);
}

void InstallAbortHandler(const v8::FunctionCallbackInfo<v8::Value>&) {
node::SetAbortHandler(TestAbortHandler);
}
} // namespace

NODE_MODULE_INIT() {
NODE_SET_METHOD(exports, "installAbortHandler", InstallAbortHandler);
}
9 changes: 9 additions & 0 deletions test/addons/abort-handler/binding.gyp
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
'targets': [
{
'target_name': 'binding',
'sources': [ 'binding.cc' ],
'includes': ['../common.gypi'],
}
]
}
43 changes: 43 additions & 0 deletions test/addons/abort-handler/test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
'use strict';
const common = require('../../common');
const assert = require('assert');
const fs = require('fs');
const path = require('path');
const { spawnSync } = require('child_process');

const bindingPath = path.resolve(
__dirname, 'build', common.buildType, 'binding.node');

if (!fs.existsSync(bindingPath))
common.skip('binding not built yet');

if (process.argv[2] === 'child') {
const binding = require(bindingPath);
binding.installAbortHandler();
process.abort();
return;
}

// We do not want to generate core files / actually crash the process when
// running this test as a regular addon test. It is also required as an
// abort test with ALLOW_CRASHES set (see ../../abort/test-addon-abort-handler.js).
if (!process.env.ALLOW_CRASHES)
common.skip('test needs ALLOW_CRASHES to spawn a crashing child');

const result = spawnSync(process.execPath, [__filename, 'child']);

const stderr = result.stderr.toString();
assert.ok(
stderr.includes('CUSTOM_ABORT_HANDLER_RAN'),
`Expected custom abort handler marker in stderr, got:\n${stderr}`);
assert.ok(
!stderr.includes('Native stack trace'),
`Expected the custom handler to replace the default dump, got:\n${stderr}`);

if (common.isWindows) {
assert.strictEqual(result.status, 134);
assert.strictEqual(result.signal, null);
} else {
assert.strictEqual(result.status, null);
assert.strictEqual(result.signal, 'SIGABRT');
}
40 changes: 40 additions & 0 deletions test/cctest/test_environment.cc
Original file line number Diff line number Diff line change
Expand Up @@ -1201,3 +1201,43 @@ TEST_F(EnvironmentTest, LoadEnvironmentWithCallbackWithESModule) {
printf("Frame: %s\n", *frame_str);
EXPECT_EQ(frame_str.ToString(), " at embedded:esm.mjs:3:15");
}

namespace {
void CustomAbortHandlerForContractTest() {}

bool abort_handler_dispatch_flag = false;
void AbortHandlerThatSetsDispatchFlag() {
abort_handler_dispatch_flag = true;
}
} // namespace

TEST(AbortHandlerTest, DefaultIsNonNullAndSetAbortHandlerRoundTrips) {
node::AbortHandler old = node::GetAbortHandler();

// There should always be a non-null default handler installed.
EXPECT_NE(node::GetAbortHandler(), nullptr);

node::SetAbortHandler(CustomAbortHandlerForContractTest);
EXPECT_EQ(node::GetAbortHandler(), CustomAbortHandlerForContractTest);

node::SetAbortHandler(nullptr);
EXPECT_NE(node::GetAbortHandler(), nullptr);
EXPECT_NE(node::GetAbortHandler(), CustomAbortHandlerForContractTest);

node::SetAbortHandler(old);
}

TEST(AbortHandlerTest, InstalledHandlerIsInvokedWhenCalled) {
node::AbortHandler old = node::GetAbortHandler();
abort_handler_dispatch_flag = false;

node::SetAbortHandler(AbortHandlerThatSetsDispatchFlag);
node::AbortHandler h = node::GetAbortHandler();
// Fail cleanly (instead of crashing on a null call) if the handler wasn't
// actually installed.
ASSERT_NE(h, nullptr);
h();
EXPECT_TRUE(abort_handler_dispatch_flag);

node::SetAbortHandler(old);
}
Loading