Skip to content

Commit d049919

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 8dd48c9 commit d049919

16 files changed

Lines changed: 582 additions & 11 deletions

‎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+
constrealRunInThisContext=Script.prototype.runInThisContext;
17+
constrealRunInContext=Script.prototype.runInContext;
18+
19+
Script.prototype.runInThisContext=function(options){
20+
if(options&&options.breakOnSigint){
21+
returnsigintHandlersWrap(()=>{
22+
returnrealRunInThisContext.call(this,options);
23+
});
24+
}else{
25+
returnrealRunInThisContext.call(this,options);
26+
}
27+
};
28+
29+
Script.prototype.runInContext=function(contextifiedSandbox,options){
30+
if(options&&options.breakOnSigint){
31+
returnsigintHandlersWrap(()=>{
32+
returnrealRunInContext.call(this,contextifiedSandbox,options);
33+
});
34+
}else{
35+
returnrealRunInContext.call(this,contextifiedSandbox,options);
36+
}
37+
};
38+
1639
Script.prototype.runInNewContext=function(sandbox,options){
1740
varcontext=exports.createContext(sandbox);
1841
returnthis.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+
functionsigintHandlersWrap(fn){
85+
// Using the internal list here to make sure `.once()` wrappers are used,
86+
// not the original ones.
87+
letsigintListeners=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+
returnfn();
97+
}finally{
98+
// Add using the public methods so that the `newListener` handler of
99+
// process can re-attach the listeners.
100+
for(constlistenerofsigintListeners){
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
@@ -3317,7 +3317,7 @@ static void AtExit() {
33173317
}
33183318

33193319

3320-
staticvoidSignalExit(int signo) {
3320+
voidSignalExit(int signo) {
33213321
uv_tty_reset_mode();
33223322
#ifdef __FreeBSD__
33233323
// FreeBSD has a nasty bug, see RegisterSignalHandler for details
@@ -3819,9 +3819,9 @@ static void EnableDebugSignalHandler(int signo) {
38193819
}
38203820

38213821

3822-
staticvoidRegisterSignalHandler(int signal,
3823-
void (*handler)(int signal),
3824-
bool reset_handler = false) {
3822+
voidRegisterSignalHandler(int signal,
3823+
void (*handler)(int signal),
3824+
bool reset_handler) {
38253825
structsigaction sa;
38263826
memset(&sa, 0, sizeof(sa));
38273827
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+
staticboolGetBreakOnSigintArg(const FunctionCallbackInfo<Value>& args,
661+
constint i) {
662+
if (args[i]->IsUndefined() || args[i]->IsString()) {
663+
returnfalse;
664+
}
665+
if (!args[i]->IsObject()) {
666+
Environment::ThrowTypeError(args.GetIsolate(),
667+
"options must be an object");
668+
returnfalse;
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
staticint64_tGetTimeoutArg(const FunctionCallbackInfo<Value>& args,
657678
constint i) {
658679
if (args[i]->IsUndefined() || args[i]->IsString()) {
@@ -798,6 +819,7 @@ class ContextifyScript : public BaseObject {
798819
staticboolEvalMachine(Environment* env,
799820
constint64_t timeout,
800821
constbool display_errors,
822+
constbool 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+
} elseif (break_on_sigint) {
845+
SigintWatchdog swd(env->isolate());
846+
result = script->Run();
847+
} elseif (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
returnfalse;
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+
voidSignalExit(int signo);
103+
#ifdef __POSIX__
104+
voidRegisterSignalHandler(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+
voidStartSigintWatchdog(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+
voidStopSigintWatchdog(const FunctionCallbackInfo<Value>& args) {
102+
bool had_pending_signals = SigintWatchdogHelper::GetInstance()->Stop();
103+
args.GetReturnValue().Set(had_pending_signals);
104+
}
105+
91106
voidInitialize(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)