// v8_utils.cpp /* Copyright 2009 10gen Inc. * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License, version 3, * as published by the Free Software Foundation. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . * * As a special exception, the copyright holders give permission to link the * code of portions of this program with the OpenSSL library under certain * conditions as described in each individual source file and distribute * linked combinations including the program with the OpenSSL library. You * must comply with the GNU Affero General Public License in all respects * for all of the code used other than as permitted herein. If you modify * file(s) with this exception, you may extend this exception to your * version of the file(s), but you are not obligated to do so. If you do not * wish to do so, delete this exception statement from your version. If you * delete this exception statement from all source files in the program, * then also delete it in the license file. */ #define MONGO_LOG_DEFAULT_COMPONENT ::mongo::logger::LogComponent::kDefault #include "mongo/scripting/v8_utils.h" #include #include #include #include #include #include #include #include #include #include #include #include "mongo/platform/cstdint.h" #include "mongo/scripting/engine_v8.h" #include "mongo/scripting/v8_db.h" #include "mongo/util/log.h" #include "mongo/util/mongoutils/str.h" using namespace std; using boost::scoped_ptr; namespace mongo { std::string toSTLString(const v8::Handle& o) { return StringData(V8String(o)).toString(); } /** Get the properties of an object (and its prototype) as a comma-delimited string */ std::string v8ObjectToString(const v8::Handle& o) { v8::Local properties = o->GetPropertyNames(); v8::String::Utf8Value str(properties); massert(16696 , "error converting js type to Utf8Value", *str); return std::string(*str, str.length()); } std::ostream& operator<<(std::ostream& s, const v8::Handle& o) { v8::String::Utf8Value str(o); s << *str; return s; } std::ostream& operator<<(std::ostream& s, const v8::TryCatch* try_catch) { v8::HandleScope handle_scope; v8::String::Utf8Value exceptionText(try_catch->Exception()); v8::Handle message = try_catch->Message(); if (message.IsEmpty()) { s << *exceptionText << endl; } else { v8::String::Utf8Value filename(message->GetScriptResourceName()); int linenum = message->GetLineNumber(); cout << *filename << ":" << linenum << " " << *exceptionText << endl; v8::String::Utf8Value sourceline(message->GetSourceLine()); cout << *sourceline << endl; int start = message->GetStartColumn(); for (int i = 0; i < start; i++) cout << " "; int end = message->GetEndColumn(); for (int i = start; i < end; i++) cout << "^"; cout << endl; } return s; } class JSThreadConfig { public: JSThreadConfig(V8Scope* scope, const v8::Arguments& args) : _started(), _done(), _sharedData(new SharedData()) { jsassert(args.Length() > 0, "need at least one argument"); jsassert(args[0]->IsFunction(), "first argument must be a function"); // arguments need to be copied into the isolate, go through bson BSONObjBuilder b; for(int i = 0; i < args.Length(); ++i) { scope->v8ToMongoElement(b, "arg" + BSONObjBuilder::numStr(i), args[i]); } _sharedData->_args = b.obj(); } ~JSThreadConfig() { } void start() { jsassert(!_started, "Thread already started"); JSThread jt(*this); _thread.reset(new boost::thread(jt)); _started = true; } void join() { jsassert(_started && !_done, "Thread not running"); _thread->join(); _done = true; } /** * Returns true if the JSThread terminated as a result of an error * during its execution, and false otherwise. This operation does * not block, nor does it require join() to have been called. */ bool hasFailed() const { jsassert(_started, "Thread not started"); return _sharedData->getErrored(); } BSONObj returnData() { if (!_done) join(); return _sharedData->_returnData; } private: /* * JSThreadConfig doesn't always outlive its JSThread (for example, if the parent thread * garbage collects the JSThreadConfig before the JSThread has finished running), so any * data shared between them has to go in a shared_ptr. */ class SharedData { public: SharedData() : _errored(false) {} BSONObj _args; BSONObj _returnData; void setErrored(bool value) { boost::lock_guard lck(_erroredMutex); _errored = value; } bool getErrored() { boost::lock_guard lck(_erroredMutex); return _errored; } private: boost::mutex _erroredMutex; bool _errored; }; class JSThread { public: JSThread(JSThreadConfig& config) : _sharedData(config._sharedData) {} void operator()() { try { scoped_ptr scope( static_cast(globalScriptEngine->newScope())); v8::Locker v8lock(scope->getIsolate()); v8::Isolate::Scope iscope(scope->getIsolate()); v8::HandleScope handle_scope; v8::Context::Scope context_scope(scope->getContext()); BSONObj args = _sharedData->_args; v8::Local f = v8::Function::Cast( *(scope->mongoToV8Element(args.firstElement(), true))); int argc = args.nFields() - 1; // TODO SERVER-8016: properly allocate handles on the stack v8::Local argv[24]; BSONObjIterator it(args); it.next(); for(int i = 0; i < argc && i < 24; ++i) { argv[i] = v8::Local::New( scope->mongoToV8Element(*it, true)); it.next(); } v8::TryCatch try_catch; v8::Handle ret = f->Call(scope->getContext()->Global(), argc, argv); if (ret.IsEmpty() || try_catch.HasCaught()) { string e = scope->v8ExceptionToSTLString(&try_catch); log() << "js thread raised js exception: " << e << endl; ret = v8::Undefined(); _sharedData->setErrored(true); } // ret is translated to BSON to switch isolate BSONObjBuilder b; scope->v8ToMongoElement(b, "ret", ret); _sharedData->_returnData = b.obj(); } catch (const DBException& e) { // Keeping behavior the same as for js exceptions. log() << "js thread threw c++ exception: " << e.toString(); _sharedData->setErrored(true); _sharedData->_returnData = BSON("ret" << BSONUndefined); } catch (const std::exception& e) { log() << "js thread threw c++ exception: " << e.what(); _sharedData->setErrored(true); _sharedData->_returnData = BSON("ret" << BSONUndefined); } catch (...) { log() << "js thread threw c++ non-exception"; _sharedData->setErrored(true); _sharedData->_returnData = BSON("ret" << BSONUndefined); } } private: boost::shared_ptr _sharedData; }; bool _started; bool _done; scoped_ptr _thread; boost::shared_ptr _sharedData; }; class CountDownLatchHolder { private: struct Latch { Latch(int32_t count) : count(count) {} boost::condition_variable cv; boost::mutex mutex; int32_t count; }; boost::shared_ptr get(int32_t desc) { boost::lock_guard lock(_mutex); Map::iterator iter = _latches.find(desc); jsassert(iter != _latches.end(), "not a valid CountDownLatch descriptor"); return iter->second; } typedef std::map< int32_t, boost::shared_ptr > Map; Map _latches; boost::mutex _mutex; int32_t _counter; public: CountDownLatchHolder() : _counter(0) {} int32_t make(int32_t count) { jsassert(count >= 0, "argument must be >= 0"); boost::lock_guard lock(_mutex); int32_t desc = ++_counter; _latches.insert(std::make_pair(desc, boost::make_shared(count))); return desc; } void await(int32_t desc) { boost::shared_ptr latch = get(desc); boost::unique_lock lock(latch->mutex); while (latch->count != 0) { latch->cv.wait(lock); } } void countDown(int32_t desc) { boost::shared_ptr latch = get(desc); boost::unique_lock lock(latch->mutex); if (latch->count > 0) { latch->count--; } if (latch->count == 0) { latch->cv.notify_all(); } } int32_t getCount(int32_t desc) { boost::shared_ptr latch = get(desc); boost::unique_lock lock(latch->mutex); return latch->count; } }; namespace { CountDownLatchHolder globalCountDownLatchHolder; } v8::Handle CountDownLatchNew(V8Scope* scope, const v8::Arguments& args) { jsassert(args.Length() == 1, "need exactly one argument"); jsassert(args[0]->IsInt32(), "argument must be an integer"); int32_t count = v8::Local::Cast(args[0])->Value(); return v8::Int32::New(globalCountDownLatchHolder.make(count)); } v8::Handle CountDownLatchAwait(V8Scope* scope, const v8::Arguments& args) { jsassert(args.Length() == 1, "need exactly one argument"); jsassert(args[0]->IsInt32(), "argument must be an integer"); globalCountDownLatchHolder.await(args[0]->ToInt32()->Value()); return v8::Undefined(); } v8::Handle CountDownLatchCountDown(V8Scope* scope, const v8::Arguments& args) { jsassert(args.Length() == 1, "need exactly one argument"); jsassert(args[0]->IsInt32(), "argument must be an integer"); globalCountDownLatchHolder.countDown(args[0]->ToInt32()->Value()); return v8::Undefined(); } v8::Handle CountDownLatchGetCount(V8Scope* scope, const v8::Arguments& args) { jsassert(args.Length() == 1, "need exactly one argument"); jsassert(args[0]->IsInt32(), "argument must be an integer"); return v8::Int32::New(globalCountDownLatchHolder.getCount(args[0]->ToInt32()->Value())); } JSThreadConfig *thisConfig(V8Scope* scope, const v8::Arguments& args) { v8::Local c = v8::External::Cast( *(args.This()->GetHiddenValue(v8::String::New("_JSThreadConfig")))); JSThreadConfig *config = static_cast*>(c->Value())->get(); return config; } v8::Handle ThreadInit(V8Scope* scope, const v8::Arguments& args) { v8::Persistent self = v8::Persistent::New(args.This()); JSThreadConfig* config = new JSThreadConfig(scope, args); v8::Local handle = scope->jsThreadConfigTracker.track(self, config); args.This()->SetHiddenValue(v8::String::New("_JSThreadConfig"), handle); invariant(thisConfig(scope, args) == config); return v8::Undefined(); } v8::Handle ScopedThreadInit(V8Scope* scope, const v8::Arguments& args) { // NOTE: ScopedThread and Thread behave identically because a new V8 Isolate is always // created. return ThreadInit(scope, args); } v8::Handle ThreadStart(V8Scope* scope, const v8::Arguments& args) { thisConfig(scope, args)->start(); return v8::Undefined(); } v8::Handle ThreadJoin(V8Scope* scope, const v8::Arguments& args) { thisConfig(scope, args)->join(); return v8::Undefined(); } // Indicates to the caller that the thread terminated as a result of an error. v8::Handle ThreadHasFailed(V8Scope* scope, const v8::Arguments& args) { bool hasFailed = thisConfig(scope, args)->hasFailed(); return v8::Boolean::New(hasFailed); } v8::Handle ThreadReturnData(V8Scope* scope, const v8::Arguments& args) { BSONObj data = thisConfig(scope, args)->returnData(); return scope->mongoToV8Element(data.firstElement(), true); } v8::Handle ThreadInject(V8Scope* scope, const v8::Arguments& args) { v8::HandleScope handle_scope; jsassert(args.Length() == 1, "threadInject takes exactly 1 argument"); jsassert(args[0]->IsObject(), "threadInject needs to be passed a prototype"); v8::Local o = args[0]->ToObject(); // install method on the Thread object scope->injectV8Function("init", ThreadInit, o); scope->injectV8Function("start", ThreadStart, o); scope->injectV8Function("join", ThreadJoin, o); scope->injectV8Function("hasFailed", ThreadHasFailed, o); scope->injectV8Function("returnData", ThreadReturnData, o); return handle_scope.Close(v8::Handle()); } v8::Handle ScopedThreadInject(V8Scope* scope, const v8::Arguments& args) { v8::HandleScope handle_scope; jsassert(args.Length() == 1, "threadInject takes exactly 1 argument"); jsassert(args[0]->IsObject(), "threadInject needs to be passed a prototype"); v8::Local o = args[0]->ToObject(); scope->injectV8Function("init", ScopedThreadInit, o); // inheritance takes care of other member functions return handle_scope.Close(v8::Handle()); } void installFork(V8Scope* scope, v8::Handle& global, v8::Handle& context) { scope->injectV8Function("_threadInject", ThreadInject, global); scope->injectV8Function("_scopedThreadInject", ScopedThreadInject, global); scope->setObject("CountDownLatch", BSONObj(), false); v8::Handle cdl = scope->get("CountDownLatch").As(); scope->injectV8Function("_new", CountDownLatchNew, cdl); scope->injectV8Function("_await", CountDownLatchAwait, cdl); scope->injectV8Function("_countDown", CountDownLatchCountDown, cdl); scope->injectV8Function("_getCount", CountDownLatchGetCount, cdl); } v8::Handle v8AssertionException(const char* errorMessage) { return v8::ThrowException(v8::Exception::Error(v8::String::New(errorMessage))); } v8::Handle v8AssertionException(const std::string& errorMessage) { return v8AssertionException(errorMessage.c_str()); } }