// engine.cpp
/* Copyright 2009 10gen Inc.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License, version 3,
* as published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see .
*
* As a special exception, the copyright holders give permission to link the
* code of portions of this program with the OpenSSL library under certain
* conditions as described in each individual source file and distribute
* linked combinations including the program with the OpenSSL library. You
* must comply with the GNU Affero General Public License in all respects
* for all of the code used other than as permitted herein. If you modify
* file(s) with this exception, you may extend this exception to your
* version of the file(s), but you are not obligated to do so. If you do not
* wish to do so, delete this exception statement from your version. If you
* delete this exception statement from all source files in the program,
* then also delete it in the license file.
*/
#define MONGO_LOG_DEFAULT_COMPONENT ::mongo::logger::LogComponent::kDefault
#include "mongo/platform/basic.h"
#include "mongo/scripting/engine.h"
#include
#include
#include "mongo/client/dbclientcursor.h"
#include "mongo/client/dbclientinterface.h"
#include "mongo/db/operation_context.h"
#include "mongo/db/service_context.h"
#include "mongo/platform/unordered_set.h"
#include "mongo/scripting/dbdirectclient_factory.h"
#include "mongo/util/fail_point_service.h"
#include "mongo/util/file.h"
#include "mongo/util/log.h"
#include "mongo/util/text.h"
namespace mongo {
using std::set;
using std::shared_ptr;
using std::string;
using std::unique_ptr;
AtomicInt64 Scope::_lastVersion(1);
namespace {
MONGO_FP_DECLARE(mr_killop_test_fp);
// 2 GB is the largest support Javascript file size.
const fileofs kMaxJsFileLength = fileofs(2) * 1024 * 1024 * 1024;
const ServiceContext::Decoration> forService =
ServiceContext::declareDecoration>();
static std::unique_ptr globalScriptEngine;
} // namespace
ScriptEngine::ScriptEngine() : _scopeInitCallback() {}
ScriptEngine::~ScriptEngine() {}
Scope::Scope()
: _localDBName(""), _loadedVersion(0), _numTimesUsed(0), _lastRetIsNativeCode(false) {}
Scope::~Scope() {}
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 NumberDecimal:
builder.append(fieldName, getNumberDecimal(scopeName));
break;
case String:
builder.append(fieldName, getString(scopeName));
break;
case Bool:
builder.appendBool(fieldName, getBoolean(scopeName));
break;
case jstNULL:
case Undefined:
builder.appendNull(fieldName);
break;
case Date:
builder.appendDate(fieldName,
Date_t::fromMillisSinceEpoch(getNumberLongLong(scopeName)));
break;
case Code:
builder.appendCode(fieldName, getString(scopeName));
break;
default:
uassert(10206, str::stream() << "can't append type from: " << t, 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, int timeoutMs) {
#ifdef _WIN32
boost::filesystem::path p(toWideString(filename.c_str()));
#else
boost::filesystem::path p(filename);
#endif
if (!exists(p)) {
error() << "file [" << filename << "] doesn't exist";
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 (!str::endsWith(sub.string().c_str(), ".js"))
continue;
if (!execFile(sub.string(), printResult, reportError, timeoutMs))
return false;
}
if (empty) {
error() << "directory [" << filename << "] doesn't have any *.js files";
return false;
}
return true;
}
File f;
f.open(filename.c_str(), true);
if (!f.is_open() || f.bad())
return false;
fileofs fo = f.len();
if (fo > kMaxJsFileLength) {
warning() << "attempted to execute javascript file larger than 2GB";
return false;
}
unsigned len = static_cast(fo);
std::unique_ptr data(new char[len + 1]);
data[len] = 0;
f.read(0, data.get(), len);
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, len - offset);
return exec(code, filename, printResult, reportError, false, timeoutMs);
}
class Scope::StoredFuncModLogOpHandler : public RecoveryUnit::Change {
public:
void commit() {
_lastVersion.fetchAndAdd(1);
}
void rollback() {}
};
void Scope::storedFuncMod(OperationContext* opCtx) {
opCtx->recoveryUnit()->registerChange(new StoredFuncModLogOpHandler());
}
void Scope::validateObjectIdString(const string& str) {
uassert(10448, "invalid object id: length", str.size() == 24);
for (size_t i = 0; i < str.size(); i++)
uassert(10430, "invalid object id: not hex", std::isxdigit(str.at(i)));
}
void Scope::loadStored(OperationContext* opCtx, bool ignoreNotConnected) {
if (_localDBName.size() == 0) {
if (ignoreNotConnected)
return;
uassert(10208, "need to have locallyConnected already", _localDBName.size());
}
int64_t lastVersion = _lastVersion.load();
if (_loadedVersion == lastVersion)
return;
_loadedVersion = lastVersion;
string coll = _localDBName + ".system.js";
auto directDBClient = DBDirectClientFactory::get(opCtx).create(opCtx);
unique_ptr c =
directDBClient->query(coll, Query(), 0, 0, NULL, QueryOption_SlaveOk, 0);
massert(16669, "unable to get db client cursor from query", c.get());
set thisTime;
while (c->more()) {
BSONObj o = c->nextSafe().getOwned();
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);
if (MONGO_FAIL_POINT(mr_killop_test_fp)) {
/* This thread sleep makes the interrupts in the test come in at a time
* where the js misses the interrupt and throw an exception instead of
* being interrupted
*/
stdx::this_thread::sleep_for(stdx::chrono::seconds(1));
}
try {
setElement(n.valuestr(), v, o);
thisTime.insert(n.valuestr());
_storedNames.insert(n.valuestr());
} catch (const DBException& setElemEx) {
if (setElemEx.code() == ErrorCodes::Interrupted) {
throw;
}
error() << "unable to load stored JavaScript function " << n.valuestr()
<< "(): " << redact(setElemEx);
}
}
// remove things from scope that were removed from the system.js collection
for (set::iterator i = _storedNames.begin(); i != _storedNames.end();) {
if (thisTime.count(*i) == 0) {
string toDelete = str::stream() << "delete " << *i;
_storedNames.erase(i++);
execSetup(toDelete, "clean up scope");
} else {
++i;
}
}
}
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++;
}
}
FunctionCacheMap::iterator i = _cachedFunctions.find(code);
if (i != _cachedFunctions.end())
return i->second;
// Get a function number, so the cache can be utilized to lookup the source on an exception
ScriptingFunction functionNumber = _createFunction(code);
_cachedFunctions[code] = functionNumber;
return functionNumber;
}
namespace JSFiles {
extern const JSFile collection;
extern const JSFile crud_api;
extern const JSFile db;
extern const JSFile explain_query;
extern const JSFile explainable;
extern const JSFile mongo;
extern const JSFile mr;
extern const JSFile session;
extern const JSFile query;
extern const JSFile utils;
extern const JSFile utils_sh;
extern const JSFile utils_auth;
extern const JSFile bulk_api;
extern const JSFile error_codes;
}
void Scope::execCoreFiles() {
execSetup(JSFiles::utils);
execSetup(JSFiles::utils_sh);
execSetup(JSFiles::utils_auth);
execSetup(JSFiles::db);
execSetup(JSFiles::mongo);
execSetup(JSFiles::mr);
execSetup(JSFiles::session);
execSetup(JSFiles::query);
execSetup(JSFiles::bulk_api);
execSetup(JSFiles::error_codes);
execSetup(JSFiles::collection);
execSetup(JSFiles::crud_api);
execSetup(JSFiles::explain_query);
execSetup(JSFiles::explainable);
}
namespace {
class ScopeCache {
public:
void release(const string& poolName, const std::shared_ptr& scope) {
stdx::lock_guard lk(_mutex);
if (scope->hasOutOfMemoryException()) {
// make some room
log() << "Clearing all idle JS contexts due to out of memory";
_pools.clear();
return;
}
if (scope->getTimesUsed() > kMaxScopeReuse)
return; // used too many times to save
if (!scope->getError().empty())
return; // not saving errored scopes
if (_pools.size() >= kMaxPoolSize) {
// prefer to keep recently-used scopes
_pools.pop_back();
}
scope->reset();
ScopeAndPool toStore = {scope, poolName};
_pools.push_front(toStore);
}
std::shared_ptr tryAcquire(OperationContext* opCtx, const string& poolName) {
stdx::lock_guard lk(_mutex);
for (Pools::iterator it = _pools.begin(); it != _pools.end(); ++it) {
if (it->poolName == poolName) {
std::shared_ptr scope = it->scope;
_pools.erase(it);
scope->incTimesUsed();
scope->reset();
scope->registerOperation(opCtx);
return scope;
}
}
return std::shared_ptr();
}
void clear() {
stdx::lock_guard lk(_mutex);
_pools.clear();
}
private:
struct ScopeAndPool {
std::shared_ptr scope;
string poolName;
};
// Note: if these numbers change, reconsider choice of datastructure for _pools
static const unsigned kMaxPoolSize = 10;
static const int kMaxScopeReuse = 10;
typedef std::deque Pools; // More-recently used Scopes are kept at the front.
Pools _pools; // protected by _mutex
stdx::mutex _mutex;
};
ScopeCache scopeCache;
} // anonymous namespace
void ScriptEngine::dropScopeCache() {
scopeCache.clear();
}
class PooledScope : public Scope {
public:
PooledScope(const std::string& pool, const std::shared_ptr& real)
: _pool(pool), _real(real) {}
virtual ~PooledScope() {
scopeCache.release(_pool, _real);
}
// wrappers for the derived (_real) scope
void reset() {
_real->reset();
}
void registerOperation(OperationContext* opCtx) {
_real->registerOperation(opCtx);
}
void unregisterOperation() {
_real->unregisterOperation();
}
void init(const BSONObj* data) {
_real->init(data);
}
void localConnectForDbEval(OperationContext* opCtx, const char* dbName) {
invariant(!"localConnectForDbEval should only be called from dbEval");
}
void setLocalDB(const string& dbName) {
_real->setLocalDB(dbName);
}
void loadStored(OperationContext* opCtx, bool ignoreNotConnected = false) {
_real->loadStored(opCtx, ignoreNotConnected);
}
void externalSetup() {
_real->externalSetup();
}
void gc() {
_real->gc();
}
void advanceGeneration() {
_real->advanceGeneration();
}
void requireOwnedObjects() override {
_real->requireOwnedObjects();
}
bool isKillPending() const {
return _real->isKillPending();
}
int type(const char* field) {
return _real->type(field);
}
string getError() {
return _real->getError();
}
bool hasOutOfMemoryException() {
return _real->hasOutOfMemoryException();
}
void rename(const char* from, const char* to) {
_real->rename(from, to);
}
double getNumber(const char* field) {
return _real->getNumber(field);
}
int getNumberInt(const char* field) {
return _real->getNumberInt(field);
}
long long getNumberLongLong(const char* field) {
return _real->getNumberLongLong(field);
}
Decimal128 getNumberDecimal(const char* field) {
return _real->getNumberDecimal(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);
}
void setNumber(const char* field, double val) {
_real->setNumber(field, val);
}
void setString(const char* field, StringData val) {
_real->setString(field, val);
}
void setElement(const char* field, const BSONElement& val, const BSONObj& parent) {
_real->setElement(field, val, parent);
}
void setObject(const char* field, const BSONObj& obj, bool readOnly = true) {
_real->setObject(field, obj, readOnly);
}
bool isLastRetNativeCode() {
return _real->isLastRetNativeCode();
}
void setBoolean(const char* field, bool val) {
_real->setBoolean(field, val);
}
void setFunction(const char* field, const char* code) {
_real->setFunction(field, code);
}
ScriptingFunction createFunction(const char* code) {
return _real->createFunction(code);
}
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);
}
bool exec(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, int timeoutMs = 0) {
return _real->execFile(filename, printResult, reportError, timeoutMs);
}
void injectNative(const char* field, NativeFunction func, void* data) {
_real->injectNative(field, func, data);
}
void append(BSONObjBuilder& builder, const char* fieldName, const char* scopeName) {
_real->append(builder, fieldName, scopeName);
}
protected:
ScriptingFunction _createFunction(const char* code) {
return _real->_createFunction(code);
}
private:
string _pool;
std::shared_ptr _real;
};
/** Get a scope from the pool of scopes matching the supplied pool name */
unique_ptr ScriptEngine::getPooledScope(OperationContext* opCtx,
const string& db,
const string& scopeType) {
const string fullPoolName = db + scopeType;
std::shared_ptr s = scopeCache.tryAcquire(opCtx, fullPoolName);
if (!s) {
s.reset(newScope());
s->registerOperation(opCtx);
}
unique_ptr p;
p.reset(new PooledScope(fullPoolName, s));
p->setLocalDB(db);
p->loadStored(opCtx, true);
return p;
}
void (*ScriptEngine::_connectCallback)(DBClientBase&) = 0;
ScriptEngine* getGlobalScriptEngine() {
if (hasGlobalServiceContext())
return forService(getGlobalServiceContext()).get();
else
return globalScriptEngine.get();
}
void setGlobalScriptEngine(ScriptEngine* impl) {
if (hasGlobalServiceContext())
forService(getGlobalServiceContext()).reset(impl);
else
globalScriptEngine.reset(impl);
}
bool hasJSReturn(const string& code) {
size_t x = code.find("return");
if (x == string::npos)
return false;
int quoteCount = 0;
int singleQuoteCount = 0;
for (size_t i = 0; i < x; i++) {
if (code[i] == '"') {
quoteCount++;
} else if (code[i] == '\'') {
singleQuoteCount++;
}
}
// if we are in either single quotes or double quotes return false
if (quoteCount % 2 != 0 || singleQuoteCount % 2 != 0) {
return false;
}
// return is at start OR preceded by space
// AND return is not followed by digit or letter
return (x == 0 || isspace(code[x - 1])) && !(isalpha(code[x + 6]) || isdigit(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;
}
}