diff --git a/lib/domain.js b/lib/domain.js index afaa922ebe5919..7edb6012ccd86d 100644 --- a/lib/domain.js +++ b/lib/domain.js @@ -32,8 +32,13 @@ var endMethods = ['end', 'abort', 'destroy', 'destroySoon']; // a few side effects. events.usingDomains = true; +// it's possible to enter one domain while already inside +// another one. the stack is each entered domain. +var stack = []; +exports._stack = stack; + // let the process know we're using domains -process._usingDomains(); +process._usingDomains(stack); exports.Domain = Domain; @@ -41,10 +46,6 @@ exports.create = exports.createDomain = function(cb) { return new Domain(cb); }; -// 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. exports.active = null; diff --git a/src/node.cc b/src/node.cc index cc67927cfd7a34..ecb85cad952dd6 100644 --- a/src/node.cc +++ b/src/node.cc @@ -106,6 +106,8 @@ Persistent domain_symbol; // declared in node_internals.h Persistent process; +static Persistent domains_stack; + static Persistent process_tickFromSpinner; static Persistent process_tickCallback; @@ -127,6 +129,8 @@ static Persistent exit_symbol; static Persistent disposed_symbol; static Persistent emitting_toplevel_domain_error_symbol; +static Persistent _events_symbol; +static Persistent error_symbol; static bool print_eval = false; static bool force_repl = false; @@ -905,25 +909,77 @@ Handle FromConstructorTemplate(Persistent t, return scope.Close(t->GetFunction()->NewInstance(argc, argv)); } -static bool IsDomainActive() { - if (domain_symbol.IsEmpty()) - domain_symbol = NODE_PSYMBOL("domain"); +static bool DomainHasErrorHandler(const Local& domain) { + HandleScope scope; + + if (_events_symbol.IsEmpty()) + _events_symbol = NODE_PSYMBOL("_events"); + + Local domain_event_listeners_v = domain->Get(_events_symbol); + if (!domain_event_listeners_v->IsObject()) + return false; + + Local domain_event_listeners_o = + domain_event_listeners_v.As(); + + if (error_symbol.IsEmpty()) + error_symbol = NODE_PSYMBOL("error"); + + Local domain_error_listeners_v = + domain_event_listeners_o->Get(error_symbol); + + if (domain_error_listeners_v->IsFunction() || + (domain_error_listeners_v->IsArray() && + domain_error_listeners_v.As()->Length() > 0)) + return true; + + return false; +} + +static bool TopDomainHasErrorHandler() { + HandleScope scope; + + if (!using_domains) + return false; - Local domain_v = process->Get(domain_symbol); + Local domains_stack_array = Local::New(domains_stack); + if (domains_stack_array->Length() == 0) + return false; - return domain_v->IsObject(); + uint32_t domains_stack_length = domains_stack_array->Length(); + if (domains_stack_length == 0) + return false; + + Local domain_v = domains_stack_array->Get(domains_stack_length - 1); + if (!domain_v->IsObject()) + return false; + + Local domain = domain_v.As(); + if (DomainHasErrorHandler(domain)) + return true; + + return false; } bool ShouldAbortOnUncaughtException() { Local emitting_toplevel_domain_error_v = process->Get(emitting_toplevel_domain_error_symbol); - return !IsDomainActive() || emitting_toplevel_domain_error_v->BooleanValue(); + + return emitting_toplevel_domain_error_v->BooleanValue() || + !TopDomainHasErrorHandler(); } Handle UsingDomains(const Arguments& args) { HandleScope scope; + if (using_domains) return scope.Close(Undefined()); + + if (!args[0]->IsArray()) { + fprintf(stderr, "domains stack must be an array\n"); + abort(); + } + using_domains = true; Local tdc_v = process->Get(String::New("_tickDomainCallback")); Local ndt_v = process->Get(String::New("_nextDomainTick")); @@ -935,6 +991,9 @@ Handle UsingDomains(const Arguments& args) { fprintf(stderr, "process._nextDomainTick assigned to non-function\n"); abort(); } + + domains_stack = Persistent::New(args[0].As()); + Local tdc = tdc_v.As(); Local ndt = ndt_v.As(); process->Set(String::New("_tickCallback"), tdc); @@ -2460,7 +2519,10 @@ Handle SetupProcessObject(int argc, char *argv[]) { process->Set(String::NewSymbol("_tickInfoBox"), info_box); // pre-set _events object for faster emit checks - process->Set(String::NewSymbol("_events"), Object::New()); + if (_events_symbol.IsEmpty()) + _events_symbol = NODE_PSYMBOL("_events"); + + process->Set(_events_symbol, Object::New()); if (emitting_toplevel_domain_error_symbol.IsEmpty()) emitting_toplevel_domain_error_symbol = diff --git a/src/node.js b/src/node.js index ab560405fa4590..cbee98595540c9 100644 --- a/src/node.js +++ b/src/node.js @@ -229,6 +229,7 @@ // and exit if there are no listeners. process._fatalException = function(er) { var caught = false; + if (process.domain) { var domain = process.domain; var domainModule = NativeModule.require('domain'); @@ -256,11 +257,17 @@ // aborting in these cases. if (domainStack.length === 1) { 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 = domain.emit('error', er); + // 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 (process.EventEmitter.listenerCount(domain, 'error') > 0) { + // 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 = domain.emit('error', er); + } } finally { process._emittingTopLevelDomainError = false; } @@ -297,9 +304,12 @@ // current tick and no domains should be left on the stack // between ticks. _clearDomainsStack(); - } else { + } + + if (!caught) { caught = process.emit('uncaughtException', er); } + // if someone handled it, then great. otherwise, die in C++ land // since that means that we'll exit the process, emit the 'exit' event if (!caught) { diff --git a/test/common.js b/test/common.js index fe2f30cb194f8c..1508937e1867d5 100644 --- a/test/common.js +++ b/test/common.js @@ -226,3 +226,37 @@ exports.busyLoop = function busyLoop(time) { ; } }; + +// 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). + var expectedSignals = ['SIGILL', 'SIGTRAP', 'SIGABRT']; + + // On Windows, v8's base::OS::Abort triggers a debug breakpoint, + // which makes the process exit with code -2147483645. + if (process.platform === 'win32') + expectedExitCodes = [-2147483645]; + + // 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; + } +}; diff --git a/test/simple/test-domain-abort-on-uncaught.js b/test/simple/test-domain-abort-on-uncaught.js new file mode 100644 index 00000000000000..ad75be74ea8d67 --- /dev/null +++ b/test/simple/test-domain-abort-on-uncaught.js @@ -0,0 +1,241 @@ +'use strict'; + +// This test makes sure that when using --abort-on-uncaught-exception and +// when throwing an error from within a domain that has an error handler +// setup, the process _does not_ abort. + +var common = require('../common'); +var assert = require('assert'); +var domain = require('domain'); +var child_process = require('child_process'); + +var errorHandlerCalled = false; + +var tests = [ + function nextTick() { + var d = domain.create(); + + d.once('error', function(err) { + errorHandlerCalled = true; + }); + + d.run(function() { + process.nextTick(function() { + throw new Error('exceptional!'); + }); + }); + }, + + function timer() { + var d = domain.create(); + + d.on('error', function(err) { + errorHandlerCalled = true; + }); + + d.run(function() { + setTimeout(function() { + throw new Error('exceptional!'); + }, 33); + }); + }, + + function immediate() { + var d = domain.create(); + + d.on('error', function errorHandler() { + errorHandlerCalled = true; + }); + + d.run(function() { + setImmediate(function() { + throw new Error('boom!'); + }); + }); + }, + + function timerPlusNextTick() { + var d = domain.create(); + + d.on('error', function(err) { + errorHandlerCalled = true; + }); + + d.run(function() { + setTimeout(function() { + process.nextTick(function() { + throw new Error('exceptional!'); + }); + }, 33); + }); + }, + + function firstRun() { + var d = domain.create(); + + d.on('error', function(err) { + errorHandlerCalled = true; + }); + + d.run(function() { + throw new Error('exceptional!'); + }); + }, + + function fsAsync() { + var 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() { + var net = require('net'); + var d = domain.create(); + + d.on('error', function(err) { + errorHandlerCalled = true; + }); + + d.run(function() { + var server = net.createServer(function(conn) { + conn.pipe(conn); + }); + server.listen(common.PORT, '0.0.0.0', function() { + var conn = net.connect(common.PORT, '0.0.0.0'); + conn.once('data', function() { + throw new Error('ok'); + }); + conn.end('ok'); + server.close(); + }); + }); + }, + + function firstRunNestedWithErrorHandler() { + var d = domain.create(); + var d2 = domain.create(); + + d2.on('error', function errorHandler() { + errorHandlerCalled = true; + }); + + d.run(function() { + d2.run(function() { + throw new Error('boom!'); + }); + }); + }, + + function timeoutNestedWithErrorHandler() { + var d = domain.create(); + var 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() { + var d = domain.create(); + var d2 = domain.create(); + + d2.on('error', function errorHandler() { + errorHandlerCalled = true; + }); + + d.run(function() { + d2.run(function() { + setImmediate(function() { + throw new Error('boom!'); + }); + }); + }); + }, + + function nextTickNestedWithErrorHandler() { + var d = domain.create(); + var 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() { + var d = domain.create(); + var 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') { + var 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); + }); + }); +} diff --git a/test/simple/test-domain-no-error-handler-abort-on-uncaught.js b/test/simple/test-domain-no-error-handler-abort-on-uncaught.js new file mode 100644 index 00000000000000..fddd80dab36d30 --- /dev/null +++ b/test/simple/test-domain-no-error-handler-abort-on-uncaught.js @@ -0,0 +1,190 @@ +'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. + */ +var common = require('../common'); +var assert = require('assert'); +var domain = require('domain'); +var child_process = require('child_process'); + +var tests = [ + function() { + var d = domain.create(); + + d.run(function() { + throw new Error('boom!'); + }); + }, + + function() { + var d = domain.create(); + var d2 = domain.create(); + + d.run(function() { + d2.run(function() { + throw new Error('boom!'); + }); + }); + }, + + /* + * In this case, only the domain at the top of the stack "d" has an error + * handler, but the domain from which the error is thrown doesn't have an + * error handler. To be consistent with the way domains bubble up errors, + * this test case _should not_ make the process abort. However it was + * determined that making such a change in v0.10.x might be a breaking + * change, so instead this test case makes sure the process aborts. + */ + function() { + var d = domain.create(); + var d2 = domain.create(); + + d.on('error', function errorHandler() { + }); + + d.run(function() { + d2.run(function() { + throw new Error('boom!'); + }); + }); + }, + + function() { + var d = domain.create(); + + d.run(function() { + setTimeout(function() { + throw new Error('boom!'); + }); + }); + }, + + function() { + var d = domain.create(); + + d.run(function() { + setImmediate(function() { + throw new Error('boom!'); + }); + }); + }, + + function() { + var d = domain.create(); + + d.run(function() { + process.nextTick(function() { + throw new Error('boom!'); + }); + }); + }, + + function() { + var d = domain.create(); + + d.run(function() { + var fs = require('fs'); + fs.exists('/non/existing/file', function onExists(exists) { + throw new Error('boom!'); + }); + }); + }, + + function() { + var d = domain.create(); + var d2 = domain.create(); + + d.on('error', function errorHandler() { + }); + + d.run(function() { + d2.run(function() { + setTimeout(function() { + throw new Error('boom!'); + }); + }); + }); + }, + + function() { + var d = domain.create(); + var d2 = domain.create(); + + d.on('error', function errorHandler() { + }); + + d.run(function() { + d2.run(function() { + setImmediate(function() { + throw new Error('boom!'); + }); + }); + }); + }, + + function() { + var d = domain.create(); + var d2 = domain.create(); + + d.on('error', function errorHandler() { + }); + + d.run(function() { + d2.run(function() { + process.nextTick(function() { + throw new Error('boom!'); + }); + }); + }); + }, + + function() { + var d = domain.create(); + var 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') { + var 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) { + var errMsg = 'Test at index ' + testIndex + ' should have aborted ' + + 'but instead exited with exit code ' + exitCode + ' and signal ' + + signal; + assert(common.nodeProcessAborted(exitCode, signal), errMsg); + }); + }); +} diff --git a/test/simple/test-domain-throw-error-then-throw-from-uncaught-exception-handler.js b/test/simple/test-domain-throw-error-then-throw-from-uncaught-exception-handler.js new file mode 100644 index 00000000000000..5339af332bb62c --- /dev/null +++ b/test/simple/test-domain-throw-error-then-throw-from-uncaught-exception-handler.js @@ -0,0 +1,102 @@ +'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. + +var common = require('../common'); +var assert = require('assert'); +var child_process = require('child_process'); +var domain = require('domain'); + +var uncaughtExceptionHandlerErrMsg = 'boom from uncaughtException handler'; +var domainErrMsg = 'boom from domain'; + +var RAN_UNCAUGHT_EXCEPTION_HANDLER_EXIT_CODE = 42; +var d; + +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); + } + })); + + 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.indexOf(uncaughtExceptionHandlerErrMsg) !== -1, + 'stderr output must include proper uncaughtException handler\'s ' + + 'error\'s message'); + assert(stderr.indexOf(domainErrMsg) === -1, '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; +} diff --git a/test/simple/test-domain-uncaught-exception.js b/test/simple/test-domain-uncaught-exception.js new file mode 100644 index 00000000000000..cc035183e9f26c --- /dev/null +++ b/test/simple/test-domain-uncaught-exception.js @@ -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. + */ + +var common = require('../common'); +var assert = require('assert'); +var domain = require('domain'); +var child_process = require('child_process'); + +var uncaughtExceptions = {}; + +var 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. + */ + var 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. + */ + var 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. + */ + var d3 = domain.create(); + var 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. + */ + var d5 = domain.create(); + var 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. + */ + var d7 = domain.create(); + var 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. + */ + var d9 = domain.create(); + var 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') { + var 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) { + var 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'); + } + }); + } + })); + }); +} diff --git a/test/simple/test-domain-with-abort-on-uncaught-exception.js b/test/simple/test-domain-with-abort-on-uncaught-exception.js index d8a1cd732a4413..d1f199cd8bc9a1 100644 --- a/test/simple/test-domain-with-abort-on-uncaught-exception.js +++ b/test/simple/test-domain-with-abort-on-uncaught-exception.js @@ -21,6 +21,8 @@ var assert = require('assert'); +var common = require('../common'); + /* * The goal of this test is to make sure that: * @@ -131,50 +133,23 @@ if (process.argv[2] === 'child') { }); child.on('exit', function onChildExited(exitCode, signal) { - // If the top-level domain's error handler does not throw, - // the process must exit gracefully, whether or not - // --abort-on-uncaught-exception was passed on the command line - var expectedExitCode = 0; - // On some platforms with KSH being 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 SIGABRT. For these platforms only, - // and when the test is expected to abort, check the actual signal - // with the expected signal instead of the exit code. - var expectedSignal; - - // When throwing errors from the top-level domain error handler - // outside of a try/catch block, the process should not exit gracefully if (!options.useTryCatch && options.throwInDomainErrHandler) { - expectedExitCode = 7; if (cmdLineOption === '--abort-on-uncaught-exception') { - // If the top-level domain's error handler throws, and only if - // --abort-on-uncaught-exception is passed on the command line, - // the process must abort. - expectedExitCode = 134; - - // On linux, v8 raises SIGTRAP when aborting because - // the "debug break" flag is on by default - if (process.platform === 'linux') - expectedExitCode = 133; - - if (process.platform === 'sunos') { - expectedExitCode = null; - expectedSignal = 'SIGABRT'; - } - - // On Windows, v8's OS::Abort also triggers a debug breakpoint - // which makes the process exit with code -2147483645 - if (process.platform === 'win32') { - expectedExitCode = -2147483645; - } + assert(common.nodeProcessAborted(exitCode, signal), + 'process should have aborted, but did not'); + } else { + // By default, uncaught exceptions make node exit with an exit + // code of 7. + assert.equal(exitCode, 7); + assert.equal(signal, null); } + } else { + // If the top-level domain's error handler does not throw, + // the process must exit gracefully, whether or not + // --abort_on_uncaught_exception was passed on the command line + assert.equal(exitCode, 0); + assert.equal(signal, null); } - - if (expectedSignal) - assert.equal(signal, expectedSignal) - - assert.equal(exitCode, expectedExitCode); }); } }