Skip to content

Commit

Permalink
vm: add ability to break on sigint/ctrl+c
Browse files Browse the repository at this point in the history
- Adds the `breakEvalOnSigint` option to `vm.runIn(This)Context`.
  This uses a watchdog thread to wait for SIGINT and generally works
  just like the existing `timeout` option.

- Adds a method to the existing timer-based watchdog to check if it
  stopped regularly or by running into the timeout. This is used to
  tell a SIGINT abort from a timer-based one.

- Adds (internal) `process._{start,stop}SigintWatchdog` methods to
  start/stop the watchdog thread used by the above option manually.
  This will be used in the REPL to set up SIGINT handling before
  entering terminal raw mode, so that there is no time window in
  which Ctrl+C fully aborts the process.

PR-URL: #6635
Reviewed-By: Ben Noordhuis <info@bnoordhuis.nl>
  • Loading branch information
addaleax authored and Fishrock123 committed Jul 5, 2016
1 parent 8dd48c9 commit d049919
Show file tree
Hide file tree
Showing 16 changed files with 582 additions and 11 deletions.
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
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 @@ -3317,7 +3317,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 @@ -3819,9 +3819,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 @@ -99,6 +99,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 @@ -88,6 +89,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 @@ -100,6 +115,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

0 comments on commit d049919

Please sign in to comment.