repl: don't use deprecated `domain` module

This commit is contained in:
RedYetiDev 2024-10-05 17:35:22 -04:00
parent 73414f34e8
commit b0d6991d96
13 changed files with 158 additions and 260 deletions

View File

@ -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.
<a id="ERR_INVALID_RETURN_PROPERTY"></a>
### `ERR_INVALID_RETURN_PROPERTY`
@ -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`

View File

@ -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
@ -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

View File

@ -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)}.`;

View File

@ -60,6 +60,7 @@ const {
ArrayPrototypeUnshift,
Boolean,
Error: MainContextError,
FunctionPrototypeApply,
FunctionPrototypeBind,
JSONStringify,
MathMaxApply,
@ -76,9 +77,9 @@ const {
ReflectApply,
RegExp,
RegExpPrototypeExec,
SafeMap,
SafePromiseRace,
SafeSet,
SafeWeakSet,
StringPrototypeCharAt,
StringPrototypeCodePointAt,
StringPrototypeEndsWith,
@ -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;
});
@ -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,
},
@ -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
@ -205,13 +205,48 @@ 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);
}
},
let addedNewListener = false;
before(asyncId) {
kActiveREPL = kAsyncREPLMap.get(asyncId) || kActiveREPL;
},
destroy(asyncId) {
kAsyncREPLMap.delete(asyncId);
},
});
let kHasSetUncaughtListener = false;
function handleUncaughtException(er) {
if (kActiveREPL) {
kActiveREPL._onEvalError(er);
// If there are no other event listeners, throw the uncaught exception.
} else if (process.listenerCount('uncaughtException') <= 1) {
throw 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.
@ -346,7 +381,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;
@ -369,28 +403,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}(.*)` +
@ -612,13 +626,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) {
@ -644,10 +653,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);
@ -665,10 +672,24 @@ function REPLServer(prompt,
}
}
self.eval = self._domain.bind(eval_);
self.eval = function(...args) {
kActiveREPL = this;
self._domain.on('error', function debugDomainError(e) {
debug('domain error');
const cb = args[3];
args[3] = (...cbArgs) => {
kActiveREPL = null;
FunctionPrototypeApply(cb, null, cbArgs);
};
try {
FunctionPrototypeApply(eval_, this, args);
} catch (e) {
self._onEvalError(e);
}
};
self._onEvalError = function _onEvalError(e) {
debug('eval error');
let errStack = '';
if (typeof e === 'object' && e !== null) {
@ -696,11 +717,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') {
@ -740,10 +756,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();
@ -778,7 +797,14 @@ function REPLServer(prompt,
self.lines.level = [];
self.displayPrompt();
}
});
};
kListeningREPLs.add(self);
if (!kHasSetUncaughtListener) {
kAsyncHook.enable();
process.on('uncaughtException', handleUncaughtException);
kHasSetUncaughtListener = true;
}
self.clearBufferedCommand();
@ -951,7 +977,7 @@ function REPLServer(prompt,
self.displayPrompt();
return;
}
self._domain.emit('error', e.err || e);
self._onEvalError(e.err || e);
}
// Clear buffer if no SyntaxErrors
@ -971,8 +997,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();
}
@ -1082,15 +1107,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() {

View File

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

View File

@ -1,45 +0,0 @@
// Copyright Joyent, Inc. and other Node contributors.
//
// Permission is hereby granted, free of charge, to any person obtaining a
// copy of this software and associated documentation files (the
// "Software"), to deal in the Software without restriction, including
// without limitation the rights to use, copy, modify, merge, publish,
// distribute, sublicense, and/or sell copies of the Software, and to permit
// persons to whom the Software is furnished to do so, subject to the
// following conditions:
//
// The above copyright notice and this permission notice shall be included
// in all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN
// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE
// USE OR OTHER DEALINGS IN THE SOFTWARE.
'use strict';
require('../common');
const ArrayStream = require('../common/arraystream');
const repl = require('repl');
const putIn = new ArrayStream();
repl.start('', putIn);
putIn.write = function(data) {
// Don't use assert for this because the domain might catch it, and
// give a false negative. Don't throw, just print and exit.
if (data === 'OK\n') {
console.log('ok');
} else {
console.error(data);
process.exit(1);
}
};
putIn.run([
'require("domain").create().on("error", function() { console.log("OK") })' +
'.run(function() { throw new Error("threw") })',
]);

View File

@ -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() {',

View File

@ -26,7 +26,7 @@ const testMe = repl.start({
});
// Some errors are passed to the domain, but do not callback
testMe._domain.on('error', assert.ifError);
testMe._onEvalError = assert.ifError;
// Tab complete provides built in libs for import()
testMe.complete('import(\'', common.mustCall((error, data) => {

View File

@ -61,7 +61,7 @@ const testMe = repl.start({
});
// Some errors are passed to the domain, but do not callback
testMe._domain.on('error', assert.ifError);
testMe._onEvalError = assert.ifError;
// Tab Complete will not break in an object literal
putIn.run([

View File

@ -11,7 +11,7 @@ const testMe = repl.start('', putIn, function(cmd, context, filename,
callback(null, cmd);
});
testMe._domain.on('error', common.mustNotCall());
testMe._onEvalError = common.mustNotCall();
testMe.complete('', function(err, results) {
assert.strictEqual(err, null);

View File

@ -1,44 +0,0 @@
'use strict';
// This verifies that adding an `uncaughtException` listener in an REPL instance
// does not suppress errors in the whole application. Adding such listener
// should throw.
require('../common');
const ArrayStream = require('../common/arraystream');
const repl = require('repl');
const assert = require('assert');
let accum = '';
const output = new ArrayStream();
output.write = (data) => accum += data.replace('\r', '');
const r = repl.start({
prompt: '',
input: new ArrayStream(),
output,
terminal: false,
useColors: false,
global: false
});
r.write(
'process.nextTick(() => {\n' +
' process.on("uncaughtException", () => console.log("Foo"));\n' +
' throw new TypeError("foobar");\n' +
'});\n'
);
r.write(
'setTimeout(() => {\n' +
' throw new RangeError("abc");\n' +
'}, 1);console.log()\n'
);
r.close();
setTimeout(() => {
const len = process.listenerCount('uncaughtException');
process.removeAllListeners('uncaughtException');
assert.strictEqual(len, 0);
assert.match(accum, /ERR_INVALID_REPL_INPUT.*(?!Type)RangeError: abc/s);
}, 2);

View File

@ -1,77 +1,74 @@
'use strict';
require('../common');
const common = require('../common');
const ArrayStream = require('../common/arraystream');
const assert = require('assert');
const repl = require('repl');
let count = 0;
const results = [];
function run({ command, expected, useColors = false }) {
let accum = '';
// Function to run a single test case
async function runTest({ command, expected, useColors = false }) {
const result = { output: '' };
const output = new ArrayStream();
output.write = (data) => accum += data.replace('\r', '');
// Custom stream to capture REPL output
const replOutput = new ArrayStream();
replOutput.write = (data) => { result.output += data.replace('\r', ''); };
const r = repl.start({
// Start REPL instance
const replInstance = repl.start({
prompt: '',
input: new ArrayStream(),
output,
output: replOutput,
terminal: false,
useColors
useColors,
});
r.write(`${command}\n`);
if (typeof expected === 'string') {
assert.strictEqual(accum, expected);
} else {
assert.match(accum, expected);
}
// Execute the command
replInstance.write(`${command}\n`);
// Verify that the repl is still working as expected.
accum = '';
r.write('1 + 1\n');
// eslint-disable-next-line no-control-regex
assert.strictEqual(accum.replace(/\u001b\[[0-9]+m/g, ''), '2\n');
r.close();
count++;
// Validate output
assert.strictEqual(result.output, expected);
// Store REPL instance for future cleanup
result.replInstance = replInstance;
results.push(result);
}
const tests = [
{
useColors: true,
command: 'x',
expected: 'Uncaught ReferenceError: x is not defined\n'
},
{
useColors: true,
command: 'throw { foo: "test" }',
expected: "Uncaught { foo: \x1B[32m'test'\x1B[39m }\n"
},
{
command: 'process.on("uncaughtException", () => console.log("Foobar"));\n',
expected: /^Uncaught:\nTypeError \[ERR_INVALID_REPL_INPUT]: Listeners for `/
},
{
command: 'x;\n',
expected: 'Uncaught ReferenceError: x is not defined\n'
},
{
command: 'process.on("uncaughtException", () => console.log("Foobar"));' +
'console.log("Baz");\n',
expected: /^Uncaught:\nTypeError \[ERR_INVALID_REPL_INPUT]: Listeners for `/
},
{
command: 'console.log("Baz");' +
'process.on("uncaughtException", () => console.log("Foobar"));\n',
expected: /^Baz\nUncaught:\nTypeError \[ERR_INVALID_REPL_INPUT]:.*uncaughtException/
},
// Test cases
const testCases = [
{ useColors: true, command: 'x', expected: 'Uncaught ReferenceError: x is not defined\n' },
{ useColors: true, command: 'throw { foo: "test" }', expected: "Uncaught { foo: \x1B[32m'test'\x1B[39m }\n" },
{ command: 'x;\n', expected: 'Uncaught ReferenceError: x is not defined\n' },
];
process.on('exit', () => {
// To actually verify that the test passed we have to make sure no
// `uncaughtException` listeners exist anymore.
process.removeAllListeners('uncaughtException');
assert.strictEqual(count, tests.length);
});
// Execute tests
testCases.forEach(runTest);
tests.forEach(run);
// Verify all tests ran
assert.strictEqual(results.length, testCases.length);
// Check 'uncaughtException' listener count
assert.strictEqual(process.listenerCount('uncaughtException'), 1);
// Test uncaught exception handling
const errorToThrow = new Error('Thrown');
process.once('uncaughtException', common.mustCall((err) => {
assert.strictEqual(err, errorToThrow);
}));
// Trigger uncaught exception
process.nextTick(() => { throw errorToThrow; });
// Cleanup
setTimeout(common.mustCall(() => {
results.forEach(({ replInstance }) => {
replInstance.close();
});
setTimeout(common.mustCall(() => {
results.forEach(({ output }) => {
assert.doesNotMatch(output, /Uncaught Error: Thrown/);
});
assert.strictEqual(process.listenerCount('uncaughtException'), 0);
}), 100);
}), 100);

View File

@ -242,6 +242,10 @@ function initRepl(mode, useGlobal) {
}
function assertOutput(output, expected) {
const lines = output.accum.trim().split('\n');
const lines = output.accum
.trim()
.split('\n')
// Remove stack trace
.filter((line) => !line.trim().startsWith('at'));
assert.deepStrictEqual(lines, expected);
}