-
Notifications
You must be signed in to change notification settings - Fork 29.8k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
process: add capture function for uncaught exceptions #17159
Changes from 2 commits
f1293d5
4c85f6e
df76a62
f78f2ac
d169b26
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -1136,6 +1136,16 @@ if (process.getuid) { | |
*Note*: This function is only available on POSIX platforms (i.e. not Windows | ||
or Android). | ||
|
||
## process.hasUncaughtExceptionCaptureCallback() | ||
<!-- YAML | ||
added: REPLACEME | ||
--> | ||
|
||
* Returns: {boolean} | ||
|
||
Indicates whether a callback has been set using | ||
[`process.setUncaughtExceptionCaptureCallback()`][]. | ||
|
||
## process.hrtime([time]) | ||
<!-- YAML | ||
added: v0.7.6 | ||
|
@@ -1637,6 +1647,29 @@ if (process.getuid && process.setuid) { | |
or Android). | ||
|
||
|
||
## process.setUncaughtExceptionCaptureCallback(fn) | ||
<!-- YAML | ||
added: REPLACEME | ||
--> | ||
|
||
* `fn` {Function|null} | ||
|
||
The `process.setUncaughtExceptionCapture` function sets a function that will | ||
be invoked when an uncaught exception occurs, which will receive the exception | ||
value itself as its first argument. | ||
|
||
If such a function is set, the [`process.on('uncaughtException')`][] event will | ||
not be emitted. If `--abort-on-uncaught-exception` was passed from the | ||
command line or set through [`v8.setFlagsFromString()`][], the process will | ||
not abort. | ||
|
||
To unset the capture function, `process.setUncaughtExceptionCapture(null)` | ||
may be used. Calling this method with a non-`null` argument while another | ||
capture function is set will throw an error. | ||
|
||
*Note*: Using this function is mutually exclusive with using the | ||
deprecated [`domain`][] built-in module. | ||
|
||
## process.stderr | ||
|
||
* {Stream} | ||
|
@@ -1921,6 +1954,7 @@ cases: | |
[`JSON.stringify` spec]: https://tc39.github.io/ecma262/#sec-json.stringify | ||
[`console.error()`]: console.html#console_console_error_data_args | ||
[`console.log()`]: console.html#console_console_log_data_args | ||
[`domain`]: domain.html | ||
[`end()`]: stream.html#stream_writable_end_chunk_encoding_callback | ||
[`net.Server`]: net.html#net_class_net_server | ||
[`net.Socket`]: net.html#net_class_net_socket | ||
|
@@ -1930,11 +1964,14 @@ cases: | |
[`process.exit()`]: #process_process_exit_code | ||
[`process.exitCode`]: #process_process_exitcode | ||
[`process.kill()`]: #process_process_kill_pid_signal | ||
[`process.on('uncaughtException')`]: process.html#process_event_uncaughtexception | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. A nit: out of ABC order) There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Mh - it's o after c, right? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The one before it is There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ah, yup. They look so similar … |
||
[`promise.catch()`]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/catch | ||
[`process.setUncaughtExceptionCaptureCallback()`]: process.html#process_process_setuncaughtexceptioncapturecallback_fn | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Out of order ;) There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. … ;) |
||
[`require()`]: globals.html#globals_require | ||
[`require.main`]: modules.html#modules_accessing_the_main_module | ||
[`require.resolve()`]: modules.html#modules_require_resolve_request_options | ||
[`setTimeout(fn, 0)`]: timers.html#timers_settimeout_callback_delay_args | ||
[`v8.setFlagsFromString()`]: v8.html#v8_v8_setflagsfromstring_flags | ||
[Child Process]: child_process.html | ||
[Cluster]: cluster.html | ||
[Duplex]: stream.html#stream_duplex_and_transform_streams | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -28,6 +28,7 @@ | |
|
||
const util = require('util'); | ||
const EventEmitter = require('events'); | ||
const errors = require('internal/errors'); | ||
const { createHook } = require('async_hooks'); | ||
|
||
// communicate with events module, but don't require that | ||
|
@@ -81,19 +82,77 @@ const asyncHook = createHook({ | |
} | ||
}); | ||
|
||
// When domains are in use, they claim full ownership of the | ||
// uncaught exception capture callback. | ||
if (process.hasUncaughtExceptionCaptureCallback()) { | ||
throw new errors.Error('ERR_DOMAIN_CALLBACK_NOT_AVAILABLE'); | ||
} | ||
|
||
// Get the stack trace at the point where `domain` was required. | ||
const domainRequireStack = new Error('require(`domain`) at this point').stack; | ||
|
||
const { setUncaughtExceptionCaptureCallback } = process; | ||
process.setUncaughtExceptionCaptureCallback = function(fn) { | ||
const err = | ||
new errors.Error('ERR_DOMAIN_CANNOT_SET_UNCAUGHT_EXCEPTION_CAPTURE'); | ||
err.stack = err.stack + '\n' + '-'.repeat(40) + '\n' + domainRequireStack; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Nit: template string here? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. But does that really make it more readable? |
||
throw err; | ||
}; | ||
|
||
// It's possible to enter one domain while already inside | ||
// another one. The stack is each entered domain. | ||
const stack = []; | ||
exports._stack = stack; | ||
process._setupDomainUse(stack); | ||
process._setupDomainUse(); | ||
|
||
class Domain extends EventEmitter { | ||
function updateExceptionCapture() { | ||
if (stack.every((domain) => domain.listenerCount('error') === 0)) { | ||
setUncaughtExceptionCaptureCallback(null); | ||
} else { | ||
setUncaughtExceptionCaptureCallback(null); | ||
setUncaughtExceptionCaptureCallback((er) => { | ||
return process.domain._errorHandler(er); | ||
}); | ||
} | ||
} | ||
|
||
|
||
process.on('newListener', (name, listener) => { | ||
if (name === 'uncaughtException' && | ||
listener !== domainUncaughtExceptionClear) { | ||
// Make sure the first listener for `uncaughtException` always clears | ||
// the domain stack. | ||
process.removeListener(name, domainUncaughtExceptionClear); | ||
process.prependListener(name, domainUncaughtExceptionClear); | ||
} | ||
}); | ||
|
||
process.on('removeListener', (name, listener) => { | ||
if (name === 'uncaughtException' && | ||
listener !== domainUncaughtExceptionClear) { | ||
// If the domain listener would be the only remaining one, remove it. | ||
const listeners = process.listeners('uncaughtException'); | ||
if (listeners.length === 1 && listeners[0] === domainUncaughtExceptionClear) | ||
process.removeListener(name, domainUncaughtExceptionClear); | ||
} | ||
}); | ||
|
||
function domainUncaughtExceptionClear() { | ||
stack.length = 0; | ||
exports.active = process.domain = null; | ||
updateExceptionCapture(); | ||
} | ||
|
||
|
||
class Domain extends EventEmitter { | ||
constructor() { | ||
super(); | ||
|
||
this.members = []; | ||
asyncHook.enable(); | ||
|
||
this.on('removeListener', updateExceptionCapture); | ||
this.on('newListener', updateExceptionCapture); | ||
} | ||
} | ||
|
||
|
@@ -131,14 +190,14 @@ Domain.prototype._errorHandler = function _errorHandler(er) { | |
// prevent the process 'uncaughtException' event from being emitted | ||
// if a listener is set. | ||
if (EventEmitter.listenerCount(this, 'error') > 0) { | ||
// Clear the uncaughtExceptionCaptureCallback so that we know that, even | ||
// if technically the top-level domain is still active, it would | ||
// be ok to abort on an uncaught exception at this point | ||
setUncaughtExceptionCaptureCallback(null); | ||
try { | ||
// Set the _emittingTopLevelDomainError so that we know that, even | ||
// if technically the top-level domain is still active, it would | ||
// be ok to abort on an uncaught exception at this point | ||
process._emittingTopLevelDomainError = true; | ||
caught = this.emit('error', er); | ||
} finally { | ||
process._emittingTopLevelDomainError = false; | ||
updateExceptionCapture(); | ||
} | ||
} | ||
} else { | ||
|
@@ -161,20 +220,21 @@ Domain.prototype._errorHandler = function _errorHandler(er) { | |
if (this === exports.active) { | ||
stack.pop(); | ||
} | ||
updateExceptionCapture(); | ||
if (stack.length) { | ||
exports.active = process.domain = stack[stack.length - 1]; | ||
caught = process._fatalException(er2); | ||
caught = process.domain._errorHandler(er2); | ||
} else { | ||
caught = false; | ||
// Pass on to the next exception handler. | ||
throw er2; | ||
} | ||
} | ||
} | ||
|
||
// Exit all domains on the stack. Uncaught exceptions end the | ||
// current tick and no domains should be left on the stack | ||
// between ticks. | ||
stack.length = 0; | ||
exports.active = process.domain = null; | ||
domainUncaughtExceptionClear(); | ||
|
||
return caught; | ||
}; | ||
|
@@ -185,6 +245,7 @@ Domain.prototype.enter = function() { | |
// to push it onto the stack so that we can pop it later. | ||
exports.active = process.domain = this; | ||
stack.push(this); | ||
updateExceptionCapture(); | ||
}; | ||
|
||
|
||
|
@@ -198,6 +259,7 @@ Domain.prototype.exit = function() { | |
|
||
exports.active = stack[stack.length - 1]; | ||
process.domain = exports.active; | ||
updateExceptionCapture(); | ||
}; | ||
|
||
|
||
|
Original file line number | Diff line number | Diff line change | ||||||||
---|---|---|---|---|---|---|---|---|---|---|
|
@@ -9,6 +9,7 @@ | |||||||||
|
||||||||||
(function(process) { | ||||||||||
let internalBinding; | ||||||||||
const exceptionHandlerState = { captureFn: null }; | ||||||||||
|
||||||||||
function startup() { | ||||||||||
const EventEmitter = NativeModule.require('events'); | ||||||||||
|
@@ -35,6 +36,7 @@ | |||||||||
const _process = NativeModule.require('internal/process'); | ||||||||||
_process.setupConfig(NativeModule._source); | ||||||||||
_process.setupSignalHandlers(); | ||||||||||
_process.setupUncaughtExceptionCapture(exceptionHandlerState); | ||||||||||
NativeModule.require('internal/process/warning').setup(); | ||||||||||
NativeModule.require('internal/process/next_tick').setup(); | ||||||||||
NativeModule.require('internal/process/stdio').setup(); | ||||||||||
|
@@ -377,8 +379,10 @@ | |||||||||
// that threw and was never cleared. So clear it now. | ||||||||||
async_id_fields[kInitTriggerAsyncId] = 0; | ||||||||||
|
||||||||||
if (process.domain && process.domain._errorHandler) | ||||||||||
caught = process.domain._errorHandler(er); | ||||||||||
if (exceptionHandlerState.captureFn !== null) { | ||||||||||
exceptionHandlerState.captureFn(er); | ||||||||||
caught = true; | ||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think this conflicts with Lines 2479 to 2482 in bb44626
We probably need better tests for that behaviour (I noticed it wasn't really tested while working on it myself)... unless I'm missing something with the above. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @apapirovski Since you brought it up in the other PR: I’m writing a PR that removes There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @addaleax Sounds good! I guess it would either have to land before this or this would still need to be updated, right? Out of curiosity, what direction are you going with it? (Because technically mine removed it too but I'm guessing you have a different solution in mind.) There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @apapirovski Here’s the PR: #17333 :) There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Oh, and regarding updating this PR: The situation you’re worried about is a domain setting a capture function, that would proceed to capture the exception generated by one of these? I think you are right, #17333 should probably land first (or at least this should not land without it)… |
||||||||||
} | ||||||||||
|
||||||||||
if (!caught) | ||||||||||
caught = process.emit('uncaughtException', er); | ||||||||||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -247,6 +247,33 @@ function setupRawDebug() { | |
}; | ||
} | ||
|
||
|
||
function setupUncaughtExceptionCapture(exceptionHandlerState) { | ||
// This is a typed array for faster communication with JS. | ||
const shouldAbortOnUncaughtToggle = process._shouldAbortOnUncaughtToggle; | ||
delete process._shouldAbortOnUncaughtToggle; | ||
|
||
process.setUncaughtExceptionCaptureCallback = function(fn) { | ||
if (fn === null) { | ||
exceptionHandlerState.captureFn = fn; | ||
shouldAbortOnUncaughtToggle[0] = 1; | ||
return; | ||
} | ||
if (exceptionHandlerState.captureFn !== null) { | ||
throw new errors.Error('ERR_UNCAUGHT_EXCEPTION_CAPTURE_ALREADY_SET'); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This error doesn't appear to be documented. |
||
} | ||
if (typeof fn !== 'function') { | ||
throw new errors.TypeError('ERR_INVALID_ARG_TYPE', 'fn', 'Function'); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I would prefer type validation before the state validation. |
||
} | ||
exceptionHandlerState.captureFn = fn; | ||
shouldAbortOnUncaughtToggle[0] = 0; | ||
}; | ||
|
||
process.hasUncaughtExceptionCaptureCallback = function() { | ||
return exceptionHandlerState.captureFn !== null; | ||
}; | ||
} | ||
|
||
module.exports = { | ||
setup_performance, | ||
setup_cpuUsage, | ||
|
@@ -256,5 +283,6 @@ module.exports = { | |
setupKillAndExit, | ||
setupSignalHandlers, | ||
setupChannel, | ||
setupRawDebug | ||
setupRawDebug, | ||
setupUncaughtExceptionCapture | ||
}; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Minor nit: s/couldn't/could not
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yup, done!