Browse Source

repl: break on sigint/ctrl+c

Adds the ability to stop execution of the current REPL command
when receiving SIGINT. This applies only to the default eval
function.

Fixes: https://github.com/nodejs/node/issues/6612
PR-URL: https://github.com/nodejs/node/pull/6635
Reviewed-By: Ben Noordhuis <info@bnoordhuis.nl>
v7.x
Anna Henningsen 9 years ago
parent
commit
6a93ab11a9
No known key found for this signature in database GPG Key ID: D8B9F5AEAE84E4CF
  1. 3
      doc/api/repl.md
  2. 3
      lib/internal/repl.js
  3. 51
      lib/repl.js
  4. 50
      test/parallel/test-repl-sigint-nested-eval.js
  5. 50
      test/parallel/test-repl-sigint.js

3
doc/api/repl.md

@ -372,6 +372,9 @@ within the action function for commands registered using the
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.

3
lib/internal/repl.js

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

51
lib/repl.js

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

50
test/parallel/test-repl-sigint-nested-eval.js

@ -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
test/parallel/test-repl-sigint.js

@ -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);
});
Loading…
Cancel
Save