Skip to content

Commit

Permalink
repl: break on sigint/ctrl+c
Browse files Browse the repository at this point in the history
Adds the ability to stop execution of the current REPL command
when receiving SIGINT. This applies only to the default eval
function.

Fixes: #6612
PR-URL: #6635
Reviewed-By: Ben Noordhuis <info@bnoordhuis.nl>
  • Loading branch information
addaleax authored and Fishrock123 committed Jun 27, 2016
1 parent da1dffb commit aad7d41
Show file tree
Hide file tree
Showing 5 changed files with 151 additions and 6 deletions.
3 changes: 3 additions & 0 deletions doc/api/repl.md
Original file line number Diff line number Diff line change
Expand Up @@ -390,6 +390,9 @@ added: v0.1.91
equivalent to prefacing every repl statement with `'use strict'`.
* `repl.REPL_MODE_MAGIC` - attempt to evaluates expressions in default
mode. If expressions fail to parse, re-try in strict mode.
* `breakEvalOnSigint` - Stop evaluating the current piece of code when
`SIGINT` is received, i.e. `Ctrl+C` is pressed. This cannot be used together
with a custom `eval` function. Defaults to `false`.

The `repl.start()` method creates and starts a `repl.REPLServer` instance.

Expand Down
3 changes: 2 additions & 1 deletion lib/internal/repl.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,8 @@ function createRepl(env, opts, cb) {
opts = opts || {
ignoreUndefined: false,
terminal: process.stdout.isTTY,
useGlobal: true
useGlobal: true,
breakEvalOnSigint: true
};

if (parseInt(env.NODE_NO_READLINE)) {
Expand Down
51 changes: 46 additions & 5 deletions lib/repl.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
const internalModule = require('internal/module');
const internalUtil = require('internal/util');
const util = require('util');
const utilBinding = process.binding('util');
const inherits = util.inherits;
const Stream = require('stream');
const vm = require('vm');
Expand Down Expand Up @@ -178,7 +179,7 @@ function REPLServer(prompt,
replMode);
}

var options, input, output, dom;
var options, input, output, dom, breakEvalOnSigint;
if (prompt !== null && typeof prompt === 'object') {
// an options object was given
options = prompt;
Expand All @@ -191,10 +192,17 @@ function REPLServer(prompt,
prompt = options.prompt;
dom = options.domain;
replMode = options.replMode;
breakEvalOnSigint = options.breakEvalOnSigint;
} else {
options = {};
}

if (breakEvalOnSigint && eval_) {
// Allowing this would not reflect user expectations.
// breakEvalOnSigint affects only the behaviour of the default eval().
throw new Error('Cannot specify both breakEvalOnSigint and eval for REPL');
}

var self = this;

self._domain = dom || domain.create();
Expand All @@ -204,6 +212,7 @@ function REPLServer(prompt,
self.replMode = replMode || exports.REPL_MODE_SLOPPY;
self.underscoreAssigned = false;
self.last = undefined;
self.breakEvalOnSigint = !!breakEvalOnSigint;

self._inTemplateLiteral = false;

Expand Down Expand Up @@ -267,14 +276,46 @@ function REPLServer(prompt,
regExMatcher.test(savedRegExMatches.join(sep));

if (!err) {
// Unset raw mode during evaluation so that Ctrl+C raises a signal.
let previouslyInRawMode;
if (self.breakEvalOnSigint) {
// Start the SIGINT watchdog before entering raw mode so that a very
// quick Ctrl+C doesn’t lead to aborting the process completely.
utilBinding.startSigintWatchdog();
previouslyInRawMode = self._setRawMode(false);
}

try {
if (self.useGlobal) {
result = script.runInThisContext({ displayErrors: false });
} else {
result = script.runInContext(context, { displayErrors: false });
try {
const scriptOptions = {
displayErrors: false,
breakOnSigint: self.breakEvalOnSigint
};

if (self.useGlobal) {
result = script.runInThisContext(scriptOptions);
} else {
result = script.runInContext(context, scriptOptions);
}
} finally {
if (self.breakEvalOnSigint) {
// Reset terminal mode to its previous value.
self._setRawMode(previouslyInRawMode);

// Returns true if there were pending SIGINTs *after* the script
// has terminated without being interrupted itself.
if (utilBinding.stopSigintWatchdog()) {
self.emit('SIGINT');
}
}
}
} catch (e) {
err = e;
if (err.message === 'Script execution interrupted.') {
// The stack trace for this case is not very useful anyway.
Object.defineProperty(err, 'stack', { value: '' });
}

if (err && process.domain) {
debug('not recoverable, send to domain');
process.domain.emit('error', err);
Expand Down
50 changes: 50 additions & 0 deletions test/parallel/test-repl-sigint-nested-eval.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
'use strict';
const common = require('../common');
const assert = require('assert');

const spawn = require('child_process').spawn;

if (process.platform === 'win32') {
// No way to send CTRL_C_EVENT to processes from JS right now.
common.skip('platform not supported');
return;
}

process.env.REPL_TEST_PPID = process.pid;
const child = spawn(process.execPath, [ '-i' ], {
stdio: [null, null, 2]
});

let stdout = '';
child.stdout.setEncoding('utf8');
child.stdout.pipe(process.stdout);
child.stdout.on('data', function(c) {
stdout += c;
});

child.stdin.write = ((original) => {
return (chunk) => {
process.stderr.write(chunk);
return original.call(child.stdin, chunk);
};
})(child.stdin.write);

child.stdout.once('data', common.mustCall(() => {
process.on('SIGUSR2', common.mustCall(() => {
process.kill(child.pid, 'SIGINT');
child.stdout.once('data', common.mustCall(() => {
// Make sure REPL still works.
child.stdin.end('"foobar"\n');
}));
}));

child.stdin.write('process.kill(+process.env.REPL_TEST_PPID, "SIGUSR2");' +
'vm.runInThisContext("while(true){}", ' +
'{ breakOnSigint: true });\n');
}));

child.on('close', function(code) {
assert.strictEqual(code, 0);
assert.notStrictEqual(stdout.indexOf('Script execution interrupted.'), -1);
assert.notStrictEqual(stdout.indexOf('foobar'), -1);
});
50 changes: 50 additions & 0 deletions test/parallel/test-repl-sigint.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
'use strict';
const common = require('../common');
const assert = require('assert');

const spawn = require('child_process').spawn;

if (process.platform === 'win32') {
// No way to send CTRL_C_EVENT to processes from JS right now.
common.skip('platform not supported');
return;
}

process.env.REPL_TEST_PPID = process.pid;
const child = spawn(process.execPath, [ '-i' ], {
stdio: [null, null, 2]
});

let stdout = '';
child.stdout.setEncoding('utf8');
child.stdout.pipe(process.stdout);
child.stdout.on('data', function(c) {
stdout += c;
});

child.stdin.write = ((original) => {
return (chunk) => {
process.stderr.write(chunk);
return original.call(child.stdin, chunk);
};
})(child.stdin.write);

child.stdout.once('data', common.mustCall(() => {
process.on('SIGUSR2', common.mustCall(() => {
process.kill(child.pid, 'SIGINT');
child.stdout.once('data', common.mustCall(() => {
// Make sure state from before the interruption is still available.
child.stdin.end('a*2*3*7\n');
}));
}));

child.stdin.write('a = 1001;' +
'process.kill(+process.env.REPL_TEST_PPID, "SIGUSR2");' +
'while(true){}\n');
}));

child.on('close', function(code) {
assert.strictEqual(code, 0);
assert.notStrictEqual(stdout.indexOf('Script execution interrupted.\n'), -1);
assert.notStrictEqual(stdout.indexOf('42042\n'), -1);
});

0 comments on commit aad7d41

Please sign in to comment.