diff --git a/doc/api/repl.md b/doc/api/repl.md index 5437880eb36827..d2f50ecd2898bf 100644 --- a/doc/api/repl.md +++ b/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. diff --git a/doc/api/vm.md b/doc/api/vm.md index b65826b2d35c0d..0141ad605eecbb 100644 --- a/doc/api/vm.md +++ b/doc/api/vm.md @@ -77,6 +77,12 @@ added: v0.3.1 * `timeout` {number} Specifies the number of milliseconds to execute `code` before terminating execution. If execution is terminated, an [`Error`][] will be thrown. + * `breakOnSigint`: if `true`, the execution will be terminated when + `SIGINT` (Ctrl+C) is received. Existing handlers for the + event that have been attached via `process.on("SIGINT")` will be disabled + during script execution, but will continue to work after that. + If execution is terminated, an [`Error`][] will be thrown. + Runs the compiled code contained by the `vm.Script` object within the given `contextifiedSandbox` and returns the result. Running code does not have access 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/readline.js b/lib/readline.js index ee2fe1de86ed63..957bb817580793 100644 --- a/lib/readline.js +++ b/lib/readline.js @@ -182,9 +182,13 @@ Interface.prototype.setPrompt = function(prompt) { Interface.prototype._setRawMode = function(mode) { + const wasInRawMode = this.input.isRaw; + if (typeof this.input.setRawMode === 'function') { - return this.input.setRawMode(mode); + this.input.setRawMode(mode); } + + return wasInRawMode; }; 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/lib/vm.js b/lib/vm.js index b4a2b9999091d2..364a37eacbe7ce 100644 --- a/lib/vm.js +++ b/lib/vm.js @@ -13,6 +13,29 @@ const Script = binding.ContextifyScript; // - isContext(sandbox) // From this we build the entire documented API. +const realRunInThisContext = Script.prototype.runInThisContext; +const realRunInContext = Script.prototype.runInContext; + +Script.prototype.runInThisContext = function(options) { + if (options && options.breakOnSigint) { + return sigintHandlersWrap(() => { + return realRunInThisContext.call(this, options); + }); + } else { + return realRunInThisContext.call(this, options); + } +}; + +Script.prototype.runInContext = function(contextifiedSandbox, options) { + if (options && options.breakOnSigint) { + return sigintHandlersWrap(() => { + return realRunInContext.call(this, contextifiedSandbox, options); + }); + } else { + return realRunInContext.call(this, contextifiedSandbox, options); + } +}; + Script.prototype.runInNewContext = function(sandbox, options) { var context = exports.createContext(sandbox); return this.runInContext(context, options); @@ -55,3 +78,27 @@ exports.runInThisContext = function(code, options) { }; exports.isContext = binding.isContext; + +// Remove all SIGINT listeners and re-attach them after the wrapped function +// has executed, so that caught SIGINT are handled by the listeners again. +function sigintHandlersWrap(fn) { + // Using the internal list here to make sure `.once()` wrappers are used, + // not the original ones. + let sigintListeners = process._events.SIGINT; + if (!Array.isArray(sigintListeners)) + sigintListeners = sigintListeners ? [sigintListeners] : []; + else + sigintListeners = sigintListeners.slice(); + + process.removeAllListeners('SIGINT'); + + try { + return fn(); + } finally { + // Add using the public methods so that the `newListener` handler of + // process can re-attach the listeners. + for (const listener of sigintListeners) { + process.addListener('SIGINT', listener); + } + } +} diff --git a/src/node.cc b/src/node.cc index e511f900a7529a..3886be6de68f33 100644 --- a/src/node.cc +++ b/src/node.cc @@ -3239,7 +3239,7 @@ static void AtExit() { } -static void SignalExit(int signo) { +void SignalExit(int signo) { uv_tty_reset_mode(); #ifdef __FreeBSD__ // FreeBSD has a nasty bug, see RegisterSignalHandler for details @@ -3735,9 +3735,9 @@ static void EnableDebugSignalHandler(int signo) { } -static void RegisterSignalHandler(int signal, - void (*handler)(int signal), - bool reset_handler = false) { +void RegisterSignalHandler(int signal, + void (*handler)(int signal), + bool reset_handler) { struct sigaction sa; memset(&sa, 0, sizeof(sa)); sa.sa_handler = handler; diff --git a/src/node_contextify.cc b/src/node_contextify.cc index 774871b852d021..a4a769359412ef 100644 --- a/src/node_contextify.cc +++ b/src/node_contextify.cc @@ -553,6 +553,7 @@ class ContextifyScript : public BaseObject { TryCatch try_catch(args.GetIsolate()); uint64_t timeout = GetTimeoutArg(args, 0); bool display_errors = GetDisplayErrorsArg(args, 0); + bool break_on_sigint = GetBreakOnSigintArg(args, 0); if (try_catch.HasCaught()) { try_catch.ReThrow(); return; @@ -560,7 +561,7 @@ class ContextifyScript : public BaseObject { // Do the eval within this context Environment* env = Environment::GetCurrent(args); - EvalMachine(env, timeout, display_errors, args, try_catch); + EvalMachine(env, timeout, display_errors, break_on_sigint, args, try_catch); } // args: sandbox, [options] @@ -569,6 +570,7 @@ class ContextifyScript : public BaseObject { int64_t timeout; bool display_errors; + bool break_on_sigint; // Assemble arguments if (!args[0]->IsObject()) { @@ -581,6 +583,7 @@ class ContextifyScript : public BaseObject { TryCatch try_catch(env->isolate()); timeout = GetTimeoutArg(args, 1); display_errors = GetDisplayErrorsArg(args, 1); + break_on_sigint = GetBreakOnSigintArg(args, 1); if (try_catch.HasCaught()) { try_catch.ReThrow(); return; @@ -605,6 +608,7 @@ class ContextifyScript : public BaseObject { if (EvalMachine(contextify_context->env(), timeout, display_errors, + break_on_sigint, args, try_catch)) { contextify_context->CopyProperties(); @@ -653,6 +657,23 @@ class ContextifyScript : public BaseObject { True(env->isolate())); } + static bool GetBreakOnSigintArg(const FunctionCallbackInfo& args, + const int i) { + if (args[i]->IsUndefined() || args[i]->IsString()) { + return false; + } + if (!args[i]->IsObject()) { + Environment::ThrowTypeError(args.GetIsolate(), + "options must be an object"); + return false; + } + + Local key = FIXED_ONE_BYTE_STRING(args.GetIsolate(), + "breakOnSigint"); + Local value = args[i].As()->Get(key); + return value->IsTrue(); + } + static int64_t GetTimeoutArg(const FunctionCallbackInfo& args, const int i) { if (args[i]->IsUndefined() || args[i]->IsString()) { @@ -798,6 +819,7 @@ class ContextifyScript : public BaseObject { static bool EvalMachine(Environment* env, const int64_t timeout, const bool display_errors, + const bool break_on_sigint, const FunctionCallbackInfo& args, TryCatch& try_catch) { if (!ContextifyScript::InstanceOf(env, args.Holder())) { @@ -813,16 +835,30 @@ class ContextifyScript : public BaseObject { Local