domains: fix handling of uncaught exceptions
Fix node exiting due to an exception being thrown rather than emitting an `'uncaughtException'` event on the process object when: 1. no error handler is set on the domain within which an error is thrown 2. an `'uncaughtException'` event listener is set on the process Also fix an issue where the process would not abort in the proper function call if an error is thrown within a domain with no error handler and `--abort-on-uncaught-exception` is used. Finally, change the behavior of --abort-on-uncaught-exception so that, if the domain within which the error is thrown has no error handler, but a domain further up the domains stack has one, the process will not abort. Fixes #3607 and #3653. PR: #3654 PR-URL: https://github.com/nodejs/node/pull/3654 Reviewed-By: Chris Dickinson <chris@neversaw.us>
This commit is contained in:
parent
1a21a5368b
commit
425a3545d2
@ -27,8 +27,13 @@ Object.defineProperty(process, 'domain', {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// It's possible to enter one domain while already inside
|
||||||
|
// another one. The stack is each entered domain.
|
||||||
|
const stack = [];
|
||||||
|
exports._stack = stack;
|
||||||
|
|
||||||
// let the process know we're using domains
|
// let the process know we're using domains
|
||||||
const _domain_flag = process._setupDomainUse(_domain);
|
const _domain_flag = process._setupDomainUse(_domain, stack);
|
||||||
|
|
||||||
exports.Domain = Domain;
|
exports.Domain = Domain;
|
||||||
|
|
||||||
@ -36,10 +41,6 @@ exports.create = exports.createDomain = function() {
|
|||||||
return new Domain();
|
return new Domain();
|
||||||
};
|
};
|
||||||
|
|
||||||
// it's possible to enter one domain while already inside
|
|
||||||
// another one. the stack is each entered domain.
|
|
||||||
var stack = [];
|
|
||||||
exports._stack = stack;
|
|
||||||
// the active domain is always the one that we're currently in.
|
// the active domain is always the one that we're currently in.
|
||||||
exports.active = null;
|
exports.active = null;
|
||||||
|
|
||||||
@ -96,6 +97,11 @@ Domain.prototype._errorHandler = function errorHandler(er) {
|
|||||||
// that these exceptions are caught, and thus would prevent it from
|
// that these exceptions are caught, and thus would prevent it from
|
||||||
// aborting in these cases.
|
// aborting in these cases.
|
||||||
if (stack.length === 1) {
|
if (stack.length === 1) {
|
||||||
|
// If there's no error handler, do not emit an 'error' event
|
||||||
|
// as this would throw an error, make the process exit, and thus
|
||||||
|
// prevent the process 'uncaughtException' event from being emitted
|
||||||
|
// if a listener is set.
|
||||||
|
if (EventEmitter.listenerCount(self, 'error') > 0) {
|
||||||
try {
|
try {
|
||||||
// Set the _emittingTopLevelDomainError so that we know that, even
|
// Set the _emittingTopLevelDomainError so that we know that, even
|
||||||
// if technically the top-level domain is still active, it would
|
// if technically the top-level domain is still active, it would
|
||||||
@ -105,6 +111,7 @@ Domain.prototype._errorHandler = function errorHandler(er) {
|
|||||||
} finally {
|
} finally {
|
||||||
process._emittingTopLevelDomainError = false;
|
process._emittingTopLevelDomainError = false;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
// wrap this in a try/catch so we don't get infinite throwing
|
// wrap this in a try/catch so we don't get infinite throwing
|
||||||
try {
|
try {
|
||||||
|
@ -242,6 +242,7 @@ namespace node {
|
|||||||
V(buffer_prototype_object, v8::Object) \
|
V(buffer_prototype_object, v8::Object) \
|
||||||
V(context, v8::Context) \
|
V(context, v8::Context) \
|
||||||
V(domain_array, v8::Array) \
|
V(domain_array, v8::Array) \
|
||||||
|
V(domains_stack_array, v8::Array) \
|
||||||
V(fs_stats_constructor_function, v8::Function) \
|
V(fs_stats_constructor_function, v8::Function) \
|
||||||
V(generic_internal_field_template, v8::ObjectTemplate) \
|
V(generic_internal_field_template, v8::ObjectTemplate) \
|
||||||
V(jsstream_constructor_template, v8::FunctionTemplate) \
|
V(jsstream_constructor_template, v8::FunctionTemplate) \
|
||||||
|
48
src/node.cc
48
src/node.cc
@ -951,17 +951,50 @@ void* ArrayBufferAllocator::Allocate(size_t size) {
|
|||||||
return malloc(size);
|
return malloc(size);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool DomainsStackHasErrorHandler(const Environment* env) {
|
||||||
|
HandleScope scope(env->isolate());
|
||||||
|
|
||||||
static bool IsDomainActive(const Environment* env) {
|
|
||||||
if (!env->using_domains())
|
if (!env->using_domains())
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
Local<Array> domain_array = env->domain_array().As<Array>();
|
Local<Array> domains_stack_array = env->domains_stack_array().As<Array>();
|
||||||
if (domain_array->Length() == 0)
|
if (domains_stack_array->Length() == 0)
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
Local<Value> domain_v = domain_array->Get(0);
|
uint32_t domains_stack_length = domains_stack_array->Length();
|
||||||
return !domain_v->IsNull();
|
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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -975,7 +1008,7 @@ static bool ShouldAbortOnUncaughtException(Isolate* isolate) {
|
|||||||
bool isEmittingTopLevelDomainError =
|
bool isEmittingTopLevelDomainError =
|
||||||
process_object->Get(emitting_top_level_domain_error_key)->BooleanValue();
|
process_object->Get(emitting_top_level_domain_error_key)->BooleanValue();
|
||||||
|
|
||||||
return !IsDomainActive(env) || isEmittingTopLevelDomainError;
|
return isEmittingTopLevelDomainError || !DomainsStackHasErrorHandler(env);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -1004,6 +1037,9 @@ void SetupDomainUse(const FunctionCallbackInfo<Value>& args) {
|
|||||||
CHECK(args[0]->IsArray());
|
CHECK(args[0]->IsArray());
|
||||||
env->set_domain_array(args[0].As<Array>());
|
env->set_domain_array(args[0].As<Array>());
|
||||||
|
|
||||||
|
CHECK(args[1]->IsArray());
|
||||||
|
env->set_domains_stack_array(args[1].As<Array>());
|
||||||
|
|
||||||
// Do a little housekeeping.
|
// Do a little housekeeping.
|
||||||
env->process_object()->Delete(
|
env->process_object()->Delete(
|
||||||
FIXED_ONE_BYTE_STRING(args.GetIsolate(), "_setupDomainUse"));
|
FIXED_ONE_BYTE_STRING(args.GetIsolate(), "_setupDomainUse"));
|
||||||
|
@ -471,3 +471,37 @@ ArrayStream.prototype.writable = true;
|
|||||||
ArrayStream.prototype.pause = function() {};
|
ArrayStream.prototype.pause = function() {};
|
||||||
ArrayStream.prototype.resume = function() {};
|
ArrayStream.prototype.resume = function() {};
|
||||||
ArrayStream.prototype.write = function() {};
|
ArrayStream.prototype.write = function() {};
|
||||||
|
|
||||||
|
// Returns true if the exit code "exitCode" and/or signal name "signal"
|
||||||
|
// represent the exit code and/or signal name of a node process that aborted,
|
||||||
|
// false otherwise.
|
||||||
|
exports.nodeProcessAborted = function nodeProcessAborted(exitCode, signal) {
|
||||||
|
// Depending on the compiler used, node will exit with either
|
||||||
|
// exit code 132 (SIGILL), 133 (SIGTRAP) or 134 (SIGABRT).
|
||||||
|
var expectedExitCodes = [132, 133, 134];
|
||||||
|
|
||||||
|
// On platforms using KSH as the default shell (like SmartOS),
|
||||||
|
// when a process aborts, KSH exits with an exit code that is
|
||||||
|
// greater than 256, and thus the exit code emitted with the 'exit'
|
||||||
|
// event is null and the signal is set to either SIGILL, SIGTRAP,
|
||||||
|
// or SIGABRT (depending on the compiler).
|
||||||
|
const expectedSignals = ['SIGILL', 'SIGTRAP', 'SIGABRT'];
|
||||||
|
|
||||||
|
// On Windows, v8's base::OS::Abort triggers an access violation,
|
||||||
|
// which corresponds to exit code 3221225477 (0xC0000005)
|
||||||
|
if (process.platform === 'win32')
|
||||||
|
expectedExitCodes = [3221225477];
|
||||||
|
|
||||||
|
// When using --abort-on-uncaught-exception, V8 will use
|
||||||
|
// base::OS::Abort to terminate the process.
|
||||||
|
// Depending on the compiler used, the shell or other aspects of
|
||||||
|
// the platform used to build the node binary, this will actually
|
||||||
|
// make V8 exit by aborting or by raising a signal. In any case,
|
||||||
|
// one of them (exit code or signal) needs to be set to one of
|
||||||
|
// the expected exit codes or signals.
|
||||||
|
if (signal !== null) {
|
||||||
|
return expectedSignals.indexOf(signal) > -1;
|
||||||
|
} else {
|
||||||
|
return expectedExitCodes.indexOf(exitCode) > -1;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
@ -1,58 +1,66 @@
|
|||||||
'use strict';
|
'use strict';
|
||||||
// Flags: --abort_on_uncaught_exception
|
|
||||||
|
|
||||||
var common = require('../common');
|
// This test makes sure that when using --abort-on-uncaught-exception and
|
||||||
var assert = require('assert');
|
// when throwing an error from within a domain that has an error handler
|
||||||
var domain = require('domain');
|
// setup, the process _does not_ abort.
|
||||||
|
|
||||||
var tests = [
|
const common = require('../common');
|
||||||
nextTick,
|
const assert = require('assert');
|
||||||
timer,
|
const domain = require('domain');
|
||||||
timerPlusNextTick,
|
const child_process = require('child_process');
|
||||||
netServer,
|
|
||||||
firstRun,
|
|
||||||
];
|
|
||||||
|
|
||||||
var errors = 0;
|
var errorHandlerCalled = false;
|
||||||
|
|
||||||
process.on('exit', function() {
|
|
||||||
assert.equal(errors, tests.length);
|
|
||||||
});
|
|
||||||
|
|
||||||
tests.forEach(function(test) { test(); });
|
|
||||||
|
|
||||||
|
const tests = [
|
||||||
function nextTick() {
|
function nextTick() {
|
||||||
var d = domain.create();
|
const d = domain.create();
|
||||||
|
|
||||||
d.once('error', function(err) {
|
d.once('error', function(err) {
|
||||||
errors += 1;
|
errorHandlerCalled = true;
|
||||||
});
|
});
|
||||||
|
|
||||||
d.run(function() {
|
d.run(function() {
|
||||||
process.nextTick(function() {
|
process.nextTick(function() {
|
||||||
throw new Error('exceptional!');
|
throw new Error('exceptional!');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
},
|
||||||
|
|
||||||
function timer() {
|
function timer() {
|
||||||
var d = domain.create();
|
const d = domain.create();
|
||||||
|
|
||||||
d.on('error', function(err) {
|
d.on('error', function(err) {
|
||||||
errors += 1;
|
errorHandlerCalled = true;
|
||||||
});
|
});
|
||||||
|
|
||||||
d.run(function() {
|
d.run(function() {
|
||||||
setTimeout(function() {
|
setTimeout(function() {
|
||||||
throw new Error('exceptional!');
|
throw new Error('exceptional!');
|
||||||
}, 33);
|
}, 33);
|
||||||
});
|
});
|
||||||
}
|
},
|
||||||
|
|
||||||
|
function immediate() {
|
||||||
|
const d = domain.create();
|
||||||
|
|
||||||
|
d.on('error', function errorHandler() {
|
||||||
|
errorHandlerCalled = true;
|
||||||
|
});
|
||||||
|
|
||||||
|
d.run(function() {
|
||||||
|
setImmediate(function() {
|
||||||
|
throw new Error('boom!');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
function timerPlusNextTick() {
|
function timerPlusNextTick() {
|
||||||
var d = domain.create();
|
const d = domain.create();
|
||||||
|
|
||||||
d.on('error', function(err) {
|
d.on('error', function(err) {
|
||||||
errors += 1;
|
errorHandlerCalled = true;
|
||||||
});
|
});
|
||||||
|
|
||||||
d.run(function() {
|
d.run(function() {
|
||||||
setTimeout(function() {
|
setTimeout(function() {
|
||||||
process.nextTick(function() {
|
process.nextTick(function() {
|
||||||
@ -60,32 +68,49 @@ function timerPlusNextTick() {
|
|||||||
});
|
});
|
||||||
}, 33);
|
}, 33);
|
||||||
});
|
});
|
||||||
}
|
},
|
||||||
|
|
||||||
function firstRun() {
|
function firstRun() {
|
||||||
var d = domain.create();
|
const d = domain.create();
|
||||||
|
|
||||||
d.on('error', function(err) {
|
d.on('error', function(err) {
|
||||||
errors += 1;
|
errorHandlerCalled = true;
|
||||||
});
|
});
|
||||||
|
|
||||||
d.run(function() {
|
d.run(function() {
|
||||||
throw new Error('exceptional!');
|
throw new Error('exceptional!');
|
||||||
});
|
});
|
||||||
}
|
},
|
||||||
|
|
||||||
|
function fsAsync() {
|
||||||
|
const d = domain.create();
|
||||||
|
|
||||||
|
d.on('error', function errorHandler() {
|
||||||
|
errorHandlerCalled = true;
|
||||||
|
});
|
||||||
|
|
||||||
|
d.run(function() {
|
||||||
|
var fs = require('fs');
|
||||||
|
fs.exists('/non/existing/file', function onExists(exists) {
|
||||||
|
throw new Error('boom!');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
function netServer() {
|
function netServer() {
|
||||||
var net = require('net');
|
const net = require('net');
|
||||||
var d = domain.create();
|
const d = domain.create();
|
||||||
|
|
||||||
d.on('error', function(err) {
|
d.on('error', function(err) {
|
||||||
errors += 1;
|
errorHandlerCalled = true;
|
||||||
});
|
});
|
||||||
|
|
||||||
d.run(function() {
|
d.run(function() {
|
||||||
var server = net.createServer(function(conn) {
|
const server = net.createServer(function(conn) {
|
||||||
conn.pipe(conn);
|
conn.pipe(conn);
|
||||||
});
|
});
|
||||||
server.listen(common.PORT, '0.0.0.0', function() {
|
server.listen(common.PORT, common.localhostIPv4, function() {
|
||||||
var conn = net.connect(common.PORT, '0.0.0.0');
|
const conn = net.connect(common.PORT, common.localhostIPv4);
|
||||||
conn.once('data', function() {
|
conn.once('data', function() {
|
||||||
throw new Error('ok');
|
throw new Error('ok');
|
||||||
});
|
});
|
||||||
@ -93,4 +118,139 @@ function netServer() {
|
|||||||
server.close();
|
server.close();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
function firstRunOnlyTopLevelErrorHandler() {
|
||||||
|
const d = domain.create();
|
||||||
|
const d2 = domain.create();
|
||||||
|
|
||||||
|
d.on('error', function errorHandler() {
|
||||||
|
errorHandlerCalled = true;
|
||||||
|
});
|
||||||
|
|
||||||
|
d.run(function() {
|
||||||
|
d2.run(function() {
|
||||||
|
throw new Error('boom!');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
function firstRunNestedWithErrorHandler() {
|
||||||
|
const d = domain.create();
|
||||||
|
const d2 = domain.create();
|
||||||
|
|
||||||
|
d2.on('error', function errorHandler() {
|
||||||
|
errorHandlerCalled = true;
|
||||||
|
});
|
||||||
|
|
||||||
|
d.run(function() {
|
||||||
|
d2.run(function() {
|
||||||
|
throw new Error('boom!');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
function timeoutNestedWithErrorHandler() {
|
||||||
|
const d = domain.create();
|
||||||
|
const d2 = domain.create();
|
||||||
|
|
||||||
|
d2.on('error', function errorHandler() {
|
||||||
|
errorHandlerCalled = true;
|
||||||
|
});
|
||||||
|
|
||||||
|
d.run(function() {
|
||||||
|
d2.run(function() {
|
||||||
|
setTimeout(function() {
|
||||||
|
console.log('foo');
|
||||||
|
throw new Error('boom!');
|
||||||
|
}, 33);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
function setImmediateNestedWithErrorHandler() {
|
||||||
|
const d = domain.create();
|
||||||
|
const d2 = domain.create();
|
||||||
|
|
||||||
|
d2.on('error', function errorHandler() {
|
||||||
|
errorHandlerCalled = true;
|
||||||
|
});
|
||||||
|
|
||||||
|
d.run(function() {
|
||||||
|
d2.run(function() {
|
||||||
|
setImmediate(function() {
|
||||||
|
throw new Error('boom!');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
function nextTickNestedWithErrorHandler() {
|
||||||
|
const d = domain.create();
|
||||||
|
const d2 = domain.create();
|
||||||
|
|
||||||
|
d2.on('error', function errorHandler() {
|
||||||
|
errorHandlerCalled = true;
|
||||||
|
});
|
||||||
|
|
||||||
|
d.run(function() {
|
||||||
|
d2.run(function() {
|
||||||
|
process.nextTick(function() {
|
||||||
|
throw new Error('boom!');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
function fsAsyncNestedWithErrorHandler() {
|
||||||
|
const d = domain.create();
|
||||||
|
const d2 = domain.create();
|
||||||
|
|
||||||
|
d2.on('error', function errorHandler() {
|
||||||
|
errorHandlerCalled = true;
|
||||||
|
});
|
||||||
|
|
||||||
|
d.run(function() {
|
||||||
|
d2.run(function() {
|
||||||
|
var fs = require('fs');
|
||||||
|
fs.exists('/non/existing/file', function onExists(exists) {
|
||||||
|
throw new Error('boom!');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
if (process.argv[2] === 'child') {
|
||||||
|
const testIndex = +process.argv[3];
|
||||||
|
|
||||||
|
tests[testIndex]();
|
||||||
|
|
||||||
|
process.on('exit', function onExit() {
|
||||||
|
assert.equal(errorHandlerCalled, true);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
|
||||||
|
tests.forEach(function(test, testIndex) {
|
||||||
|
var testCmd = '';
|
||||||
|
if (process.platform !== 'win32') {
|
||||||
|
// Do not create core files, as it can take a lot of disk space on
|
||||||
|
// continuous testing and developers' machines
|
||||||
|
testCmd += 'ulimit -c 0 && ';
|
||||||
|
}
|
||||||
|
|
||||||
|
testCmd += process.argv[0];
|
||||||
|
testCmd += ' ' + '--abort-on-uncaught-exception';
|
||||||
|
testCmd += ' ' + process.argv[1];
|
||||||
|
testCmd += ' ' + 'child';
|
||||||
|
testCmd += ' ' + testIndex;
|
||||||
|
|
||||||
|
var child = child_process.exec(testCmd);
|
||||||
|
|
||||||
|
child.on('exit', function onExit(code, signal) {
|
||||||
|
assert.equal(code, 0, 'Test at index ' + testIndex +
|
||||||
|
' should have exited with exit code 0 but instead exited with code ' +
|
||||||
|
code + ' and signal ' + signal);
|
||||||
|
});
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
168
test/parallel/test-domain-no-error-handler-abort-on-uncaught.js
Normal file
168
test/parallel/test-domain-no-error-handler-abort-on-uncaught.js
Normal file
@ -0,0 +1,168 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
/*
|
||||||
|
* This test makes sure that when using --abort-on-uncaught-exception and
|
||||||
|
* when throwing an error from within a domain that does not have an error
|
||||||
|
* handler setup, the process aborts.
|
||||||
|
*/
|
||||||
|
const common = require('../common');
|
||||||
|
const assert = require('assert');
|
||||||
|
const domain = require('domain');
|
||||||
|
const child_process = require('child_process');
|
||||||
|
|
||||||
|
const tests = [
|
||||||
|
function() {
|
||||||
|
const d = domain.create();
|
||||||
|
|
||||||
|
d.run(function() {
|
||||||
|
throw new Error('boom!');
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
function() {
|
||||||
|
const d = domain.create();
|
||||||
|
const d2 = domain.create();
|
||||||
|
|
||||||
|
d.run(function() {
|
||||||
|
d2.run(function() {
|
||||||
|
throw new Error('boom!');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
function() {
|
||||||
|
const d = domain.create();
|
||||||
|
|
||||||
|
d.run(function() {
|
||||||
|
setTimeout(function() {
|
||||||
|
throw new Error('boom!');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
function() {
|
||||||
|
const d = domain.create();
|
||||||
|
|
||||||
|
d.run(function() {
|
||||||
|
setImmediate(function() {
|
||||||
|
throw new Error('boom!');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
function() {
|
||||||
|
const d = domain.create();
|
||||||
|
|
||||||
|
d.run(function() {
|
||||||
|
process.nextTick(function() {
|
||||||
|
throw new Error('boom!');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
function() {
|
||||||
|
const d = domain.create();
|
||||||
|
|
||||||
|
d.run(function() {
|
||||||
|
var fs = require('fs');
|
||||||
|
fs.exists('/non/existing/file', function onExists(exists) {
|
||||||
|
throw new Error('boom!');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
function() {
|
||||||
|
const d = domain.create();
|
||||||
|
const d2 = domain.create();
|
||||||
|
|
||||||
|
d.on('error', function errorHandler() {
|
||||||
|
});
|
||||||
|
|
||||||
|
d.run(function() {
|
||||||
|
d2.run(function() {
|
||||||
|
setTimeout(function() {
|
||||||
|
throw new Error('boom!');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
function() {
|
||||||
|
const d = domain.create();
|
||||||
|
const d2 = domain.create();
|
||||||
|
|
||||||
|
d.on('error', function errorHandler() {
|
||||||
|
});
|
||||||
|
|
||||||
|
d.run(function() {
|
||||||
|
d2.run(function() {
|
||||||
|
setImmediate(function() {
|
||||||
|
throw new Error('boom!');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
function() {
|
||||||
|
const d = domain.create();
|
||||||
|
const d2 = domain.create();
|
||||||
|
|
||||||
|
d.on('error', function errorHandler() {
|
||||||
|
});
|
||||||
|
|
||||||
|
d.run(function() {
|
||||||
|
d2.run(function() {
|
||||||
|
process.nextTick(function() {
|
||||||
|
throw new Error('boom!');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
function() {
|
||||||
|
const d = domain.create();
|
||||||
|
const d2 = domain.create();
|
||||||
|
|
||||||
|
d.on('error', function errorHandler() {
|
||||||
|
});
|
||||||
|
|
||||||
|
d.run(function() {
|
||||||
|
d2.run(function() {
|
||||||
|
var fs = require('fs');
|
||||||
|
fs.exists('/non/existing/file', function onExists(exists) {
|
||||||
|
throw new Error('boom!');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
if (process.argv[2] === 'child') {
|
||||||
|
const testIndex = +process.argv[3];
|
||||||
|
tests[testIndex]();
|
||||||
|
} else {
|
||||||
|
|
||||||
|
tests.forEach(function(test, testIndex) {
|
||||||
|
var testCmd = '';
|
||||||
|
if (process.platform !== 'win32') {
|
||||||
|
// Do not create core files, as it can take a lot of disk space on
|
||||||
|
// continuous testing and developers' machines
|
||||||
|
testCmd += 'ulimit -c 0 && ';
|
||||||
|
}
|
||||||
|
|
||||||
|
testCmd += process.argv[0];
|
||||||
|
testCmd += ' ' + '--abort-on-uncaught-exception';
|
||||||
|
testCmd += ' ' + process.argv[1];
|
||||||
|
testCmd += ' ' + 'child';
|
||||||
|
testCmd += ' ' + testIndex;
|
||||||
|
|
||||||
|
var child = child_process.exec(testCmd);
|
||||||
|
|
||||||
|
child.on('exit', function onExit(exitCode, signal) {
|
||||||
|
const errMsg = 'Test at index ' + testIndex + ' should have aborted ' +
|
||||||
|
'but instead exited with exit code ' + exitCode + ' and signal ' +
|
||||||
|
signal;
|
||||||
|
assert(common.nodeProcessAborted(exitCode, signal), errMsg);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
@ -0,0 +1,101 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
// This test makes sure that when throwing an error from a domain, and then
|
||||||
|
// handling that error in an uncaughtException handler by throwing an error
|
||||||
|
// again, the exit code, signal and error messages are the ones we expect with
|
||||||
|
// and without using --abort-on-uncaught-exception.
|
||||||
|
|
||||||
|
const common = require('../common');
|
||||||
|
const assert = require('assert');
|
||||||
|
const child_process = require('child_process');
|
||||||
|
const domain = require('domain');
|
||||||
|
|
||||||
|
const uncaughtExceptionHandlerErrMsg = 'boom from uncaughtException handler';
|
||||||
|
const domainErrMsg = 'boom from domain';
|
||||||
|
|
||||||
|
const RAN_UNCAUGHT_EXCEPTION_HANDLER_EXIT_CODE = 42;
|
||||||
|
|
||||||
|
if (process.argv[2] === 'child') {
|
||||||
|
process.on('uncaughtException', common.mustCall(function onUncaught() {
|
||||||
|
if (process.execArgv.indexOf('--abort-on-uncaught-exception') !== -1) {
|
||||||
|
// When passing --abort-on-uncaught-exception to the child process,
|
||||||
|
// we want to make sure that this handler (the process' uncaughtException
|
||||||
|
// event handler) wasn't called. Unfortunately we can't parse the child
|
||||||
|
// process' output to do that, since on Windows the standard error output
|
||||||
|
// is not properly flushed in V8's Isolate::Throw right before the
|
||||||
|
// process aborts due to an uncaught exception, and thus the error
|
||||||
|
// message representing the error that was thrown cannot be read by the
|
||||||
|
// parent process. So instead of parsing the child process' stdandard
|
||||||
|
// error, the parent process will check that in the case
|
||||||
|
// --abort-on-uncaught-exception was passed, the process did not exit
|
||||||
|
// with exit code RAN_UNCAUGHT_EXCEPTION_HANDLER_EXIT_CODE.
|
||||||
|
process.exit(RAN_UNCAUGHT_EXCEPTION_HANDLER_EXIT_CODE);
|
||||||
|
} else {
|
||||||
|
// On the other hand, when not passing --abort-on-uncaught-exception to
|
||||||
|
// the node process, we want to throw in this event handler to make sure
|
||||||
|
// that the proper error message, exit code and signal are the ones we
|
||||||
|
// expect.
|
||||||
|
throw new Error(uncaughtExceptionHandlerErrMsg);
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
|
||||||
|
const d = domain.create();
|
||||||
|
d.run(common.mustCall(function() {
|
||||||
|
throw new Error(domainErrMsg);
|
||||||
|
}));
|
||||||
|
} else {
|
||||||
|
runTestWithoutAbortOnUncaughtException();
|
||||||
|
runTestWithAbortOnUncaughtException();
|
||||||
|
}
|
||||||
|
|
||||||
|
function runTestWithoutAbortOnUncaughtException() {
|
||||||
|
child_process.exec(createTestCmdLine(),
|
||||||
|
function onTestDone(err, stdout, stderr) {
|
||||||
|
// When _not_ passing --abort-on-uncaught-exception, the process'
|
||||||
|
// uncaughtException handler _must_ be called, and thus the error
|
||||||
|
// message must include only the message of the error thrown from the
|
||||||
|
// process' uncaughtException handler.
|
||||||
|
assert(stderr.includes(uncaughtExceptionHandlerErrMsg),
|
||||||
|
'stderr output must include proper uncaughtException handler\'s ' +
|
||||||
|
'error\'s message');
|
||||||
|
assert(!stderr.includes(domainErrMsg), 'stderr output must not ' +
|
||||||
|
'include domain\'s error\'s message');
|
||||||
|
|
||||||
|
assert.notEqual(err.code, 0,
|
||||||
|
'child process should have exited with a non-zero exit code, ' +
|
||||||
|
'but did not');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function runTestWithAbortOnUncaughtException() {
|
||||||
|
child_process.exec(createTestCmdLine({
|
||||||
|
withAbortOnUncaughtException: true
|
||||||
|
}), function onTestDone(err, stdout, stderr) {
|
||||||
|
assert.notEqual(err.code, RAN_UNCAUGHT_EXCEPTION_HANDLER_EXIT_CODE,
|
||||||
|
'child process should not have run its uncaughtException event ' +
|
||||||
|
'handler');
|
||||||
|
assert(common.nodeProcessAborted(err.code, err.signal),
|
||||||
|
'process should have aborted, but did not');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function createTestCmdLine(options) {
|
||||||
|
var testCmd = '';
|
||||||
|
|
||||||
|
if (process.platform !== 'win32') {
|
||||||
|
// Do not create core files, as it can take a lot of disk space on
|
||||||
|
// continuous testing and developers' machines
|
||||||
|
testCmd += 'ulimit -c 0 && ';
|
||||||
|
}
|
||||||
|
|
||||||
|
testCmd += process.argv[0];
|
||||||
|
|
||||||
|
if (options && options.withAbortOnUncaughtException) {
|
||||||
|
testCmd += ' ' + '--abort-on-uncaught-exception';
|
||||||
|
}
|
||||||
|
|
||||||
|
testCmd += ' ' + process.argv[1];
|
||||||
|
testCmd += ' ' + 'child';
|
||||||
|
|
||||||
|
return testCmd;
|
||||||
|
}
|
205
test/parallel/test-domain-uncaught-exception.js
Normal file
205
test/parallel/test-domain-uncaught-exception.js
Normal file
@ -0,0 +1,205 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
/*
|
||||||
|
* The goal of this test is to make sure that errors thrown within domains
|
||||||
|
* are handled correctly. It checks that the process' 'uncaughtException' event
|
||||||
|
* is emitted when appropriate, and not emitted when it shouldn't. It also
|
||||||
|
* checks that the proper domain error handlers are called when they should
|
||||||
|
* be called, and not called when they shouldn't.
|
||||||
|
*/
|
||||||
|
|
||||||
|
const common = require('../common');
|
||||||
|
const assert = require('assert');
|
||||||
|
const domain = require('domain');
|
||||||
|
const child_process = require('child_process');
|
||||||
|
|
||||||
|
const uncaughtExceptions = {};
|
||||||
|
|
||||||
|
const tests = [];
|
||||||
|
|
||||||
|
function test1() {
|
||||||
|
/*
|
||||||
|
* Throwing from an async callback from within a domain that doesn't have
|
||||||
|
* an error handler must result in emitting the process' uncaughtException
|
||||||
|
* event.
|
||||||
|
*/
|
||||||
|
const d = domain.create();
|
||||||
|
d.run(function() {
|
||||||
|
setTimeout(function onTimeout() {
|
||||||
|
throw new Error('boom!');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
tests.push({
|
||||||
|
fn: test1,
|
||||||
|
expectedMessages: ['uncaughtException']
|
||||||
|
});
|
||||||
|
|
||||||
|
function test2() {
|
||||||
|
/*
|
||||||
|
* Throwing from from within a domain that doesn't have an error handler must
|
||||||
|
* result in emitting the process' uncaughtException event.
|
||||||
|
*/
|
||||||
|
const d2 = domain.create();
|
||||||
|
d2.run(function() {
|
||||||
|
throw new Error('boom!');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
tests.push({
|
||||||
|
fn: test2,
|
||||||
|
expectedMessages: ['uncaughtException']
|
||||||
|
});
|
||||||
|
|
||||||
|
function test3() {
|
||||||
|
/*
|
||||||
|
* This test creates two nested domains: d3 and d4. d4 doesn't register an
|
||||||
|
* error handler, but d3 does. The error is handled by the d3 domain and thus
|
||||||
|
* an 'uncaughtException' event should _not_ be emitted.
|
||||||
|
*/
|
||||||
|
const d3 = domain.create();
|
||||||
|
const d4 = domain.create();
|
||||||
|
|
||||||
|
d3.on('error', function onErrorInD3Domain(err) {
|
||||||
|
process.send('errorHandledByDomain');
|
||||||
|
});
|
||||||
|
|
||||||
|
d3.run(function() {
|
||||||
|
d4.run(function() {
|
||||||
|
throw new Error('boom!');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
tests.push({
|
||||||
|
fn: test3,
|
||||||
|
expectedMessages: ['errorHandledByDomain']
|
||||||
|
});
|
||||||
|
|
||||||
|
function test4() {
|
||||||
|
/*
|
||||||
|
* This test creates two nested domains: d5 and d6. d6 doesn't register an
|
||||||
|
* error handler. When the timer's callback is called, because async
|
||||||
|
* operations like timer callbacks are bound to the domain that was active
|
||||||
|
* at the time of their creation, and because both d5 and d6 domains have
|
||||||
|
* exited by the time the timer's callback is called, its callback runs with
|
||||||
|
* only d6 on the domains stack. Since d6 doesn't register an error handler,
|
||||||
|
* the process' uncaughtException event should be emitted.
|
||||||
|
*/
|
||||||
|
const d5 = domain.create();
|
||||||
|
const d6 = domain.create();
|
||||||
|
|
||||||
|
d5.on('error', function onErrorInD2Domain(err) {
|
||||||
|
process.send('errorHandledByDomain');
|
||||||
|
});
|
||||||
|
|
||||||
|
d5.run(function() {
|
||||||
|
d6.run(function() {
|
||||||
|
setTimeout(function onTimeout() {
|
||||||
|
throw new Error('boom!');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
tests.push({
|
||||||
|
fn: test4,
|
||||||
|
expectedMessages: ['uncaughtException']
|
||||||
|
});
|
||||||
|
|
||||||
|
function test5() {
|
||||||
|
/*
|
||||||
|
* This test creates two nested domains: d7 and d8. d8 _does_ register an
|
||||||
|
* error handler, so throwing within that domain should not emit an uncaught
|
||||||
|
* exception.
|
||||||
|
*/
|
||||||
|
const d7 = domain.create();
|
||||||
|
const d8 = domain.create();
|
||||||
|
|
||||||
|
d8.on('error', function onErrorInD3Domain(err) {
|
||||||
|
process.send('errorHandledByDomain');
|
||||||
|
});
|
||||||
|
|
||||||
|
d7.run(function() {
|
||||||
|
d8.run(function() {
|
||||||
|
throw new Error('boom!');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
tests.push({
|
||||||
|
fn: test5,
|
||||||
|
expectedMessages: ['errorHandledByDomain']
|
||||||
|
});
|
||||||
|
|
||||||
|
function test6() {
|
||||||
|
/*
|
||||||
|
* This test creates two nested domains: d9 and d10. d10 _does_ register an
|
||||||
|
* error handler, so throwing within that domain in an async callback should
|
||||||
|
* _not_ emit an uncaught exception.
|
||||||
|
*/
|
||||||
|
const d9 = domain.create();
|
||||||
|
const d10 = domain.create();
|
||||||
|
|
||||||
|
d10.on('error', function onErrorInD2Domain(err) {
|
||||||
|
process.send('errorHandledByDomain');
|
||||||
|
});
|
||||||
|
|
||||||
|
d9.run(function() {
|
||||||
|
d10.run(function() {
|
||||||
|
setTimeout(function onTimeout() {
|
||||||
|
throw new Error('boom!');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
tests.push({
|
||||||
|
fn: test6,
|
||||||
|
expectedMessages: ['errorHandledByDomain']
|
||||||
|
});
|
||||||
|
|
||||||
|
if (process.argv[2] === 'child') {
|
||||||
|
const testIndex = process.argv[3];
|
||||||
|
process.on('uncaughtException', function onUncaughtException() {
|
||||||
|
process.send('uncaughtException');
|
||||||
|
});
|
||||||
|
|
||||||
|
tests[testIndex].fn();
|
||||||
|
} else {
|
||||||
|
// Run each test's function in a child process. Listen on
|
||||||
|
// messages sent by each child process and compare expected
|
||||||
|
// messages defined for each test with the actual received messages.
|
||||||
|
tests.forEach(function doTest(test, testIndex) {
|
||||||
|
const testProcess = child_process.fork(__filename, ['child', testIndex]);
|
||||||
|
|
||||||
|
testProcess.on('message', function onMsg(msg) {
|
||||||
|
if (test.messagesReceived === undefined)
|
||||||
|
test.messagesReceived = [];
|
||||||
|
|
||||||
|
test.messagesReceived.push(msg);
|
||||||
|
});
|
||||||
|
|
||||||
|
testProcess.on('disconnect', common.mustCall(function onExit() {
|
||||||
|
// Make sure that all expected messages were sent from the
|
||||||
|
// child process
|
||||||
|
test.expectedMessages.forEach(function(expectedMessage) {
|
||||||
|
if (test.messagesReceived === undefined ||
|
||||||
|
test.messagesReceived.indexOf(expectedMessage) === -1)
|
||||||
|
assert(false, 'test ' + test.fn.name +
|
||||||
|
' should have sent message: ' + expectedMessage +
|
||||||
|
' but didn\'t');
|
||||||
|
});
|
||||||
|
|
||||||
|
if (test.messagesReceived) {
|
||||||
|
test.messagesReceived.forEach(function(receivedMessage) {
|
||||||
|
if (test.expectedMessages.indexOf(receivedMessage) === -1) {
|
||||||
|
assert(false, 'test ' + test.fn.name +
|
||||||
|
' should not have sent message: ' + receivedMessage +
|
||||||
|
' but did');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
}
|
@ -131,38 +131,8 @@ if (process.argv[2] === 'child') {
|
|||||||
// outside of a try/catch block, the process should not exit gracefully
|
// outside of a try/catch block, the process should not exit gracefully
|
||||||
if (!options.useTryCatch && options.throwInDomainErrHandler) {
|
if (!options.useTryCatch && options.throwInDomainErrHandler) {
|
||||||
if (cmdLineOption === '--abort_on_uncaught_exception') {
|
if (cmdLineOption === '--abort_on_uncaught_exception') {
|
||||||
// If the top-level domain's error handler throws, and only if
|
assert(common.nodeProcessAborted(exitCode, signal),
|
||||||
// --abort_on_uncaught_exception is passed on the command line,
|
'process should have aborted, but did not');
|
||||||
// the process must abort.
|
|
||||||
//
|
|
||||||
// Depending on the compiler used, node will exit with either
|
|
||||||
// exit code 132 (SIGILL), 133 (SIGTRAP) or 134 (SIGABRT).
|
|
||||||
expectedExitCodes = [132, 133, 134];
|
|
||||||
|
|
||||||
// On platforms using KSH as the default shell (like SmartOS),
|
|
||||||
// when a process aborts, KSH exits with an exit code that is
|
|
||||||
// greater than 256, and thus the exit code emitted with the 'exit'
|
|
||||||
// event is null and the signal is set to either SIGILL, SIGTRAP,
|
|
||||||
// or SIGABRT (depending on the compiler).
|
|
||||||
expectedSignals = ['SIGILL', 'SIGTRAP', 'SIGABRT'];
|
|
||||||
|
|
||||||
// On Windows, v8's base::OS::Abort triggers an access violation,
|
|
||||||
// which corresponds to exit code 3221225477 (0xC0000005)
|
|
||||||
if (process.platform === 'win32')
|
|
||||||
expectedExitCodes = [3221225477];
|
|
||||||
|
|
||||||
// When using --abort-on-uncaught-exception, V8 will use
|
|
||||||
// base::OS::Abort to terminate the process.
|
|
||||||
// Depending on the compiler used, the shell or other aspects of
|
|
||||||
// the platform used to build the node binary, this will actually
|
|
||||||
// make V8 exit by aborting or by raising a signal. In any case,
|
|
||||||
// one of them (exit code or signal) needs to be set to one of
|
|
||||||
// the expected exit codes or signals.
|
|
||||||
if (signal !== null) {
|
|
||||||
assert.ok(expectedSignals.indexOf(signal) > -1);
|
|
||||||
} else {
|
|
||||||
assert.ok(expectedExitCodes.indexOf(exitCode) > -1);
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
// By default, uncaught exceptions make node exit with an exit
|
// By default, uncaught exceptions make node exit with an exit
|
||||||
// code of 7.
|
// code of 7.
|
||||||
|
Loading…
x
Reference in New Issue
Block a user