Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

vm,repl: (add ability to) break on sigint/ctrl+c #6635

Merged
merged 3 commits into from
Jun 18, 2016
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions doc/api/repl.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down
6 changes: 6 additions & 0 deletions doc/api/vm.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
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
6 changes: 5 additions & 1 deletion lib/readline.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
};


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
47 changes: 47 additions & 0 deletions lib/vm.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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);
}
}
}
8 changes: 4 additions & 4 deletions src/node.cc
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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;
Expand Down
42 changes: 39 additions & 3 deletions src/node_contextify.cc
Original file line number Diff line number Diff line change
Expand Up @@ -553,14 +553,15 @@ 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;
}

// 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]
Expand All @@ -569,6 +570,7 @@ class ContextifyScript : public BaseObject {

int64_t timeout;
bool display_errors;
bool break_on_sigint;

// Assemble arguments
if (!args[0]->IsObject()) {
Expand All @@ -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;
Expand All @@ -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();
Expand Down Expand Up @@ -653,6 +657,23 @@ class ContextifyScript : public BaseObject {
True(env->isolate()));
}

static bool GetBreakOnSigintArg(const FunctionCallbackInfo<Value>& 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<String> key = FIXED_ONE_BYTE_STRING(args.GetIsolate(),
"breakOnSigint");
Local<Value> value = args[i].As<Object>()->Get(key);
return value->IsTrue();
}

static int64_t GetTimeoutArg(const FunctionCallbackInfo<Value>& args,
const int i) {
if (args[i]->IsUndefined() || args[i]->IsString()) {
Expand Down Expand Up @@ -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<Value>& args,
TryCatch& try_catch) {
if (!ContextifyScript::InstanceOf(env, args.Holder())) {
Expand All @@ -813,16 +835,30 @@ class ContextifyScript : public BaseObject {
Local<Script> script = unbound_script->BindToCurrentContext();

Local<Value> result;
if (timeout != -1) {
bool timed_out = false;
if (break_on_sigint && timeout != -1) {
Watchdog wd(env->isolate(), timeout);
SigintWatchdog swd(env->isolate());
result = script->Run();
timed_out = wd.HasTimedOut();
} else if (break_on_sigint) {
SigintWatchdog swd(env->isolate());
result = script->Run();
} else if (timeout != -1) {
Watchdog wd(env->isolate(), timeout);
result = script->Run();
timed_out = wd.HasTimedOut();
} else {
result = script->Run();
}

if (try_catch.HasCaught() && try_catch.HasTerminated()) {
env->isolate()->CancelTerminateExecution();
env->ThrowError("Script execution timed out.");
if (timed_out) {
env->ThrowError("Script execution timed out.");
} else {
env->ThrowError("Script execution interrupted.");
}
try_catch.ReThrow();
return false;
}
Expand Down
7 changes: 7 additions & 0 deletions src/node_internals.h
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,13 @@ void GetSockOrPeerName(const v8::FunctionCallbackInfo<v8::Value>& args) {
args.GetReturnValue().Set(err);
}

void SignalExit(int signo);
#ifdef __POSIX__
void RegisterSignalHandler(int signal,
void (*handler)(int signal),
bool reset_handler = false);
#endif

#ifdef _WIN32
// emulate snprintf() on windows, _snprintf() doesn't zero-terminate the buffer
// on overflow...
Expand Down
18 changes: 18 additions & 0 deletions src/node_util.cc
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
#include "node.h"
#include "node_watchdog.h"
#include "v8.h"
#include "env.h"
#include "env-inl.h"
Expand Down Expand Up @@ -98,6 +99,20 @@ static void SetHiddenValue(const FunctionCallbackInfo<Value>& args) {
}


void StartSigintWatchdog(const FunctionCallbackInfo<Value>& args) {
int ret = SigintWatchdogHelper::GetInstance()->Start();
if (ret != 0) {
Environment* env = Environment::GetCurrent(args);
env->ThrowErrnoException(ret, "StartSigintWatchdog");
}
}


void StopSigintWatchdog(const FunctionCallbackInfo<Value>& args) {
bool had_pending_signals = SigintWatchdogHelper::GetInstance()->Stop();
args.GetReturnValue().Set(had_pending_signals);
}

void Initialize(Local<Object> target,
Local<Value> unused,
Local<Context> context) {
Expand All @@ -120,6 +135,9 @@ void Initialize(Local<Object> target,
env->SetMethod(target, "getHiddenValue", GetHiddenValue);
env->SetMethod(target, "setHiddenValue", SetHiddenValue);
env->SetMethod(target, "getProxyDetails", GetProxyDetails);

env->SetMethod(target, "startSigintWatchdog", StartSigintWatchdog);
env->SetMethod(target, "stopSigintWatchdog", StopSigintWatchdog);
}

} // namespace util
Expand Down
Loading