Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions packages/cli-repl/src/async-repl.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,21 @@ describe('AsyncRepl', () => {
await expectInStream(output, 'meow');
});

it('disables raw mode for input during both sync and async evaluation when async sigint is enabled', async() => {
const { input, output, repl } = createDefaultAsyncRepl({ onAsyncSigint: () => false });
let isRaw = true;
Object.defineProperty(input, 'isRaw', {
get() { return isRaw; },
enumerable: true
});
(input as any).setRawMode = (value: boolean) => { isRaw = value; };
repl.context.isRawMode = () => isRaw;

input.write('const before = isRawMode(); new Promise(setImmediate).then(() => ({before, after: isRawMode()}))\n');
await expectInStream(output, 'before: false, after: false');
expect(isRaw).to.equal(true);
});

it('handles asynchronous exceptions well', async() => {
const { input, output } = createDefaultAsyncRepl();

Expand Down
26 changes: 26 additions & 0 deletions packages/cli-repl/src/async-repl.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
/* eslint-disable chai-friendly/no-unused-expressions */
import { Domain } from 'domain';
import type { EventEmitter } from 'events';
import type { ReadStream } from 'tty';
import isRecoverableError from 'is-recoverable-error';
import { Interface, ReadLineOptions } from 'readline';
import type { ReplOptions, REPLServer } from 'repl';
Expand Down Expand Up @@ -75,11 +76,31 @@ export function start(opts: AsyncREPLOptions): REPLServer {
repl.input,
wrapNoSyncDomainError(repl.eval.bind(repl))));

const setRawMode = (mode: boolean): boolean => {
const input = repl.input as ReadStream;
const wasInRawMode = input.isRaw;
if (typeof input.setRawMode === 'function') {
input.setRawMode(mode);
}
return wasInRawMode;
};

(repl as Mutable<typeof repl>).eval = async(
input: string,
context: any,
filename: string,
callback: (err: Error|null, result?: any) => void): Promise<void> => {
let previouslyInRawMode;

if (onAsyncSigint) {
// Unset raw mode during evaluation so that Ctrl+C raises a signal. This
// is something REPL already does while originalEval is running, but as
// the actual eval result might be a promise that we will be awaiting, we
// want the raw mode to be disabled for the whole duration of our custom
// async eval
previouslyInRawMode = setRawMode(false);
}

let result;
repl.emit(evalStart, { input } as EvalStartEvent);

Expand Down Expand Up @@ -150,6 +171,11 @@ export function start(opts: AsyncREPLOptions): REPLServer {
evalResult.then(resolve, reject);
});
} finally {
// Restore raw mode
if (typeof previouslyInRawMode !== 'undefined') {
setRawMode(previouslyInRawMode);
}

// Remove our 'SIGINT' listener and re-install the REPL one(s).
if (sigintListener !== undefined) {
repl.removeListener('SIGINT', sigintListener);
Expand Down
112 changes: 73 additions & 39 deletions packages/cli-repl/test/e2e.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -705,47 +705,81 @@ describe('e2e', function() {
}
});

let shell: TestShell;
beforeEach(async() => {
shell = TestShell.start({ args: [ '--nodb' ], removeSigintListeners: true });
await shell.waitForPrompt();
shell.assertNoErrors();
describe('non-interactive', function() {
it('interrupts file execution', async function() {
const filename = path.resolve(
__dirname,
'fixtures',
'load',
'long-sleep.js'
);
const shell = TestShell.start({
args: ['--nodb', filename],
removeSigintListeners: true,
forceTerminal: true
});

await eventually(() => {
if (shell.output.includes('Long sleep')) {
return;
}
throw new Error('Waiting for the file to load...');
});

shell.kill('SIGINT');

await eventually(() => {
if (shell.output.includes('MongoshInterruptedError')) {
return;
}
throw new Error('Waiting for the interruption...');
});
});
});

it('interrupts sync execution', async() => {
await shell.executeLine('void process.removeAllListeners("SIGINT")');
const result = shell.executeLine('while(true);');
setTimeout(() => shell.kill('SIGINT'), 1000);
await result;
shell.assertContainsError('interrupted');
});
it('interrupts async awaiting', async() => {
const result = shell.executeLine('new Promise(() => {});');
setTimeout(() => shell.kill('SIGINT'), 3000);
await result;
shell.assertContainsOutput('Stopping execution...');
});
it('interrupts load()', async() => {
const filename = path.resolve(__dirname, 'fixtures', 'load', 'infinite-loop.js');
const result = shell.executeLine(`load(${JSON.stringify(filename)})`);
setTimeout(() => shell.kill('SIGINT'), 3000);
await result;
// The while loop in the script is run as "sync" code
shell.assertContainsError('interrupted');
});
it('behaves normally after an exception', async() => {
await shell.executeLine('throw new Error()');
await new Promise((resolve) => setTimeout(resolve, 100));
shell.kill('SIGINT');
await shell.waitForPrompt();
await new Promise((resolve) => setTimeout(resolve, 100));
shell.assertNotContainsOutput('interrupted');
shell.assertNotContainsOutput('Stopping execution');
});
it('does not trigger MaxListenersExceededWarning', async() => {
await shell.executeLine('for (let i = 0; i < 11; i++) { console.log("hi"); }\n');
await shell.executeLine('for (let i = 0; i < 20; i++) (async() => { await sleep(0) })()');
shell.assertNotContainsOutput('MaxListenersExceededWarning');
describe('interactive', function() {
let shell: TestShell;
beforeEach(async() => {
shell = TestShell.start({ args: [ '--nodb' ], removeSigintListeners: true });
await shell.waitForPrompt();
shell.assertNoErrors();
});

it('interrupts sync execution', async() => {
await shell.executeLine('void process.removeAllListeners("SIGINT")');
const result = shell.executeLine('while(true);');
setTimeout(() => shell.kill('SIGINT'), 1000);
await result;
shell.assertContainsError('interrupted');
});
it('interrupts async awaiting', async() => {
const result = shell.executeLine('new Promise(() => {});');
setTimeout(() => shell.kill('SIGINT'), 3000);
await result;
shell.assertContainsOutput('Stopping execution...');
});
it('interrupts load()', async() => {
const filename = path.resolve(__dirname, 'fixtures', 'load', 'infinite-loop.js');
const result = shell.executeLine(`load(${JSON.stringify(filename)})`);
setTimeout(() => shell.kill('SIGINT'), 3000);
await result;
// The while loop in the script is run as "sync" code
shell.assertContainsError('interrupted');
});
it('behaves normally after an exception', async() => {
await shell.executeLine('throw new Error()');
await new Promise((resolve) => setTimeout(resolve, 100));
shell.kill('SIGINT');
await shell.waitForPrompt();
await new Promise((resolve) => setTimeout(resolve, 100));
shell.assertNotContainsOutput('interrupted');
shell.assertNotContainsOutput('Stopping execution');
});
it('does not trigger MaxListenersExceededWarning', async() => {
await shell.executeLine('for (let i = 0; i < 11; i++) { console.log("hi"); }\n');
await shell.executeLine('for (let i = 0; i < 20; i++) (async() => { await sleep(0) })()');
shell.assertNotContainsOutput('MaxListenersExceededWarning');
});
});
});

Expand Down
5 changes: 5 additions & 0 deletions packages/cli-repl/test/fixtures/load/long-sleep.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
/* eslint-disable */
console.log('Long sleep');
(async() => {
await sleep(1_000_000);
})();