Skip to content

Commit

Permalink
repl: don't use deprecated domain module
Browse files Browse the repository at this point in the history
  • Loading branch information
avivkeller committed Oct 5, 2024
1 parent 89a2f56 commit 9be6f17
Show file tree
Hide file tree
Showing 11 changed files with 109 additions and 189 deletions.
28 changes: 4 additions & 24 deletions doc/api/repl.md
Original file line number Diff line number Diff line change
Expand Up @@ -157,28 +157,11 @@ changes:
repl is used as standalone program.
-->

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

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

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

Expand Down Expand Up @@ -768,12 +751,9 @@ 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
Expand Down
129 changes: 75 additions & 54 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 @@ -118,6 +119,7 @@ const { inspect } = require('internal/util/inspect');
const vm = require('vm');

const { runInThisContext, runInContext } = vm.Script.prototype;
const { setUncaughtExceptionCaptureCallback } = process;

const path = require('path');
const fs = require('fs');
Expand All @@ -138,7 +140,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 Down Expand Up @@ -191,6 +192,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 +207,44 @@ 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;
},

let addedNewListener = false;
destroy(asyncId) {
kAsyncREPLMap.delete(asyncId);
},
});

let kHasSetUncaughtListener = false;

function handleUncaughtException(er) {
kActiveREPL?._onEvalError(er);
}

function removeListeningREPL(repl) {
kListeningREPLs.delete(repl);
if (kListeningREPLs.size === 0) {
kAsyncHook.disable();
kHasSetUncaughtListener = false;
setUncaughtExceptionCaptureCallback(null);
process.setUncaughtExceptionCaptureCallback = setUncaughtExceptionCaptureCallback;
}
}

try {
// Hack for require.resolve("./relative") to work properly.
Expand Down Expand Up @@ -347,7 +380,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 @@ -370,28 +402,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 @@ -613,13 +625,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 @@ -645,10 +652,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 @@ -666,10 +671,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 @@ -697,11 +709,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 @@ -779,7 +786,20 @@ function REPLServer(prompt,
self.lines.level = [];
self.displayPrompt();
}
});
};
kListeningREPLs.add(self);

if (!kHasSetUncaughtListener) {
kAsyncHook.enable();
process.setUncaughtExceptionCaptureCallback = () => {
throw new ERR_INVALID_REPL_INPUT();
};
// Set to null first to prevent ERR_UNCAUGHT_EXCEPTION_CAPTURE_ALREADY_SET from
// being thrown on set.
setUncaughtExceptionCaptureCallback(null);
setUncaughtExceptionCaptureCallback(handleUncaughtException);
kHasSetUncaughtListener = true;
}

self.clearBufferedCommand();

Expand Down Expand Up @@ -952,7 +972,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 @@ -972,8 +992,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 @@ -1083,15 +1102,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
45 changes: 0 additions & 45 deletions test/parallel/test-repl-domain.js

This file was deleted.

24 changes: 24 additions & 0 deletions test/parallel/test-repl-long-timeout-uncaught.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
'use strict';

const common = require('../common');
const repl = require('repl');
const { PassThrough } = require('stream');

// Setup input and output streams
const firstInput = new PassThrough();
const secondInput = new PassThrough();

const firstREPL = repl.start({ input: firstInput, output: new PassThrough() });
const secondREPL = repl.start({ input: secondInput, output: new PassThrough() });

firstREPL._onEvalError = common.mustCall();
secondREPL._onEvalError = common.mustNotCall();

// Write commands to the REPL
firstInput.write('setTimeout(() => { throw 1; }, 10);\n');
secondInput.write('1+1;\n');

setTimeout(() => {
firstREPL.close();
secondREPL.close();
}, 11);
4 changes: 2 additions & 2 deletions test/parallel/test-repl-save-load.js
Original file line number Diff line number Diff line change
Expand Up @@ -35,11 +35,11 @@ const putIn = new ArrayStream();
const testMe = repl.start('', putIn);

// Some errors might be passed to the domain.
testMe._domain.on('error', function(reason) {
testMe._onEvalError = function(reason) {
const err = new Error('Test failed');
err.reason = reason;
throw err;
});
};

const testFile = [
'let inner = (function() {',
Expand Down
Loading

0 comments on commit 9be6f17

Please sign in to comment.