From 15dc9e157b7d37fcf3696930cf09eb2a6670bb3a Mon Sep 17 00:00:00 2001 From: Simon Best Date: Tue, 9 Apr 2013 22:52:42 +0100 Subject: [PATCH] Script timeout thread to forcefully terminate the current thread of V8 execution in the corresponding isolate. Note that threads are implemented using std::thread which is only available in C++0x. The relevant compile flags have been added but compiler support has not been tested and is therefore not guaranteed. --- config.m4 | 2 +- v8js.cc | 137 ++++++++++++++++++++++++++++++++++++++++++++++++------ 2 files changed, 123 insertions(+), 16 deletions(-) diff --git a/config.m4 b/config.m4 index 2b13bfd..5fdac77 100644 --- a/config.m4 +++ b/config.m4 @@ -67,7 +67,7 @@ LDFLAGS=$old_LDFLAGS AC_DEFINE_UNQUOTED([PHP_V8_VERSION], "$ac_cv_v8_version", [ ]) fi - PHP_NEW_EXTENSION(v8js, v8js.cc v8js_convert.cc v8js_methods.cc v8js_variables.cc, $ext_shared) + PHP_NEW_EXTENSION(v8js, v8js.cc v8js_convert.cc v8js_methods.cc v8js_variables.cc, $ext_shared, , "-std=c++0x") PHP_ADD_MAKEFILE_FRAGMENT fi diff --git a/v8js.cc b/v8js.cc index 41ddfab..643f572 100644 --- a/v8js.cc +++ b/v8js.cc @@ -38,10 +38,34 @@ extern "C" { #include #include "php_v8js_macros.h" +#include +#include +#include + /* Forward declarations */ static void php_v8js_throw_exception(v8::TryCatch * TSRMLS_DC); static void php_v8js_create_exception(zval *, v8::TryCatch * TSRMLS_DC); +/* {{{ Context container */ +struct php_v8js_ctx { + zend_object std; + v8::Persistent object_name; + v8::Persistent context; + zend_bool report_uncaught; + zval *pending_exception; + int in_execution; + v8::Isolate *isolate; + bool execution_terminated; +}; +/* }}} */ + +// Timer context +struct php_v8js_timer_ctx +{ + std::chrono::time_point time_point; + php_v8js_ctx *v8js_ctx; +}; + /* Module globals */ ZEND_BEGIN_MODULE_GLOBALS(v8js) int v8_initialized; @@ -52,6 +76,11 @@ ZEND_BEGIN_MODULE_GLOBALS(v8js) /* Ini globals */ char *v8_flags; /* V8 command line flags */ int max_disposed_contexts; /* Max disposed context allowed before forcing V8 GC */ + + // Timer thread globals + std::stack timer_stack; + std::thread *timer_thread; + bool timer_stop; ZEND_END_MODULE_GLOBALS(v8js) #ifdef ZTS @@ -126,17 +155,6 @@ struct php_v8js_jsext { }; /* }}} */ -/* {{{ Context container */ -struct php_v8js_ctx { - zend_object std; - v8::Persistent object_name; - v8::Persistent context; - zend_bool report_uncaught; - zval *pending_exception; - int in_execution; -}; -/* }}} */ - /* {{{ Object container */ struct php_v8js_object { zend_object std; @@ -579,6 +597,8 @@ static PHP_METHOD(V8Js, __construct) c->report_uncaught = report_uncaught; c->pending_exception = NULL; c->in_execution = 0; + c->isolate = v8::Isolate::New(); + c->execution_terminated = false; /* Initialize V8 */ php_v8js_init(TSRMLS_C); @@ -597,6 +617,10 @@ static PHP_METHOD(V8Js, __construct) /* Declare configuration for extensions */ v8::ExtensionConfiguration extension_conf(exts_count, exts); + // Isolate execution + v8::Locker locker(c->isolate); + v8::Isolate::Scope isolate_scope(c->isolate); + /* Handle scope */ v8::HandleScope handle_scope; @@ -663,17 +687,73 @@ static PHP_METHOD(V8Js, __construct) } \ \ (ctx) = (php_v8js_ctx *) zend_object_store_get_object(object TSRMLS_CC); \ + v8::Locker locker((ctx)->isolate); \ + v8::Isolate::Scope isolate_scope((ctx)->isolate); \ v8::Context::Scope context_scope((ctx)->context); +static void timer_push(long timeout, php_v8js_ctx *c) +{ + // Create context for this timer + php_v8js_timer_ctx *timer_ctx = (php_v8js_timer_ctx *)malloc(sizeof(php_v8js_timer_ctx)); + + // Calculate the time point when the timeout is exceeded + std::chrono::milliseconds duration(timeout); + std::chrono::time_point from = std::chrono::high_resolution_clock::now(); + + // Push the timer context + timer_ctx->time_point = from + duration; + timer_ctx->v8js_ctx = c; + V8JSG(timer_stack).push(timer_ctx); +} + +static void timer_pop() +{ + if (V8JSG(timer_stack).size()) { + // Free the timer context memory + php_v8js_timer_ctx *timer_ctx = V8JSG(timer_stack).top(); + free(timer_ctx); + + // Remove the timer context from the stack + V8JSG(timer_stack).pop(); + } +} + +static void php_v8js_timer_thread() +{ + while (!V8JSG(timer_stop)) { + std::chrono::time_point now = std::chrono::high_resolution_clock::now(); + + if (V8JSG(timer_stack).size()) { + // Get the current timer context + php_v8js_timer_ctx *timer_ctx = V8JSG(timer_stack).top(); + php_v8js_ctx *c = timer_ctx->v8js_ctx; + + if (now > timer_ctx->time_point) { + // Forcefully terminate the current thread of V8 execution in the isolate + v8::V8::TerminateExecution(c->isolate); + + // Remove this timer from the stack + timer_pop(); + + c->execution_terminated = true; + } + } + + // Sleep for 10ms + std::chrono::milliseconds duration(10); + std::this_thread::sleep_for(duration); + } +} + /* {{{ proto mixed V8Js::executeString(string script [, string identifier [, int flags]]) */ static PHP_METHOD(V8Js, executeString) { char *str = NULL, *identifier = NULL; int str_len = 0, identifier_len = 0; - long flags = V8JS_FLAG_NONE; + long flags = V8JS_FLAG_NONE, timeout = 0; - if (zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "s|sl", &str, &str_len, &identifier, &identifier_len, &flags) == FAILURE) { + if (zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "s|sll", &str, &str_len, &identifier, &identifier_len, &flags, &timeout) == FAILURE) { return; } @@ -700,14 +780,32 @@ static PHP_METHOD(V8Js, executeString) /* Set flags for runtime use */ V8JS_GLOBAL_SET_FLAGS(flags); + if (timeout > 0) { + // If timer thread is not running then start it + if (!V8JSG(timer_thread)) { + // If not, start timer thread + V8JSG(timer_thread) = new std::thread(php_v8js_timer_thread); + } + + timer_push(timeout, c); + } + /* Execute script */ c->in_execution++; v8::Local result = script->Run(); c->in_execution--; - /* Script possibly terminated, return immediately */ + if (timeout > 0) { + timer_pop(); + } + + if (c->execution_terminated) { + // Execution has been terminated due to timeout + zend_throw_exception(php_ce_v8js_exception, "Script timeout", 0 TSRMLS_CC); + } + if (!try_catch.CanContinue()) { - /* TODO: throw PHP exception here? */ + // At this point we can't re-throw the exception return; } @@ -920,6 +1018,7 @@ ZEND_BEGIN_ARG_INFO_EX(arginfo_v8js_executestring, 0, 0, 1) ZEND_ARG_INFO(0, script) ZEND_ARG_INFO(0, identifier) ZEND_ARG_INFO(0, flags) + ZEND_ARG_INFO(0, timeout) ZEND_END_ARG_INFO() ZEND_BEGIN_ARG_INFO(arginfo_v8js_getpendingexception, 0) @@ -1211,6 +1310,12 @@ static PHP_MSHUTDOWN_FUNCTION(v8js) */ static PHP_RSHUTDOWN_FUNCTION(v8js) { + // If the timer thread is running then stop it + if (V8JSG(timer_thread)) { + V8JSG(timer_stop) = true; + V8JSG(timer_thread)->join(); + } + #if V8JS_DEBUG v8::HeapStatistics stats; v8::V8::GetHeapStatistics(&stats); @@ -1254,6 +1359,8 @@ static PHP_GINIT_FUNCTION(v8js) v8js_globals->disposed_contexts = 0; v8js_globals->v8_initialized = 0; v8js_globals->v8_flags = NULL; + v8js_globals->timer_thread = NULL; + v8js_globals->timer_stop = false; } /* }}} */