Skip to content

Commit dcf5e94

Browse files
committed
repl: don't use deprecated domain module
1 parent 73414f3 commit dcf5e94

12 files changed

Lines changed: 86 additions & 215 deletions

doc/api/errors.md

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2016,13 +2016,6 @@ An invalid `options.protocol` was passed to `http.request()`.
20162016
Both `breakEvalOnSigint` and `eval` options were set in the [`REPL`][] config,
20172017
which is not supported.
20182018

2019-
<a id="ERR_INVALID_REPL_INPUT"></a>
2020-
2021-
### `ERR_INVALID_REPL_INPUT`
2022-
2023-
The input may not be used in the [`REPL`][]. The conditions under which this
2024-
error is used are described in the [`REPL`][] documentation.
2025-
20262019
<a id="ERR_INVALID_RETURN_PROPERTY"></a>
20272020

20282021
### `ERR_INVALID_RETURN_PROPERTY`
@@ -3538,6 +3531,13 @@ removed: v16.7.0
35383531
While using the Performance Timing API (`perf_hooks`), a performance mark is
35393532
invalid.
35403533

3534+
<a id="ERR_INVALID_REPL_INPUT"></a>
3535+
3536+
### `ERR_INVALID_REPL_INPUT`
3537+
3538+
The input may not be used in the [`REPL`][]. The conditions under which this
3539+
error is used are described in the [`REPL`][] documentation.
3540+
35413541
<a id="ERR_INVALID_TRANSFER_OBJECT"></a>
35423542

35433543
### `ERR_INVALID_TRANSFER_OBJECT`

doc/api/repl.md

Lines changed: 0 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -147,39 +147,6 @@ global or scoped variable, the input `fs` will be evaluated on-demand as
147147
> fs.createReadStream('./some/file');
148148
```
149149

150-
#### Global uncaught exceptions
151-
152-
<!-- YAML
153-
changes:
154-
- version: v12.3.0
155-
pr-url: https://github.com/nodejs/node/pull/27151
156-
description: The `'uncaughtException'` event is from now on triggered if the
157-
repl is used as standalone program.
158-
-->
159-
160-
The REPL uses the [`domain`][] module to catch all uncaught exceptions for that
161-
REPL session.
162-
163-
This use of the [`domain`][] module in the REPL has these side effects:
164-
165-
* Uncaught exceptions only emit the [`'uncaughtException'`][] event in the
166-
standalone REPL. Adding a listener for this event in a REPL within
167-
another Node.js program results in [`ERR_INVALID_REPL_INPUT`][].
168-
169-
```js
170-
const r = repl.start();
171-
172-
r.write('process.on("uncaughtException", () => console.log("Foobar"));\n');
173-
// Output stream includes:
174-
// TypeError [ERR_INVALID_REPL_INPUT]: Listeners for `uncaughtException`
175-
// cannot be used in the REPL
176-
177-
r.close();
178-
```
179-
180-
* Trying to use [`process.setUncaughtExceptionCaptureCallback()`][] throws
181-
an [`ERR_DOMAIN_CANNOT_SET_UNCAUGHT_EXCEPTION_CAPTURE`][] error.
182-
183150
#### Assignment of the `_` (underscore) variable
184151

185152
<!-- YAML
@@ -768,13 +735,8 @@ avoiding open network interfaces.
768735

769736
[TTY keybindings]: readline.md#tty-keybindings
770737
[ZSH]: https://en.wikipedia.org/wiki/Z_shell
771-
[`'uncaughtException'`]: process.md#event-uncaughtexception
772738
[`--no-experimental-repl-await`]: cli.md#--no-experimental-repl-await
773-
[`ERR_DOMAIN_CANNOT_SET_UNCAUGHT_EXCEPTION_CAPTURE`]: errors.md#err_domain_cannot_set_uncaught_exception_capture
774-
[`ERR_INVALID_REPL_INPUT`]: errors.md#err_invalid_repl_input
775739
[`curl(1)`]: https://curl.haxx.se/docs/manpage.html
776-
[`domain`]: domain.md
777-
[`process.setUncaughtExceptionCaptureCallback()`]: process.md#processsetuncaughtexceptioncapturecallbackfn
778740
[`readline.InterfaceCompleter`]: readline.md#use-of-the-completer-function
779741
[`repl.ReplServer`]: #class-replserver
780742
[`repl.start()`]: #replstartoptions

lib/internal/errors.js

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1502,7 +1502,6 @@ E('ERR_INVALID_PROTOCOL',
15021502
TypeError);
15031503
E('ERR_INVALID_REPL_EVAL_CONFIG',
15041504
'Cannot specify both "breakEvalOnSigint" and "eval" for REPL', TypeError);
1505-
E('ERR_INVALID_REPL_INPUT', '%s', TypeError);
15061505
E('ERR_INVALID_RETURN_PROPERTY', (input, name, prop, value) => {
15071506
return `Expected a valid ${input} to be returned for the "${prop}" from the` +
15081507
` "${name}" hook but got ${determineSpecificType(value)}.`;

lib/repl.js

Lines changed: 73 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@ const {
6060
ArrayPrototypeUnshift,
6161
Boolean,
6262
Error: MainContextError,
63+
FunctionPrototypeApply,
6364
FunctionPrototypeBind,
6465
JSONStringify,
6566
MathMaxApply,
@@ -76,9 +77,9 @@ const {
7677
ReflectApply,
7778
RegExp,
7879
RegExpPrototypeExec,
80+
SafeMap,
7981
SafePromiseRace,
8082
SafeSet,
81-
SafeWeakSet,
8283
StringPrototypeCharAt,
8384
StringPrototypeCodePointAt,
8485
StringPrototypeEndsWith,
@@ -138,7 +139,6 @@ ArrayPrototypeForEach(
138139
BuiltinModule.getSchemeOnlyModuleNames(),
139140
(lib) => ArrayPrototypePush(nodeSchemeBuiltinLibs, `node:${lib}`),
140141
);
141-
const domain = require('domain');
142142
let debug = require('internal/util/debuglog').debuglog('repl', (fn) => {
143143
debug = fn;
144144
});
@@ -147,7 +147,6 @@ const {
147147
codes: {
148148
ERR_CANNOT_WATCH_SIGINT,
149149
ERR_INVALID_REPL_EVAL_CONFIG,
150-
ERR_INVALID_REPL_INPUT,
151150
ERR_MISSING_ARGS,
152151
ERR_SCRIPT_EXECUTION_INTERRUPTED,
153152
},
@@ -191,6 +190,7 @@ const {
191190
const {
192191
makeContextifyScript,
193192
} = require('internal/vm');
193+
const { createHook } = require('async_hooks');
194194
let nextREPLResourceNumber = 1;
195195
// This prevents v8 code cache from getting confused and using a different
196196
// cache from a resource of the same name
@@ -205,13 +205,43 @@ const globalBuiltins =
205205
new SafeSet(vm.runInNewContext('Object.getOwnPropertyNames(globalThis)'));
206206

207207
const parentModule = module;
208-
const domainSet = new SafeWeakSet();
209208

210209
const kBufferedCommandSymbol = Symbol('bufferedCommand');
211210
const kContextId = Symbol('contextId');
212211
const kLoadingSymbol = Symbol('loading');
212+
const kListeningREPLs = new SafeSet();
213+
const kAsyncREPLMap = new SafeMap();
214+
let kActiveREPL;
215+
const kAsyncHook = createHook({
216+
init(asyncId) {
217+
if (kActiveREPL) {
218+
kAsyncREPLMap.set(asyncId, kActiveREPL);
219+
}
220+
},
221+
222+
before(asyncId) {
223+
kActiveREPL = kAsyncREPLMap.get(asyncId) || kActiveREPL;
224+
},
225+
226+
destroy(asyncId) {
227+
kAsyncREPLMap.delete(asyncId);
228+
},
229+
});
230+
231+
let kHasSetUncaughtListener = false;
213232

214-
let addedNewListener = false;
233+
function handleUncaughtException(er) {
234+
kActiveREPL?._onEvalError(er);
235+
}
236+
237+
function removeListeningREPL(repl) {
238+
kListeningREPLs.delete(repl);
239+
if (kListeningREPLs.size === 0) {
240+
kAsyncHook.disable();
241+
kHasSetUncaughtListener = false;
242+
process.off('uncaughtException', handleUncaughtException);
243+
}
244+
}
215245

216246
try {
217247
// Hack for require.resolve("./relative") to work properly.
@@ -346,7 +376,6 @@ function REPLServer(prompt,
346376

347377
this.allowBlockingCompletions = !!options.allowBlockingCompletions;
348378
this.useColors = !!options.useColors;
349-
this._domain = options.domain || domain.create();
350379
this.useGlobal = !!useGlobal;
351380
this.ignoreUndefined = !!ignoreUndefined;
352381
this.replMode = replMode || module.exports.REPL_MODE_SLOPPY;
@@ -369,28 +398,8 @@ function REPLServer(prompt,
369398
// It is possible to introspect the running REPL accessing this variable
370399
// from inside the REPL. This is useful for anyone working on the REPL.
371400
module.exports.repl = this;
372-
} else if (!addedNewListener) {
373-
// Add this listener only once and use a WeakSet that contains the REPLs
374-
// domains. Otherwise we'd have to add a single listener to each REPL
375-
// instance and that could trigger the `MaxListenersExceededWarning`.
376-
process.prependListener('newListener', (event, listener) => {
377-
if (event === 'uncaughtException' &&
378-
process.domain &&
379-
listener.name !== 'domainUncaughtExceptionClear' &&
380-
domainSet.has(process.domain)) {
381-
// Throw an error so that the event will not be added and the current
382-
// domain takes over. That way the user is notified about the error
383-
// and the current code evaluation is stopped, just as any other code
384-
// that contains an error.
385-
throw new ERR_INVALID_REPL_INPUT(
386-
'Listeners for `uncaughtException` cannot be used in the REPL');
387-
}
388-
});
389-
addedNewListener = true;
390401
}
391402

392-
domainSet.add(this._domain);
393-
394403
const savedRegExMatches = ['', '', '', '', '', '', '', '', '', ''];
395404
const sep = '\u0000\u0000\u0000';
396405
const regExMatcher = new RegExp(`^${sep}(.*)${sep}(.*)${sep}(.*)${sep}(.*)` +
@@ -612,13 +621,8 @@ function REPLServer(prompt,
612621
}
613622
} catch (e) {
614623
err = e;
615-
616-
if (process.domain) {
617-
debug('not recoverable, send to domain');
618-
process.domain.emit('error', err);
619-
process.domain.exit();
620-
return;
621-
}
624+
self._onEvalError(e);
625+
return;
622626
}
623627

624628
if (awaitPromise && !err) {
@@ -644,10 +648,8 @@ function REPLServer(prompt,
644648
const result = (await promise)?.value;
645649
finishExecution(null, result);
646650
} catch (err) {
647-
if (err && process.domain) {
648-
debug('not recoverable, send to domain');
649-
process.domain.emit('error', err);
650-
process.domain.exit();
651+
if (err) {
652+
self._onEvalError(err);
651653
return;
652654
}
653655
finishExecution(err);
@@ -665,10 +667,17 @@ function REPLServer(prompt,
665667
}
666668
}
667669

668-
self.eval = self._domain.bind(eval_);
670+
self.eval = function(...args) {
671+
try {
672+
kActiveREPL = this;
673+
FunctionPrototypeApply(eval_, this, args);
674+
} catch (e) {
675+
self._onEvalError(e);
676+
}
677+
};
669678

670-
self._domain.on('error', function debugDomainError(e) {
671-
debug('domain error');
679+
self._onEvalError = function _onEvalError(e) {
680+
debug('eval error');
672681
let errStack = '';
673682

674683
if (typeof e === 'object' && e !== null) {
@@ -696,11 +705,6 @@ function REPLServer(prompt,
696705
});
697706
decorateErrorStack(e);
698707

699-
if (e.domainThrown) {
700-
delete e.domain;
701-
delete e.domainThrown;
702-
}
703-
704708
if (isError(e)) {
705709
if (e.stack) {
706710
if (e.name === 'SyntaxError') {
@@ -740,10 +744,13 @@ function REPLServer(prompt,
740744
self.lastError = e;
741745
}
742746

743-
if (options[kStandaloneREPL] &&
744-
process.listenerCount('uncaughtException') !== 0) {
747+
if (options[kStandaloneREPL] && process.listenerCount('uncaughtException') > 1) {
745748
process.nextTick(() => {
746-
process.emit('uncaughtException', e);
749+
const listeners = process.listeners('uncaughtException');
750+
for (let i = 0; i < listeners.length; i++) {
751+
const listener = listeners[i];
752+
if (listener !== handleUncaughtException) listener(e);
753+
}
747754
self.clearBufferedCommand();
748755
self.lines.level = [];
749756
self.displayPrompt();
@@ -778,7 +785,14 @@ function REPLServer(prompt,
778785
self.lines.level = [];
779786
self.displayPrompt();
780787
}
781-
});
788+
};
789+
kListeningREPLs.add(self);
790+
791+
if (!kHasSetUncaughtListener) {
792+
kAsyncHook.enable();
793+
process.on('uncaughtException', handleUncaughtException);
794+
kHasSetUncaughtListener = true;
795+
}
782796

783797
self.clearBufferedCommand();
784798

@@ -951,7 +965,7 @@ function REPLServer(prompt,
951965
self.displayPrompt();
952966
return;
953967
}
954-
self._domain.emit('error', e.err || e);
968+
self._onEvalError(e.err || e);
955969
}
956970

957971
// Clear buffer if no SyntaxErrors
@@ -971,8 +985,7 @@ function REPLServer(prompt,
971985
self.output.write(self.writer(ret) + '\n');
972986
}
973987

974-
// Display prompt again (unless we already did by emitting the 'error'
975-
// event on the domain instance).
988+
// Display prompt again
976989
if (!e) {
977990
self.displayPrompt();
978991
}
@@ -1082,15 +1095,17 @@ REPLServer.prototype.clearBufferedCommand = function clearBufferedCommand() {
10821095
REPLServer.prototype.close = function close() {
10831096
if (this.terminal && this._flushing && !this._closingOnFlush) {
10841097
this._closingOnFlush = true;
1085-
this.once('flushHistory', () =>
1086-
ReflectApply(Interface.prototype.close, this, []),
1087-
);
1098+
this.once('flushHistory', () => {
1099+
removeListeningREPL(this);
1100+
ReflectApply(Interface.prototype.close, this, []);
1101+
});
10881102

10891103
return;
10901104
}
1091-
process.nextTick(() =>
1092-
ReflectApply(Interface.prototype.close, this, []),
1093-
);
1105+
process.nextTick(() => {
1106+
removeListeningREPL(this);
1107+
ReflectApply(Interface.prototype.close, this, []);
1108+
});
10941109
};
10951110

10961111
REPLServer.prototype.createContext = function() {

test/fixtures/repl-tab-completion-nested-repls.js

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -32,9 +32,7 @@ const putIn = new ArrayStream();
3232
const testMe = repl.start('', putIn);
3333

3434
// Some errors are passed to the domain, but do not callback.
35-
testMe._domain.on('error', function(err) {
36-
throw err;
37-
});
35+
testMe._onEvalError = (err) => { throw err };
3836

3937
// Nesting of structures causes REPL to use a nested REPL for completion.
4038
putIn.run([

0 commit comments

Comments
 (0)