Skip to content

Commit 5f6d5ec

Browse files
committed
repl: don't use deprecated domain module
1 parent 89a2f56 commit 5f6d5ec

10 files changed

+85
-189
lines changed

doc/api/repl.md

Lines changed: 4 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -157,28 +157,11 @@ changes:
157157
repl is used as standalone program.
158158
-->
159159

160-
The REPL uses the [`domain`][] module to catch all uncaught exceptions for that
161-
REPL session.
160+
The REPL uses the [`process.setUncaughtExceptionCaptureCallback()`][] to catch all uncaught exceptions for that
161+
REPL session. For more information on potential side effects, refer to it's documentation.
162162

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.
163+
When trying to use [`process.setUncaughtExceptionCaptureCallback()`][] while a REPL
164+
is active, [`ERR_INVALID_REPL_INPUT`][] will be thrown.
182165

183166
#### Assignment of the `_` (underscore) variable
184167

@@ -768,12 +751,9 @@ avoiding open network interfaces.
768751

769752
[TTY keybindings]: readline.md#tty-keybindings
770753
[ZSH]: https://en.wikipedia.org/wiki/Z_shell
771-
[`'uncaughtException'`]: process.md#event-uncaughtexception
772754
[`--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
774755
[`ERR_INVALID_REPL_INPUT`]: errors.md#err_invalid_repl_input
775756
[`curl(1)`]: https://curl.haxx.se/docs/manpage.html
776-
[`domain`]: domain.md
777757
[`process.setUncaughtExceptionCaptureCallback()`]: process.md#processsetuncaughtexceptioncapturecallbackfn
778758
[`readline.InterfaceCompleter`]: readline.md#use-of-the-completer-function
779759
[`repl.ReplServer`]: #class-replserver

lib/repl.js

Lines changed: 75 additions & 54 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,
@@ -118,6 +119,7 @@ const { inspect } = require('internal/util/inspect');
118119
const vm = require('vm');
119120

120121
const { runInThisContext, runInContext } = vm.Script.prototype;
122+
const { setUncaughtExceptionCaptureCallback } = process;
121123

122124
const path = require('path');
123125
const fs = require('fs');
@@ -138,7 +140,6 @@ ArrayPrototypeForEach(
138140
BuiltinModule.getSchemeOnlyModuleNames(),
139141
(lib) => ArrayPrototypePush(nodeSchemeBuiltinLibs, `node:${lib}`),
140142
);
141-
const domain = require('domain');
142143
let debug = require('internal/util/debuglog').debuglog('repl', (fn) => {
143144
debug = fn;
144145
});
@@ -191,6 +192,7 @@ const {
191192
const {
192193
makeContextifyScript,
193194
} = require('internal/vm');
195+
const { createHook } = require('async_hooks');
194196
let nextREPLResourceNumber = 1;
195197
// This prevents v8 code cache from getting confused and using a different
196198
// cache from a resource of the same name
@@ -205,13 +207,44 @@ const globalBuiltins =
205207
new SafeSet(vm.runInNewContext('Object.getOwnPropertyNames(globalThis)'));
206208

207209
const parentModule = module;
208-
const domainSet = new SafeWeakSet();
209210

210211
const kBufferedCommandSymbol = Symbol('bufferedCommand');
211212
const kContextId = Symbol('contextId');
212213
const kLoadingSymbol = Symbol('loading');
214+
const kListeningREPLs = new SafeSet();
215+
const kAsyncREPLMap = new SafeMap();
216+
let kActiveREPL;
217+
const kAsyncHook = createHook({
218+
init(asyncId) {
219+
if (kActiveREPL) {
220+
kAsyncREPLMap.set(asyncId, kActiveREPL);
221+
}
222+
},
223+
224+
before(asyncId) {
225+
kActiveREPL = kAsyncREPLMap.get(asyncId) || kActiveREPL;
226+
},
213227

214-
let addedNewListener = false;
228+
destroy(asyncId) {
229+
kAsyncREPLMap.delete(asyncId);
230+
},
231+
});
232+
233+
let kHasSetUncaughtListener = false;
234+
235+
function handleUncaughtException(er) {
236+
kActiveREPL?._onEvalError(er);
237+
}
238+
239+
function removeListeningREPL(repl) {
240+
kListeningREPLs.delete(repl);
241+
if (kListeningREPLs.size === 0) {
242+
kAsyncHook.disable();
243+
kHasSetUncaughtListener = false;
244+
setUncaughtExceptionCaptureCallback(null);
245+
process.setUncaughtExceptionCaptureCallback = setUncaughtExceptionCaptureCallback;
246+
}
247+
}
215248

216249
try {
217250
// Hack for require.resolve("./relative") to work properly.
@@ -347,7 +380,6 @@ function REPLServer(prompt,
347380

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

393-
domainSet.add(this._domain);
394-
395407
const savedRegExMatches = ['', '', '', '', '', '', '', '', '', ''];
396408
const sep = '\u0000\u0000\u0000';
397409
const regExMatcher = new RegExp(`^${sep}(.*)${sep}(.*)${sep}(.*)${sep}(.*)` +
@@ -613,13 +625,8 @@ function REPLServer(prompt,
613625
}
614626
} catch (e) {
615627
err = e;
616-
617-
if (process.domain) {
618-
debug('not recoverable, send to domain');
619-
process.domain.emit('error', err);
620-
process.domain.exit();
621-
return;
622-
}
628+
self._onEvalError(e);
629+
return;
623630
}
624631

625632
if (awaitPromise && !err) {
@@ -645,10 +652,8 @@ function REPLServer(prompt,
645652
const result = (await promise)?.value;
646653
finishExecution(null, result);
647654
} catch (err) {
648-
if (err && process.domain) {
649-
debug('not recoverable, send to domain');
650-
process.domain.emit('error', err);
651-
process.domain.exit();
655+
if (err) {
656+
self._onEvalError(err);
652657
return;
653658
}
654659
finishExecution(err);
@@ -666,10 +671,17 @@ function REPLServer(prompt,
666671
}
667672
}
668673

669-
self.eval = self._domain.bind(eval_);
674+
self.eval = function(...args) {
675+
try {
676+
kActiveREPL = this;
677+
FunctionPrototypeApply(eval_, this, args);
678+
} catch (e) {
679+
self._onEvalError(e);
680+
}
681+
};
670682

671-
self._domain.on('error', function debugDomainError(e) {
672-
debug('domain error');
683+
self._onEvalError = function _onEvalError(e) {
684+
debug('eval error');
673685
let errStack = '';
674686

675687
if (typeof e === 'object' && e !== null) {
@@ -697,11 +709,6 @@ function REPLServer(prompt,
697709
});
698710
decorateErrorStack(e);
699711

700-
if (e.domainThrown) {
701-
delete e.domain;
702-
delete e.domainThrown;
703-
}
704-
705712
if (isError(e)) {
706713
if (e.stack) {
707714
if (e.name === 'SyntaxError') {
@@ -779,7 +786,20 @@ function REPLServer(prompt,
779786
self.lines.level = [];
780787
self.displayPrompt();
781788
}
782-
});
789+
};
790+
kListeningREPLs.add(self);
791+
792+
if (!kHasSetUncaughtListener) {
793+
kAsyncHook.enable();
794+
process.setUncaughtExceptionCaptureCallback = () => {
795+
throw new ERR_INVALID_REPL_INPUT();
796+
};
797+
// Set to null first to prevent ERR_UNCAUGHT_EXCEPTION_CAPTURE_ALREADY_SET from
798+
// being thrown on set.
799+
setUncaughtExceptionCaptureCallback(null);
800+
setUncaughtExceptionCaptureCallback(handleUncaughtException);
801+
kHasSetUncaughtListener = true;
802+
}
783803

784804
self.clearBufferedCommand();
785805

@@ -952,7 +972,7 @@ function REPLServer(prompt,
952972
self.displayPrompt();
953973
return;
954974
}
955-
self._domain.emit('error', e.err || e);
975+
self._onEvalError(e.err || e);
956976
}
957977

958978
// Clear buffer if no SyntaxErrors
@@ -972,8 +992,7 @@ function REPLServer(prompt,
972992
self.output.write(self.writer(ret) + '\n');
973993
}
974994

975-
// Display prompt again (unless we already did by emitting the 'error'
976-
// event on the domain instance).
995+
// Display prompt again
977996
if (!e) {
978997
self.displayPrompt();
979998
}
@@ -1083,15 +1102,17 @@ REPLServer.prototype.clearBufferedCommand = function clearBufferedCommand() {
10831102
REPLServer.prototype.close = function close() {
10841103
if (this.terminal && this._flushing && !this._closingOnFlush) {
10851104
this._closingOnFlush = true;
1086-
this.once('flushHistory', () =>
1087-
ReflectApply(Interface.prototype.close, this, []),
1088-
);
1105+
this.once('flushHistory', () => {
1106+
removeListeningREPL(this);
1107+
ReflectApply(Interface.prototype.close, this, []);
1108+
});
10891109

10901110
return;
10911111
}
1092-
process.nextTick(() =>
1093-
ReflectApply(Interface.prototype.close, this, []),
1094-
);
1112+
process.nextTick(() => {
1113+
removeListeningREPL(this);
1114+
ReflectApply(Interface.prototype.close, this, []);
1115+
});
10951116
};
10961117

10971118
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([

test/parallel/test-repl-domain.js

Lines changed: 0 additions & 45 deletions
This file was deleted.

test/parallel/test-repl-save-load.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,11 +35,11 @@ const putIn = new ArrayStream();
3535
const testMe = repl.start('', putIn);
3636

3737
// Some errors might be passed to the domain.
38-
testMe._domain.on('error', function(reason) {
38+
testMe._onEvalError = function(reason) {
3939
const err = new Error('Test failed');
4040
err.reason = reason;
4141
throw err;
42-
});
42+
};
4343

4444
const testFile = [
4545
'let inner = (function() {',

test/parallel/test-repl-tab-complete-import.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ const testMe = repl.start({
2626
});
2727

2828
// Some errors are passed to the domain, but do not callback
29-
testMe._domain.on('error', assert.ifError);
29+
testMe._onEvalError = assert.ifError;
3030

3131
// Tab complete provides built in libs for import()
3232
testMe.complete('import(\'', common.mustCall((error, data) => {

test/parallel/test-repl-tab-complete.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ const testMe = repl.start({
6161
});
6262

6363
// Some errors are passed to the domain, but do not callback
64-
testMe._domain.on('error', assert.ifError);
64+
testMe._onEvalError = assert.ifError;
6565

6666
// Tab Complete will not break in an object literal
6767
putIn.run([

test/parallel/test-repl-tab.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ const testMe = repl.start('', putIn, function(cmd, context, filename,
1111
callback(null, cmd);
1212
});
1313

14-
testMe._domain.on('error', common.mustNotCall());
14+
testMe._onEvalError = common.mustNotCall();
1515

1616
testMe.complete('', function(err, results) {
1717
assert.strictEqual(err, null);

0 commit comments

Comments
 (0)