vm: add support for timeout argument
authorAndrew Paprocki <andrew@ishiboo.com>
Mon, 8 Oct 2012 19:24:08 +0000 (21:24 +0200)
committerBen Noordhuis <info@bnoordhuis.nl>
Mon, 29 Apr 2013 21:38:19 +0000 (23:38 +0200)
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.

doc/api/vm.markdown
lib/module.js
node.gyp
src/node.js
src/node_script.cc
src/node_watchdog.cc [new file with mode: 0644]
src/node_watchdog.h [new file with mode: 0644]
test/simple/test-vm-run-timeout.js [new file with mode: 0644]

index 49df036..1767493 100644 (file)
@@ -79,12 +79,14 @@ In case of syntax error in `code`, `vm.runInThisContext` emits the syntax error
 and throws an exception.
 
 
-## vm.runInNewContext(code, [sandbox], [filename])
+## vm.runInNewContext(code, [sandbox], [filename], [timeout])
 
 `vm.runInNewContext` compiles `code`, then runs it in `sandbox` and returns the
 result. Running code does not have access to local scope. The object `sandbox`
 will be used as the global object for `code`.
 `sandbox` and `filename` are optional, `filename` is only used in stack traces.
+`timeout` specifies an optional number of milliseconds to execute `code` before
+terminating execution. If execution is terminated, `null` will be thrown.
 
 Example: compile and execute code that increments a global variable and sets a new one.
 These globals are contained in the sandbox.
@@ -108,7 +110,7 @@ requires a separate process.
 In case of syntax error in `code`, `vm.runInNewContext` emits the syntax error to stderr
 and throws an exception.
 
-## vm.runInContext(code, context, [filename])
+## vm.runInContext(code, context, [filename], [timeout])
 
 `vm.runInContext` compiles `code`, then runs it in `context` and returns the
 result. A (V8) context comprises a global object, together with a set of
@@ -116,6 +118,8 @@ built-in objects and functions. Running code does not have access to local scope
 and the global object held within `context` will be used as the global object
 for `code`.
 `filename` is optional, it's used only in stack traces.
+`timeout` specifies an optional number of milliseconds to execute `code` before
+terminating execution. If execution is terminated, `null` will be thrown.
 
 Example: compile and execute code in a existing context.
 
@@ -165,12 +169,14 @@ and throws an exception.
 
 A class for running scripts.  Returned by vm.createScript.
 
-### script.runInThisContext()
+### script.runInThisContext([timeout])
 
 Similar to `vm.runInThisContext` but a method of a precompiled `Script` object.
 `script.runInThisContext` runs the code of `script` and returns the result.
 Running code does not have access to local scope, but does have access to the `global` object
 (v8: in actual context).
+`timeout` specifies an optional number of milliseconds to execute `code` before
+terminating execution. If execution is terminated, `null` will be thrown.
 
 Example of using `script.runInThisContext` to compile code once and run it multiple times:
 
@@ -189,11 +195,13 @@ Example of using `script.runInThisContext` to compile code once and run it multi
     // 1000
 
 
-### script.runInNewContext([sandbox])
+### script.runInNewContext([sandbox], [timeout])
 
 Similar to `vm.runInNewContext` a method of a precompiled `Script` object.
 `script.runInNewContext` runs the code of `script` with `sandbox` as the global object and returns the result.
 Running code does not have access to local scope. `sandbox` is optional.
+`timeout` specifies an optional number of milliseconds to execute `code` before
+terminating execution. If execution is terminated, `null` will be thrown.
 
 Example: compile code that increments a global variable and sets one, then execute this code multiple times.
 These globals are contained in the sandbox.
index 27bba73..460d870 100644 (file)
@@ -419,7 +419,7 @@ Module.prototype._compile = function(content, filename) {
       sandbox.global = sandbox;
       sandbox.root = root;
 
-      return runInNewContext(content, sandbox, filename, true);
+      return runInNewContext(content, sandbox, filename, 0, true);
     }
 
     debug('load root module');
@@ -430,13 +430,13 @@ Module.prototype._compile = function(content, filename) {
     global.__dirname = dirname;
     global.module = self;
 
-    return runInThisContext(content, filename, true);
+    return runInThisContext(content, filename, 0, true);
   }
 
   // create wrapper function
   var wrapper = Module.wrap(content);
 
-  var compiledWrapper = runInThisContext(wrapper, filename, true);
+  var compiledWrapper = runInThisContext(wrapper, filename, 0, true);
   if (global.v8debug) {
     if (!resolvedArgv) {
       // we enter the repl if we're not given a filename argument.
index 6179a51..bb183db 100644 (file)
--- a/node.gyp
+++ b/node.gyp
@@ -99,6 +99,7 @@
         'src/node_script.cc',
         'src/node_stat_watcher.cc',
         'src/node_string.cc',
+        'src/node_watchdog.cc',
         'src/node_zlib.cc',
         'src/pipe_wrap.cc',
         'src/signal_wrap.cc',
         'src/node_script.h',
         'src/node_string.h',
         'src/node_version.h',
+        'src/node_watchdog.h',
         'src/ngx-queue.h',
         'src/pipe_wrap.h',
         'src/tty_wrap.h',
index e5833cb..0b5fe39 100644 (file)
                'global.require = require;\n' +
                'return require("vm").runInThisContext(' +
                JSON.stringify(body) + ', ' +
-               JSON.stringify(name) + ', true);\n';
+               JSON.stringify(name) + ', 0, true);\n';
     }
     var result = module._compile(script, name + '-wrapper');
     if (process._print_eval) console.log(result);
     var source = NativeModule.getSource(this.id);
     source = NativeModule.wrap(source);
 
-    var fn = runInThisContext(source, this.filename, true);
+    var fn = runInThisContext(source, this.filename, 0, true);
     fn(this.exports, NativeModule.require, this, this.filename);
 
     this.loaded = true;
index 854ee80..21e169d 100644 (file)
@@ -21,6 +21,7 @@
 
 #include "node.h"
 #include "node_script.h"
+#include "node_watchdog.h"
 #include <assert.h>
 
 namespace node {
@@ -42,6 +43,7 @@ using v8::Persistent;
 using v8::Integer;
 using v8::Function;
 using v8::FunctionTemplate;
+using v8::V8;
 
 
 class WrappedContext : ObjectWrap {
@@ -74,10 +76,12 @@ class WrappedScript : ObjectWrap {
   enum EvalInputFlags { compileCode, unwrapExternal };
   enum EvalContextFlags { thisContext, newContext, userContext };
   enum EvalOutputFlags { returnResult, wrapExternal };
+  enum EvalTimeoutFlags { noTimeout, useTimeout };
 
   template <EvalInputFlags input_flag,
             EvalContextFlags context_flag,
-            EvalOutputFlags output_flag>
+            EvalOutputFlags output_flag,
+            EvalTimeoutFlags timeout_flag>
   static Handle<Value> EvalMachine(const Arguments& args);
 
  protected:
@@ -243,7 +247,8 @@ Handle<Value> WrappedScript::New(const Arguments& args) {
   t->Wrap(args.This());
 
   return
-    WrappedScript::EvalMachine<compileCode, thisContext, wrapExternal>(args);
+    WrappedScript::EvalMachine<
+      compileCode, thisContext, wrapExternal, noTimeout>(args);
 }
 
 
@@ -275,43 +280,50 @@ Handle<Value> WrappedScript::CreateContext(const Arguments& args) {
 
 Handle<Value> WrappedScript::RunInContext(const Arguments& args) {
   return
-    WrappedScript::EvalMachine<unwrapExternal, userContext, returnResult>(args);
+    WrappedScript::EvalMachine<
+      unwrapExternal, userContext, returnResult, useTimeout>(args);
 }
 
 
 Handle<Value> WrappedScript::RunInThisContext(const Arguments& args) {
   return
-    WrappedScript::EvalMachine<unwrapExternal, thisContext, returnResult>(args);
+    WrappedScript::EvalMachine<
+      unwrapExternal, thisContext, returnResult, useTimeout>(args);
 }
 
 
 Handle<Value> WrappedScript::RunInNewContext(const Arguments& args) {
   return
-    WrappedScript::EvalMachine<unwrapExternal, newContext, returnResult>(args);
+    WrappedScript::EvalMachine<
+      unwrapExternal, newContext, returnResult, useTimeout>(args);
 }
 
 
 Handle<Value> WrappedScript::CompileRunInContext(const Arguments& args) {
   return
-    WrappedScript::EvalMachine<compileCode, userContext, returnResult>(args);
+    WrappedScript::EvalMachine<
+      compileCode, userContext, returnResult, useTimeout>(args);
 }
 
 
 Handle<Value> WrappedScript::CompileRunInThisContext(const Arguments& args) {
   return
-    WrappedScript::EvalMachine<compileCode, thisContext, returnResult>(args);
+    WrappedScript::EvalMachine<
+      compileCode, thisContext, returnResult, useTimeout>(args);
 }
 
 
 Handle<Value> WrappedScript::CompileRunInNewContext(const Arguments& args) {
   return
-    WrappedScript::EvalMachine<compileCode, newContext, returnResult>(args);
+    WrappedScript::EvalMachine<
+      compileCode, newContext, returnResult, useTimeout>(args);
 }
 
 
 template <WrappedScript::EvalInputFlags input_flag,
           WrappedScript::EvalContextFlags context_flag,
-          WrappedScript::EvalOutputFlags output_flag>
+          WrappedScript::EvalOutputFlags output_flag,
+          WrappedScript::EvalTimeoutFlags timeout_flag>
 Handle<Value> WrappedScript::EvalMachine(const Arguments& args) {
   HandleScope scope(node_isolate);
 
@@ -346,7 +358,18 @@ Handle<Value> WrappedScript::EvalMachine(const Arguments& args) {
                            ? args[filename_index]->ToString()
                            : String::New("evalmachine.<anonymous>");
 
-  const int display_error_index = args.Length() - 1;
+  uint64_t timeout = 0;
+  const int timeout_index = filename_index + 1;
+  if (timeout_flag == useTimeout && args.Length() > timeout_index) {
+    if (!args[timeout_index]->IsUint32()) {
+      return ThrowException(Exception::TypeError(
+            String::New("needs an unsigned integer 'ms' argument.")));
+    }
+    timeout = args[timeout_index]->Uint32Value();
+  }
+
+  const int display_error_index = timeout_index +
+                                  (timeout_flag == noTimeout ? 0 : 1);
   bool display_error = false;
   if (args.Length() > display_error_index &&
       args[display_error_index]->IsBoolean() &&
@@ -416,7 +439,17 @@ Handle<Value> WrappedScript::EvalMachine(const Arguments& args) {
 
 
   if (output_flag == returnResult) {
-    result = script->Run();
+    if (timeout) {
+      Watchdog wd(timeout);
+      result = script->Run();
+    } else {
+      result = script->Run();
+    }
+    if (try_catch.HasCaught() && try_catch.HasTerminated()) {
+      V8::CancelTerminateExecution(args.GetIsolate());
+      return ThrowException(Exception::Error(
+            String::New("Script execution timed out.")));
+    }
     if (result.IsEmpty()) {
       if (display_error) DisplayExceptionLine(try_catch);
       return try_catch.ReThrow();
diff --git a/src/node_watchdog.cc b/src/node_watchdog.cc
new file mode 100644 (file)
index 0000000..a8ec719
--- /dev/null
@@ -0,0 +1,99 @@
+// Copyright Joyent, Inc. and other Node contributors.
+//
+// Permission is hereby granted, free of charge, to any person obtaining a
+// copy of this software and associated documentation files (the
+// "Software"), to deal in the Software without restriction, including
+// without limitation the rights to use, copy, modify, merge, publish,
+// distribute, sublicense, and/or sell copies of the Software, and to permit
+// persons to whom the Software is furnished to do so, subject to the
+// following conditions:
+//
+// The above copyright notice and this permission notice shall be included
+// in all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
+// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN
+// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
+// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
+// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE
+// USE OR OTHER DEALINGS IN THE SOFTWARE.
+
+#include "node_watchdog.h"
+
+namespace node {
+
+using v8::V8;
+
+
+Watchdog::Watchdog(uint64_t ms)
+  : timer_started_(false)
+  , thread_created_(false)
+  , destroyed_(false) {
+
+  loop_ = uv_loop_new();
+  if (!loop_)
+    return;
+
+  int rc = uv_timer_init(loop_, &timer_);
+  if (rc) {
+    return;
+  }
+
+  rc = uv_timer_start(&timer_, &Watchdog::Timer, ms, 0);
+  if (rc) {
+    return;
+  }
+  timer_started_ = true;
+
+  rc = uv_thread_create(&thread_, &Watchdog::Run, this);
+  if (rc) {
+    return;
+  }
+  thread_created_ = true;
+}
+
+
+Watchdog::~Watchdog() {
+  Destroy();
+}
+
+
+void Watchdog::Dispose() {
+  Destroy();
+}
+
+
+void Watchdog::Destroy() {
+  if (destroyed_) {
+    return;
+  }
+
+  if (timer_started_) {
+    uv_timer_stop(&timer_);
+  }
+
+  if (loop_) {
+    uv_loop_delete(loop_);
+  }
+
+  if (thread_created_) {
+    uv_thread_join(&thread_);
+  }
+
+  destroyed_ = true;
+}
+
+
+void Watchdog::Run(void* arg) {
+  Watchdog* wd = static_cast<Watchdog*>(arg);
+  uv_run(wd->loop_, UV_RUN_DEFAULT);
+}
+
+
+void Watchdog::Timer(uv_timer_t* timer, int status) {
+  V8::TerminateExecution();
+}
+
+
+}  // namespace node
diff --git a/src/node_watchdog.h b/src/node_watchdog.h
new file mode 100644 (file)
index 0000000..1bb4317
--- /dev/null
@@ -0,0 +1,53 @@
+// Copyright Joyent, Inc. and other Node contributors.
+//
+// Permission is hereby granted, free of charge, to any person obtaining a
+// copy of this software and associated documentation files (the
+// "Software"), to deal in the Software without restriction, including
+// without limitation the rights to use, copy, modify, merge, publish,
+// distribute, sublicense, and/or sell copies of the Software, and to permit
+// persons to whom the Software is furnished to do so, subject to the
+// following conditions:
+//
+// The above copyright notice and this permission notice shall be included
+// in all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
+// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN
+// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
+// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
+// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE
+// USE OR OTHER DEALINGS IN THE SOFTWARE.
+
+#ifndef SRC_NODE_WATCHDOG_H_
+#define SRC_NODE_WATCHDOG_H_
+
+#include "v8.h"
+#include "uv.h"
+
+namespace node {
+
+class Watchdog {
+ public:
+  Watchdog(uint64_t ms);
+  ~Watchdog();
+
+  void Dispose();
+
+ private:
+  void Destroy();
+
+  static void Run(void* arg);
+  static void Timer(uv_timer_t* timer, int status);
+
+  uv_thread_t thread_;
+  uv_loop_t* loop_;
+  uv_timer_t timer_;
+  bool timer_started_;
+  bool thread_created_;
+  bool destroyed_;
+};
+
+}  // namespace node
+
+#endif  // SRC_NODE_WATCHDOG_H_
diff --git a/test/simple/test-vm-run-timeout.js b/test/simple/test-vm-run-timeout.js
new file mode 100644 (file)
index 0000000..ccc95f9
--- /dev/null
@@ -0,0 +1,37 @@
+// Copyright Joyent, Inc. and other Node contributors.
+//
+// Permission is hereby granted, free of charge, to any person obtaining a
+// copy of this software and associated documentation files (the
+// "Software"), to deal in the Software without restriction, including
+// without limitation the rights to use, copy, modify, merge, publish,
+// distribute, sublicense, and/or sell copies of the Software, and to permit
+// persons to whom the Software is furnished to do so, subject to the
+// following conditions:
+//
+// The above copyright notice and this permission notice shall be included
+// in all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
+// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN
+// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
+// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
+// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE
+// USE OR OTHER DEALINGS IN THE SOFTWARE.
+
+var common = require('../common');
+var assert = require('assert');
+var vm = require('vm');
+
+assert.throws(function() {
+  vm.runInThisContext('while(true) {}', '', 100);
+});
+
+assert.throws(function() {
+  vm.runInThisContext('', '', -1);
+});
+
+assert.doesNotThrow(function() {
+  vm.runInThisContext('', '', 0);
+  vm.runInThisContext('', '', 100);
+});