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
|
||||
const _domain_flag = process._setupDomainUse(_domain);
|
||||
const _domain_flag = process._setupDomainUse(_domain, stack);
|
||||
|
||||
exports.Domain = Domain;
|
||||
|
||||
@ -36,10 +41,6 @@ exports.create = exports.createDomain = function() {
|
||||
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.
|
||||
exports.active = null;
|
||||
|
||||
@ -96,14 +97,20 @@ Domain.prototype._errorHandler = function errorHandler(er) {
|
||||
// that these exceptions are caught, and thus would prevent it from
|
||||
// aborting in these cases.
|
||||
if (stack.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 = emitError();
|
||||
} finally {
|
||||
process._emittingTopLevelDomainError = false;
|
||||
// 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 {
|
||||
// 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 = emitError();
|
||||
} finally {
|
||||
process._emittingTopLevelDomainError = false;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// wrap this in a try/catch so we don't get infinite throwing
|
||||
|
@ -242,6 +242,7 @@ namespace node {
|
||||
V(buffer_prototype_object, v8::Object) \
|
||||
V(context, v8::Context) \
|
||||
V(domain_array, v8::Array) \
|
||||
V(domains_stack_array, v8::Array) \
|
||||
V(fs_stats_constructor_function, v8::Function) \
|
||||
V(generic_internal_field_template, v8::ObjectTemplate) \
|
||||
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);
|
||||
}
|
||||
|
||||
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())
|
||||
return false;
|
||||
|
||||
Local<Array> domain_array = env->domain_array().As<Array>();
|
||||
if (domain_array->Length() == 0)
|
||||
Local<Array> domains_stack_array = env->domains_stack_array().As<Array>();
|
||||
if (domains_stack_array->Length() == 0)
|
||||
return false;
|
||||
|
||||
Local<Value> domain_v = domain_array->Get(0);
|
||||
return !domain_v->IsNull();
|
||||
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;
|
||||
}
|
||||
|
||||
|
||||
@ -975,7 +1008,7 @@ static bool ShouldAbortOnUncaughtException(Isolate* isolate) {
|
||||
bool isEmittingTopLevelDomainError =
|
||||
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());
|
||||
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.
|
||||
env->process_object()->Delete(
|
||||
FIXED_ONE_BYTE_STRING(args.GetIsolate(), "_setupDomainUse"));
|
||||
|
@ -471,3 +471,37 @@ ArrayStream.prototype.writable = true;
|
||||
ArrayStream.prototype.pause = function() {};
|
||||
ArrayStream.prototype.resume = 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,96 +1,256 @@
|
||||
'use strict';
|
||||
// Flags: --abort_on_uncaught_exception
|
||||
|
||||
var common = require('../common');
|
||||
var assert = require('assert');
|
||||
var domain = require('domain');
|
||||
// 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 tests = [
|
||||
nextTick,
|
||||
timer,
|
||||
timerPlusNextTick,
|
||||
netServer,
|
||||
firstRun,
|
||||
];
|
||||
const common = require('../common');
|
||||
const assert = require('assert');
|
||||
const domain = require('domain');
|
||||
const child_process = require('child_process');
|
||||
|
||||
var errors = 0;
|
||||
var errorHandlerCalled = false;
|
||||
|
||||
process.on('exit', function() {
|
||||
assert.equal(errors, tests.length);
|
||||
});
|
||||
const tests = [
|
||||
function nextTick() {
|
||||
const d = domain.create();
|
||||
|
||||
tests.forEach(function(test) { test(); });
|
||||
|
||||
function nextTick() {
|
||||
var d = domain.create();
|
||||
|
||||
d.once('error', function(err) {
|
||||
errors += 1;
|
||||
});
|
||||
d.run(function() {
|
||||
process.nextTick(function() {
|
||||
throw new Error('exceptional!');
|
||||
d.once('error', function(err) {
|
||||
errorHandlerCalled = true;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function timer() {
|
||||
var d = domain.create();
|
||||
|
||||
d.on('error', function(err) {
|
||||
errors += 1;
|
||||
});
|
||||
d.run(function() {
|
||||
setTimeout(function() {
|
||||
throw new Error('exceptional!');
|
||||
}, 33);
|
||||
});
|
||||
}
|
||||
|
||||
function timerPlusNextTick() {
|
||||
var d = domain.create();
|
||||
|
||||
d.on('error', function(err) {
|
||||
errors += 1;
|
||||
});
|
||||
d.run(function() {
|
||||
setTimeout(function() {
|
||||
d.run(function() {
|
||||
process.nextTick(function() {
|
||||
throw new Error('exceptional!');
|
||||
});
|
||||
}, 33);
|
||||
});
|
||||
}
|
||||
|
||||
function firstRun() {
|
||||
var d = domain.create();
|
||||
|
||||
d.on('error', function(err) {
|
||||
errors += 1;
|
||||
});
|
||||
d.run(function() {
|
||||
throw new Error('exceptional!');
|
||||
});
|
||||
}
|
||||
|
||||
function netServer() {
|
||||
var net = require('net');
|
||||
var d = domain.create();
|
||||
|
||||
d.on('error', function(err) {
|
||||
errors += 1;
|
||||
});
|
||||
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');
|
||||
},
|
||||
|
||||
function timer() {
|
||||
const d = domain.create();
|
||||
|
||||
d.on('error', function(err) {
|
||||
errorHandlerCalled = true;
|
||||
});
|
||||
|
||||
d.run(function() {
|
||||
setTimeout(function() {
|
||||
throw new Error('exceptional!');
|
||||
}, 33);
|
||||
});
|
||||
},
|
||||
|
||||
function immediate() {
|
||||
const d = domain.create();
|
||||
|
||||
d.on('error', function errorHandler() {
|
||||
errorHandlerCalled = true;
|
||||
});
|
||||
|
||||
d.run(function() {
|
||||
setImmediate(function() {
|
||||
throw new Error('boom!');
|
||||
});
|
||||
conn.end('ok');
|
||||
server.close();
|
||||
});
|
||||
},
|
||||
|
||||
function timerPlusNextTick() {
|
||||
const 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() {
|
||||
const d = domain.create();
|
||||
|
||||
d.on('error', function(err) {
|
||||
errorHandlerCalled = true;
|
||||
});
|
||||
|
||||
d.run(function() {
|
||||
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() {
|
||||
const net = require('net');
|
||||
const d = domain.create();
|
||||
|
||||
d.on('error', function(err) {
|
||||
errorHandlerCalled = true;
|
||||
});
|
||||
|
||||
d.run(function() {
|
||||
const server = net.createServer(function(conn) {
|
||||
conn.pipe(conn);
|
||||
});
|
||||
server.listen(common.PORT, common.localhostIPv4, function() {
|
||||
const conn = net.connect(common.PORT, common.localhostIPv4);
|
||||
conn.once('data', function() {
|
||||
throw new Error('ok');
|
||||
});
|
||||
conn.end('ok');
|
||||
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
|
||||
if (!options.useTryCatch && options.throwInDomainErrHandler) {
|
||||
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.
|
||||
//
|
||||
// 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);
|
||||
}
|
||||
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.
|
||||
|
Loading…
x
Reference in New Issue
Block a user