From aad7d4164d2b1cc4096c82964c7ad95774f62f26 Mon Sep 17 00:00:00 2001 From: Anna Henningsen Date: Sun, 8 May 2016 03:30:23 +0200 Subject: [PATCH] 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 --- doc/api/repl.md | 3 ++ lib/internal/repl.js | 3 +- lib/repl.js | 51 +++++++++++++++++-- test/parallel/test-repl-sigint-nested-eval.js | 50 ++++++++++++++++++ test/parallel/test-repl-sigint.js | 50 ++++++++++++++++++ 5 files changed, 151 insertions(+), 6 deletions(-) create mode 100644 test/parallel/test-repl-sigint-nested-eval.js create mode 100644 test/parallel/test-repl-sigint.js diff --git a/doc/api/repl.md b/doc/api/repl.md index 0c3c1b10e9ab1c..7524a373eea366 100644 --- a/doc/api/repl.md +++ b/doc/api/repl.md @@ -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. diff --git a/lib/internal/repl.js b/lib/internal/repl.js index b72741609ba87e..dd14f42fa5273c 100644 --- a/lib/internal/repl.js +++ b/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)) { diff --git a/lib/repl.js b/lib/repl.js index 387e3b5446f678..db5754ec041196 100644 --- a/lib/repl.js +++ b/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); diff --git a/test/parallel/test-repl-sigint-nested-eval.js b/test/parallel/test-repl-sigint-nested-eval.js new file mode 100644 index 00000000000000..288c4bceebcd61 --- /dev/null +++ b/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); +}); diff --git a/test/parallel/test-repl-sigint.js b/test/parallel/test-repl-sigint.js new file mode 100644 index 00000000000000..6b342ce8612d28 --- /dev/null +++ b/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); +});