process: add flag for uncaught exception abort

Introduce `process.shouldAbortOnUncaughtException` to control
`--abort-on-uncaught-exception` behaviour, and implement
some of the domains functionality on top of it.

PR-URL: https://github.com/nodejs/node/pull/17159
Refs: https://github.com/nodejs/node/issues/17143
Reviewed-By: James M Snell <jasnell@gmail.com>
Reviewed-By: Andreas Madsen <amwebdk@gmail.com>
This commit is contained in:
Anna Henningsen 2017-11-20 19:57:20 +01:00
parent 04e3aa28bb
commit 4503da8a3a
No known key found for this signature in database
GPG Key ID: 9C63F3A6CD2AD8F9
16 changed files with 320 additions and 74 deletions

View File

@ -183,6 +183,10 @@ added: v0.10
Aborting instead of exiting causes a core file to be generated for post-mortem
analysis using a debugger (such as `lldb`, `gdb`, and `mdb`).
*Note*: If this flag is passed, the behavior can still be set to not abort
through [`process.setUncaughtExceptionCaptureCallback()`][] (and through usage
of the `domain` module that uses it).
### `--trace-warnings`
<!-- YAML
added: v6.0.0
@ -598,3 +602,4 @@ greater than `4` (its current default value). For more information, see the
[debugger]: debugger.html
[emit_warning]: process.html#process_process_emitwarning_warning_type_code_ctor
[libuv threadpool documentation]: http://docs.libuv.org/en/latest/threadpool.html
[`process.setUncaughtExceptionCaptureCallback()`]: process.html#process_process_setuncaughtexceptioncapturecallback_fn

View File

@ -729,6 +729,23 @@ A signing `key` was not provided to the [`sign.sign()`][] method.
`c-ares` failed to set the DNS server.
<a id="ERR_DOMAIN_CALLBACK_NOT_AVAILABLE"></a>
### ERR_DOMAIN_CALLBACK_NOT_AVAILABLE
The `domain` module was not usable since it could not establish the required
error handling hooks, because
[`process.setUncaughtExceptionCaptureCallback()`][] had been called at an
earlier point in time.
<a id="ERR_DOMAIN_CANNOT_SET_UNCAUGHT_EXCEPTION_CAPTURE"></a>
### ERR_DOMAIN_CANNOT_SET_UNCAUGHT_EXCEPTION_CAPTURE
[`process.setUncaughtExceptionCaptureCallback()`][] could not be called
because the `domain` module has been loaded at an earlier point in time.
The stack trace is extended to include the point in time at which the
`domain` module had been loaded.
<a id="ERR_ENCODING_INVALID_ENCODED_DATA"></a>
### ERR_ENCODING_INVALID_ENCODED_DATA
@ -1459,6 +1476,15 @@ A Transform stream finished while it was still transforming.
A Transform stream finished with data still in the write buffer.
<a id="ERR_UNCAUGHT_EXCEPTION_CAPTURE_ALREADY_SET"></a>
### ERR_UNCAUGHT_EXCEPTION_CAPTURE_ALREADY_SET
[`process.setUncaughtExceptionCaptureCallback()`][] was called twice,
without first resetting the callback to `null`.
This error is designed to prevent accidentally overwriting a callback registered
from another module.
<a id="ERR_UNESCAPED_CHARACTERS"></a>
### ERR_UNESCAPED_CHARACTERS
@ -1565,6 +1591,7 @@ Creation of a [`zlib`][] object failed due to incorrect configuration.
[`new URLSearchParams(iterable)`]: url.html#url_constructor_new_urlsearchparams_iterable
[`process.on('uncaughtException')`]: process.html#process_event_uncaughtexception
[`process.send()`]: process.html#process_process_send_message_sendhandle_options_callback
[`process.setUncaughtExceptionCaptureCallback()`]: process.html#process_process_setuncaughtexceptioncapturecallback_fn
[`require('crypto').setEngine()`]: crypto.html#crypto_crypto_setengine_engine_flags
[`server.listen()`]: net.html#net_server_listen
[ES6 module]: esm.html

View File

@ -1136,6 +1136,16 @@ if (process.getuid) {
*Note*: This function is only available on POSIX platforms (i.e. not Windows
or Android).
## process.hasUncaughtExceptionCaptureCallback()
<!-- YAML
added: REPLACEME
-->
* Returns: {boolean}
Indicates whether a callback has been set using
[`process.setUncaughtExceptionCaptureCallback()`][].
## process.hrtime([time])
<!-- YAML
added: v0.7.6
@ -1637,6 +1647,29 @@ if (process.getuid && process.setuid) {
or Android).
## process.setUncaughtExceptionCaptureCallback(fn)
<!-- YAML
added: REPLACEME
-->
* `fn` {Function|null}
The `process.setUncaughtExceptionCapture` function sets a function that will
be invoked when an uncaught exception occurs, which will receive the exception
value itself as its first argument.
If such a function is set, the [`process.on('uncaughtException')`][] event will
not be emitted. If `--abort-on-uncaught-exception` was passed from the
command line or set through [`v8.setFlagsFromString()`][], the process will
not abort.
To unset the capture function, `process.setUncaughtExceptionCapture(null)`
may be used. Calling this method with a non-`null` argument while another
capture function is set will throw an error.
*Note*: Using this function is mutually exclusive with using the
deprecated [`domain`][] built-in module.
## process.stderr
* {Stream}
@ -1921,6 +1954,7 @@ cases:
[`JSON.stringify` spec]: https://tc39.github.io/ecma262/#sec-json.stringify
[`console.error()`]: console.html#console_console_error_data_args
[`console.log()`]: console.html#console_console_log_data_args
[`domain`]: domain.html
[`end()`]: stream.html#stream_writable_end_chunk_encoding_callback
[`net.Server`]: net.html#net_class_net_server
[`net.Socket`]: net.html#net_class_net_socket
@ -1930,11 +1964,14 @@ cases:
[`process.exit()`]: #process_process_exit_code
[`process.exitCode`]: #process_process_exitcode
[`process.kill()`]: #process_process_kill_pid_signal
[`process.on('uncaughtException')`]: process.html#process_event_uncaughtexception
[`process.setUncaughtExceptionCaptureCallback()`]: process.html#process_process_setuncaughtexceptioncapturecallback_fn
[`promise.catch()`]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/catch
[`require()`]: globals.html#globals_require
[`require.main`]: modules.html#modules_accessing_the_main_module
[`require.resolve()`]: modules.html#modules_require_resolve_request_options
[`setTimeout(fn, 0)`]: timers.html#timers_settimeout_callback_delay_args
[`v8.setFlagsFromString()`]: v8.html#v8_v8_setflagsfromstring_flags
[Child Process]: child_process.html
[Cluster]: cluster.html
[Duplex]: stream.html#stream_duplex_and_transform_streams

View File

@ -28,6 +28,7 @@
const util = require('util');
const EventEmitter = require('events');
const errors = require('internal/errors');
const { createHook } = require('async_hooks');
// communicate with events module, but don't require that
@ -81,19 +82,77 @@ const asyncHook = createHook({
}
});
// When domains are in use, they claim full ownership of the
// uncaught exception capture callback.
if (process.hasUncaughtExceptionCaptureCallback()) {
throw new errors.Error('ERR_DOMAIN_CALLBACK_NOT_AVAILABLE');
}
// Get the stack trace at the point where `domain` was required.
const domainRequireStack = new Error('require(`domain`) at this point').stack;
const { setUncaughtExceptionCaptureCallback } = process;
process.setUncaughtExceptionCaptureCallback = function(fn) {
const err =
new errors.Error('ERR_DOMAIN_CANNOT_SET_UNCAUGHT_EXCEPTION_CAPTURE');
err.stack = err.stack + '\n' + '-'.repeat(40) + '\n' + domainRequireStack;
throw err;
};
// It's possible to enter one domain while already inside
// another one. The stack is each entered domain.
const stack = [];
exports._stack = stack;
process._setupDomainUse(stack);
process._setupDomainUse();
function updateExceptionCapture() {
if (stack.every((domain) => domain.listenerCount('error') === 0)) {
setUncaughtExceptionCaptureCallback(null);
} else {
setUncaughtExceptionCaptureCallback(null);
setUncaughtExceptionCaptureCallback((er) => {
return process.domain._errorHandler(er);
});
}
}
process.on('newListener', (name, listener) => {
if (name === 'uncaughtException' &&
listener !== domainUncaughtExceptionClear) {
// Make sure the first listener for `uncaughtException` always clears
// the domain stack.
process.removeListener(name, domainUncaughtExceptionClear);
process.prependListener(name, domainUncaughtExceptionClear);
}
});
process.on('removeListener', (name, listener) => {
if (name === 'uncaughtException' &&
listener !== domainUncaughtExceptionClear) {
// If the domain listener would be the only remaining one, remove it.
const listeners = process.listeners('uncaughtException');
if (listeners.length === 1 && listeners[0] === domainUncaughtExceptionClear)
process.removeListener(name, domainUncaughtExceptionClear);
}
});
function domainUncaughtExceptionClear() {
stack.length = 0;
exports.active = process.domain = null;
updateExceptionCapture();
}
class Domain extends EventEmitter {
constructor() {
super();
this.members = [];
asyncHook.enable();
this.on('removeListener', updateExceptionCapture);
this.on('newListener', updateExceptionCapture);
}
}
@ -131,14 +190,14 @@ Domain.prototype._errorHandler = function _errorHandler(er) {
// prevent the process 'uncaughtException' event from being emitted
// if a listener is set.
if (EventEmitter.listenerCount(this, 'error') > 0) {
// Clear the uncaughtExceptionCaptureCallback so that we know that, even
// if technically the top-level domain is still active, it would
// be ok to abort on an uncaught exception at this point
setUncaughtExceptionCaptureCallback(null);
try {
// Set the _emittingTopLevelDomainError so that we know that, even
// if technically the top-level domain is still active, it would
// be ok to abort on an uncaught exception at this point
process._emittingTopLevelDomainError = true;
caught = this.emit('error', er);
} finally {
process._emittingTopLevelDomainError = false;
updateExceptionCapture();
}
}
} else {
@ -161,11 +220,13 @@ Domain.prototype._errorHandler = function _errorHandler(er) {
if (this === exports.active) {
stack.pop();
}
updateExceptionCapture();
if (stack.length) {
exports.active = process.domain = stack[stack.length - 1];
caught = process._fatalException(er2);
caught = process.domain._errorHandler(er2);
} else {
caught = false;
// Pass on to the next exception handler.
throw er2;
}
}
}
@ -173,8 +234,7 @@ Domain.prototype._errorHandler = function _errorHandler(er) {
// Exit all domains on the stack. Uncaught exceptions end the
// current tick and no domains should be left on the stack
// between ticks.
stack.length = 0;
exports.active = process.domain = null;
domainUncaughtExceptionClear();
return caught;
};
@ -185,6 +245,7 @@ Domain.prototype.enter = function() {
// to push it onto the stack so that we can pop it later.
exports.active = process.domain = this;
stack.push(this);
updateExceptionCapture();
};
@ -198,6 +259,7 @@ Domain.prototype.exit = function() {
exports.active = stack[stack.length - 1];
process.domain = exports.active;
updateExceptionCapture();
};

View File

@ -9,6 +9,7 @@
(function(process) {
let internalBinding;
const exceptionHandlerState = { captureFn: null };
function startup() {
const EventEmitter = NativeModule.require('events');
@ -35,6 +36,7 @@
const _process = NativeModule.require('internal/process');
_process.setupConfig(NativeModule._source);
_process.setupSignalHandlers();
_process.setupUncaughtExceptionCapture(exceptionHandlerState);
NativeModule.require('internal/process/warning').setup();
NativeModule.require('internal/process/next_tick').setup();
NativeModule.require('internal/process/stdio').setup();
@ -377,8 +379,10 @@
// that threw and was never cleared. So clear it now.
async_id_fields[kInitTriggerAsyncId] = 0;
if (process.domain && process.domain._errorHandler)
caught = process.domain._errorHandler(er);
if (exceptionHandlerState.captureFn !== null) {
exceptionHandlerState.captureFn(er);
caught = true;
}
if (!caught)
caught = process.emit('uncaughtException', er);

View File

@ -291,6 +291,13 @@ E('ERR_CRYPTO_SIGN_KEY_REQUIRED', 'No key provided to sign');
E('ERR_CRYPTO_TIMING_SAFE_EQUAL_LENGTH',
'Input buffers must have the same length');
E('ERR_DNS_SET_SERVERS_FAILED', 'c-ares failed to set servers: "%s" [%s]');
E('ERR_DOMAIN_CALLBACK_NOT_AVAILABLE',
'A callback was registered through ' +
'process.setUncaughtExceptionCaptureCallback(), which is mutually ' +
'exclusive with using the `domain` module');
E('ERR_DOMAIN_CANNOT_SET_UNCAUGHT_EXCEPTION_CAPTURE',
'The `domain` module is in use, which is mutually exclusive with calling ' +
'process.setUncaughtExceptionCaptureCallback()');
E('ERR_ENCODING_INVALID_ENCODED_DATA',
'The encoded data was not valid for encoding %s');
E('ERR_ENCODING_NOT_SUPPORTED', 'The "%s" encoding is not supported');
@ -461,6 +468,9 @@ E('ERR_TRANSFORM_ALREADY_TRANSFORMING',
'Calling transform done when still transforming');
E('ERR_TRANSFORM_WITH_LENGTH_0',
'Calling transform done when writableState.length != 0');
E('ERR_UNCAUGHT_EXCEPTION_CAPTURE_ALREADY_SET',
'`process.setupUncaughtExceptionCapture()` was called while a capture ' +
'callback was already active');
E('ERR_UNESCAPED_CHARACTERS', '%s contains unescaped characters');
E('ERR_UNHANDLED_ERROR',
(err) => {

View File

@ -247,6 +247,34 @@ function setupRawDebug() {
};
}
function setupUncaughtExceptionCapture(exceptionHandlerState) {
// This is a typed array for faster communication with JS.
const shouldAbortOnUncaughtToggle = process._shouldAbortOnUncaughtToggle;
delete process._shouldAbortOnUncaughtToggle;
process.setUncaughtExceptionCaptureCallback = function(fn) {
if (fn === null) {
exceptionHandlerState.captureFn = fn;
shouldAbortOnUncaughtToggle[0] = 1;
return;
}
if (typeof fn !== 'function') {
throw new errors.TypeError('ERR_INVALID_ARG_TYPE', 'fn',
['Function', 'null']);
}
if (exceptionHandlerState.captureFn !== null) {
throw new errors.Error('ERR_UNCAUGHT_EXCEPTION_CAPTURE_ALREADY_SET');
}
exceptionHandlerState.captureFn = fn;
shouldAbortOnUncaughtToggle[0] = 0;
};
process.hasUncaughtExceptionCaptureCallback = function() {
return exceptionHandlerState.captureFn !== null;
};
}
module.exports = {
setup_performance,
setup_cpuUsage,
@ -256,5 +284,6 @@ module.exports = {
setupKillAndExit,
setupSignalHandlers,
setupChannel,
setupRawDebug
setupRawDebug,
setupUncaughtExceptionCapture
};

View File

@ -268,6 +268,7 @@ inline Environment::Environment(IsolateData* isolate_data,
emit_napi_warning_(true),
makecallback_cntr_(0),
scheduled_immediate_count_(isolate_, 1),
should_abort_on_uncaught_toggle_(isolate_, 1),
#if HAVE_INSPECTOR
inspector_agent_(new inspector::Agent(this)),
#endif
@ -305,6 +306,9 @@ inline Environment::Environment(IsolateData* isolate_data,
performance_state_->milestones[
performance::NODE_PERFORMANCE_MILESTONE_V8_START] =
performance::performance_v8_start;
// By default, always abort when --abort-on-uncaught-exception was passed.
should_abort_on_uncaught_toggle_[0] = 1;
}
inline Environment::~Environment() {
@ -399,6 +403,11 @@ inline void Environment::set_abort_on_uncaught_exception(bool value) {
abort_on_uncaught_exception_ = value;
}
inline AliasedBuffer<uint32_t, v8::Uint32Array>&
Environment::should_abort_on_uncaught_toggle() {
return should_abort_on_uncaught_toggle_;
}
inline std::vector<double>* Environment::destroy_async_id_list() {
return &destroy_async_id_list_;
}

View File

@ -134,7 +134,6 @@ class ModuleWrap;
V(dns_txt_string, "TXT") \
V(domain_string, "domain") \
V(emit_string, "emit") \
V(emitting_top_level_domain_error_string, "_emittingTopLevelDomainError") \
V(exchange_string, "exchange") \
V(enumerable_string, "enumerable") \
V(idle_string, "idle") \
@ -309,7 +308,6 @@ class ModuleWrap;
V(internal_binding_cache_object, v8::Object) \
V(buffer_prototype_object, v8::Object) \
V(context, v8::Context) \
V(domains_stack_array, v8::Array) \
V(http2ping_constructor_template, v8::ObjectTemplate) \
V(http2stream_constructor_template, v8::ObjectTemplate) \
V(inspector_console_api_object, v8::Object) \
@ -568,8 +566,15 @@ class Environment {
void PrintSyncTrace() const;
inline void set_trace_sync_io(bool value);
// This stores whether the --abort-on-uncaught-exception flag was passed
// to Node.
inline bool abort_on_uncaught_exception() const;
inline void set_abort_on_uncaught_exception(bool value);
// This is a pseudo-boolean that keeps track of whether an uncaught exception
// should abort the process or not if --abort-on-uncaught-exception was
// passed to Node. If the flag was not passed, it is ignored.
inline AliasedBuffer<uint32_t, v8::Uint32Array>&
should_abort_on_uncaught_toggle();
// The necessary API for async_hooks.
inline double new_async_id();
@ -717,6 +722,7 @@ class Environment {
std::vector<double> destroy_async_id_list_;
AliasedBuffer<uint32_t, v8::Uint32Array> scheduled_immediate_count_;
AliasedBuffer<uint32_t, v8::Uint32Array> should_abort_on_uncaught_toggle_;
performance::performance_state* performance_state_ = nullptr;
std::map<std::string, uint64_t> performance_marks_;

View File

@ -779,66 +779,13 @@ void* ArrayBufferAllocator::Allocate(size_t size) {
namespace {
bool DomainHasErrorHandler(const Environment* env,
const Local<Object>& domain) {
HandleScope scope(env->isolate());
Local<Value> domain_event_listeners_v = domain->Get(env->events_string());
if (!domain_event_listeners_v->IsObject())
return false;
Local<Object> domain_event_listeners_o =
domain_event_listeners_v.As<Object>();
Local<Value> domain_error_listeners_v =
domain_event_listeners_o->Get(env->error_string());
if (domain_error_listeners_v->IsFunction() ||
(domain_error_listeners_v->IsArray() &&
domain_error_listeners_v.As<Array>()->Length() > 0))
return true;
return false;
}
bool DomainsStackHasErrorHandler(const Environment* env) {
HandleScope scope(env->isolate());
if (!env->using_domains())
return false;
Local<Array> domains_stack_array = env->domains_stack_array().As<Array>();
if (domains_stack_array->Length() == 0)
return false;
uint32_t domains_stack_length = domains_stack_array->Length();
for (uint32_t i = domains_stack_length; i > 0; --i) {
Local<Value> domain_v = domains_stack_array->Get(i - 1);
if (!domain_v->IsObject())
return false;
Local<Object> domain = domain_v.As<Object>();
if (DomainHasErrorHandler(env, domain))
return true;
}
return false;
}
bool ShouldAbortOnUncaughtException(Isolate* isolate) {
HandleScope scope(isolate);
Environment* env = Environment::GetCurrent(isolate);
Local<Object> process_object = env->process_object();
Local<String> emitting_top_level_domain_error_key =
env->emitting_top_level_domain_error_string();
bool isEmittingTopLevelDomainError =
process_object->Get(emitting_top_level_domain_error_key)->BooleanValue();
return isEmittingTopLevelDomainError || !DomainsStackHasErrorHandler(env);
return env->should_abort_on_uncaught_toggle()[0];
}
Local<Value> GetDomainProperty(Environment* env, Local<Object> object) {
Local<Value> domain_v =
object->GetPrivate(env->context(), env->domain_private_symbol())
@ -888,9 +835,6 @@ void SetupDomainUse(const FunctionCallbackInfo<Value>& args) {
HandleScope scope(env->isolate());
CHECK(args[0]->IsArray());
env->set_domains_stack_array(args[0].As<Array>());
// Do a little housekeeping.
env->process_object()->Delete(
env->context(),
@ -3161,6 +3105,13 @@ void SetupProcessObject(Environment* env,
scheduled_immediate_count,
env->scheduled_immediate_count().GetJSArray()).FromJust());
auto should_abort_on_uncaught_toggle =
FIXED_ONE_BYTE_STRING(env->isolate(), "_shouldAbortOnUncaughtToggle");
CHECK(process->Set(env->context(),
should_abort_on_uncaught_toggle,
env->should_abort_on_uncaught_toggle().GetJSArray())
.FromJust());
// -e, --eval
if (eval_string) {
READONLY_PROPERTY(process,

View File

@ -0,0 +1,18 @@
'use strict';
const common = require('../common');
const assert = require('assert');
process.setUncaughtExceptionCaptureCallback(common.mustNotCall());
common.expectsError(
() => require('domain'),
{
code: 'ERR_DOMAIN_CALLBACK_NOT_AVAILABLE',
type: Error,
message: /^A callback was registered.*with using the `domain` module/
}
);
process.setUncaughtExceptionCaptureCallback(null);
assert.doesNotThrow(() => require('domain'));

View File

@ -0,0 +1,28 @@
'use strict';
const common = require('../common');
const assert = require('assert');
(function foobar() {
require('domain');
})();
assert.throws(
() => process.setUncaughtExceptionCaptureCallback(common.mustNotCall()),
(err) => {
common.expectsError(
{
code: 'ERR_DOMAIN_CANNOT_SET_UNCAUGHT_EXCEPTION_CAPTURE',
type: Error,
message: /^The `domain` module is in use, which is mutually/
}
)(err);
assert(err.stack.includes('-'.repeat(40)),
`expected ${err.stack} to contain dashes`);
const location = `at foobar (${__filename}:`;
assert(err.stack.includes(location),
`expected ${err.stack} to contain ${location}`);
return true;
}
);

View File

@ -0,0 +1,22 @@
'use strict';
const common = require('../common');
common.expectsError(
() => process.setUncaughtExceptionCaptureCallback(42),
{
code: 'ERR_INVALID_ARG_TYPE',
type: TypeError,
message: 'The "fn" argument must be one of type Function or null'
}
);
process.setUncaughtExceptionCaptureCallback(common.mustNotCall());
common.expectsError(
() => process.setUncaughtExceptionCaptureCallback(common.mustNotCall()),
{
code: 'ERR_UNCAUGHT_EXCEPTION_CAPTURE_ALREADY_SET',
type: Error,
message: /setupUncaughtExceptionCapture.*called while a capture callback/
}
);

View File

@ -0,0 +1,13 @@
'use strict';
const common = require('../common');
const assert = require('assert');
const v8 = require('v8');
assert.strictEqual(process.hasUncaughtExceptionCaptureCallback(), false);
v8.setFlagsFromString('--abort-on-uncaught-exception');
// This should make the process not crash even though the flag was passed.
process.setUncaughtExceptionCaptureCallback(common.mustCall((err) => {
assert.strictEqual(err.message, 'foo');
}));
throw new Error('foo');

View File

@ -0,0 +1,12 @@
// Flags: --abort-on-uncaught-exception
'use strict';
const common = require('../common');
const assert = require('assert');
assert.strictEqual(process.hasUncaughtExceptionCaptureCallback(), false);
// This should make the process not crash even though the flag was passed.
process.setUncaughtExceptionCaptureCallback(common.mustCall((err) => {
assert.strictEqual(err.message, 'foo');
}));
throw new Error('foo');

View File

@ -0,0 +1,13 @@
// Flags: --abort-on-uncaught-exception
'use strict';
const common = require('../common');
const assert = require('assert');
assert.strictEqual(process.hasUncaughtExceptionCaptureCallback(), false);
// This should make the process not crash even though the flag was passed.
process.setUncaughtExceptionCaptureCallback(common.mustCall((err) => {
assert.strictEqual(err.message, 'foo');
}));
process.on('uncaughtException', common.mustNotCall());
throw new Error('foo');