diff options
| author | Antonin Kral <a.kral@bobek.cz> | 2012-08-29 20:54:51 +0200 |
|---|---|---|
| committer | Antonin Kral <a.kral@bobek.cz> | 2012-08-29 20:54:51 +0200 |
| commit | 83957b73f9177f6e38bd5375bd93ca1f6a47188c (patch) | |
| tree | f20b7d6ac9a9c64ff5bb6b5910a24abbb356b1d5 /src/mongo/scripting | |
| parent | 5071d203970edd4c995493d810abe20987e76fe9 (diff) | |
Imported Upstream version 2.2.0upstream/2.2.0
Diffstat (limited to 'src/mongo/scripting')
| -rw-r--r-- | src/mongo/scripting/bench.cpp | 884 | ||||
| -rw-r--r-- | src/mongo/scripting/bench.h | 412 | ||||
| -rw-r--r-- | src/mongo/scripting/bson_template_evaluator.cpp | 157 | ||||
| -rw-r--r-- | src/mongo/scripting/bson_template_evaluator.h | 152 | ||||
| -rw-r--r-- | src/mongo/scripting/bson_template_evaluator_test.cpp | 360 | ||||
| -rw-r--r-- | src/mongo/scripting/engine.cpp | 521 | ||||
| -rw-r--r-- | src/mongo/scripting/engine.h | 231 | ||||
| -rw-r--r-- | src/mongo/scripting/engine_none.cpp | 24 | ||||
| -rw-r--r-- | src/mongo/scripting/engine_spidermonkey.cpp | 2010 | ||||
| -rw-r--r-- | src/mongo/scripting/engine_spidermonkey.h | 101 | ||||
| -rw-r--r-- | src/mongo/scripting/engine_v8.cpp | 1706 | ||||
| -rw-r--r-- | src/mongo/scripting/engine_v8.h | 269 | ||||
| -rw-r--r-- | src/mongo/scripting/sm_db.cpp | 1716 | ||||
| -rw-r--r-- | src/mongo/scripting/utils.cpp | 71 | ||||
| -rw-r--r-- | src/mongo/scripting/v8_db.cpp | 1145 | ||||
| -rw-r--r-- | src/mongo/scripting/v8_db.h | 94 | ||||
| -rw-r--r-- | src/mongo/scripting/v8_utils.cpp | 295 | ||||
| -rw-r--r-- | src/mongo/scripting/v8_utils.h | 43 | ||||
| -rw-r--r-- | src/mongo/scripting/v8_wrapper.cpp | 91 | ||||
| -rw-r--r-- | src/mongo/scripting/v8_wrapper.h | 34 |
20 files changed, 10316 insertions, 0 deletions
diff --git a/src/mongo/scripting/bench.cpp b/src/mongo/scripting/bench.cpp new file mode 100644 index 00000000000..db3ca7351a7 --- /dev/null +++ b/src/mongo/scripting/bench.cpp @@ -0,0 +1,884 @@ +/** @file bench.cpp */ + +/* + * Copyright (C) 2010 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 <http://www.gnu.org/licenses/>. + */ + + + +#include "pch.h" + +#include "mongo/scripting/bench.h" + +#include <boost/thread/thread.hpp> + +#include "mongo/client/dbclientcursor.h" +#include "mongo/scripting/engine.h" +#include "mongo/util/md5.h" +#include "mongo/util/timer.h" +#include "mongo/util/version.h" + + +// --------------------------------- +// ---- benchmarking system -------- +// --------------------------------- + +// TODO: Maybe extract as library to avoid code duplication? +namespace { + inline pcrecpp::RE_Options flags2options(const char* flags) { + pcrecpp::RE_Options options; + options.set_utf8(true); + while ( flags && *flags ) { + if ( *flags == 'i' ) + options.set_caseless(true); + else if ( *flags == 'm' ) + options.set_multiline(true); + else if ( *flags == 'x' ) + options.set_extended(true); + flags++; + } + return options; + } +} + +namespace mongo { + + BenchRunEventCounter::BenchRunEventCounter() { + reset(); + } + + BenchRunEventCounter::~BenchRunEventCounter() {} + + void BenchRunEventCounter::reset() { + _numEvents = 0; + _totalTimeMicros = 0; + } + + void BenchRunEventCounter::updateFrom(const BenchRunEventCounter &other) { + _numEvents += other._numEvents; + _totalTimeMicros += other._totalTimeMicros; + } + + BenchRunStats::BenchRunStats() { + reset(); + } + + BenchRunStats::~BenchRunStats() {} + + void BenchRunStats::reset() { + error = false; + errCount = 0; + + findOneCounter.reset(); + updateCounter.reset(); + insertCounter.reset(); + deleteCounter.reset(); + queryCounter.reset(); + + trappedErrors.clear(); + } + + void BenchRunStats::updateFrom(const BenchRunStats &other) { + if (other.error) + error = true; + errCount += other.errCount; + + findOneCounter.updateFrom(other.findOneCounter); + updateCounter.updateFrom(other.updateCounter); + insertCounter.updateFrom(other.insertCounter); + deleteCounter.updateFrom(other.deleteCounter); + queryCounter.updateFrom(other.queryCounter); + + for (size_t i = 0; i < other.trappedErrors.size(); ++i) + trappedErrors.push_back(other.trappedErrors[i]); + } + + BenchRunConfig::BenchRunConfig() { + initializeToDefaults(); + } + + void BenchRunConfig::initializeToDefaults() { + host = "localhost"; + db = "test"; + username = ""; + password = ""; + + parallel = 1; + seconds = 1; + hideResults = true; + handleErrors = false; + hideErrors = false; + + trapPattern.reset(); + noTrapPattern.reset(); + watchPattern.reset(); + noWatchPattern.reset(); + + ops = BSONObj(); + + throwGLE = false; + breakOnTrap = true; + } + + BenchRunConfig *BenchRunConfig::createFromBson( const BSONObj &args ) { + BenchRunConfig *config = new BenchRunConfig(); + config->initializeFromBson( args ); + return config; + } + + void BenchRunConfig::initializeFromBson( const BSONObj &args ) { + initializeToDefaults(); + + if ( args["host"].type() == String ) + this->host = args["host"].String(); + if ( args["db"].type() == String ) + this->db = args["db"].String(); + if ( args["username"].type() == String ) + this->username = args["username"].String(); + if ( args["password"].type() == String ) + this->db = args["password"].String(); + + if ( args["parallel"].isNumber() ) + this->parallel = args["parallel"].numberInt(); + if ( args["seconds"].isNumber() ) + this->seconds = args["seconds"].number(); + if ( ! args["hideResults"].eoo() ) + this->hideResults = args["hideResults"].trueValue(); + if ( ! args["handleErrors"].eoo() ) + this->handleErrors = args["handleErrors"].trueValue(); + if ( ! args["hideErrors"].eoo() ) + this->hideErrors = args["hideErrors"].trueValue(); + if ( ! args["throwGLE"].eoo() ) + this->throwGLE = args["throwGLE"].trueValue(); + if ( ! args["breakOnTrap"].eoo() ) + this->breakOnTrap = args["breakOnTrap"].trueValue(); + + uassert(16164, "loopCommands config not supported", args["loopCommands"].eoo()); + + if ( ! args["trapPattern"].eoo() ){ + const char* regex = args["trapPattern"].regex(); + const char* flags = args["trapPattern"].regexFlags(); + this->trapPattern = shared_ptr< pcrecpp::RE >( new pcrecpp::RE( regex, flags2options( flags ) ) ); + } + + if ( ! args["noTrapPattern"].eoo() ){ + const char* regex = args["noTrapPattern"].regex(); + const char* flags = args["noTrapPattern"].regexFlags(); + this->noTrapPattern = shared_ptr< pcrecpp::RE >( new pcrecpp::RE( regex, flags2options( flags ) ) ); + } + + if ( ! args["watchPattern"].eoo() ){ + const char* regex = args["watchPattern"].regex(); + const char* flags = args["watchPattern"].regexFlags(); + this->watchPattern = shared_ptr< pcrecpp::RE >( new pcrecpp::RE( regex, flags2options( flags ) ) ); + } + + if ( ! args["noWatchPattern"].eoo() ){ + const char* regex = args["noWatchPattern"].regex(); + const char* flags = args["noWatchPattern"].regexFlags(); + this->noWatchPattern = shared_ptr< pcrecpp::RE >( new pcrecpp::RE( regex, flags2options( flags ) ) ); + } + + this->ops = args["ops"].Obj().getOwned(); + } + + DBClientBase *BenchRunConfig::createConnection() const { + std::string errorMessage; + ConnectionString connectionString = ConnectionString::parse( host, errorMessage ); + uassert( 16157, errorMessage, connectionString.isValid() ); + DBClientBase *connection = connectionString.connect(errorMessage); + uassert( 16158, errorMessage, connection != NULL ); + return connection; + } + + BenchRunState::BenchRunState( unsigned numWorkers ) + : _mutex(), + _numUnstartedWorkers( numWorkers ), + _numActiveWorkers( 0 ), + _isShuttingDown( 0 ) { + } + + BenchRunState::~BenchRunState() { + wassert(_numActiveWorkers == 0 && _numUnstartedWorkers == 0); + } + + void BenchRunState::waitForState(State awaitedState) { + boost::mutex::scoped_lock lk(_mutex); + + switch ( awaitedState ) { + case BRS_RUNNING: + while ( _numUnstartedWorkers > 0 ) { + massert( 16147, "Already finished.", _numUnstartedWorkers + _numActiveWorkers > 0 ); + _stateChangeCondition.wait( _mutex ); + } + break; + case BRS_FINISHED: + while ( _numUnstartedWorkers + _numActiveWorkers > 0 ) { + _stateChangeCondition.wait( _mutex ); + } + break; + default: + msgasserted(16152, mongoutils::str::stream() << "Cannot wait for state " << awaitedState); + } + } + + void BenchRunState::tellWorkersToFinish() { + _isShuttingDown.set( 1 ); + } + + void BenchRunState::assertFinished() { + boost::mutex::scoped_lock lk(_mutex); + verify(0 == _numUnstartedWorkers + _numActiveWorkers); + } + + bool BenchRunState::shouldWorkerFinish() { + return bool(_isShuttingDown.get()); + } + + void BenchRunState::onWorkerStarted() { + boost::mutex::scoped_lock lk(_mutex); + verify( _numUnstartedWorkers > 0 ); + --_numUnstartedWorkers; + ++_numActiveWorkers; + if (_numUnstartedWorkers == 0) { + _stateChangeCondition.notify_all(); + } + } + + void BenchRunState::onWorkerFinished() { + boost::mutex::scoped_lock lk(_mutex); + verify( _numActiveWorkers > 0 ); + --_numActiveWorkers; + if (_numActiveWorkers + _numUnstartedWorkers == 0) { + _stateChangeCondition.notify_all(); + } + } + + BSONObj benchStart( const BSONObj& , void* ); + BSONObj benchFinish( const BSONObj& , void* ); + + static bool _hasSpecial( const BSONObj& obj ) { + BSONObjIterator i( obj ); + while ( i.more() ) { + BSONElement e = i.next(); + if ( e.fieldName()[0] == '#' ) + return true; + + if ( ! e.isABSONObj() ) + continue; + + if ( _hasSpecial( e.Obj() ) ) + return true; + } + return false; + } + + static void _fixField( BSONObjBuilder& b , const BSONElement& e ) { + verify( e.type() == Object ); + + BSONObj sub = e.Obj(); + verify( sub.nFields() == 1 ); + + BSONElement f = sub.firstElement(); + if ( str::equals( "#RAND_INT" , f.fieldName() ) ) { + BSONObjIterator i( f.Obj() ); + int min = i.next().numberInt(); + int max = i.next().numberInt(); + + int x = min + ( rand() % ( max - min ) ); + + if ( i.more() ) + x *= i.next().numberInt(); + + b.append( e.fieldName() , x ); + } + else { + uasserted( 14811 , str::stream() << "invalid bench dynamic piece: " << f.fieldName() ); + } + + } + + static void fixQuery( BSONObjBuilder& b , const BSONObj& obj ) { + BSONObjIterator i( obj ); + while ( i.more() ) { + BSONElement e = i.next(); + + if ( ! e.isABSONObj() ) { + b.append( e ); + continue; + } + + BSONObj sub = e.Obj(); + if ( sub.firstElement().fieldName()[0] == '#' ) { + _fixField( b , e ); + } + else { + BSONObjBuilder xx( e.type() == Object ? b.subobjStart( e.fieldName() ) : b.subarrayStart( e.fieldName() ) ); + fixQuery( xx , sub ); + xx.done(); + } + + } + } + + static BSONObj fixQuery( const BSONObj& obj ) { + + if ( ! _hasSpecial( obj ) ) + return obj; + + BSONObjBuilder b( obj.objsize() + 128 ); + fixQuery( b , obj ); + return b.obj(); + } + + BenchRunWorker::BenchRunWorker(const BenchRunConfig *config, BenchRunState *brState) + : _config(config), _brState(brState) { + } + + BenchRunWorker::~BenchRunWorker() {} + + void BenchRunWorker::start() { + boost::thread(boost::bind(&BenchRunWorker::run, this)); + } + + bool BenchRunWorker::shouldStop() const { + return _brState->shouldWorkerFinish(); + } + + void doNothing(const BSONObj&) { } + + void BenchRunWorker::generateLoadOnConnection( DBClientBase* conn ) { + verify( conn ); + long long count = 0; + mongo::Timer timer; + + while ( !shouldStop() ) { + BSONObjIterator i( _config->ops ); + while ( i.more() ) { + + if ( shouldStop() ) break; + + BSONElement e = i.next(); + + string ns = e["ns"].String(); + string op = e["op"].String(); + + int delay = e["delay"].eoo() ? 0 : e["delay"].Int(); + + BSONObj context = e["context"].eoo() ? BSONObj() : e["context"].Obj(); + + auto_ptr<Scope> scope; + ScriptingFunction scopeFunc = 0; + BSONObj scopeObj; + + if (_config->username != "") { + string errmsg; + if (!conn->auth(_config->db, _config->username, _config->password, errmsg)) { + uasserted(15931, "Authenticating to connection for _benchThread failed: " + errmsg); + } + } + + bool check = ! e["check"].eoo(); + if( check ){ + if ( e["check"].type() == CodeWScope || e["check"].type() == Code || e["check"].type() == String ) { + scope = globalScriptEngine->getPooledScope( ns ); + verify( scope.get() ); + + if ( e.type() == CodeWScope ) { + scopeFunc = scope->createFunction( e["check"].codeWScopeCode() ); + scopeObj = BSONObj( e.codeWScopeScopeDataUnsafe() ); + } + else { + scopeFunc = scope->createFunction( e["check"].valuestr() ); + } + + scope->init( &scopeObj ); + verify( scopeFunc ); + } + else { + warning() << "Invalid check type detected in benchRun op : " << e << endl; + check = false; + } + } + + try { + if ( op == "findOne" ) { + + BSONObj result; + { + BenchRunEventTrace _bret(&_stats.findOneCounter); + result = conn->findOne( ns , fixQuery( e["query"].Obj() ) ); + } + + if( check ){ + int err = scope->invoke( scopeFunc , 0 , &result, 1000 * 60 , false ); + if( err ){ + log() << "Error checking in benchRun thread [findOne]" << causedBy( scope->getError() ) << endl; + + _stats.errCount++; + + return; + } + } + + if( ! _config->hideResults || e["showResult"].trueValue() ) log() << "Result from benchRun thread [findOne] : " << result << endl; + + } + else if ( op == "command" ) { + + BSONObj result; + // TODO + /* bool ok = */ conn->runCommand( ns , fixQuery( e["command"].Obj() ), result, e["options"].numberInt() ); + + if( check ){ + int err = scope->invoke( scopeFunc , 0 , &result, 1000 * 60 , false ); + if( err ){ + log() << "Error checking in benchRun thread [command]" << causedBy( scope->getError() ) << endl; + + _stats.errCount++; + + return; + } + } + + if( ! _config->hideResults || e["showResult"].trueValue() ) log() << "Result from benchRun thread [command] : " << result << endl; + + } + else if( op == "find" || op == "query" ) { + + int limit = e["limit"].eoo() ? 0 : e["limit"].numberInt(); + int skip = e["skip"].eoo() ? 0 : e["skip"].Int(); + int options = e["options"].eoo() ? 0 : e["options"].Int(); + int batchSize = e["batchSize"].eoo() ? 0 : e["batchSize"].Int(); + BSONObj filter = e["filter"].eoo() ? BSONObj() : e["filter"].Obj(); + int expected = e["expected"].eoo() ? -1 : e["expected"].Int(); + + auto_ptr<DBClientCursor> cursor; + int count; + + BSONObj fixedQuery = fixQuery(e["query"].Obj()); + + // use special query function for exhaust query option + if (options & QueryOption_Exhaust) { + BenchRunEventTrace _bret(&_stats.queryCounter); + boost::function<void (const BSONObj&)> castedDoNothing(doNothing); + count = conn->query(castedDoNothing, ns, fixedQuery, &filter, options); + } + else { + BenchRunEventTrace _bret(&_stats.queryCounter); + cursor = conn->query( ns, fixedQuery, limit, skip, &filter, options, batchSize ); + count = cursor->itcount(); + } + + if ( expected >= 0 && count != expected ) { + cout << "bench query on: " << ns << " expected: " << expected << " got: " << count << endl; + verify(false); + } + + if( check ){ + BSONObj thisValue = BSON( "count" << count << "context" << context ); + int err = scope->invoke( scopeFunc , 0 , &thisValue, 1000 * 60 , false ); + if( err ){ + log() << "Error checking in benchRun thread [find]" << causedBy( scope->getError() ) << endl; + + _stats.errCount++; + + return; + } + } + + if( ! _config->hideResults || e["showResult"].trueValue() ) log() << "Result from benchRun thread [query] : " << count << endl; + + } + else if( op == "update" ) { + + bool multi = e["multi"].trueValue(); + bool upsert = e["upsert"].trueValue(); + BSONObj query = e["query"].eoo() ? BSONObj() : e["query"].Obj(); + BSONObj update = e["update"].Obj(); + BSONObj result; + bool safe = e["safe"].trueValue(); + + { + BenchRunEventTrace _bret(&_stats.updateCounter); + conn->update( ns, fixQuery( query ), update, upsert , multi ); + if (safe) + result = conn->getLastErrorDetailed(); + } + + if( safe ){ + if( check ){ + int err = scope->invoke( scopeFunc , 0 , &result, 1000 * 60 , false ); + if( err ){ + log() << "Error checking in benchRun thread [update]" << causedBy( scope->getError() ) << endl; + + _stats.errCount++; + + return; + } + } + + if( ! _config->hideResults || e["showResult"].trueValue() ) log() << "Result from benchRun thread [safe update] : " << result << endl; + + if( ! result["err"].eoo() && result["err"].type() == String && ( _config->throwGLE || e["throwGLE"].trueValue() ) ) + throw DBException( (string)"From benchRun GLE" + causedBy( result["err"].String() ), + result["code"].eoo() ? 0 : result["code"].Int() ); + } + } + else if( op == "insert" ) { + bool safe = e["safe"].trueValue(); + BSONObj result; + { + BenchRunEventTrace _bret(&_stats.insertCounter); + conn->insert( ns, fixQuery( e["doc"].Obj() ) ); + if (safe) + result = conn->getLastErrorDetailed(); + } + + if( safe ){ + if( check ){ + int err = scope->invoke( scopeFunc , 0 , &result, 1000 * 60 , false ); + if( err ){ + log() << "Error checking in benchRun thread [insert]" << causedBy( scope->getError() ) << endl; + + _stats.errCount++; + + return; + } + } + + if( ! _config->hideResults || e["showResult"].trueValue() ) log() << "Result from benchRun thread [safe insert] : " << result << endl; + + if( ! result["err"].eoo() && result["err"].type() == String && ( _config->throwGLE || e["throwGLE"].trueValue() ) ) + throw DBException( (string)"From benchRun GLE" + causedBy( result["err"].String() ), + result["code"].eoo() ? 0 : result["code"].Int() ); + } + } + else if( op == "delete" || op == "remove" ) { + + bool multi = e["multi"].eoo() ? true : e["multi"].trueValue(); + BSONObj query = e["query"].eoo() ? BSONObj() : e["query"].Obj(); + bool safe = e["safe"].trueValue(); + BSONObj result; + + { + BenchRunEventTrace _bret(&_stats.deleteCounter); + conn->remove( ns, fixQuery( query ), ! multi ); + if (safe) + result = conn->getLastErrorDetailed(); + } + + if( safe ){ + if( check ){ + int err = scope->invoke( scopeFunc , 0 , &result, 1000 * 60 , false ); + if( err ){ + log() << "Error checking in benchRun thread [delete]" << causedBy( scope->getError() ) << endl; + + _stats.errCount++; + + return; + } + } + + if( ! _config->hideResults || e["showResult"].trueValue() ) log() << "Result from benchRun thread [safe remove] : " << result << endl; + + if( ! result["err"].eoo() && result["err"].type() == String && ( _config->throwGLE || e["throwGLE"].trueValue() ) ) + throw DBException( (string)"From benchRun GLE " + causedBy( result["err"].String() ), + result["code"].eoo() ? 0 : result["code"].Int() ); + } + } + else if ( op == "createIndex" ) { + conn->ensureIndex( ns , e["key"].Obj() , false , "" , false ); + } + else if ( op == "dropIndex" ) { + conn->dropIndex( ns , e["key"].Obj() ); + } + else { + log() << "don't understand op: " << op << endl; + _stats.error = true; + return; + } + } + catch( DBException& ex ){ + if( ! _config->hideErrors || e["showError"].trueValue() ){ + + bool yesWatch = ( _config->watchPattern && _config->watchPattern->FullMatch( ex.what() ) ); + bool noWatch = ( _config->noWatchPattern && _config->noWatchPattern->FullMatch( ex.what() ) ); + + if( ( ! _config->watchPattern && _config->noWatchPattern && ! noWatch ) || // If we're just ignoring things + ( ! _config->noWatchPattern && _config->watchPattern && yesWatch ) || // If we're just watching things + ( _config->watchPattern && _config->noWatchPattern && yesWatch && ! noWatch ) ) + log() << "Error in benchRun thread for op " << e << causedBy( ex ) << endl; + } + + bool yesTrap = ( _config->trapPattern && _config->trapPattern->FullMatch( ex.what() ) ); + bool noTrap = ( _config->noTrapPattern && _config->noTrapPattern->FullMatch( ex.what() ) ); + + if( ( ! _config->trapPattern && _config->noTrapPattern && ! noTrap ) || + ( ! _config->noTrapPattern && _config->trapPattern && yesTrap ) || + ( _config->trapPattern && _config->noTrapPattern && yesTrap && ! noTrap ) ){ + { + _stats.trappedErrors.push_back( BSON( "error" << ex.what() << "op" << e << "count" << count ) ); + } + if( _config->breakOnTrap ) return; + } + if( ! _config->handleErrors && ! e["handleError"].trueValue() ) return; + + _stats.errCount++; + } + catch( ... ){ + if( ! _config->hideErrors || e["showError"].trueValue() ) log() << "Error in benchRun thread caused by unknown error for op " << e << endl; + if( ! _config->handleErrors && ! e["handleError"].trueValue() ) return; + + _stats.errCount++; + } + + if ( ++count % 100 == 0 ) { + conn->getLastError(); + } + + sleepmillis( delay ); + } + } + + conn->getLastError(); + } + + namespace { + class BenchRunWorkerStateGuard : private boost::noncopyable { + public: + explicit BenchRunWorkerStateGuard( BenchRunState *brState ) : _brState( brState ) { + _brState->onWorkerStarted(); + } + + ~BenchRunWorkerStateGuard() { + _brState->onWorkerFinished(); + } + + private: + BenchRunState *_brState; + }; + } // namespace + + void BenchRunWorker::run() { + BenchRunWorkerStateGuard _workerStateGuard( _brState ); + + boost::scoped_ptr<DBClientBase> conn( _config->createConnection() ); + + try { + if ( !_config->username.empty() ) { + string errmsg; + if (!conn->auth(_config->db, _config->username, _config->password, errmsg)) { + uasserted(15932, "Authenticating to connection for benchThread failed: " + errmsg); + } + } + generateLoadOnConnection( conn.get() ); + } + catch( DBException& e ){ + error() << "DBException not handled in benchRun thread" << causedBy( e ) << endl; + } + catch( std::exception& e ){ + error() << "std::exception not handled in benchRun thread" << causedBy( e ) << endl; + } + catch( ... ){ + error() << "Unknown exception not handled in benchRun thread." << endl; + } + } + + BenchRunner::BenchRunner( BenchRunConfig *config ) + : _brState(config->parallel), + _config(config) { + + _oid.init(); + boost::mutex::scoped_lock lk(_staticMutex); + _activeRuns[_oid] = this; + } + + BenchRunner::~BenchRunner() { + for (size_t i = 0; i < _workers.size(); ++i) + delete _workers[i]; + } + + void BenchRunner::start( ) { + + + { + boost::scoped_ptr<DBClientBase> conn( _config->createConnection() ); + // Get initial stats + conn->simpleCommand( "admin" , &before , "serverStatus" ); + before = before.getOwned(); + } + + // Start threads + for ( unsigned i = 0; i < _config->parallel; i++ ) { + BenchRunWorker *worker = new BenchRunWorker(_config.get(), &_brState); + worker->start(); + _workers.push_back(worker); + } + + _brState.waitForState(BenchRunState::BRS_RUNNING); + } + + void BenchRunner::stop() { + _brState.tellWorkersToFinish(); + _brState.waitForState(BenchRunState::BRS_FINISHED); + + { + boost::scoped_ptr<DBClientBase> conn( _config->createConnection() ); + // Get final stats + conn->simpleCommand( "admin" , &after , "serverStatus" ); + after = after.getOwned(); + } + + { + boost::mutex::scoped_lock lk(_staticMutex); + _activeRuns.erase( _oid ); + } + } + + BenchRunner* BenchRunner::createWithConfig( const BSONObj &configArgs ) { + BenchRunConfig *config = BenchRunConfig::createFromBson( configArgs ); + return new BenchRunner(config); + } + + BenchRunner* BenchRunner::get( OID oid ) { + boost::mutex::scoped_lock lk(_staticMutex); + return _activeRuns[ oid ]; + } + + void BenchRunner::populateStats( BenchRunStats *stats ) { + _brState.assertFinished(); + stats->reset(); + for ( size_t i = 0; i < _workers.size(); ++i ) + stats->updateFrom( _workers[i]->stats() ); + BSONObj before = this->before["opcounters"].Obj(); + BSONObj after = this->after["opcounters"].Obj(); + { + BSONObjIterator i( after ); + while ( i.more() ) { + BSONElement e = i.next(); + long long delta = e.numberLong(); + delta -= before[e.fieldName()].numberLong(); + stats->opcounters[e.fieldName()] = delta; + } + } + } + + static void appendAverageMicrosIfAvailable( + BSONObjBuilder &buf, const std::string &name, const BenchRunEventCounter &counter) { + + if (counter.getNumEvents() > 0) + buf.append(name, + static_cast<double>(counter.getTotalTimeMicros()) / counter.getNumEvents()); + } + + BSONObj BenchRunner::finish( BenchRunner* runner ) { + + runner->stop(); + + BenchRunStats stats; + runner->populateStats(&stats); + + // vector<BSONOBj> errors = runner->config.errors; + bool error = stats.error; + + if ( error ) + return BSON( "err" << 1 ); + + // compute actual ops/sec + BSONObj before = runner->before["opcounters"].Obj(); + BSONObj after = runner->after["opcounters"].Obj(); + + BSONObjBuilder buf; + buf.append( "note" , "values per second" ); + buf.append( "errCount", (long long) stats.errCount ); + buf.append( "trapped", "error: not implemented" ); + appendAverageMicrosIfAvailable(buf, "findOneLatencyAverageMicros", stats.findOneCounter); + appendAverageMicrosIfAvailable(buf, "insertLatencyAverageMicros", stats.insertCounter); + appendAverageMicrosIfAvailable(buf, "deleteLatencyAverageMicros", stats.deleteCounter); + appendAverageMicrosIfAvailable(buf, "updateLatencyAverageMicros", stats.updateCounter); + appendAverageMicrosIfAvailable(buf, "queryLatencyAverageMicros", stats.queryCounter); + + { + BSONObjIterator i( after ); + while ( i.more() ) { + BSONElement e = i.next(); + double x = e.number(); + x -= before[e.fieldName()].number(); + buf.append( e.fieldName() , x / runner->_config->seconds ); + } + } + + BSONObj zoo = buf.obj(); + + delete runner; + return zoo; + } + + boost::mutex BenchRunner::_staticMutex; + map< OID, BenchRunner* > BenchRunner::_activeRuns; + + /** + * benchRun( { ops : [] , host : XXX , db : XXXX , parallel : 5 , seconds : 5 } + */ + BSONObj benchRunSync( const BSONObj& argsFake, void* data ) { + + BSONObj start = benchStart( argsFake, data ); + + OID oid = OID( start.firstElement().String() ); + BenchRunner* runner = BenchRunner::get( oid ); + sleepmillis( (int)(1000.0 * runner->config().seconds) ); + + return benchFinish( start, data ); + } + + /** + * benchRun( { ops : [] , host : XXX , db : XXXX , parallel : 5 , seconds : 5 } + */ + BSONObj benchStart( const BSONObj& argsFake, void* data ) { + + verify( argsFake.firstElement().isABSONObj() ); + BSONObj args = argsFake.firstElement().Obj(); + + // Get new BenchRunner object + BenchRunner* runner = BenchRunner::createWithConfig( args ); + + runner->start(); + return BSON( "" << runner->oid().toString() ); + } + + /** + * benchRun( { ops : [] , host : XXX , db : XXXX , parallel : 5 , seconds : 5 } + */ + BSONObj benchFinish( const BSONObj& argsFake, void* data ) { + + OID oid = OID( argsFake.firstElement().String() ); + + // Get new BenchRunner object + BenchRunner* runner = BenchRunner::get( oid ); + + BSONObj finalObj = BenchRunner::finish( runner ); + + return BSON( "" << finalObj ); + } + + void installBenchmarkSystem( Scope& scope ) { + scope.injectNative( "benchRun" , benchRunSync ); + scope.injectNative( "benchRunSync" , benchRunSync ); + scope.injectNative( "benchStart" , benchStart ); + scope.injectNative( "benchFinish" , benchFinish ); + } + +} diff --git a/src/mongo/scripting/bench.h b/src/mongo/scripting/bench.h new file mode 100644 index 00000000000..98c0ebc4666 --- /dev/null +++ b/src/mongo/scripting/bench.h @@ -0,0 +1,412 @@ +/* + * Copyright (C) 2010 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 <http://www.gnu.org/licenses/>. + */ + +#pragma once + +#include <string> + +#include <boost/shared_ptr.hpp> +#include <boost/thread/condition.hpp> +#include <boost/thread/mutex.hpp> +#include <pcrecpp.h> + +#include "mongo/bson/util/atomic_int.h" +#include "mongo/client/dbclientinterface.h" +#include "mongo/db/jsobj.h" +#include "mongo/util/timer.h" + +namespace mongo { + + /** + * Configuration object describing a bench run activity. + */ + class BenchRunConfig : private boost::noncopyable { + public: + + /** + * Create a new BenchRunConfig object, and initialize it from the BSON + * document, "args". + * + * Caller owns the returned object, and is responsible for its deletion. + */ + static BenchRunConfig *createFromBson( const BSONObj &args ); + + BenchRunConfig(); + + void initializeFromBson( const BSONObj &args ); + + // Create a new connection to the mongo instance specified by this configuration. + DBClientBase *createConnection() const; + + /** + * Connection string describing the host to which to connect. + */ + std::string host; + + /** + * Name of the database on which to operate. + */ + std::string db; + + /** + * Optional username for authenticating to the database. + */ + std::string username; + + /** + * Optional password for authenticating to the database. + * + * Only useful if username is non-empty. + */ + std::string password; + + /** + * Number of parallel threads to perform the bench run activity. + */ + unsigned parallel; + + /** + * Desired duration of the bench run activity, in seconds. + * + * NOTE: Only used by the javascript benchRun() and benchRunSync() functions. + */ + double seconds; + + bool hideResults; + bool handleErrors; + bool hideErrors; + + boost::shared_ptr< pcrecpp::RE > trapPattern; + boost::shared_ptr< pcrecpp::RE > noTrapPattern; + boost::shared_ptr< pcrecpp::RE > watchPattern; + boost::shared_ptr< pcrecpp::RE > noWatchPattern; + + /** + * Operation description. A BSON array of objects, each describing a single + * operation. + * + * Every thread in a benchRun job will perform these operations in sequence, restarting at + * the beginning when the end is reached, until the job is stopped. + * + * TODO: Document the operation objects. + * + * TODO: Introduce support for performing each operation exactly N times. + */ + BSONObj ops; + + bool throwGLE; + bool breakOnTrap; + + private: + /// Initialize a config object to its default values. + void initializeToDefaults(); + }; + + /** + * An event counter for events that have an associated duration. + * + * Not thread safe. Expected use is one instance per thread during parallel execution. + */ + class BenchRunEventCounter : private boost::noncopyable { + public: + /// Constructs a zeroed out counter. + BenchRunEventCounter(); + ~BenchRunEventCounter(); + + /** + * Zero out the counter. + */ + void reset(); + + /** + * Conceptually the equivalent of "+=". Adds "other" into this. + */ + void updateFrom( const BenchRunEventCounter &other ); + + /** + * Count one instance of the event, which took "timeMicros" microseconds. + */ + void countOne(unsigned long long timeMicros) { + ++_numEvents; + _totalTimeMicros += timeMicros; + } + + /** + * Get the total number of microseconds ellapsed during all observed events. + */ + unsigned long long getTotalTimeMicros() const { return _totalTimeMicros; } + + /** + * Get the number of observed events. + */ + unsigned long long getNumEvents() const { return _numEvents; } + + private: + unsigned long long _numEvents; + unsigned long long _totalTimeMicros; + }; + + /** + * RAII object for tracing an event. + * + * Construct an instance of this at the beginning of an event, and have it go out of scope at + * the end, to facilitate tracking events. + * + * This type can be used to separately count failures and successes by passing two event + * counters to the BenchRunEventCounter constructor, and calling "succeed()" on the object at + * the end of a successful event. If an exception is thrown, the fail counter will receive the + * event, and otherwise, the succes counter will. + * + * In all cases, the counter objects must outlive the trace object. + */ + class BenchRunEventTrace : private boost::noncopyable { + public: + explicit BenchRunEventTrace(BenchRunEventCounter *eventCounter) { + initialize(eventCounter, eventCounter, false); + } + + BenchRunEventTrace(BenchRunEventCounter *successCounter, + BenchRunEventCounter *failCounter, + bool defaultToFailure=true) { + initialize(successCounter, failCounter, defaultToFailure); + } + + ~BenchRunEventTrace() { + (_succeeded ? _successCounter : _failCounter)->countOne(_timer.micros()); + } + + void succeed() { _succeeded = true; } + void fail() { _succeeded = false; } + + private: + void initialize(BenchRunEventCounter *successCounter, + BenchRunEventCounter *failCounter, + bool defaultToFailure) { + _successCounter = successCounter; + _failCounter = failCounter; + _succeeded = !defaultToFailure; + } + + Timer _timer; + BenchRunEventCounter *_successCounter; + BenchRunEventCounter *_failCounter; + bool _succeeded; + }; + + /** + * Statistics object representing the result of a bench run activity. + */ + class BenchRunStats : private boost::noncopyable { + public: + BenchRunStats(); + ~BenchRunStats(); + + void reset(); + + void updateFrom( const BenchRunStats &other ); + + bool error; + unsigned long long errCount; + + BenchRunEventCounter findOneCounter; + BenchRunEventCounter updateCounter; + BenchRunEventCounter insertCounter; + BenchRunEventCounter deleteCounter; + BenchRunEventCounter queryCounter; + + std::map<std::string, long long> opcounters; + std::vector<BSONObj> trappedErrors; + }; + + /** + * State of a BenchRun activity. + * + * Logically, the states are "starting up", "running" and "finished." + */ + class BenchRunState : private boost::noncopyable { + public: + enum State { BRS_STARTING_UP, BRS_RUNNING, BRS_FINISHED }; + + explicit BenchRunState(unsigned numWorkers); + ~BenchRunState(); + + // + // Functions called by the job-controlling thread, through an instance of BenchRunner. + // + + /** + * Block until the current state is "awaitedState." + * + * massert() (uassert()?) if "awaitedState" is unreachable from + * the current state. + */ + void waitForState(State awaitedState); + + /** + * Notify the worker threads to wrap up. Does not block. + */ + void tellWorkersToFinish(); + + /// Check that the current state is BRS_FINISHED. + void assertFinished(); + + // + // Functions called by the worker threads, through instances of BenchRunWorker. + // + + /** + * Predicate that workers call to see if they should finish (as a result of a call + * to tellWorkersToFinish()). + */ + bool shouldWorkerFinish(); + + /** + * Called by each BenchRunWorker from within its thread context, immediately before it + * starts sending requests to the configured mongo instance. + */ + void onWorkerStarted(); + + /** + * Called by each BenchRunWorker from within its thread context, shortly after it finishes + * sending requests to the configured mongo instance. + */ + void onWorkerFinished(); + + private: + boost::mutex _mutex; + boost::condition _stateChangeCondition; + unsigned _numUnstartedWorkers; + unsigned _numActiveWorkers; + AtomicUInt _isShuttingDown; + }; + + /** + * A single worker in the bench run activity. + * + * Represents the behavior of one thread working in a bench run activity. + */ + class BenchRunWorker : private boost::noncopyable { + public: + + /** + * Create a new worker, performing one thread's worth of the activity described in + * "config", and part of the larger activity with state "brState". Both "config" + * and "brState" must exist for the life of this object. + */ + BenchRunWorker(const BenchRunConfig *config, BenchRunState *brState); + ~BenchRunWorker(); + + /** + * Start performing the "work" behavior in a new thread. + */ + void start(); + + /** + * Get the run statistics for a worker. + * + * Should only be observed _after_ the worker has signaled its completion by calling + * onWorkerFinished() on the BenchRunState passed into its constructor. + */ + const BenchRunStats &stats() const { return _stats; } + + private: + /// The main method of the worker, executed inside the thread launched by start(). + void run(); + + /// The function that actually sets about generating the load described in "_config". + void generateLoadOnConnection( DBClientBase *conn ); + + /// Predicate, used to decide whether or not it's time to terminate the worker. + bool shouldStop() const; + + const BenchRunConfig *_config; + BenchRunState *_brState; + BenchRunStats _stats; + }; + + /** + * Object representing a "bench run" activity. + */ + class BenchRunner : private boost::noncopyable { + public: + /** + * Utility method to create a new bench runner from a BSONObj representation + * of a configuration. + * + * TODO: This is only really for the use of the javascript benchRun() methods, + * and should probably move out of the BenchRunner class. + */ + static BenchRunner* createWithConfig( const BSONObj &configArgs ); + + /** + * Look up a bench runner object by OID. + * + * TODO: Same todo as for "createWithConfig". + */ + static BenchRunner* get( OID oid ); + + /** + * Stop a running "runner", and return a BSON representation of its resultant + * BenchRunStats. + * + * TODO: Same as for "createWithConfig". + */ + static BSONObj finish( BenchRunner* runner ); + + /** + * Create a new bench runner, to perform the activity described by "*config." + * + * Takes ownership of "config", and will delete it. + */ + explicit BenchRunner( BenchRunConfig *config ); + ~BenchRunner(); + + /** + * Start the activity. Only call once per instance of BenchRunner. + */ + void start(); + + /** + * Stop the activity. Block until the activitiy has stopped. + */ + void stop(); + + /** + * Store the collected event data from a completed bench run activity into "stats." + * + * Illegal to call until after stop() returns. + */ + void populateStats(BenchRunStats *stats); + + OID oid() const { return _oid; } + + const BenchRunConfig &config() const { return *_config; } // TODO: Remove this function. + + private: + // TODO: Same as for createWithConfig. + static boost::mutex _staticMutex; + static map< OID, BenchRunner* > _activeRuns; + + OID _oid; + BenchRunState _brState; + boost::scoped_ptr<BenchRunConfig> _config; + vector<BenchRunWorker *> _workers; + + BSONObj before; + BSONObj after; + }; + +} // namespace mongo diff --git a/src/mongo/scripting/bson_template_evaluator.cpp b/src/mongo/scripting/bson_template_evaluator.cpp new file mode 100644 index 00000000000..b38bed37051 --- /dev/null +++ b/src/mongo/scripting/bson_template_evaluator.cpp @@ -0,0 +1,157 @@ +/** @file mongo/scripting/templateevaluator.cpp */ + +/* + * Copyright (C) 2012 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 <http://www.gnu.org/licenses/>. + */ + +#include "mongo/scripting/bson_template_evaluator.h" + +#include <cstddef> +#include <cstdlib> + +#include "mongo/util/map_util.h" + +namespace mongo { + + void BsonTemplateEvaluator::initializeEvaluator() { + addOperator("RAND_INT", &BsonTemplateEvaluator::evalRandInt); + addOperator("RAND_STRING", &BsonTemplateEvaluator::evalRandString); + addOperator("CONCAT", &BsonTemplateEvaluator::evalConcat); + } + + BsonTemplateEvaluator::BsonTemplateEvaluator() { + initializeEvaluator(); + } + + void BsonTemplateEvaluator::addOperator(const std::string& name, const OperatorFn& op) { + _operatorFunctions[name] = op; + } + + BsonTemplateEvaluator::OperatorFn BsonTemplateEvaluator::operatorEvaluator( + const std::string& op) const { + return mapFindWithDefault(_operatorFunctions, op, OperatorFn()); + } + + /* This is the top level method for using this library. It takes a BSON Object as input, + * evaluates the templates and saves the result in the builder object. + * The method returns appropriate Status on success/error condition. + */ + BsonTemplateEvaluator::Status BsonTemplateEvaluator::evaluate(const BSONObj& in, + BSONObjBuilder& builder) { + BSONForEach(e, in) { + Status st = _evalElem(e, builder); + if (st != StatusSuccess) + return st; + } + return StatusSuccess; + } + + BsonTemplateEvaluator::Status BsonTemplateEvaluator::_evalElem(BSONElement in, + BSONObjBuilder& out) { + if (in.type() != Object) { + out.append(in); + return StatusSuccess; + } + BSONObj subObj = in.embeddedObject(); + const char* opOrNot = subObj.firstElementFieldName(); + if (opOrNot[0] != '#') { + out.append(in); + return StatusSuccess; + } + const char* op = opOrNot+1; + OperatorFn fn = operatorEvaluator(op); + if (!fn) + return StatusBadOperator; + Status st = fn(this, in.fieldName(), subObj, out); + if (st != StatusSuccess) + return st; + return StatusSuccess; + } + + BsonTemplateEvaluator::Status BsonTemplateEvaluator::evalRandInt(BsonTemplateEvaluator* btl, + const char* fieldName, + const BSONObj in, + BSONObjBuilder& out) { + // in = { #RAND_INT: [10, 20] } + BSONObj range = in.firstElement().embeddedObject(); + if (!range[0].isNumber() || !range[1].isNumber()) + return StatusOpEvaluationError; + const int min = range["0"].numberInt(); + const int max = range["1"].numberInt(); + if (max <= min) + return StatusOpEvaluationError; + int randomNum = min + (rand() % (max - min)); + if (range.nFields() == 3) { + if (!range[2].isNumber()) + return StatusOpEvaluationError; + randomNum *= range[2].numberInt(); + } + out.append(fieldName, randomNum); + return StatusSuccess; + } + + BsonTemplateEvaluator::Status BsonTemplateEvaluator::evalRandString(BsonTemplateEvaluator* btl, + const char* fieldName, + const BSONObj in, + BSONObjBuilder& out) { + // in = { #RAND_STRING: [10] } + BSONObj range = in.firstElement().embeddedObject(); + if (range.nFields() != 1) + return StatusOpEvaluationError; + if (!range[0].isNumber()) + return StatusOpEvaluationError; + const int length = range["0"].numberInt(); + if (length <= 0) + return StatusOpEvaluationError; + static const char alphanum[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZ" + "abcdefghijklmnopqrstuvwxyz" + "0123456789+/"; + static const size_t alphaNumLength = sizeof(alphanum) - 1; + BOOST_STATIC_ASSERT(alphaNumLength == 64); + unsigned currentRand = 0; + std::string str; + for (int i = 0; i < length; ++i, currentRand >>= 6) { + if (i % 5 == 0) + currentRand = rand(); + str.push_back(alphanum[currentRand % alphaNumLength]); + } + out.append(fieldName, str); + return StatusSuccess; + } + + BsonTemplateEvaluator::Status BsonTemplateEvaluator::evalConcat(BsonTemplateEvaluator* btl, + const char* fieldName, + const BSONObj in, + BSONObjBuilder& out) { + // in = { #CONCAT: ["hello", " ", "world"] } + BSONObjBuilder objectBuilder; + Status status = btl->evaluate(in.firstElement().embeddedObject(), objectBuilder); + if (status != StatusSuccess) + return status; + BSONObj parts = objectBuilder.obj(); + if (parts.nFields() <= 1) + return StatusOpEvaluationError; + StringBuilder stringBuilder; + BSONForEach(part, parts) { + if (part.type() == String) + stringBuilder << part.String(); + else + part.toString(stringBuilder,false); + } + out.append(fieldName, stringBuilder.str()); + return StatusSuccess; + } + +} // end namespace mongo diff --git a/src/mongo/scripting/bson_template_evaluator.h b/src/mongo/scripting/bson_template_evaluator.h new file mode 100644 index 00000000000..2c60df73514 --- /dev/null +++ b/src/mongo/scripting/bson_template_evaluator.h @@ -0,0 +1,152 @@ +/** @file templateevaluator.h */ + +/* + * Copyright (C) 2012 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 <http://www.gnu.org/licenses/>. + */ + +/* + * This library supports a templating language that helps in generating BSON documents from a + * template. The language supports the following template: + * #RAND_INT, #RAND_STRING and #CONCAT. + * + * The language will help in quickly expressing richer documents for use in benchRun. + * Ex. : { key : { #RAND_INT: [10, 20] } } or { key : { #CONCAT: ["hello", " ", "world"] } } + * + * Where possible, the templates can also be combined together and evaluated. For eg. + * { key : { #CONCAT: [{ #RAND_INT: [10, 20] }, " ", "world"] } } + * + * This library DOES NOT support combining or nesting the templates in an arbitrary fashion. + * eg. { key : { #RAND_INT: [{ #RAND_INT: [10, 15] }, 20] } } is not supported. + * + */ +#pragma once + +#include <map> +#include <string> + +#include <boost/function.hpp> +#include <boost/noncopyable.hpp> + +#include "mongo/db/jsobj.h" + +namespace mongo { + + /* + * BsonTemplateEvaluator Object for evaluating the templates. The Object exposes + * methods to evaluate existing template operators (#RAND_INT) and add new template operators. + * + * To evaluate a template, call the object's 'evaluate' method and pass it as arguments, the + * template object that you want to evaluate and a BSONObjBuilder object that will contain the + * resultant BSON object. Eg. + * + * Status st = bsonTemplEvalObj->evaluate(inputTemplateObj, outputBSONObjBuilder) + * + * The 'evaluate' method will never throw an exception and will return an appropriate Status + * code on success/error scenario. + * + * High level working : The evaluate() method takes in a BSONObj as input, iterates over the + * BSON elements in the input BSONObj, and calls _evalElem() method. The _evalElem() method + * figures out the specific template and then calls the corresponding template function. + * The evaluated result is added to the BSONObjBuilder object and is returned to the evaluate() + * method. + * + */ + class BsonTemplateEvaluator : private boost::noncopyable { + public: + /* Status of template evaluation. Logically the the status are "success", "bad operator" + * and "operation evaluation error." */ + enum Status { + StatusSuccess = 0, + StatusBadOperator, + StatusOpEvaluationError + }; + + /* + * OperatorFn : function object wrappers that define a call interface. + * All template operators have this signature. + * @params btl : pointer to the BsonTemplateEvaluator Object + * fieldName : key of the object being evaluated + * in : the embedded BSONObj + * builder : the output BSONObj + * Eg. for object { key : { #RAND_INT: [10, 20] } } + * fieldName : key + * in : { #RAND_INT: [10, 20] } + */ + typedef boost::function< Status (BsonTemplateEvaluator* btl, const char* fieldName, + const BSONObj& in, BSONObjBuilder& builder) > OperatorFn; + + BsonTemplateEvaluator(); + ~BsonTemplateEvaluator(); + + /* + * "Add a new operator, "name" with behavior "op" to this evaluator. + */ + void addOperator(const std::string& name, const OperatorFn& op); + + /* + * Returns the OperatorFn registered for the operator named "op", or + * OperatorFn() if there is no such operator. + */ + OperatorFn operatorEvaluator(const std::string& op) const; + + /* This is the top level method for using this library. It takes a BSON Object as input, + * evaluates the templates and saves the result in the builder object. + * The method returns a status code on success/error condition. + * The templates cannot be used at the top level. + * So this is okay as an input { key : {#RAND_INT: [10, 20]} } + * but not this { {#RAND_INT : [10, 20]} : some_value } + */ + Status evaluate(const BSONObj& src, BSONObjBuilder& builder); + + private: + void initializeEvaluator(); + // map that holds operators along with their respective function pointers + typedef std::map< std::string, OperatorFn > OperatorMap; + OperatorMap _operatorFunctions; + + // evaluates a BSON element. This is internally called by the top level evaluate method. + Status _evalElem(BSONElement in, BSONObjBuilder& out); + + /* + * Operator method to support #RAND_INT : { key : { #RAND_INT: [10, 20] } } + * The array arguments to #RAND_INT are the min and mix range between which a random number + * will be chosen. The chosen random number is inclusive at the lower end but not at the + * upper end. + * This will evaluate to something like { key : 14 } + * #RAND_INT also supports a third optional argument which is a multiplier. + * Thus for an input { key : { #RAND_INT: [10, 20, 4] } }, the method will + * choose a random number between 10 and 20 and then multiple the chosen value with 4. + */ + static Status evalRandInt(BsonTemplateEvaluator* btl, const char* fieldName, + const BSONObj in, BSONObjBuilder& out); + /* + * Operator method to support #RAND_STRING : { key : { #RAND_STRING: [12] } } + * The array argument to RAND_STRING is the length of the string that is desired. + * This will evaluate to something like { key : "randomstring" } + */ + static Status evalRandString(BsonTemplateEvaluator* btl, const char* fieldName, + const BSONObj in, BSONObjBuilder& out); + /* + * Operator method to support #CONCAT : { key : { #CONCAT: ["hello", " ", "world", 2012] } } + * The array argument to CONCAT are the strings to be concatenated. If the argument is not + * a string it will be stringified and concatendated. + * This will evaluate to { key : "hello world2012" } + */ + static Status evalConcat(BsonTemplateEvaluator* btl, const char* fieldName, + const BSONObj in, BSONObjBuilder& out); + + }; + +} // end namespace diff --git a/src/mongo/scripting/bson_template_evaluator_test.cpp b/src/mongo/scripting/bson_template_evaluator_test.cpp new file mode 100644 index 00000000000..5b334e0404f --- /dev/null +++ b/src/mongo/scripting/bson_template_evaluator_test.cpp @@ -0,0 +1,360 @@ +/** @file mongo/scripting/bson_template_evaluator_tests : tests related to bson template language */ + +/** + * Copyright (C) 2012 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 <http://www.gnu.org/licenses/>. + */ + +#include "mongo/db/jsobj.h" +#include "mongo/scripting/bson_template_evaluator.h" +#include "mongo/unittest/unittest.h" + +namespace mongo { + + namespace { + + TEST(BSONTemplateEvaluatorTest, RAND_INT) { + + BsonTemplateEvaluator *t = new BsonTemplateEvaluator(); + int randValue1, randValue2; + + // Test failure when the arguments to RAND_INT are not integers + BSONObjBuilder builder1; + BSONObj randObj = BSON( "#RAND_INT" << BSON_ARRAY("hello" << "world") ); + ASSERT_EQUALS( BsonTemplateEvaluator::StatusOpEvaluationError, + t->evaluate(BSON("randField" << randObj), builder1) ); + + // Test failure when operator does not exists + BSONObjBuilder builder2; + randObj = BSON( "#RAND_OP_NOT_EXISTS" << BSON_ARRAY( 5 << 0 ) ); + ASSERT_EQUALS( BsonTemplateEvaluator::StatusBadOperator, + t->evaluate(BSON("randField" << randObj), builder2) ); + + // Test failure when arguments to RAND_INT are not correct (max < min) + BSONObjBuilder builder3; + randObj = BSON( "#RAND_INT" << BSON_ARRAY( 5 << 0 ) ); + ASSERT_EQUALS( BsonTemplateEvaluator::StatusOpEvaluationError, + t->evaluate(BSON("randField" << randObj), builder3) ); + + // Test failure when operators are arbitrarily nested + // {id: { #RAND_INT: [ { #RAND_INT: [10, 20] }, 20] } + BSONObjBuilder builder4; + BSONObj innerRandObj = BSON( "#RAND_INT" << BSON_ARRAY( 0 << 5 ) ); + BSONObj outerRandObj = BSON( "#RAND_INT" << BSON_ARRAY( innerRandObj << 10 ) ); + ASSERT_EQUALS( BsonTemplateEvaluator::StatusOpEvaluationError, + t->evaluate(BSON("randField" << outerRandObj), builder4) ); + + // Test success with a single element + BSONObjBuilder builder5; + randObj = BSON( "#RAND_INT" << BSON_ARRAY( 0 << 5 ) ); + ASSERT_EQUALS( BsonTemplateEvaluator::StatusSuccess, + t->evaluate(BSON("randField" << randObj), builder5) ); + BSONObj obj5 = builder5.obj(); + ASSERT_EQUALS(obj5.nFields(), 1); + ASSERT_GREATER_THAN_OR_EQUALS(obj5.firstElement().numberInt(), 0); + ASSERT_LESS_THAN(obj5.firstElement().numberInt(), 5); + + // Test success with two #RAND_INT elements + BSONObjBuilder builder6; + randObj = BSON( "#RAND_INT" << BSON_ARRAY( 0 << 5 ) ); + ASSERT_EQUALS( BsonTemplateEvaluator::StatusSuccess, + t->evaluate(BSON("randField1" << randObj << + "randField2" << randObj), builder6) ); + BSONObj obj6 = builder6.obj(); + ASSERT_EQUALS(obj6.nFields(), 2); + BSONObjIterator iter6(obj6); + randValue1 = iter6.next().numberInt(); + randValue2 = iter6.next().numberInt(); + ASSERT_GREATER_THAN_OR_EQUALS(randValue1, 0); + ASSERT_LESS_THAN(randValue1, 5); + ASSERT_GREATER_THAN_OR_EQUALS(randValue2, 0); + ASSERT_LESS_THAN(randValue2, 5); + + // Test success with #RAND_INT as the last element + BSONObjBuilder builder7; + randObj = BSON( "#RAND_INT" << BSON_ARRAY( 0 << 5 ) ); + ASSERT_EQUALS( BsonTemplateEvaluator::StatusSuccess, + t->evaluate(BSON("id" << 1 << "hello" << "world" << + "randField" << randObj), builder7) ); + BSONObj obj7 = builder7.obj(); + ASSERT_EQUALS(obj7.nFields(), 3); + BSONObjIterator iter7(obj7); + iter7++; + randValue1 = iter7.next().numberInt(); + ASSERT_GREATER_THAN_OR_EQUALS(randValue1, 0); + ASSERT_LESS_THAN(randValue1, 5); + + // Test success with #RAND_INT as first element + BSONObjBuilder builder8; + randObj = BSON( "#RAND_INT" << BSON_ARRAY( 0 << 5 ) ); + ASSERT_EQUALS( BsonTemplateEvaluator::StatusSuccess, + t->evaluate(BSON("randField" << randObj << "hello" << "world" << + "id" << 1), builder8) ); + BSONObj obj8 = builder8.obj(); + ASSERT_EQUALS(obj8.nFields(), 3); + ASSERT_GREATER_THAN_OR_EQUALS(obj8.firstElement().numberInt(), 0); + ASSERT_LESS_THAN(obj8.firstElement().numberInt(), 5); + + // Test success with #RAND_INT as the middle element + BSONObjBuilder builder9; + randObj = BSON( "#RAND_INT" << BSON_ARRAY( 0 << 5 ) ); + ASSERT_EQUALS( BsonTemplateEvaluator::StatusSuccess, + t->evaluate(BSON("id" << 1 << "randField" << randObj << "hello" << + "world"), builder9) ); + BSONObj obj9 = builder9.obj(); + ASSERT_EQUALS(obj9.nFields(), 3); + BSONObjIterator iter9(obj9); + randValue1 = iter9.next().numberInt(); + ASSERT_GREATER_THAN_OR_EQUALS(randValue1, 0); + ASSERT_LESS_THAN(randValue1, 5); + + // Test success with #RAND_INT as the first and the last element + BSONObjBuilder builder10; + randObj = BSON( "#RAND_INT" << BSON_ARRAY( 0 << 5 ) ); + ASSERT_EQUALS( BsonTemplateEvaluator::StatusSuccess, + t->evaluate(BSON("randField1" << randObj << "hello" << + "world" << "randField2" << randObj), builder10) ); + BSONObj obj10 = builder10.obj(); + ASSERT_EQUALS(obj10.nFields(), 3); + + BSONObjIterator iter10(obj10); + randValue1 = (*iter10).numberInt(); + ASSERT_GREATER_THAN_OR_EQUALS(randValue1, 0); + ASSERT_LESS_THAN(randValue1, 5); + iter10++; + randValue2 = iter10.next().numberInt(); + ASSERT_GREATER_THAN_OR_EQUALS(randValue2, 0); + ASSERT_LESS_THAN(randValue2, 5); + + // Test success when one of the element is an array + BSONObjBuilder builder11; + randObj = BSON( "#RAND_INT" << BSON_ARRAY( 0 << 5 ) ); + ASSERT_EQUALS( BsonTemplateEvaluator::StatusSuccess, + t->evaluate(BSON("testArray" << BSON_ARRAY( 0 << 5 << 10 << 20 ) << + "hello" << "world" << + "randField" << randObj), builder11) ); + BSONObj obj11 = builder11.obj(); + ASSERT_EQUALS(obj11.nFields(), 3); + BSONObjIterator iter11(obj11); + iter11++; + randValue1 = iter11.next().numberInt(); + ASSERT_GREATER_THAN_OR_EQUALS(randValue1, 0); + ASSERT_LESS_THAN(randValue1, 5); + + // Test success with a 3rd argument to #RAND_INT + BSONObjBuilder builder12; + randObj = BSON( "#RAND_INT" << BSON_ARRAY( 0 << 5 << 4 ) ); + ASSERT_EQUALS( BsonTemplateEvaluator::StatusSuccess, + t->evaluate(BSON("id" << randObj), builder12) ); + BSONObj obj12 = builder12.obj(); + ASSERT_EQUALS(obj12.nFields(), 1); + ASSERT_GREATER_THAN_OR_EQUALS(obj12.firstElement().numberInt(), 0); + ASSERT_LESS_THAN_OR_EQUALS(obj12.firstElement().numberInt(), 16); + } + + TEST(BSONTemplateEvaluatorTest, RAND_STRING) { + + BsonTemplateEvaluator *t = new BsonTemplateEvaluator(); + + // Test failure when the arguments to RAND_STRING is not an integer + BSONObjBuilder builder1; + BSONObj randObj = BSON( "#RAND_STRING" << BSON_ARRAY("hello") ); + ASSERT_EQUALS( BsonTemplateEvaluator::StatusOpEvaluationError, + t->evaluate(BSON("randField" << randObj), builder1) ); + + // Test failure when there is more than 1 argument to RAND_STRING + BSONObjBuilder builder2; + randObj = BSON( "#RAND_STRING" << BSON_ARRAY( 2 << 8 ) ); + ASSERT_EQUALS( BsonTemplateEvaluator::StatusOpEvaluationError, + t->evaluate(BSON("randField" << randObj), builder2) ); + + // Test failure when length argument to RAND_STRING is 0 + BSONObjBuilder builder3; + randObj = BSON( "#RAND_STRING" << BSON_ARRAY(0 ) ); + ASSERT_EQUALS( BsonTemplateEvaluator::StatusOpEvaluationError, + t->evaluate(BSON("randField" << randObj), builder3) ); + + // Test success with a single element + BSONObjBuilder builder4; + randObj = BSON( "#RAND_STRING" << BSON_ARRAY(5) ); + ASSERT_EQUALS( BsonTemplateEvaluator::StatusSuccess, + t->evaluate(BSON("randField" << randObj), builder4) ); + BSONObj obj4 = builder4.obj(); + ASSERT_EQUALS(obj4.nFields(), 1); + ASSERT_EQUALS(obj4.firstElement().str().length(), 5U); + + // Test success with two #RAND_STRING elements + BSONObjBuilder builder5; + randObj = BSON( "#RAND_STRING" << BSON_ARRAY(5) ); + ASSERT_EQUALS( BsonTemplateEvaluator::StatusSuccess, + t->evaluate(BSON("randField1" << randObj << + "randField2" << randObj), builder5) ); + BSONObj obj5 = builder5.obj(); + ASSERT_EQUALS(obj5.nFields(), 2); + BSONObjIterator iter5(obj5); + ASSERT_EQUALS(iter5.next().str().length(), 5U); + ASSERT_EQUALS(iter5.next().str().length(), 5U); + + // Test success with #RAND_STRING as the last element + BSONObjBuilder builder6; + randObj = BSON( "#RAND_STRING" << BSON_ARRAY(5) ); + ASSERT_EQUALS( BsonTemplateEvaluator::StatusSuccess, + t->evaluate(BSON("id" << 1 << "hello" << "world" << + "randField" << randObj), builder6) ); + BSONObj obj6 = builder6.obj(); + ASSERT_EQUALS(obj6.nFields(), 3); + BSONObjIterator iter6(obj6); + iter6++; + ASSERT_EQUALS(iter6.next().str().length(), 5U); + + // Test success with #RAND_STRING as first element + BSONObjBuilder builder7; + randObj = BSON( "#RAND_STRING" << BSON_ARRAY(5) ); + ASSERT_EQUALS( BsonTemplateEvaluator::StatusSuccess, + t->evaluate(BSON("randField" << randObj << "hello" << "world" << + "id" << 1), builder7) ); + BSONObj obj7 = builder7.obj(); + ASSERT_EQUALS(obj7.nFields(), 3); + ASSERT_EQUALS(obj7.firstElement().str().length(), 5U); + + // Test success with #RAND_STRING as the middle element + BSONObjBuilder builder8; + randObj = BSON( "#RAND_STRING" << BSON_ARRAY(5) ); + ASSERT_EQUALS( BsonTemplateEvaluator::StatusSuccess, + t->evaluate(BSON("id" << 1 << "randField" << randObj << "hello" << + "world"), builder8) ); + BSONObj obj8 = builder8.obj(); + ASSERT_EQUALS(obj8.nFields(), 3); + BSONObjIterator iter8(obj8); + iter8++; + ASSERT_EQUALS((*iter8).str().length(), 5U); + + // Test success with #RAND_INT as the first and the last element + BSONObjBuilder builder10; + randObj = BSON( "#RAND_STRING" << BSON_ARRAY(5) ); + ASSERT_EQUALS( BsonTemplateEvaluator::StatusSuccess, + t->evaluate(BSON("randField1" << randObj << "hello" << + "world" << "randField2" << randObj), builder10) ); + BSONObj obj10 = builder10.obj(); + ASSERT_EQUALS(obj10.nFields(), 3); + + BSONObjIterator iter10(obj10); + ASSERT_EQUALS((*iter10).str().length(), 5U); + iter10++; + ASSERT_EQUALS(iter10.next().str().length(), 5U); + + // Test success when one of the element is an array + BSONObjBuilder builder11; + randObj = BSON( "#RAND_STRING" << BSON_ARRAY(5) ); + ASSERT_EQUALS( BsonTemplateEvaluator::StatusSuccess, + t->evaluate(BSON("testArray" << BSON_ARRAY( 0 << 5 << 10 << 20 ) << + "hello" << "world" << + "randField" << randObj), builder11) ); + BSONObj obj11 = builder11.obj(); + ASSERT_EQUALS(obj11.nFields(), 3); + BSONObjIterator iter11(obj11); + iter11++; + ASSERT_EQUALS(iter11.next().str().length(), 5U); + } + + TEST(BSONTemplateEvaluatorTest, CONCAT) { + + BsonTemplateEvaluator *t = new BsonTemplateEvaluator(); + + // Test failure when the arguments to #CONCAT has only one argument + BSONObjBuilder builder1; + BSONObj concatObj = BSON( "#CONCAT" << BSON_ARRAY("hello") ); + ASSERT_EQUALS( BsonTemplateEvaluator::StatusOpEvaluationError, + t->evaluate(BSON("concatField" << concatObj), builder1) ); + + // Test success when all arguments to #CONCAT are strings + BSONObjBuilder builder2; + concatObj = BSON( "#CONCAT" << BSON_ARRAY("hello" << " " << "world")); + ASSERT_EQUALS( BsonTemplateEvaluator::StatusSuccess, + t->evaluate(BSON("concatField" << concatObj), builder2) ); + BSONObj obj2 = builder2.obj(); + ASSERT_EQUALS(obj2.nFields(), 1); + BSONObj expectedObj = BSON("concatField" << "hello world"); + ASSERT_EQUALS(obj2.equal(expectedObj), true); + + // Test success when some arguments to #CONCAT are integers + BSONObjBuilder builder3; + concatObj = BSON( "#CONCAT" << BSON_ARRAY("F" << 1 << "racing")); + ASSERT_EQUALS( BsonTemplateEvaluator::StatusSuccess, + t->evaluate(BSON("concatField" << concatObj), builder3) ); + BSONObj obj3 = builder3.obj(); + ASSERT_EQUALS(obj3.nFields(), 1); + expectedObj = BSON("concatField" << "F1racing"); + ASSERT_EQUALS(obj3.equal(expectedObj), true); + + // Test success with #CONCAT as first element and last element + BSONObjBuilder builder4; + concatObj = BSON( "#CONCAT" << BSON_ARRAY("hello" << " " << "world")); + ASSERT_EQUALS( BsonTemplateEvaluator::StatusSuccess, + t->evaluate(BSON("concatField1" << concatObj << + "middleKey" << 1 << + "concatField2" << concatObj), builder4) ); + BSONObj obj4 = builder4.obj(); + ASSERT_EQUALS(obj4.nFields(), 3); + expectedObj = BSON("concatField1" << "hello world" << + "middleKey" << 1 << + "concatField2" << "hello world"); + ASSERT_EQUALS(obj4.equal(expectedObj), true); + + // Test success when one of the arguments to #CONCAT is an array + BSONObjBuilder builder5; + concatObj = BSON( "#CONCAT" << BSON_ARRAY("hello" << BSON_ARRAY(1 << 10) << "world")); + ASSERT_EQUALS( BsonTemplateEvaluator::StatusSuccess, + t->evaluate(BSON("concatField" << concatObj), builder5) ); + BSONObj obj5 = builder5.obj(); + ASSERT_EQUALS(obj5.nFields(), 1); + expectedObj = BSON("concatField" << "hello[ 1, 10 ]world"); + ASSERT_EQUALS(obj5.equal(expectedObj), true); + } + + TEST(BSONTemplateEvaluatorTest, COMBINED_OPERATORS) { + + BsonTemplateEvaluator *t = new BsonTemplateEvaluator(); + BSONObj randIntObj = BSON( "#RAND_INT" << BSON_ARRAY( 0 << 5 ) ); + BSONObj randStrObj = BSON( "#RAND_STRING" << BSON_ARRAY(5) ); + + // Test success when #RAND_INT, and #RAND_STRING are combined + BSONObjBuilder builder1; + ASSERT_EQUALS( BsonTemplateEvaluator::StatusSuccess, + t->evaluate(BSON("randInt" << randIntObj << + "randStr" << randStrObj), builder1) ); + BSONObj obj1 = builder1.obj(); + ASSERT_EQUALS(obj1.nFields(), 2); + BSONObjIterator iter1(obj1); + int randInt = iter1.next().numberInt(); + ASSERT_GREATER_THAN_OR_EQUALS(randInt, 0); + ASSERT_LESS_THAN(randInt, 5); + string randStr = iter1.next().str(); + ASSERT_EQUALS(randStr.length(), 5U); + + // Test success when the #CONCAT and #RAND_INT and #RAND_STRING are combined + BSONObjBuilder builder2; + BSONObj concatObj = BSON("#CONCAT" << BSON_ARRAY(randIntObj << " hello world " << + randStrObj)); + ASSERT_EQUALS( BsonTemplateEvaluator::StatusSuccess, + t->evaluate(BSON("concatField" << concatObj), builder2) ); + BSONObj obj2 = builder2.obj(); + ASSERT_EQUALS(obj2.nFields(), 1); + // check that the resulting string has a length of 19. + // randIntObj.length = 1, " hello world " = 13, randStrObj.length = 5 + // so total string length should 1 + 13 + 5 = 19 + ASSERT_EQUALS(obj2.firstElement().str().length(), 19U); + } + } // end anonymous namespace +} // end namespace mongo diff --git a/src/mongo/scripting/engine.cpp b/src/mongo/scripting/engine.cpp new file mode 100644 index 00000000000..9da44fe9b3e --- /dev/null +++ b/src/mongo/scripting/engine.cpp @@ -0,0 +1,521 @@ +// engine.cpp + +/* Copyright 2009 10gen Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "pch.h" +#include "engine.h" +#include "../util/file.h" +#include "mongo/client/dbclientinterface.h" +#include "mongo/client/dbclientcursor.h" +#include <boost/filesystem/operations.hpp> + +namespace mongo { + + long long Scope::_lastVersion = 1; + + int Scope::_numScopes = 0; + + Scope::Scope() : _localDBName("") , _loadedVersion(0), _numTimeUsed(0) { + _numScopes++; + } + + Scope::~Scope() { + _numScopes--; + } + + ScriptEngine::ScriptEngine() : _scopeInitCallback() { + } + + ScriptEngine::~ScriptEngine() { + } + + void Scope::append( BSONObjBuilder & builder , const char * fieldName , const char * scopeName ) { + int t = type( scopeName ); + + switch ( t ) { + case Object: + builder.append( fieldName , getObject( scopeName ) ); + break; + case Array: + builder.appendArray( fieldName , getObject( scopeName ) ); + break; + case NumberDouble: + builder.append( fieldName , getNumber( scopeName ) ); + break; + case NumberInt: + builder.append( fieldName , getNumberInt( scopeName ) ); + break; + case NumberLong: + builder.append( fieldName , getNumberLongLong( scopeName ) ); + break; + case String: + builder.append( fieldName , getString( scopeName ).c_str() ); + break; + case Bool: + builder.appendBool( fieldName , getBoolean( scopeName ) ); + break; + case jstNULL: + case Undefined: + builder.appendNull( fieldName ); + break; + case Date: + // TODO: make signed + builder.appendDate( fieldName , Date_t((unsigned long long)getNumber( scopeName )) ); + break; + case Code: + builder.appendCode( fieldName , getString( scopeName ) ); + break; + default: + stringstream temp; + temp << "can't append type from:"; + temp << t; + uassert( 10206 , temp.str() , 0 ); + } + + } + + int Scope::invoke( const char* code , const BSONObj* args, const BSONObj* recv, int timeoutMs ) { + ScriptingFunction func = createFunction( code ); + uassert( 10207 , "compile failed" , func ); + return invoke( func , args, recv, timeoutMs ); + } + + bool Scope::execFile( const string& filename , bool printResult , bool reportError , bool assertOnError, int timeoutMs ) { + + boost::filesystem::path p( filename ); + + if ( ! exists( p ) ) { + log() << "file [" << filename << "] doesn't exist" << endl; + if ( assertOnError ) + verify( 0 ); + return false; + } + + // iterate directories and recurse using all *.js files in the directory + if ( boost::filesystem::is_directory( p ) ) { + boost::filesystem::directory_iterator end; + bool empty = true; + for (boost::filesystem::directory_iterator it (p); it != end; it++) { + empty = false; + boost::filesystem::path sub(*it); + if (!endsWith(sub.string().c_str(), ".js")) + continue; + if (!execFile(sub.string().c_str(), printResult, reportError, assertOnError, timeoutMs)) + return false; + } + + if (empty) { + log() << "directory [" << filename << "] doesn't have any *.js files" << endl; + if ( assertOnError ) + verify( 0 ); + return false; + } + + return true; + } + + File f; + f.open( filename.c_str() , true ); + + unsigned L; + { + fileofs fo = f.len(); + verify( fo <= 0x7ffffffe ); + L = (unsigned) fo; + } + boost::scoped_array<char> data (new char[L+1]); + data[L] = 0; + f.read( 0 , data.get() , L ); + + int offset = 0; + if (data[0] == '#' && data[1] == '!') { + const char* newline = strchr(data.get(), '\n'); + if (! newline) + return true; // file of just shebang treated same as empty file + offset = newline - data.get(); + } + + StringData code (data.get() + offset, L - offset); + + return exec( code , filename , printResult , reportError , assertOnError, timeoutMs ); + } + + void Scope::storedFuncMod() { + _lastVersion++; + } + + void Scope::validateObjectIdString( const string &str ) { + massert( 10448 , "invalid object id: length", str.size() == 24 ); + + for ( string::size_type i=0; i<str.size(); i++ ) { + char c = str[i]; + if ( ( c >= '0' && c <= '9' ) || + ( c >= 'a' && c <= 'f' ) || + ( c >= 'A' && c <= 'F' ) ) { + continue; + } + massert( 10430 , "invalid object id: not hex", false ); + } + } + + void Scope::loadStored( bool ignoreNotConnected ) { + if ( _localDBName.size() == 0 ) { + if ( ignoreNotConnected ) + return; + uassert( 10208 , "need to have locallyConnected already" , _localDBName.size() ); + } + if ( _loadedVersion == _lastVersion ) + return; + + _loadedVersion = _lastVersion; + + string coll = _localDBName + ".system.js"; + + static DBClientBase * db = createDirectClient(); + auto_ptr<DBClientCursor> c = db->query( coll , Query(), 0, 0, NULL, QueryOption_SlaveOk, 0 ); + verify( c.get() ); + + set<string> thisTime; + + while ( c->more() ) { + BSONObj o = c->nextSafe(); + + BSONElement n = o["_id"]; + BSONElement v = o["value"]; + + uassert( 10209 , str::stream() << "name has to be a string: " << n , n.type() == String ); + uassert( 10210 , "value has to be set" , v.type() != EOO ); + + setElement( n.valuestr() , v ); + + thisTime.insert( n.valuestr() ); + _storedNames.insert( n.valuestr() ); + + } + + // --- remove things from scope that were removed + + list<string> toremove; + + for ( set<string>::iterator i=_storedNames.begin(); i!=_storedNames.end(); i++ ) { + string n = *i; + if ( thisTime.count( n ) == 0 ) + toremove.push_back( n ); + } + + for ( list<string>::iterator i=toremove.begin(); i!=toremove.end(); i++ ) { + string n = *i; + _storedNames.erase( n ); + execSetup( (string)"delete " + n , "clean up scope" ); + } + + } + + ScriptingFunction Scope::createFunction( const char * code ) { + if ( code[0] == '/' && code [1] == '*' ) { + code += 2; + while ( code[0] && code[1] ) { + if ( code[0] == '*' && code[1] == '/' ) { + code += 2; + break; + } + code++; + } + } + map<string,ScriptingFunction>::iterator i = _cachedFunctions.find( code ); + if ( i != _cachedFunctions.end() ) + return i->second; + ScriptingFunction f = _createFunction( code ); + _cachedFunctions[code] = f; + return f; + } + + namespace JSFiles { + extern const JSFile collection; + extern const JSFile db; + extern const JSFile mongo; + extern const JSFile mr; + extern const JSFile query; + extern const JSFile utils; + extern const JSFile utils_sh; + } + + void Scope::execCoreFiles() { + // keeping same order as in SConstruct + execSetup(JSFiles::utils); + execSetup(JSFiles::utils_sh); + execSetup(JSFiles::db); + execSetup(JSFiles::mongo); + execSetup(JSFiles::mr); + execSetup(JSFiles::query); + execSetup(JSFiles::collection); + } + + typedef map< string , list<Scope*> > PoolToScopes; + + class ScopeCache { + public: + + ScopeCache() : _mutex("ScopeCache") { + _magic = 17; + } + + ~ScopeCache() { + verify( _magic == 17 ); + _magic = 1; + + if ( inShutdown() ) + return; + + clear(); + } + + void done( const string& pool , Scope * s ) { + scoped_lock lk( _mutex ); + list<Scope*> & l = _pools[pool]; + bool oom = s->hasOutOfMemoryException(); + + // do not keep too many contexts, or use them for too long + if ( l.size() > 10 || s->getTimeUsed() > 10 || oom ) { + delete s; + } + else { + l.push_back( s ); + s->reset(); + } + + if (oom) { + // out of mem, make some room + log() << "Clearing all idle JS contexts due to out of memory" << endl; + clear(); + } + } + + Scope * get( const string& pool ) { + scoped_lock lk( _mutex ); + list<Scope*> & l = _pools[pool]; + if ( l.size() == 0 ) + return 0; + + Scope * s = l.back(); + l.pop_back(); + s->reset(); + s->incTimeUsed(); + return s; + } + + void clear() { + set<Scope*> seen; + + for ( PoolToScopes::iterator i=_pools.begin() ; i != _pools.end(); i++ ) { + for ( list<Scope*>::iterator j=i->second.begin(); j != i->second.end(); j++ ) { + Scope * s = *j; + verify( ! seen.count( s ) ); + delete s; + seen.insert( s ); + } + } + + _pools.clear(); + } + + private: + PoolToScopes _pools; + mongo::mutex _mutex; + int _magic; + }; + + thread_specific_ptr<ScopeCache> scopeCache; + + class PooledScope : public Scope { + public: + PooledScope( const string pool , Scope * real ) : _pool( pool ) , _real( real ) { + _real->loadStored( true ); + }; + virtual ~PooledScope() { + ScopeCache * sc = scopeCache.get(); + if ( sc ) { + sc->done( _pool , _real ); + _real = 0; + } + else { + // this means that the Scope was killed from a different thread + // for example a cursor got timed out that has a $where clause + log(3) << "warning: scopeCache is empty!" << endl; + delete _real; + _real = 0; + } + } + + void reset() { + _real->reset(); + } + void init( const BSONObj * data ) { + _real->init( data ); + } + + void localConnect( const char * dbName ) { + _real->localConnect( dbName ); + } + void externalSetup() { + _real->externalSetup(); + } + + double getNumber( const char *field ) { + return _real->getNumber( field ); + } + string getString( const char *field ) { + return _real->getString( field ); + } + bool getBoolean( const char *field ) { + return _real->getBoolean( field ); + } + BSONObj getObject( const char *field ) { + return _real->getObject( field ); + } + + int type( const char *field ) { + return _real->type( field ); + } + + void setElement( const char *field , const BSONElement& val ) { + _real->setElement( field , val ); + } + void setNumber( const char *field , double val ) { + _real->setNumber( field , val ); + } + void setString( const char *field , const char * val ) { + _real->setString( field , val ); + } + void setObject( const char *field , const BSONObj& obj , bool readOnly=true ) { + _real->setObject( field , obj , readOnly ); + } + void setBoolean( const char *field , bool val ) { + _real->setBoolean( field , val ); + } +// void setThis( const BSONObj * obj ) { +// _real->setThis( obj ); +// } + + void setFunction( const char *field , const char * code ) { + _real->setFunction(field, code); + } + + ScriptingFunction createFunction( const char * code ) { + return _real->createFunction( code ); + } + + ScriptingFunction _createFunction( const char * code ) { + return _real->createFunction( code ); + } + + void rename( const char * from , const char * to ) { + _real->rename( from , to ); + } + + /** + * @return 0 on success + */ + int invoke( ScriptingFunction func , const BSONObj* args, const BSONObj* recv, int timeoutMs , bool ignoreReturn, bool readOnlyArgs, bool readOnlyRecv ) { + return _real->invoke( func , args , recv, timeoutMs , ignoreReturn, readOnlyArgs, readOnlyRecv ); + } + + string getError() { + return _real->getError(); + } + + bool hasOutOfMemoryException() { + return _real->hasOutOfMemoryException(); + } + + bool exec( const StringData& code , const string& name , bool printResult , bool reportError , bool assertOnError, int timeoutMs = 0 ) { + return _real->exec( code , name , printResult , reportError , assertOnError , timeoutMs ); + } + bool execFile( const string& filename , bool printResult , bool reportError , bool assertOnError, int timeoutMs = 0 ) { + return _real->execFile( filename , printResult , reportError , assertOnError , timeoutMs ); + } + + void injectNative( const char *field, NativeFunction func, void* data ) { + _real->injectNative( field , func, data ); + } + + void gc() { + _real->gc(); + } + + void append( BSONObjBuilder & builder , const char * fieldName , const char * scopeName ) { + _real->append(builder, fieldName, scopeName); + } + + private: + string _pool; + Scope * _real; + }; + + auto_ptr<Scope> ScriptEngine::getPooledScope( const string& pool ) { + if ( ! scopeCache.get() ) { + scopeCache.reset( new ScopeCache() ); + } + + Scope * s = scopeCache->get( pool ); + if ( ! s ) { + s = newScope(); + } + + auto_ptr<Scope> p; + p.reset( new PooledScope( pool , s ) ); + return p; + } + + void ScriptEngine::threadDone() { + ScopeCache * sc = scopeCache.get(); + if ( sc ) { + sc->clear(); + } + } + + void ( *ScriptEngine::_connectCallback )( DBClientWithCommands & ) = 0; + const char * ( *ScriptEngine::_checkInterruptCallback )() = 0; + unsigned ( *ScriptEngine::_getInterruptSpecCallback )() = 0; + + ScriptEngine * globalScriptEngine = 0; + + bool hasJSReturn( const string& code ) { + size_t x = code.find( "return" ); + if ( x == string::npos ) + return false; + + return + ( x == 0 || ! isalpha( code[x-1] ) ) && + ! isalpha( code[x+6] ); + } + + const char * jsSkipWhiteSpace( const char * raw ) { + while ( raw[0] ) { + while (isspace(*raw)) { + raw++; + } + + if ( raw[0] != '/' || raw[1] != '/' ) + break; + + while ( raw[0] && raw[0] != '\n' ) + raw++; + } + return raw; + } +} + diff --git a/src/mongo/scripting/engine.h b/src/mongo/scripting/engine.h new file mode 100644 index 00000000000..6b0064d28e6 --- /dev/null +++ b/src/mongo/scripting/engine.h @@ -0,0 +1,231 @@ +// engine.h + +/* Copyright 2009 10gen Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include "mongo/db/jsobj.h" + +namespace mongo { + + struct JSFile { + const char* name; + const StringData& source; + }; + + typedef unsigned long long ScriptingFunction; + typedef BSONObj (*NativeFunction) ( const BSONObj &args, void* data ); + + class Scope : boost::noncopyable { + public: + Scope(); + virtual ~Scope(); + + virtual void reset() = 0; + virtual void init( const BSONObj * data ) = 0; + void init( const char * data ) { + BSONObj o( data ); + init( &o ); + } + + virtual void localConnect( const char * dbName ) = 0; + virtual void externalSetup() = 0; + + class NoDBAccess { + Scope * _s; + public: + NoDBAccess( Scope * s ) { + _s = s; + } + ~NoDBAccess() { + _s->rename( "____db____" , "db" ); + } + }; + NoDBAccess disableDBAccess( const char * why ) { + rename( "db" , "____db____" ); + return NoDBAccess( this ); + } + + virtual double getNumber( const char *field ) = 0; + virtual int getNumberInt( const char *field ) { return (int)getNumber( field ); } + virtual long long getNumberLongLong( const char *field ) { return (long long)getNumber( field ); } + virtual string getString( const char *field ) = 0; + virtual bool getBoolean( const char *field ) = 0; + virtual BSONObj getObject( const char *field ) = 0; + + virtual int type( const char *field ) = 0; + + virtual void append( BSONObjBuilder & builder , const char * fieldName , const char * scopeName ); + + virtual void setElement( const char *field , const BSONElement& e ) = 0; + virtual void setNumber( const char *field , double val ) = 0; + virtual void setString( const char *field , const char * val ) = 0; + virtual void setObject( const char *field , const BSONObj& obj , bool readOnly=true ) = 0; + virtual void setBoolean( const char *field , bool val ) = 0; + virtual void setFunction( const char *field , const char * code ) = 0; +// virtual void setThis( const BSONObj * obj ) = 0; + + virtual ScriptingFunction createFunction( const char * code ); + + virtual void rename( const char * from , const char * to ) = 0; + /** + * @return 0 on success + */ + virtual int invoke( ScriptingFunction func , const BSONObj* args, const BSONObj* recv, int timeoutMs = 0 , bool ignoreReturn = false, bool readOnlyArgs = false, bool readOnlyRecv = false ) = 0; + void invokeSafe( ScriptingFunction func , const BSONObj* args, const BSONObj* recv, int timeoutMs = 0 , bool ignoreReturn = false, bool readOnlyArgs = false, bool readOnlyRecv = false ) { + int res = invoke( func , args , recv, timeoutMs, ignoreReturn, readOnlyArgs, readOnlyRecv ); + if ( res == 0 ) + return; + throw UserException( 9004 , (string)"invoke failed: " + getError() ); + } + virtual string getError() = 0; + virtual bool hasOutOfMemoryException() = 0; + + int invoke( const char* code , const BSONObj* args, const BSONObj* recv, int timeoutMs = 0 ); + void invokeSafe( const char* code , const BSONObj* args, const BSONObj* recv, int timeoutMs = 0 ) { + if ( invoke( code , args , recv, timeoutMs ) == 0 ) + return; + throw UserException( 9005 , (string)"invoke failed: " + getError() ); + } + + virtual bool exec( const StringData& code , const string& name , bool printResult , bool reportError , bool assertOnError, int timeoutMs = 0 ) = 0; + virtual void execSetup( const StringData& code , const string& name = "setup" ) { + exec( code , name , false , true , true , 0 ); + } + + void execSetup( const JSFile& file) { + execSetup(file.source, file.name); + } + + void execCoreFiles(); + + virtual bool execFile( const string& filename , bool printResult , bool reportError , bool assertOnError, int timeoutMs = 0 ); + + virtual void injectNative( const char *field, NativeFunction func, void* data = 0 ) = 0; + + virtual void gc() = 0; + + void loadStored( bool ignoreNotConnected = false ); + + /** + if any changes are made to .system.js, call this + right now its just global - slightly inefficient, but a lot simpler + */ + static void storedFuncMod(); + + static int getNumScopes() { + return _numScopes; + } + + static void validateObjectIdString( const string &str ); + + /** increments the number of times a scope was used */ + void incTimeUsed() { ++_numTimeUsed; } + /** gets the number of times a scope was used */ + int getTimeUsed() { return _numTimeUsed; } + + protected: + + virtual ScriptingFunction _createFunction( const char * code ) = 0; + + string _localDBName; + long long _loadedVersion; + set<string> _storedNames; + static long long _lastVersion; + map<string,ScriptingFunction> _cachedFunctions; + int _numTimeUsed; + + static int _numScopes; + }; + + void installGlobalUtils( Scope& scope ); + + class DBClientWithCommands; + + class ScriptEngine : boost::noncopyable { + public: + ScriptEngine(); + virtual ~ScriptEngine(); + + virtual Scope * newScope() { + Scope *s = createScope(); + if ( s && _scopeInitCallback ) + _scopeInitCallback( *s ); + installGlobalUtils( *s ); + return s; + } + + virtual void runTest() = 0; + + virtual bool utf8Ok() const = 0; + + static void setup(); + + /** gets a scope from the pool or a new one if pool is empty + * @param pool An identifier for the pool, usually the db name + * @return the scope */ + auto_ptr<Scope> getPooledScope( const string& pool ); + + /** call this method to release some JS resources when a thread is done */ + void threadDone(); + + void setScopeInitCallback( void ( *func )( Scope & ) ) { _scopeInitCallback = func; } + static void setConnectCallback( void ( *func )( DBClientWithCommands& ) ) { _connectCallback = func; } + static void runConnectCallback( DBClientWithCommands &c ) { + if ( _connectCallback ) + _connectCallback( c ); + } + + // engine implementation may either respond to interrupt events or + // poll for interrupts + + // the interrupt functions must not wait indefinitely on a lock + virtual void interrupt( unsigned opSpec ) {} + virtual void interruptAll() {} + + static void setGetInterruptSpecCallback( unsigned ( *func )() ) { _getInterruptSpecCallback = func; } + static bool haveGetInterruptSpecCallback() { return _getInterruptSpecCallback; } + static unsigned getInterruptSpec() { + massert( 13474, "no _getInterruptSpecCallback", _getInterruptSpecCallback ); + return _getInterruptSpecCallback(); + } + + static void setCheckInterruptCallback( const char * ( *func )() ) { _checkInterruptCallback = func; } + static bool haveCheckInterruptCallback() { return _checkInterruptCallback; } + static const char * checkInterrupt() { + return _checkInterruptCallback ? _checkInterruptCallback() : ""; + } + static bool interrupted() { + const char *r = checkInterrupt(); + return r && r[ 0 ]; + } + + protected: + virtual Scope * createScope() = 0; + + private: + void ( *_scopeInitCallback )( Scope & ); + static void ( *_connectCallback )( DBClientWithCommands & ); + static const char * ( *_checkInterruptCallback )(); + static unsigned ( *_getInterruptSpecCallback )(); + }; + + bool hasJSReturn( const string& s ); + + const char * jsSkipWhiteSpace( const char * raw ); + + extern ScriptEngine * globalScriptEngine; +} diff --git a/src/mongo/scripting/engine_none.cpp b/src/mongo/scripting/engine_none.cpp new file mode 100644 index 00000000000..d13dbecc06e --- /dev/null +++ b/src/mongo/scripting/engine_none.cpp @@ -0,0 +1,24 @@ +// engine_none.cpp + +/* Copyright 2009 10gen Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "engine.h" + +namespace mongo { + void ScriptEngine::setup() { + // noop + } +} diff --git a/src/mongo/scripting/engine_spidermonkey.cpp b/src/mongo/scripting/engine_spidermonkey.cpp new file mode 100644 index 00000000000..4909ddd9763 --- /dev/null +++ b/src/mongo/scripting/engine_spidermonkey.cpp @@ -0,0 +1,2010 @@ +// engine_spidermonkey.cpp + +/* Copyright 2009 10gen Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "pch.h" + +#include "mongo/scripting/engine_spidermonkey.h" + +#include <boost/smart_ptr/scoped_array.hpp> +#include <boost/thread/recursive_mutex.hpp> +#ifndef _WIN32 +#include <boost/date_time/posix_time/posix_time.hpp> +#endif + +#include <third_party/js-1.7/jsdate.h> + +#include "mongo/util/mongoutils/str.h" + +#define smuassert( cx , msg , val ) \ + if ( ! ( val ) ){ \ + JS_ReportError( cx , msg ); \ + return JS_FALSE; \ + } + +#define CHECKNEWOBJECT(xx,ctx,w) \ + if ( ! xx ){ \ + massert(13072,(string)"JS_NewObject failed: " + w ,xx); \ + } + +#define CHECKJSALLOC( newthing ) \ + massert( 13615 , "JS allocation failed, either memory leak or using too much memory" , newthing ) + +namespace mongo { + + string trim( string s ) { + while ( s.size() && isspace( s[0] ) ) + s = s.substr( 1 ); + + while ( s.size() && isspace( s[s.size()-1] ) ) + s = s.substr( 0 , s.size() - 1 ); + + return s; + } + + boost::thread_specific_ptr<SMScope> currentScope( dontDeleteScope ); + boost::recursive_mutex &smmutex = *( new boost::recursive_mutex ); +#define smlock boost::recursive_mutex::scoped_lock ___lk( smmutex ); + +#define GETHOLDER(x,o) ((BSONHolder*)JS_GetPrivate( x , o )) + + class BSONFieldIterator; + + class BSONHolder { + public: + + BSONHolder( BSONObj obj ) { + _obj = obj.getOwned(); + _inResolve = false; + _modified = false; + _magic = 17; + } + + ~BSONHolder() { + _magic = 18; + } + + void check() { + uassert( 10212 , "holder magic value is wrong" , _magic == 17 && _obj.isValid() ); + } + + BSONFieldIterator * it(); + + BSONObj _obj; + bool _inResolve; + char _magic; + list<string> _extra; + set<string> _removed; + bool _modified; + }; + + class BSONFieldIterator { + public: + + BSONFieldIterator( BSONHolder * holder ) { + + set<string> added; + + BSONObjIterator it( holder->_obj ); + while ( it.more() ) { + BSONElement e = it.next(); + if ( holder->_removed.count( e.fieldName() ) ) + continue; + _names.push_back( e.fieldName() ); + added.insert( e.fieldName() ); + } + + for ( list<string>::iterator i = holder->_extra.begin(); i != holder->_extra.end(); i++ ) { + if ( ! added.count( *i ) ) + _names.push_back( *i ); + } + + _it = _names.begin(); + } + + bool more() { + return _it != _names.end(); + } + + string next() { + string s = *_it; + _it++; + return s; + } + + private: + list<string> _names; + list<string>::iterator _it; + }; + + BSONFieldIterator * BSONHolder::it() { + return new BSONFieldIterator( this ); + } + + class TraverseStack { + public: + TraverseStack() { + _o = 0; + _parent = 0; + } + + TraverseStack( JSObject * o , const TraverseStack * parent ) { + _o = o; + _parent = parent; + } + + TraverseStack dive( JSObject * o ) const { + if ( o ) { + uassert( 13076 , (string)"recursive toObject" , ! has( o ) ); + } + return TraverseStack( o , this ); + } + + int depth() const { + int d = 0; + const TraverseStack * s = _parent; + while ( s ) { + s = s->_parent; + d++; + } + return d; + } + + bool isTop() const { + return _parent == 0; + } + + bool has( JSObject * o ) const { + if ( ! o ) + return false; + const TraverseStack * s = this; + while ( s ) { + if ( s->_o == o ) + return true; + s = s->_parent; + } + return false; + } + + JSObject * _o; + const TraverseStack * _parent; + }; + + class Convertor : boost::noncopyable { + public: + Convertor( JSContext * cx ) { + _context = cx; + } + + string toString( JSString* jsString ) { + size_t srclen = JS_GetStringLength( jsString ); + if( srclen == 0 ) + return ""; + + size_t len = (srclen * 6) + 1; + boost::scoped_array<char> utf8Chars( new char[len] ); + jschar* utf16Chars = JS_GetStringChars( jsString ); + if ( !JS_EncodeCharacters( _context, utf16Chars, srclen, utf8Chars.get(), &len ) ) { + uasserted( 16268, "error converting UTF-16 string to UTF-8" ); + } + return string( utf8Chars.get(), len ); + } + + string toString( jsval v ) { + return toString( JS_ValueToString( _context , v ) ); + } + + // NOTE No validation of passed in object + long long toNumberLongUnsafe( JSObject *o ) { + boost::uint64_t val; + if ( hasProperty( o, "top" ) ) { + val = + ( (boost::uint64_t)(boost::uint32_t)getNumber( o , "top" ) << 32 ) + + ( boost::uint32_t)( getNumber( o , "bottom" ) ); + } + else { + val = (boost::uint64_t)(boost::int64_t) getNumber( o, "floatApprox" ); + } + return val; + } + + int toNumberInt( JSObject *o ) { + return (boost::uint32_t)(boost::int32_t) getNumber( o, "floatApprox" ); + } + + double toNumber( jsval v ) { + double d; + uassert( 10214 , "not a number" , JS_ValueToNumber( _context , v , &d ) ); + return d; + } + + bool toBoolean( jsval v ) { + JSBool b; + verify( JS_ValueToBoolean( _context, v , &b ) ); + return b; + } + + OID toOID( jsval v ) { + JSContext * cx = _context; + verify( JSVAL_IS_OID( v ) ); + + JSObject * o = JSVAL_TO_OBJECT( v ); + OID oid; + oid.init( getString( o , "str" ) ); + return oid; + } + + BSONObj toObject( JSObject * o , const TraverseStack& stack=TraverseStack() ) { + if ( ! o ) + return BSONObj(); + + if ( JS_InstanceOf( _context , o , &bson_ro_class , 0 ) ) { + BSONHolder * holder = GETHOLDER( _context , o ); + verify( holder ); + return holder->_obj.getOwned(); + } + + BSONObj orig; + if ( JS_InstanceOf( _context , o , &bson_class , 0 ) ) { + BSONHolder * holder = GETHOLDER(_context,o); + verify( holder ); + if ( ! holder->_modified ) { + return holder->_obj; + } + orig = holder->_obj; + } + + BSONObjBuilder b; + + if ( ! appendSpecialDBObject( this , b , "value" , OBJECT_TO_JSVAL( o ) , o ) ) { + + if ( stack.isTop() ) { + jsval theid = getProperty( o , "_id" ); + if ( ! JSVAL_IS_VOID( theid ) ) { + append( b , "_id" , theid , EOO , stack.dive( o ) ); + } + } + + JSIdArray * properties = JS_Enumerate( _context , o ); + verify( properties ); + + try { + for ( jsint i=0; i<properties->length; i++ ) { + jsid id = properties->vector[i]; + jsval nameval; + verify( JS_IdToValue( _context, id, &nameval ) ); + string name = toString( nameval ); + if ( stack.isTop() && name == "_id" ) + continue; + + append( b, + name, + getProperty( o, name.c_str() ), + orig[name].type(), + stack.dive( o ) ); + } + } + catch ( const AssertionException& ) { + JS_DestroyIdArray( _context , properties ); + throw; + } + catch ( const std::exception& e ) { + log() << "unhandled exception: " << e.what() << ", throwing Fatal Assertion" << endl; + fassertFailed( 16269 ); + } + JS_DestroyIdArray( _context , properties ); + } + + return b.obj(); + } + + BSONObj toObject( jsval v ) { + if ( JSVAL_IS_NULL( v ) || JSVAL_IS_VOID( v ) ) + return BSONObj(); + + uassert( 10215, "not an object", JSVAL_IS_OBJECT( v ) ); + return toObject( JSVAL_TO_OBJECT( v ) ); + } + + string getFunctionCode( JSFunction * func ) { + return toString( JS_DecompileFunction( _context , func , 0 ) ); + } + + string getFunctionCode( jsval v ) { + uassert( 10216, "not a function", JS_TypeOfValue( _context, v ) == JSTYPE_FUNCTION ); + return getFunctionCode( JS_ValueToFunction( _context , v ) ); + } + + void appendRegex( BSONObjBuilder& b , const string& name , string s ) { + verify( s[0] == '/' ); + s = s.substr(1); + string::size_type end = s.rfind( '/' ); + b.appendRegex( name , s.substr( 0 , end ) , s.substr( end + 1 ) ); + } + + void append( BSONObjBuilder& b , string name , jsval val , BSONType oldType = EOO , const TraverseStack& stack=TraverseStack() ) { + //cout << "name: " << name << "\t" << typeString( val ) << " oldType: " << oldType << endl; + switch ( JS_TypeOfValue( _context , val ) ) { + + case JSTYPE_VOID: b.appendUndefined( name ); break; + case JSTYPE_NULL: b.appendNull( name ); break; + + case JSTYPE_NUMBER: { + double d = toNumber( val ); + if ( oldType == NumberInt && ((int)d) == d ) + b.append( name , (int)d ); + else + b.append( name , d ); + break; + } + case JSTYPE_STRING: b.append( name , toString( val ) ); break; + case JSTYPE_BOOLEAN: b.appendBool( name , toBoolean( val ) ); break; + + case JSTYPE_OBJECT: { + JSObject * o = JSVAL_TO_OBJECT( val ); + if ( ! o || o == JSVAL_NULL ) { + b.appendNull( name ); + } + else if ( ! appendSpecialDBObject( this , b , name , val , o ) ) { + BSONObj sub = toObject( o , stack ); + if ( JS_IsArrayObject( _context , o ) ) { + b.appendArray( name , sub ); + } + else { + b.append( name , sub ); + } + } + break; + } + + case JSTYPE_FUNCTION: { + string s = toString(val); + if ( s[0] == '/' ) { + appendRegex( b , name , s ); + } + else { + b.appendCode( name , getFunctionCode( val ) ); + } + break; + } + + default: uassert( 10217 , (string)"can't append field. name:" + name + " type: " + typeString( val ) , 0 ); + } + } + + // ---------- to spider monkey --------- + + bool hasFunctionIdentifier( const string& code ) { + if ( code.size() < 9 || code.find( "function" ) != 0 ) + return false; + + return code[8] == ' ' || code[8] == '('; + } + + bool isSimpleStatement( const string& code ) { + if ( hasJSReturn( code ) ) + return false; + + if ( code.find( ';' ) != string::npos && + code.find( ';' ) != code.rfind( ';' ) ) + return false; + + if ( code.find( '\n') != string::npos ) + return false; + + if ( code.find( "for(" ) != string::npos || + code.find( "for (" ) != string::npos || + code.find( "while (" ) != string::npos || + code.find( "while(" ) != string::npos ) + return false; + + return true; + } + + JSFunction * compileFunction( const char * code, JSObject * assoc = 0 ) { + const char * gcName = "unknown"; + JSFunction * f = _compileFunction( code , assoc , gcName ); + return f; + } + + JSFunction * _compileFunction( const char * raw , JSObject * assoc , const char *& gcName ) { + if ( ! assoc ) + assoc = JS_GetGlobalObject( _context ); + + raw = jsSkipWhiteSpace( raw ); + + //cout << "RAW\n---\n" << raw << "\n---" << endl; + + static int fnum = 1; + stringstream fname; + fname << "__cf__" << fnum++ << "__"; + + if ( ! hasFunctionIdentifier( raw ) ) { + string s = raw; + if ( isSimpleStatement( s ) ) { + s = "return " + s; + } + gcName = "cf anon"; + fname << "anon"; + return JS_CompileFunction( _context , assoc , fname.str().c_str() , 0 , 0 , s.c_str() , s.size() , "nofile_a" , 0 ); + } + + string code = raw; + + size_t start = code.find( '(' ); + verify( start != string::npos ); + + string fbase; + if ( start > 9 ) { + fbase = trim( code.substr( 9 , start - 9 ) ); + } + if ( fbase.length() == 0 ) { + fbase = "anonymous_function"; + } + fname << "f__" << fbase; + + code = code.substr( start + 1 ); + size_t end = code.find( ')' ); + verify( end != string::npos ); + + string paramString = trim( code.substr( 0 , end ) ); + code = code.substr( end + 1 ); + + vector<string> params; + while ( paramString.size() ) { + size_t c = paramString.find( ',' ); + if ( c == string::npos ) { + params.push_back( paramString ); + break; + } + params.push_back( trim( paramString.substr( 0 , c ) ) ); + paramString = trim( paramString.substr( c + 1 ) ); + paramString = trim( paramString ); + } + + boost::scoped_array<const char *> paramArray (new const char*[params.size()]); + for ( size_t i=0; i<params.size(); i++ ) + paramArray[i] = params[i].c_str(); + + // avoid munging previously munged name (kludge; switching to v8 fixes underlying issue) + if ( fbase.find("__cf__") != 0 && fbase.find("__f__") == string::npos ) { + fbase = fname.str(); + } + + JSFunction * func = JS_CompileFunction( _context , assoc , fbase.c_str() , params.size() , paramArray.get() , code.c_str() , code.size() , "nofile_b" , 0 ); + + if ( ! func ) { + log() << "compile failed for: " << raw << endl; + return 0; + } + gcName = "cf normal"; + return func; + } + + jsval toval( double d ) { + jsval val; + verify( JS_NewNumberValue( _context, d , &val ) ); + return val; + } + + jsval toval( const char * c ) { + JSString * s = JS_NewStringCopyZ( _context , c ); + if ( s ) + return STRING_TO_JSVAL( s ); + + // possibly unicode, try manual + + size_t len = strlen( c ); + size_t dstlen = len * 4; + jschar * dst = (jschar*)malloc( dstlen ); + + JSBool res = JS_DecodeBytes( _context , c , len , dst, &dstlen ); + if ( res ) { + s = JS_NewUCStringCopyN( _context , dst , dstlen ); + } + + free( dst ); + + if ( ! res ) { + tlog() << "decode failed. probably invalid utf-8 string [" << c << "]" << endl; + jsval v; + if ( JS_GetPendingException( _context , &v ) ) + tlog() << "\t why: " << toString( v ) << endl; + uassert( 16270, "conversion from string to JavaScript value failed", false ); + } + + CHECKJSALLOC( s ); + return STRING_TO_JSVAL( s ); + } + + JSObject * toJSObject( const BSONObj * obj , bool readOnly=false ) { + static string ref = "$ref"; + if ( ref == obj->firstElementFieldName() ) { + JSObject * o = JS_NewObject( _context , &dbref_class , NULL, NULL); + CHECKNEWOBJECT(o,_context,"toJSObject1"); + verify( JS_SetPrivate( _context , o , (void*)(new BSONHolder( obj->getOwned() ) ) ) ); + return o; + } + JSObject * o = JS_NewObject( _context , readOnly ? &bson_ro_class : &bson_class , NULL, NULL); + CHECKNEWOBJECT(o,_context,"toJSObject2"); + verify( JS_SetPrivate( _context , o , (void*)(new BSONHolder( obj->getOwned() ) ) ) ); + return o; + } + + jsval toval( const BSONObj* obj , bool readOnly=false ) { + JSObject * o = toJSObject( obj , readOnly ); + return OBJECT_TO_JSVAL( o ); + } + + void makeLongObj( long long n, JSObject * o ) { + boost::uint64_t val = (boost::uint64_t)n; + CHECKNEWOBJECT(o,_context,"NumberLong1"); + double floatApprox = (double)(boost::int64_t)val; + setProperty( o , "floatApprox" , toval( floatApprox ) ); + if ( (boost::int64_t)val != (boost::int64_t)floatApprox ) { + // using 2 doubles here instead of a single double because certain double + // bit patterns represent undefined values and sm might trash them + setProperty( o , "top" , toval( (double)(boost::uint32_t)( val >> 32 ) ) ); + setProperty( o , "bottom" , toval( (double)(boost::uint32_t)( val & 0x00000000ffffffff ) ) ); + } + } + + jsval toval( long long n ) { + JSObject * o = JS_NewObject( _context , &numberlong_class , 0 , 0 ); + makeLongObj( n, o ); + return OBJECT_TO_JSVAL( o ); + } + + void makeIntObj( int n, JSObject * o ) { + boost::uint32_t val = (boost::uint32_t)n; + CHECKNEWOBJECT(o,_context,"NumberInt1"); + double floatApprox = (double)(boost::int32_t)val; + setProperty( o , "floatApprox" , toval( floatApprox ) ); + } + + jsval toval( int n ) { + JSObject * o = JS_NewObject( _context , &numberint_class , 0 , 0 ); + makeIntObj( n, o ); + return OBJECT_TO_JSVAL( o ); + } + + jsval toval( const BSONElement& e ) { + + switch( e.type() ) { + case EOO: + case jstNULL: + case Undefined: + return JSVAL_NULL; + case NumberDouble: + case NumberInt: + return toval( e.number() ); +// case NumberInt: +// return toval( e.numberInt() ); + case Symbol: // TODO: should we make a special class for this + case String: + return toval( e.valuestr() ); + case Bool: + return e.boolean() ? JSVAL_TRUE : JSVAL_FALSE; + case Object: { + BSONObj embed = e.embeddedObject().getOwned(); + return toval( &embed ); + } + case Array: { + + BSONObj embed = e.embeddedObject().getOwned(); + + if ( embed.isEmpty() ) { + return OBJECT_TO_JSVAL( JS_NewArrayObject( _context , 0 , 0 ) ); + } + + JSObject * array = JS_NewArrayObject( _context , 1 , 0 ); + CHECKJSALLOC( array ); + + jsval myarray = OBJECT_TO_JSVAL( array ); + + BSONObjIterator i( embed ); + while ( i.more() ){ + const BSONElement& e = i.next(); + jsval v = toval( e ); + verify( JS_SetElement( _context , array , atoi(e.fieldName()) , &v ) ); + } + + return myarray; + } + case jstOID: { + OID oid = e.__oid(); + JSObject * o = JS_NewObject( _context , &object_id_class , 0 , 0 ); + CHECKNEWOBJECT(o,_context,"jstOID"); + setProperty( o , "str" , toval( oid.str().c_str() ) ); + return OBJECT_TO_JSVAL( o ); + } + case RegEx: { + const char * flags = e.regexFlags(); + uintN flagNumber = 0; + while ( *flags ) { + switch ( *flags ) { + case 'g': flagNumber |= JSREG_GLOB; break; + case 'i': flagNumber |= JSREG_FOLD; break; + case 'm': flagNumber |= JSREG_MULTILINE; break; + //case 'y': flagNumber |= JSREG_STICKY; break; + + default: + log() << "warning: unknown regex flag:" << *flags << endl; + } + flags++; + } + + JSObject * r = JS_NewRegExpObject( _context , (char*)e.regex() , strlen( e.regex() ) , flagNumber ); + verify( r ); + return OBJECT_TO_JSVAL( r ); + } + case Code: { + JSFunction * func = compileFunction( e.valuestr() ); + if ( func ) + return OBJECT_TO_JSVAL( JS_GetFunctionObject( func ) ); + return JSVAL_NULL; + } + case CodeWScope: { + JSFunction * func = compileFunction( e.codeWScopeCode() ); + if ( !func ) + return JSVAL_NULL; + + BSONObj extraScope = e.codeWScopeObject(); + if ( ! extraScope.isEmpty() ) { + log() << "warning: CodeWScope doesn't transfer to db.eval" << endl; + } + + return OBJECT_TO_JSVAL( JS_GetFunctionObject( func ) ); + } + case Date: + return OBJECT_TO_JSVAL( js_NewDateObjectMsec( _context , (jsdouble) ((long long)e.date().millis) ) ); + + case MinKey: + return OBJECT_TO_JSVAL( JS_NewObject( _context , &minkey_class , 0 , 0 ) ); + + case MaxKey: + return OBJECT_TO_JSVAL( JS_NewObject( _context , &maxkey_class , 0 , 0 ) ); + + case Timestamp: { + JSObject * o = JS_NewObject( _context , ×tamp_class , 0 , 0 ); + CHECKNEWOBJECT(o,_context,"Timestamp1"); + setProperty( o , "t" , toval( (double)(e.timestampTime()) ) ); + setProperty( o , "i" , toval( (double)(e.timestampInc()) ) ); + return OBJECT_TO_JSVAL( o ); + } + case NumberLong: { + return toval( e.numberLong() ); + } + case DBRef: { + JSObject * o = JS_NewObject( _context , &dbpointer_class , 0 , 0 ); + CHECKNEWOBJECT(o,_context,"DBRef1"); + setProperty( o , "ns" , toval( e.dbrefNS() ) ); + + JSObject * oid = JS_NewObject( _context , &object_id_class , 0 , 0 ); + CHECKNEWOBJECT(oid,_context,"DBRef2"); + setProperty( oid , "str" , toval( e.dbrefOID().str().c_str() ) ); + + setProperty( o , "id" , OBJECT_TO_JSVAL( oid ) ); + return OBJECT_TO_JSVAL( o ); + } + case BinData: { + JSObject * o = JS_NewObject( _context , &bindata_class , 0 , 0 ); + CHECKNEWOBJECT(o,_context,"Bindata_BinData1"); + int len; + const char * data = e.binData( len ); + verify( data ); + verify( JS_SetPrivate( _context , o , new BinDataHolder( data , len ) ) ); + + setProperty( o , "len" , toval( (double)len ) ); + setProperty( o , "type" , toval( (double)e.binDataType() ) ); + return OBJECT_TO_JSVAL( o ); + } + } + + log() << "toval: unknown type: " << (int) e.type() << endl; + uassert( 10218 , "not done: toval" , 0 ); + return 0; + } + + // ------- object helpers ------ + + JSObject * getJSObject( JSObject * o , const char * name ) { + jsval v; + verify( JS_GetProperty( _context , o , name , &v ) ); + return JSVAL_TO_OBJECT( v ); + } + + JSObject * getGlobalObject( const char * name ) { + return getJSObject( JS_GetGlobalObject( _context ) , name ); + } + + JSObject * getGlobalPrototype( const char * name ) { + return getJSObject( getGlobalObject( name ) , "prototype" ); + } + + bool hasProperty( JSObject * o , const char * name ) { + JSBool res; + verify( JS_HasProperty( _context , o , name , & res ) ); + return res; + } + + jsval getProperty( JSObject * o , const char * field ) { + uassert( 10219 , "object passed to getPropery is null" , o ); + jsval v; + verify( JS_GetProperty( _context , o , field , &v ) ); + return v; + } + + void setProperty( JSObject * o , const char * field , jsval v ) { + verify( JS_SetProperty( _context , o , field , &v ) ); + } + + string typeString( jsval v ) { + JSType t = JS_TypeOfValue( _context , v ); + return JS_GetTypeName( _context , t ); + } + + bool getBoolean( JSObject * o , const char * field ) { + return toBoolean( getProperty( o , field ) ); + } + + double getNumber( JSObject * o , const char * field ) { + return toNumber( getProperty( o , field ) ); + } + + string getString( JSObject * o , const char * field ) { + return toString( getProperty( o , field ) ); + } + + JSClass * getClass( JSObject * o , const char * field ) { + jsval v; + verify( JS_GetProperty( _context , o , field , &v ) ); + if ( ! JSVAL_IS_OBJECT( v ) ) + return 0; + return JS_GET_CLASS( _context , JSVAL_TO_OBJECT( v ) ); + } + + JSContext * _context; + + + }; + + + void bson_finalize( JSContext * cx , JSObject * obj ) { + try { + BSONHolder * o = GETHOLDER( cx , obj ); + if ( o ) { + delete o; + verify( JS_SetPrivate( cx , obj , 0 ) ); + } + } + catch ( const AssertionException& e ) { + if ( ! JS_IsExceptionPending( cx ) ) JS_ReportError( cx, e.what() ); + } + catch ( const std::exception& e ) { + log() << "unhandled exception: " << e.what() << ", throwing Fatal Assertion" << endl; + fassertFailed( 16271 ); + } + } + + JSBool bson_enumerate( JSContext *cx, JSObject *obj, JSIterateOp enum_op, jsval *statep, jsid *idp ) { + try { + BSONHolder * o = GETHOLDER( cx , obj ); + + if ( enum_op == JSENUMERATE_INIT ) { + if ( o ) { + BSONFieldIterator * it = o->it(); + *statep = PRIVATE_TO_JSVAL( it ); + } + else { + *statep = 0; + } + if ( idp ) + *idp = JSVAL_ZERO; + return JS_TRUE; + } + + BSONFieldIterator * it = (BSONFieldIterator*)JSVAL_TO_PRIVATE( *statep ); + if ( ! it ) { + *statep = 0; + return JS_TRUE; + } + + if ( enum_op == JSENUMERATE_NEXT ) { + if ( it->more() ) { + string name = it->next(); + Convertor c(cx); + verify( JS_ValueToId( cx , c.toval( name.c_str() ) , idp ) ); + } + else { + delete it; + *statep = 0; + } + return JS_TRUE; + } + + if ( enum_op == JSENUMERATE_DESTROY ) { + if ( it ) + delete it; + return JS_TRUE; + } + + uassert( 10220 , "don't know what to do with this op" , 0 ); + return JS_FALSE; + } + catch ( const AssertionException& e ) { + if ( ! JS_IsExceptionPending( cx ) ) { + JS_ReportError( cx, e.what() ); + } + return JS_FALSE; + } + catch ( const std::exception& e ) { + log() << "unhandled exception: " << e.what() << ", throwing Fatal Assertion" << endl; + fassertFailed( 16272 ); + } + } + + JSBool noaccess( JSContext *cx, JSObject *obj, jsval idval, jsval *vp) { + try { + BSONHolder * holder = GETHOLDER( cx , obj ); + if ( ! holder ) { + // in init code still + return JS_TRUE; + } + if ( holder->_inResolve ) + return JS_TRUE; + JS_ReportError( cx , "doing write op on read only operation" ); + return JS_FALSE; + } + catch ( const AssertionException& e ) { + if ( ! JS_IsExceptionPending( cx ) ) { + JS_ReportError( cx, e.what() ); + } + return JS_FALSE; + } + catch ( const std::exception& e ) { + log() << "unhandled exception: " << e.what() << ", throwing Fatal Assertion" << endl; + fassertFailed( 16273 ); + } + } + + JSClass bson_ro_class = { + "bson_ro_object", // class name + JSCLASS_HAS_PRIVATE | JSCLASS_NEW_RESOLVE | JSCLASS_NEW_ENUMERATE, // flags + noaccess, // addProperty + noaccess, // delProperty + JS_PropertyStub, // getProperty + noaccess, // setProperty + (JSEnumerateOp)bson_enumerate, // enumerate + (JSResolveOp)resolveBSONField, // resolve + JS_ConvertStub, // convert + bson_finalize, // finalize + JSCLASS_NO_OPTIONAL_MEMBERS // optional members + }; + + JSBool bson_cons( JSContext *cx, JSObject *obj, uintN argc, jsval *argv, jsval *rval ) { + JS_ReportError( cx , "can't construct bson object" ); + return JS_FALSE; + } + + JSFunctionSpec bson_functions[] = { + { 0 } + }; + + JSBool bson_add_prop( JSContext *cx, JSObject *obj, jsval idval, jsval *vp) { + try { + BSONHolder * holder = GETHOLDER( cx , obj ); + if ( ! holder ) { + // static init + return JS_TRUE; + } + if ( ! holder->_inResolve ) { + Convertor c(cx); + string name( c.toString( idval ) ); + if ( holder->_obj[name].eoo() ) { + holder->_extra.push_back( name ); + } + holder->_modified = true; + } + } + catch ( const AssertionException& e ) { + if ( ! JS_IsExceptionPending( cx ) ) { + JS_ReportError( cx, e.what() ); + } + return JS_FALSE; + } + catch ( const std::exception& e ) { + log() << "unhandled exception: " << e.what() << ", throwing Fatal Assertion" << endl; + fassertFailed( 16274 ); + } + return JS_TRUE; + } + + + JSBool mark_modified( JSContext *cx, JSObject *obj, jsval idval, jsval *vp) { + try { + Convertor c(cx); + BSONHolder * holder = GETHOLDER( cx , obj ); + if ( !holder ) // needed when we're messing with DBRef.prototype + return JS_TRUE; + if ( holder->_inResolve ) + return JS_TRUE; + holder->_modified = true; + holder->_removed.erase( c.toString( idval ) ); + } + catch ( const AssertionException& e ) { + if ( ! JS_IsExceptionPending( cx ) ) { + JS_ReportError( cx, e.what() ); + } + return JS_FALSE; + } + catch ( const std::exception& e ) { + log() << "unhandled exception: " << e.what() << ", throwing Fatal Assertion" << endl; + fassertFailed( 16275 ); + } + return JS_TRUE; + } + + JSBool mark_modified_remove( JSContext *cx, JSObject *obj, jsval idval, jsval *vp) { + try { + Convertor c(cx); + BSONHolder * holder = GETHOLDER( cx , obj ); + if ( holder->_inResolve ) + return JS_TRUE; + holder->_modified = true; + holder->_removed.insert( c.toString( idval ) ); + } + catch ( const AssertionException& e ) { + if ( ! JS_IsExceptionPending( cx ) ) { + JS_ReportError( cx, e.what() ); + } + return JS_FALSE; + } + catch ( const std::exception& e ) { + log() << "unhandled exception: " << e.what() << ", throwing Fatal Assertion" << endl; + fassertFailed( 16276 ); + } + return JS_TRUE; + } + + JSClass bson_class = { + "bson_object", // class name + JSCLASS_HAS_PRIVATE | JSCLASS_NEW_RESOLVE | JSCLASS_NEW_ENUMERATE, // flags + bson_add_prop, // addProperty + mark_modified_remove, // delProperty + JS_PropertyStub, // getProperty + mark_modified, // setProperty + (JSEnumerateOp)bson_enumerate, // enumerate + (JSResolveOp)resolveBSONField, // resolve + JS_ConvertStub, // convert + bson_finalize, // finalize + JSCLASS_NO_OPTIONAL_MEMBERS // optional members + }; + + static JSClass global_class = { + "global", // class name + JSCLASS_GLOBAL_FLAGS, // flags + JS_PropertyStub, // addProperty + JS_PropertyStub, // delProperty + JS_PropertyStub, // getProperty + JS_PropertyStub, // setProperty + JS_EnumerateStub, // enumerate + JS_ResolveStub, // resolve + JS_ConvertStub, // convert + JS_FinalizeStub, // finalize + JSCLASS_NO_OPTIONAL_MEMBERS // optional members + }; + + // --- global helpers --- + + static void hexToBinData( JSContext* cx, + Convertor* c, + jsval* rval, + int subtype, + const string& s ) { + JSObject * o = JS_NewObject( cx , &bindata_class , 0 , 0 ); + CHECKNEWOBJECT(o,_context,"Bindata_BinData1"); + size_t s_size = s.size(); + int len = s_size / 2; + boost::scoped_array<char> data( new char[len] ); + char* p = data.get(); + const char *src = s.c_str(); + for( size_t i = 0; i+1 < s_size; i += 2 ) { + *p++ = fromHex( src + i ); + } + verify( JS_SetPrivate( cx, o, new BinDataHolder( data.get(), len ) ) ); + c->setProperty( o, "len", c->toval( static_cast<double>(len) ) ); + c->setProperty( o, "type", c->toval( static_cast<double>(subtype) ) ); + *rval = OBJECT_TO_JSVAL( o ); + } + + static bool testHexString( JSContext* cx, const string& hexString ) { + size_t len = hexString.length(); + for ( size_t i = 0; i < len; ++i ) { + char ch = hexString[i]; + if ( (ch>='0' && ch<='9') || (ch>='a' && ch<='f') || (ch>='A' && ch<='F') ) { + continue; + } + JS_ReportError( cx, "invalid hexstring character '%c'", ch ); + return false; + } + return true; + } + + JSBool _HexData( JSContext * cx , JSObject * obj , uintN argc, jsval *argv, jsval *rval ) { + try { + if ( argc != 2 ) { + JS_ReportError( cx , "HexData needs 2 arguments -- HexData(subtype,hexstring)" ); + return JS_FALSE; + } + Convertor c( cx ); + int subtype = static_cast<int>( c.toNumber( argv[ 0 ] ) ); + if ( subtype == 2 ) { + JS_ReportError( cx , "BinData subtype 2 is deprecated" ); + return JS_FALSE; + } + else if ( subtype < 0 || subtype > 255 ) { + JS_ReportError( cx, "subtype must be between 0 and 255" ); + return JS_FALSE; + } + string s( c.toString( argv[1] ) ); + if ( ! testHexString( cx, s ) ) { + return JS_FALSE; + } + size_t len = s.length(); + if ( 0 != ( len % 2 ) ) { + JS_ReportError( cx, "hexstring must be even length" ); + return JS_FALSE; + } + hexToBinData(cx, &c, rval, subtype, s); + } + catch ( const AssertionException& e ) { + if ( ! JS_IsExceptionPending( cx ) ) { + JS_ReportError( cx, e.what() ); + } + return JS_FALSE; + } + catch ( const std::exception& e ) { + log() << "unhandled exception: " << e.what() << ", throwing Fatal Assertion" << endl; + fassertFailed( 16277 ); + } + return JS_TRUE; + } + + JSBool _UUID( JSContext * cx , JSObject * obj , uintN argc, jsval *argv, jsval *rval ) { + try { + if ( argc != 1 ) { + JS_ReportError( cx , "UUID needs argument -- UUID(hexstring)" ); + return JS_FALSE; + } + Convertor c( cx ); + string s( c.toString( argv[0] ) ); + if ( ! testHexString( cx, s ) ) { + return JS_FALSE; + } + size_t len = s.length(); + if ( len != 32 ) { + JS_ReportError( cx, "UUID hexstring length is %d, must be 32", len ); + return JS_FALSE; + } + hexToBinData(cx, &c, rval, 3, s); + } + catch ( const AssertionException& e ) { + if ( ! JS_IsExceptionPending( cx ) ) { + JS_ReportError( cx, e.what() ); + } + return JS_FALSE; + } + catch ( const std::exception& e ) { + log() << "unhandled exception: " << e.what() << ", throwing Fatal Assertion" << endl; + fassertFailed( 16278 ); + } + return JS_TRUE; + } + + JSBool _MD5( JSContext * cx , JSObject * obj , uintN argc, jsval *argv, jsval *rval ) { + try { + if ( argc != 1 ) { + JS_ReportError( cx , "MD5 needs argument -- MD5(hexstring)" ); + return JS_FALSE; + } + Convertor c( cx ); + string s( c.toString( argv[0] ) ); + if ( ! testHexString( cx, s ) ) { + return JS_FALSE; + } + size_t len = s.length(); + if ( len != 32 ) { + JS_ReportError( cx, "MD5 hexstring length is %d, must be 32", len ); + return JS_FALSE; + } + hexToBinData(cx, &c, rval, 5, s); + } + catch ( const AssertionException& e ) { + if ( ! JS_IsExceptionPending( cx ) ) { + JS_ReportError( cx, e.what() ); + } + return JS_FALSE; + } + catch ( const std::exception& e ) { + log() << "unhandled exception: " << e.what() << ", throwing Fatal Assertion" << endl; + fassertFailed( 16279 ); + } + return JS_TRUE; + } + + JSBool native_print( JSContext * cx, JSObject * obj, uintN argc, jsval *argv, jsval *rval ) { + stringstream ss; + bool someWritten = false; + try { + Convertor c( cx ); + for ( uintN i=0; i<argc; i++ ) { + if ( i > 0 ) + ss << " "; + ss << c.toString( argv[i] ); + someWritten = true; + } + ss << "\n"; + Logstream::logLockless( ss.str() ); + } + catch ( const AssertionException& ) { + if ( someWritten ) { + ss << "\n"; + Logstream::logLockless( ss.str() ); + } + return JS_FALSE; + } + catch ( const std::exception& e ) { + log() << "unhandled exception: " << e.what() << ", throwing Fatal Assertion" << endl; + fassertFailed( 16280 ); + } + return JS_TRUE; + } + + JSBool native_helper( JSContext *cx , JSObject *obj , uintN argc, jsval *argv , jsval *rval ) { + try { + Convertor c(cx); + NativeFunction func = reinterpret_cast<NativeFunction>( + static_cast<long long>( c.getNumber( obj , "x" ) ) ); + void* data = reinterpret_cast<void*>( + static_cast<long long>( c.getNumber( obj , "y" ) ) ); + verify( func ); + + BSONObj a; + if ( argc > 0 ) { + BSONObjBuilder args; + for ( uintN i = 0; i < argc; ++i ) { + c.append( args , args.numStr( i ) , argv[i] ); + } + a = args.obj(); + } + + BSONObj out; + try { + out = func( a, data ); + } + catch ( std::exception& e ) { + if ( ! JS_IsExceptionPending( cx ) ) { + JS_ReportError( cx, e.what() ); + } + return JS_FALSE; + } + + if ( out.isEmpty() ) { + *rval = JSVAL_VOID; + } + else { + *rval = c.toval( out.firstElement() ); + } + } + catch ( const AssertionException& e ) { + if ( ! JS_IsExceptionPending( cx ) ) { + JS_ReportError( cx, e.what() ); + } + return JS_FALSE; + } + catch ( const std::exception& e ) { + log() << "unhandled exception: " << e.what() << ", throwing Fatal Assertion" << endl; + fassertFailed( 16281 ); + } + return JS_TRUE; + } + + JSBool native_load( JSContext *cx , JSObject *obj , uintN argc, jsval *argv , jsval *rval ); + + JSBool native_gc( JSContext *cx , JSObject *obj , uintN argc, jsval *argv , jsval *rval ) { + JS_GC( cx ); + return JS_TRUE; + } + + JSFunctionSpec globalHelpers[] = { + { "print" , &native_print , 0 , 0 , 0 } , + { "nativeHelper" , &native_helper , 1 , 0 , 0 } , + { "load" , &native_load , 1 , 0 , 0 } , + { "gc" , &native_gc , 1 , 0 , 0 } , + { "UUID", &_UUID, 0, 0, 0 } , + { "MD5", &_MD5, 0, 0, 0 } , + { "HexData", &_HexData, 0, 0, 0 } , + { 0 , 0 , 0 , 0 , 0 } + }; + + // ----END global helpers ---- + + // Object helpers + + JSBool bson_get_size(JSContext *cx, JSObject *obj, uintN argc, jsval *argv, jsval *rval) { + try { + if ( argc != 1 || !JSVAL_IS_OBJECT( argv[ 0 ] ) ) { + JS_ReportError( cx , "bsonsize requires one valid object" ); + return JS_FALSE; + } + Convertor c(cx); + if ( argv[0] == JSVAL_VOID || argv[0] == JSVAL_NULL ) { + *rval = c.toval( 0.0 ); + return JS_TRUE; + } + + JSObject * o = JSVAL_TO_OBJECT( argv[0] ); + double size = 0.0; + + if ( JS_InstanceOf( cx , o , &bson_ro_class , 0 ) || + JS_InstanceOf( cx , o , &bson_class , 0 ) ) { + BSONHolder * h = GETHOLDER( cx , o ); + if ( h ) { + size = h->_obj.objsize(); + } + } + else { + BSONObj temp = c.toObject( o ); + size = temp.objsize(); + } + *rval = c.toval( size ); + } + catch ( const AssertionException& e ) { + if ( ! JS_IsExceptionPending( cx ) ) { + JS_ReportError( cx, e.what() ); + } + return JS_FALSE; + } + catch ( const std::exception& e ) { + log() << "unhandled exception: " << e.what() << ", throwing Fatal Assertion" << endl; + fassertFailed( 16282 ); + } + return JS_TRUE; + } + + JSFunctionSpec objectHelpers[] = { + { "bsonsize" , &bson_get_size , 1 , 0 , 0 } , + { 0 , 0 , 0 , 0 , 0 } + }; + + // end Object helpers + + JSBool resolveBSONField( JSContext *cx, JSObject *obj, jsval id, uintN flags, JSObject **objp ) { + try { + verify( JS_EnterLocalRootScope( cx ) ); + } + catch ( const AssertionException& e ) { + JS_ReportError( cx, e.what() ); + return JS_FALSE; + } + catch ( const std::exception& e ) { + log() << "unhandled exception: " << e.what() << ", throwing Fatal Assertion" << endl; + fassertFailed( 16283 ); + } + + try { + BSONHolder * holder = GETHOLDER( cx , obj ); + if ( ! holder ) { + // static init + *objp = 0; + JS_LeaveLocalRootScope( cx ); + return JS_TRUE; + } + holder->check(); + + Convertor c( cx ); + string s( c.toString( id ) ); + BSONElement e = holder->_obj[ s.c_str() ]; + if ( e.type() == EOO || holder->_removed.count( s ) ) { + *objp = 0; + JS_LeaveLocalRootScope( cx ); + return JS_TRUE; + } + + jsval val = c.toval( e ); + + verify( ! holder->_inResolve ); + holder->_inResolve = true; + verify( JS_SetProperty( cx , obj , s.c_str() , &val ) ); + holder->_inResolve = false; + + if ( val != JSVAL_NULL && val != JSVAL_VOID && JSVAL_IS_OBJECT( val ) ) { + // TODO: this is a hack to get around sub objects being modified + // basically right now whenever a sub object is read we mark whole obj as possibly modified + JSObject * oo = JSVAL_TO_OBJECT( val ); + if ( JS_InstanceOf( cx , oo , &bson_class , 0 ) || + JS_IsArrayObject( cx , oo ) ) { + holder->_modified = true; + } + } + + *objp = obj; + JS_LeaveLocalRootScope( cx ); + } + catch ( const AssertionException& e ) { + JS_LeaveLocalRootScope( cx ); + if ( ! JS_IsExceptionPending( cx ) ) { + JS_ReportError( cx, e.what() ); + } + return JS_FALSE; + } + catch ( const std::exception& e ) { + log() << "unhandled exception: " << e.what() << ", throwing Fatal Assertion" << endl; + fassertFailed( 16284 ); + } + return JS_TRUE; + } + + + class SMScope; + + class SMEngine : public ScriptEngine { + public: + + SMEngine() { +#ifdef SM18 + JS_SetCStringsAreUTF8(); +#endif + + _runtime = JS_NewRuntime(64L * 1024L * 1024L); + uassert( 10221 , "JS_NewRuntime failed" , _runtime ); + + if ( ! utf8Ok() ) { + log() << "*** warning: spider monkey build without utf8 support. consider rebuilding with utf8 support" << endl; + } + } + + ~SMEngine() { + JS_DestroyRuntime( _runtime ); + JS_ShutDown(); + } + + Scope * createScope(); + + void runTest(); + + virtual bool utf8Ok() const { return JS_CStringsAreUTF8(); } + +#ifdef XULRUNNER + JSClass * _dateClass; + JSClass * _regexClass; +#endif + + + private: + JSRuntime * _runtime; + friend class SMScope; + }; + + SMEngine * globalSMEngine; + + + void ScriptEngine::setup() { + globalSMEngine = new SMEngine(); + globalScriptEngine = globalSMEngine; + } + + + // ------ scope ------ + + + JSBool no_gc(JSContext *cx, JSGCStatus status) { + return JS_FALSE; + } + + JSBool yes_gc(JSContext *cx, JSGCStatus status) { + return JS_TRUE; + } + + class SMScope : public Scope { + public: + SMScope() : + _this( 0 ), + _reportError( true ), + _externalSetup( false ), + _localConnect( false ) { + smlock; + _context = JS_NewContext( globalSMEngine->_runtime , 8192 ); + _convertor = new Convertor( _context ); + massert( 10431 , "JS_NewContext failed" , _context ); + + JS_SetOptions( _context , JSOPTION_VAROBJFIX); + //JS_SetVersion( _context , JSVERSION_LATEST); TODO + JS_SetErrorReporter( _context , errorReporter ); + + _global = JS_NewObject( _context , &global_class, NULL, NULL); + massert( 10432 , "JS_NewObject failed for global" , _global ); + JS_SetGlobalObject( _context , _global ); + massert( 10433 , "js init failed" , JS_InitStandardClasses( _context , _global ) ); + + JS_SetOptions( _context , JS_GetOptions( _context ) | JSOPTION_VAROBJFIX ); + + JS_DefineFunctions( _context , _global , globalHelpers ); + + JS_DefineFunctions( _context , _convertor->getGlobalObject( "Object" ), objectHelpers ); + + //JS_SetGCCallback( _context , no_gc ); // this is useful for seeing if something is a gc problem + + _postCreateHacks(); + } + + ~SMScope() { + smlock; + uassert( 10223 , "deleted SMScope twice?" , _convertor ); + + if ( _this ) { + JS_RemoveRoot( _context , &_this ); + _this = 0; + } + + if ( _convertor ) { + delete _convertor; + _convertor = 0; + } + + if ( _context ) { + // This is expected to reclaim _global as well. + JS_DestroyContext( _context ); + _context = 0; + } + + } + + void reset() { + smlock; + verify( _convertor ); + } + + void init( const BSONObj * data ) { + smlock; + if ( ! data ) + return; + + BSONObjIterator i( *data ); + while ( i.more() ) { + BSONElement e = i.next(); + _convertor->setProperty( _global , e.fieldName() , _convertor->toval( e ) ); + _initFieldNames.insert( e.fieldName() ); + } + + } + + bool hasOutOfMemoryException() { + string err = getError(); + return err.find("out of memory") != string::npos; + } + + void externalSetup() { + smlock; + uassert( 10224 , "already local connected" , ! _localConnect ); + if ( _externalSetup ) + return; + initMongoJS( this , _context , _global , false ); + _externalSetup = true; + } + + void localConnect( const char * dbName ) { + { + smlock; + uassert( 10225 , "already setup for external db" , ! _externalSetup ); + if ( _localConnect ) { + uassert( 10226 , "connected to different db" , _localDBName == dbName ); + return; + } + + initMongoJS( this , _context , _global , true ); + + exec( "_mongo = new Mongo();" ); + exec( ((string)"db = _mongo.getDB( \"" + dbName + "\" ); ").c_str() ); + + _localConnect = true; + _localDBName = dbName; + } + loadStored(); + } + + // ----- getters ------ + double getNumber( const char *field ) { + smlock; + jsval val; + verify( JS_GetProperty( _context , _global , field , &val ) ); + return _convertor->toNumber( val ); + } + + string getString( const char *field ) { + smlock; + jsval val; + verify( JS_GetProperty( _context , _global , field , &val ) ); + JSString * s = JS_ValueToString( _context , val ); + return _convertor->toString( s ); + } + + bool getBoolean( const char *field ) { + smlock; + return _convertor->getBoolean( _global , field ); + } + + BSONObj getObject( const char *field ) { + smlock; + return _convertor->toObject( _convertor->getProperty( _global , field ) ); + } + + JSObject * getJSObject( const char * field ) { + smlock; + return _convertor->getJSObject( _global , field ); + } + + int type( const char *field ) { + smlock; + jsval val; + verify( JS_GetProperty( _context , _global , field , &val ) ); + + switch ( JS_TypeOfValue( _context , val ) ) { + case JSTYPE_VOID: return Undefined; + case JSTYPE_NULL: return jstNULL; + case JSTYPE_OBJECT: { + if ( val == JSVAL_NULL ) + return jstNULL; + JSObject * o = JSVAL_TO_OBJECT( val ); + if ( JS_IsArrayObject( _context , o ) ) + return Array; + if ( isDate( _context , o ) ) + return Date; + return Object; + } + case JSTYPE_FUNCTION: return Code; + case JSTYPE_STRING: return String; + case JSTYPE_NUMBER: return NumberDouble; + case JSTYPE_BOOLEAN: return Bool; + default: + uassert( 10227 , "unknown type" , 0 ); + } + return 0; + } + + // ----- setters ------ + + void setElement( const char *field , const BSONElement& val ) { + smlock; + jsval v = _convertor->toval( val ); + verify( JS_SetProperty( _context , _global , field , &v ) ); + } + + void setNumber( const char *field , double val ) { + smlock; + jsval v = _convertor->toval( val ); + verify( JS_SetProperty( _context , _global , field , &v ) ); + } + + void setString( const char *field , const char * val ) { + smlock; + jsval v = _convertor->toval( val ); + verify( JS_SetProperty( _context , _global , field , &v ) ); + } + + void setObject( const char *field , const BSONObj& obj , bool readOnly ) { + smlock; + jsval v = _convertor->toval( &obj , readOnly ); + JS_SetProperty( _context , _global , field , &v ); + } + + void setBoolean( const char *field , bool val ) { + smlock; + jsval v = BOOLEAN_TO_JSVAL( val ); + verify( JS_SetProperty( _context , _global , field , &v ) ); + } + + void setThis( const BSONObj * obj ) { + smlock; + if ( _this ) { + JS_RemoveRoot( _context , &_this ); + _this = 0; + } + + if ( obj ) { + _this = _convertor->toJSObject( obj ); + JS_AddNamedRoot( _context , &_this , "scope this" ); + } + } + + void setFunction( const char *field , const char * code ) { + smlock; + jsval v = OBJECT_TO_JSVAL(JS_GetFunctionObject(_convertor->compileFunction(code))); + JS_SetProperty( _context , _global , field , &v ); + } + + void rename( const char * from , const char * to ) { + smlock; + jsval v; + verify( JS_GetProperty( _context , _global , from , &v ) ); + verify( JS_SetProperty( _context , _global , to , &v ) ); + v = JSVAL_VOID; + verify( JS_SetProperty( _context , _global , from , &v ) ); + } + + // ---- functions ----- + + ScriptingFunction _createFunction( const char * code ) { + smlock; + precall(); + return (ScriptingFunction)_convertor->compileFunction( code ); + } + + struct TimeoutSpec { + boost::posix_time::ptime start; + boost::posix_time::time_duration timeout; + int count; + }; + + // should not generate exceptions, as those can be caught in + // javascript code; returning false without an exception exits + // immediately + static JSBool _interrupt( JSContext *cx ) { + TimeoutSpec &spec = *(TimeoutSpec *)( JS_GetContextPrivate( cx ) ); + if ( ++spec.count % 1000 != 0 ) + return JS_TRUE; + const char * interrupt = ScriptEngine::checkInterrupt(); + if ( interrupt && interrupt[ 0 ] ) { + return JS_FALSE; + } + if ( spec.timeout.ticks() == 0 ) { + return JS_TRUE; + } + boost::posix_time::time_duration elapsed = ( boost::posix_time::microsec_clock::local_time() - spec.start ); + if ( elapsed < spec.timeout ) { + return JS_TRUE; + } + return JS_FALSE; + + } + + static JSBool interrupt( JSContext *cx, JSScript *script ) { + return _interrupt( cx ); + } + + void installInterrupt( int timeoutMs ) { + if ( timeoutMs != 0 || ScriptEngine::haveCheckInterruptCallback() ) { + TimeoutSpec *spec = new TimeoutSpec; + spec->timeout = boost::posix_time::millisec( timeoutMs ); + spec->start = boost::posix_time::microsec_clock::local_time(); + spec->count = 0; + JS_SetContextPrivate( _context, (void*)spec ); +#if defined(SM181) && !defined(XULRUNNER190) + JS_SetOperationCallback( _context, _interrupt ); +#else + JS_SetBranchCallback( _context, interrupt ); +#endif + } + } + + void uninstallInterrupt( int timeoutMs ) { + if ( timeoutMs != 0 || ScriptEngine::haveCheckInterruptCallback() ) { +#if defined(SM181) && !defined(XULRUNNER190) + JS_SetOperationCallback( _context , 0 ); +#else + JS_SetBranchCallback( _context, 0 ); +#endif + delete (TimeoutSpec *)JS_GetContextPrivate( _context ); + JS_SetContextPrivate( _context, 0 ); + } + } + + void precall() { + _error = ""; + currentScope.reset( this ); + } + + bool isReportingErrors() const { return _reportError; } + + bool exec( const StringData& code, + const string& name = "(anon)", + bool printResult = false, + bool reportError = true, + bool assertOnError = true, + int timeoutMs = 0 ) { + smlock; + precall(); + + jsval ret = JSVAL_VOID; + + installInterrupt( timeoutMs ); + _reportError = reportError; + JSBool worked = JS_EvaluateScript( _context, + _global, + code.data(), + code.size(), + name.c_str(), + 1, + &ret ); + _reportError = true; + uninstallInterrupt( timeoutMs ); + + if ( ! worked && _error.size() == 0 ) { + jsval v; + if ( JS_GetPendingException( _context , &v ) ) { + _error = _convertor->toString( v ); + if ( reportError ) + cout << _error << endl; + } + } + + uassert( 10228, mongoutils::str::stream() << name + " exec failed: " << _error, + worked || ! assertOnError ); + + if ( worked ) + _convertor->setProperty( _global , "__lastres__" , ret ); + + if ( worked && printResult && ! JSVAL_IS_VOID( ret ) ) + cout << _convertor->toString( ret ) << endl; + + return worked; + } + + int invoke( JSFunction* func, + const BSONObj* args, + const BSONObj* recv, + int timeoutMs, + bool ignoreReturn, + bool readOnlyArgs, + bool readOnlyRecv ) { + smlock; + precall(); + + try { + verify( JS_EnterLocalRootScope( _context ) ); + } + catch ( const AssertionException& e ) { + if ( ! JS_IsExceptionPending( _context ) ) { + JS_ReportError( _context, e.what() ); + } + return 0; + } + catch ( const std::exception& e ) { + log() << "unhandled exception: " << e.what() << ", throwing Fatal Assertion" << endl; + fassertFailed( 16285 ); + } + + int nargs = args ? args->nFields() : 0; + scoped_array<jsval> smargsPtr( new jsval[nargs] ); + try { + if ( nargs ) { + BSONObjIterator it( *args ); + for ( int i=0; i<nargs; i++ ) { + smargsPtr[i] = _convertor->toval( it.next() ); + } + } + + if ( !args ) { + _convertor->setProperty( _global , "args" , JSVAL_NULL ); + } + else { + setObject( "args" , *args , true ); // this is for backwards compatability + } + } + catch ( const AssertionException& e ) { + JS_LeaveLocalRootScope( _context ); + if ( ! JS_IsExceptionPending( _context ) ) { + JS_ReportError( _context, e.what() ); + } + return 0; + } + catch ( const std::exception& e ) { + log() << "unhandled exception: " << e.what() << ", throwing Fatal Assertion" << endl; + fassertFailed( 16286 ); + } + JS_LeaveLocalRootScope( _context ); + + installInterrupt( timeoutMs ); + jsval rval; + setThis(recv); + JSBool ret = JS_CallFunction( _context, + _this ? _this : _global, + func, + nargs, + smargsPtr.get(), + &rval ); + setThis(0); + uninstallInterrupt( timeoutMs ); + + if ( !ret ) { + return -3; + } + + if ( ! ignoreReturn ) { + verify( JS_SetProperty( _context , _global , "return" , &rval ) ); + } + + return 0; + } + + int invoke( ScriptingFunction funcAddr, + const BSONObj* args, + const BSONObj* recv, + int timeoutMs = 0, + bool ignoreReturn = 0, + bool readOnlyArgs = false, + bool readOnlyRecv = false ) { + return invoke( reinterpret_cast<JSFunction*>( funcAddr ), + args, + recv, + timeoutMs, + ignoreReturn, + readOnlyArgs, + readOnlyRecv ); + } + + void gotError( string s ) { + _error = s; + } + + string getError() { + return _error; + } + + void injectNative( const char *field, NativeFunction func, void* data ) { + smlock; + string name = field; + jsval v; + v = _convertor->toval( static_cast<double>( reinterpret_cast<long long>(func) ) ); + _convertor->setProperty( _global, (name + "_").c_str(), v ); + + stringstream code; + if (data) { + v = _convertor->toval( static_cast<double>( reinterpret_cast<long long>(data) ) ); + _convertor->setProperty( _global, (name + "_data_").c_str(), v ); + code << field << "_" << " = { x : " << field << "_ , y: " << field << "_data_ }; "; + } else { + code << field << "_" << " = { x : " << field << "_ }; "; + } + code << field << " = function(){ return nativeHelper.apply( " << + field << "_ , arguments ); }"; + exec( code.str() ); + } + + virtual void gc() { + smlock; + JS_GC( _context ); + } + + JSContext *SavedContext() const { return _context; } + + private: + + void _postCreateHacks() { +#ifdef XULRUNNER + exec( "__x__ = new Date(1);" ); + globalSMEngine->_dateClass = _convertor->getClass( _global , "__x__" ); + exec( "__x__ = /abc/i" ); + globalSMEngine->_regexClass = _convertor->getClass( _global , "__x__" ); +#endif + } + + JSContext * _context; + Convertor * _convertor; + + JSObject * _global; + JSObject * _this; + + string _error; + bool _reportError; + + bool _externalSetup; + bool _localConnect; + + set<string> _initFieldNames; + + }; + + /* used to make the logging not overly chatty in the mongo shell. */ + extern bool isShell; + + void errorReporter( JSContext *cx, const char *message, JSErrorReport *report ) { + stringstream ss; + if( !isShell ) + ss << "JS Error: "; + ss << message; + + if ( report && report->filename ) { + ss << " " << report->filename << ":" << report->lineno; + } + + if ( !currentScope.get() || currentScope->isReportingErrors() ) { + tlog() << ss.str() << endl; + } + + if ( currentScope.get() ) { + currentScope->gotError( ss.str() ); + } + } + + JSBool native_load( JSContext *cx , JSObject *obj , uintN argc, jsval *argv , jsval *rval ) { + try { + Convertor c(cx); + + Scope * s = currentScope.get(); + + for ( uintN i=0; i<argc; i++ ) { + string filename( c.toString( argv[i] ) ); + //cout << "load [" << filename << "]" << endl; + + if ( ! s->execFile( filename, false, true, false ) ) { + JS_ReportError( cx, (string("error loading js file: ") + filename).c_str() ); + return JS_FALSE; + } + } + } + catch ( const AssertionException& e ) { + if ( ! JS_IsExceptionPending( cx ) ) { + JS_ReportError( cx, e.what() ); + } + return JS_FALSE; + } + catch ( const std::exception& e ) { + log() << "unhandled exception: " << e.what() << ", throwing Fatal Assertion" << endl; + fassertFailed( 16287 ); + } + return JS_TRUE; + } + + + + void SMEngine::runTest() { + SMScope s; + + s.localConnect( "foo" ); + + s.exec( "verify( db.getMongo() )" ); + s.exec( "verify( db.bar , 'collection getting does not work' ); " ); + s.exec( "assert.eq( db._name , 'foo' );" ); + s.exec( "verify( _mongo == db.getMongo() ); " ); + s.exec( "verify( _mongo == db._mongo ); " ); + s.exec( "verify( typeof DB.bar == 'undefined' ); " ); + s.exec( "verify( typeof DB.prototype.bar == 'undefined' , 'resolution is happening on prototype, not object' ); " ); + + s.exec( "verify( db.bar ); " ); + s.exec( "verify( typeof db.addUser == 'function' )" ); + s.exec( "verify( db.addUser == DB.prototype.addUser )" ); + s.exec( "assert.eq( 'foo.bar' , db.bar._fullName ); " ); + s.exec( "db.bar.verify();" ); + + s.exec( "db.bar.silly.verify();" ); + s.exec( "assert.eq( 'foo.bar.silly' , db.bar.silly._fullName )" ); + s.exec( "assert.eq( 'function' , typeof _mongo.find , 'mongo.find is not a function' )" ); + + verify( (string)"abc" == trim( "abc" ) ); + verify( (string)"abc" == trim( " abc" ) ); + verify( (string)"abc" == trim( "abc " ) ); + verify( (string)"abc" == trim( " abc " ) ); + + } + + Scope * SMEngine::createScope() { + return new SMScope(); + } + +} +#include "sm_db.cpp" diff --git a/src/mongo/scripting/engine_spidermonkey.h b/src/mongo/scripting/engine_spidermonkey.h new file mode 100644 index 00000000000..37dcda30975 --- /dev/null +++ b/src/mongo/scripting/engine_spidermonkey.h @@ -0,0 +1,101 @@ +// engine_spidermonkey.h + +/* Copyright 2009 10gen Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include "mongo/scripting/engine.h" + +// START inc hacking + +#ifdef WIN32 +#include <third_party/js-1.7/jstypes.h> +#undef JS_PUBLIC_API +#undef JS_PUBLIC_DATA +#define JS_PUBLIC_API(t) t __cdecl +#define JS_PUBLIC_DATA(t) t +#endif + +#include <third_party/js-1.7/jsapi.h> + +// END inc hacking + +// -- SM 1.6 hacks --- +#ifndef JSCLASS_GLOBAL_FLAGS +#error old version of spider monkey ( probably 1.6 ) you should upgrade to at least 1.7 +#endif +// -- END SM 1.6 hacks --- + +#ifdef JSVAL_IS_TRACEABLE +#define SM18 +#endif + +#ifdef XULRUNNER +#define SM181 +#endif + +namespace mongo { + + class SMScope; + class Convertor; + + extern JSClass bson_class; + extern JSClass bson_ro_class; + + extern JSClass object_id_class; + extern JSClass dbpointer_class; + extern JSClass dbref_class; + extern JSClass bindata_class; + extern JSClass timestamp_class; + extern JSClass numberlong_class; + extern JSClass numberint_class; + extern JSClass minkey_class; + extern JSClass maxkey_class; + + // internal things + void dontDeleteScope( SMScope * s ) {} + void errorReporter( JSContext *cx, const char *message, JSErrorReport *report ); + extern boost::thread_specific_ptr<SMScope> currentScope; + + // bson + JSBool resolveBSONField( JSContext *cx, JSObject *obj, jsval id, uintN flags, JSObject **objp ); + + // mongo + void initMongoJS( SMScope * scope , JSContext * cx , JSObject * global , bool local ); + bool appendSpecialDBObject( Convertor * c , BSONObjBuilder& b , const string& name , jsval val , JSObject * o ); + +#define JSVAL_IS_OID(v) ( JSVAL_IS_OBJECT( v ) && JS_InstanceOf( cx , JSVAL_TO_OBJECT( v ) , &object_id_class , 0 ) ) + + bool isDate( JSContext * cx , JSObject * o ); + + // JS private data must be 2byte aligned, so we use a holder to refer to an unaligned pointer. + struct BinDataHolder { + BinDataHolder( const char *c, int copyLen = -1 ) : + c_( const_cast< char * >( c ) ), + iFree_( copyLen != -1 ) { + if ( copyLen != -1 ) { + c_ = (char*)malloc( copyLen ); + memcpy( c_, c, copyLen ); + } + } + ~BinDataHolder() { + if ( iFree_ ) + free( c_ ); + } + char *c_; + bool iFree_; + }; +} diff --git a/src/mongo/scripting/engine_v8.cpp b/src/mongo/scripting/engine_v8.cpp new file mode 100644 index 00000000000..15d0fba2f29 --- /dev/null +++ b/src/mongo/scripting/engine_v8.cpp @@ -0,0 +1,1706 @@ +//engine_v8.cpp + +/* Copyright 2009 10gen Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "engine_v8.h" + +#include "v8_wrapper.h" +#include "v8_utils.h" +#include "v8_db.h" + +#define V8_SIMPLE_HEADER v8::Locker l(_isolate); v8::Isolate::Scope iscope(_isolate); HandleScope handle_scope; Context::Scope context_scope( _context ); + +namespace mongo { + + // guarded by v8 mutex + map< unsigned, int > __interruptSpecToThreadId; + map< unsigned, v8::Isolate* > __interruptSpecToIsolate; + + /** + * Unwraps a BSONObj from the JS wrapper + */ + static BSONObj unwrapBSONObj(const Handle<v8::Object>& obj) { + Handle<External> field = Handle<External>::Cast(obj->GetInternalField(0)); + if (field.IsEmpty() || !field->IsExternal()) { + return BSONObj(); + } + void* ptr = field->Value(); + return ((BSONHolder*)ptr)->_obj; + } + + static BSONHolder* unwrapHolder(const Handle<v8::Object>& obj) { + Handle<External> field = Handle<External>::Cast(obj->GetInternalField(0)); + if (field.IsEmpty() || !field->IsExternal()) + return 0; + void* ptr = field->Value(); + return (BSONHolder*)ptr; + } + + static void weakRefBSONCallback(v8::Persistent<v8::Value> p, void* scope) { + // should we lock here? no idea, and no doc from v8 of course + HandleScope handle_scope; + if (!p.IsNearDeath()) + return; + Handle<External> field = Handle<External>::Cast(p->ToObject()->GetInternalField(0)); + BSONHolder* data = (BSONHolder*) field->Value(); + delete data; + p.Dispose(); + } + + Persistent<v8::Object> V8Scope::wrapBSONObject(Local<v8::Object> obj, BSONHolder* data) { + obj->SetInternalField(0, v8::External::New(data)); + Persistent<v8::Object> p = Persistent<v8::Object>::New(obj); + p.MakeWeak(this, weakRefBSONCallback); + return p; + } + + static void weakRefArrayCallback(v8::Persistent<v8::Value> p, void* scope) { + // should we lock here? no idea, and no doc from v8 of course + HandleScope handle_scope; + if (!p.IsNearDeath()) + return; + Handle<External> field = Handle<External>::Cast(p->ToObject()->GetInternalField(0)); + char* data = (char*) field->Value(); + delete [] data; + p.Dispose(); + } + + Persistent<v8::Object> V8Scope::wrapArrayObject(Local<v8::Object> obj, char* data) { + obj->SetInternalField(0, v8::External::New(data)); + Persistent<v8::Object> p = Persistent<v8::Object>::New(obj); + p.MakeWeak(this, weakRefArrayCallback); + return p; + } + + static Handle<v8::Value> namedGet(Local<v8::String> name, const v8::AccessorInfo &info) { + if ( info.This()->HasRealNamedProperty( name ) ) { + // value already cached + return info.This()->GetRealNamedProperty(name); + } + + string key = toSTLString(name); + BSONHolder* holder = unwrapHolder(info.Holder()); + if ( holder->_removed.count( key ) ) + return Handle<Value>(); + + BSONObj obj = holder->_obj; + BSONElement elmt = obj.getField(key.c_str()); + if (elmt.eoo()) + return Handle<Value>(); + Local< External > scp = External::Cast( *info.Data() ); + V8Scope* scope = (V8Scope*)(scp->Value()); + Handle<v8::Value> val = scope->mongoToV8Element(elmt, false); + info.This()->ForceSet(name, val, DontEnum); + + if (elmt.type() == mongo::Object || elmt.type() == mongo::Array) { + // if accessing a subobject, it may get modified and base obj would not know + // have to set base as modified, which means some optim is lost + unwrapHolder(info.Holder())->_modified = true; + } + return val; + } + + static Handle<v8::Value> namedGetRO(Local<v8::String> name, const v8::AccessorInfo &info) { + string key = toSTLString(name); + BSONObj obj = unwrapBSONObj(info.Holder()); + BSONElement elmt = obj.getField(key.c_str()); + if (elmt.eoo()) + return Handle<Value>(); + Local< External > scp = External::Cast( *info.Data() ); + V8Scope* scope = (V8Scope*)(scp->Value()); + Handle<v8::Value> val = scope->mongoToV8Element(elmt, true); + return val; + } + + static Handle<v8::Value> namedSet(Local<v8::String> name, Local<v8::Value> value_obj, const v8::AccessorInfo& info) { + string key = toSTLString( name ); + BSONHolder* holder = unwrapHolder(info.Holder()); + holder->_removed.erase( key ); + holder->_extra.push_back( key ); + holder->_modified = true; + + // set into JS object + return Handle<Value>(); + } + + static Handle<v8::Array> namedEnumerator(const AccessorInfo &info) { + BSONHolder* holder = unwrapHolder(info.Holder()); + BSONObj obj = holder->_obj; + Handle<v8::Array> arr = Handle<v8::Array>(v8::Array::New(obj.nFields())); + int i = 0; + Local< External > scp = External::Cast( *info.Data() ); + V8Scope* scope = (V8Scope*)(scp->Value()); + + set<string> added; + // note here that if keys are parseable number, v8 will access them using index + for ( BSONObjIterator it(obj); it.more(); ++i) { + const BSONElement& f = it.next(); +// arr->Set(i, v8::String::NewExternal(new ExternalString(f.fieldName()))); + string sname = f.fieldName(); + if ( holder->_removed.count( sname ) ) + continue; + + Handle<v8::String> name = scope->getV8Str( sname ); + added.insert( sname ); + arr->Set(i, name); + } + + for ( list<string>::iterator it = holder->_extra.begin(); it != holder->_extra.end(); it++ ) { + string sname = *it; + if ( added.count( sname ) ) + continue; + arr->Set(i++, scope->getV8Str( sname )); + } + return arr; + } + + Handle<Boolean> namedDelete( Local<v8::String> name, const AccessorInfo& info ) { + string key = toSTLString( name ); + BSONHolder* holder = unwrapHolder(info.Holder()); + holder->_removed.insert( key ); + holder->_extra.remove( key ); + holder->_modified = true; + + // also delete in JS obj + return Handle<Boolean>(); + } + +// v8::Handle<v8::Integer> namedQuery(Local<v8::String> property, const AccessorInfo& info) { +// string key = ToString(property); +// return v8::Integer::New(None); +// } + + static Handle<v8::Value> indexedGet(::uint32_t index, const v8::AccessorInfo &info) { + StringBuilder ss; + ss << index; + string key = ss.str(); + Local< External > scp = External::Cast( *info.Data() ); + V8Scope* scope = (V8Scope*)(scp->Value()); + Handle<v8::String> name = scope->getV8Str(key); + + if ( info.This()->HasRealIndexedProperty( index ) ) { + // value already cached + return info.This()->GetRealNamedProperty( name ); + } + + BSONHolder* holder = unwrapHolder(info.Holder()); + if ( holder->_removed.count( key ) ) + return Handle<Value>(); + + BSONObj obj = holder->_obj; + BSONElement elmt = obj.getField(key); + if (elmt.eoo()) + return Handle<Value>(); + Handle<Value> val = scope->mongoToV8Element(elmt, false); + info.This()->ForceSet(name, val, DontEnum); + + if (elmt.type() == mongo::Object || elmt.type() == mongo::Array) { + // if accessing a subobject, it may get modified and base obj would not know + // have to set base as modified, which means some optim is lost + unwrapHolder(info.Holder())->_modified = true; + } + return val; + } + + Handle<Boolean> indexedDelete( ::uint32_t index, const AccessorInfo& info ) { + StringBuilder ss; + ss << index; + string key = ss.str(); + + BSONHolder* holder = unwrapHolder(info.Holder()); + holder->_removed.insert( key ); + holder->_extra.remove( key ); + holder->_modified = true; + + // also delete in JS obj + return Handle<Boolean>(); + } + + static Handle<v8::Value> indexedGetRO(::uint32_t index, const v8::AccessorInfo &info) { + StringBuilder ss; + ss << index; + string key = ss.str(); + Local< External > scp = External::Cast( *info.Data() ); + V8Scope* scope = (V8Scope*)(scp->Value()); + // cannot get v8 to properly cache the indexed val in the js object +// Handle<v8::String> name = scope->getV8Str(key); +// // v8 API really confusing here, must check existence on index, but then fetch with name +// if (info.This()->HasRealIndexedProperty(index)) { +// Handle<v8::Value> val = info.This()->GetRealNamedProperty(name); +// if (!val.IsEmpty() && !val->IsNull()) +// return val; +// } + BSONObj obj = unwrapBSONObj(info.Holder()); + BSONElement elmt = obj.getField(key); + if (elmt.eoo()) + return Handle<Value>(); + Handle<Value> val = scope->mongoToV8Element(elmt, true); +// info.This()->ForceSet(name, val); + return val; + } + + static Handle<v8::Value> indexedSet(::uint32_t index, Local<v8::Value> value_obj, const v8::AccessorInfo& info) { + StringBuilder ss; + ss << index; + string key = ss.str(); + BSONHolder* holder = unwrapHolder(info.Holder()); + holder->_removed.erase( key ); + holder->_extra.push_back( key ); + holder->_modified = true; + + // set into JS object + return Handle<Value>(); + } + +// static Handle<v8::Array> indexedEnumerator(const AccessorInfo &info) { +// BSONObj *obj = unwrapBSONObj(info.Holder()); +// Handle<v8::Array> arr = Handle<v8::Array>(v8::Array::New(obj->nFields())); +// Local< External > scp = External::Cast( *info.Data() ); +// V8Scope* scope = (V8Scope*)(scp->Value()); +// int i = 0; +// for ( BSONObjIterator it(*obj); it.more(); ++i) { +// const BSONElement& f = it.next(); +//// arr->Set(i, v8::String::NewExternal(new ExternalString(f.fieldName()))); +// arr->Set(i, scope->getV8Str(f.fieldName())); +// } +// return arr; +// } + + Handle<Value> NamedReadOnlySet( Local<v8::String> property, Local<Value> value, const AccessorInfo& info ) { + string key = toSTLString(property); + cout << "cannot write property " << key << " to read-only object" << endl; + return value; + } + + Handle<Boolean> NamedReadOnlyDelete( Local<v8::String> property, const AccessorInfo& info ) { + string key = toSTLString(property); + cout << "cannot delete property " << key << " from read-only object" << endl; + return Boolean::New( false ); + } + + Handle<Value> IndexedReadOnlySet( ::uint32_t index, Local<Value> value, const AccessorInfo& info ) { + cout << "cannot write property " << index << " to read-only array" << endl; + return value; + } + + Handle<Boolean> IndexedReadOnlyDelete( ::uint32_t index, const AccessorInfo& info ) { + cout << "cannot delete property " << index << " from read-only array" << endl; + return Boolean::New( false ); + } + + // --- engine --- + +// void fatalHandler(const char* s1, const char* s2) { +// cout << "Fatal handler " << s1 << " " << s2 << endl; +// } + + void gcCallback(GCType type, GCCallbackFlags flags) { + HeapStatistics stats; + V8::GetHeapStatistics( &stats ); + log(1) << "V8 GC heap stats - " + << " total: " << stats.total_heap_size() + << " exec: " << stats.total_heap_size_executable() + << " used: " << stats.used_heap_size()<< " limit: " + << stats.heap_size_limit() + << endl; + } + + V8ScriptEngine::V8ScriptEngine() { + // set resource contraints before any call + int K = 1024; + v8::ResourceConstraints rc; +// rc.set_max_young_space_size(4 * K * K); + rc.set_max_old_space_size( 64 * K * K ); + v8::SetResourceConstraints( &rc ); + + // keep engine up after OOM + v8::V8::IgnoreOutOfMemoryException(); +// v8::V8::SetFatalErrorHandler(fatalHandler); + +// v8::Locker l; +// v8::Locker::StartPreemption( 10 ); + + v8::V8::Initialize(); + } + + V8ScriptEngine::~V8ScriptEngine() { + } + + void ScriptEngine::setup() { + if ( !globalScriptEngine ) { + globalScriptEngine = new V8ScriptEngine(); + } + } + + void V8ScriptEngine::interrupt( unsigned opSpec ) { + v8::Locker l; + v8Locks::InterruptLock il; + if ( __interruptSpecToThreadId.count( opSpec ) ) { + int thread = __interruptSpecToThreadId[ opSpec ]; + if ( thread == -2 || thread == -3) { + // just mark as interrupted + __interruptSpecToThreadId[ opSpec ] = -3; + return; + } + + V8::TerminateExecution( __interruptSpecToIsolate[ opSpec ] ); + } + } + + void V8ScriptEngine::interruptAll() { + v8::Locker l; + v8Locks::InterruptLock il; + vector< Isolate* > toKill; // v8 mutex could potentially be yielded during the termination call + + for( map< unsigned, Isolate* >::const_iterator i = __interruptSpecToIsolate.begin(); i != __interruptSpecToIsolate.end(); ++i ) { + toKill.push_back( i->second ); + } + + for( vector< Isolate* >::const_iterator i = toKill.begin(); i != toKill.end(); ++i ) { + V8::TerminateExecution( *i ); + } + } + + // --- scope --- + + V8Scope::V8Scope( V8ScriptEngine * engine ) + : _engine( engine ) , + _connectState( NOT ) { + + // create new isolate and enter it via a scope + _isolate = v8::Isolate::New(); + v8::Isolate::Scope iscope(_isolate); + + // resource constraints must be set on isolate, before any call or lock + int K = 1024; + v8::ResourceConstraints rc; +// rc.set_max_young_space_size(4 * K * K); + rc.set_max_old_space_size( 64 * K * K ); + v8::SetResourceConstraints(&rc); + V8::AddGCPrologueCallback(gcCallback, kGCTypeMarkSweepCompact); + + // keep engine up after OOM + v8::V8::IgnoreOutOfMemoryException(); +// V8::SetFatalErrorHandler(fatalHandler); + + v8::Locker l(_isolate); + + HandleScope handleScope; + _context = Context::New(); + Context::Scope context_scope( _context ); + _global = Persistent< v8::Object >::New( _context->Global() ); + _emptyObj = Persistent< v8::Object >::New( v8::Object::New() ); + + V8STR_CONN = getV8Str( "_conn" ); + V8STR_ID = getV8Str( "_id" ); + V8STR_LENGTH = getV8Str( "length" ); + V8STR_LEN = getV8Str( "len" ); + V8STR_TYPE = getV8Str( "type" ); + V8STR_ISOBJECTID = getV8Str( "isObjectId" ); + V8STR_RETURN = getV8Str( "return" ); + V8STR_ARGS = getV8Str( "args" ); + V8STR_T = getV8Str( "t" ); + V8STR_I = getV8Str( "i" ); + V8STR_EMPTY = getV8Str( "" ); + V8STR_MINKEY = getV8Str( "$MinKey" ); + V8STR_MAXKEY = getV8Str( "$MaxKey" ); + V8STR_NUMBERLONG = getV8Str( "__NumberLong" ); + V8STR_NUMBERINT = getV8Str( "__NumberInt" ); + V8STR_DBPTR = getV8Str( "__DBPointer" ); + V8STR_BINDATA = getV8Str( "__BinData" ); + V8STR_NATIVE_FUNC = getV8Str( "_native_function" ); + V8STR_NATIVE_DATA = getV8Str( "_native_data" ); + V8STR_V8_FUNC = getV8Str( "_v8_function" ); + V8STR_RO = getV8Str( "_ro" ); + V8STR_FULLNAME = getV8Str( "_fullName" ); + V8STR_BSON = getV8Str( "_bson" ); + + // initialize lazy object template + lzObjectTemplate = Persistent<ObjectTemplate>::New(ObjectTemplate::New()); + lzObjectTemplate->SetInternalFieldCount( 1 ); + lzObjectTemplate->SetNamedPropertyHandler(namedGet, namedSet, 0, namedDelete, namedEnumerator, v8::External::New(this)); + lzObjectTemplate->SetIndexedPropertyHandler(indexedGet, indexedSet, 0, indexedDelete, namedEnumerator, v8::External::New(this)); + lzObjectTemplate->NewInstance()->GetPrototype()->ToObject()->Set(V8STR_BSON, v8::Boolean::New(true), DontEnum); + + roObjectTemplate = Persistent<ObjectTemplate>::New(ObjectTemplate::New()); + roObjectTemplate->SetInternalFieldCount( 1 ); + roObjectTemplate->SetNamedPropertyHandler(namedGetRO, NamedReadOnlySet, 0, NamedReadOnlyDelete, namedEnumerator, v8::External::New(this)); + roObjectTemplate->SetIndexedPropertyHandler(indexedGetRO, IndexedReadOnlySet, 0, IndexedReadOnlyDelete, 0, v8::External::New(this)); + roObjectTemplate->NewInstance()->GetPrototype()->ToObject()->Set(V8STR_BSON, v8::Boolean::New(true), DontEnum); + + // initialize lazy array template + // unfortunately it is not possible to create true v8 array from a template + // this means we use an object template and copy methods over + // this it creates issues when calling certain methods that check array type + lzArrayTemplate = Persistent<ObjectTemplate>::New(ObjectTemplate::New()); + lzArrayTemplate->SetInternalFieldCount( 1 ); + lzArrayTemplate->SetIndexedPropertyHandler(indexedGet, 0, 0, 0, 0, v8::External::New(this)); + lzArrayTemplate->NewInstance()->GetPrototype()->ToObject()->Set(V8STR_BSON, v8::Boolean::New(true), DontEnum); + + internalFieldObjects = Persistent<ObjectTemplate>::New(ObjectTemplate::New()); + internalFieldObjects->SetInternalFieldCount( 1 ); + + injectV8Function("print", Print); + injectV8Function("version", Version); + injectV8Function("load", load); + + _wrapper = Persistent< v8::Function >::New( getObjectWrapperTemplate(this)->GetFunction() ); + + injectV8Function("gc", GCV8); + + installDBTypes( this, _global ); + } + + V8Scope::~V8Scope() { + // make sure to disable interrupt, otherwise can get segfault on race condition + disableV8Interrupt(); + + { + V8_SIMPLE_HEADER + _wrapper.Dispose(); + _emptyObj.Dispose(); + for( unsigned i = 0; i < _funcs.size(); ++i ) + _funcs[ i ].Dispose(); + _funcs.clear(); + _global.Dispose(); + std::map <string, v8::Persistent <v8::String> >::iterator it = _strCache.begin(); + std::map <string, v8::Persistent <v8::String> >::iterator end = _strCache.end(); + while (it != end) { + it->second.Dispose(); + ++it; + } + lzObjectTemplate.Dispose(); + lzArrayTemplate.Dispose(); + roObjectTemplate.Dispose(); + internalFieldObjects.Dispose(); + _context.Dispose(); + } + + _isolate->Dispose(); + } + + bool V8Scope::hasOutOfMemoryException() { + if (!_context.IsEmpty()) + return _context->HasOutOfMemoryException(); + return false; + } + + /** + * JS Callback that will call a c++ function with BSON arguments. + */ + Handle< Value > V8Scope::nativeCallback( V8Scope* scope, const Arguments &args ) { +// V8Lock l; + HandleScope handle_scope; + Local< External > f = External::Cast( *args.Callee()->Get( scope->V8STR_NATIVE_FUNC ) ); + NativeFunction function = (NativeFunction)(f->Value()); + Local< External > data = External::Cast( *args.Callee()->Get( scope->V8STR_NATIVE_DATA ) ); + BSONObjBuilder b; + for( int i = 0; i < args.Length(); ++i ) { + stringstream ss; + ss << i; + scope->v8ToMongoElement( b, ss.str(), args[ i ] ); + } + BSONObj nativeArgs = b.obj(); + BSONObj ret; + try { + ret = function( nativeArgs, data->Value() ); + } + catch( const std::exception &e ) { + return v8::ThrowException(v8::String::New(e.what())); + } + catch( ... ) { + return v8::ThrowException(v8::String::New("unknown exception")); + } + return handle_scope.Close( scope->mongoToV8Element( ret.firstElement() ) ); + } + + Handle< Value > V8Scope::load( V8Scope* scope, const Arguments &args ) { + Context::Scope context_scope(scope->_context); + for (int i = 0; i < args.Length(); ++i) { + std::string filename(toSTLString(args[i])); + if (!scope->execFile(filename, false , true , false)) { + return v8::ThrowException(v8::String::New((std::string("error loading file: ") + filename).c_str())); + } + } + return v8::True(); + } + + /** + * JS Callback that will call a c++ function with the v8 scope and v8 arguments. + * Handles interrupts, exception handling, etc + * + * The implementation below assumes that SERVER-1816 has been fixed - in + * particular, interrupted() must return true if an interrupt was ever + * sent; currently that is not the case if a new killop overwrites the data + * for an old one + */ + v8::Handle< v8::Value > V8Scope::v8Callback( const v8::Arguments &args ) { + Local< External > f = External::Cast( *args.Callee()->Get( v8::String::New( "_v8_function" ) ) ); + v8Function function = (v8Function)(f->Value()); + Local< External > scp = External::Cast( *args.Data() ); + V8Scope* scope = (V8Scope*)(scp->Value()); + + // originally v8 interrupt where disabled here cause: don't want to have to audit all v8 calls for termination exceptions + // but we do need to keep interrupt because much time may be spent here (e.g. sleep) + bool paused = scope->pauseV8Interrupt(); + + v8::Handle< v8::Value > ret; + string exception; + try { + ret = function( scope, args ); + } + catch( const std::exception &e ) { + exception = e.what(); + } + catch( ... ) { + exception = "unknown exception"; + } + if (paused) { + bool resume = scope->resumeV8Interrupt(); + if ( !resume || globalScriptEngine->interrupted() ) { + v8::V8::TerminateExecution(scope->_isolate); + return v8::ThrowException( v8::String::New( "Interruption in V8 native callback" ) ); + } + } + if ( !exception.empty() ) { + return v8::ThrowException( v8::String::New( exception.c_str() ) ); + } + return ret; + } + + // ---- global stuff ---- + + void V8Scope::init( const BSONObj * data ) { +// V8Lock l; + if ( ! data ) + return; + + BSONObjIterator i( *data ); + while ( i.more() ) { + BSONElement e = i.next(); + setElement( e.fieldName() , e ); + } + } + + void V8Scope::setNumber( const char * field , double val ) { + V8_SIMPLE_HEADER + _global->Set( getV8Str( field ) , v8::Number::New( val ) ); + } + + void V8Scope::setString( const char * field , const char * val ) { + V8_SIMPLE_HEADER + _global->Set( getV8Str( field ) , v8::String::New( val ) ); + } + + void V8Scope::setBoolean( const char * field , bool val ) { + V8_SIMPLE_HEADER + _global->Set( getV8Str( field ) , v8::Boolean::New( val ) ); + } + + void V8Scope::setElement( const char *field , const BSONElement& e ) { + V8_SIMPLE_HEADER + _global->Set( getV8Str( field ) , mongoToV8Element( e ) ); + } + + void V8Scope::setObject( const char *field , const BSONObj& obj , bool readOnly) { + V8_SIMPLE_HEADER + // Set() accepts a ReadOnly parameter, but this just prevents the field itself + // from being overwritten and doesn't protect the object stored in 'field'. + _global->Set( getV8Str( field ) , mongoToLZV8( obj, false, readOnly) ); + } + + int V8Scope::type( const char *field ) { + V8_SIMPLE_HEADER + Handle<Value> v = get( field ); + if ( v->IsNull() ) + return jstNULL; + if ( v->IsUndefined() ) + return Undefined; + if ( v->IsString() ) + return String; + if ( v->IsFunction() ) + return Code; + if ( v->IsArray() ) + return Array; + if ( v->IsBoolean() ) + return Bool; + // needs to be explicit NumberInt to use integer +// if ( v->IsInt32() ) +// return NumberInt; + if ( v->IsNumber() ) + return NumberDouble; + if ( v->IsExternal() ) { + uassert( 10230 , "can't handle external yet" , 0 ); + return -1; + } + if ( v->IsDate() ) + return Date; + if ( v->IsObject() ) + return Object; + + throw UserException( 12509, (string)"don't know what this is: " + field ); + } + + v8::Handle<v8::Value> V8Scope::get( const char * field ) { + return _global->Get( getV8Str( field ) ); + } + + double V8Scope::getNumber( const char *field ) { + V8_SIMPLE_HEADER + return get( field )->ToNumber()->Value(); + } + + int V8Scope::getNumberInt( const char *field ) { + V8_SIMPLE_HEADER + return get( field )->ToInt32()->Value(); + } + + long long V8Scope::getNumberLongLong( const char *field ) { + V8_SIMPLE_HEADER + return get( field )->ToInteger()->Value(); + } + + string V8Scope::getString( const char *field ) { + V8_SIMPLE_HEADER + return toSTLString( get( field ) ); + } + + bool V8Scope::getBoolean( const char *field ) { + V8_SIMPLE_HEADER + return get( field )->ToBoolean()->Value(); + } + + BSONObj V8Scope::getObject( const char * field ) { + V8_SIMPLE_HEADER + Handle<Value> v = get( field ); + if ( v->IsNull() || v->IsUndefined() ) + return BSONObj(); + uassert( 10231 , "not an object" , v->IsObject() ); + return v8ToMongo( v->ToObject() ); + } + + // --- functions ----- + + bool hasFunctionIdentifier( const string& code ) { + if ( code.size() < 9 || code.find( "function" ) != 0 ) + return false; + + return code[8] == ' ' || code[8] == '('; + } + + Local< v8::Function > V8Scope::__createFunction( const char * raw ) { + raw = jsSkipWhiteSpace( raw ); + string code = raw; + if ( !hasFunctionIdentifier( code ) ) { + if ( code.find( "\n" ) == string::npos && + ! hasJSReturn( code ) && + ( code.find( ";" ) == string::npos || code.find( ";" ) == code.size() - 1 ) ) { + code = "return " + code; + } + code = "function(){ " + code + "}"; + } + + int num = _funcs.size() + 1; + + string fn; + { + stringstream ss; + ss << "_funcs" << num; + fn = ss.str(); + } + + code = fn + " = " + code; + + TryCatch try_catch; + // this might be time consuming, consider allowing an interrupt + Handle<Script> script = v8::Script::Compile( v8::String::New( code.c_str() ) , + v8::String::New( fn.c_str() ) ); + if ( script.IsEmpty() ) { + _error = (string)"compile error: " + toSTLString( &try_catch ); + log() << _error << endl; + return Local< v8::Function >(); + } + + Local<Value> result = script->Run(); + if ( result.IsEmpty() ) { + _error = (string)"compile error: " + toSTLString( &try_catch ); + log() << _error << endl; + return Local< v8::Function >(); + } + + return v8::Function::Cast( *_global->Get( v8::String::New( fn.c_str() ) ) ); + } + + ScriptingFunction V8Scope::_createFunction( const char * raw ) { + V8_SIMPLE_HEADER + Local< Value > ret = __createFunction( raw ); + if ( ret.IsEmpty() ) + return 0; + Persistent<Value> f = Persistent< Value >::New( ret ); + uassert( 10232, "not a func" , f->IsFunction() ); + int num = _funcs.size() + 1; + _funcs.push_back( f ); + return num; + } + + void V8Scope::setFunction( const char *field , const char * code ) { + V8_SIMPLE_HEADER + _global->Set( getV8Str( field ) , __createFunction(code) ); + } + +// void V8Scope::setThis( const BSONObj * obj ) { +// V8_SIMPLE_HEADER +// if ( ! obj ) { +// _this = Persistent< v8::Object >::New( v8::Object::New() ); +// return; +// } +// +// //_this = mongoToV8( *obj ); +// v8::Handle<v8::Value> argv[1]; +// argv[0] = v8::External::New( createWrapperHolder( this, obj , true , false ) ); +// _this = Persistent< v8::Object >::New( _wrapper->NewInstance( 1, argv ) ); +// } + + void V8Scope::rename( const char * from , const char * to ) { + V8_SIMPLE_HEADER; + Handle<v8::String> f = getV8Str( from ); + Handle<v8::String> t = getV8Str( to ); + _global->Set( t , _global->Get( f ) ); + _global->Set( f , v8::Undefined() ); + } + + int V8Scope::invoke( ScriptingFunction func , const BSONObj* argsObject, const BSONObj* recv, int timeoutMs , bool ignoreReturn, bool readOnlyArgs, bool readOnlyRecv ) { + V8_SIMPLE_HEADER + Handle<Value> funcValue = _funcs[func-1]; + + TryCatch try_catch; + int nargs = argsObject ? argsObject->nFields() : 0; + scoped_array< Handle<Value> > args; + if ( nargs ) { + args.reset( new Handle<Value>[nargs] ); + BSONObjIterator it( *argsObject ); + for ( int i=0; i<nargs; i++ ) { + BSONElement next = it.next(); + args[i] = mongoToV8Element( next, readOnlyArgs ); + } + setObject( "args", *argsObject, readOnlyArgs); // for backwards compatibility + } + else { + _global->Set( V8STR_ARGS, v8::Undefined() ); + } + if ( globalScriptEngine->interrupted() ) { + stringstream ss; + ss << "error in invoke: " << globalScriptEngine->checkInterrupt(); + _error = ss.str(); + log() << _error << endl; + return 1; + } + Handle<v8::Object> v8recv; + if (recv != 0) + v8recv = mongoToLZV8(*recv, false, readOnlyRecv); + else + v8recv = _global; + + enableV8Interrupt(); // because of v8 locker we can check interrupted, then enable + Local<Value> result = ((v8::Function*)(*funcValue))->Call( v8recv , nargs , nargs ? args.get() : 0 ); + disableV8Interrupt(); + + if ( result.IsEmpty() ) { + stringstream ss; + if ( try_catch.HasCaught() && !try_catch.CanContinue() ) { + ss << "error in invoke: " << globalScriptEngine->checkInterrupt(); + } + else { + ss << "error in invoke: " << toSTLString( &try_catch ); + } + _error = ss.str(); + log() << _error << endl; + return 1; + } + + if ( ! ignoreReturn ) { + _global->Set( V8STR_RETURN , result ); + } + + return 0; + } + + bool V8Scope::exec( const StringData& code , const string& name , bool printResult , bool reportError , bool assertOnError, int timeoutMs ) { + if ( timeoutMs ) { + static bool t = 1; + if ( t ) { + log() << "timeoutMs not support for v8 yet code: " << code << endl; + t = 0; + } + } + + V8_SIMPLE_HEADER + + TryCatch try_catch; + + Handle<Script> script = v8::Script::Compile( v8::String::New( code.data() ) , + v8::String::New( name.c_str() ) ); + if (script.IsEmpty()) { + stringstream ss; + ss << "compile error: " << toSTLString( &try_catch ); + _error = ss.str(); + if (reportError) + log() << _error << endl; + if ( assertOnError ) + uassert( 10233 , _error , 0 ); + return false; + } + + if ( globalScriptEngine->interrupted() ) { + _error = (string)"exec error: " + globalScriptEngine->checkInterrupt(); + if ( reportError ) { + log() << _error << endl; + } + if ( assertOnError ) { + uassert( 13475 , _error , 0 ); + } + return false; + } + enableV8Interrupt(); // because of v8 locker we can check interrupted, then enable + Handle<v8::Value> result = script->Run(); + disableV8Interrupt(); + if ( result.IsEmpty() ) { + if ( try_catch.HasCaught() && !try_catch.CanContinue() ) { + _error = (string)"exec error: " + globalScriptEngine->checkInterrupt(); + } + else { + _error = (string)"exec error: " + toSTLString( &try_catch ); + } + if ( reportError ) + log() << _error << endl; + if ( assertOnError ) + uassert( 10234 , _error , 0 ); + return false; + } + + _global->Set( getV8Str( "__lastres__" ) , result ); + + if ( printResult && ! result->IsUndefined() ) { + cout << toSTLString( result ) << endl; + } + + return true; + } + + void V8Scope::injectNative( const char *field, NativeFunction func, void* data ) { + injectNative(field, func, _global, data); + } + + void V8Scope::injectNative( const char *field, NativeFunction func, Handle<v8::Object>& obj, void* data ) { + V8_SIMPLE_HEADER + + Handle< FunctionTemplate > ft = createV8Function(nativeCallback); + ft->Set( this->V8STR_NATIVE_FUNC, External::New( (void*)func ) ); + ft->Set( this->V8STR_NATIVE_DATA, External::New( data ) ); + obj->Set( getV8Str( field ), ft->GetFunction() ); + } + + void V8Scope::injectV8Function( const char *field, v8Function func ) { + injectV8Function(field, func, _global); + } + + void V8Scope::injectV8Function( const char *field, v8Function func, Handle<v8::Object>& obj ) { + V8_SIMPLE_HEADER + + Handle< FunctionTemplate > ft = createV8Function(func); + Handle<v8::Function> f = ft->GetFunction(); + obj->Set( getV8Str( field ), f ); + } + + void V8Scope::injectV8Function( const char *field, v8Function func, Handle<v8::Template>& t ) { + V8_SIMPLE_HEADER + + Handle< FunctionTemplate > ft = createV8Function(func); + Handle<v8::Function> f = ft->GetFunction(); + t->Set( getV8Str( field ), f ); + } + + Handle<FunctionTemplate> V8Scope::createV8Function( v8Function func ) { + Handle< FunctionTemplate > ft = v8::FunctionTemplate::New(v8Callback, External::New( this )); + ft->Set( this->V8STR_V8_FUNC, External::New( (void*)func ) ); + return ft; + } + + void V8Scope::gc() { + cout << "in gc" << endl; + V8Lock l; + V8::LowMemoryNotification(); + } + + // ----- db access ----- + + void V8Scope::localConnect( const char * dbName ) { + { + V8_SIMPLE_HEADER + + if ( _connectState == EXTERNAL ) + throw UserException( 12510, "externalSetup already called, can't call externalSetup" ); + if ( _connectState == LOCAL ) { + if ( _localDBName == dbName ) + return; + throw UserException( 12511, "localConnect called with a different name previously" ); + } + + //_global->Set( v8::String::New( "Mongo" ) , _engine->_externalTemplate->GetFunction() ); + _global->Set( getV8Str( "Mongo" ) , getMongoFunctionTemplate( this, true )->GetFunction() ); + execCoreFiles(); + exec( "_mongo = new Mongo();" , "local connect 2" , false , true , true , 0 ); + exec( (string)"db = _mongo.getDB(\"" + dbName + "\");" , "local connect 3" , false , true , true , 0 ); + _connectState = LOCAL; + _localDBName = dbName; + } + loadStored(); + } + + void V8Scope::externalSetup() { + V8_SIMPLE_HEADER + if ( _connectState == EXTERNAL ) + return; + if ( _connectState == LOCAL ) + throw UserException( 12512, "localConnect already called, can't call externalSetup" ); + + installFork( this, _global, _context ); + _global->Set( getV8Str( "Mongo" ) , getMongoFunctionTemplate( this, false )->GetFunction() ); + execCoreFiles(); + _connectState = EXTERNAL; + } + + // ----- internal ----- + + void V8Scope::reset() { + _startCall(); + } + + void V8Scope::_startCall() { + _error = ""; + } + + Local< v8::Value > newFunction( const char *code ) { + stringstream codeSS; + codeSS << "____MongoToV8_newFunction_temp = " << code; + string codeStr = codeSS.str(); + Local< Script > compiled = Script::New( v8::String::New( codeStr.c_str() ) ); + if ( compiled.IsEmpty() ) { + warning() << "Could not compile function: " << codeStr.c_str() << endl; + return Local< v8::Value >::New( v8::Null() ); + } + Local< Value > ret = compiled->Run(); + return ret; + } + + Local< v8::Value > V8Scope::newId( const OID &id ) { + v8::Function * idCons = this->getObjectIdCons(); + v8::Handle<v8::Value> argv[1]; + argv[0] = v8::String::New( id.str().c_str() ); + return idCons->NewInstance( 1 , argv ); + } + + Local<v8::Object> V8Scope::mongoToV8( const BSONObj& m , bool array, bool readOnly ) { + + Local<v8::Object> o; + + // handle DBRef. needs to come first. isn't it? (metagoto) + static string ref = "$ref"; + if ( ref == m.firstElement().fieldName() ) { + const BSONElement& id = m["$id"]; + if (!id.eoo()) { // there's no check on $id exitence in sm implementation. risky ? + v8::Function* dbRef = getNamedCons( "DBRef" ); + o = dbRef->NewInstance(); + } + } + + Local< v8::ObjectTemplate > readOnlyObjects; + + if ( !o.IsEmpty() ) { + readOnly = false; + } + else if ( array ) { + // NOTE Looks like it's impossible to add interceptors to v8 arrays. + // so array itself will never be read only, but its values can be + o = v8::Array::New(); + } + else if ( !readOnly ) { + o = v8::Object::New(); + } + else { + // NOTE Our readOnly implemention relies on undocumented ObjectTemplate + // functionality that may be fragile, but it still seems like the best option + // for now -- fwiw, the v8 docs are pretty sparse. I've determined experimentally + // that when property handlers are set for an object template, they will attach + // to objects previously created by that template. To get this to work, though, + // it is necessary to initialize the template's property handlers before + // creating objects from the template (as I have in the following few lines + // of code). + // NOTE In my first attempt, I configured the permanent property handlers before + // constructiong the object and replaced the Set() calls below with ForceSet(). + // However, it turns out that ForceSet() only bypasses handlers for named + // properties and not for indexed properties. + readOnlyObjects = v8::ObjectTemplate::New(); + // NOTE This internal field will store type info for special db types. For + // regular objects the field is unnecessary - for simplicity I'm creating just + // one readOnlyObjects template for objects where the field is & isn't necessary, + // assuming that the overhead of an internal field is slight. + readOnlyObjects->SetInternalFieldCount( 1 ); + readOnlyObjects->SetNamedPropertyHandler( 0 ); + readOnlyObjects->SetIndexedPropertyHandler( 0 ); + o = readOnlyObjects->NewInstance(); + } + + mongo::BSONObj sub; + + for ( BSONObjIterator i(m); i.more(); ) { + const BSONElement& f = i.next(); + + Local<Value> v; + Handle<v8::String> name = getV8Str(f.fieldName()); + + switch ( f.type() ) { + + case mongo::Code: + o->Set( name, newFunction( f.valuestr() ) ); + break; + + case CodeWScope: + if ( !f.codeWScopeObject().isEmpty() ) + log() << "warning: CodeWScope doesn't transfer to db.eval" << endl; + o->Set( name, newFunction( f.codeWScopeCode() ) ); + break; + + case mongo::String: + o->Set( name , v8::String::New( f.valuestr() ) ); + break; + + case mongo::jstOID: { + v8::Function * idCons = getObjectIdCons(); + v8::Handle<v8::Value> argv[1]; + argv[0] = v8::String::New( f.__oid().str().c_str() ); + o->Set( name , + idCons->NewInstance( 1 , argv ) ); + break; + } + + case mongo::NumberDouble: + case mongo::NumberInt: + o->Set( name , v8::Number::New( f.number() ) ); + break; + +// case mongo::NumberInt: { +// Local<v8::Object> sub = readOnly ? readOnlyObjects->NewInstance() : internalFieldObjects->NewInstance(); +// int val = f.numberInt(); +// v8::Function* numberInt = getNamedCons( "NumberInt" ); +// v8::Handle<v8::Value> argv[1]; +// argv[0] = v8::Int32::New( val ); +// o->Set( name, numberInt->NewInstance( 1, argv ) ); +// break; +// } + + case mongo::Array: + sub = f.embeddedObject(); + o->Set( name , mongoToV8( sub , true, readOnly ) ); + break; + case mongo::Object: + sub = f.embeddedObject(); + o->Set( name , mongoToLZV8( sub , false, readOnly ) ); + break; + + case mongo::Date: + o->Set( name , v8::Date::New( (double) ((long long)f.date().millis) )); + break; + + case mongo::Bool: + o->Set( name , v8::Boolean::New( f.boolean() ) ); + break; + + case mongo::jstNULL: + case mongo::Undefined: // duplicate sm behavior + o->Set( name , v8::Null() ); + break; + + case mongo::RegEx: { + v8::Function * regex = getNamedCons( "RegExp" ); + + v8::Handle<v8::Value> argv[2]; + argv[0] = v8::String::New( f.regex() ); + argv[1] = v8::String::New( f.regexFlags() ); + + o->Set( name , regex->NewInstance( 2 , argv ) ); + break; + } + + case mongo::BinData: { + int len; + const char *data = f.binData( len ); + + v8::Function* binData = getNamedCons( "BinData" ); + v8::Handle<v8::Value> argv[3]; + argv[0] = v8::Number::New( len ); + argv[1] = v8::Number::New( f.binDataType() ); + argv[2] = v8::String::New( data, len ); + o->Set( name, binData->NewInstance(3, argv) ); + break; + } + + case mongo::Timestamp: { + Local<v8::Object> sub = readOnly ? readOnlyObjects->NewInstance() : internalFieldObjects->NewInstance(); + + sub->Set( V8STR_T , v8::Number::New( f.timestampTime() ) ); + sub->Set( V8STR_I , v8::Number::New( f.timestampInc() ) ); + sub->SetInternalField( 0, v8::Uint32::New( f.type() ) ); + + o->Set( name , sub ); + break; + } + + case mongo::NumberLong: { + unsigned long long val = f.numberLong(); + v8::Function* numberLong = getNamedCons( "NumberLong" ); + double floatApprox = (double)(long long)val; + // values above 2^53 are not accurately represented in JS + if ( (long long)val == (long long)floatApprox && val < 9007199254740992ULL ) { + v8::Handle<v8::Value> argv[1]; + argv[0] = v8::Number::New( floatApprox ); + o->Set( name, numberLong->NewInstance( 1, argv ) ); + } + else { + v8::Handle<v8::Value> argv[3]; + argv[0] = v8::Number::New( floatApprox ); + argv[1] = v8::Integer::New( val >> 32 ); + argv[2] = v8::Integer::New( (unsigned long)(val & 0x00000000ffffffff) ); + o->Set( name, numberLong->NewInstance(3, argv) ); + } + break; + } + + case mongo::MinKey: { + Local<v8::Object> sub = readOnly ? readOnlyObjects->NewInstance() : internalFieldObjects->NewInstance(); + sub->Set( V8STR_MINKEY, v8::Boolean::New( true ) ); + sub->SetInternalField( 0, v8::Uint32::New( f.type() ) ); + o->Set( name , sub ); + break; + } + + case mongo::MaxKey: { + Local<v8::Object> sub = readOnly ? readOnlyObjects->NewInstance() : internalFieldObjects->NewInstance(); + sub->Set( V8STR_MAXKEY, v8::Boolean::New( true ) ); + sub->SetInternalField( 0, v8::Uint32::New( f.type() ) ); + o->Set( name , sub ); + break; + } + + case mongo::DBRef: { + v8::Function* dbPointer = getNamedCons( "DBPointer" ); + v8::Handle<v8::Value> argv[2]; + argv[0] = getV8Str( f.dbrefNS() ); + argv[1] = newId( f.dbrefOID() ); + o->Set( name, dbPointer->NewInstance(2, argv) ); + break; + } + + default: + cout << "can't handle type: "; + cout << f.type() << " "; + cout << f.toString(); + cout << endl; + break; + } + + } + + if ( !array && readOnly ) { + readOnlyObjects->SetNamedPropertyHandler( 0, NamedReadOnlySet, 0, NamedReadOnlyDelete ); + readOnlyObjects->SetIndexedPropertyHandler( 0, IndexedReadOnlySet, 0, IndexedReadOnlyDelete ); + } + + return o; + } + + /** + * converts a BSONObj to a Lazy V8 object + */ + Handle<v8::Object> V8Scope::mongoToLZV8( const BSONObj& m , bool array, bool readOnly ) { + Local<v8::Object> o; + BSONHolder* own = new BSONHolder(m); + + if ( readOnly ) { + o = roObjectTemplate->NewInstance(); + } else { + if (array) { + o = lzArrayTemplate->NewInstance(); + o->SetPrototype(v8::Array::New(1)->GetPrototype()); + o->Set(V8STR_LENGTH, v8::Integer::New(m.nFields()), DontEnum); + } else { + o = lzObjectTemplate->NewInstance(); + + static string ref = "$ref"; + if ( ref == m.firstElement().fieldName() ) { + const BSONElement& id = m["$id"]; + if (!id.eoo()) { + v8::Function* dbRef = getNamedCons( "DBRef" ); + o->SetPrototype(dbRef->NewInstance()->GetPrototype()); + } + } + } + } + + Persistent<v8::Object> p = wrapBSONObject(o, own); + +// if (!readOnly) { +// // need to set all keys with dummy values, so that order of keys is correct during enumeration +// // otherwise v8 will list any newly set property in JS before the ones of underlying BSON obj. +// for (BSONObjIterator it(m); it.more();) { +// const BSONElement& f = it.next(); +// o->ForceSet(getV8Str(f.fieldName()), v8::Undefined()); +// } +// own->_modified = false; +// } + + return p; + } + + Handle<v8::Value> V8Scope::mongoToV8Element( const BSONElement &f, bool readOnly ) { +// Local< v8::ObjectTemplate > internalFieldObjects = v8::ObjectTemplate::New(); +// internalFieldObjects->SetInternalFieldCount( 1 ); + + switch ( f.type() ) { + + case mongo::Code: + return newFunction( f.valuestr() ); + + case CodeWScope: + if ( !f.codeWScopeObject().isEmpty() ) + log() << "warning: CodeWScope doesn't transfer to db.eval" << endl; + return newFunction( f.codeWScopeCode() ); + + case mongo::String: +// return v8::String::NewExternal( new ExternalString( f.valuestr() )); + return v8::String::New( f.valuestr() ); +// return getV8Str( f.valuestr() ); + + case mongo::jstOID: + return newId( f.__oid() ); + + case mongo::NumberDouble: + case mongo::NumberInt: + return v8::Number::New( f.number() ); + + case mongo::Array: + // for arrays it's better to use non lazy object because: + // - the lazy array is not a true v8 array and requires some v8 src change for all methods to work + // - it made several tests about 1.5x slower + // - most times when an array is accessed, all its values will be used + return mongoToV8( f.embeddedObject() , true, readOnly ); + case mongo::Object: + return mongoToLZV8( f.embeddedObject() , false, readOnly); + + case mongo::Date: + return v8::Date::New( (double) ((long long)f.date().millis) ); + + case mongo::Bool: + return v8::Boolean::New( f.boolean() ); + + case mongo::EOO: + case mongo::jstNULL: + case mongo::Undefined: // duplicate sm behavior + return v8::Null(); + + case mongo::RegEx: { + v8::Function * regex = getNamedCons( "RegExp" ); + + v8::Handle<v8::Value> argv[2]; + argv[0] = v8::String::New( f.regex() ); + argv[1] = v8::String::New( f.regexFlags() ); + + return regex->NewInstance( 2 , argv ); + break; + } + + case mongo::BinData: { + int len; + const char *data = f.binData( len ); + + v8::Function* binData = getNamedCons( "BinData" ); + v8::Handle<v8::Value> argv[3]; + argv[0] = v8::Number::New( len ); + argv[1] = v8::Number::New( f.binDataType() ); + argv[2] = v8::String::New( data, len ); + return binData->NewInstance( 3, argv ); + }; + + case mongo::Timestamp: { + Local<v8::Object> sub = internalFieldObjects->NewInstance(); + + sub->Set( V8STR_T , v8::Number::New( f.timestampTime() ) ); + sub->Set( V8STR_I , v8::Number::New( f.timestampInc() ) ); + sub->SetInternalField( 0, v8::Uint32::New( f.type() ) ); + + return sub; + } + + case mongo::NumberLong: { + unsigned long long val = f.numberLong(); + v8::Function* numberLong = getNamedCons( "NumberLong" ); + // values above 2^53 are not accurately represented in JS + if ( (long long)val == (long long)(double)(long long)(val) && val < 9007199254740992ULL ) { + v8::Handle<v8::Value> argv[1]; + argv[0] = v8::Number::New( (double)(long long)( val ) ); + return numberLong->NewInstance( 1, argv ); + } + else { + v8::Handle<v8::Value> argv[3]; + argv[0] = v8::Number::New( (double)(long long)( val ) ); + argv[1] = v8::Integer::New( val >> 32 ); + argv[2] = v8::Integer::New( (unsigned long)(val & 0x00000000ffffffff) ); + return numberLong->NewInstance( 3, argv ); + } + } + +// case mongo::NumberInt: { +// Local<v8::Object> sub = internalFieldObjects->NewInstance(); +// int val = f.numberInt(); +// v8::Function* numberInt = getNamedCons( "NumberInt" ); +// v8::Handle<v8::Value> argv[1]; +// argv[0] = v8::Int32::New(val); +// return numberInt->NewInstance( 1, argv ); +// } + + case mongo::MinKey: { + Local<v8::Object> sub = internalFieldObjects->NewInstance(); + sub->Set( V8STR_MINKEY, v8::Boolean::New( true ) ); + sub->SetInternalField( 0, v8::Uint32::New( f.type() ) ); + return sub; + } + + case mongo::MaxKey: { + Local<v8::Object> sub = internalFieldObjects->NewInstance(); + sub->Set( V8STR_MAXKEY, v8::Boolean::New( true ) ); + sub->SetInternalField( 0, v8::Uint32::New( f.type() ) ); + return sub; + } + + case mongo::DBRef: { + v8::Function* dbPointer = getNamedCons( "DBPointer" ); + v8::Handle<v8::Value> argv[2]; + argv[0] = getV8Str( f.dbrefNS() ); + argv[1] = newId( f.dbrefOID() ); + return dbPointer->NewInstance(2, argv); + } + + default: + cout << "can't handle type: "; + cout << f.type() << " "; + cout << f.toString(); + cout << endl; + break; + } + + return v8::Undefined(); + } + + void V8Scope::append( BSONObjBuilder & builder , const char * fieldName , const char * scopeName ) { + V8_SIMPLE_HEADER + Handle<v8::String> v8name = getV8Str(scopeName); + Handle<Value> value = _global->Get( v8name ); + v8ToMongoElement(builder, fieldName, value); + } + + void V8Scope::v8ToMongoElement( BSONObjBuilder & b , const string sname , v8::Handle<v8::Value> value , int depth, BSONObj* originalParent ) { + + if ( value->IsString() ) { +// Handle<v8::String> str = Handle<v8::String>::Cast(value); +// ExternalString* es = (ExternalString*) (str->GetExternalAsciiStringResource()); +// b.append( sname , es->data() ); + b.append( sname , toSTLString( value ).c_str() ); + return; + } + + if ( value->IsFunction() ) { + b.appendCode( sname , toSTLString( value ) ); + return; + } + + if ( value->IsNumber() ) { + double val = value->ToNumber()->Value(); + // if previous type was integer, keep it + int intval = (int)val; + if (val == intval && originalParent) { + BSONElement elmt = originalParent->getField(sname); + if (elmt.type() == mongo::NumberInt) { + b.append( sname , intval ); + return; + } + } + + b.append( sname , val ); + return; + } + + if ( value->IsArray() ) { + BSONObj sub = v8ToMongo( value->ToObject() , depth ); + b.appendArray( sname , sub ); + return; + } + + if ( value->IsDate() ) { + long long dateval = (long long)(v8::Date::Cast( *value )->NumberValue()); + b.appendDate( sname , Date_t( (unsigned long long) dateval ) ); + return; + } + + if ( value->IsExternal() ) + return; + + if ( value->IsObject() ) { + // The user could potentially modify the fields of these special objects, + // wreaking havoc when we attempt to reinterpret them. Not doing any validation + // for now... + Local< v8::Object > obj = value->ToObject(); + if ( obj->InternalFieldCount() && obj->GetInternalField( 0 )->IsNumber() ) { + switch( obj->GetInternalField( 0 )->ToInt32()->Value() ) { // NOTE Uint32's Value() gave me a linking error, so going with this instead + case Timestamp: + b.appendTimestamp( sname, + Date_t( (unsigned long long)(obj->Get( V8STR_T )->ToNumber()->Value() )), + obj->Get( V8STR_I )->ToInt32()->Value() ); + return; + case MinKey: + b.appendMinKey( sname ); + return; + case MaxKey: + b.appendMaxKey( sname ); + return; + default: + verify( "invalid internal field" == 0 ); + } + } + string s = toSTLString( value ); + if ( s.size() && s[0] == '/' ) { + s = s.substr( 1 ); + string r = s.substr( 0 , s.rfind( "/" ) ); + string o = s.substr( s.rfind( "/" ) + 1 ); + b.appendRegex( sname , r , o ); + } + else if ( value->ToObject()->GetPrototype()->IsObject() && + value->ToObject()->GetPrototype()->ToObject()->HasRealNamedProperty( V8STR_ISOBJECTID ) ) { + OID oid; + oid.init( toSTLString( value->ToObject()->Get(getV8Str("str")) ) ); + b.appendOID( sname , &oid ); + } + else if ( !value->ToObject()->GetHiddenValue( V8STR_NUMBERLONG ).IsEmpty() ) { + // TODO might be nice to potentially speed this up with an indexed internal + // field, but I don't yet know how to use an ObjectTemplate with a + // constructor. + v8::Handle< v8::Object > it = value->ToObject(); + long long val; + if ( !it->Has( getV8Str( "top" ) ) ) { + val = (long long)( it->Get( getV8Str( "floatApprox" ) )->NumberValue() ); + } + else { + val = (long long) + ( (unsigned long long)( it->Get( getV8Str( "top" ) )->ToInt32()->Value() ) << 32 ) + + (unsigned)( it->Get( getV8Str( "bottom" ) )->ToInt32()->Value() ); + } + + b.append( sname, val ); + } + else if ( !value->ToObject()->GetHiddenValue( V8STR_NUMBERINT ).IsEmpty() ) { + v8::Handle< v8::Object > it = value->ToObject(); + b.append(sname, it->GetHiddenValue(V8STR_NUMBERINT)->Int32Value()); + } + else if ( !value->ToObject()->GetHiddenValue( V8STR_DBPTR ).IsEmpty() ) { + OID oid; + Local<Value> theid = value->ToObject()->Get( getV8Str( "id" ) ); + oid.init( toSTLString( theid->ToObject()->Get(getV8Str("str")) ) ); + string ns = toSTLString( value->ToObject()->Get( getV8Str( "ns" ) ) ); + b.appendDBRef( sname, ns, oid ); + } + else if ( !value->ToObject()->GetHiddenValue( V8STR_BINDATA ).IsEmpty() ) { + int len = obj->Get( getV8Str( "len" ) )->ToInt32()->Value(); + Local<External> c = External::Cast( *(obj->GetInternalField( 0 )) ); + const char* dataArray = (char*)(c->Value());; + b.appendBinData( sname, + len, + mongo::BinDataType( obj->Get( getV8Str( "type" ) )->ToInt32()->Value() ), + dataArray ); + } + else { + BSONObj sub = v8ToMongo( value->ToObject() , depth ); + b.append( sname , sub ); + } + return; + } + + if ( value->IsBoolean() ) { + b.appendBool( sname , value->ToBoolean()->Value() ); + return; + } + + else if ( value->IsUndefined() ) { + b.appendUndefined( sname ); + return; + } + + else if ( value->IsNull() ) { + b.appendNull( sname ); + return; + } + + cout << "don't know how to convert to mongo field [" << sname << "]\t" << value << endl; + } + + BSONObj V8Scope::v8ToMongo( v8::Handle<v8::Object> o , int depth ) { + BSONObj originalBSON; + if (o->Has(V8STR_BSON)) { + originalBSON = unwrapBSONObj(o); + BSONHolder* holder = unwrapHolder(o); + if ( !holder->_modified ) { + // object was not modified, use bson as is + return originalBSON; + } + } + + BSONObjBuilder b; + + if ( depth == 0 ) { + if ( o->HasRealNamedProperty( V8STR_ID ) ) { + v8ToMongoElement( b , "_id" , o->Get( V8STR_ID ), 0, &originalBSON ); + } + } + + Local<v8::Array> names = o->GetPropertyNames(); + for ( unsigned int i=0; i<names->Length(); i++ ) { + v8::Local<v8::String> name = names->Get( i )->ToString(); + +// if ( o->GetPrototype()->IsObject() && +// o->GetPrototype()->ToObject()->HasRealNamedProperty( name ) ) +// continue; + + v8::Local<v8::Value> value = o->Get( name ); + + const string sname = toSTLString( name ); + if ( depth == 0 && sname == "_id" ) + continue; + + v8ToMongoElement( b , sname , value , depth + 1, &originalBSON ); + } + return b.obj(); + } + + // --- random utils ---- + + v8::Function * V8Scope::getNamedCons( const char * name ) { + return v8::Function::Cast( *(v8::Context::GetCurrent()->Global()->Get( getV8Str( name ) ) ) ); + } + + v8::Function * V8Scope::getObjectIdCons() { + return getNamedCons( "ObjectId" ); + } + + Handle<v8::Value> V8Scope::Print(V8Scope* scope, const Arguments& args) { + bool first = true; + for (int i = 0; i < args.Length(); i++) { + HandleScope handle_scope; + if (first) { + first = false; + } + else { + printf(" "); + } + v8::String::Utf8Value str(args[i]); + printf("%s", *str); + } + printf("\n"); + return v8::Undefined(); + } + + Handle<v8::Value> V8Scope::Version(V8Scope* scope, const Arguments& args) { + HandleScope handle_scope; + return handle_scope.Close( v8::String::New(v8::V8::GetVersion()) ); + } + + Handle<v8::Value> V8Scope::GCV8(V8Scope* scope, const Arguments& args) { + V8Lock l; + v8::V8::LowMemoryNotification(); + return v8::Undefined(); + } + + /** + * Gets a V8 strings from the scope's cache, creating one if needed + */ + v8::Handle<v8::String> V8Scope::getV8Str(string str) { + Persistent<v8::String> ptr = _strCache[str]; + if (ptr.IsEmpty()) { + ptr = Persistent<v8::String>::New(v8::String::New(str.c_str())); + _strCache[str] = ptr; +// cout << "Adding str " + str << endl; + } +// cout << "Returning str " + str << endl; + return ptr; + } + + // to be called with v8 mutex + void V8Scope::enableV8Interrupt() { + v8Locks::InterruptLock l; + if ( globalScriptEngine->haveGetInterruptSpecCallback() ) { + unsigned op = globalScriptEngine->getInterruptSpec(); + __interruptSpecToThreadId[ op ] = v8::V8::GetCurrentThreadId(); + __interruptSpecToIsolate[ op ] = _isolate; + } + } + + // to be called with v8 mutex + void V8Scope::disableV8Interrupt() { + v8Locks::InterruptLock l; + if ( globalScriptEngine->haveGetInterruptSpecCallback() ) { + unsigned op = globalScriptEngine->getInterruptSpec(); + __interruptSpecToIsolate.erase( op ); + __interruptSpecToThreadId.erase( op ); + } + } + + // to be called with v8 mutex + bool V8Scope::pauseV8Interrupt() { + v8Locks::InterruptLock l; + if ( globalScriptEngine->haveGetInterruptSpecCallback() ) { + unsigned op = globalScriptEngine->getInterruptSpec(); + int thread = __interruptSpecToThreadId[ op ]; + if ( thread == -2 || thread == -3) { + // already paused + return false; + } + __interruptSpecToThreadId[ op ] = -2; + } + return true; + } + + // to be called with v8 mutex + bool V8Scope::resumeV8Interrupt() { + v8Locks::InterruptLock l; + if ( globalScriptEngine->haveGetInterruptSpecCallback() ) { + unsigned op = globalScriptEngine->getInterruptSpec(); + if (__interruptSpecToThreadId[ op ] == -3) { + // was interrupted + return false; + } + __interruptSpecToThreadId[ op ] = v8::V8::GetCurrentThreadId(); + } + return true; + } + +} // namespace mongo diff --git a/src/mongo/scripting/engine_v8.h b/src/mongo/scripting/engine_v8.h new file mode 100644 index 00000000000..2991022421b --- /dev/null +++ b/src/mongo/scripting/engine_v8.h @@ -0,0 +1,269 @@ +//engine_v8.h + +/* Copyright 2009 10gen Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include <vector> +#include "engine.h" +#include <v8.h> + +using namespace v8; + +namespace mongo { + + class V8ScriptEngine; + class V8Scope; + + typedef Handle< Value > (*v8Function) ( V8Scope* scope, const v8::Arguments& args ); + + // Preemption is going to be allowed for the v8 mutex, and some of our v8 + // usage is not preemption safe. So we are using an additional mutex that + // will not be preempted. The V8Lock should be used in place of v8::Locker + // except in certain special cases involving interrupts. + namespace v8Locks { + struct InterruptLock { + InterruptLock(); + ~InterruptLock(); + }; + + // the implementations are quite simple - objects must be destroyed in + // reverse of the order created, and should not be shared between threads + struct RecursiveLock { + RecursiveLock(); + ~RecursiveLock(); + bool _unlock; + }; + struct RecursiveUnlock { + RecursiveUnlock(); + ~RecursiveUnlock(); + bool _lock; + }; + } // namespace v8Locks + + class V8Lock { + public: + V8Lock() : _preemptionLock(Isolate::GetCurrent()){} + + private: + v8Locks::RecursiveLock _noPreemptionLock; + v8::Locker _preemptionLock; + }; + + struct V8Unlock { + public: + V8Unlock() : _preemptionUnlock(Isolate::GetCurrent()){} + + private: + v8::Unlocker _preemptionUnlock; + v8Locks::RecursiveUnlock _noPreemptionUnlock; + }; + + class BSONHolder { + public: + + BSONHolder( BSONObj obj ) { + _obj = obj.getOwned(); + _modified = false; + } + + ~BSONHolder() { + } + + BSONObj _obj; + bool _modified; + list<string> _extra; + set<string> _removed; + + }; + + class V8Scope : public Scope { + public: + + V8Scope( V8ScriptEngine * engine ); + ~V8Scope(); + + virtual void reset(); + virtual void init( const BSONObj * data ); + + virtual void localConnect( const char * dbName ); + virtual void externalSetup(); + + v8::Handle<v8::Value> get( const char * field ); // caller must create context and handle scopes + virtual double getNumber( const char *field ); + virtual int getNumberInt( const char *field ); + virtual long long getNumberLongLong( const char *field ); + virtual string getString( const char *field ); + virtual bool getBoolean( const char *field ); + virtual BSONObj getObject( const char *field ); + Handle<v8::Object> getGlobalObject() { return _global; }; + + virtual int type( const char *field ); + + virtual void setNumber( const char *field , double val ); + virtual void setString( const char *field , const char * val ); + virtual void setBoolean( const char *field , bool val ); + virtual void setElement( const char *field , const BSONElement& e ); + virtual void setObject( const char *field , const BSONObj& obj , bool readOnly); + virtual void setFunction( const char *field , const char * code ); +// virtual void setThis( const BSONObj * obj ); + + virtual void rename( const char * from , const char * to ); + + virtual ScriptingFunction _createFunction( const char * code ); + Local< v8::Function > __createFunction( const char * code ); + virtual int invoke( ScriptingFunction func , const BSONObj* args, const BSONObj* recv, int timeoutMs = 0 , bool ignoreReturn = false, bool readOnlyArgs = false, bool readOnlyRecv = false ); + virtual bool exec( const StringData& code , const string& name , bool printResult , bool reportError , bool assertOnError, int timeoutMs ); + virtual string getError() { return _error; } + virtual bool hasOutOfMemoryException(); + + virtual void injectNative( const char *field, NativeFunction func, void* data = 0 ); + void injectNative( const char *field, NativeFunction func, Handle<v8::Object>& obj, void* data = 0 ); + void injectV8Function( const char *field, v8Function func ); + void injectV8Function( const char *field, v8Function func, Handle<v8::Object>& obj ); + void injectV8Function( const char *field, v8Function func, Handle<v8::Template>& t ); + Handle<v8::FunctionTemplate> createV8Function( v8Function func ); + + void gc(); + + Handle< Context > context() const { return _context; } + + v8::Local<v8::Object> mongoToV8( const mongo::BSONObj & m , bool array = 0 , bool readOnly = false ); + v8::Handle<v8::Object> mongoToLZV8( const mongo::BSONObj & m , bool array = 0 , bool readOnly = false ); + mongo::BSONObj v8ToMongo( v8::Handle<v8::Object> o , int depth = 0 ); + + void v8ToMongoElement( BSONObjBuilder & b , const string sname , v8::Handle<v8::Value> value , int depth = 0, BSONObj* originalParent=0 ); + v8::Handle<v8::Value> mongoToV8Element( const BSONElement &f, bool readOnly = false ); + virtual void append( BSONObjBuilder & builder , const char * fieldName , const char * scopeName ); + + v8::Function * getNamedCons( const char * name ); + v8::Function * getObjectIdCons(); + Local< v8::Value > newId( const OID &id ); + + Persistent<v8::Object> wrapBSONObject(Local<v8::Object> obj, BSONHolder* data); + Persistent<v8::Object> wrapArrayObject(Local<v8::Object> obj, char* data); + + v8::Handle<v8::String> getV8Str(string str); +// inline v8::Handle<v8::String> getV8Str(string str) { return v8::String::New(str.c_str()); } + inline v8::Handle<v8::String> getLocalV8Str(string str) { return v8::String::New(str.c_str()); } + + v8::Isolate* getIsolate() { return _isolate; } + Persistent<Context> getContext() { return _context; } + + // call with v8 mutex: + void enableV8Interrupt(); + void disableV8Interrupt(); + bool pauseV8Interrupt(); + bool resumeV8Interrupt(); + + Handle<v8::String> V8STR_CONN; + Handle<v8::String> V8STR_ID; + Handle<v8::String> V8STR_LENGTH; + Handle<v8::String> V8STR_LEN; + Handle<v8::String> V8STR_TYPE; + Handle<v8::String> V8STR_ISOBJECTID; + Handle<v8::String> V8STR_NATIVE_FUNC; + Handle<v8::String> V8STR_NATIVE_DATA; + Handle<v8::String> V8STR_V8_FUNC; + Handle<v8::String> V8STR_RETURN; + Handle<v8::String> V8STR_ARGS; + Handle<v8::String> V8STR_T; + Handle<v8::String> V8STR_I; + Handle<v8::String> V8STR_EMPTY; + Handle<v8::String> V8STR_MINKEY; + Handle<v8::String> V8STR_MAXKEY; + Handle<v8::String> V8STR_NUMBERLONG; + Handle<v8::String> V8STR_NUMBERINT; + Handle<v8::String> V8STR_DBPTR; + Handle<v8::String> V8STR_BINDATA; + Handle<v8::String> V8STR_WRAPPER; + Handle<v8::String> V8STR_RO; + Handle<v8::String> V8STR_FULLNAME; + Handle<v8::String> V8STR_BSON; + + private: + void _startCall(); + + static Handle< Value > nativeCallback( V8Scope* scope, const Arguments &args ); + static v8::Handle< v8::Value > v8Callback( const v8::Arguments &args ); + static Handle< Value > load( V8Scope* scope, const Arguments &args ); + static Handle< Value > Print(V8Scope* scope, const v8::Arguments& args); + static Handle< Value > Version(V8Scope* scope, const v8::Arguments& args); + static Handle< Value > GCV8(V8Scope* scope, const v8::Arguments& args); + + + V8ScriptEngine * _engine; + + Persistent<Context> _context; + Persistent<v8::Object> _global; + + string _error; + vector< Persistent<Value> > _funcs; + v8::Persistent<v8::Object> _emptyObj; + + v8::Persistent<v8::Function> _wrapper; + + enum ConnectState { NOT , LOCAL , EXTERNAL }; + ConnectState _connectState; + + std::map <string, v8::Persistent <v8::String> > _strCache; + + Persistent<v8::FunctionTemplate> lzFunctionTemplate; + Persistent<v8::ObjectTemplate> lzObjectTemplate; + Persistent<v8::ObjectTemplate> roObjectTemplate; + Persistent<v8::ObjectTemplate> lzArrayTemplate; + Persistent<v8::ObjectTemplate> internalFieldObjects; + v8::Isolate* _isolate; + }; + + class V8ScriptEngine : public ScriptEngine { + public: + V8ScriptEngine(); + virtual ~V8ScriptEngine(); + + virtual Scope * createScope() { return new V8Scope( this ); } + + virtual void runTest() {} + + bool utf8Ok() const { return true; } + + virtual void interrupt( unsigned opSpec ); + virtual void interruptAll(); + + private: + friend class V8Scope; + }; + + class ExternalString : public v8::String::ExternalAsciiStringResource { + public: + ExternalString(std::string str) : _data(str) { + } + + ~ExternalString() { + } + + const char* data () const { return _data.c_str(); } + size_t length () const { return _data.length(); } + private: +// string _str; +// const char* _data; + std::string _data; +// size_t _len; + }; + + extern ScriptEngine * globalScriptEngine; + +} diff --git a/src/mongo/scripting/sm_db.cpp b/src/mongo/scripting/sm_db.cpp new file mode 100644 index 00000000000..e7b547b8df8 --- /dev/null +++ b/src/mongo/scripting/sm_db.cpp @@ -0,0 +1,1716 @@ +// sm_db.cpp + +/* Copyright 2009 10gen Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +// hacked in right now from engine_spidermonkey.cpp + +#include "mongo/client/dbclientcursor.h" +#include "mongo/db/namespacestring.h" +#include "mongo/util/base64.h" +#include "mongo/util/text.h" + +#if( BOOST_VERSION >= 104200 ) +#define HAVE_UUID 1 +#endif + +namespace mongo { + + bool haveLocalShardingInfo( const string& ns ); + + // ------------ some defs needed --------------- + + JSObject * doCreateCollection( JSContext * cx , JSObject * db , const string& shortName ); + + // ------------ utils ------------------ + + + bool isSpecialName( const string& name ) { + static set<string> names; + if ( names.size() == 0 ) { + names.insert( "tojson" ); + names.insert( "toJson" ); + names.insert( "toString" ); + } + + if ( name.length() == 0 ) + return false; + + if ( name[0] == '_' ) + return true; + + return names.count( name ) > 0; + } + + + // ------ cursor ------ + + class CursorHolder { + public: + CursorHolder( auto_ptr< DBClientCursor > &cursor, const shared_ptr< DBClientWithCommands > &connection ) : + connection_( connection ), + cursor_( cursor ) { + verify( cursor_.get() ); + } + DBClientCursor *get() const { return cursor_.get(); } + private: + shared_ptr< DBClientWithCommands > connection_; + auto_ptr< DBClientCursor > cursor_; + }; + + DBClientCursor *getCursor( JSContext *cx, JSObject *obj ) { + CursorHolder * holder = (CursorHolder*)JS_GetPrivate( cx , obj ); + uassert( 10235 , "no cursor!" , holder ); + return holder->get(); + } + + JSBool internal_cursor_constructor( JSContext *cx, JSObject *obj, uintN argc, jsval *argv, jsval *rval ) { + try { + uassert( 10236 , "no args to internal_cursor_constructor" , argc == 0 ); + verify( JS_SetPrivate( cx , obj , 0 ) ); // just for safety + } + catch ( const AssertionException& e ) { + if ( ! JS_IsExceptionPending( cx ) ) { + JS_ReportError( cx, e.what() ); + } + return JS_FALSE; + } + catch ( const std::exception& e ) { + log() << "unhandled exception: " << e.what() << ", throwing Fatal Assertion" << endl; + fassertFailed( 16288 ); + } + return JS_TRUE; + } + + void internal_cursor_finalize( JSContext * cx , JSObject * obj ) { + try { + CursorHolder * holder = (CursorHolder*)JS_GetPrivate( cx , obj ); + if ( holder ) { + delete holder; + verify( JS_SetPrivate( cx , obj , 0 ) ); + } + } + catch ( const AssertionException& e ) { + if ( ! JS_IsExceptionPending( cx ) ) { + JS_ReportError( cx, e.what() ); + } + } + catch ( const std::exception& e ) { + log() << "unhandled exception: " << e.what() << ", throwing Fatal Assertion" << endl; + fassertFailed( 16289 ); + } + } + + JSBool internal_cursor_hasNext(JSContext *cx, JSObject *obj, uintN argc, jsval *argv, jsval *rval) { + try { + DBClientCursor *cursor = getCursor( cx, obj ); + *rval = cursor->more() ? JSVAL_TRUE : JSVAL_FALSE; + } + catch ( const AssertionException& e ) { + if ( ! JS_IsExceptionPending( cx ) ) { + JS_ReportError( cx, e.what() ); + } + return JS_FALSE; + } + catch ( const std::exception& e ) { + log() << "unhandled exception: " << e.what() << ", throwing Fatal Assertion" << endl; + fassertFailed( 16290 ); + } + return JS_TRUE; + } + + JSBool internal_cursor_objsLeftInBatch(JSContext *cx, JSObject *obj, uintN argc, jsval *argv, jsval *rval) { + try { + DBClientCursor *cursor = getCursor( cx, obj ); + Convertor c(cx); + *rval = c.toval((double) cursor->objsLeftInBatch() ); + } + catch ( const AssertionException& e ) { + if ( ! JS_IsExceptionPending( cx ) ) { + JS_ReportError( cx, e.what() ); + } + return JS_FALSE; + } + catch ( const std::exception& e ) { + log() << "unhandled exception: " << e.what() << ", throwing Fatal Assertion" << endl; + fassertFailed( 16291 ); + } + return JS_TRUE; + } + + JSBool internal_cursor_next(JSContext *cx, JSObject *obj, uintN argc, jsval *argv, jsval *rval) { + try { + DBClientCursor *cursor = getCursor( cx, obj ); + if ( ! cursor->more() ) { + JS_ReportError( cx , "cursor at the end" ); + return JS_FALSE; + } + + BSONObj n = cursor->next(); + Convertor c(cx); + *rval = c.toval( &n ); + } + catch ( const AssertionException& e ) { + if ( ! JS_IsExceptionPending( cx ) ) { + JS_ReportError( cx, e.what() ); + } + return JS_FALSE; + } + catch ( const std::exception& e ) { + log() << "unhandled exception: " << e.what() << ", throwing Fatal Assertion" << endl; + fassertFailed( 16292 ); + } + return JS_TRUE; + } + + JSFunctionSpec internal_cursor_functions[] = { + { "hasNext" , internal_cursor_hasNext , 0 , JSPROP_READONLY | JSPROP_PERMANENT, 0 } , + { "objsLeftInBatch" , internal_cursor_objsLeftInBatch , 0 , JSPROP_READONLY | JSPROP_PERMANENT, 0 } , + { "next" , internal_cursor_next , 0 , JSPROP_READONLY | JSPROP_PERMANENT, 0 } , + { 0 } + }; + + JSClass internal_cursor_class = { + "InternalCursor", // class name + JSCLASS_HAS_PRIVATE, // flags + JS_PropertyStub, // addProperty + JS_PropertyStub, // delProperty + JS_PropertyStub, // getProperty + JS_PropertyStub, // setProperty + JS_EnumerateStub, // enumerate + JS_ResolveStub, // resolve + JS_ConvertStub, // convert + internal_cursor_finalize, // finalize + JSCLASS_NO_OPTIONAL_MEMBERS // optional members + }; + + + // ------ mongo stuff ------ + + JSBool mongo_local_constructor( JSContext *cx, JSObject *obj, uintN argc, jsval *argv, jsval *rval ) { + try { + shared_ptr< DBClientWithCommands > client( createDirectClient() ); + verify( JS_SetPrivate( cx , obj , (void*)( new shared_ptr< DBClientWithCommands >( client ) ) ) ); + + Convertor c( cx ); + jsval host = c.toval( "EMBEDDED" ); + verify( JS_SetProperty( cx , obj , "host" , &host ) ); + } + catch ( const AssertionException& e ) { + if ( ! JS_IsExceptionPending( cx ) ) { + JS_ReportError( cx, e.what() ); + } + return JS_FALSE; + } + catch ( const std::exception& e ) { + log() << "unhandled exception: " << e.what() << ", throwing Fatal Assertion" << endl; + fassertFailed( 16293 ); + } + return JS_TRUE; + } + + JSBool mongo_external_constructor( JSContext *cx, JSObject *obj, uintN argc, jsval *argv, jsval *rval ) { + try { + smuassert( cx , "0 or 1 args to Mongo" , argc <= 1 ); + + string host( "127.0.0.1" ); + Convertor c( cx ); + if ( argc > 0 ) + host = c.toString( argv[0] ); + + string errmsg; + ConnectionString cs = ConnectionString::parse( host , errmsg ); + if ( ! cs.isValid() ) { + JS_ReportError( cx , errmsg.c_str() ); + return JS_FALSE; + } + + shared_ptr< DBClientWithCommands > conn( cs.connect( errmsg ) ); + if ( ! conn ) { + JS_ReportError( cx , errmsg.c_str() ); + return JS_FALSE; + } + + try { + ScriptEngine::runConnectCallback( *conn ); + } + catch ( const AssertionException& e ){ + // Can happen if connection goes down while we're starting up here + // Catch so that we don't get a hard-to-trace segfault from SM + JS_ReportError( cx, ((string)( str::stream() << "Error during mongo startup." << causedBy( e ) )).c_str() ); + return JS_FALSE; + } + catch ( const std::exception& e ) { + log() << "unhandled exception: " << e.what() << ", throwing Fatal Assertion" << endl; + fassertFailed( 16294 ); + } + + verify( JS_SetPrivate( cx , obj , (void*)( new shared_ptr< DBClientWithCommands >( conn ) ) ) ); + jsval host_val = c.toval( host.c_str() ); + verify( JS_SetProperty( cx , obj , "host" , &host_val ) ); + } + catch ( const AssertionException& e ) { + if ( ! JS_IsExceptionPending( cx ) ) { + JS_ReportError( cx, e.what() ); + } + return JS_FALSE; + } + catch ( const std::exception& e ) { + log() << "unhandled exception: " << e.what() << ", throwing Fatal Assertion" << endl; + fassertFailed( 16295 ); + } + return JS_TRUE; + } + + DBClientWithCommands *getConnection( JSContext *cx, JSObject *obj ) { + shared_ptr< DBClientWithCommands > * connHolder = (shared_ptr< DBClientWithCommands >*)JS_GetPrivate( cx , obj ); + uassert( 10239 , "no connection!" , connHolder && connHolder->get() ); + return connHolder->get(); + } + + void mongo_finalize( JSContext * cx , JSObject * obj ) { + try { + shared_ptr< DBClientWithCommands > * connHolder = (shared_ptr< DBClientWithCommands >*)JS_GetPrivate( cx , obj ); + if ( connHolder ) { + delete connHolder; + verify( JS_SetPrivate( cx , obj , 0 ) ); + } + } + catch ( const AssertionException& e ) { + if ( ! JS_IsExceptionPending( cx ) ) { + JS_ReportError( cx, e.what() ); + } + } + catch ( const std::exception& e ) { + log() << "unhandled exception: " << e.what() << ", throwing Fatal Assertion" << endl; + fassertFailed( 16296 ); + } + } + + JSClass mongo_class = { + "Mongo", // class name + JSCLASS_HAS_PRIVATE | JSCLASS_NEW_RESOLVE, // flags + JS_PropertyStub, // addProperty + JS_PropertyStub, // delProperty + JS_PropertyStub, // getProperty + JS_PropertyStub, // setProperty + JS_EnumerateStub, // enumerate + JS_ResolveStub, // resolve + JS_ConvertStub, // convert + mongo_finalize, // finalize + JSCLASS_NO_OPTIONAL_MEMBERS // optional members + }; + + JSBool mongo_auth(JSContext *cx, JSObject *obj, uintN argc, jsval *argv, jsval *rval) { + try { + smuassert( cx , "mongo_auth needs 3 args" , argc == 3 ); + shared_ptr< DBClientWithCommands > * connHolder = (shared_ptr< DBClientWithCommands >*)JS_GetPrivate( cx , obj ); + smuassert( cx , "no connection!" , connHolder && connHolder->get() ); + DBClientWithCommands *conn = connHolder->get(); + + Convertor c( cx ); + + string db = c.toString( argv[0] ); + string username = c.toString( argv[1] ); + string password = c.toString( argv[2] ); + string errmsg = ""; + + if ( ! conn->auth( db, username, password, errmsg ) ) { + JS_ReportError( cx, errmsg.c_str() ); + return JS_FALSE; + } + } + catch ( const AssertionException& e ) { + if ( ! JS_IsExceptionPending( cx ) ) { + JS_ReportError( cx, e.what() ); + } + return JS_FALSE; + } + catch ( const SocketException& e ) { + if ( ! JS_IsExceptionPending( cx ) ) { + JS_ReportError( cx, e.toString().c_str() ); + } + return JS_FALSE; + } + catch ( const std::exception& e ) { + log() << "unhandled exception: " << e.what() << ", throwing Fatal Assertion" << endl; + fassertFailed( 16297 ); + } + return JS_TRUE; + } + + JSBool mongo_logout(JSContext *context, JSObject *obj, uintN argc, jsval *argv, jsval *rval) { + try { + smuassert(context, "mongo_logout needs 1 arg" , argc == 1); + shared_ptr<DBClientWithCommands>* connHolder = + reinterpret_cast<shared_ptr<DBClientWithCommands>*>( + JS_GetPrivate(context , obj)); + smuassert(context, "no connection!", connHolder && connHolder->get()); + DBClientWithCommands *conn = connHolder->get(); + + Convertor convertor(context); + string db = convertor.toString(argv[0]); + + BSONObj ret; + conn->logout(db, ret); + *rval = convertor.toval(&ret); + } + catch (const AssertionException& e) { + if (!JS_IsExceptionPending(context)) { + JS_ReportError(context, e.what()); + } + return JS_FALSE; + } + catch (const SocketException& e) { + if (!JS_IsExceptionPending(context)) { + JS_ReportError(context, e.toString().c_str()); + } + return JS_FALSE; + } + catch (const std::exception& e) { + log() << "unhandled exception: " << e.what() << ", throwing Fatal Assertion" << endl; + fassertFailed(16396); + } + + return JS_TRUE; + } + + JSBool mongo_find(JSContext *cx, JSObject *obj, uintN argc, jsval *argv, jsval *rval) { + try { + smuassert( cx , "mongo_find needs 7 args" , argc == 7 ); + shared_ptr< DBClientWithCommands > * connHolder = (shared_ptr< DBClientWithCommands >*)JS_GetPrivate( cx , obj ); + smuassert( cx , "no connection!" , connHolder && connHolder->get() ); + DBClientWithCommands *conn = connHolder->get(); + + Convertor c( cx ); + + string ns = c.toString( argv[0] ); + + BSONObj q = c.toObject( argv[1] ); + BSONObj f = c.toObject( argv[2] ); + + int nToReturn = (int) c.toNumber( argv[3] ); + int nToSkip = (int) c.toNumber( argv[4] ); + int batchSize = (int) c.toNumber( argv[5] ); + int options = (int)c.toNumber( argv[6] ); + + auto_ptr<DBClientCursor> cursor = conn->query( ns , q , nToReturn , nToSkip , f.nFields() ? &f : 0 , options , batchSize ); + if ( ! cursor.get() ) { + log() << "query failed : " << ns << " " << q << " to: " << conn->toString() << endl; + JS_ReportError( cx , "error doing query: failed" ); + return JS_FALSE; + } + JSObject * mycursor = JS_NewObject( cx , &internal_cursor_class , 0 , 0 ); + CHECKNEWOBJECT( mycursor, cx, "internal_cursor_class" ); + verify( JS_SetPrivate( cx , mycursor , new CursorHolder( cursor, *connHolder ) ) ); + *rval = OBJECT_TO_JSVAL( mycursor ); + } + catch ( const AssertionException& e ) { + if ( ! JS_IsExceptionPending( cx ) ) { + JS_ReportError( cx, e.what() ); + } + return JS_FALSE; + } + catch ( const SocketException& e ) { + if ( ! JS_IsExceptionPending( cx ) ) { + JS_ReportError( cx, e.toString().c_str() ); + } + return JS_FALSE; + } + catch ( const std::exception& e ) { + log() << "unhandled exception: " << e.what() << ", throwing Fatal Assertion" << endl; + fassertFailed( 16298 ); + } + return JS_TRUE; + } + + JSBool mongo_update(JSContext *cx, JSObject *obj, uintN argc, jsval *argv, jsval *rval) { + try { + smuassert( cx , "mongo_update needs at least 3 args" , argc >= 3 ); + smuassert( cx , "2nd param to update has to be an object" , JSVAL_IS_OBJECT( argv[1] ) ); + smuassert( cx , "3rd param to update has to be an object" , JSVAL_IS_OBJECT( argv[2] ) ); + + Convertor c( cx ); + if ( c.getBoolean( obj , "readOnly" ) ) { + JS_ReportError( cx , "js db in read only mode - mongo_update" ); + return JS_FALSE; + } + + DBClientWithCommands * conn = getConnection( cx, obj ); + uassert( 10245 , "no connection!" , conn ); + + string ns = c.toString( argv[0] ); + + bool upsert = argc > 3 && c.toBoolean( argv[3] ); + bool multi = argc > 4 && c.toBoolean( argv[4] ); + + conn->update( ns , c.toObject( argv[1] ) , c.toObject( argv[2] ) , upsert , multi ); + } + catch ( const AssertionException& e ) { + if ( ! JS_IsExceptionPending( cx ) ) { + JS_ReportError( cx, e.what() ); + } + return JS_FALSE; + } + catch ( const SocketException& e ) { + if ( ! JS_IsExceptionPending( cx ) ) { + JS_ReportError( cx, e.toString().c_str() ); + } + return JS_FALSE; + } + catch ( const std::exception& e ) { + log() << "unhandled exception: " << e.what() << ", throwing Fatal Assertion" << endl; + fassertFailed( 16299 ); + } + return JS_TRUE; + } + + JSBool mongo_insert(JSContext *cx, JSObject *obj, uintN argc, jsval *argv, jsval *rval) { + try { + smuassert( cx , "mongo_insert needs 2 args" , argc == 2 ); + smuassert( cx , "2nd param to insert has to be an object" , JSVAL_IS_OBJECT( argv[1] ) ); + + Convertor c( cx ); + if ( c.getBoolean( obj , "readOnly" ) ) { + JS_ReportError( cx , "js db in read only mode - mongo_insert" ); + return JS_FALSE; + } + + DBClientWithCommands * conn = getConnection( cx, obj ); + uassert( 10248 , "no connection!" , conn ); + + string ns = c.toString( argv[0] ); + + JSObject * insertObj = JSVAL_TO_OBJECT( argv[1] ); + + if( JS_IsArrayObject( cx, insertObj ) ){ + vector<BSONObj> bos; + + jsuint len; + JSBool gotLen = JS_GetArrayLength( cx, insertObj, &len ); + smuassert( cx, "could not get length of array", gotLen ); + + for( jsuint i = 0; i < len; i++ ){ + + jsval el; + JSBool inserted = JS_GetElement( cx, insertObj, i, &el); + smuassert( cx, "could not find element in array object", inserted ); + + bos.push_back( c.toObject( el ) ); + } + + conn->insert( ns, bos ); + } + else { + BSONObj o = c.toObject( argv[1] ); + // TODO: add _id + + conn->insert( ns , o ); + } + } + catch ( const AssertionException& e ) { + if ( ! JS_IsExceptionPending( cx ) ) { + JS_ReportError( cx, e.what() ); + } + return JS_FALSE; + } + catch ( const SocketException& e ) { + if ( ! JS_IsExceptionPending( cx ) ) { + JS_ReportError( cx, e.toString().c_str() ); + } + return JS_FALSE; + } + catch ( const std::exception& e ) { + log() << "unhandled exception: " << e.what() << ", throwing Fatal Assertion" << endl; + fassertFailed( 16300 ); + } + return JS_TRUE; + } + + JSBool mongo_remove(JSContext *cx, JSObject *obj, uintN argc, jsval *argv, jsval *rval) { + try { + smuassert( cx , "mongo_remove needs 2 or 3 arguments" , argc == 2 || argc == 3 ); + smuassert( cx , "2nd param to insert has to be an object" , JSVAL_IS_OBJECT( argv[1] ) ); + + Convertor c( cx ); + if ( c.getBoolean( obj , "readOnly" ) ) { + JS_ReportError( cx , "js db in read only mode - mongo_remove" ); + return JS_FALSE; + } + + DBClientWithCommands * conn = getConnection( cx, obj ); + uassert( 10251 , "no connection!" , conn ); + + string ns = c.toString( argv[0] ); + BSONObj o = c.toObject( argv[1] ); + bool justOne = false; + if ( argc > 2 ) + justOne = c.toBoolean( argv[2] ); + + conn->remove( ns , o , justOne ); + } + catch ( const AssertionException& e ) { + if ( ! JS_IsExceptionPending( cx ) ) { + JS_ReportError( cx, e.what() ); + } + return JS_FALSE; + } + catch ( const SocketException& e ) { + if ( ! JS_IsExceptionPending( cx ) ) { + JS_ReportError( cx, e.toString().c_str() ); + } + return JS_FALSE; + } + catch ( const std::exception& e ) { + log() << "unhandled exception: " << e.what() << ", throwing Fatal Assertion" << endl; + fassertFailed( 16301 ); + } + return JS_TRUE; + } + + JSFunctionSpec mongo_functions[] = { + { "auth" , mongo_auth , 0 , JSPROP_READONLY | JSPROP_PERMANENT, 0 } , + { "logout", mongo_logout, 0, JSPROP_READONLY | JSPROP_PERMANENT, 0 }, + { "find" , mongo_find , 0 , JSPROP_READONLY | JSPROP_PERMANENT, 0 } , + { "update" , mongo_update , 0 , JSPROP_READONLY | JSPROP_PERMANENT, 0 } , + { "insert" , mongo_insert , 0 , JSPROP_READONLY | JSPROP_PERMANENT, 0 } , + { "remove" , mongo_remove , 0 , JSPROP_READONLY | JSPROP_PERMANENT, 0 } , + { 0 } + }; + + // ------------- db_collection ------------- + + JSBool db_collection_constructor( JSContext *cx, JSObject *obj, uintN argc, jsval *argv, jsval *rval ) { + try { + smuassert( cx , "db_collection_constructor wrong args" , argc == 4 ); + Convertor c(cx); + if ( haveLocalShardingInfo( c.toString( argv[3] ) ) ) { + JS_ReportError( cx , "can't use sharded collection from db.eval" ); + return JS_FALSE; + } + + verify( JS_SetProperty( cx , obj , "_mongo" , &(argv[0]) ) ); + verify( JS_SetProperty( cx , obj , "_db" , &(argv[1]) ) ); + verify( JS_SetProperty( cx , obj , "_shortName" , &(argv[2]) ) ); + verify( JS_SetProperty( cx , obj , "_fullName" , &(argv[3]) ) ); + } + catch ( const AssertionException& e ) { + if ( ! JS_IsExceptionPending( cx ) ) { + JS_ReportError( cx, e.what() ); + } + return JS_FALSE; + } + catch ( const std::exception& e ) { + log() << "unhandled exception: " << e.what() << ", throwing Fatal Assertion" << endl; + fassertFailed( 16302 ); + } + return JS_TRUE; + } + + JSBool db_collection_resolve( JSContext *cx, JSObject *obj, jsval id, uintN flags, JSObject **objp ) { + try { + if ( flags & JSRESOLVE_ASSIGNING ) + return JS_TRUE; + + Convertor c( cx ); + string collname = c.toString( id ); + + if ( isSpecialName( collname ) ) + return JS_TRUE; + + if ( obj == c.getGlobalPrototype( "DBCollection" ) ) + return JS_TRUE; + + JSObject * proto = JS_GetPrototype( cx , obj ); + if ( c.hasProperty( obj , collname.c_str() ) || ( proto && c.hasProperty( proto , collname.c_str() ) ) ) + return JS_TRUE; + + string name = c.toString( c.getProperty( obj , "_shortName" ) ); + name += "."; + name += collname; + + jsval db = c.getProperty( obj , "_db" ); + if ( ! JSVAL_IS_OBJECT( db ) ) + return JS_TRUE; + + JSObject * coll = doCreateCollection( cx , JSVAL_TO_OBJECT( db ) , name ); + if ( ! coll ) + return JS_FALSE; + c.setProperty( obj , collname.c_str() , OBJECT_TO_JSVAL( coll ) ); + *objp = obj; + } + catch ( const AssertionException& e ) { + if ( ! JS_IsExceptionPending( cx ) ) { + JS_ReportError( cx, e.what() ); + } + return JS_FALSE; + } + catch ( const std::exception& e ) { + log() << "unhandled exception: " << e.what() << ", throwing Fatal Assertion" << endl; + fassertFailed( 16303 ); + } + return JS_TRUE; + } + + JSClass db_collection_class = { + "DBCollection", // class name + JSCLASS_HAS_PRIVATE | JSCLASS_NEW_RESOLVE, // flags + JS_PropertyStub, // addProperty + JS_PropertyStub, // delProperty + JS_PropertyStub, // getProperty + JS_PropertyStub, // setProperty + JS_EnumerateStub, // enumerate + (JSResolveOp)db_collection_resolve, // resolve + JS_ConvertStub, // convert + JS_FinalizeStub, // finalize + JSCLASS_NO_OPTIONAL_MEMBERS // optional members + }; + + + JSObject * doCreateCollection( JSContext * cx , JSObject * db , const string& shortName ) { + Convertor c(cx); + + verify( c.hasProperty( db , "_mongo" ) ); + verify( c.hasProperty( db , "_name" ) ); + + JSObject * coll = JS_NewObject( cx , &db_collection_class , 0 , 0 ); + CHECKNEWOBJECT( coll, cx, "doCreateCollection" ); + c.setProperty( coll , "_mongo" , c.getProperty( db , "_mongo" ) ); + c.setProperty( coll , "_db" , OBJECT_TO_JSVAL( db ) ); + c.setProperty( coll , "_shortName" , c.toval( shortName.c_str() ) ); + + string name = c.toString( c.getProperty( db , "_name" ) ); + name += "." + shortName; + c.setProperty( coll , "_fullName" , c.toval( name.c_str() ) ); + + if ( haveLocalShardingInfo( name ) ) { + JS_ReportError( cx , "can't use sharded collection from db.eval" ); + return 0; + } + + return coll; + } + + // -------------- DB --------------- + + + JSBool db_constructor( JSContext *cx, JSObject *obj, uintN argc, jsval *argv, jsval *rval ) { + try { + smuassert( cx, "wrong number of arguments to DB" , argc == 2 ); + + Convertor convertor( cx ); + string dbName = convertor.toString( argv[1] ); + string msg = str::stream() << "[" << dbName << "] is not a valid database name"; + smuassert( cx, msg.c_str(), NamespaceString::validDBName( dbName ) ); + + verify( JS_SetProperty( cx , obj , "_mongo" , &(argv[0]) ) ); + verify( JS_SetProperty( cx , obj , "_name" , &(argv[1]) ) ); + } + catch ( const AssertionException& e ) { + if ( ! JS_IsExceptionPending( cx ) ) { + JS_ReportError( cx, e.what() ); + } + return JS_FALSE; + } + catch ( const std::exception& e ) { + log() << "unhandled exception: " << e.what() << ", throwing Fatal Assertion" << endl; + fassertFailed( 16304 ); + } + return JS_TRUE; + } + + JSBool db_resolve( JSContext *cx, JSObject *obj, jsval id, uintN flags, JSObject **objp ) { + try { + if ( flags & JSRESOLVE_ASSIGNING ) + return JS_TRUE; + + Convertor c( cx ); + + if ( obj == c.getGlobalPrototype( "DB" ) ) + return JS_TRUE; + + string collname = c.toString( id ); + + if ( isSpecialName( collname ) ) + return JS_TRUE; + + JSObject * proto = JS_GetPrototype( cx , obj ); + if ( proto && c.hasProperty( proto , collname.c_str() ) ) + return JS_TRUE; + + JSObject * coll = doCreateCollection( cx , obj , collname ); + if ( ! coll ) + return JS_FALSE; + c.setProperty( obj , collname.c_str() , OBJECT_TO_JSVAL( coll ) ); + + *objp = obj; + } + catch ( const AssertionException& e ) { + if ( ! JS_IsExceptionPending( cx ) ) { + JS_ReportError( cx, e.what() ); + } + return JS_FALSE; + } + catch ( const std::exception& e ) { + log() << "unhandled exception: " << e.what() << ", throwing Fatal Assertion" << endl; + fassertFailed( 16305 ); + } + return JS_TRUE; + } + + JSClass db_class = { + "DB" , JSCLASS_HAS_PRIVATE | JSCLASS_NEW_RESOLVE , + JS_PropertyStub, JS_PropertyStub, JS_PropertyStub, JS_PropertyStub, + JS_EnumerateStub, (JSResolveOp)(&db_resolve) , JS_ConvertStub, JS_FinalizeStub, + JSCLASS_NO_OPTIONAL_MEMBERS + }; + + + // -------------- object id ------------- + + JSBool object_id_constructor( JSContext *cx, JSObject *obj, uintN argc, jsval *argv, jsval *rval ) { + try { + Convertor c( cx ); + + OID oid; + if ( argc == 0 ) { + oid.init(); + } + else { + smuassert( cx , "object_id_constructor can't take more than 1 param" , argc == 1 ); + string s = c.toString( argv[0] ); + + Scope::validateObjectIdString( s ); + oid.init( s ); + } + + if ( ! JS_InstanceOf( cx , obj , &object_id_class , 0 ) ) { + obj = JS_NewObject( cx , &object_id_class , 0 , 0 ); + CHECKNEWOBJECT( obj, cx, "object_id_constructor" ); + *rval = OBJECT_TO_JSVAL( obj ); + } + + jsval v = c.toval( oid.str().c_str() ); + verify( JS_SetProperty( cx , obj , "str" , &v ) ); + } + catch ( const AssertionException& e ) { + if ( ! JS_IsExceptionPending( cx ) ) { + JS_ReportError( cx, e.what() ); + } + return JS_FALSE; + } + catch ( const std::exception& e ) { + log() << "unhandled exception: " << e.what() << ", throwing Fatal Assertion" << endl; + fassertFailed( 16306 ); + } + return JS_TRUE; + } + + JSClass object_id_class = { + "ObjectId" , JSCLASS_HAS_PRIVATE , + JS_PropertyStub, JS_PropertyStub, JS_PropertyStub, JS_PropertyStub, + JS_EnumerateStub, JS_ResolveStub , JS_ConvertStub, JS_FinalizeStub, + JSCLASS_NO_OPTIONAL_MEMBERS + }; + + // dbpointer + + JSBool dbpointer_constructor( JSContext *cx, JSObject *obj, uintN argc, jsval *argv, jsval *rval ) { + try { + if ( argc != 2 ) { + JS_ReportError( cx, "DBPointer takes 2 arguments -- DBPointer(namespace,objectId)" ); + return JS_FALSE; + } + if ( ! JSVAL_IS_OID( argv[1] ) ) { + JS_ReportError( cx , "2nd argument to DBPointer must be objectId" ); + return JS_FALSE; + } + Convertor c( cx ); + if ( ! JS_InstanceOf( cx , obj , &dbpointer_class , 0 ) ) { + obj = JS_NewObject( cx , &dbpointer_class , 0 , 0 ); + CHECKNEWOBJECT( obj, cx, "dbpointer_constructor" ); + *rval = OBJECT_TO_JSVAL( obj ); + } + verify( JS_SetProperty( cx , obj , "ns" , &(argv[0]) ) ); + verify( JS_SetProperty( cx , obj , "id" , &(argv[1]) ) ); + } + catch ( const AssertionException& e ) { + if ( ! JS_IsExceptionPending( cx ) ) { + JS_ReportError( cx, e.what() ); + } + return JS_FALSE; + } + catch ( const std::exception& e ) { + log() << "unhandled exception: " << e.what() << ", throwing Fatal Assertion" << endl; + fassertFailed( 16307 ); + } + return JS_TRUE; + } + + JSClass dbpointer_class = { + "DBPointer" , JSCLASS_HAS_PRIVATE , + JS_PropertyStub, JS_PropertyStub, JS_PropertyStub, JS_PropertyStub, + JS_EnumerateStub, JS_ResolveStub , JS_ConvertStub, JS_FinalizeStub, + JSCLASS_NO_OPTIONAL_MEMBERS + }; + + JSFunctionSpec dbpointer_functions[] = { + { 0 } + }; + + + JSBool dbref_constructor( JSContext *cx, JSObject *obj, uintN argc, jsval *argv, jsval *rval ) { + try { + if ( argc != 2 ) { + JS_ReportError( cx , "DBRef takes 2 arguments -- DBRef($ref,$id)" ); + verify( JS_SetPrivate( cx , obj , (void*)(new BSONHolder( BSONObj().getOwned() ) ) ) ); + return JS_FALSE; + } + Convertor c( cx ); + if ( ! JS_InstanceOf( cx , obj , &dbref_class , 0 ) ) { + obj = JS_NewObject( cx , &dbref_class , 0 , 0 ); + CHECKNEWOBJECT( obj, cx, "dbref_constructor" ); + *rval = OBJECT_TO_JSVAL( obj ); + } + JSObject * o = JS_NewObject( cx , NULL , NULL, NULL ); + CHECKNEWOBJECT( o, cx, "dbref_constructor" ); + verify( JS_SetProperty( cx, o , "$ref" , &argv[ 0 ] ) ); + verify( JS_SetProperty( cx, o , "$id" , &argv[ 1 ] ) ); + BSONObj bo = c.toObject( o ); + verify( JS_SetPrivate( cx , obj , (void*)(new BSONHolder( bo.getOwned() ) ) ) ); + } + catch ( const AssertionException& e ) { + if ( ! JS_IsExceptionPending( cx ) ) { + JS_ReportError( cx, e.what() ); + } + return JS_FALSE; + } + catch ( const std::exception& e ) { + log() << "unhandled exception: " << e.what() << ", throwing Fatal Assertion" << endl; + fassertFailed( 16308 ); + } + return JS_TRUE; + } + + JSClass dbref_class = bson_class; // name will be fixed later + + // UUID ************************** + +#if 0 + JSBool uuid_constructor( JSContext *cx, JSObject *obj, uintN argc, jsval *argv, jsval *rval ) { + Convertor c( cx ); + + if( argc == 0 ) { +#if defined(HAVE_UUID) + //uuids::uuid +#else +#endif + JS_ReportError( cx , "UUID needs 1 argument -- UUID(hexstr)" ); + return JS_FALSE; + } + else if ( argc == 1 ) { + + string encoded = c.toString( argv[ 0 ] ); + if( encoded.size() != 32 ) { + JS_ReportError( cx, "expect 32 char hex string to UUID()" ); + return JS_FALSE; + } + + char buf[16]; + for( int i = 0; i < 16; i++ ) { + buf[i] = fromHex(encoded.c_str() + i * 2); + } + +zzz + + verify( JS_SetPrivate( cx, obj, new BinDataHolder( buf, 16 ) ) ); + c.setProperty( obj, "len", c.toval( (double)16 ) ); + c.setProperty( obj, "type", c.toval( (double)3 ) ); + + return JS_TRUE; + } + else { + JS_ReportError( cx , "UUID needs 1 argument -- UUID(hexstr)" ); + return JS_FALSE; + } + } + + JSBool uuid_tostring(JSContext *cx, JSObject *obj, uintN argc, jsval *argv, jsval *rval) { + Convertor c(cx); + void *holder = JS_GetPrivate( cx, obj ); + verify( holder ); + const char *data = ( ( BinDataHolder* )( holder ) )->c_; + stringstream ss; + ss << "UUID(\"" << toHex(data, 16); + ss << "\")"; + string ret = ss.str(); + return *rval = c.toval( ret.c_str() ); + } + + void uuid_finalize( JSContext * cx , JSObject * obj ) { + Convertor c(cx); + void *holder = JS_GetPrivate( cx, obj ); + if ( holder ) { + delete ( BinDataHolder* )holder; + verify( JS_SetPrivate( cx , obj , 0 ) ); + } + } + + JSClass uuid_class = { + "UUID" , JSCLASS_HAS_PRIVATE , + JS_PropertyStub, JS_PropertyStub, JS_PropertyStub, JS_PropertyStub, + JS_EnumerateStub, JS_ResolveStub , JS_ConvertStub, uuid_finalize, + JSCLASS_NO_OPTIONAL_MEMBERS + }; + + JSFunctionSpec uuid_functions[] = { + { "toString" , uuid_tostring , 0 , JSPROP_READONLY | JSPROP_PERMANENT, 0 } , + { 0 } + }; + +#endif + + // BinData ************************** + + JSBool bindata_constructor( JSContext *cx, JSObject *obj, uintN argc, jsval *argv, jsval *rval ) { + try { + if ( argc != 2 ) { + JS_ReportError( cx , "BinData takes 2 arguments -- BinData(subtype,data)" ); + return JS_FALSE; + } + Convertor c( cx ); + int subtype = static_cast<int>( c.toNumber( argv[ 0 ] ) ); + if ( subtype < 0 || subtype > 255 ) { + JS_ReportError( cx, "BinData subtype must be between 0 and 255" ); + return JS_FALSE; + } + string encoded( c.toString( argv[ 1 ] ) ); + string decoded; + try { + decoded = base64::decode( encoded ); + } + catch(...) { + JS_ReportError( cx, "BinData could not decode base64 parameter" ); + return JS_FALSE; + } + if ( ! JS_InstanceOf( cx , obj , &bindata_class , 0 ) ) { + obj = JS_NewObject( cx , &bindata_class , 0 , 0 ); + CHECKNEWOBJECT( obj, cx, "bindata_constructor" ); + *rval = OBJECT_TO_JSVAL( obj ); + } + verify( JS_SetPrivate( cx, obj, new BinDataHolder( decoded.data(), decoded.length() ) ) ); + c.setProperty( obj, "len", c.toval( (double)decoded.length() ) ); + c.setProperty( obj, "type", c.toval( (double)subtype ) ); + } + catch ( const AssertionException& e ) { + if ( ! JS_IsExceptionPending( cx ) ) { + JS_ReportError( cx, e.what() ); + } + return JS_FALSE; + } + catch ( const std::exception& e ) { + log() << "unhandled exception: " << e.what() << ", throwing Fatal Assertion" << endl; + fassertFailed( 16309 ); + } + return JS_TRUE; + } + + JSBool bindata_tostring(JSContext *cx, JSObject *obj, uintN argc, jsval *argv, jsval *rval) { + try { + Convertor c(cx); + int type = static_cast<int>( c.getNumber( obj, "type" ) ); + int len = static_cast<int>(c.getNumber( obj, "len" ) ); + void *holder = JS_GetPrivate( cx, obj ); + verify( holder ); + const char *data = ( ( BinDataHolder* )( holder ) )->c_; + stringstream ss; + ss << "BinData(" << type << ",\""; + base64::encode( ss, data, len ); + ss << "\")"; + string ret = ss.str(); + *rval = c.toval( ret.c_str() ); + } + catch ( const AssertionException& e ) { + if ( ! JS_IsExceptionPending( cx ) ) { + JS_ReportError( cx, e.what() ); + } + return JS_FALSE; + } + catch ( const std::exception& e ) { + log() << "unhandled exception: " << e.what() << ", throwing Fatal Assertion" << endl; + fassertFailed( 16310 ); + } + return JS_TRUE; + } + + JSBool bindataBase64(JSContext *cx, JSObject *obj, uintN argc, jsval *argv, jsval *rval) { + try { + Convertor c(cx); + int len = static_cast<int>( c.getNumber( obj, "len" ) ); + void *holder = JS_GetPrivate( cx, obj ); + verify( holder ); + const char *data = ( ( BinDataHolder* )( holder ) )->c_; + stringstream ss; + base64::encode( ss, data, len ); + string ret = ss.str(); + *rval = c.toval( ret.c_str() ); + } + catch ( const AssertionException& e ) { + if ( ! JS_IsExceptionPending( cx ) ) { + JS_ReportError( cx, e.what() ); + } + return JS_FALSE; + } + catch ( const std::exception& e ) { + log() << "unhandled exception: " << e.what() << ", throwing Fatal Assertion" << endl; + fassertFailed( 16311 ); + } + return JS_TRUE; + } + + JSBool bindataAsHex(JSContext *cx, JSObject *obj, uintN argc, jsval *argv, jsval *rval) { + try { + Convertor c(cx); + int len = static_cast<int>( c.getNumber( obj, "len" ) ); + void *holder = JS_GetPrivate( cx, obj ); + verify( holder ); + const char *data = ( ( BinDataHolder* )( holder ) )->c_; + stringstream ss; + ss.setf( ios_base::hex, ios_base::basefield ); + ss.fill( '0' ); + ss.setf( ios_base::right, ios_base::adjustfield ); + for( int i = 0; i < len; i++ ) { + unsigned v = static_cast<unsigned char>( data[i] ); + ss << setw(2) << v; + } + string ret = ss.str(); + *rval = c.toval( ret.c_str() ); + } + catch ( const AssertionException& e ) { + if ( ! JS_IsExceptionPending( cx ) ) { + JS_ReportError( cx, e.what() ); + } + return JS_FALSE; + } + catch ( const std::exception& e ) { + log() << "unhandled exception: " << e.what() << ", throwing Fatal Assertion" << endl; + fassertFailed( 16312 ); + } + return JS_TRUE; + } + + void bindata_finalize( JSContext * cx , JSObject * obj ) { + try { + void *holder = JS_GetPrivate( cx, obj ); + if ( holder ) { + delete reinterpret_cast<BinDataHolder*>( holder ); + verify( JS_SetPrivate( cx , obj , 0 ) ); + } + } + catch ( const AssertionException& e ) { + if ( ! JS_IsExceptionPending( cx ) ) { + JS_ReportError( cx, e.what() ); + } + } + catch ( const std::exception& e ) { + log() << "unhandled exception: " << e.what() << ", throwing Fatal Assertion" << endl; + fassertFailed( 16313 ); + } + } + + JSClass bindata_class = { + "BinData" , JSCLASS_HAS_PRIVATE , + JS_PropertyStub, JS_PropertyStub, JS_PropertyStub, JS_PropertyStub, + JS_EnumerateStub, JS_ResolveStub , JS_ConvertStub, bindata_finalize, + JSCLASS_NO_OPTIONAL_MEMBERS + }; + + JSFunctionSpec bindata_functions[] = { + { "toString" , bindata_tostring , 0 , JSPROP_READONLY | JSPROP_PERMANENT, 0 } , + { "hex", bindataAsHex, 0, JSPROP_READONLY | JSPROP_PERMANENT, 0 } , + { "base64", bindataBase64, 0, JSPROP_READONLY | JSPROP_PERMANENT, 0 } , + { 0 } + }; + + // Map + + bool specialMapString( const string& s ) { + return s == "put" || s == "get" || s == "_get" || s == "values" || s == "_data" || s == "constructor" ; + } + + JSBool map_constructor( JSContext *cx, JSObject *obj, uintN argc, jsval *argv, jsval *rval ) { + try { + if ( argc > 0 ) { + JS_ReportError( cx , "Map takes no arguments" ); + return JS_FALSE; + } + + JSObject * arrayObj = JS_NewObject( cx , 0 , 0 , 0 ); + CHECKNEWOBJECT( arrayObj, cx, "map_constructor" ); + + jsval a = OBJECT_TO_JSVAL( arrayObj ); + JS_SetProperty( cx , obj , "_data" , &a ); + } + catch ( const AssertionException& e ) { + if ( ! JS_IsExceptionPending( cx ) ) { + JS_ReportError( cx, e.what() ); + } + return JS_FALSE; + } + catch ( const std::exception& e ) { + log() << "unhandled exception: " << e.what() << ", throwing Fatal Assertion" << endl; + fassertFailed( 16314 ); + } + return JS_TRUE; + } + + JSBool map_prop( JSContext *cx, JSObject *obj, jsval idval, jsval *vp ) { + try { + Convertor c(cx); + string str( c.toString( idval ) ); + if ( ! specialMapString( str ) ) { + log() << "illegal prop access: " << str << endl; + JS_ReportError( cx , "can't use array access with Map" ); + return JS_FALSE; + } + } + catch ( const AssertionException& e ) { + if ( ! JS_IsExceptionPending( cx ) ) { + JS_ReportError( cx, e.what() ); + } + return JS_FALSE; + } + catch ( const std::exception& e ) { + log() << "unhandled exception: " << e.what() << ", throwing Fatal Assertion" << endl; + fassertFailed( 16315 ); + } + return JS_TRUE; + } + + JSClass map_class = { + "Map" , JSCLASS_HAS_PRIVATE , + map_prop, JS_PropertyStub, map_prop, map_prop, + JS_EnumerateStub, JS_ResolveStub , JS_ConvertStub, JS_FinalizeStub, + JSCLASS_NO_OPTIONAL_MEMBERS + }; + + JSFunctionSpec map_functions[] = { + { 0 } + }; + + + // ----- + + JSClass timestamp_class = { + "Timestamp" , JSCLASS_HAS_PRIVATE , + JS_PropertyStub, JS_PropertyStub, JS_PropertyStub, JS_PropertyStub, + JS_EnumerateStub, JS_ResolveStub , JS_ConvertStub, JS_FinalizeStub, + JSCLASS_NO_OPTIONAL_MEMBERS + }; + + JSBool timestamp_constructor( JSContext *cx, JSObject *obj, uintN argc, jsval *argv, jsval *rval ) { + try { + smuassert( cx, + "Timestamp takes 0 or 2 arguments -- Timestamp() or Timestamp(t,i)", + argc == 0 || argc == 2 ); + + if ( ! JS_InstanceOf( cx , obj , ×tamp_class , 0 ) ) { + obj = JS_NewObject( cx , ×tamp_class , 0 , 0 ); + CHECKNEWOBJECT( obj, cx, "timestamp_constructor" ); + *rval = OBJECT_TO_JSVAL( obj ); + } + + Convertor c( cx ); + if ( argc == 0 ) { + c.setProperty( obj, "t", c.toval( 0.0 ) ); + c.setProperty( obj, "i", c.toval( 0.0 ) ); + } + else { + c.setProperty( obj, "t", argv[ 0 ] ); + c.setProperty( obj, "i", argv[ 1 ] ); + } + } + catch ( const AssertionException& e ) { + if ( ! JS_IsExceptionPending( cx ) ) { + JS_ReportError( cx, e.what() ); + } + return JS_FALSE; + } + catch ( const std::exception& e ) { + log() << "unhandled exception: " << e.what() << ", throwing Fatal Assertion" << endl; + fassertFailed( 16316 ); + } + return JS_TRUE; + } + + JSClass numberlong_class = { + "NumberLong" , JSCLASS_HAS_PRIVATE , + JS_PropertyStub, JS_PropertyStub, JS_PropertyStub, JS_PropertyStub, + JS_EnumerateStub, JS_ResolveStub , JS_ConvertStub, JS_FinalizeStub, + JSCLASS_NO_OPTIONAL_MEMBERS + }; + + JSBool numberlong_constructor( JSContext *cx, JSObject *obj, uintN argc, jsval *argv, jsval *rval ) { + try { + smuassert( cx , "NumberLong needs 0 or 1 args" , argc == 0 || argc == 1 ); + + if ( ! JS_InstanceOf( cx , obj , &numberlong_class , 0 ) ) { + obj = JS_NewObject( cx , &numberlong_class , 0 , 0 ); + CHECKNEWOBJECT( obj, cx, "numberlong_constructor" ); + *rval = OBJECT_TO_JSVAL( obj ); + } + + Convertor c( cx ); + if ( argc == 0 ) { + c.setProperty( obj, "floatApprox", c.toval( 0.0 ) ); + } + else if ( JSVAL_IS_NUMBER( argv[ 0 ] ) ) { + c.setProperty( obj, "floatApprox", argv[ 0 ] ); + } + else { + string num = c.toString( argv[ 0 ] ); + //PRINT(num); + const char *numStr = num.c_str(); + long long n; + try { + n = parseLL( numStr ); + //PRINT(n); + } + catch ( const AssertionException & ) { + smuassert( cx , "could not convert string to long long" , false ); + } + c.makeLongObj( n, obj ); + } + } + catch ( const AssertionException& e ) { + if ( ! JS_IsExceptionPending( cx ) ) { + JS_ReportError( cx, e.what() ); + } + return JS_FALSE; + } + catch ( const std::exception& e ) { + log() << "unhandled exception: " << e.what() << ", throwing Fatal Assertion" << endl; + fassertFailed( 16317 ); + } + return JS_TRUE; + } + + JSBool numberlong_valueof(JSContext *cx, JSObject *obj, uintN argc, jsval *argv, jsval *rval) { + try { + Convertor c(cx); + *rval = c.toval( static_cast<double>( c.toNumberLongUnsafe( obj ) ) ); + } + catch ( const AssertionException& e ) { + if ( ! JS_IsExceptionPending( cx ) ) { + JS_ReportError( cx, e.what() ); + } + return JS_FALSE; + } + catch ( const std::exception& e ) { + log() << "unhandled exception: " << e.what() << ", throwing Fatal Assertion" << endl; + fassertFailed( 16318 ); + } + return JS_TRUE; + } + + JSBool numberlong_tonumber(JSContext *cx, JSObject *obj, uintN argc, jsval *argv, jsval *rval) { + return numberlong_valueof( cx, obj, argc, argv, rval ); + } + + JSBool numberlong_tostring(JSContext *cx, JSObject *obj, uintN argc, jsval *argv, jsval *rval) { + try { + Convertor c(cx); + stringstream ss; + long long val = c.toNumberLongUnsafe( obj ); + const long long limit = 2LL << 30; + + if ( val <= -limit || limit <= val ) + ss << "NumberLong(\"" << val << "\")"; + else + ss << "NumberLong(" << val << ")"; + + string ret = ss.str(); + *rval = c.toval( ret.c_str() ); + } + catch ( const AssertionException& e ) { + if ( ! JS_IsExceptionPending( cx ) ) { + JS_ReportError( cx, e.what() ); + } + return JS_FALSE; + } + catch ( const std::exception& e ) { + log() << "unhandled exception: " << e.what() << ", throwing Fatal Assertion" << endl; + fassertFailed( 16319 ); + } + return JS_TRUE; + } + + JSFunctionSpec numberlong_functions[] = { + { "valueOf" , numberlong_valueof , 0 , JSPROP_READONLY | JSPROP_PERMANENT, 0 } , + { "toNumber" , numberlong_tonumber , 0 , JSPROP_READONLY | JSPROP_PERMANENT, 0 } , + { "toString" , numberlong_tostring , 0 , JSPROP_READONLY | JSPROP_PERMANENT, 0 } , + { 0 } + }; + + JSClass numberint_class = { + "NumberInt" , JSCLASS_HAS_PRIVATE , + JS_PropertyStub, JS_PropertyStub, JS_PropertyStub, JS_PropertyStub, + JS_EnumerateStub, JS_ResolveStub , JS_ConvertStub, JS_FinalizeStub, + JSCLASS_NO_OPTIONAL_MEMBERS + }; + + JSBool numberint_constructor( JSContext *cx, JSObject *obj, uintN argc, jsval *argv, jsval *rval ) { + try { + smuassert( cx , "NumberInt needs 0 or 1 args" , argc == 0 || argc == 1 ); + + if ( ! JS_InstanceOf( cx , obj , &numberint_class , 0 ) ) { + obj = JS_NewObject( cx , &numberint_class , 0 , 0 ); + CHECKNEWOBJECT( obj, cx, "numberint_constructor" ); + *rval = OBJECT_TO_JSVAL( obj ); + } + + Convertor c( cx ); + if ( argc == 0 ) { + c.setProperty( obj, "floatApprox", c.toval( 0.0 ) ); + } + else if ( JSVAL_IS_NUMBER( argv[ 0 ] ) ) { + c.setProperty( obj, "floatApprox", argv[ 0 ] ); + } + else { + string num = c.toString( argv[ 0 ] ); + //PRINT(num); + const char *numStr = num.c_str(); + int n; + try { + n = static_cast<int>( parseLL( numStr ) ); + //PRINT(n); + } + catch ( const AssertionException & ) { + smuassert( cx , "could not convert string to integer" , false ); + } + c.makeIntObj( n, obj ); + } + } + catch ( const AssertionException& e ) { + if ( ! JS_IsExceptionPending( cx ) ) { + JS_ReportError( cx, e.what() ); + } + return JS_FALSE; + } + catch ( const std::exception& e ) { + log() << "unhandled exception: " << e.what() << ", throwing Fatal Assertion" << endl; + fassertFailed( 16320 ); + } + return JS_TRUE; + } + + JSBool numberint_valueof(JSContext *cx, JSObject *obj, uintN argc, jsval *argv, jsval *rval) { + try { + Convertor c(cx); + *rval = c.toval( static_cast<double>( c.toNumberInt( obj ) ) ); + } + catch ( const AssertionException& e ) { + if ( ! JS_IsExceptionPending( cx ) ) { + JS_ReportError( cx, e.what() ); + } + return JS_FALSE; + } + catch ( const std::exception& e ) { + log() << "unhandled exception: " << e.what() << ", throwing Fatal Assertion" << endl; + fassertFailed( 16321 ); + } + return JS_TRUE; + } + + JSBool numberint_tonumber(JSContext *cx, JSObject *obj, uintN argc, jsval *argv, jsval *rval) { + return numberint_valueof( cx, obj, argc, argv, rval ); + } + + JSBool numberint_tostring(JSContext *cx, JSObject *obj, uintN argc, jsval *argv, jsval *rval) { + try { + Convertor c(cx); + int val = c.toNumberInt( obj ); + string ret = str::stream() << "NumberInt(" << val << ")"; + *rval = c.toval( ret.c_str() ); + } + catch ( const AssertionException& e ) { + if ( ! JS_IsExceptionPending( cx ) ) { + JS_ReportError( cx, e.what() ); + } + return JS_FALSE; + } + catch ( const std::exception& e ) { + log() << "unhandled exception: " << e.what() << ", throwing Fatal Assertion" << endl; + fassertFailed( 16322 ); + } + return JS_TRUE; + } + + JSFunctionSpec numberint_functions[] = { + { "valueOf" , numberint_valueof , 0 , JSPROP_READONLY | JSPROP_PERMANENT, 0 } , + { "toNumber" , numberint_tonumber , 0 , JSPROP_READONLY | JSPROP_PERMANENT, 0 } , + { "toString" , numberint_tostring , 0 , JSPROP_READONLY | JSPROP_PERMANENT, 0 } , + { 0 } + }; + + JSClass minkey_class = { + "MinKey" , JSCLASS_HAS_PRIVATE , + JS_PropertyStub, JS_PropertyStub, JS_PropertyStub, JS_PropertyStub, + JS_EnumerateStub, JS_ResolveStub , JS_ConvertStub, JS_FinalizeStub, + JSCLASS_NO_OPTIONAL_MEMBERS + }; + + JSClass maxkey_class = { + "MaxKey" , JSCLASS_HAS_PRIVATE , + JS_PropertyStub, JS_PropertyStub, JS_PropertyStub, JS_PropertyStub, + JS_EnumerateStub, JS_ResolveStub , JS_ConvertStub, JS_FinalizeStub, + JSCLASS_NO_OPTIONAL_MEMBERS + }; + + // dbquery + + JSBool dbquery_constructor( JSContext *cx, JSObject *obj, uintN argc, jsval *argv, jsval *rval ) { + try { + smuassert( cx , "DDQuery needs at least 4 args" , argc >= 4 ); + + Convertor c(cx); + c.setProperty( obj , "_mongo" , argv[0] ); + c.setProperty( obj , "_db" , argv[1] ); + c.setProperty( obj , "_collection" , argv[2] ); + c.setProperty( obj , "_ns" , argv[3] ); + + if ( argc > 4 && JSVAL_IS_OBJECT( argv[4] ) ) + c.setProperty( obj , "_query" , argv[4] ); + else { + JSObject * temp = JS_NewObject( cx , 0 , 0 , 0 ); + CHECKNEWOBJECT( temp, cx, "dbquery_constructor" ); + c.setProperty( obj , "_query" , OBJECT_TO_JSVAL( temp ) ); + } + + if ( argc > 5 && JSVAL_IS_OBJECT( argv[5] ) ) + c.setProperty( obj , "_fields" , argv[5] ); + else + c.setProperty( obj , "_fields" , JSVAL_NULL ); + + + if ( argc > 6 && JSVAL_IS_NUMBER( argv[6] ) ) + c.setProperty( obj , "_limit" , argv[6] ); + else + c.setProperty( obj , "_limit" , JSVAL_ZERO ); + + if ( argc > 7 && JSVAL_IS_NUMBER( argv[7] ) ) + c.setProperty( obj , "_skip" , argv[7] ); + else + c.setProperty( obj , "_skip" , JSVAL_ZERO ); + + if ( argc > 8 && JSVAL_IS_NUMBER( argv[8] ) ) + c.setProperty( obj , "_batchSize" , argv[8] ); + else + c.setProperty( obj , "_batchSize" , JSVAL_ZERO ); + + if ( argc > 9 && JSVAL_IS_NUMBER( argv[9] ) ) + c.setProperty( obj , "_options" , argv[9] ); + else + c.setProperty( obj , "_options" , JSVAL_ZERO ); + + c.setProperty( obj , "_cursor" , JSVAL_NULL ); + c.setProperty( obj , "_numReturned" , JSVAL_ZERO ); + c.setProperty( obj , "_special" , JSVAL_FALSE ); + } + catch ( const AssertionException& e ) { + if ( ! JS_IsExceptionPending( cx ) ) { + JS_ReportError( cx, e.what() ); + } + return JS_FALSE; + } + catch ( const std::exception& e ) { + log() << "unhandled exception: " << e.what() << ", throwing Fatal Assertion" << endl; + fassertFailed( 16323 ); + } + return JS_TRUE; + } + + JSBool dbquery_resolve( JSContext *cx, JSObject *obj, jsval id, uintN flags, JSObject **objp ) { + try { + if ( flags & JSRESOLVE_ASSIGNING ) + return JS_TRUE; + + if ( ! JSVAL_IS_NUMBER( id ) ) + return JS_TRUE; + + jsval val = JSVAL_VOID; + verify( JS_CallFunctionName( cx , obj , "arrayAccess" , 1 , &id , &val ) ); + Convertor c(cx); + c.setProperty( obj , c.toString( id ).c_str() , val ); + *objp = obj; + } + catch ( const AssertionException& e ) { + if ( ! JS_IsExceptionPending( cx ) ) { + JS_ReportError( cx, e.what() ); + } + return JS_FALSE; + } + catch ( const std::exception& e ) { + log() << "unhandled exception: " << e.what() << ", throwing Fatal Assertion" << endl; + fassertFailed( 16324 ); + } + return JS_TRUE; + } + + JSClass dbquery_class = { + "DBQuery" , JSCLASS_NEW_RESOLVE , + JS_PropertyStub, JS_PropertyStub, JS_PropertyStub, JS_PropertyStub, + JS_EnumerateStub, (JSResolveOp)(&dbquery_resolve) , JS_ConvertStub, JS_FinalizeStub, + JSCLASS_NO_OPTIONAL_MEMBERS + }; + + // ---- other stuff ---- + + void initMongoJS( SMScope * scope , JSContext * cx , JSObject * global , bool local ) { + + verify( JS_InitClass( cx , global , 0 , &mongo_class , local ? mongo_local_constructor : mongo_external_constructor , 0 , 0 , mongo_functions , 0 , 0 ) ); + + verify( JS_InitClass( cx , global , 0 , &object_id_class , object_id_constructor , 0 , 0 , 0 , 0 , 0 ) ); + verify( JS_InitClass( cx , global , 0 , &db_class , db_constructor , 2 , 0 , 0 , 0 , 0 ) ); + verify( JS_InitClass( cx , global , 0 , &db_collection_class , db_collection_constructor , 4 , 0 , 0 , 0 , 0 ) ); + verify( JS_InitClass( cx , global , 0 , &internal_cursor_class , internal_cursor_constructor , 0 , 0 , internal_cursor_functions , 0 , 0 ) ); + verify( JS_InitClass( cx , global , 0 , &dbquery_class , dbquery_constructor , 0 , 0 , 0 , 0 , 0 ) ); + verify( JS_InitClass( cx , global , 0 , &dbpointer_class , dbpointer_constructor , 0 , 0 , dbpointer_functions , 0 , 0 ) ); + verify( JS_InitClass( cx , global , 0 , &bindata_class , bindata_constructor , 0 , 0 , bindata_functions , 0 , 0 ) ); +// verify( JS_InitClass( cx , global , 0 , &uuid_class , uuid_constructor , 0 , 0 , uuid_functions , 0 , 0 ) ); + + verify( JS_InitClass( cx , global , 0 , ×tamp_class , timestamp_constructor , 0 , 0 , 0 , 0 , 0 ) ); + verify( JS_InitClass( cx , global , 0 , &numberlong_class , numberlong_constructor , 0 , 0 , numberlong_functions , 0 , 0 ) ); + verify( JS_InitClass( cx , global , 0 , &numberint_class , numberint_constructor , 0 , 0 , numberint_functions , 0 , 0 ) ); + verify( JS_InitClass( cx , global , 0 , &minkey_class , 0 , 0 , 0 , 0 , 0 , 0 ) ); + verify( JS_InitClass( cx , global , 0 , &maxkey_class , 0 , 0 , 0 , 0 , 0 , 0 ) ); + + verify( JS_InitClass( cx , global , 0 , &map_class , map_constructor , 0 , 0 , map_functions , 0 , 0 ) ); + + verify( JS_InitClass( cx , global , 0 , &bson_ro_class , bson_cons , 0 , 0 , bson_functions , 0 , 0 ) ); + verify( JS_InitClass( cx , global , 0 , &bson_class , bson_cons , 0 , 0 , bson_functions , 0 , 0 ) ); + + static const char *dbrefName = "DBRef"; + dbref_class.name = dbrefName; + verify( JS_InitClass( cx , global , 0 , &dbref_class , dbref_constructor , 2 , 0 , bson_functions , 0 , 0 ) ); + + scope->execCoreFiles(); + } + + bool appendSpecialDBObject( Convertor * c , BSONObjBuilder& b , const string& name , jsval val , JSObject * o ) { + + if ( JS_InstanceOf( c->_context , o , &object_id_class , 0 ) ) { + OID oid; + oid.init( c->getString( o , "str" ) ); + b.append( name , oid ); + return true; + } + + if ( JS_InstanceOf( c->_context , o , &minkey_class , 0 ) ) { + b.appendMinKey( name ); + return true; + } + + if ( JS_InstanceOf( c->_context , o , &maxkey_class , 0 ) ) { + b.appendMaxKey( name ); + return true; + } + + if ( JS_InstanceOf( c->_context , o , ×tamp_class , 0 ) ) { + b.appendTimestamp( name , (unsigned long long)c->getNumber( o , "t" ) , (unsigned int )c->getNumber( o , "i" ) ); + return true; + } + + if ( JS_InstanceOf( c->_context , o , &numberlong_class , 0 ) ) { + b.append( name , c->toNumberLongUnsafe( o ) ); + return true; + } + + if ( JS_InstanceOf( c->_context , o , &numberint_class , 0 ) ) { + b.append( name , c->toNumberInt( o ) ); + return true; + } + + if ( JS_InstanceOf( c->_context , o , &dbpointer_class , 0 ) ) { + b.appendDBRef( name , c->getString( o , "ns" ) , c->toOID( c->getProperty( o , "id" ) ) ); + return true; + } + + if ( JS_InstanceOf( c->_context , o , &bindata_class , 0 ) ) { + void *holder = JS_GetPrivate( c->_context , o ); + const char *data = ( ( BinDataHolder * )( holder ) )->c_; + b.appendBinData( name , + (int)(c->getNumber( o , "len" )) , (BinDataType)((char)(c->getNumber( o , "type" ) ) ) , + data + ); + return true; + } + +#if defined( SM16 ) || defined( MOZJS ) +#warning dates do not work in your version of spider monkey + { + jsdouble d = js_DateGetMsecSinceEpoch( c->_context , o ); + if ( d ) { + b.appendDate( name , Date_t(d) ); + return true; + } + } +#elif defined( XULRUNNER ) + if ( JS_InstanceOf( c->_context , o, globalSMEngine->_dateClass , 0 ) ) { + jsdouble d = js_DateGetMsecSinceEpoch( c->_context , o ); + b.appendDate( name , Date_t(d) ); + return true; + } +#else + if ( JS_InstanceOf( c->_context , o, &js_DateClass , 0 ) ) { + jsdouble d = js_DateGetMsecSinceEpoch( c->_context , o ); + long long d2 = (long long)d; + b.appendDate( name , Date_t((unsigned long long)d2) ); + return true; + } +#endif + + + if ( JS_InstanceOf( c->_context , o , &dbquery_class , 0 ) || + JS_InstanceOf( c->_context , o , &mongo_class , 0 ) || + JS_InstanceOf( c->_context , o , &db_collection_class , 0 ) ) { + b.append( name , c->toString( val ) ); + return true; + } + +#if defined( XULRUNNER ) + if ( JS_InstanceOf( c->_context , o , globalSMEngine->_regexClass , 0 ) ) { + c->appendRegex( b , name , c->toString( val ) ); + return true; + } +#elif defined( SM18 ) + if ( JS_InstanceOf( c->_context , o , &js_RegExpClass , 0 ) ) { + c->appendRegex( b , name , c->toString( val ) ); + return true; + } +#endif + + return false; + } + + bool isDate( JSContext * cx , JSObject * o ) { +#if defined( SM16 ) || defined( MOZJS ) || defined( XULRUNNER ) + return js_DateGetMsecSinceEpoch( cx , o ) != 0; +#else + return JS_InstanceOf( cx , o, &js_DateClass, 0 ); +#endif + } + +} diff --git a/src/mongo/scripting/utils.cpp b/src/mongo/scripting/utils.cpp new file mode 100644 index 00000000000..99e33ac4600 --- /dev/null +++ b/src/mongo/scripting/utils.cpp @@ -0,0 +1,71 @@ +// utils.cpp +/* + * Copyright (C) 2010 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 <http://www.gnu.org/licenses/>. + */ + +#include "mongo/scripting/engine.h" +#include "mongo/util/md5.hpp" +#include "mongo/util/version.h" + +namespace mongo { + + void installBenchmarkSystem( Scope& scope ); + + static BSONObj native_hex_md5( const BSONObj& args, void* data ) { + uassert( 10261, + "hex_md5 takes a single string argument -- hex_md5(string)", + args.nFields() == 1 && args.firstElement().type() == String ); + const char * s = args.firstElement().valuestrsafe(); + + md5digest d; + md5_state_t st; + md5_init(&st); + md5_append( &st , (const md5_byte_t*)s , strlen( s ) ); + md5_finish(&st, d); + + return BSON( "" << digestToString( d ) ); + } + + static BSONObj native_version( const BSONObj& args, void* data ) { + cout << "version: " << versionString << endl; + if ( strstr( versionString , "+" ) ) + printGitVersion(); + return BSONObj(); + } + + static BSONObj native_sleep( const mongo::BSONObj& args, void* data ) { + uassert( 16259, + "sleep takes a single numeric argument -- sleep(milliseconds)", + args.nFields() == 1 && args.firstElement().isNumber() ); + sleepmillis( static_cast<long long>( args.firstElement().number() ) ); + + BSONObjBuilder b; + b.appendUndefined( "" ); + return b.obj(); + } + + // --------------------------------- + // ---- installer -------- + // --------------------------------- + + void installGlobalUtils( Scope& scope ) { + scope.injectNative( "hex_md5" , native_hex_md5 ); + scope.injectNative( "version" , native_version ); + scope.injectNative( "sleep" , native_sleep ); + + installBenchmarkSystem( scope ); + } + +} diff --git a/src/mongo/scripting/v8_db.cpp b/src/mongo/scripting/v8_db.cpp new file mode 100644 index 00000000000..cc9b5ce000a --- /dev/null +++ b/src/mongo/scripting/v8_db.cpp @@ -0,0 +1,1145 @@ +// v8_db.cpp + +/* Copyright 2009 10gen Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "v8_wrapper.h" +#include "v8_utils.h" +#include "engine_v8.h" +#include "v8_db.h" +#include "util/base64.h" +#include "util/text.h" +#include "../client/syncclusterconnection.h" +#include "../s/d_logic.h" +#include "../db/namespacestring.h" +#include <iostream> + +using namespace std; +using namespace v8; + +namespace mongo { + +#define DDD(x) + + static v8::Handle<v8::Value> newInstance( v8::Function* f, const v8::Arguments& args ) { + // need to translate arguments into an array + int argc = args.Length(); + scoped_array< Handle<Value> > argv( new Handle<Value>[argc] ); + for (int i = 0; i < argc; ++i) { + argv[i] = args[i]; + } + return f->NewInstance(argc, argv.get()); + } + + v8::Handle<v8::FunctionTemplate> getMongoFunctionTemplate( V8Scope* scope, bool local ) { + v8::Handle<v8::FunctionTemplate> mongo; + if ( local ) { + mongo = scope->createV8Function(mongoConsLocal); + } + else { + mongo = scope->createV8Function(mongoConsExternal); + } + mongo->InstanceTemplate()->SetInternalFieldCount( 1 ); + v8::Handle<v8::Template> proto = mongo->PrototypeTemplate(); + scope->injectV8Function("find", mongoFind, proto); + scope->injectV8Function("insert", mongoInsert, proto); + scope->injectV8Function("remove", mongoRemove, proto); + scope->injectV8Function("update", mongoUpdate, proto); + scope->injectV8Function("auth", mongoAuth, proto); + scope->injectV8Function("logout", mongoLogout, proto); + + v8::Handle<FunctionTemplate> ic = scope->createV8Function(internalCursorCons); + ic->InstanceTemplate()->SetInternalFieldCount( 1 ); + v8::Handle<v8::Template> icproto = ic->PrototypeTemplate(); + scope->injectV8Function("next", internalCursorNext, icproto); + scope->injectV8Function("hasNext", internalCursorHasNext, icproto); + scope->injectV8Function("objsLeftInBatch", internalCursorObjsLeftInBatch, icproto); + scope->injectV8Function("readOnly", internalCursorReadOnly, icproto); + proto->Set( scope->getV8Str( "internalCursor" ) , ic ); + + return mongo; + } + + v8::Handle<v8::FunctionTemplate> getNumberLongFunctionTemplate(V8Scope* scope) { + v8::Handle<v8::FunctionTemplate> numberLong = scope->createV8Function(numberLongInit); + v8::Local<v8::Template> proto = numberLong->PrototypeTemplate(); + scope->injectV8Function("valueOf", numberLongValueOf, proto); + scope->injectV8Function("toNumber", numberLongToNumber, proto); + scope->injectV8Function("toString", numberLongToString, proto); + + return numberLong; + } + + v8::Handle<v8::FunctionTemplate> getNumberIntFunctionTemplate(V8Scope* scope) { + v8::Handle<v8::FunctionTemplate> numberInt = scope->createV8Function(numberIntInit); + v8::Local<v8::Template> proto = numberInt->PrototypeTemplate(); + scope->injectV8Function("valueOf", numberIntValueOf, proto); + scope->injectV8Function("toNumber", numberIntToNumber, proto); + scope->injectV8Function("toString", numberIntToString, proto); + + return numberInt; + } + + v8::Handle<v8::FunctionTemplate> getBinDataFunctionTemplate(V8Scope* scope) { + v8::Handle<v8::FunctionTemplate> binData = scope->createV8Function(binDataInit); + binData->InstanceTemplate()->SetInternalFieldCount(1); + v8::Local<v8::Template> proto = binData->PrototypeTemplate(); + scope->injectV8Function("toString", binDataToString, proto); + scope->injectV8Function("base64", binDataToBase64, proto); + scope->injectV8Function("hex", binDataToHex, proto); + return binData; + } + + v8::Handle<v8::FunctionTemplate> getUUIDFunctionTemplate(V8Scope* scope) { + v8::Handle<v8::FunctionTemplate> templ = scope->createV8Function(uuidInit); + return templ; + } + + v8::Handle<v8::FunctionTemplate> getMD5FunctionTemplate(V8Scope* scope) { + v8::Handle<v8::FunctionTemplate> templ = scope->createV8Function(md5Init); + return templ; + } + + v8::Handle<v8::FunctionTemplate> getHexDataFunctionTemplate(V8Scope* scope) { + v8::Handle<v8::FunctionTemplate> templ = scope->createV8Function(hexDataInit); + return templ; + } + + v8::Handle<v8::FunctionTemplate> getTimestampFunctionTemplate(V8Scope* scope) { + v8::Handle<v8::FunctionTemplate> ts = scope->createV8Function(dbTimestampInit); + ts->InstanceTemplate()->SetInternalFieldCount( 1 ); + return ts; + } + +// void installDBTypes( V8Scope* scope, Handle<ObjectTemplate>& global ) { +// v8::Handle<v8::FunctionTemplate> db = scope->createV8Function(dbInit); +// db->InstanceTemplate()->SetNamedPropertyHandler( collectionFallback ); +// global->Set(v8::String::New("DB") , db ); +// +// v8::Handle<v8::FunctionTemplate> dbCollection = scope->createV8Function(collectionInit); +// dbCollection->InstanceTemplate()->SetNamedPropertyHandler( collectionFallback ); +// global->Set(v8::String::New("DBCollection") , dbCollection ); +// +// +// v8::Handle<v8::FunctionTemplate> dbQuery = scope->createV8Function(dbQueryInit); +// dbQuery->InstanceTemplate()->SetIndexedPropertyHandler( dbQueryIndexAccess ); +// global->Set(v8::String::New("DBQuery") , dbQuery ); +// +// global->Set( v8::String::New("ObjectId") , newV8Function< objectIdInit >(scope) ); +// +// global->Set( v8::String::New("DBRef") , newV8Function< dbRefInit >(scope) ); +// +// global->Set( v8::String::New("DBPointer") , newV8Function< dbPointerInit >(scope) ); +// +// global->Set( v8::String::New("BinData") , getBinDataFunctionTemplate(scope) ); +// +// global->Set( v8::String::New("NumberLong") , getNumberLongFunctionTemplate(scope) ); +// +// global->Set( v8::String::New("Timestamp") , getTimestampFunctionTemplate(scope) ); +// } + + void installDBTypes( V8Scope* scope, v8::Handle<v8::Object>& global ) { + v8::Handle<v8::FunctionTemplate> db = scope->createV8Function(dbInit); + db->InstanceTemplate()->SetNamedPropertyHandler( collectionGetter, collectionSetter ); + global->Set(scope->getV8Str("DB") , db->GetFunction() ); + v8::Handle<v8::FunctionTemplate> dbCollection = scope->createV8Function(collectionInit); + dbCollection->InstanceTemplate()->SetNamedPropertyHandler( collectionGetter, collectionSetter ); + global->Set(scope->getV8Str("DBCollection") , dbCollection->GetFunction() ); + + + v8::Handle<v8::FunctionTemplate> dbQuery = scope->createV8Function(dbQueryInit); + dbQuery->InstanceTemplate()->SetIndexedPropertyHandler( dbQueryIndexAccess ); + global->Set(scope->getV8Str("DBQuery") , dbQuery->GetFunction() ); + + scope->injectV8Function("ObjectId", objectIdInit, global); + scope->injectV8Function("DBRef", dbRefInit, global); + scope->injectV8Function("DBPointer", dbPointerInit, global); + + global->Set( scope->getV8Str("BinData") , getBinDataFunctionTemplate(scope)->GetFunction() ); + global->Set( scope->getV8Str("UUID") , getUUIDFunctionTemplate(scope)->GetFunction() ); + global->Set( scope->getV8Str("MD5") , getMD5FunctionTemplate(scope)->GetFunction() ); + global->Set( scope->getV8Str("HexData") , getHexDataFunctionTemplate(scope)->GetFunction() ); + global->Set( scope->getV8Str("NumberLong") , getNumberLongFunctionTemplate(scope)->GetFunction() ); + global->Set( scope->getV8Str("NumberInt") , getNumberIntFunctionTemplate(scope)->GetFunction() ); + global->Set( scope->getV8Str("Timestamp") , getTimestampFunctionTemplate(scope)->GetFunction() ); + + BSONObjBuilder b; + b.appendMaxKey( "" ); + b.appendMinKey( "" ); + BSONObj o = b.obj(); + BSONObjIterator i( o ); + global->Set( scope->getV8Str("MaxKey"), scope->mongoToV8Element( i.next() ) ); + global->Set( scope->getV8Str("MinKey"), scope->mongoToV8Element( i.next() ) ); + + global->Get( scope->getV8Str( "Object" ) )->ToObject()->Set( scope->getV8Str("bsonsize") , scope->createV8Function(bsonsize)->GetFunction() ); + } + + void destroyConnection( Persistent<Value> self, void* parameter) { + delete static_cast<DBClientBase*>(parameter); + self.Dispose(); + self.Clear(); + } + + Handle<Value> mongoConsExternal(V8Scope* scope, const Arguments& args) { + + char host[255]; + + if ( args.Length() > 0 && args[0]->IsString() ) { + verify( args[0]->ToString()->Utf8Length() < 250 ); + args[0]->ToString()->WriteAscii( host ); + } + else { + strcpy( host , "127.0.0.1" ); + } + + string errmsg; + ConnectionString cs = ConnectionString::parse( host , errmsg ); + if ( ! cs.isValid() ) + return v8::ThrowException( v8::String::New( errmsg.c_str() ) ); + + + DBClientWithCommands * conn; + { + //V8Unlock ul; + conn = cs.connect( errmsg ); + } + if ( ! conn ) + return v8::ThrowException( v8::String::New( errmsg.c_str() ) ); + + Persistent<v8::Object> self = Persistent<v8::Object>::New( args.Holder() ); + self.MakeWeak( conn , destroyConnection ); + + { + //V8Unlock ul; + ScriptEngine::runConnectCallback( *conn ); + } + + args.This()->SetInternalField( 0 , External::New( conn ) ); + args.This()->Set( scope->getV8Str( "slaveOk" ) , Boolean::New( false ) ); + args.This()->Set( scope->getV8Str( "host" ) , scope->getV8Str( host ) ); + + return v8::Undefined(); + } + + Handle<Value> mongoConsLocal(V8Scope* scope, const Arguments& args) { + + if ( args.Length() > 0 ) + return v8::ThrowException( v8::String::New( "local Mongo constructor takes no args" ) ); + + DBClientBase * conn; + { + //V8Unlock ul; + conn = createDirectClient(); + } + + Persistent<v8::Object> self = Persistent<v8::Object>::New( args.This() ); + self.MakeWeak( conn , destroyConnection ); + + // NOTE I don't believe the conn object will ever be freed. + args.This()->SetInternalField( 0 , External::New( conn ) ); + args.This()->Set( scope->getV8Str( "slaveOk" ) , Boolean::New( false ) ); + args.This()->Set( scope->getV8Str( "host" ) , scope->getV8Str( "EMBEDDED" ) ); + + return v8::Undefined(); + } + + + // --- + +#ifdef _WIN32 +#define GETNS char * ns = new char[args[0]->ToString()->Utf8Length()]; args[0]->ToString()->WriteUtf8( ns ); +#else +#define GETNS char ns[args[0]->ToString()->Utf8Length()]; args[0]->ToString()->WriteUtf8( ns ); +#endif + + DBClientBase * getConnection( const Arguments& args ) { + Local<External> c = External::Cast( *(args.This()->GetInternalField( 0 )) ); + DBClientBase * conn = (DBClientBase*)(c->Value()); + verify( conn ); + return conn; + } + + // ---- real methods + + void destroyCursor( Persistent<Value> self, void* parameter) { + delete static_cast<mongo::DBClientCursor*>(parameter); + self.Dispose(); + self.Clear(); + } + + /** + 0 - namespace + 1 - query + 2 - fields + 3 - limit + 4 - skip + */ + Handle<Value> mongoFind(V8Scope* scope, const Arguments& args) { + HandleScope handle_scope; + + jsassert( args.Length() == 7 , "find needs 7 args" ); + jsassert( args[1]->IsObject() , "needs to be an object" ); + DBClientBase * conn = getConnection( args ); + GETNS; + + BSONObj q = scope->v8ToMongo( args[1]->ToObject() ); + DDD( "query:" << q ); + + BSONObj fields; + bool haveFields = args[2]->IsObject() && args[2]->ToObject()->GetPropertyNames()->Length() > 0; + if ( haveFields ) + fields = scope->v8ToMongo( args[2]->ToObject() ); + + Local<v8::Object> mongo = args.This(); + + try { + auto_ptr<mongo::DBClientCursor> cursor; + int nToReturn = (int)(args[3]->ToNumber()->Value()); + int nToSkip = (int)(args[4]->ToNumber()->Value()); + int batchSize = (int)(args[5]->ToNumber()->Value()); + int options = (int)(args[6]->ToNumber()->Value()); + { + //V8Unlock u; + cursor = conn->query( ns, q , nToReturn , nToSkip , haveFields ? &fields : 0, options , batchSize ); + if ( ! cursor.get() ) + return v8::ThrowException( v8::String::New( "error doing query: failed" ) ); + } + v8::Function * cons = (v8::Function*)( *( mongo->Get( scope->getV8Str( "internalCursor" ) ) ) ); + if ( !cons ) { + // may get here in case of thread termination + return v8::ThrowException( v8::String::New( "Could not create a cursor" ) ); + } + + Persistent<v8::Object> c = Persistent<v8::Object>::New( cons->NewInstance() ); + c.MakeWeak( cursor.get() , destroyCursor ); + c->SetInternalField( 0 , External::New( cursor.release() ) ); + return handle_scope.Close(c); + } + catch ( ... ) { + return v8::ThrowException( v8::String::New( "socket error on query" ) ); + } + } + + v8::Handle<v8::Value> mongoInsert(V8Scope* scope, const v8::Arguments& args) { + jsassert( args.Length() == 2 , "insert needs 2 args" ); + jsassert( args[1]->IsObject() , "have to insert an object" ); + + if ( args.This()->Get( scope->getV8Str( "readOnly" ) )->BooleanValue() ) + return v8::ThrowException( v8::String::New( "js db in read only mode" ) ); + + DBClientBase * conn = getConnection( args ); + GETNS; + + v8::Handle<v8::Object> in = args[1]->ToObject(); + + if( args[1]->IsArray() ){ + + v8::Local<v8::Array> arr = v8::Array::Cast( *args[1] ); + vector<BSONObj> bos; + uint32_t len = arr->Length(); + + for( uint32_t i = 0; i < len; i++ ){ + + v8::Local<v8::Object> el = arr->CloneElementAt( i ); + + // Set ID on the element if necessary + if ( ! el->Has( scope->getV8Str( "_id" ) ) ) { + v8::Handle<v8::Value> argv[1]; + el->Set( scope->getV8Str( "_id" ) , scope->getObjectIdCons()->NewInstance( 0 , argv ) ); + } + + bos.push_back( scope->v8ToMongo( arr->CloneElementAt( i ) ) ); + } + + DDD( "want to save batch : " << bos.length ); + try { + //V8Unlock u; + conn->insert( ns , bos ); + } + catch ( ... ) { + return v8::ThrowException( v8::String::New( "socket error on bulk insert" ) ); + } + + } + else { + + if ( ! in->Has( scope->getV8Str( "_id" ) ) ) { + v8::Handle<v8::Value> argv[1]; + in->Set( scope->getV8Str( "_id" ) , scope->getObjectIdCons()->NewInstance( 0 , argv ) ); + } + + BSONObj o = scope->v8ToMongo( in ); + + DDD( "want to save : " << o.jsonString() ); + try { + //V8Unlock u; + conn->insert( ns , o ); + } + catch ( ... ) { + return v8::ThrowException( v8::String::New( "socket error on insert" ) ); + } + + } + + return v8::Undefined(); + } + + v8::Handle<v8::Value> mongoRemove(V8Scope* scope, const v8::Arguments& args) { + jsassert( args.Length() == 2 || args.Length() == 3 , "remove needs 2 args" ); + jsassert( args[1]->IsObject() , "have to remove an object template" ); + + if ( args.This()->Get( scope->getV8Str( "readOnly" ) )->BooleanValue() ) + return v8::ThrowException( v8::String::New( "js db in read only mode" ) ); + + DBClientBase * conn = getConnection( args ); + GETNS; + + v8::Handle<v8::Object> in = args[1]->ToObject(); + BSONObj o = scope->v8ToMongo( in ); + + bool justOne = false; + if ( args.Length() > 2 ) { + justOne = args[2]->BooleanValue(); + } + + DDD( "want to remove : " << o.jsonString() ); + try { + //V8Unlock u; + conn->remove( ns , o , justOne ); + } + catch ( ... ) { + return v8::ThrowException( v8::String::New( "socket error on remove" ) ); + } + + return v8::Undefined(); + } + + v8::Handle<v8::Value> mongoUpdate(V8Scope* scope, const v8::Arguments& args) { + jsassert( args.Length() >= 3 , "update needs at least 3 args" ); + jsassert( args[1]->IsObject() , "1st param to update has to be an object" ); + jsassert( args[2]->IsObject() , "2nd param to update has to be an object" ); + + if ( args.This()->Get( scope->getV8Str( "readOnly" ) )->BooleanValue() ) + return v8::ThrowException( v8::String::New( "js db in read only mode" ) ); + + DBClientBase * conn = getConnection( args ); + GETNS; + + v8::Handle<v8::Object> q = args[1]->ToObject(); + v8::Handle<v8::Object> o = args[2]->ToObject(); + + bool upsert = args.Length() > 3 && args[3]->IsBoolean() && args[3]->ToBoolean()->Value(); + bool multi = args.Length() > 4 && args[4]->IsBoolean() && args[4]->ToBoolean()->Value(); + + try { + BSONObj q1 = scope->v8ToMongo( q ); + BSONObj o1 = scope->v8ToMongo( o ); + //V8Unlock u; + conn->update( ns , q1 , o1 , upsert, multi ); + } + catch ( ... ) { + return v8::ThrowException( v8::String::New( "socket error on remove" ) ); + } + + return v8::Undefined(); + } + + v8::Handle<v8::Value> mongoAuth(V8Scope* scope, const v8::Arguments& args) { + jsassert( args.Length() >= 3 , "update needs at least 3 args" ); + DBClientBase * conn = getConnection( args ); + string db = toSTLString(args[0]); + string username = toSTLString(args[1]); + string password = toSTLString(args[2]); + string errmsg = ""; + + try { + if (conn->auth(db, username, password, errmsg)) { + return v8::Boolean::New(true); + } + } catch ( ... ) { + } + return v8::ThrowException( v8::String::New( errmsg.c_str() ) ); + } + + v8::Handle<v8::Value> mongoLogout(V8Scope* scope, const v8::Arguments& args) { + jsassert(args.Length() == 1, "update needs 1 arg"); + DBClientBase* conn = getConnection(args); + const string db = toSTLString(args[0]); + + BSONObj ret; + try { + conn->logout(db, ret); + } + catch (const std::exception& ex) { + return v8::ThrowException(v8::String::New(ex.what())); + } + + return scope->mongoToLZV8(ret, false, false); + } + +// + JSBool mongo_auth(JSContext *cx, JSObject *obj, uintN argc, jsval *argv, jsval *rval) { +// + smuassert( cx , "mongo_auth needs 3 args" , argc == 3 ); +// + shared_ptr< DBClientWithCommands > * connHolder = (shared_ptr< DBClientWithCommands >*)JS_GetPrivate( cx , obj ); +// + smuassert( cx , "no connection!" , connHolder && connHolder->get() ); +// + DBClientWithCommands *conn = connHolder->get(); +// + +// + Convertor c( cx ); +// + +// + string db = c.toString( argv[0] ); +// + string username = c.toString( argv[1] ); +// + string password = c.toString( argv[2] ); +// + string errmsg = ""; +// + +// + try { +// + if (conn->auth(db, username, password, errmsg)) { +// + return JS_TRUE; +// + } +// + JS_ReportError( cx, errmsg.c_str() ); +// + } +// + catch ( ... ) { +// + JS_ReportError( cx , "error doing query: unknown" ); +// + } +// + return JS_FALSE; +// + } + + + // --- cursor --- + + mongo::DBClientCursor * getCursor( const Arguments& args ) { + Local<External> c = External::Cast( *(args.This()->GetInternalField( 0 ) ) ); + + mongo::DBClientCursor * cursor = (mongo::DBClientCursor*)(c->Value()); + return cursor; + } + + v8::Handle<v8::Value> internalCursorCons(V8Scope* scope, const v8::Arguments& args) { + return v8::Undefined(); + } + + v8::Handle<v8::Value> internalCursorNext(V8Scope* scope, const v8::Arguments& args) { + mongo::DBClientCursor * cursor = getCursor( args ); + if ( ! cursor ) + return v8::Undefined(); + BSONObj o; + { + //V8Unlock u; + o = cursor->next(); + } + bool ro = false; + if (args.This()->Has(scope->V8STR_RO)) + ro = args.This()->Get(scope->V8STR_RO)->BooleanValue(); + return scope->mongoToLZV8( o, false, ro ); + } + + v8::Handle<v8::Value> internalCursorHasNext(V8Scope* scope, const v8::Arguments& args) { + mongo::DBClientCursor * cursor = getCursor( args ); + if ( ! cursor ) + return Boolean::New( false ); + bool ret; + { + //V8Unlock u; + ret = cursor->more(); + } + return Boolean::New( ret ); + } + + v8::Handle<v8::Value> internalCursorObjsLeftInBatch(V8Scope* scope, const v8::Arguments& args) { + mongo::DBClientCursor * cursor = getCursor( args ); + if ( ! cursor ) + return v8::Number::New( (double) 0 ); + int ret; + { + //V8Unlock u; + ret = cursor->objsLeftInBatch(); + } + return v8::Number::New( (double) ret ); + } + + v8::Handle<v8::Value> internalCursorReadOnly(V8Scope* scope, const v8::Arguments& args) { + Local<v8::Object> cursor = args.This(); + cursor->Set(scope->V8STR_RO, v8::Boolean::New(true)); + return cursor; + } + + // --- DB ---- + + v8::Handle<v8::Value> dbInit(V8Scope* scope, const v8::Arguments& args) { + verify( args.Length() == 2 ); + + args.This()->Set( scope->getV8Str( "_mongo" ) , args[0] ); + args.This()->Set( scope->getV8Str( "_name" ) , args[1] ); + + for ( int i=0; i<args.Length(); i++ ) + verify( ! args[i]->IsUndefined() ); + + string dbName = toSTLString( args[1] ); + if ( !NamespaceString::validDBName( dbName)) { + string msg = str::stream() << "[" << dbName << "] is not a " + << "valid database name"; + return v8::ThrowException( v8::String::New( msg.c_str() )); + } + + return v8::Undefined(); + } + + v8::Handle<v8::Value> collectionInit( V8Scope* scope, const v8::Arguments& args ) { + verify( args.Length() == 4 ); + + args.This()->Set( scope->getV8Str( "_mongo" ) , args[0] ); + args.This()->Set( scope->getV8Str( "_db" ) , args[1] ); + args.This()->Set( scope->getV8Str( "_shortName" ) , args[2] ); + args.This()->Set( scope->V8STR_FULLNAME , args[3] ); + + if ( haveLocalShardingInfo( toSTLString( args[3] ) ) ) + return v8::ThrowException( v8::String::New( "can't use sharded collection from db.eval" ) ); + + for ( int i=0; i<args.Length(); i++ ) + verify( ! args[i]->IsUndefined() ); + + return v8::Undefined(); + } + + v8::Handle<v8::Value> dbQueryInit( V8Scope* scope, const v8::Arguments& args ) { + + v8::Handle<v8::Object> t = args.This(); + + verify( args.Length() >= 4 ); + + t->Set( scope->getV8Str( "_mongo" ) , args[0] ); + t->Set( scope->getV8Str( "_db" ) , args[1] ); + t->Set( scope->getV8Str( "_collection" ) , args[2] ); + t->Set( scope->getV8Str( "_ns" ) , args[3] ); + + if ( args.Length() > 4 && args[4]->IsObject() ) + t->Set( scope->getV8Str( "_query" ) , args[4] ); + else + t->Set( scope->getV8Str( "_query" ) , v8::Object::New() ); + + if ( args.Length() > 5 && args[5]->IsObject() ) + t->Set( scope->getV8Str( "_fields" ) , args[5] ); + else + t->Set( scope->getV8Str( "_fields" ) , v8::Null() ); + + + if ( args.Length() > 6 && args[6]->IsNumber() ) + t->Set( scope->getV8Str( "_limit" ) , args[6] ); + else + t->Set( scope->getV8Str( "_limit" ) , Number::New( 0 ) ); + + if ( args.Length() > 7 && args[7]->IsNumber() ) + t->Set( scope->getV8Str( "_skip" ) , args[7] ); + else + t->Set( scope->getV8Str( "_skip" ) , Number::New( 0 ) ); + + if ( args.Length() > 8 && args[8]->IsNumber() ) + t->Set( scope->getV8Str( "_batchSize" ) , args[8] ); + else + t->Set( scope->getV8Str( "_batchSize" ) , Number::New( 0 ) ); + + if ( args.Length() > 9 && args[9]->IsNumber() ) + t->Set( scope->getV8Str( "_options" ) , args[9] ); + else + t->Set( scope->getV8Str( "_options" ) , Number::New( 0 ) ); + + + t->Set( scope->getV8Str( "_cursor" ) , v8::Null() ); + t->Set( scope->getV8Str( "_numReturned" ) , v8::Number::New(0) ); + t->Set( scope->getV8Str( "_special" ) , Boolean::New(false) ); + + return v8::Undefined(); + } + + Handle<Value> collectionSetter( Local<v8::String> name, Local<Value> value, const AccessorInfo& info ) { + // a collection name cannot be overwritten by a variable + string sname = toSTLString( name ); + if ( sname.length() == 0 || sname[0] == '_' ) { + // if starts with '_' we allow overwrite + return Handle<Value>(); + } + // dont set + return value; + } + + v8::Handle<v8::Value> collectionGetter( v8::Local<v8::String> name, const v8::AccessorInfo &info) { + DDD( "collectionFallback [" << name << "]" ); + + // first look in prototype, may be a function + v8::Handle<v8::Value> real = info.This()->GetPrototype()->ToObject()->Get( name ); + if ( !real->IsUndefined() ) + return real; + + // 2nd look into real values, may be cached collection object + string sname = toSTLString( name ); + if (info.This()->HasRealNamedProperty(name)) { + v8::Local<v8::Value> prop = info.This()->GetRealNamedProperty( name ); + if (prop->IsObject() && prop->ToObject()->HasRealNamedProperty(v8::String::New("_fullName"))) { + // need to check every time that the collection did not get sharded + if ( haveLocalShardingInfo( toSTLString( prop->ToObject()->GetRealNamedProperty(v8::String::New("_fullName")) ) ) ) + return v8::ThrowException( v8::String::New( "can't use sharded collection from db.eval" ) ); + } + return prop; + } else if ( sname.length() == 0 || sname[0] == '_' ) { + // if starts with '_' we dont return collection, one must use getCollection() + return v8::Handle<v8::Value>(); + } + + // no hit, create new collection + v8::Handle<v8::Value> getCollection = info.This()->GetPrototype()->ToObject()->Get( v8::String::New( "getCollection" ) ); + verify( getCollection->IsFunction() ); + + TryCatch tryCatch; + v8::Function * f = (v8::Function*)(*getCollection); + v8::Handle<v8::Value> argv[1]; + argv[0] = name; + v8::Local<v8::Value> coll = f->Call( info.This() , 1 , argv ); + if (coll.IsEmpty()) { + if (tryCatch.HasCaught()) { + return v8::ThrowException( tryCatch.Exception() ); + } + return Handle<Value>(); + } + + // cache collection for reuse, dont enumerate + info.This()->ForceSet(name, coll, v8::DontEnum); + return coll; + } + + v8::Handle<v8::Value> dbQueryIndexAccess( unsigned int index , const v8::AccessorInfo& info ) { + v8::Handle<v8::Value> arrayAccess = info.This()->GetPrototype()->ToObject()->Get( v8::String::New( "arrayAccess" ) ); + verify( arrayAccess->IsFunction() ); + + v8::Function * f = (v8::Function*)(*arrayAccess); + v8::Handle<v8::Value> argv[1]; + argv[0] = v8::Number::New( index ); + + return f->Call( info.This() , 1 , argv ); + } + + v8::Handle<v8::Value> objectIdInit( V8Scope* scope, const v8::Arguments& args ) { + v8::Handle<v8::Object> it = args.This(); + if ( it->IsUndefined() || it == v8::Context::GetCurrent()->Global() ) { + v8::Function * f = scope->getObjectIdCons(); + return newInstance(f, args); + } + + OID oid; + + if ( args.Length() == 0 ) { + oid.init(); + } + else { + string s = toSTLString( args[0] ); + try { + Scope::validateObjectIdString( s ); + } + catch ( const MsgAssertionException &m ) { + string error = m.toString(); + return v8::ThrowException( v8::String::New( error.c_str() ) ); + } + oid.init( s ); + } + + it->Set( scope->getV8Str( "str" ) , v8::String::New( oid.str().c_str() ) ); + + return it; + } + + v8::Handle<v8::Value> dbRefInit( V8Scope* scope, const v8::Arguments& args ) { + v8::Handle<v8::Object> it = args.This(); + if ( it->IsUndefined() || it == v8::Context::GetCurrent()->Global() ) { + v8::Function * f = scope->getNamedCons( "DBRef" ); + return newInstance(f, args); + } + + if (args.Length() != 2 && args.Length() != 0) { + return v8::ThrowException( v8::String::New( "DBRef needs 2 arguments" ) ); + } + + if ( args.Length() == 2 ) { + it->Set( scope->getV8Str( "$ref" ) , args[0] ); + it->Set( scope->getV8Str( "$id" ) , args[1] ); + } + + return it; + } + + v8::Handle<v8::Value> dbPointerInit( V8Scope* scope, const v8::Arguments& args ) { + v8::Handle<v8::Object> it = args.This(); + if ( it->IsUndefined() || it == v8::Context::GetCurrent()->Global() ) { + v8::Function * f = scope->getNamedCons( "DBPointer" ); + return newInstance(f, args); + } + + if (args.Length() != 2) { + return v8::ThrowException( v8::String::New( "DBPointer needs 2 arguments" ) ); + } + + it->Set( scope->getV8Str( "ns" ) , args[0] ); + it->Set( scope->getV8Str( "id" ) , args[1] ); + it->SetHiddenValue( scope->getV8Str( "__DBPointer" ), v8::Number::New( 1 ) ); + + return it; + } + + v8::Handle<v8::Value> dbTimestampInit( V8Scope* scope, const v8::Arguments& args ) { + v8::Handle<v8::Object> it = args.This(); + if ( it->IsUndefined() || it == v8::Context::GetCurrent()->Global() ) { + v8::Function * f = scope->getNamedCons( "Timestamp" ); + return newInstance(f, args); + } + + if ( args.Length() == 0 ) { + it->Set( scope->getV8Str( "t" ) , v8::Number::New( 0 ) ); + it->Set( scope->getV8Str( "i" ) , v8::Number::New( 0 ) ); + } + else if ( args.Length() == 2 ) { + it->Set( scope->getV8Str( "t" ) , args[0] ); + it->Set( scope->getV8Str( "i" ) , args[1] ); + } + else { + return v8::ThrowException( v8::String::New( "Timestamp needs 0 or 2 arguments" ) ); + } + + it->SetInternalField( 0, v8::Uint32::New( Timestamp ) ); + + return it; + } + + + v8::Handle<v8::Value> binDataInit( V8Scope* scope, const v8::Arguments& args ) { + v8::Local<v8::Object> it = args.This(); + if ( it->IsUndefined() || it == v8::Context::GetCurrent()->Global() ) { + v8::Function* f = scope->getNamedCons( "BinData" ); + return newInstance(f, args); + } + + Handle<Value> type; + Handle<Value> len; + int rlen; + char* data; + if (args.Length() == 3) { + // 3 args: len, type, data + len = args[0]; + rlen = len->IntegerValue(); + type = args[1]; + v8::String::Utf8Value utf( args[ 2 ] ); + char* tmp = *utf; + data = new char[rlen]; + memcpy(data, tmp, rlen); + } + else if ( args.Length() == 2 ) { + // 2 args: type, base64 string + type = args[0]; + v8::String::Utf8Value utf( args[ 1 ] ); + string decoded = base64::decode( *utf ); + const char* tmp = decoded.data(); + rlen = decoded.length(); + data = new char[rlen]; + memcpy(data, tmp, rlen); + len = v8::Number::New(rlen); +// it->Set( scope->getV8Str( "data" ), v8::String::New( decoded.data(), decoded.length() ) ); + } else if (args.Length() == 0) { + // this is called by subclasses that will fill properties + return it; + } else { + return v8::ThrowException( v8::String::New( "BinData needs 2 or 3 arguments" ) ); + } + + it->Set( scope->getV8Str( "len" ) , len ); + it->Set( scope->getV8Str( "type" ) , type ); + it->SetHiddenValue( scope->V8STR_BINDATA, v8::Number::New( 1 ) ); + Persistent<v8::Object> res = scope->wrapArrayObject(it, data); + return res; + } + + v8::Handle<v8::Value> binDataToString( V8Scope* scope, const v8::Arguments& args ) { + v8::Handle<v8::Object> it = args.This(); + int len = it->Get( scope->V8STR_LEN )->Int32Value(); + int type = it->Get( scope->V8STR_TYPE )->Int32Value(); + Local<External> c = External::Cast( *(it->GetInternalField( 0 )) ); + char* data = (char*)(c->Value()); + + stringstream ss; + ss << "BinData(" << type << ",\""; + base64::encode( ss, data, len ); + ss << "\")"; + string ret = ss.str(); + return v8::String::New( ret.c_str() ); + } + + v8::Handle<v8::Value> binDataToBase64( V8Scope* scope, const v8::Arguments& args ) { + v8::Handle<v8::Object> it = args.This(); + int len = Handle<v8::Number>::Cast(it->Get(scope->V8STR_LEN))->Int32Value(); + Local<External> c = External::Cast( *(it->GetInternalField( 0 )) ); + char* data = (char*)(c->Value()); + stringstream ss; + base64::encode( ss, (const char *)data, len ); + return v8::String::New(ss.str().c_str()); + } + + v8::Handle<v8::Value> binDataToHex( V8Scope* scope, const v8::Arguments& args ) { + v8::Handle<v8::Object> it = args.This(); + int len = Handle<v8::Number>::Cast(it->Get(scope->V8STR_LEN))->Int32Value(); + Local<External> c = External::Cast( *(it->GetInternalField( 0 )) ); + char* data = (char*)(c->Value()); + stringstream ss; + ss.setf (ios_base::hex , ios_base::basefield); + ss.fill ('0'); + ss.setf (ios_base::right , ios_base::adjustfield); + for( int i = 0; i < len; i++ ) { + unsigned v = (unsigned char) data[i]; + ss << setw(2) << v; + } + return v8::String::New(ss.str().c_str()); + } + + static v8::Handle<v8::Value> hexToBinData( V8Scope* scope, v8::Local<v8::Object> it, int type, string hexstr ) { + int len = hexstr.length() / 2; + char* data = new char[len]; + const char* src = hexstr.c_str(); + for( int i = 0; i < 16; i++ ) { + data[i] = fromHex(src + i * 2); + } + + it->Set( scope->V8STR_LEN , v8::Number::New(len) ); + it->Set( scope->V8STR_TYPE , v8::Number::New(type) ); + it->SetHiddenValue( scope->V8STR_BINDATA, v8::Number::New( 1 ) ); + Persistent<v8::Object> res = scope->wrapArrayObject(it, data); + return res; + } + + v8::Handle<v8::Value> uuidInit( V8Scope* scope, const v8::Arguments& args ) { + if (args.Length() != 1) { + return v8::ThrowException( v8::String::New( "UUIS needs 1 argument" ) ); + } + v8::String::Utf8Value utf( args[ 0 ] ); + if( utf.length() != 32 ) { + return v8::ThrowException( v8::String::New( "UUIS string must have 32 characters" ) ); + } + + v8::Function * f = scope->getNamedCons("BinData"); + Local<v8::Object> it = f->NewInstance(); + return hexToBinData(scope, it, bdtUUID, *utf); + } + + v8::Handle<v8::Value> md5Init( V8Scope* scope, const v8::Arguments& args ) { + if (args.Length() != 1) { + return v8::ThrowException( v8::String::New( "MD5 needs 1 argument" ) ); + } + v8::String::Utf8Value utf( args[ 0 ] ); + if( utf.length() != 32 ) { + return v8::ThrowException( v8::String::New( "MD5 string must have 32 characters" ) ); + } + + v8::Function * f = scope->getNamedCons("BinData"); + Local<v8::Object> it = f->NewInstance(); + return hexToBinData(scope, it, MD5Type, *utf); + } + + v8::Handle<v8::Value> hexDataInit( V8Scope* scope, const v8::Arguments& args ) { + if (args.Length() != 2) { + return v8::ThrowException( v8::String::New( "HexData needs 2 arguments" ) ); + } + v8::String::Utf8Value utf( args[ 1 ] ); + v8::Function * f = scope->getNamedCons("BinData"); + Local<v8::Object> it = f->NewInstance(); + return hexToBinData(scope, it, args[0]->IntegerValue(), *utf); + } + + v8::Handle<v8::Value> numberLongInit( V8Scope* scope, const v8::Arguments& args ) { + v8::Handle<v8::Object> it = args.This(); + if ( it->IsUndefined() || it == v8::Context::GetCurrent()->Global() ) { + v8::Function * f = scope->getNamedCons( "NumberLong" ); + return newInstance(f, args); + } + + if (args.Length() != 0 && args.Length() != 1 && args.Length() != 3) { + return v8::ThrowException( v8::String::New( "NumberLong needs 0, 1 or 3 arguments" ) ); + } + + if ( args.Length() == 0 ) { + it->Set( scope->getV8Str( "floatApprox" ), v8::Number::New( 0 ) ); + } + else if ( args.Length() == 1 ) { + if ( args[ 0 ]->IsNumber() ) { + it->Set( scope->getV8Str( "floatApprox" ), args[ 0 ] ); + } + else { + v8::String::Utf8Value data( args[ 0 ] ); + string num = *data; + const char *numStr = num.c_str(); + long long n; + try { + n = parseLL( numStr ); + } + catch ( const AssertionException & ) { + return v8::ThrowException( v8::String::New( "could not convert string to long long" ) ); + } + unsigned long long val = n; + // values above 2^53 are not accurately represented in JS + if ( (long long)val == (long long)(double)(long long)(val) && val < 9007199254740992ULL ) { + it->Set( scope->getV8Str( "floatApprox" ), v8::Number::New( (double)(long long)( val ) ) ); + } + else { + it->Set( scope->getV8Str( "floatApprox" ), v8::Number::New( (double)(long long)( val ) ) ); + it->Set( scope->getV8Str( "top" ), v8::Integer::New( val >> 32 ) ); + it->Set( scope->getV8Str( "bottom" ), v8::Integer::New( (unsigned long)(val & 0x00000000ffffffff) ) ); + } + } + } + else { + it->Set( scope->getV8Str( "floatApprox" ) , args[0] ); + it->Set( scope->getV8Str( "top" ) , args[1] ); + it->Set( scope->getV8Str( "bottom" ) , args[2] ); + } + it->SetHiddenValue( scope->V8STR_NUMBERLONG, v8::Number::New( 1 ) ); + + return it; + } + + long long numberLongVal( const v8::Handle< v8::Object > &it ) { + if ( !it->Has( v8::String::New( "top" ) ) ) + return (long long)( it->Get( v8::String::New( "floatApprox" ) )->NumberValue() ); + return + (long long) + ( (unsigned long long)( it->Get( v8::String::New( "top" ) )->ToInt32()->Value() ) << 32 ) + + (unsigned)( it->Get( v8::String::New( "bottom" ) )->ToInt32()->Value() ); + } + + v8::Handle<v8::Value> numberLongValueOf( V8Scope* scope, const v8::Arguments& args ) { + v8::Handle<v8::Object> it = args.This(); + long long val = numberLongVal( it ); + return v8::Number::New( double( val ) ); + } + + v8::Handle<v8::Value> numberLongToNumber( V8Scope* scope, const v8::Arguments& args ) { + return numberLongValueOf( scope, args ); + } + + v8::Handle<v8::Value> numberLongToString( V8Scope* scope, const v8::Arguments& args ) { + v8::Handle<v8::Object> it = args.This(); + + stringstream ss; + long long val = numberLongVal( it ); + const long long limit = 2LL << 30; + + if ( val <= -limit || limit <= val ) + ss << "NumberLong(\"" << val << "\")"; + else + ss << "NumberLong(" << val << ")"; + + string ret = ss.str(); + return v8::String::New( ret.c_str() ); + } + + v8::Handle<v8::Value> numberIntInit( V8Scope* scope, const v8::Arguments& args ) { + v8::Handle<v8::Object> it = args.This(); + if ( it->IsUndefined() || it == v8::Context::GetCurrent()->Global() ) { + v8::Function * f = scope->getNamedCons( "NumberInt" ); + return newInstance(f, args); + } + + if (args.Length() != 0 && args.Length() != 1) { + return v8::ThrowException( v8::String::New( "NumberInt needs 0, 1 argument" ) ); + } + + if ( args.Length() == 0 ) { + it->SetHiddenValue( scope->V8STR_NUMBERINT, v8::Number::New( 0 ) ); + } + else if ( args.Length() == 1 ) { + it->SetHiddenValue( scope->V8STR_NUMBERINT, args[0]->ToInt32() ); + } + + return it; + } + + v8::Handle<v8::Value> numberIntValueOf( V8Scope* scope, const v8::Arguments& args ) { + v8::Handle<v8::Object> it = args.This(); + int val = it->GetHiddenValue( scope->V8STR_NUMBERINT )->Int32Value(); + return v8::Number::New( double( val ) ); + } + + v8::Handle<v8::Value> numberIntToNumber( V8Scope* scope, const v8::Arguments& args ) { + return numberIntValueOf( scope, args ); + } + + v8::Handle<v8::Value> numberIntToString( V8Scope* scope, const v8::Arguments& args ) { + v8::Handle<v8::Object> it = args.This(); + + stringstream ss; + int val = it->GetHiddenValue( scope->V8STR_NUMBERINT )->Int32Value(); + ss << "NumberInt(" << val << ")"; + + string ret = ss.str(); + return v8::String::New( ret.c_str() ); + } + + v8::Handle<v8::Value> bsonsize( V8Scope* scope, const v8::Arguments& args ) { + + if ( args.Length() != 1 ) + return v8::ThrowException( v8::String::New( "bsonsize needs 1 argument" ) ); + + if ( args[0]->IsNull() ) + return v8::Number::New(0); + + if ( ! args[ 0 ]->IsObject() ) + return v8::ThrowException( v8::String::New( "argument to bsonsize has to be an object" ) ); + + return v8::Number::New( scope->v8ToMongo( args[ 0 ]->ToObject() ).objsize() ); + } + + namespace v8Locks { + boost::mutex& __interruptMutex = *( new boost::mutex ); + + InterruptLock::InterruptLock() { + __interruptMutex.lock(); + } + + InterruptLock::~InterruptLock() { + __interruptMutex.unlock(); + } + + boost::mutex& __v8Mutex = *( new boost::mutex ); + ThreadLocalValue< bool > __locked; + + RecursiveLock::RecursiveLock() : _unlock() { + if ( !__locked.get() ) { + __v8Mutex.lock(); + __locked.set( true ); + _unlock = true; + } + } + RecursiveLock::~RecursiveLock() { + if ( _unlock ) { + __v8Mutex.unlock(); + __locked.set( false ); + } + } + + RecursiveUnlock::RecursiveUnlock() : _lock() { + if ( __locked.get() ) { + __v8Mutex.unlock(); + __locked.set( false ); + _lock = true; + } + } + RecursiveUnlock::~RecursiveUnlock() { + if ( _lock ) { + __v8Mutex.lock(); + __locked.set( true ); + } + } + } // namespace v8Locks +} diff --git a/src/mongo/scripting/v8_db.h b/src/mongo/scripting/v8_db.h new file mode 100644 index 00000000000..a77115de35f --- /dev/null +++ b/src/mongo/scripting/v8_db.h @@ -0,0 +1,94 @@ +// v8_db.h + +/* Copyright 2009 10gen Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include <v8.h> +#include <cstring> +#include <cstdio> +#include <cstdlib> + +#include "engine_v8.h" + +namespace mongo { + class DBClientBase; + // These functions may depend on the caller creating a handle scope and context scope. + + v8::Handle<v8::FunctionTemplate> getMongoFunctionTemplate( V8Scope * scope, bool local ); +// void installDBTypes( V8Scope * scope, v8::Handle<v8::ObjectTemplate>& global ); + void installDBTypes( V8Scope * scope, v8::Handle<v8::Object>& global ); + + // the actual globals + + mongo::DBClientBase * getConnection( const v8::Arguments& args ); + + // Mongo members + v8::Handle<v8::Value> mongoConsLocal(V8Scope* scope, const v8::Arguments& args); + v8::Handle<v8::Value> mongoConsExternal(V8Scope* scope, const v8::Arguments& args); + + v8::Handle<v8::Value> mongoFind(V8Scope* scope, const v8::Arguments& args); + v8::Handle<v8::Value> mongoInsert(V8Scope* scope, const v8::Arguments& args); + v8::Handle<v8::Value> mongoRemove(V8Scope* scope, const v8::Arguments& args); + v8::Handle<v8::Value> mongoUpdate(V8Scope* scope, const v8::Arguments& args); + v8::Handle<v8::Value> mongoAuth(V8Scope* scope, const v8::Arguments& args); + v8::Handle<v8::Value> mongoLogout(V8Scope* scope, const v8::Arguments& args); + + v8::Handle<v8::Value> internalCursorCons(V8Scope* scope, const v8::Arguments& args); + v8::Handle<v8::Value> internalCursorNext(V8Scope* scope, const v8::Arguments& args); + v8::Handle<v8::Value> internalCursorHasNext(V8Scope* scope, const v8::Arguments& args); + v8::Handle<v8::Value> internalCursorObjsLeftInBatch(V8Scope* scope, const v8::Arguments& args); + v8::Handle<v8::Value> internalCursorReadOnly(V8Scope* scope, const v8::Arguments& args); + + // DB members + + v8::Handle<v8::Value> dbInit(V8Scope* scope, const v8::Arguments& args); + v8::Handle<v8::Value> collectionInit(V8Scope* scope, const v8::Arguments& args ); + v8::Handle<v8::Value> objectIdInit( V8Scope* scope, const v8::Arguments& args ); + + v8::Handle<v8::Value> dbRefInit( V8Scope* scope, const v8::Arguments& args ); + v8::Handle<v8::Value> dbPointerInit( V8Scope* scope, const v8::Arguments& args ); + v8::Handle<v8::Value> dbTimestampInit( V8Scope* scope, const v8::Arguments& args ); + + v8::Handle<v8::Value> binDataInit( V8Scope* scope, const v8::Arguments& args ); + v8::Handle<v8::Value> binDataToString( V8Scope* scope, const v8::Arguments& args ); + v8::Handle<v8::Value> binDataToBase64( V8Scope* scope, const v8::Arguments& args ); + v8::Handle<v8::Value> binDataToHex( V8Scope* scope, const v8::Arguments& args ); + + v8::Handle<v8::Value> uuidInit( V8Scope* scope, const v8::Arguments& args ); + v8::Handle<v8::Value> md5Init( V8Scope* scope, const v8::Arguments& args ); + v8::Handle<v8::Value> hexDataInit( V8Scope* scope, const v8::Arguments& args ); + + v8::Handle<v8::Value> numberLongInit( V8Scope* scope, const v8::Arguments& args ); + v8::Handle<v8::Value> numberLongToNumber(V8Scope* scope, const v8::Arguments& args); + v8::Handle<v8::Value> numberLongValueOf(V8Scope* scope, const v8::Arguments& args); + v8::Handle<v8::Value> numberLongToString(V8Scope* scope, const v8::Arguments& args); + + v8::Handle<v8::Value> numberIntInit( V8Scope* scope, const v8::Arguments& args ); + v8::Handle<v8::Value> numberIntToNumber(V8Scope* scope, const v8::Arguments& args); + v8::Handle<v8::Value> numberIntValueOf(V8Scope* scope, const v8::Arguments& args); + v8::Handle<v8::Value> numberIntToString(V8Scope* scope, const v8::Arguments& args); + + v8::Handle<v8::Value> dbQueryInit( V8Scope* scope, const v8::Arguments& args ); + v8::Handle<v8::Value> dbQueryIndexAccess( ::uint32_t index , const v8::AccessorInfo& info ); + + v8::Handle<v8::Value> collectionGetter( v8::Local<v8::String> name, const v8::AccessorInfo &info); + v8::Handle<v8::Value> collectionSetter( Local<v8::String> name, Local<Value> value, const AccessorInfo& info ); + + v8::Handle<v8::Value> bsonsize( V8Scope* scope, const v8::Arguments& args ); + +} + diff --git a/src/mongo/scripting/v8_utils.cpp b/src/mongo/scripting/v8_utils.cpp new file mode 100644 index 00000000000..9e7e8072220 --- /dev/null +++ b/src/mongo/scripting/v8_utils.cpp @@ -0,0 +1,295 @@ +// v8_utils.cpp + +/* Copyright 2009 10gen Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#if defined(_WIN32) +/** this is a hack - v8stdint.h defined uint16_t etc. on _WIN32 only, and that collides with + our usage of boost */ +#include "boost/cstdint.hpp" +using namespace boost; +#define V8STDINT_H_ +#endif + +#include "v8_utils.h" +#include "v8_db.h" +#include <iostream> +#include <map> +#include <sstream> +#include <vector> +#include <boost/smart_ptr.hpp> +#include <boost/thread/thread.hpp> +#include <boost/thread/xtime.hpp> +#include "engine_v8.h" + +using namespace std; +using namespace v8; + +namespace mongo { + + std::string toSTLString( const Handle<v8::Value> & o ) { + v8::String::Utf8Value str(o); + const char * foo = *str; + std::string s(foo); + return s; + } + + std::string toSTLString( const v8::TryCatch * try_catch ) { + + stringstream ss; + + //while ( try_catch ){ // disabled for v8 bleeding edge + + v8::String::Utf8Value exception(try_catch->Exception()); + Handle<v8::Message> message = try_catch->Message(); + + if (message.IsEmpty()) { + ss << *exception << endl; + } + else { + + v8::String::Utf8Value filename(message->GetScriptResourceName()); + int linenum = message->GetLineNumber(); + ss << *filename << ":" << linenum << " " << *exception << endl; + + v8::String::Utf8Value sourceline(message->GetSourceLine()); + ss << *sourceline << endl; + + int start = message->GetStartColumn(); + for (int i = 0; i < start; i++) + ss << " "; + + int end = message->GetEndColumn(); + for (int i = start; i < end; i++) + ss << "^"; + + ss << endl; + } + + //try_catch = try_catch->next_; + //} + + return ss.str(); + } + + + std::ostream& operator<<( std::ostream &s, const Handle<v8::Value> & o ) { + v8::String::Utf8Value str(o); + s << *str; + return s; + } + + std::ostream& operator<<( std::ostream &s, const v8::TryCatch * try_catch ) { + HandleScope handle_scope; + v8::String::Utf8Value exception(try_catch->Exception()); + Handle<v8::Message> message = try_catch->Message(); + + if (message.IsEmpty()) { + s << *exception << endl; + } + else { + + v8::String::Utf8Value filename(message->GetScriptResourceName()); + int linenum = message->GetLineNumber(); + cout << *filename << ":" << linenum << " " << *exception << 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; + } + + //if ( try_catch->next_ ) // disabled for v8 bleeding edge + // s << try_catch->next_; + + return s; + } + + void ReportException(v8::TryCatch* try_catch) { + cout << try_catch << endl; + } + + Handle< Context > baseContext_; + + class JSThreadConfig { + public: + JSThreadConfig( V8Scope* scope, const Arguments &args, bool newScope = false ) : started_(), done_(), newScope_( newScope ) { + 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" + i, args[i]); + } + args_ = b.obj(); + } + + ~JSThreadConfig() { + } + + void start() { + jsassert( !started_, "Thread already started" ); + // obtain own scope for execution + // do it here, not in constructor, otherwise it creates an infinite recursion from ScopedThread + _scope.reset( dynamic_cast< V8Scope * >( globalScriptEngine->newScope() ) ); + + JSThread jt( *this ); + thread_.reset( new boost::thread( jt ) ); + started_ = true; + } + void join() { + jsassert( started_ && !done_, "Thread not running" ); + thread_->join(); + done_ = true; + } + + BSONObj returnData() { + if ( !done_ ) + join(); + return returnData_; + } + + private: + class JSThread { + public: + JSThread( JSThreadConfig &config ) : config_( config ) {} + + void operator()() { + V8Scope* scope = config_._scope.get(); + v8::Isolate::Scope iscope(scope->getIsolate()); + v8::Locker l(scope->getIsolate()); + HandleScope handle_scope; + Context::Scope context_scope( scope->getContext() ); + + BSONObj args = config_.args_; + Local< v8::Function > f = v8::Function::Cast( *(scope->mongoToV8Element(args.firstElement(), true)) ); + int argc = args.nFields() - 1; + + boost::scoped_array< Local< Value > > argv( new Local< Value >[ argc ] ); + BSONObjIterator it(args); + it.next(); + for( int i = 0; i < argc; ++i ) { + argv[ i ] = Local< Value >::New( scope->mongoToV8Element(*it, true) ); + it.next(); + } + TryCatch try_catch; + Handle< Value > ret = f->Call( scope->getContext()->Global(), argc, argv.get() ); + if ( ret.IsEmpty() ) { + string e = toSTLString( &try_catch ); + log() << "js thread raised exception: " << e << endl; + // v8 probably does something sane if ret is empty, but not going to assume that for now + ret = v8::Undefined(); + } + // ret is translated to BSON to switch isolate + BSONObjBuilder b; + scope->v8ToMongoElement(b, "ret", ret); + config_.returnData_ = b.obj(); + } + + private: + JSThreadConfig &config_; + }; + + bool started_; + bool done_; + bool newScope_; + BSONObj args_; + scoped_ptr< boost::thread > thread_; + scoped_ptr< V8Scope > _scope; + BSONObj returnData_; + }; + + Handle< Value > ThreadInit( V8Scope* scope, const Arguments &args ) { + Handle<v8::Object> it = args.This(); + // NOTE I believe the passed JSThreadConfig will never be freed. If this + // policy is changed, JSThread may no longer be able to store JSThreadConfig + // by reference. + it->SetHiddenValue( v8::String::New( "_JSThreadConfig" ), External::New( new JSThreadConfig( scope, args ) ) ); + return v8::Undefined(); + } + + Handle< Value > ScopedThreadInit( V8Scope* scope, const Arguments &args ) { + Handle<v8::Object> it = args.This(); + // NOTE I believe the passed JSThreadConfig will never be freed. If this + // policy is changed, JSThread may no longer be able to store JSThreadConfig + // by reference. + it->SetHiddenValue( v8::String::New( "_JSThreadConfig" ), External::New( new JSThreadConfig( scope, args, true ) ) ); + return v8::Undefined(); + } + + JSThreadConfig *thisConfig( V8Scope* scope, const Arguments &args ) { + Local< External > c = External::Cast( *(args.This()->GetHiddenValue( v8::String::New( "_JSThreadConfig" ) ) ) ); + JSThreadConfig *config = (JSThreadConfig *)( c->Value() ); + return config; + } + + Handle< Value > ThreadStart( V8Scope* scope, const Arguments &args ) { + thisConfig( scope, args )->start(); + return v8::Undefined(); + } + + Handle< Value > ThreadJoin( V8Scope* scope, const Arguments &args ) { + thisConfig( scope, args )->join(); + return v8::Undefined(); + } + + Handle< Value > ThreadReturnData( V8Scope* scope, const Arguments &args ) { + BSONObj data = thisConfig( scope, args )->returnData(); + return scope->mongoToV8Element(data.firstElement(), true); + } + + Handle< Value > ThreadInject( V8Scope* scope, const Arguments &args ) { + jsassert( args.Length() == 1 , "threadInject takes exactly 1 argument" ); + jsassert( args[0]->IsObject() , "threadInject needs to be passed a prototype" ); + + Local<v8::Object> 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("returnData", ThreadReturnData, o); + return v8::Undefined(); + } + + Handle< Value > ScopedThreadInject( V8Scope* scope, const Arguments &args ) { + jsassert( args.Length() == 1 , "threadInject takes exactly 1 argument" ); + jsassert( args[0]->IsObject() , "threadInject needs to be passed a prototype" ); + + Local<v8::Object> o = args[0]->ToObject(); + + scope->injectV8Function("init", ScopedThreadInit, o); + // inheritance takes care of other member functions + + return v8::Undefined(); + } + + void installFork( V8Scope* scope, v8::Handle< v8::Object > &global, v8::Handle< v8::Context > &context ) { + if ( baseContext_.IsEmpty() ) // if this is the shell, first call will be with shell context, otherwise don't expect to use fork() anyway + baseContext_ = context; + scope->injectV8Function("_threadInject", ThreadInject, global); + scope->injectV8Function("_scopedThreadInject", ScopedThreadInject, global); + } + +} diff --git a/src/mongo/scripting/v8_utils.h b/src/mongo/scripting/v8_utils.h new file mode 100644 index 00000000000..dadea6bcbd3 --- /dev/null +++ b/src/mongo/scripting/v8_utils.h @@ -0,0 +1,43 @@ +// v8_utils.h + +/* Copyright 2009 10gen Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include <v8.h> + +#include <cstring> +#include <cstdio> +#include <cstdlib> +#include <assert.h> +#include <iostream> + +namespace mongo { + + void ReportException(v8::TryCatch* handler); + +#define jsassert(x,msg) verify(x) + + std::ostream& operator<<( std::ostream &s, const v8::Handle<v8::Value> & o ); + std::ostream& operator<<( std::ostream &s, const v8::Handle<v8::TryCatch> * try_catch ); + + std::string toSTLString( const v8::Handle<v8::Value> & o ); + std::string toSTLString( const v8::TryCatch * try_catch ); + + class V8Scope; + void installFork( V8Scope* scope, v8::Handle< v8::Object > &global, v8::Handle< v8::Context > &context ); +} + diff --git a/src/mongo/scripting/v8_wrapper.cpp b/src/mongo/scripting/v8_wrapper.cpp new file mode 100644 index 00000000000..b3bb7857ac3 --- /dev/null +++ b/src/mongo/scripting/v8_wrapper.cpp @@ -0,0 +1,91 @@ +// v8_wrapper.cpp + +/* Copyright 2009 10gen Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "v8_wrapper.h" +#include "v8_utils.h" +#include "v8_db.h" +#include "engine_v8.h" + +#include <iostream> + +using namespace std; +using namespace v8; + +namespace mongo { + +#define DDD(x) + + // --- object wrapper --- + + class WrapperHolder { + public: + WrapperHolder( V8Scope* scope, const BSONObj * o , bool readOnly , bool iDelete ) + : _scope(scope), _o(o), _readOnly( readOnly ), _iDelete( iDelete ) { + } + + ~WrapperHolder() { + if ( _o && _iDelete ) { + delete _o; + } + _o = 0; + } + + v8::Handle<v8::Value> get( v8::Local<v8::String> name ) { + const string& s = toSTLString( name ); + const BSONElement& e = _o->getField( s ); + return _scope->mongoToV8Element(e); + } + + V8Scope* _scope; + const BSONObj * _o; + bool _readOnly; + bool _iDelete; + }; + + WrapperHolder * createWrapperHolder( V8Scope* scope, const BSONObj * o , bool readOnly , bool iDelete ) { + return new WrapperHolder( scope, o , readOnly , iDelete ); + } + + WrapperHolder * getWrapper( v8::Handle<v8::Object> o ) { + Handle<v8::Value> t = o->GetRealNamedProperty( v8::String::New( "_wrapper" ) ); + verify( t->IsExternal() ); + Local<External> c = External::Cast( *t ); + WrapperHolder * w = (WrapperHolder*)(c->Value()); + verify( w ); + return w; + } + + + Handle<Value> wrapperCons(V8Scope* scope, const Arguments& args) { + if ( ! ( args.Length() == 1 && args[0]->IsExternal() ) ) + return v8::ThrowException( v8::String::New( "wrapperCons needs 1 External arg" ) ); + + args.This()->Set( v8::String::New( "_wrapper" ) , args[0] ); + + return v8::Undefined(); + } + + v8::Handle<v8::Value> wrapperGetHandler( v8::Local<v8::String> name, const v8::AccessorInfo &info) { + return getWrapper( info.This() )->get( name ); + } + + v8::Handle<v8::FunctionTemplate> getObjectWrapperTemplate(V8Scope* scope) { + v8::Handle<v8::FunctionTemplate> t = scope->createV8Function(wrapperCons); + t->InstanceTemplate()->SetNamedPropertyHandler( wrapperGetHandler ); + return t; + } +} diff --git a/src/mongo/scripting/v8_wrapper.h b/src/mongo/scripting/v8_wrapper.h new file mode 100644 index 00000000000..22f14e6ae94 --- /dev/null +++ b/src/mongo/scripting/v8_wrapper.h @@ -0,0 +1,34 @@ +// v8_wrapper.h + +/* Copyright 2009 10gen Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include <v8.h> +#include <cstring> +#include <cstdio> +#include <cstdlib> +#include "../db/jsobj.h" +#include "engine_v8.h" + +namespace mongo { + + v8::Handle<v8::FunctionTemplate> getObjectWrapperTemplate(V8Scope* scope); + + class WrapperHolder; + WrapperHolder * createWrapperHolder( V8Scope* scope, const BSONObj * o , bool readOnly , bool iDelete ); + +} |
