Skip to content

Commit c081809

Browse files
apaprockibnoordhuis
authored andcommitted
vm: add support for timeout argument
Add a watchdog class which executes a timer in a separate event loop in a separate thread that will terminate v8 execution if it expires. Add timeout argument to functions in vm module which use the watchdog if a non-zero timeout is specified.
1 parent b5c1721 commit c081809

8 files changed

Lines changed: 252 additions & 20 deletions

File tree

doc/api/vm.markdown

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -79,12 +79,14 @@ In case of syntax error in `code`, `vm.runInThisContext` emits the syntax error
7979
and throws an exception.
8080

8181

82-
## vm.runInNewContext(code, [sandbox], [filename])
82+
## vm.runInNewContext(code, [sandbox], [filename], [timeout])
8383

8484
`vm.runInNewContext` compiles `code`, then runs it in `sandbox` and returns the
8585
result. Running code does not have access to local scope. The object `sandbox`
8686
will be used as the global object for `code`.
8787
`sandbox` and `filename` are optional, `filename` is only used in stack traces.
88+
`timeout` specifies an optional number of milliseconds to execute `code` before
89+
terminating execution. If execution is terminated, `null` will be thrown.
8890

8991
Example: compile and execute code that increments a global variable and sets a new one.
9092
These globals are contained in the sandbox.
@@ -108,14 +110,16 @@ requires a separate process.
108110
In case of syntax error in `code`, `vm.runInNewContext` emits the syntax error to stderr
109111
and throws an exception.
110112

111-
## vm.runInContext(code, context, [filename])
113+
## vm.runInContext(code, context, [filename], [timeout])
112114

113115
`vm.runInContext` compiles `code`, then runs it in `context` and returns the
114116
result. A (V8) context comprises a global object, together with a set of
115117
built-in objects and functions. Running code does not have access to local scope
116118
and the global object held within `context` will be used as the global object
117119
for `code`.
118120
`filename` is optional, it's used only in stack traces.
121+
`timeout` specifies an optional number of milliseconds to execute `code` before
122+
terminating execution. If execution is terminated, `null` will be thrown.
119123

120124
Example: compile and execute code in a existing context.
121125

@@ -165,12 +169,14 @@ and throws an exception.
165169

166170
A class for running scripts. Returned by vm.createScript.
167171

168-
### script.runInThisContext()
172+
### script.runInThisContext([timeout])
169173

170174
Similar to `vm.runInThisContext` but a method of a precompiled `Script` object.
171175
`script.runInThisContext` runs the code of `script` and returns the result.
172176
Running code does not have access to local scope, but does have access to the `global` object
173177
(v8: in actual context).
178+
`timeout` specifies an optional number of milliseconds to execute `code` before
179+
terminating execution. If execution is terminated, `null` will be thrown.
174180

175181
Example of using `script.runInThisContext` to compile code once and run it multiple times:
176182

@@ -189,11 +195,13 @@ Example of using `script.runInThisContext` to compile code once and run it multi
189195
// 1000
190196

191197

192-
### script.runInNewContext([sandbox])
198+
### script.runInNewContext([sandbox], [timeout])
193199

194200
Similar to `vm.runInNewContext` a method of a precompiled `Script` object.
195201
`script.runInNewContext` runs the code of `script` with `sandbox` as the global object and returns the result.
196202
Running code does not have access to local scope. `sandbox` is optional.
203+
`timeout` specifies an optional number of milliseconds to execute `code` before
204+
terminating execution. If execution is terminated, `null` will be thrown.
197205

198206
Example: compile code that increments a global variable and sets one, then execute this code multiple times.
199207
These globals are contained in the sandbox.

lib/module.js

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -419,7 +419,7 @@ Module.prototype._compile = function(content, filename) {
419419
sandbox.global = sandbox;
420420
sandbox.root = root;
421421

422-
return runInNewContext(content, sandbox, filename, true);
422+
return runInNewContext(content, sandbox, filename, 0, true);
423423
}
424424

425425
debug('load root module');
@@ -430,13 +430,13 @@ Module.prototype._compile = function(content, filename) {
430430
global.__dirname = dirname;
431431
global.module = self;
432432

433-
return runInThisContext(content, filename, true);
433+
return runInThisContext(content, filename, 0, true);
434434
}
435435

436436
// create wrapper function
437437
var wrapper = Module.wrap(content);
438438

439-
var compiledWrapper = runInThisContext(wrapper, filename, true);
439+
var compiledWrapper = runInThisContext(wrapper, filename, 0, true);
440440
if (global.v8debug) {
441441
if (!resolvedArgv) {
442442
// we enter the repl if we're not given a filename argument.

node.gyp

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,7 @@
9999
'src/node_script.cc',
100100
'src/node_stat_watcher.cc',
101101
'src/node_string.cc',
102+
'src/node_watchdog.cc',
102103
'src/node_zlib.cc',
103104
'src/pipe_wrap.cc',
104105
'src/signal_wrap.cc',
@@ -126,6 +127,7 @@
126127
'src/node_script.h',
127128
'src/node_string.h',
128129
'src/node_version.h',
130+
'src/node_watchdog.h',
129131
'src/ngx-queue.h',
130132
'src/pipe_wrap.h',
131133
'src/tty_wrap.h',

src/node.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -527,7 +527,7 @@
527527
'global.require = require;\n' +
528528
'return require("vm").runInThisContext(' +
529529
JSON.stringify(body) + ', ' +
530-
JSON.stringify(name) + ', true);\n';
530+
JSON.stringify(name) + ', 0, true);\n';
531531
}
532532
var result = module._compile(script, name + '-wrapper');
533533
if (process._print_eval) console.log(result);
@@ -888,7 +888,7 @@
888888
var source = NativeModule.getSource(this.id);
889889
source = NativeModule.wrap(source);
890890

891-
var fn = runInThisContext(source, this.filename, true);
891+
var fn = runInThisContext(source, this.filename, 0, true);
892892
fn(this.exports, NativeModule.require, this, this.filename);
893893

894894
this.loaded = true;

src/node_script.cc

Lines changed: 44 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121

2222
#include "node.h"
2323
#include "node_script.h"
24+
#include "node_watchdog.h"
2425
#include <assert.h>
2526

2627
namespace node {
@@ -42,6 +43,7 @@ using v8::Persistent;
4243
using v8::Integer;
4344
using v8::Function;
4445
using v8::FunctionTemplate;
46+
using v8::V8;
4547

4648

4749
class WrappedContext : ObjectWrap {
@@ -74,10 +76,12 @@ class WrappedScript : ObjectWrap {
7476
enum EvalInputFlags { compileCode, unwrapExternal };
7577
enum EvalContextFlags { thisContext, newContext, userContext };
7678
enum EvalOutputFlags { returnResult, wrapExternal };
79+
enum EvalTimeoutFlags { noTimeout, useTimeout };
7780

7881
template <EvalInputFlags input_flag,
7982
EvalContextFlags context_flag,
80-
EvalOutputFlags output_flag>
83+
EvalOutputFlags output_flag,
84+
EvalTimeoutFlags timeout_flag>
8185
static Handle<Value> EvalMachine(const Arguments& args);
8286

8387
protected:
@@ -243,7 +247,8 @@ Handle<Value> WrappedScript::New(const Arguments& args) {
243247
t->Wrap(args.This());
244248

245249
return
246-
WrappedScript::EvalMachine<compileCode, thisContext, wrapExternal>(args);
250+
WrappedScript::EvalMachine<
251+
compileCode, thisContext, wrapExternal, noTimeout>(args);
247252
}
248253

249254

@@ -275,43 +280,50 @@ Handle<Value> WrappedScript::CreateContext(const Arguments& args) {
275280

276281
Handle<Value> WrappedScript::RunInContext(const Arguments& args) {
277282
return
278-
WrappedScript::EvalMachine<unwrapExternal, userContext, returnResult>(args);
283+
WrappedScript::EvalMachine<
284+
unwrapExternal, userContext, returnResult, useTimeout>(args);
279285
}
280286

281287

282288
Handle<Value> WrappedScript::RunInThisContext(const Arguments& args) {
283289
return
284-
WrappedScript::EvalMachine<unwrapExternal, thisContext, returnResult>(args);
290+
WrappedScript::EvalMachine<
291+
unwrapExternal, thisContext, returnResult, useTimeout>(args);
285292
}
286293

287294

288295
Handle<Value> WrappedScript::RunInNewContext(const Arguments& args) {
289296
return
290-
WrappedScript::EvalMachine<unwrapExternal, newContext, returnResult>(args);
297+
WrappedScript::EvalMachine<
298+
unwrapExternal, newContext, returnResult, useTimeout>(args);
291299
}
292300

293301

294302
Handle<Value> WrappedScript::CompileRunInContext(const Arguments& args) {
295303
return
296-
WrappedScript::EvalMachine<compileCode, userContext, returnResult>(args);
304+
WrappedScript::EvalMachine<
305+
compileCode, userContext, returnResult, useTimeout>(args);
297306
}
298307

299308

300309
Handle<Value> WrappedScript::CompileRunInThisContext(const Arguments& args) {
301310
return
302-
WrappedScript::EvalMachine<compileCode, thisContext, returnResult>(args);
311+
WrappedScript::EvalMachine<
312+
compileCode, thisContext, returnResult, useTimeout>(args);
303313
}
304314

305315

306316
Handle<Value> WrappedScript::CompileRunInNewContext(const Arguments& args) {
307317
return
308-
WrappedScript::EvalMachine<compileCode, newContext, returnResult>(args);
318+
WrappedScript::EvalMachine<
319+
compileCode, newContext, returnResult, useTimeout>(args);
309320
}
310321

311322

312323
template <WrappedScript::EvalInputFlags input_flag,
313324
WrappedScript::EvalContextFlags context_flag,
314-
WrappedScript::EvalOutputFlags output_flag>
325+
WrappedScript::EvalOutputFlags output_flag,
326+
WrappedScript::EvalTimeoutFlags timeout_flag>
315327
Handle<Value> WrappedScript::EvalMachine(const Arguments& args) {
316328
HandleScope scope(node_isolate);
317329

@@ -346,7 +358,18 @@ Handle<Value> WrappedScript::EvalMachine(const Arguments& args) {
346358
? args[filename_index]->ToString()
347359
: String::New("evalmachine.<anonymous>");
348360

349-
const int display_error_index = args.Length() - 1;
361+
uint64_t timeout = 0;
362+
const int timeout_index = filename_index + 1;
363+
if (timeout_flag == useTimeout && args.Length() > timeout_index) {
364+
if (!args[timeout_index]->IsUint32()) {
365+
return ThrowException(Exception::TypeError(
366+
String::New("needs an unsigned integer 'ms' argument.")));
367+
}
368+
timeout = args[timeout_index]->Uint32Value();
369+
}
370+
371+
const int display_error_index = timeout_index +
372+
(timeout_flag == noTimeout ? 0 : 1);
350373
bool display_error = false;
351374
if (args.Length() > display_error_index &&
352375
args[display_error_index]->IsBoolean() &&
@@ -416,7 +439,17 @@ Handle<Value> WrappedScript::EvalMachine(const Arguments& args) {
416439

417440

418441
if (output_flag == returnResult) {
419-
result = script->Run();
442+
if (timeout) {
443+
Watchdog wd(timeout);
444+
result = script->Run();
445+
} else {
446+
result = script->Run();
447+
}
448+
if (try_catch.HasCaught() && try_catch.HasTerminated()) {
449+
V8::CancelTerminateExecution(args.GetIsolate());
450+
return ThrowException(Exception::Error(
451+
String::New("Script execution timed out.")));
452+
}
420453
if (result.IsEmpty()) {
421454
if (display_error) DisplayExceptionLine(try_catch);
422455
return try_catch.ReThrow();

src/node_watchdog.cc

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
// Copyright Joyent, Inc. and other Node contributors.
2+
//
3+
// Permission is hereby granted, free of charge, to any person obtaining a
4+
// copy of this software and associated documentation files (the
5+
// "Software"), to deal in the Software without restriction, including
6+
// without limitation the rights to use, copy, modify, merge, publish,
7+
// distribute, sublicense, and/or sell copies of the Software, and to permit
8+
// persons to whom the Software is furnished to do so, subject to the
9+
// following conditions:
10+
//
11+
// The above copyright notice and this permission notice shall be included
12+
// in all copies or substantial portions of the Software.
13+
//
14+
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
15+
// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16+
// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN
17+
// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
18+
// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
19+
// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE
20+
// USE OR OTHER DEALINGS IN THE SOFTWARE.
21+
22+
#include "node_watchdog.h"
23+
24+
namespace node {
25+
26+
using v8::V8;
27+
28+
29+
Watchdog::Watchdog(uint64_t ms)
30+
: timer_started_(false)
31+
, thread_created_(false)
32+
, destroyed_(false) {
33+
34+
loop_ = uv_loop_new();
35+
if (!loop_)
36+
return;
37+
38+
int rc = uv_timer_init(loop_, &timer_);
39+
if (rc) {
40+
return;
41+
}
42+
43+
rc = uv_timer_start(&timer_, &Watchdog::Timer, ms, 0);
44+
if (rc) {
45+
return;
46+
}
47+
timer_started_ = true;
48+
49+
rc = uv_thread_create(&thread_, &Watchdog::Run, this);
50+
if (rc) {
51+
return;
52+
}
53+
thread_created_ = true;
54+
}
55+
56+
57+
Watchdog::~Watchdog() {
58+
Destroy();
59+
}
60+
61+
62+
void Watchdog::Dispose() {
63+
Destroy();
64+
}
65+
66+
67+
void Watchdog::Destroy() {
68+
if (destroyed_) {
69+
return;
70+
}
71+
72+
if (timer_started_) {
73+
uv_timer_stop(&timer_);
74+
}
75+
76+
if (loop_) {
77+
uv_loop_delete(loop_);
78+
}
79+
80+
if (thread_created_) {
81+
uv_thread_join(&thread_);
82+
}
83+
84+
destroyed_ = true;
85+
}
86+
87+
88+
void Watchdog::Run(void* arg) {
89+
Watchdog* wd = static_cast<Watchdog*>(arg);
90+
uv_run(wd->loop_, UV_RUN_DEFAULT);
91+
}
92+
93+
94+
void Watchdog::Timer(uv_timer_t* timer, int status) {
95+
V8::TerminateExecution();
96+
}
97+
98+
99+
} // namespace node

0 commit comments

Comments
 (0)