Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings

Commit 347164a

Browse filesBrowse 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 fd501b3 commit 347164a
Copy full SHA for 347164a
Expand file treeCollapse file tree

16 files changed

+320
-74
lines changed
Open diff view settings
Collapse file

‎doc/api/cli.md‎

Copy file name to clipboardExpand all lines: doc/api/cli.md
+5Lines changed: 5 additions & 0 deletions
  • Display the source diff
  • Display the rich diff
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
Collapse file

‎doc/api/errors.md‎

Copy file name to clipboardExpand all lines: doc/api/errors.md
+27Lines changed: 27 additions & 0 deletions
  • Display the source diff
  • Display the rich diff
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
Collapse file

‎doc/api/process.md‎

Copy file name to clipboardExpand all lines: doc/api/process.md
+37Lines changed: 37 additions & 0 deletions
  • Display the source diff
  • Display the rich diff
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
Collapse file

‎lib/domain.js‎

Copy file name to clipboardExpand all lines: lib/domain.js
+73-11Lines changed: 73 additions & 11 deletions
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

Collapse file

‎lib/internal/bootstrap_node.js‎

Copy file name to clipboardExpand all lines: lib/internal/bootstrap_node.js
+6-2Lines changed: 6 additions & 2 deletions
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);
Collapse file

‎lib/internal/errors.js‎

Copy file name to clipboardExpand all lines: lib/internal/errors.js
+10Lines changed: 10 additions & 0 deletions
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) => {
Collapse file

‎lib/internal/process.js‎

Copy file name to clipboardExpand all lines: lib/internal/process.js
+30-1Lines changed: 30 additions & 1 deletion
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 (0)
Morty Proxy This is a proxified and sanitized view of the page, visit original site.