Skip to content

Commit 778d783

Browse files
addaleaxMylesBorins
authored andcommitted
process: add flag for uncaught exception abort
Introduce `process.shouldAbortOnUncaughtException` to control `--abort-on-uncaught-exception` behaviour, and implement some of the domains functionality on top of it. PR-URL: #17159 Refs: #17143 Reviewed-By: James M Snell <jasnell@gmail.com> Reviewed-By: Andreas Madsen <amwebdk@gmail.com>
1 parent 4ad61b5 commit 778d783

16 files changed

+320
-74
lines changed

doc/api/cli.md

+5
Original file line numberDiff line numberDiff line change
@@ -183,6 +183,10 @@ added: v0.10
183183
Aborting instead of exiting causes a core file to be generated for post-mortem
184184
analysis using a debugger (such as `lldb`, `gdb`, and `mdb`).
185185

186+
*Note*: If this flag is passed, the behavior can still be set to not abort
187+
through [`process.setUncaughtExceptionCaptureCallback()`][] (and through usage
188+
of the `domain` module that uses it).
189+
186190
### `--trace-warnings`
187191
<!-- YAML
188192
added: v6.0.0
@@ -598,3 +602,4 @@ greater than `4` (its current default value). For more information, see the
598602
[debugger]: debugger.html
599603
[emit_warning]: process.html#process_process_emitwarning_warning_type_code_ctor
600604
[libuv threadpool documentation]: http://docs.libuv.org/en/latest/threadpool.html
605+
[`process.setUncaughtExceptionCaptureCallback()`]: process.html#process_process_setuncaughtexceptioncapturecallback_fn

doc/api/errors.md

+27
Original file line numberDiff line numberDiff line change
@@ -701,6 +701,23 @@ A signing `key` was not provided to the [`sign.sign()`][] method.
701701

702702
`c-ares` failed to set the DNS server.
703703

704+
<a id="ERR_DOMAIN_CALLBACK_NOT_AVAILABLE"></a>
705+
### ERR_DOMAIN_CALLBACK_NOT_AVAILABLE
706+
707+
The `domain` module was not usable since it could not establish the required
708+
error handling hooks, because
709+
[`process.setUncaughtExceptionCaptureCallback()`][] had been called at an
710+
earlier point in time.
711+
712+
<a id="ERR_DOMAIN_CANNOT_SET_UNCAUGHT_EXCEPTION_CAPTURE"></a>
713+
### ERR_DOMAIN_CANNOT_SET_UNCAUGHT_EXCEPTION_CAPTURE
714+
715+
[`process.setUncaughtExceptionCaptureCallback()`][] could not be called
716+
because the `domain` module has been loaded at an earlier point in time.
717+
718+
The stack trace is extended to include the point in time at which the
719+
`domain` module had been loaded.
720+
704721
<a id="ERR_ENCODING_INVALID_ENCODED_DATA"></a>
705722
### ERR_ENCODING_INVALID_ENCODED_DATA
706723

@@ -1419,6 +1436,15 @@ A Transform stream finished while it was still transforming.
14191436

14201437
A Transform stream finished with data still in the write buffer.
14211438

1439+
<a id="ERR_UNCAUGHT_EXCEPTION_CAPTURE_ALREADY_SET"></a>
1440+
### ERR_UNCAUGHT_EXCEPTION_CAPTURE_ALREADY_SET
1441+
1442+
[`process.setUncaughtExceptionCaptureCallback()`][] was called twice,
1443+
without first resetting the callback to `null`.
1444+
1445+
This error is designed to prevent accidentally overwriting a callback registered
1446+
from another module.
1447+
14221448
<a id="ERR_UNESCAPED_CHARACTERS"></a>
14231449
### ERR_UNESCAPED_CHARACTERS
14241450

@@ -1524,6 +1550,7 @@ Creation of a [`zlib`][] object failed due to incorrect configuration.
15241550
[`new URLSearchParams(iterable)`]: url.html#url_constructor_new_urlsearchparams_iterable
15251551
[`process.on('uncaughtException')`]: process.html#process_event_uncaughtexception
15261552
[`process.send()`]: process.html#process_process_send_message_sendhandle_options_callback
1553+
[`process.setUncaughtExceptionCaptureCallback()`]: process.html#process_process_setuncaughtexceptioncapturecallback_fn
15271554
[`require('crypto').setEngine()`]: crypto.html#crypto_crypto_setengine_engine_flags
15281555
[`server.listen()`]: net.html#net_server_listen
15291556
[ES6 module]: esm.html

doc/api/process.md

+37
Original file line numberDiff line numberDiff line change
@@ -1136,6 +1136,16 @@ if (process.getuid) {
11361136
*Note*: This function is only available on POSIX platforms (i.e. not Windows
11371137
or Android).
11381138

1139+
## process.hasUncaughtExceptionCaptureCallback()
1140+
<!-- YAML
1141+
added: REPLACEME
1142+
-->
1143+
1144+
* Returns: {boolean}
1145+
1146+
Indicates whether a callback has been set using
1147+
[`process.setUncaughtExceptionCaptureCallback()`][].
1148+
11391149
## process.hrtime([time])
11401150
<!-- YAML
11411151
added: v0.7.6
@@ -1637,6 +1647,29 @@ if (process.getuid && process.setuid) {
16371647
or Android).
16381648

16391649

1650+
## process.setUncaughtExceptionCaptureCallback(fn)
1651+
<!-- YAML
1652+
added: REPLACEME
1653+
-->
1654+
1655+
* `fn` {Function|null}
1656+
1657+
The `process.setUncaughtExceptionCapture` function sets a function that will
1658+
be invoked when an uncaught exception occurs, which will receive the exception
1659+
value itself as its first argument.
1660+
1661+
If such a function is set, the [`process.on('uncaughtException')`][] event will
1662+
not be emitted. If `--abort-on-uncaught-exception` was passed from the
1663+
command line or set through [`v8.setFlagsFromString()`][], the process will
1664+
not abort.
1665+
1666+
To unset the capture function, `process.setUncaughtExceptionCapture(null)`
1667+
may be used. Calling this method with a non-`null` argument while another
1668+
capture function is set will throw an error.
1669+
1670+
*Note*: Using this function is mutually exclusive with using the
1671+
deprecated [`domain`][] built-in module.
1672+
16401673
## process.stderr
16411674

16421675
* {Stream}
@@ -1921,6 +1954,7 @@ cases:
19211954
[`JSON.stringify` spec]: https://tc39.github.io/ecma262/#sec-json.stringify
19221955
[`console.error()`]: console.html#console_console_error_data_args
19231956
[`console.log()`]: console.html#console_console_log_data_args
1957+
[`domain`]: domain.html
19241958
[`end()`]: stream.html#stream_writable_end_chunk_encoding_callback
19251959
[`net.Server`]: net.html#net_class_net_server
19261960
[`net.Socket`]: net.html#net_class_net_socket
@@ -1930,11 +1964,14 @@ cases:
19301964
[`process.exit()`]: #process_process_exit_code
19311965
[`process.exitCode`]: #process_process_exitcode
19321966
[`process.kill()`]: #process_process_kill_pid_signal
1967+
[`process.on('uncaughtException')`]: process.html#process_event_uncaughtexception
1968+
[`process.setUncaughtExceptionCaptureCallback()`]: process.html#process_process_setuncaughtexceptioncapturecallback_fn
19331969
[`promise.catch()`]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/catch
19341970
[`require()`]: globals.html#globals_require
19351971
[`require.main`]: modules.html#modules_accessing_the_main_module
19361972
[`require.resolve()`]: modules.html#modules_require_resolve_request_options
19371973
[`setTimeout(fn, 0)`]: timers.html#timers_settimeout_callback_delay_args
1974+
[`v8.setFlagsFromString()`]: v8.html#v8_v8_setflagsfromstring_flags
19381975
[Child Process]: child_process.html
19391976
[Cluster]: cluster.html
19401977
[Duplex]: stream.html#stream_duplex_and_transform_streams

lib/domain.js

+73-11
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828

2929
const util = require('util');
3030
const EventEmitter = require('events');
31+
const errors = require('internal/errors');
3132
const { createHook } = require('async_hooks');
3233

3334
// communicate with events module, but don't require that
@@ -81,19 +82,77 @@ const asyncHook = createHook({
8182
}
8283
});
8384

85+
// When domains are in use, they claim full ownership of the
86+
// uncaught exception capture callback.
87+
if (process.hasUncaughtExceptionCaptureCallback()) {
88+
throw new errors.Error('ERR_DOMAIN_CALLBACK_NOT_AVAILABLE');
89+
}
90+
91+
// Get the stack trace at the point where `domain` was required.
92+
const domainRequireStack = new Error('require(`domain`) at this point').stack;
93+
94+
const { setUncaughtExceptionCaptureCallback } = process;
95+
process.setUncaughtExceptionCaptureCallback = function(fn) {
96+
const err =
97+
new errors.Error('ERR_DOMAIN_CANNOT_SET_UNCAUGHT_EXCEPTION_CAPTURE');
98+
err.stack = err.stack + '\n' + '-'.repeat(40) + '\n' + domainRequireStack;
99+
throw err;
100+
};
101+
84102
// It's possible to enter one domain while already inside
85103
// another one. The stack is each entered domain.
86104
const stack = [];
87105
exports._stack = stack;
88-
process._setupDomainUse(stack);
106+
process._setupDomainUse();
89107

90-
class Domain extends EventEmitter {
108+
function updateExceptionCapture() {
109+
if (stack.every((domain) => domain.listenerCount('error') === 0)) {
110+
setUncaughtExceptionCaptureCallback(null);
111+
} else {
112+
setUncaughtExceptionCaptureCallback(null);
113+
setUncaughtExceptionCaptureCallback((er) => {
114+
return process.domain._errorHandler(er);
115+
});
116+
}
117+
}
118+
119+
120+
process.on('newListener', (name, listener) => {
121+
if (name === 'uncaughtException' &&
122+
listener !== domainUncaughtExceptionClear) {
123+
// Make sure the first listener for `uncaughtException` always clears
124+
// the domain stack.
125+
process.removeListener(name, domainUncaughtExceptionClear);
126+
process.prependListener(name, domainUncaughtExceptionClear);
127+
}
128+
});
129+
130+
process.on('removeListener', (name, listener) => {
131+
if (name === 'uncaughtException' &&
132+
listener !== domainUncaughtExceptionClear) {
133+
// If the domain listener would be the only remaining one, remove it.
134+
const listeners = process.listeners('uncaughtException');
135+
if (listeners.length === 1 && listeners[0] === domainUncaughtExceptionClear)
136+
process.removeListener(name, domainUncaughtExceptionClear);
137+
}
138+
});
91139

140+
function domainUncaughtExceptionClear() {
141+
stack.length = 0;
142+
exports.active = process.domain = null;
143+
updateExceptionCapture();
144+
}
145+
146+
147+
class Domain extends EventEmitter {
92148
constructor() {
93149
super();
94150

95151
this.members = [];
96152
asyncHook.enable();
153+
154+
this.on('removeListener', updateExceptionCapture);
155+
this.on('newListener', updateExceptionCapture);
97156
}
98157
}
99158

@@ -131,14 +190,14 @@ Domain.prototype._errorHandler = function _errorHandler(er) {
131190
// prevent the process 'uncaughtException' event from being emitted
132191
// if a listener is set.
133192
if (EventEmitter.listenerCount(this, 'error') > 0) {
193+
// Clear the uncaughtExceptionCaptureCallback so that we know that, even
194+
// if technically the top-level domain is still active, it would
195+
// be ok to abort on an uncaught exception at this point
196+
setUncaughtExceptionCaptureCallback(null);
134197
try {
135-
// Set the _emittingTopLevelDomainError so that we know that, even
136-
// if technically the top-level domain is still active, it would
137-
// be ok to abort on an uncaught exception at this point
138-
process._emittingTopLevelDomainError = true;
139198
caught = this.emit('error', er);
140199
} finally {
141-
process._emittingTopLevelDomainError = false;
200+
updateExceptionCapture();
142201
}
143202
}
144203
} else {
@@ -161,20 +220,21 @@ Domain.prototype._errorHandler = function _errorHandler(er) {
161220
if (this === exports.active) {
162221
stack.pop();
163222
}
223+
updateExceptionCapture();
164224
if (stack.length) {
165225
exports.active = process.domain = stack[stack.length - 1];
166-
caught = process._fatalException(er2);
226+
caught = process.domain._errorHandler(er2);
167227
} else {
168-
caught = false;
228+
// Pass on to the next exception handler.
229+
throw er2;
169230
}
170231
}
171232
}
172233

173234
// Exit all domains on the stack. Uncaught exceptions end the
174235
// current tick and no domains should be left on the stack
175236
// between ticks.
176-
stack.length = 0;
177-
exports.active = process.domain = null;
237+
domainUncaughtExceptionClear();
178238

179239
return caught;
180240
};
@@ -185,6 +245,7 @@ Domain.prototype.enter = function() {
185245
// to push it onto the stack so that we can pop it later.
186246
exports.active = process.domain = this;
187247
stack.push(this);
248+
updateExceptionCapture();
188249
};
189250

190251

@@ -198,6 +259,7 @@ Domain.prototype.exit = function() {
198259

199260
exports.active = stack[stack.length - 1];
200261
process.domain = exports.active;
262+
updateExceptionCapture();
201263
};
202264

203265

lib/internal/bootstrap_node.js

+6-2
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99

1010
(function(process) {
1111
let internalBinding;
12+
const exceptionHandlerState = { captureFn: null };
1213

1314
function startup() {
1415
const EventEmitter = NativeModule.require('events');
@@ -34,6 +35,7 @@
3435
const _process = NativeModule.require('internal/process');
3536
_process.setupConfig(NativeModule._source);
3637
_process.setupSignalHandlers();
38+
_process.setupUncaughtExceptionCapture(exceptionHandlerState);
3739
NativeModule.require('internal/process/warning').setup();
3840
NativeModule.require('internal/process/next_tick').setup();
3941
NativeModule.require('internal/process/stdio').setup();
@@ -376,8 +378,10 @@
376378
// that threw and was never cleared. So clear it now.
377379
async_id_fields[kInitTriggerAsyncId] = 0;
378380

379-
if (process.domain && process.domain._errorHandler)
380-
caught = process.domain._errorHandler(er);
381+
if (exceptionHandlerState.captureFn !== null) {
382+
exceptionHandlerState.captureFn(er);
383+
caught = true;
384+
}
381385

382386
if (!caught)
383387
caught = process.emit('uncaughtException', er);

lib/internal/errors.js

+10
Original file line numberDiff line numberDiff line change
@@ -171,6 +171,13 @@ E('ERR_CRYPTO_SIGN_KEY_REQUIRED', 'No key provided to sign');
171171
E('ERR_CRYPTO_TIMING_SAFE_EQUAL_LENGTH',
172172
'Input buffers must have the same length');
173173
E('ERR_DNS_SET_SERVERS_FAILED', 'c-ares failed to set servers: "%s" [%s]');
174+
E('ERR_DOMAIN_CALLBACK_NOT_AVAILABLE',
175+
'A callback was registered through ' +
176+
'process.setUncaughtExceptionCaptureCallback(), which is mutually ' +
177+
'exclusive with using the `domain` module');
178+
E('ERR_DOMAIN_CANNOT_SET_UNCAUGHT_EXCEPTION_CAPTURE',
179+
'The `domain` module is in use, which is mutually exclusive with calling ' +
180+
'process.setUncaughtExceptionCaptureCallback()');
174181
E('ERR_ENCODING_INVALID_ENCODED_DATA',
175182
'The encoded data was not valid for encoding %s');
176183
E('ERR_ENCODING_NOT_SUPPORTED', 'The "%s" encoding is not supported');
@@ -339,6 +346,9 @@ E('ERR_TRANSFORM_ALREADY_TRANSFORMING',
339346
'Calling transform done when still transforming');
340347
E('ERR_TRANSFORM_WITH_LENGTH_0',
341348
'Calling transform done when writableState.length != 0');
349+
E('ERR_UNCAUGHT_EXCEPTION_CAPTURE_ALREADY_SET',
350+
'`process.setupUncaughtExceptionCapture()` was called while a capture ' +
351+
'callback was already active');
342352
E('ERR_UNESCAPED_CHARACTERS', '%s contains unescaped characters');
343353
E('ERR_UNHANDLED_ERROR',
344354
(err) => {

lib/internal/process.js

+30-1
Original file line numberDiff line numberDiff line change
@@ -247,6 +247,34 @@ function setupRawDebug() {
247247
};
248248
}
249249

250+
251+
function setupUncaughtExceptionCapture(exceptionHandlerState) {
252+
// This is a typed array for faster communication with JS.
253+
const shouldAbortOnUncaughtToggle = process._shouldAbortOnUncaughtToggle;
254+
delete process._shouldAbortOnUncaughtToggle;
255+
256+
process.setUncaughtExceptionCaptureCallback = function(fn) {
257+
if (fn === null) {
258+
exceptionHandlerState.captureFn = fn;
259+
shouldAbortOnUncaughtToggle[0] = 1;
260+
return;
261+
}
262+
if (typeof fn !== 'function') {
263+
throw new errors.TypeError('ERR_INVALID_ARG_TYPE', 'fn',
264+
['Function', 'null']);
265+
}
266+
if (exceptionHandlerState.captureFn !== null) {
267+
throw new errors.Error('ERR_UNCAUGHT_EXCEPTION_CAPTURE_ALREADY_SET');
268+
}
269+
exceptionHandlerState.captureFn = fn;
270+
shouldAbortOnUncaughtToggle[0] = 0;
271+
};
272+
273+
process.hasUncaughtExceptionCaptureCallback = function() {
274+
return exceptionHandlerState.captureFn !== null;
275+
};
276+
}
277+
250278
module.exports = {
251279
setup_performance,
252280
setup_cpuUsage,
@@ -256,5 +284,6 @@ module.exports = {
256284
setupKillAndExit,
257285
setupSignalHandlers,
258286
setupChannel,
259-
setupRawDebug
287+
setupRawDebug,
288+
setupUncaughtExceptionCapture
260289
};

0 commit comments

Comments
 (0)