Skip to content

Commit 14d3794

Browse files
addaleaxFishrock123
authored andcommitted
vm: add ability to break on sigint/ctrl+c
- 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>
1 parent 04c3878 commit 14d3794

16 files changed

+582
-11
lines changed

doc/api/vm.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,12 @@ added: v0.3.1
7777
* `timeout` {number} Specifies the number of milliseconds to execute `code`
7878
before terminating execution. If execution is terminated, an [`Error`][]
7979
will be thrown.
80+
* `breakOnSigint`: if `true`, the execution will be terminated when
81+
`SIGINT` (Ctrl+C) is received. Existing handlers for the
82+
event that have been attached via `process.on("SIGINT")` will be disabled
83+
during script execution, but will continue to work after that.
84+
If execution is terminated, an [`Error`][] will be thrown.
85+
8086

8187
Runs the compiled code contained by the `vm.Script` object within the given
8288
`contextifiedSandbox` and returns the result. Running code does not have access

lib/vm.js

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,29 @@ const Script = binding.ContextifyScript;
1313
// - isContext(sandbox)
1414
// From this we build the entire documented API.
1515

16+
const realRunInThisContext = Script.prototype.runInThisContext;
17+
const realRunInContext = Script.prototype.runInContext;
18+
19+
Script.prototype.runInThisContext = function(options) {
20+
if (options && options.breakOnSigint) {
21+
return sigintHandlersWrap(() => {
22+
return realRunInThisContext.call(this, options);
23+
});
24+
} else {
25+
return realRunInThisContext.call(this, options);
26+
}
27+
};
28+
29+
Script.prototype.runInContext = function(contextifiedSandbox, options) {
30+
if (options && options.breakOnSigint) {
31+
return sigintHandlersWrap(() => {
32+
return realRunInContext.call(this, contextifiedSandbox, options);
33+
});
34+
} else {
35+
return realRunInContext.call(this, contextifiedSandbox, options);
36+
}
37+
};
38+
1639
Script.prototype.runInNewContext = function(sandbox, options) {
1740
var context = exports.createContext(sandbox);
1841
return this.runInContext(context, options);
@@ -55,3 +78,27 @@ exports.runInThisContext = function(code, options) {
5578
};
5679

5780
exports.isContext = binding.isContext;
81+
82+
// Remove all SIGINT listeners and re-attach them after the wrapped function
83+
// has executed, so that caught SIGINT are handled by the listeners again.
84+
function sigintHandlersWrap(fn) {
85+
// Using the internal list here to make sure `.once()` wrappers are used,
86+
// not the original ones.
87+
let sigintListeners = process._events.SIGINT;
88+
if (!Array.isArray(sigintListeners))
89+
sigintListeners = sigintListeners ? [sigintListeners] : [];
90+
else
91+
sigintListeners = sigintListeners.slice();
92+
93+
process.removeAllListeners('SIGINT');
94+
95+
try {
96+
return fn();
97+
} finally {
98+
// Add using the public methods so that the `newListener` handler of
99+
// process can re-attach the listeners.
100+
for (const listener of sigintListeners) {
101+
process.addListener('SIGINT', listener);
102+
}
103+
}
104+
}

src/node.cc

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3290,7 +3290,7 @@ static void AtExit() {
32903290
}
32913291

32923292

3293-
static void SignalExit(int signo) {
3293+
void SignalExit(int signo) {
32943294
uv_tty_reset_mode();
32953295
#ifdef __FreeBSD__
32963296
// FreeBSD has a nasty bug, see RegisterSignalHandler for details
@@ -3754,9 +3754,9 @@ static void EnableDebugSignalHandler(int signo) {
37543754
}
37553755

37563756

3757-
static void RegisterSignalHandler(int signal,
3758-
void (*handler)(int signal),
3759-
bool reset_handler = false) {
3757+
void RegisterSignalHandler(int signal,
3758+
void (*handler)(int signal),
3759+
bool reset_handler) {
37603760
struct sigaction sa;
37613761
memset(&sa, 0, sizeof(sa));
37623762
sa.sa_handler = handler;

src/node_contextify.cc

Lines changed: 39 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -553,14 +553,15 @@ class ContextifyScript : public BaseObject {
553553
TryCatch try_catch(args.GetIsolate());
554554
uint64_t timeout = GetTimeoutArg(args, 0);
555555
bool display_errors = GetDisplayErrorsArg(args, 0);
556+
bool break_on_sigint = GetBreakOnSigintArg(args, 0);
556557
if (try_catch.HasCaught()) {
557558
try_catch.ReThrow();
558559
return;
559560
}
560561

561562
// Do the eval within this context
562563
Environment* env = Environment::GetCurrent(args);
563-
EvalMachine(env, timeout, display_errors, args, try_catch);
564+
EvalMachine(env, timeout, display_errors, break_on_sigint, args, try_catch);
564565
}
565566

566567
// args: sandbox, [options]
@@ -569,6 +570,7 @@ class ContextifyScript : public BaseObject {
569570

570571
int64_t timeout;
571572
bool display_errors;
573+
bool break_on_sigint;
572574

573575
// Assemble arguments
574576
if (!args[0]->IsObject()) {
@@ -581,6 +583,7 @@ class ContextifyScript : public BaseObject {
581583
TryCatch try_catch(env->isolate());
582584
timeout = GetTimeoutArg(args, 1);
583585
display_errors = GetDisplayErrorsArg(args, 1);
586+
break_on_sigint = GetBreakOnSigintArg(args, 1);
584587
if (try_catch.HasCaught()) {
585588
try_catch.ReThrow();
586589
return;
@@ -605,6 +608,7 @@ class ContextifyScript : public BaseObject {
605608
if (EvalMachine(contextify_context->env(),
606609
timeout,
607610
display_errors,
611+
break_on_sigint,
608612
args,
609613
try_catch)) {
610614
contextify_context->CopyProperties();
@@ -653,6 +657,23 @@ class ContextifyScript : public BaseObject {
653657
True(env->isolate()));
654658
}
655659

660+
static bool GetBreakOnSigintArg(const FunctionCallbackInfo<Value>& args,
661+
const int i) {
662+
if (args[i]->IsUndefined() || args[i]->IsString()) {
663+
return false;
664+
}
665+
if (!args[i]->IsObject()) {
666+
Environment::ThrowTypeError(args.GetIsolate(),
667+
"options must be an object");
668+
return false;
669+
}
670+
671+
Local<String> key = FIXED_ONE_BYTE_STRING(args.GetIsolate(),
672+
"breakOnSigint");
673+
Local<Value> value = args[i].As<Object>()->Get(key);
674+
return value->IsTrue();
675+
}
676+
656677
static int64_t GetTimeoutArg(const FunctionCallbackInfo<Value>& args,
657678
const int i) {
658679
if (args[i]->IsUndefined() || args[i]->IsString()) {
@@ -798,6 +819,7 @@ class ContextifyScript : public BaseObject {
798819
static bool EvalMachine(Environment* env,
799820
const int64_t timeout,
800821
const bool display_errors,
822+
const bool break_on_sigint,
801823
const FunctionCallbackInfo<Value>& args,
802824
TryCatch& try_catch) {
803825
if (!ContextifyScript::InstanceOf(env, args.Holder())) {
@@ -813,16 +835,30 @@ class ContextifyScript : public BaseObject {
813835
Local<Script> script = unbound_script->BindToCurrentContext();
814836

815837
Local<Value> result;
816-
if (timeout != -1) {
838+
bool timed_out = false;
839+
if (break_on_sigint && timeout != -1) {
817840
Watchdog wd(env->isolate(), timeout);
841+
SigintWatchdog swd(env->isolate());
818842
result = script->Run();
843+
timed_out = wd.HasTimedOut();
844+
} else if (break_on_sigint) {
845+
SigintWatchdog swd(env->isolate());
846+
result = script->Run();
847+
} else if (timeout != -1) {
848+
Watchdog wd(env->isolate(), timeout);
849+
result = script->Run();
850+
timed_out = wd.HasTimedOut();
819851
} else {
820852
result = script->Run();
821853
}
822854

823855
if (try_catch.HasCaught() && try_catch.HasTerminated()) {
824856
env->isolate()->CancelTerminateExecution();
825-
env->ThrowError("Script execution timed out.");
857+
if (timed_out) {
858+
env->ThrowError("Script execution timed out.");
859+
} else {
860+
env->ThrowError("Script execution interrupted.");
861+
}
826862
try_catch.ReThrow();
827863
return false;
828864
}

src/node_internals.h

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,13 @@ void GetSockOrPeerName(const v8::FunctionCallbackInfo<v8::Value>& args) {
9999
args.GetReturnValue().Set(err);
100100
}
101101

102+
void SignalExit(int signo);
103+
#ifdef __POSIX__
104+
void RegisterSignalHandler(int signal,
105+
void (*handler)(int signal),
106+
bool reset_handler = false);
107+
#endif
108+
102109
#ifdef _WIN32
103110
// emulate snprintf() on windows, _snprintf() doesn't zero-terminate the buffer
104111
// on overflow...

src/node_util.cc

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
#include "node.h"
2+
#include "node_watchdog.h"
23
#include "v8.h"
34
#include "env.h"
45
#include "env-inl.h"
@@ -88,6 +89,20 @@ static void SetHiddenValue(const FunctionCallbackInfo<Value>& args) {
8889
}
8990

9091

92+
void StartSigintWatchdog(const FunctionCallbackInfo<Value>& args) {
93+
int ret = SigintWatchdogHelper::GetInstance()->Start();
94+
if (ret != 0) {
95+
Environment* env = Environment::GetCurrent(args);
96+
env->ThrowErrnoException(ret, "StartSigintWatchdog");
97+
}
98+
}
99+
100+
101+
void StopSigintWatchdog(const FunctionCallbackInfo<Value>& args) {
102+
bool had_pending_signals = SigintWatchdogHelper::GetInstance()->Stop();
103+
args.GetReturnValue().Set(had_pending_signals);
104+
}
105+
91106
void Initialize(Local<Object> target,
92107
Local<Value> unused,
93108
Local<Context> context) {
@@ -100,6 +115,9 @@ void Initialize(Local<Object> target,
100115
env->SetMethod(target, "getHiddenValue", GetHiddenValue);
101116
env->SetMethod(target, "setHiddenValue", SetHiddenValue);
102117
env->SetMethod(target, "getProxyDetails", GetProxyDetails);
118+
119+
env->SetMethod(target, "startSigintWatchdog", StartSigintWatchdog);
120+
env->SetMethod(target, "stopSigintWatchdog", StopSigintWatchdog);
103121
}
104122

105123
} // namespace util

0 commit comments

Comments
 (0)