Skip to content
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

repl: don't use deprecated domain module #55204

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 7 additions & 7 deletions doc/api/errors.md
Original file line number Diff line number Diff line change
Expand Up @@ -2016,13 +2016,6 @@ An invalid `options.protocol` was passed to `http.request()`.
Both `breakEvalOnSigint` and `eval` options were set in the [`REPL`][] config,
which is not supported.

<a id="ERR_INVALID_REPL_INPUT"></a>

### `ERR_INVALID_REPL_INPUT`

The input may not be used in the [`REPL`][]. The conditions under which this
error is used are described in the [`REPL`][] documentation.

RedYetiDev marked this conversation as resolved.
Show resolved Hide resolved
<a id="ERR_INVALID_RETURN_PROPERTY"></a>

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

<a id="ERR_INVALID_REPL_INPUT"></a>

### `ERR_INVALID_REPL_INPUT`

The input may not be used in the [`REPL`][]. The conditions under which this
error is used are described in the [`REPL`][] documentation.

<a id="ERR_INVALID_TRANSFER_OBJECT"></a>

### `ERR_INVALID_TRANSFER_OBJECT`
Expand Down
38 changes: 0 additions & 38 deletions doc/api/repl.md
Original file line number Diff line number Diff line change
Expand Up @@ -147,39 +147,6 @@ global or scoped variable, the input `fs` will be evaluated on-demand as
> fs.createReadStream('./some/file');
```

#### Global uncaught exceptions

<!-- YAML
changes:
- version: v12.3.0
pr-url: https://github.com/nodejs/node/pull/27151
description: The `'uncaughtException'` event is from now on triggered if the
repl is used as standalone program.
-->

The REPL uses the [`domain`][] module to catch all uncaught exceptions for that
REPL session.

This use of the [`domain`][] module in the REPL has these side effects:

* Uncaught exceptions only emit the [`'uncaughtException'`][] event in the
standalone REPL. Adding a listener for this event in a REPL within
another Node.js program results in [`ERR_INVALID_REPL_INPUT`][].

```js
const r = repl.start();

r.write('process.on("uncaughtException", () => console.log("Foobar"));\n');
// Output stream includes:
// TypeError [ERR_INVALID_REPL_INPUT]: Listeners for `uncaughtException`
// cannot be used in the REPL

r.close();
```

* Trying to use [`process.setUncaughtExceptionCaptureCallback()`][] throws
an [`ERR_DOMAIN_CANNOT_SET_UNCAUGHT_EXCEPTION_CAPTURE`][] error.

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

<!-- YAML
Expand Down Expand Up @@ -768,13 +735,8 @@ avoiding open network interfaces.

[TTY keybindings]: readline.md#tty-keybindings
[ZSH]: https://en.wikipedia.org/wiki/Z_shell
[`'uncaughtException'`]: process.md#event-uncaughtexception
[`--no-experimental-repl-await`]: cli.md#--no-experimental-repl-await
[`ERR_DOMAIN_CANNOT_SET_UNCAUGHT_EXCEPTION_CAPTURE`]: errors.md#err_domain_cannot_set_uncaught_exception_capture
[`ERR_INVALID_REPL_INPUT`]: errors.md#err_invalid_repl_input
[`curl(1)`]: https://curl.haxx.se/docs/manpage.html
[`domain`]: domain.md
[`process.setUncaughtExceptionCaptureCallback()`]: process.md#processsetuncaughtexceptioncapturecallbackfn
[`readline.InterfaceCompleter`]: readline.md#use-of-the-completer-function
[`repl.ReplServer`]: #class-replserver
[`repl.start()`]: #replstartoptions
Expand Down
1 change: 0 additions & 1 deletion lib/internal/errors.js
Original file line number Diff line number Diff line change
Expand Up @@ -1502,7 +1502,6 @@ E('ERR_INVALID_PROTOCOL',
TypeError);
E('ERR_INVALID_REPL_EVAL_CONFIG',
'Cannot specify both "breakEvalOnSigint" and "eval" for REPL', TypeError);
E('ERR_INVALID_REPL_INPUT', '%s', TypeError);
E('ERR_INVALID_RETURN_PROPERTY', (input, name, prop, value) => {
return `Expected a valid ${input} to be returned for the "${prop}" from the` +
` "${name}" hook but got ${determineSpecificType(value)}.`;
Expand Down
131 changes: 73 additions & 58 deletions lib/repl.js
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ const {
ArrayPrototypeUnshift,
Boolean,
Error: MainContextError,
FunctionPrototypeApply,
FunctionPrototypeBind,
JSONStringify,
MathMaxApply,
Expand All @@ -76,9 +77,9 @@ const {
ReflectApply,
RegExp,
RegExpPrototypeExec,
SafeMap,
SafePromiseRace,
SafeSet,
SafeWeakSet,
StringPrototypeCharAt,
StringPrototypeCodePointAt,
StringPrototypeEndsWith,
Expand Down Expand Up @@ -138,7 +139,6 @@ ArrayPrototypeForEach(
BuiltinModule.getSchemeOnlyModuleNames(),
(lib) => ArrayPrototypePush(nodeSchemeBuiltinLibs, `node:${lib}`),
);
const domain = require('domain');
let debug = require('internal/util/debuglog').debuglog('repl', (fn) => {
debug = fn;
});
Expand All @@ -147,7 +147,6 @@ const {
codes: {
ERR_CANNOT_WATCH_SIGINT,
ERR_INVALID_REPL_EVAL_CONFIG,
ERR_INVALID_REPL_INPUT,
ERR_MISSING_ARGS,
ERR_SCRIPT_EXECUTION_INTERRUPTED,
},
Expand Down Expand Up @@ -191,6 +190,7 @@ const {
const {
makeContextifyScript,
} = require('internal/vm');
const { createHook } = require('async_hooks');
let nextREPLResourceNumber = 1;
// This prevents v8 code cache from getting confused and using a different
// cache from a resource of the same name
Expand All @@ -205,13 +205,43 @@ const globalBuiltins =
new SafeSet(vm.runInNewContext('Object.getOwnPropertyNames(globalThis)'));

const parentModule = module;
const domainSet = new SafeWeakSet();

const kBufferedCommandSymbol = Symbol('bufferedCommand');
const kContextId = Symbol('contextId');
const kLoadingSymbol = Symbol('loading');
const kListeningREPLs = new SafeSet();
const kAsyncREPLMap = new SafeMap();
let kActiveREPL;
const kAsyncHook = createHook({
init(asyncId) {
if (kActiveREPL) {
kAsyncREPLMap.set(asyncId, kActiveREPL);
}
},

before(asyncId) {
kActiveREPL = kAsyncREPLMap.get(asyncId) || kActiveREPL;
},

destroy(asyncId) {
kAsyncREPLMap.delete(asyncId);
},
});

let kHasSetUncaughtListener = false;

let addedNewListener = false;
function handleUncaughtException(er) {
kActiveREPL?._onEvalError(er);
}

function removeListeningREPL(repl) {
kListeningREPLs.delete(repl);
if (kListeningREPLs.size === 0) {
kAsyncHook.disable();
kHasSetUncaughtListener = false;
process.off('uncaughtException', handleUncaughtException);
}
}

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

this.allowBlockingCompletions = !!options.allowBlockingCompletions;
this.useColors = !!options.useColors;
this._domain = options.domain || domain.create();
this.useGlobal = !!useGlobal;
this.ignoreUndefined = !!ignoreUndefined;
this.replMode = replMode || module.exports.REPL_MODE_SLOPPY;
Expand All @@ -369,28 +398,8 @@ function REPLServer(prompt,
// It is possible to introspect the running REPL accessing this variable
// from inside the REPL. This is useful for anyone working on the REPL.
module.exports.repl = this;
} else if (!addedNewListener) {
// Add this listener only once and use a WeakSet that contains the REPLs
// domains. Otherwise we'd have to add a single listener to each REPL
// instance and that could trigger the `MaxListenersExceededWarning`.
process.prependListener('newListener', (event, listener) => {
if (event === 'uncaughtException' &&
process.domain &&
listener.name !== 'domainUncaughtExceptionClear' &&
domainSet.has(process.domain)) {
// Throw an error so that the event will not be added and the current
// domain takes over. That way the user is notified about the error
// and the current code evaluation is stopped, just as any other code
// that contains an error.
throw new ERR_INVALID_REPL_INPUT(
'Listeners for `uncaughtException` cannot be used in the REPL');
}
});
addedNewListener = true;
}

domainSet.add(this._domain);

const savedRegExMatches = ['', '', '', '', '', '', '', '', '', ''];
const sep = '\u0000\u0000\u0000';
const regExMatcher = new RegExp(`^${sep}(.*)${sep}(.*)${sep}(.*)${sep}(.*)` +
Expand Down Expand Up @@ -612,13 +621,8 @@ function REPLServer(prompt,
}
} catch (e) {
err = e;

if (process.domain) {
debug('not recoverable, send to domain');
process.domain.emit('error', err);
process.domain.exit();
return;
}
self._onEvalError(e);
return;
}

if (awaitPromise && !err) {
Expand All @@ -644,10 +648,8 @@ function REPLServer(prompt,
const result = (await promise)?.value;
finishExecution(null, result);
} catch (err) {
if (err && process.domain) {
debug('not recoverable, send to domain');
process.domain.emit('error', err);
process.domain.exit();
if (err) {
self._onEvalError(err);
return;
}
finishExecution(err);
Expand All @@ -665,10 +667,17 @@ function REPLServer(prompt,
}
}

self.eval = self._domain.bind(eval_);
self.eval = function(...args) {
try {
kActiveREPL = this;
FunctionPrototypeApply(eval_, this, args);
} catch (e) {
self._onEvalError(e);
}
};

self._domain.on('error', function debugDomainError(e) {
debug('domain error');
self._onEvalError = function _onEvalError(e) {
debug('eval error');
let errStack = '';

if (typeof e === 'object' && e !== null) {
Expand Down Expand Up @@ -696,11 +705,6 @@ function REPLServer(prompt,
});
decorateErrorStack(e);

if (e.domainThrown) {
delete e.domain;
delete e.domainThrown;
}

if (isError(e)) {
if (e.stack) {
if (e.name === 'SyntaxError') {
Expand Down Expand Up @@ -740,10 +744,13 @@ function REPLServer(prompt,
self.lastError = e;
}

if (options[kStandaloneREPL] &&
process.listenerCount('uncaughtException') !== 0) {
if (options[kStandaloneREPL] && process.listenerCount('uncaughtException') > 1) {
process.nextTick(() => {
process.emit('uncaughtException', e);
const listeners = process.listeners('uncaughtException');
for (let i = 0; i < listeners.length; i++) {
const listener = listeners[i];
if (listener !== handleUncaughtException) listener(e);
}
self.clearBufferedCommand();
self.lines.level = [];
self.displayPrompt();
Expand Down Expand Up @@ -778,7 +785,14 @@ function REPLServer(prompt,
self.lines.level = [];
self.displayPrompt();
}
});
};
kListeningREPLs.add(self);

if (!kHasSetUncaughtListener) {
kAsyncHook.enable();
process.on('uncaughtException', handleUncaughtException);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is still leaking things in the process object, and domain is a much safer way to do so anyway.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

and domain is a much safer way to do so anyway.

I disagree. This is different than the domain. The domain bypasses the listener and inserts a callback before these listeners can even be called. This merely adds a listener, and later removes it. How is that "leaking"? The attachment to the process object is controlled.

kHasSetUncaughtListener = true;
}

self.clearBufferedCommand();

Expand Down Expand Up @@ -951,7 +965,7 @@ function REPLServer(prompt,
self.displayPrompt();
return;
}
self._domain.emit('error', e.err || e);
self._onEvalError(e.err || e);
}

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

// Display prompt again (unless we already did by emitting the 'error'
// event on the domain instance).
// Display prompt again
if (!e) {
self.displayPrompt();
}
Expand Down Expand Up @@ -1082,15 +1095,17 @@ REPLServer.prototype.clearBufferedCommand = function clearBufferedCommand() {
REPLServer.prototype.close = function close() {
if (this.terminal && this._flushing && !this._closingOnFlush) {
this._closingOnFlush = true;
this.once('flushHistory', () =>
ReflectApply(Interface.prototype.close, this, []),
);
this.once('flushHistory', () => {
removeListeningREPL(this);
ReflectApply(Interface.prototype.close, this, []);
});

return;
}
process.nextTick(() =>
ReflectApply(Interface.prototype.close, this, []),
);
process.nextTick(() => {
removeListeningREPL(this);
ReflectApply(Interface.prototype.close, this, []);
});
};

REPLServer.prototype.createContext = function() {
Expand Down
4 changes: 1 addition & 3 deletions test/fixtures/repl-tab-completion-nested-repls.js
Original file line number Diff line number Diff line change
Expand Up @@ -32,9 +32,7 @@ const putIn = new ArrayStream();
const testMe = repl.start('', putIn);

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

// Nesting of structures causes REPL to use a nested REPL for completion.
putIn.run([
Expand Down
Loading
Loading