mirror of
https://github.com/nodejs/node.git
synced 2026-10-29 02:35:20 +08:00
repl: remove dependency on domain module
Replace the domain-based error handling with AsyncLocalStorage and setUncaughtExceptionCaptureCallback. This removes the REPL's dependency on the deprecated domain module while preserving all existing behavior: - Synchronous errors during eval are caught and displayed - Async errors (setTimeout, promises, etc.) are caught via the uncaught exception capture callback - Top-level await errors are caught and displayed - The REPL continues operating after errors - Multiple REPL instances can coexist with errors routed correctly Changes: - Use AsyncLocalStorage to track which REPL instance owns an async context, replacing domain's automatic async tracking - Add setupExceptionCapture() to install setUncaughtExceptionCaptureCallback for catching async errors and routing them to the correct REPL - Extract error handling logic into REPLServer.prototype._handleError() - Wrap eval execution in replContext.run() for async context tracking - Update newListener protection to check AsyncLocalStorage context - Throw ERR_INVALID_ARG_VALUE if options.domain is passed PR-URL: https://github.com/nodejs/node/pull/61227 Reviewed-By: Yagiz Nizipli <yagiz@nizipli.com> Reviewed-By: Marco Ippolito <marcoippolito54@gmail.com> Reviewed-By: Rafael Gonzaga <rafael.nunu@hotmail.com>
This commit is contained in:
1 parent
798797aa89
commit
a9da9ffc04
26 files changed
+417
-289
No files matched your search
+46
-2
@@ -736,6 +736,44 @@ generate a core file.
|
||||
|
||||
This feature is not available in [`Worker`][] threads.
|
||||
|
||||
## `process.addUncaughtExceptionCaptureCallback(fn)`
|
||||
|
||||
<!-- YAML
|
||||
added: REPLACEME
|
||||
-->
|
||||
|
||||
> Stability: 1 - Experimental
|
||||
|
||||
* `fn` {Function}
|
||||
|
||||
The `process.addUncaughtExceptionCaptureCallback()` function adds a callback
|
||||
that will be invoked when an uncaught exception occurs, receiving the exception
|
||||
value as its first argument.
|
||||
|
||||
Unlike [`process.setUncaughtExceptionCaptureCallback()`][], this function allows
|
||||
multiple callbacks to be registered and does not conflict with the
|
||||
[`domain`][] module. Callbacks are called in reverse order of registration
|
||||
(most recent first). If a callback returns `true`, subsequent callbacks
|
||||
and the default uncaught exception handling are skipped.
|
||||
|
||||
```mjs
|
||||
import process from 'node:process';
|
||||
|
||||
process.addUncaughtExceptionCaptureCallback((err) => {
|
||||
console.error('Caught exception:', err.message);
|
||||
return true; // Indicates exception was handled
|
||||
});
|
||||
```
|
||||
|
||||
```cjs
|
||||
const process = require('node:process');
|
||||
|
||||
process.addUncaughtExceptionCaptureCallback((err) => {
|
||||
console.error('Caught exception:', err.message);
|
||||
return true; // Indicates exception was handled
|
||||
});
|
||||
```
|
||||
|
||||
## `process.allowedNodeEnvironmentFlags`
|
||||
|
||||
<!-- YAML
|
||||
@@ -4021,6 +4059,11 @@ This implies calling `module.setSourceMapsSupport()` with an option
|
||||
|
||||
<!-- YAML
|
||||
added: v9.3.0
|
||||
changes:
|
||||
- version: REPLACEME
|
||||
pr-url: https://github.com/nodejs/node/pull/61227
|
||||
description: Use `process.addUncaughtExceptionCaptureCallback()` to
|
||||
register multiple callbacks.
|
||||
-->
|
||||
|
||||
* `fn` {Function|null}
|
||||
@@ -4040,8 +4083,8 @@ To unset the capture function,
|
||||
method with a non-`null` argument while another capture function is set will
|
||||
throw an error.
|
||||
|
||||
Using this function is mutually exclusive with using the deprecated
|
||||
[`domain`][] built-in module.
|
||||
To register multiple callbacks that can coexist, use
|
||||
[`process.addUncaughtExceptionCaptureCallback()`][] instead.
|
||||
|
||||
## `process.sourceMapsEnabled`
|
||||
|
||||
@@ -4573,6 +4616,7 @@ cases:
|
||||
[`net.Socket`]: net.md#class-netsocket
|
||||
[`os.constants.dlopen`]: os.md#dlopen-constants
|
||||
[`postMessageToThread()`]: worker_threads.md#worker_threadspostmessagetothreadthreadid-value-transferlist-timeout
|
||||
[`process.addUncaughtExceptionCaptureCallback()`]: #processadduncaughtexceptioncapturecallbackfn
|
||||
[`process.argv`]: #processargv
|
||||
[`process.config`]: #processconfig
|
||||
[`process.execPath`]: #processexecpath
|
||||
|
||||
+2
-18
@@ -40,14 +40,11 @@ const {
|
||||
ReflectApply,
|
||||
SafeMap,
|
||||
SafeWeakMap,
|
||||
StringPrototypeRepeat,
|
||||
Symbol,
|
||||
} = primordials;
|
||||
|
||||
const EventEmitter = require('events');
|
||||
const {
|
||||
ERR_DOMAIN_CALLBACK_NOT_AVAILABLE,
|
||||
ERR_DOMAIN_CANNOT_SET_UNCAUGHT_EXCEPTION_CAPTURE,
|
||||
ERR_UNHANDLED_ERROR,
|
||||
} = require('internal/errors').codes;
|
||||
const { createHook } = require('async_hooks');
|
||||
@@ -119,22 +116,9 @@ const asyncHook = createHook({
|
||||
},
|
||||
});
|
||||
|
||||
// When domains are in use, they claim full ownership of the
|
||||
// uncaught exception capture callback.
|
||||
if (process.hasUncaughtExceptionCaptureCallback()) {
|
||||
throw new ERR_DOMAIN_CALLBACK_NOT_AVAILABLE();
|
||||
}
|
||||
|
||||
// Get the stack trace at the point where `domain` was required.
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
const domainRequireStack = new Error('require(`domain`) at this point').stack;
|
||||
|
||||
// Domain uses the stacking capability of setUncaughtExceptionCaptureCallback
|
||||
// to coexist with other callbacks (e.g., REPL).
|
||||
const { setUncaughtExceptionCaptureCallback } = process;
|
||||
process.setUncaughtExceptionCaptureCallback = function(fn) {
|
||||
const err = new ERR_DOMAIN_CANNOT_SET_UNCAUGHT_EXCEPTION_CAPTURE();
|
||||
err.stack += `\n${StringPrototypeRepeat('-', 40)}\n${domainRequireStack}`;
|
||||
throw err;
|
||||
};
|
||||
|
||||
|
||||
let sendMakeCallbackDeprecation = false;
|
||||
|
||||
@@ -307,6 +307,7 @@ ObjectDefineProperty(process, 'features', {
|
||||
const {
|
||||
onGlobalUncaughtException,
|
||||
setUncaughtExceptionCaptureCallback,
|
||||
addUncaughtExceptionCaptureCallback,
|
||||
hasUncaughtExceptionCaptureCallback,
|
||||
} = require('internal/process/execution');
|
||||
|
||||
@@ -319,6 +320,8 @@ ObjectDefineProperty(process, 'features', {
|
||||
process._fatalException = onGlobalUncaughtException;
|
||||
process.setUncaughtExceptionCaptureCallback =
|
||||
setUncaughtExceptionCaptureCallback;
|
||||
process.addUncaughtExceptionCaptureCallback =
|
||||
addUncaughtExceptionCaptureCallback;
|
||||
process.hasUncaughtExceptionCaptureCallback =
|
||||
hasUncaughtExceptionCaptureCallback;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
'use strict';
|
||||
|
||||
const {
|
||||
ArrayPrototypePush,
|
||||
RegExpPrototypeExec,
|
||||
StringPrototypeIndexOf,
|
||||
StringPrototypeSlice,
|
||||
@@ -17,6 +18,7 @@ const {
|
||||
ERR_UNCAUGHT_EXCEPTION_CAPTURE_ALREADY_SET,
|
||||
},
|
||||
} = require('internal/errors');
|
||||
const { validateFunction } = require('internal/validators');
|
||||
const { pathToFileURL } = require('internal/url');
|
||||
const { exitCodes: { kGenericUserError } } = internalBinding('errors');
|
||||
const {
|
||||
@@ -105,15 +107,18 @@ function evalScript(name, body, breakFirstLine, print, shouldLoadESM = false) {
|
||||
}
|
||||
|
||||
const exceptionHandlerState = {
|
||||
captureFn: null,
|
||||
captureFn: null, // Primary callback (for domain's exclusive use)
|
||||
auxiliaryCallbacks: [], // Auxiliary callbacks (for REPL, etc.) - always called
|
||||
reportFlag: false,
|
||||
};
|
||||
|
||||
function setUncaughtExceptionCaptureCallback(fn) {
|
||||
if (fn === null) {
|
||||
exceptionHandlerState.captureFn = fn;
|
||||
shouldAbortOnUncaughtToggle[0] = 1;
|
||||
process.report.reportOnUncaughtException = exceptionHandlerState.reportFlag;
|
||||
if (exceptionHandlerState.auxiliaryCallbacks.length === 0) {
|
||||
shouldAbortOnUncaughtToggle[0] = 1;
|
||||
process.report.reportOnUncaughtException = exceptionHandlerState.reportFlag;
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (typeof fn !== 'function') {
|
||||
@@ -129,6 +134,21 @@ function setUncaughtExceptionCaptureCallback(fn) {
|
||||
process.report.reportOnUncaughtException = false;
|
||||
}
|
||||
|
||||
// Add an auxiliary callback that coexists with the primary callback.
|
||||
// Auxiliary callbacks are called first; if any returns true, the error is handled.
|
||||
// Otherwise, the primary callback (if set) is called.
|
||||
function addUncaughtExceptionCaptureCallback(fn) {
|
||||
validateFunction(fn, 'fn');
|
||||
if (exceptionHandlerState.auxiliaryCallbacks.length === 0 &&
|
||||
exceptionHandlerState.captureFn === null) {
|
||||
exceptionHandlerState.reportFlag =
|
||||
process.report.reportOnUncaughtException === true;
|
||||
process.report.reportOnUncaughtException = false;
|
||||
shouldAbortOnUncaughtToggle[0] = 0;
|
||||
}
|
||||
ArrayPrototypePush(exceptionHandlerState.auxiliaryCallbacks, fn);
|
||||
}
|
||||
|
||||
function hasUncaughtExceptionCaptureCallback() {
|
||||
return exceptionHandlerState.captureFn !== null;
|
||||
}
|
||||
@@ -154,21 +174,33 @@ function createOnGlobalUncaughtException() {
|
||||
|
||||
const type = fromPromise ? 'unhandledRejection' : 'uncaughtException';
|
||||
process.emit('uncaughtExceptionMonitor', er, type);
|
||||
// Primary callback (e.g., domain) has priority and always handles the exception
|
||||
if (exceptionHandlerState.captureFn !== null) {
|
||||
exceptionHandlerState.captureFn(er);
|
||||
} else if (!process.emit('uncaughtException', er, type)) {
|
||||
// If someone handled it, then great. Otherwise, die in C++ land
|
||||
// since that means that we'll exit the process, emit the 'exit' event.
|
||||
try {
|
||||
if (!process._exiting) {
|
||||
process._exiting = true;
|
||||
process.exitCode = kGenericUserError;
|
||||
process.emit('exit', kGenericUserError);
|
||||
} else {
|
||||
// If no primary callback, try auxiliary callbacks (e.g., REPL)
|
||||
// They must return true to indicate handling
|
||||
let handled = false;
|
||||
for (let i = exceptionHandlerState.auxiliaryCallbacks.length - 1; i >= 0; i--) {
|
||||
if (exceptionHandlerState.auxiliaryCallbacks[i](er) === true) {
|
||||
handled = true;
|
||||
break;
|
||||
}
|
||||
} catch {
|
||||
// Nothing to be done about it at this point.
|
||||
}
|
||||
return false;
|
||||
if (!handled && !process.emit('uncaughtException', er, type)) {
|
||||
// If someone handled it, then great. Otherwise, die in C++ land
|
||||
// since that means that we'll exit the process, emit the 'exit' event.
|
||||
try {
|
||||
if (!process._exiting) {
|
||||
process._exiting = true;
|
||||
process.exitCode = kGenericUserError;
|
||||
process.emit('exit', kGenericUserError);
|
||||
}
|
||||
} catch {
|
||||
// Nothing to be done about it at this point.
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// If we handled an error, then make sure any ticks get processed
|
||||
@@ -477,5 +509,6 @@ module.exports = {
|
||||
evalScript,
|
||||
onGlobalUncaughtException: createOnGlobalUncaughtException(),
|
||||
setUncaughtExceptionCaptureCallback,
|
||||
addUncaughtExceptionCaptureCallback,
|
||||
hasUncaughtExceptionCaptureCallback,
|
||||
};
|
||||
+183
-143
@@ -73,7 +73,6 @@ const {
|
||||
RegExpPrototypeExec,
|
||||
SafePromiseRace,
|
||||
SafeSet,
|
||||
SafeWeakSet,
|
||||
StringPrototypeCharAt,
|
||||
StringPrototypeEndsWith,
|
||||
StringPrototypeIncludes,
|
||||
@@ -115,7 +114,7 @@ const {
|
||||
const { Console } = require('console');
|
||||
const { shouldColorize } = require('internal/util/colors');
|
||||
const CJSModule = require('internal/modules/cjs/loader').Module;
|
||||
const domain = require('domain');
|
||||
const { AsyncLocalStorage } = require('async_hooks');
|
||||
let debug = require('internal/util/debuglog').debuglog('repl', (fn) => {
|
||||
debug = fn;
|
||||
});
|
||||
@@ -123,6 +122,7 @@ const {
|
||||
ErrorPrepareStackTrace,
|
||||
codes: {
|
||||
ERR_CANNOT_WATCH_SIGINT,
|
||||
ERR_INVALID_ARG_VALUE,
|
||||
ERR_INVALID_REPL_EVAL_CONFIG,
|
||||
ERR_INVALID_REPL_INPUT,
|
||||
ERR_MISSING_ARGS,
|
||||
@@ -178,22 +178,47 @@ const {
|
||||
let processTopLevelAwait;
|
||||
|
||||
const parentModule = module;
|
||||
const domainSet = new SafeWeakSet();
|
||||
|
||||
// AsyncLocalStorage to track which REPL instance owns the current async context
|
||||
// This replaces the domain-based tracking for error handling
|
||||
const replContext = new AsyncLocalStorage();
|
||||
let exceptionCaptureSetup = false;
|
||||
|
||||
/**
|
||||
* Sets up the uncaught exception capture callback to route errors
|
||||
* to the appropriate REPL instance. This replaces domain-based error handling.
|
||||
* Uses addUncaughtExceptionCaptureCallback to coexist with the primary
|
||||
* callback (e.g., domain module).
|
||||
*/
|
||||
function setupExceptionCapture() {
|
||||
if (exceptionCaptureSetup) return;
|
||||
|
||||
process.addUncaughtExceptionCaptureCallback((err) => {
|
||||
const store = replContext.getStore();
|
||||
if (store?.replServer && !store.replServer.closed) {
|
||||
store.replServer._handleError(err);
|
||||
return true; // We handled it
|
||||
}
|
||||
// No active REPL context - let other handlers try
|
||||
});
|
||||
|
||||
exceptionCaptureSetup = true;
|
||||
}
|
||||
|
||||
const kBufferedCommandSymbol = Symbol('bufferedCommand');
|
||||
const kLoadingSymbol = Symbol('loading');
|
||||
|
||||
function processNewListener(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');
|
||||
if (event === 'uncaughtException') {
|
||||
const store = replContext.getStore();
|
||||
if (store?.replServer) {
|
||||
// Throw an error so that the event will not be added and the
|
||||
// current REPL handles it. 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');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -341,7 +366,13 @@ class REPLServer extends Interface {
|
||||
|
||||
this.allowBlockingCompletions = !!options.allowBlockingCompletions;
|
||||
this.useColors = !!options.useColors;
|
||||
this._domain = options.domain || domain.create();
|
||||
this._isStandalone = !!options[kStandaloneREPL];
|
||||
|
||||
if (options.domain !== undefined) {
|
||||
throw new ERR_INVALID_ARG_VALUE('options.domain', options.domain,
|
||||
'is no longer supported');
|
||||
}
|
||||
|
||||
this.useGlobal = !!useGlobal;
|
||||
this.ignoreUndefined = !!ignoreUndefined;
|
||||
this.replMode = replMode || module.exports.REPL_MODE_SLOPPY;
|
||||
@@ -370,7 +401,8 @@ class REPLServer extends Interface {
|
||||
this.once('exit', removeProcessNewListener);
|
||||
}
|
||||
|
||||
domainSet.add(this._domain);
|
||||
// Set up exception capture for async error handling
|
||||
setupExceptionCapture();
|
||||
|
||||
const savedRegExMatches = ['', '', '', '', '', '', '', '', '', ''];
|
||||
const sep = '\u0000\u0000\u0000';
|
||||
@@ -591,13 +623,16 @@ class REPLServer extends Interface {
|
||||
}
|
||||
} catch (e) {
|
||||
err = e;
|
||||
|
||||
if (process.domain) {
|
||||
debug('not recoverable, send to domain');
|
||||
// If there's an active domain with error listeners, let it handle the error
|
||||
if (process.domain?.listenerCount('error') > 0) {
|
||||
debug('domain handling error');
|
||||
process.domain.emit('error', err);
|
||||
process.domain.exit();
|
||||
return;
|
||||
}
|
||||
// Handle non-recoverable errors directly
|
||||
debug('not recoverable, handle error');
|
||||
self._handleError(err);
|
||||
return;
|
||||
}
|
||||
|
||||
if (awaitPromise && !err) {
|
||||
@@ -623,13 +658,15 @@ class REPLServer extends Interface {
|
||||
const result = (await promise)?.value;
|
||||
finishExecution(null, result);
|
||||
} catch (err) {
|
||||
if (err && process.domain) {
|
||||
debug('not recoverable, send to domain');
|
||||
// If there's an active domain with error listeners, let it handle the error
|
||||
if (process.domain?.listenerCount('error') > 0) {
|
||||
debug('domain handling async error');
|
||||
process.domain.emit('error', err);
|
||||
process.domain.exit();
|
||||
return;
|
||||
} else {
|
||||
// Handle non-recoverable async errors directly
|
||||
debug('not recoverable, handle error');
|
||||
self._handleError(err);
|
||||
}
|
||||
finishExecution(err);
|
||||
} finally {
|
||||
// Remove prioritized SIGINT listener if it was not called.
|
||||
prioritizedSigintQueue.delete(sigintListener);
|
||||
@@ -644,124 +681,16 @@ class REPLServer extends Interface {
|
||||
}
|
||||
}
|
||||
|
||||
self.eval = self._domain.bind(eval_);
|
||||
|
||||
self._domain.on('error', function debugDomainError(e) {
|
||||
debug('domain error');
|
||||
let errStack = '';
|
||||
|
||||
if (typeof e === 'object' && e !== null) {
|
||||
overrideStackTrace.set(e, (error, stackFrames) => {
|
||||
let frames;
|
||||
if (typeof stackFrames === 'object') {
|
||||
// Search from the bottom of the call stack to
|
||||
// find the first frame with a null function name
|
||||
const idx = ArrayPrototypeFindLastIndex(
|
||||
stackFrames,
|
||||
(frame) => frame.getFunctionName() === null,
|
||||
);
|
||||
// If found, get rid of it and everything below it
|
||||
frames = ArrayPrototypeSlice(stackFrames, 0, idx);
|
||||
} else {
|
||||
frames = stackFrames;
|
||||
}
|
||||
// FIXME(devsnek): this is inconsistent with the checks
|
||||
// that the real prepareStackTrace dispatch uses in
|
||||
// lib/internal/errors.js.
|
||||
if (typeof MainContextError.prepareStackTrace === 'function') {
|
||||
return MainContextError.prepareStackTrace(error, frames);
|
||||
}
|
||||
return ErrorPrepareStackTrace(error, frames);
|
||||
});
|
||||
decorateErrorStack(e);
|
||||
|
||||
if (e.domainThrown) {
|
||||
delete e.domain;
|
||||
delete e.domainThrown;
|
||||
}
|
||||
|
||||
if (isError(e)) {
|
||||
if (e.stack) {
|
||||
if (e.name === 'SyntaxError') {
|
||||
// Remove stack trace.
|
||||
e.stack = SideEffectFreeRegExpPrototypeSymbolReplace(
|
||||
/^\s+at\s.*\n?/gm,
|
||||
SideEffectFreeRegExpPrototypeSymbolReplace(/^REPL\d+:\d+\r?\n/, e.stack, ''),
|
||||
'');
|
||||
const importErrorStr = 'Cannot use import statement outside a ' +
|
||||
'module';
|
||||
if (StringPrototypeIncludes(e.message, importErrorStr)) {
|
||||
e.message = 'Cannot use import statement inside the Node.js ' +
|
||||
'REPL, alternatively use dynamic import: ' + toDynamicImport(ArrayPrototypeAt(self.lines, -1));
|
||||
e.stack = SideEffectFreeRegExpPrototypeSymbolReplace(
|
||||
/SyntaxError:.*\n/,
|
||||
e.stack,
|
||||
`SyntaxError: ${e.message}\n`);
|
||||
}
|
||||
} else if (self.replMode === module.exports.REPL_MODE_STRICT) {
|
||||
e.stack = SideEffectFreeRegExpPrototypeSymbolReplace(
|
||||
/(\s+at\s+REPL\d+:)(\d+)/,
|
||||
e.stack,
|
||||
(_, pre, line) => pre + (line - 1),
|
||||
);
|
||||
}
|
||||
}
|
||||
errStack = self.writer(e);
|
||||
|
||||
// Remove one line error braces to keep the old style in place.
|
||||
if (errStack[0] === '[' && errStack[errStack.length - 1] === ']') {
|
||||
errStack = StringPrototypeSlice(errStack, 1, -1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!self.underscoreErrAssigned) {
|
||||
self.lastError = e;
|
||||
}
|
||||
|
||||
if (options[kStandaloneREPL] &&
|
||||
process.listenerCount('uncaughtException') !== 0) {
|
||||
process.nextTick(() => {
|
||||
process.emit('uncaughtException', e);
|
||||
self.clearBufferedCommand();
|
||||
self.lines.level = [];
|
||||
if (!self.closed) {
|
||||
self.displayPrompt();
|
||||
}
|
||||
});
|
||||
} else {
|
||||
if (errStack === '') {
|
||||
errStack = self.writer(e);
|
||||
}
|
||||
const lines = SideEffectFreeRegExpPrototypeSymbolSplit(/(?<=\n)/, errStack);
|
||||
let matched = false;
|
||||
|
||||
errStack = '';
|
||||
ArrayPrototypeForEach(lines, (line) => {
|
||||
if (!matched &&
|
||||
RegExpPrototypeExec(/^\[?([A-Z][a-z0-9_]*)*Error/, line) !== null) {
|
||||
errStack += writer.options.breakLength >= line.length ?
|
||||
`Uncaught ${line}` :
|
||||
`Uncaught:\n${line}`;
|
||||
matched = true;
|
||||
} else {
|
||||
errStack += line;
|
||||
}
|
||||
});
|
||||
if (!matched) {
|
||||
const ln = lines.length === 1 ? ' ' : ':\n';
|
||||
errStack = `Uncaught${ln}${errStack}`;
|
||||
}
|
||||
// Normalize line endings.
|
||||
errStack += StringPrototypeEndsWith(errStack, '\n') ? '' : '\n';
|
||||
self.output.write(errStack);
|
||||
self.clearBufferedCommand();
|
||||
self.lines.level = [];
|
||||
if (!self.closed) {
|
||||
self.displayPrompt();
|
||||
}
|
||||
}
|
||||
});
|
||||
// Wrap eval to run within the REPL's async context for error tracking.
|
||||
// The function names are needed for stack trace filtering - they must not
|
||||
// be anonymous, but we can't use 'eval' as a name since it's reserved.
|
||||
const originalEval = eval_;
|
||||
// eslint-disable-next-line func-name-matching
|
||||
self.eval = function REPLEval(code, context, file, cb) {
|
||||
replContext.run({ replServer: self }, function REPLEvalInContext() {
|
||||
originalEval(code, context, file, cb);
|
||||
});
|
||||
};
|
||||
|
||||
self.clearBufferedCommand();
|
||||
|
||||
@@ -925,7 +854,7 @@ class REPLServer extends Interface {
|
||||
}
|
||||
|
||||
if (e) {
|
||||
self._domain.emit('error', e.err || e);
|
||||
self._handleError(e.err || e);
|
||||
self[kLastCommandErrored] = true;
|
||||
}
|
||||
|
||||
@@ -1051,6 +980,117 @@ class REPLServer extends Interface {
|
||||
clearBufferedCommand() {
|
||||
this[kBufferedCommandSymbol] = '';
|
||||
}
|
||||
_handleError(e) {
|
||||
debug('handle error');
|
||||
let errStack = '';
|
||||
|
||||
if (typeof e === 'object' && e !== null) {
|
||||
overrideStackTrace.set(e, (error, stackFrames) => {
|
||||
let frames;
|
||||
if (typeof stackFrames === 'object') {
|
||||
// Search from the bottom of the call stack to
|
||||
// find the first frame with a null function name
|
||||
const idx = ArrayPrototypeFindLastIndex(
|
||||
stackFrames,
|
||||
(frame) => frame.getFunctionName() === null,
|
||||
);
|
||||
// If found, get rid of it and everything below it
|
||||
frames = ArrayPrototypeSlice(stackFrames, 0, idx);
|
||||
} else {
|
||||
frames = stackFrames;
|
||||
}
|
||||
// FIXME(devsnek): this is inconsistent with the checks
|
||||
// that the real prepareStackTrace dispatch uses in
|
||||
// lib/internal/errors.js.
|
||||
if (typeof MainContextError.prepareStackTrace === 'function') {
|
||||
return MainContextError.prepareStackTrace(error, frames);
|
||||
}
|
||||
return ErrorPrepareStackTrace(error, frames);
|
||||
});
|
||||
decorateErrorStack(e);
|
||||
|
||||
if (isError(e)) {
|
||||
if (e.stack) {
|
||||
if (e.name === 'SyntaxError') {
|
||||
// Remove stack trace.
|
||||
e.stack = SideEffectFreeRegExpPrototypeSymbolReplace(
|
||||
/^\s+at\s.*\n?/gm,
|
||||
SideEffectFreeRegExpPrototypeSymbolReplace(/^REPL\d+:\d+\r?\n/, e.stack, ''),
|
||||
'');
|
||||
const importErrorStr = 'Cannot use import statement outside a ' +
|
||||
'module';
|
||||
if (StringPrototypeIncludes(e.message, importErrorStr)) {
|
||||
e.message = 'Cannot use import statement inside the Node.js ' +
|
||||
'REPL, alternatively use dynamic import: ' + toDynamicImport(ArrayPrototypeAt(this.lines, -1));
|
||||
e.stack = SideEffectFreeRegExpPrototypeSymbolReplace(
|
||||
/SyntaxError:.*\n/,
|
||||
e.stack,
|
||||
`SyntaxError: ${e.message}\n`);
|
||||
}
|
||||
} else if (this.replMode === module.exports.REPL_MODE_STRICT) {
|
||||
e.stack = SideEffectFreeRegExpPrototypeSymbolReplace(
|
||||
/(\s+at\s+REPL\d+:)(\d+)/,
|
||||
e.stack,
|
||||
(_, pre, line) => pre + (line - 1),
|
||||
);
|
||||
}
|
||||
}
|
||||
errStack = this.writer(e);
|
||||
|
||||
// Remove one line error braces to keep the old style in place.
|
||||
if (errStack[0] === '[' && errStack[errStack.length - 1] === ']') {
|
||||
errStack = StringPrototypeSlice(errStack, 1, -1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!this.underscoreErrAssigned) {
|
||||
this.lastError = e;
|
||||
}
|
||||
|
||||
if (this._isStandalone &&
|
||||
process.listenerCount('uncaughtException') !== 0) {
|
||||
process.nextTick(() => {
|
||||
process.emit('uncaughtException', e);
|
||||
this.clearBufferedCommand();
|
||||
this.lines.level = [];
|
||||
if (!this.closed) {
|
||||
this.displayPrompt();
|
||||
}
|
||||
});
|
||||
} else {
|
||||
if (errStack === '') {
|
||||
errStack = this.writer(e);
|
||||
}
|
||||
const lines = SideEffectFreeRegExpPrototypeSymbolSplit(/(?<=\n)/, errStack);
|
||||
let matched = false;
|
||||
|
||||
errStack = '';
|
||||
ArrayPrototypeForEach(lines, (line) => {
|
||||
if (!matched &&
|
||||
RegExpPrototypeExec(/^\[?([A-Z][a-z0-9_]*)*Error/, line) !== null) {
|
||||
errStack += writer.options.breakLength >= line.length ?
|
||||
`Uncaught ${line}` :
|
||||
`Uncaught:\n${line}`;
|
||||
matched = true;
|
||||
} else {
|
||||
errStack += line;
|
||||
}
|
||||
});
|
||||
if (!matched) {
|
||||
const ln = lines.length === 1 ? ' ' : ':\n';
|
||||
errStack = `Uncaught${ln}${errStack}`;
|
||||
}
|
||||
// Normalize line endings.
|
||||
errStack += StringPrototypeEndsWith(errStack, '\n') ? '' : '\n';
|
||||
this.output.write(errStack);
|
||||
this.clearBufferedCommand();
|
||||
this.lines.level = [];
|
||||
if (!this.closed) {
|
||||
this.displayPrompt();
|
||||
}
|
||||
}
|
||||
}
|
||||
close() {
|
||||
if (this.terminal && this.historyManager?.isFlushing && !this._closingOnFlush) {
|
||||
this._closingOnFlush = true;
|
||||
|
||||
+1
-7
@@ -2,9 +2,8 @@
|
||||
|
||||
const ArrayStream = require('../common/arraystream');
|
||||
const repl = require('node:repl');
|
||||
const assert = require('node:assert');
|
||||
|
||||
function startNewREPLServer(replOpts = {}, testingOpts = {}) {
|
||||
function startNewREPLServer(replOpts = {}) {
|
||||
const input = new ArrayStream();
|
||||
const output = new ArrayStream();
|
||||
|
||||
@@ -20,11 +19,6 @@ function startNewREPLServer(replOpts = {}, testingOpts = {}) {
|
||||
...replOpts,
|
||||
});
|
||||
|
||||
if (!testingOpts.disableDomainErrorAssert) {
|
||||
// Some errors are passed to the domain, but do not callback
|
||||
replServer._domain.on('error', assert.ifError);
|
||||
}
|
||||
|
||||
return { replServer, input, output };
|
||||
}
|
||||
|
||||
|
||||
+1
-7
@@ -1,6 +1,5 @@
|
||||
// Tab completion sometimes uses a separate REPL instance under the hood.
|
||||
// That REPL instance has its own domain. Make sure domain errors trickle back
|
||||
// up to the main REPL.
|
||||
// Make sure errors in completion callbacks are properly thrown.
|
||||
//
|
||||
// Ref: https://github.com/nodejs/node/issues/21586
|
||||
|
||||
@@ -31,11 +30,6 @@ const repl = require('repl');
|
||||
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;
|
||||
});
|
||||
|
||||
// Nesting of structures causes REPL to use a nested REPL for completion.
|
||||
putIn.run([
|
||||
'var top = function() {',
|
||||
|
||||
@@ -1,17 +1,22 @@
|
||||
'use strict';
|
||||
// Tests that domain can be loaded after setUncaughtExceptionCaptureCallback
|
||||
// has been called. This verifies that the mutual exclusivity has been removed.
|
||||
const common = require('../common');
|
||||
const assert = require('assert');
|
||||
|
||||
// Set up a capture callback first
|
||||
process.setUncaughtExceptionCaptureCallback(common.mustNotCall());
|
||||
|
||||
assert.throws(
|
||||
() => require('domain'),
|
||||
{
|
||||
code: 'ERR_DOMAIN_CALLBACK_NOT_AVAILABLE',
|
||||
name: 'Error',
|
||||
message: /^A callback was registered.*with using the `domain` module/
|
||||
}
|
||||
);
|
||||
// Loading domain should not throw (coexistence is now supported)
|
||||
const domain = require('domain');
|
||||
|
||||
// Verify domain module loaded successfully
|
||||
const assert = require('assert');
|
||||
assert.ok(domain);
|
||||
assert.ok(domain.create);
|
||||
|
||||
// Clean up
|
||||
process.setUncaughtExceptionCaptureCallback(null);
|
||||
require('domain'); // Should not throw.
|
||||
|
||||
// Domain should still be usable
|
||||
const d = domain.create();
|
||||
assert.ok(d);
|
||||
@@ -1,30 +1,23 @@
|
||||
'use strict';
|
||||
// Tests that setUncaughtExceptionCaptureCallback can be called after domain
|
||||
// is loaded. This verifies that the mutual exclusivity has been removed.
|
||||
const common = require('../common');
|
||||
const assert = require('assert');
|
||||
|
||||
Error.stackTraceLimit = Infinity;
|
||||
// Load domain first
|
||||
const domain = require('domain');
|
||||
assert.ok(domain);
|
||||
|
||||
(function foobar() {
|
||||
require('domain');
|
||||
})();
|
||||
// Setting callback should not throw (coexistence is now supported)
|
||||
process.setUncaughtExceptionCaptureCallback(common.mustNotCall());
|
||||
|
||||
assert.throws(
|
||||
() => process.setUncaughtExceptionCaptureCallback(common.mustNotCall()),
|
||||
(err) => {
|
||||
common.expectsError(
|
||||
{
|
||||
code: 'ERR_DOMAIN_CANNOT_SET_UNCAUGHT_EXCEPTION_CAPTURE',
|
||||
name: 'Error',
|
||||
message: /^The `domain` module is in use, which is mutually/
|
||||
}
|
||||
)(err);
|
||||
// Verify callback is registered
|
||||
assert.ok(process.hasUncaughtExceptionCaptureCallback());
|
||||
|
||||
assert(err.stack.includes('-'.repeat(40)),
|
||||
`expected ${err.stack} to contain dashes`);
|
||||
// Clean up
|
||||
process.setUncaughtExceptionCaptureCallback(null);
|
||||
assert.ok(!process.hasUncaughtExceptionCaptureCallback());
|
||||
|
||||
const location = `at foobar (${__filename}:`;
|
||||
assert(err.stack.includes(location),
|
||||
`expected ${err.stack} to contain ${location}`);
|
||||
return true;
|
||||
}
|
||||
);
|
||||
// Domain should still be usable after callback operations
|
||||
const d = domain.create();
|
||||
assert.ok(d);
|
||||
@@ -20,8 +20,6 @@ const assert = require('node:assert');
|
||||
eval$.resolve();
|
||||
});
|
||||
},
|
||||
}, {
|
||||
disableDomainErrorAssert: true,
|
||||
});
|
||||
|
||||
replServer.write('\n');
|
||||
|
||||
@@ -3,5 +3,5 @@ require('../common');
|
||||
const { startNewREPLServer } = require('../common/repl');
|
||||
|
||||
// Regression test for https://github.com/nodejs/node/issues/6802
|
||||
const { input } = startNewREPLServer({ useGlobal: true }, { disableDomainErrorAssert: true });
|
||||
const { input } = startNewREPLServer({ useGlobal: true });
|
||||
input.run(['let process']);
|
||||
@@ -30,8 +30,8 @@ function testSloppyMode() {
|
||||
}
|
||||
|
||||
function testStrictMode() {
|
||||
const { input, output } = startNewREPLServer({ replMode: repl.REPL_MODE_STRICT, terminal: false, prompt: '> ' }, {
|
||||
disableDomainErrorAssert: true,
|
||||
const { input, output } = startNewREPLServer({
|
||||
replMode: repl.REPL_MODE_STRICT, terminal: false, prompt: '> '
|
||||
});
|
||||
|
||||
input.emit('data', 'x = 3\n');
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
'use strict';
|
||||
|
||||
// This test verifies that when multiple REPL instances exist concurrently,
|
||||
// async errors are correctly routed to the REPL instance that created them.
|
||||
|
||||
const common = require('../common');
|
||||
const assert = require('assert');
|
||||
const repl = require('repl');
|
||||
const { Writable, PassThrough } = require('stream');
|
||||
|
||||
// Create two REPLs with separate inputs and outputs
|
||||
let output1 = '';
|
||||
let output2 = '';
|
||||
|
||||
const input1 = new PassThrough();
|
||||
const input2 = new PassThrough();
|
||||
|
||||
const writable1 = new Writable({
|
||||
write(chunk, encoding, callback) {
|
||||
output1 += chunk.toString();
|
||||
callback();
|
||||
}
|
||||
});
|
||||
|
||||
const writable2 = new Writable({
|
||||
write(chunk, encoding, callback) {
|
||||
output2 += chunk.toString();
|
||||
callback();
|
||||
}
|
||||
});
|
||||
|
||||
const r1 = repl.start({
|
||||
input: input1,
|
||||
output: writable1,
|
||||
terminal: false,
|
||||
prompt: 'R1> ',
|
||||
});
|
||||
|
||||
const r2 = repl.start({
|
||||
input: input2,
|
||||
output: writable2,
|
||||
terminal: false,
|
||||
prompt: 'R2> ',
|
||||
});
|
||||
|
||||
// Create async error in REPL 1
|
||||
input1.write('setTimeout(() => { throw new Error("error from repl1") }, 10)\n');
|
||||
|
||||
// Create async error in REPL 2
|
||||
input2.write('setTimeout(() => { throw new Error("error from repl2") }, 20)\n');
|
||||
|
||||
setTimeout(common.mustCall(() => {
|
||||
r1.close();
|
||||
r2.close();
|
||||
|
||||
// Verify error from REPL 1 went to REPL 1's output
|
||||
assert.match(output1, /error from repl1/,
|
||||
'REPL 1 should have received its own async error');
|
||||
|
||||
// Verify error from REPL 2 went to REPL 2's output
|
||||
assert.match(output2, /error from repl2/,
|
||||
'REPL 2 should have received its own async error');
|
||||
|
||||
// Verify errors did not cross over to wrong REPL
|
||||
assert.doesNotMatch(output1, /error from repl2/,
|
||||
'REPL 1 should not have received REPL 2\'s error');
|
||||
assert.doesNotMatch(output2, /error from repl1/,
|
||||
'REPL 2 should not have received REPL 1\'s error');
|
||||
}), 100);
|
||||
@@ -10,8 +10,6 @@ function run({ command, expected }) {
|
||||
const { replServer, output } = startNewREPLServer({
|
||||
terminal: false,
|
||||
useColors: false
|
||||
}, {
|
||||
disableDomainErrorAssert: true,
|
||||
});
|
||||
|
||||
replServer.write(`${command}\n`);
|
||||
|
||||
@@ -5,10 +5,7 @@ const { startNewREPLServer } = require('../common/repl');
|
||||
|
||||
const testingReplPrompt = '_REPL_TESTING_PROMPT_>';
|
||||
|
||||
const { replServer, output } = startNewREPLServer(
|
||||
{ prompt: testingReplPrompt },
|
||||
{ disableDomainErrorAssert: true }
|
||||
);
|
||||
const { replServer, output } = startNewREPLServer({ prompt: testingReplPrompt });
|
||||
|
||||
replServer.write('throw new Error("foo[a]")\n');
|
||||
|
||||
|
||||
@@ -7,14 +7,11 @@ const { startNewREPLServer } = require('../common/repl');
|
||||
const stackRegExp = /(at .*REPL\d+:)[0-9]+:[0-9]+/g;
|
||||
|
||||
function run({ command, expected, ...extraREPLOptions }, i) {
|
||||
const { replServer, output } = startNewREPLServer(
|
||||
{
|
||||
terminal: false,
|
||||
useColors: false,
|
||||
...extraREPLOptions
|
||||
},
|
||||
{ disableDomainErrorAssert: true }
|
||||
);
|
||||
const { replServer, output } = startNewREPLServer({
|
||||
terminal: false,
|
||||
useColors: false,
|
||||
...extraREPLOptions
|
||||
});
|
||||
|
||||
replServer.write(`${command}\n`);
|
||||
if (typeof expected === 'string') {
|
||||
|
||||
@@ -6,9 +6,7 @@ const { startNewREPLServer } = require('../common/repl');
|
||||
|
||||
common.skipIfInspectorDisabled();
|
||||
|
||||
const { input, output } = startNewREPLServer(
|
||||
{ useColors: true }, { disableDomainErrorAssert: true }
|
||||
);
|
||||
const { input, output } = startNewREPLServer({ useColors: true });
|
||||
|
||||
output.accumulator = '';
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@ process.on('exit', () => {
|
||||
assert.strictEqual(found, true);
|
||||
});
|
||||
|
||||
const { input, output } = startNewREPLServer({}, { disableDomainErrorAssert: true });
|
||||
const { input, output } = startNewREPLServer();
|
||||
|
||||
output.write = (data) => {
|
||||
// Matching only on a minimal piece of the stack because the string will vary
|
||||
|
||||
@@ -4,7 +4,7 @@ const common = require('../common');
|
||||
const assert = require('assert');
|
||||
const { startNewREPLServer } = require('../common/repl');
|
||||
|
||||
const { replServer, input } = startNewREPLServer({}, { disableDomainErrorAssert: true });
|
||||
const { replServer, input } = startNewREPLServer();
|
||||
|
||||
// https://github.com/nodejs/node/issues/3346
|
||||
// Tab-completion should be empty
|
||||
|
||||
@@ -10,6 +10,4 @@ const testMe = repl.start('', putIn, function(cmd, context, filename,
|
||||
callback(null, cmd);
|
||||
});
|
||||
|
||||
testMe._domain.on('error', common.mustNotCall());
|
||||
|
||||
testMe.complete('', common.mustSucceed());
|
||||
@@ -180,7 +180,7 @@ async function ordinaryTests() {
|
||||
['k', '234'],
|
||||
['const k = await Promise.resolve(345)', "Uncaught SyntaxError: Identifier 'k' has already been declared"],
|
||||
// Regression test for https://github.com/nodejs/node/issues/43777.
|
||||
['await Promise.resolve(123), Promise.resolve(456)', 'Promise {', { line: 0 }],
|
||||
['await Promise.resolve(123), Promise.resolve(456)', 'Promise { 456 }'],
|
||||
['await Promise.resolve(123), await Promise.resolve(456)', '456'],
|
||||
['await (Promise.resolve(123), Promise.resolve(456))', '456'],
|
||||
];
|
||||
|
||||
@@ -8,17 +8,12 @@ const common = require('../common');
|
||||
const assert = require('assert');
|
||||
const { startNewREPLServer } = require('../common/repl');
|
||||
|
||||
const { replServer, output } = startNewREPLServer(
|
||||
{
|
||||
prompt: '',
|
||||
terminal: false,
|
||||
useColors: false,
|
||||
global: false,
|
||||
},
|
||||
{
|
||||
disableDomainErrorAssert: true
|
||||
},
|
||||
);
|
||||
const { replServer, output } = startNewREPLServer({
|
||||
prompt: '',
|
||||
terminal: false,
|
||||
useColors: false,
|
||||
global: false,
|
||||
});
|
||||
|
||||
replServer.write(
|
||||
'process.nextTick(() => {\n' +
|
||||
|
||||
@@ -3,21 +3,16 @@ const common = require('../common');
|
||||
const assert = require('assert');
|
||||
const { startNewREPLServer } = require('../common/repl');
|
||||
|
||||
const { replServer, output } = startNewREPLServer(
|
||||
{
|
||||
prompt: '',
|
||||
terminal: false,
|
||||
useColors: false,
|
||||
global: false,
|
||||
eval: common.mustCall((code, context, filename, cb) => {
|
||||
replServer.setPrompt('prompt! ');
|
||||
cb(new Error('err'));
|
||||
})
|
||||
},
|
||||
{
|
||||
disableDomainErrorAssert: true
|
||||
},
|
||||
);
|
||||
const { replServer, output } = startNewREPLServer({
|
||||
prompt: '',
|
||||
terminal: false,
|
||||
useColors: false,
|
||||
global: false,
|
||||
eval: common.mustCall((code, context, filename, cb) => {
|
||||
replServer.setPrompt('prompt! ');
|
||||
cb(new Error('err'));
|
||||
})
|
||||
});
|
||||
|
||||
replServer.write('foo\n');
|
||||
|
||||
|
||||
@@ -6,16 +6,11 @@ const { startNewREPLServer } = require('../common/repl');
|
||||
let count = 0;
|
||||
|
||||
function run({ command, expected, useColors = false }) {
|
||||
const { replServer, output } = startNewREPLServer(
|
||||
{
|
||||
prompt: '',
|
||||
terminal: false,
|
||||
useColors,
|
||||
},
|
||||
{
|
||||
disableDomainErrorAssert: true
|
||||
},
|
||||
);
|
||||
const { replServer, output } = startNewREPLServer({
|
||||
prompt: '',
|
||||
terminal: false,
|
||||
useColors,
|
||||
});
|
||||
|
||||
replServer.write(`${command}\n`);
|
||||
|
||||
|
||||
@@ -138,8 +138,6 @@ function testError() {
|
||||
prompt: testingReplPrompt,
|
||||
replMode: repl.REPL_MODE_STRICT,
|
||||
preview: false,
|
||||
}, {
|
||||
disableDomainErrorAssert: true
|
||||
});
|
||||
|
||||
replServer.write(`_error; // initial value undefined
|
||||
|
||||
@@ -8,7 +8,7 @@ const { startNewREPLServer } = require('../common/repl');
|
||||
|
||||
const cpuUsage = process.cpuUsage();
|
||||
|
||||
const { replServer } = startNewREPLServer({}, { disableDomainErrorAssert: true });
|
||||
const { replServer } = startNewREPLServer();
|
||||
replServer.input.emit('data', '{}');
|
||||
replServer.input.emit('keypress', '', { name: 'left' });
|
||||
replServer.input.emit('data', 'node');
|
||||
|
||||
Reference in new issue
Block a user