Skip to content

Commit

Permalink
repl: limit line processing to one at a time
Browse files Browse the repository at this point in the history
  • Loading branch information
ejose19 committed Jul 14, 2021
1 parent 25e2f17 commit d4f88a0
Show file tree
Hide file tree
Showing 6 changed files with 110 additions and 49 deletions.
4 changes: 3 additions & 1 deletion doc/api/repl.md
Original file line number Diff line number Diff line change
Expand Up @@ -603,7 +603,9 @@ changes:
* `eval` {Function} The function to be used when evaluating each given line
of input. **Default:** an async wrapper for the JavaScript `eval()`
function. An `eval` function can error with `repl.Recoverable` to indicate
the input was incomplete and prompt for additional lines.
the input was incomplete and prompt for additional lines. If a custom
`eval` function is provided, `callback` must be invoked to allow processing
next command.
* `useColors` {boolean} If `true`, specifies that the default `writer`
function should include ANSI color styling to REPL output. If a custom
`writer` function is provided then this has no effect. **Default:** checking
Expand Down
121 changes: 76 additions & 45 deletions lib/repl.js
Original file line number Diff line number Diff line change
Expand Up @@ -634,6 +634,7 @@ function REPLServer(prompt,
self._domain.on('error', function debugDomainError(e) {
debug('domain error');
let errStack = '';
self.processing = false;

if (typeof e === 'object' && e !== null) {
overrideStackTrace.set(e, (error, stackFrames) => {
Expand Down Expand Up @@ -807,6 +808,8 @@ function REPLServer(prompt,
let sawCtrlD = false;
const prioritizedSigintQueue = new SafeSet();
self.on('SIGINT', function onSigInt() {
self.processing = false;

if (prioritizedSigintQueue.size > 0) {
for (const task of prioritizedSigintQueue) {
task();
Expand Down Expand Up @@ -838,7 +841,18 @@ function REPLServer(prompt,
self.displayPrompt();
});

self.on('line', function onLine(cmd) {
self.processing = false;
self.queuedLines = [];
self.skipQueue = false;

function _onLine(cmd) {
if (self.processing && !self.skipQueue) {
debug('queued line %j', cmd);
ArrayPrototypePush(self.queuedLines, cmd);
return;
}
self.skipQueue = false;
self.processing = true;
debug('line %j', cmd);
cmd = cmd || '';
sawSIGINT = false;
Expand All @@ -856,6 +870,7 @@ function REPLServer(prompt,
self.cursor = prefix.length;
}
ReflectApply(_memory, self, [cmd]);
self.processing = false;
return;
}

Expand All @@ -872,6 +887,7 @@ function REPLServer(prompt,
const keyword = matches && matches[1];
const rest = matches && matches[2];
if (ReflectApply(_parseREPLKeyword, self, [keyword, rest]) === true) {
self.processing = false;
return;
}
if (!self[kBufferedCommandSymbol]) {
Expand All @@ -888,56 +904,70 @@ function REPLServer(prompt,
self.eval(evalCmd, self.context, getREPLResourceName(), finish);

function finish(e, ret) {
debug('finish', e, ret);
ReflectApply(_memory, self, [cmd]);
try {
(() => {
debug('finish', e, ret);
ReflectApply(_memory, self, [cmd]);

if (e && !self[kBufferedCommandSymbol] &&
StringPrototypeStartsWith(StringPrototypeTrim(cmd), 'npm ')) {
self.output.write('npm should be run outside of the ' +
'Node.js REPL, in your normal shell.\n' +
'(Press Ctrl+D to exit.)\n');
self.displayPrompt();
return;
}

if (e && !self[kBufferedCommandSymbol] &&
StringPrototypeStartsWith(StringPrototypeTrim(cmd), 'npm ')) {
self.output.write('npm should be run outside of the ' +
'Node.js REPL, in your normal shell.\n' +
'(Press Ctrl+D to exit.)\n');
self.displayPrompt();
return;
}
// If error was SyntaxError and not JSON.parse error
if (e) {
if (e instanceof Recoverable && !sawCtrlD) {
// Start buffering data like that:
// {
// ... x: 1
// ... }
self[kBufferedCommandSymbol] += cmd + '\n';
self.displayPrompt();
return;
}
self._domain.emit('error', e.err || e);
}

// If error was SyntaxError and not JSON.parse error
if (e) {
if (e instanceof Recoverable && !sawCtrlD) {
// Start buffering data like that:
// {
// ... x: 1
// ... }
self[kBufferedCommandSymbol] += cmd + '\n';
self.displayPrompt();
return;
}
self._domain.emit('error', e.err || e);
}
// Clear buffer if no SyntaxErrors
self.clearBufferedCommand();
sawCtrlD = false;

// If we got any output - print it (if no error)
if (!e &&
// When an invalid REPL command is used, error message is printed
// immediately. We don't have to print anything else.
// So, only when the second argument to this function is there,
// print it.
arguments.length === 2 &&
(!self.ignoreUndefined || ret !== undefined)) {
if (!self.underscoreAssigned) {
self.last = ret;
}
self.output.write(self.writer(ret) + '\n');
}

// Clear buffer if no SyntaxErrors
self.clearBufferedCommand();
sawCtrlD = false;

// If we got any output - print it (if no error)
if (!e &&
// When an invalid REPL command is used, error message is printed
// immediately. We don't have to print anything else. So, only when
// the second argument to this function is there, print it.
arguments.length === 2 &&
(!self.ignoreUndefined || ret !== undefined)) {
if (!self.underscoreAssigned) {
self.last = ret;
// Display prompt again (unless we already did by emitting the 'error'
// event on the domain instance).
if (!e) {
self.displayPrompt();
}
})();
} finally {
if (self.queuedLines.length) {
self.skipQueue = true;
_onLine(ArrayPrototypeShift(self.queuedLines));
} else {
self.processing = false;
}
self.output.write(self.writer(ret) + '\n');
}

// Display prompt again (unless we already did by emitting the 'error'
// event on the domain instance).
if (!e) {
self.displayPrompt();
}
}
});
}

self.on('line', _onLine);

self.on('SIGCONT', function onSigCont() {
if (self.editorMode) {
Expand Down Expand Up @@ -1731,6 +1761,7 @@ function defineDefaultCommands(repl) {
if (stats && stats.isFile()) {
_turnOnEditorMode(this);
const data = fs.readFileSync(file, 'utf8');
repl.skipQueue = true;
this.write(data);
_turnOffEditorMode(this);
this.write('\n');
Expand Down
2 changes: 1 addition & 1 deletion test/parallel/test-repl-autolibs.js
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ function test1() {
`${util.inspect(require('fs'), null, 2, false)}\n`);
// Globally added lib matches required lib
assert.strictEqual(global.fs, require('fs'));
test2();
process.nextTick(test2);
}
};
assert(!gotWrite);
Expand Down
3 changes: 2 additions & 1 deletion test/parallel/test-repl-empty.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,11 @@ const repl = require('repl');
let evalCalledWithExpectedArgs = false;

const options = {
eval: common.mustCall((cmd, context) => {
eval: common.mustCall((cmd, _context, _file, cb) => {
// Assertions here will not cause the test to exit with an error code
// so set a boolean that is checked later instead.
evalCalledWithExpectedArgs = (cmd === '\n');
cb();
})
};

Expand Down
3 changes: 2 additions & 1 deletion test/parallel/test-repl-eval.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,12 @@ const repl = require('repl');
let evalCalledWithExpectedArgs = false;

const options = {
eval: common.mustCall((cmd, context) => {
eval: common.mustCall((cmd, context, _file, cb) => {
// Assertions here will not cause the test to exit with an error code
// so set a boolean that is checked later instead.
evalCalledWithExpectedArgs = (cmd === 'function f() {}\n' &&
context.foo === 'bar');
cb();
})
};

Expand Down
26 changes: 26 additions & 0 deletions test/parallel/test-repl-line-queue.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
'use strict';
require('../common');
const ArrayStream = require('../common/arraystream');

const assert = require('assert');
const repl = require('repl');

// Flags: --expose-internals --experimental-repl-await

const putIn = new ArrayStream();
repl.start({
input: putIn,
output: putIn,
useGlobal: false
});

let expectedIndex = -1;
const expected = ['undefined', '> ', '1', '> '];

putIn.write = function(data) {
assert.strict(data, expected[expectedIndex += 1]);
};

putIn.run([
'const x = await new Promise((r) => setTimeout(() => r(1), 500));\nx;',
]);

0 comments on commit d4f88a0

Please sign in to comment.