summaryrefslogtreecommitdiff
path: root/src/mongo/scripting/mozjs/implscope.cpp
diff options
context:
space:
mode:
Diffstat (limited to 'src/mongo/scripting/mozjs/implscope.cpp')
-rw-r--r--src/mongo/scripting/mozjs/implscope.cpp728
1 files changed, 728 insertions, 0 deletions
diff --git a/src/mongo/scripting/mozjs/implscope.cpp b/src/mongo/scripting/mozjs/implscope.cpp
new file mode 100644
index 00000000000..0d5d3675603
--- /dev/null
+++ b/src/mongo/scripting/mozjs/implscope.cpp
@@ -0,0 +1,728 @@
+/**
+ * Copyright (C) 2015 MongoDB 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/>.
+ *
+ * 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::kQuery
+
+#include "mongo/platform/basic.h"
+
+#include "mongo/scripting/mozjs/implscope.h"
+
+#include <jscustomallocator.h>
+#include <jsfriendapi.h>
+
+#include "mongo/base/error_codes.h"
+#include "mongo/db/operation_context.h"
+#include "mongo/scripting/mozjs/objectwrapper.h"
+#include "mongo/scripting/mozjs/valuereader.h"
+#include "mongo/scripting/mozjs/valuewriter.h"
+#include "mongo/stdx/mutex.h"
+#include "mongo/util/concurrency/threadlocal.h"
+#include "mongo/util/log.h"
+
+using namespace mongoutils;
+
+namespace mongo {
+
+// Generated symbols for JS files
+namespace JSFiles {
+extern const JSFile types;
+extern const JSFile assert;
+} // namespace
+
+namespace mozjs {
+
+const char* const MozJSImplScope::kExecResult = "__lastres__";
+const char* const MozJSImplScope::kInvokeResult = "__returnValue";
+
+namespace {
+
+/**
+ * The maximum amount of memory to be given out per thread to mozilla. We
+ * manage this by trapping all calls to malloc, free, etc. and keeping track of
+ * counts in some thread locals
+ */
+const size_t kMallocMemoryLimit = 1024ul * 1024 * 1024 * 1.1;
+
+/**
+ * The number of bytes to allocate after which garbage collection is run
+ */
+const int kMaxBytesBeforeGC = 8 * 1024 * 1024;
+
+/**
+ * The size, in bytes, of each "stack chunk". 8192 is the recommended amount
+ * from mozilla
+ */
+const int kStackChunkSize = 8192;
+
+/**
+ * Runtime's can race on first creation (on some function statics), so we just
+ * serialize the initial Runtime creation.
+ */
+stdx::mutex gRuntimeCreationMutex;
+bool gFirstRuntimeCreated = false;
+
+} // namespace
+
+MONGO_TRIVIALLY_CONSTRUCTIBLE_THREAD_LOCAL MozJSImplScope* kCurrentScope;
+
+struct MozJSImplScope::MozJSEntry {
+ MozJSEntry(MozJSImplScope* scope) : ar(scope->_context), ac(scope->_context, scope->_global) {}
+
+ JSAutoRequest ar;
+ JSAutoCompartment ac;
+};
+
+void MozJSImplScope::_reportError(JSContext* cx, const char* message, JSErrorReport* report) {
+ auto scope = getScope(cx);
+
+ if (!JSREPORT_IS_WARNING(report->flags)) {
+ scope->_status =
+ Status(report->errorNumber ? static_cast<ErrorCodes::Error>(report->errorNumber)
+ : ErrorCodes::JSInterpreterFailure,
+ str::stream() << message << ":\n"
+ << JS::FormatStackDump(cx, nullptr, true, true, false) << "\n");
+ }
+}
+
+std::string MozJSImplScope::getError() {
+ return "";
+}
+
+void MozJSImplScope::registerOperation(OperationContext* txn) {
+ invariant(_opId == 0);
+ _opId = txn->getOpID();
+
+ _engine->registerOperation(txn, this);
+}
+
+void MozJSImplScope::unregisterOperation() {
+ if (_opId != 0) {
+ _engine->unregisterOperation(_opId);
+
+ _opId = 0;
+ }
+}
+
+void MozJSImplScope::kill() {
+ _pendingKill.store(true);
+ JS_RequestInterruptCallback(_runtime);
+}
+
+bool MozJSImplScope::isKillPending() const {
+ return _pendingKill.load();
+}
+
+OperationContext* MozJSImplScope::getOpContext() const {
+ return _opCtx;
+}
+
+bool MozJSImplScope::_interruptCallback(JSContext* cx) {
+ auto scope = getScope(cx);
+
+ if (scope->_pendingGC.load()) {
+ JS_GC(scope->_runtime);
+ }
+
+ bool kill = scope->isKillPending();
+
+ if (kill) {
+ scope->_engine->getDeadlineMonitor().stopDeadline(scope);
+ scope->unregisterOperation();
+ }
+
+ return !kill;
+}
+
+void MozJSImplScope::_gcCallback(JSRuntime* rt, JSGCStatus status, void* data) {
+ if (!shouldLog(logger::LogSeverity::Debug(1))) {
+ // don't collect stats unless verbose
+ return;
+ }
+
+ log() << "MozJS GC " << (status == JSGC_BEGIN ? "prologue" : "epilogue") << " heap stats - "
+ << " total: " << mongo::sm::get_total_bytes() << " limit: " << mongo::sm::get_max_bytes()
+ << std::endl;
+}
+
+MozJSImplScope::MozRuntime::MozRuntime() {
+ mongo::sm::reset(kMallocMemoryLimit);
+
+ {
+ stdx::unique_lock<stdx::mutex> lk(gRuntimeCreationMutex);
+
+ if (gFirstRuntimeCreated) {
+ // If we've already made a runtime, just proceed
+ lk.unlock();
+ } else {
+ // If this is the first one, hold the lock until after the first
+ // one's done
+ gFirstRuntimeCreated = true;
+ }
+
+ _runtime = JS_NewRuntime(kMaxBytesBeforeGC);
+ }
+
+ uassert(ErrorCodes::JSInterpreterFailure, "Failed to initialize JSRuntime", _runtime);
+
+ _context = JS_NewContext(_runtime, kStackChunkSize);
+ uassert(ErrorCodes::JSInterpreterFailure, "Failed to initialize JSContext", _context);
+}
+
+MozJSImplScope::MozRuntime::~MozRuntime() {
+ JS_DestroyContext(_context);
+ JS_DestroyRuntime(_runtime);
+}
+
+MozJSImplScope::MozJSImplScope(MozJSScriptEngine* engine)
+ : _engine(engine),
+ _mr(),
+ _runtime(_mr._runtime),
+ _context(_mr._context),
+ _globalProto(_context),
+ _global(_globalProto.getProto()),
+ _funcs(),
+ _pendingKill(false),
+ _opId(0),
+ _opCtx(nullptr),
+ _pendingGC(false),
+ _connectState(ConnectState::Not),
+ _status(Status::OK()),
+ _binDataProto(_context),
+ _bsonProto(_context),
+ _countDownLatchProto(_context),
+ _cursorProto(_context),
+ _dbCollectionProto(_context),
+ _dbPointerProto(_context),
+ _dbQueryProto(_context),
+ _dbProto(_context),
+ _dbRefProto(_context),
+ _jsThreadProto(_context),
+ _maxKeyProto(_context),
+ _minKeyProto(_context),
+ _mongoExternalProto(_context),
+ _mongoLocalProto(_context),
+ _nativeFunctionProto(_context),
+ _numberIntProto(_context),
+ _numberLongProto(_context),
+ _objectProto(_context),
+ _oidProto(_context),
+ _regExpProto(_context),
+ _timestampProto(_context) {
+ kCurrentScope = this;
+
+ // The default is quite low and doesn't seem to directly correlate with
+ // malloc'd bytes. Set it to MAX_INT here and catching things in the
+ // jscustomallocator.cpp
+ JS_SetGCParameter(_runtime, JSGC_MAX_BYTES, 0xffffffff);
+
+ JS_SetInterruptCallback(_runtime, _interruptCallback);
+ JS_SetGCCallback(_runtime, _gcCallback, this);
+ JS_SetContextPrivate(_context, this);
+ JSAutoRequest ar(_context);
+
+ JS_SetErrorReporter(_runtime, _reportError);
+
+ JSAutoCompartment ac(_context, _global);
+
+ _checkErrorState(JS_InitStandardClasses(_context, _global));
+
+ installBSONTypes();
+ execSetup(JSFiles::assert);
+ execSetup(JSFiles::types);
+
+ // install process-specific utilities in the global scope (dependancy: types.js, assert.js)
+ if (_engine->getScopeInitCallback())
+ _engine->getScopeInitCallback()(*this);
+
+ // install global utility functions
+ installGlobalUtils(*this);
+}
+
+MozJSImplScope::~MozJSImplScope() {
+ for (auto&& x : _funcs) {
+ x.reset();
+ }
+
+ unregisterOperation();
+}
+
+bool MozJSImplScope::hasOutOfMemoryException() {
+ return false;
+}
+
+void MozJSImplScope::init(const BSONObj* data) {
+ if (!data)
+ return;
+
+ BSONObjIterator i(*data);
+ while (i.more()) {
+ BSONElement e = i.next();
+ setElement(e.fieldName(), e);
+ }
+}
+
+void MozJSImplScope::setNumber(const char* field, double val) {
+ MozJSEntry entry(this);
+
+ ObjectWrapper(_context, _global).setNumber(field, val);
+}
+
+void MozJSImplScope::setString(const char* field, StringData val) {
+ MozJSEntry entry(this);
+
+ ObjectWrapper(_context, _global).setString(field, val);
+}
+
+void MozJSImplScope::setBoolean(const char* field, bool val) {
+ MozJSEntry entry(this);
+
+ ObjectWrapper(_context, _global).setBoolean(field, val);
+}
+
+void MozJSImplScope::setElement(const char* field, const BSONElement& e) {
+ MozJSEntry entry(this);
+
+ ObjectWrapper(_context, _global).setBSONElement(field, e, false);
+}
+
+void MozJSImplScope::setObject(const char* field, const BSONObj& obj, bool readOnly) {
+ MozJSEntry entry(this);
+
+ ObjectWrapper(_context, _global).setBSON(field, obj, readOnly);
+}
+
+int MozJSImplScope::type(const char* field) {
+ MozJSEntry entry(this);
+
+ return ObjectWrapper(_context, _global).type(field);
+}
+
+double MozJSImplScope::getNumber(const char* field) {
+ MozJSEntry entry(this);
+
+ return ObjectWrapper(_context, _global).getNumber(field);
+}
+
+int MozJSImplScope::getNumberInt(const char* field) {
+ MozJSEntry entry(this);
+
+ return ObjectWrapper(_context, _global).getNumberInt(field);
+}
+
+long long MozJSImplScope::getNumberLongLong(const char* field) {
+ MozJSEntry entry(this);
+
+ return ObjectWrapper(_context, _global).getNumberLongLong(field);
+}
+
+std::string MozJSImplScope::getString(const char* field) {
+ MozJSEntry entry(this);
+
+ return ObjectWrapper(_context, _global).getString(field);
+}
+
+bool MozJSImplScope::getBoolean(const char* field) {
+ MozJSEntry entry(this);
+
+ return ObjectWrapper(_context, _global).getBoolean(field);
+}
+
+BSONObj MozJSImplScope::getObject(const char* field) {
+ MozJSEntry entry(this);
+
+ return ObjectWrapper(_context, _global).getObject(field);
+}
+
+void MozJSImplScope::newFunction(StringData raw, JS::MutableHandleValue out) {
+ MozJSEntry entry(this);
+
+ std::string code = str::stream() << "____MongoToSM_newFunction_temp = " << raw;
+
+ JS::CompileOptions co(_context);
+ setCompileOptions(&co);
+ _checkErrorState(JS::Evaluate(_context, _global, co, code.c_str(), code.length(), out));
+}
+
+BSONObj MozJSImplScope::callThreadArgs(const BSONObj& args) {
+ MozJSEntry entry(this);
+
+ JS::RootedValue function(_context);
+ ValueReader(_context, &function).fromBSONElement(args.firstElement(), true);
+
+ int argc = args.nFields() - 1;
+
+ JS::AutoValueVector argv(_context);
+ BSONObjIterator it(args);
+ it.next();
+ JS::RootedValue value(_context);
+
+ for (int i = 0; i < argc; ++i) {
+ ValueReader(_context, &value).fromBSONElement(*it, true);
+ argv.append(value);
+ it.next();
+ }
+
+ JS::RootedValue out(_context);
+ JS::RootedObject thisv(_context);
+
+ bool success = JS::Call(_context, thisv, function, argv, &out);
+
+ if (!success) {
+ auto status = currentJSExceptionToStatus(
+ _context, ErrorCodes::JSInterpreterFailure, "Unknown callThread failure");
+
+ log() << "js thread raised js exception: " << status;
+
+ uasserted(status.code(), status.reason());
+ }
+
+ BSONObjBuilder b;
+ ValueWriter(_context, out).writeThis(&b, "ret");
+
+ return b.obj();
+}
+
+bool hasFunctionIdentifier(StringData code) {
+ if (code.size() < 9 || code.find("function") != 0)
+ return false;
+
+ return code[8] == ' ' || code[8] == '(';
+}
+
+// TODO: This function identification code is broken. Fix it up to be more robust
+//
+// See: SERVER-16703 for more info
+void MozJSImplScope::_MozJSCreateFunction(const char* raw,
+ ScriptingFunction functionNumber,
+ JS::MutableHandleValue fun) {
+ std::string code = jsSkipWhiteSpace(raw);
+ if (!hasFunctionIdentifier(code)) {
+ if (code.find('\n') == std::string::npos && !hasJSReturn(code) &&
+ (code.find(';') == std::string::npos || code.find(';') == code.size() - 1)) {
+ code = "return " + code;
+ }
+ code = "function(){ " + code + "}";
+ }
+
+ code = str::stream() << "_funcs" << functionNumber << " = " << code;
+
+ JS::CompileOptions co(_context);
+ setCompileOptions(&co);
+
+ _checkErrorState(JS::Evaluate(_context, _global, co, code.c_str(), code.length(), fun));
+ uassert(10232,
+ "not a function",
+ fun.isObject() && JS_ObjectIsFunction(_context, fun.toObjectOrNull()));
+}
+
+ScriptingFunction MozJSImplScope::_createFunction(const char* raw,
+ ScriptingFunction functionNumber) {
+ MozJSEntry entry(this);
+
+ JS::RootedValue fun(_context);
+ _MozJSCreateFunction(raw, functionNumber, &fun);
+ _funcs.emplace_back(_context, fun.get());
+
+ return functionNumber;
+}
+
+void MozJSImplScope::setFunction(const char* field, const char* code) {
+ MozJSEntry entry(this);
+
+ JS::RootedValue fun(_context);
+
+ _MozJSCreateFunction(code, getFunctionCache().size() + 1, &fun);
+
+ ObjectWrapper(_context, _global).setValue(field, fun);
+}
+
+void MozJSImplScope::rename(const char* from, const char* to) {
+ MozJSEntry entry(this);
+
+ ObjectWrapper(_context, _global).rename(from, to);
+}
+
+int MozJSImplScope::invoke(ScriptingFunction func,
+ const BSONObj* argsObject,
+ const BSONObj* recv,
+ int timeoutMs,
+ bool ignoreReturn,
+ bool readOnlyArgs,
+ bool readOnlyRecv) {
+ MozJSEntry entry(this);
+
+ auto funcValue = _funcs[func - 1];
+ JS::RootedValue result(_context);
+
+ const int nargs = argsObject ? argsObject->nFields() : 0;
+
+ JS::AutoValueVector args(_context);
+
+ if (nargs) {
+ BSONObjIterator it(*argsObject);
+ for (int i = 0; i < nargs; i++) {
+ BSONElement next = it.next();
+
+ JS::RootedValue value(_context);
+ ValueReader(_context, &value).fromBSONElement(next, readOnlyArgs);
+
+ args.append(value);
+ }
+ }
+
+ JS::RootedValue smrecv(_context);
+ if (recv)
+ ValueReader(_context, &smrecv).fromBSON(*recv, readOnlyRecv);
+ else
+ smrecv.setObjectOrNull(_global);
+
+ if (timeoutMs)
+ _engine->getDeadlineMonitor().startDeadline(this, timeoutMs);
+
+ JS::RootedValue out(_context);
+ JS::RootedObject obj(_context, smrecv.toObjectOrNull());
+
+ bool success = JS::Call(_context, obj, funcValue, args, &out);
+
+ if (timeoutMs)
+ _engine->getDeadlineMonitor().stopDeadline(this);
+
+ _checkErrorState(success);
+
+ if (!ignoreReturn) {
+ // must validate the handle because TerminateExecution may have
+ // been thrown after the above checks
+ if (out.isObject() && _nativeFunctionProto.instanceOf(out)) {
+ warning() << "storing native function as return value";
+ _lastRetIsNativeCode = true;
+ } else {
+ _lastRetIsNativeCode = false;
+ }
+
+ ObjectWrapper(_context, _global).setValue(kInvokeResult, out);
+ }
+
+ return 0;
+}
+
+bool MozJSImplScope::exec(StringData code,
+ const std::string& name,
+ bool printResult,
+ bool reportError,
+ bool assertOnError,
+ int timeoutMs) {
+ MozJSEntry entry(this);
+
+ JS::CompileOptions co(_context);
+ setCompileOptions(&co);
+ JS::RootedScript script(_context);
+
+ bool success = JS::Compile(_context, _global, co, code.rawData(), code.size(), &script);
+
+ if (_checkErrorState(success, reportError, assertOnError))
+ return false;
+
+ if (timeoutMs)
+ _engine->getDeadlineMonitor().startDeadline(this, timeoutMs);
+
+ JS::RootedValue out(_context);
+
+ success = JS_ExecuteScript(_context, _global, script, &out);
+
+ if (timeoutMs)
+ _engine->getDeadlineMonitor().stopDeadline(this);
+
+ if (_checkErrorState(success, reportError, assertOnError))
+ return false;
+
+ ObjectWrapper(_context, _global).setValue(kExecResult, out);
+
+ if (printResult && !out.isUndefined()) {
+ // TODO: We seem to use this productively in v8, but it seems
+ // unecessary under sm. That probably means somethings off
+ //
+ // appears to only be used by shell
+ // std::cout << ValueWriter(_context, out).toString() << std::endl;
+ }
+
+ return true;
+}
+
+void MozJSImplScope::injectNative(const char* field, NativeFunction func, void* data) {
+ MozJSEntry entry(this);
+
+ JS::RootedObject obj(_context);
+
+ NativeFunctionInfo::make(_context, &obj, func, data);
+
+ JS::RootedValue value(_context);
+ value.setObjectOrNull(obj);
+ ObjectWrapper(_context, _global).setValue(field, value);
+}
+
+void MozJSImplScope::gc() {
+ _pendingGC.store(true);
+ JS_RequestInterruptCallback(_runtime);
+}
+
+void MozJSImplScope::localConnectForDbEval(OperationContext* txn, const char* dbName) {
+ MozJSEntry entry(this);
+
+ invariant(_opCtx == NULL);
+ _opCtx = txn;
+
+ if (_connectState == ConnectState::External)
+ uasserted(12510, "externalSetup already called, can't call localConnect");
+ if (_connectState == ConnectState::Local) {
+ if (_localDBName == dbName)
+ return;
+ uasserted(12511,
+ str::stream() << "localConnect previously called with name " << _localDBName);
+ }
+
+ // NOTE: order is important here. the following methods must be called after
+ // the above conditional statements.
+
+ // install db access functions in the global object
+ installDBAccess();
+
+ // install the Mongo function object and instantiate the 'db' global
+ _mongoLocalProto.install(_global);
+ execCoreFiles();
+
+ const char* const makeMongo = "_mongo = new Mongo()";
+ exec(makeMongo, "local connect 2", false, true, true, 0);
+
+ std::string makeDB = str::stream() << "db = _mongo.getDB(\"" << dbName << "\");";
+ exec(makeDB, "local connect 3", false, true, true, 0);
+
+ _connectState = ConnectState::Local;
+ _localDBName = dbName;
+
+ loadStored(txn);
+}
+
+void MozJSImplScope::externalSetup() {
+ MozJSEntry entry(this);
+
+ if (_connectState == ConnectState::External)
+ return;
+ if (_connectState == ConnectState::Local)
+ uasserted(12512, "localConnect already called, can't call externalSetup");
+
+ mongo::sm::reset(0);
+
+ // install db access functions in the global object
+ installDBAccess();
+
+ // install thread-related functions (e.g. _threadInject)
+ installFork();
+
+ // install the Mongo function object
+ _mongoExternalProto.install(_global);
+ execCoreFiles();
+ _connectState = ConnectState::External;
+}
+
+void MozJSImplScope::reset() {
+ unregisterOperation();
+ _pendingKill.store(false);
+ _pendingGC.store(false);
+}
+
+void MozJSImplScope::installBSONTypes() {
+ _binDataProto.install(_global);
+ _bsonProto.install(_global);
+ _dbPointerProto.install(_global);
+ _dbRefProto.install(_global);
+ _maxKeyProto.install(_global);
+ _minKeyProto.install(_global);
+ _nativeFunctionProto.install(_global);
+ _numberIntProto.install(_global);
+ _numberLongProto.install(_global);
+ _objectProto.install(_global);
+ _oidProto.install(_global);
+ _regExpProto.install(_global);
+ _timestampProto.install(_global);
+
+ // This builtin map is a javascript 6 thing. We want our version. so
+ // take theirs out
+ ObjectWrapper(_context, _global).deleteProperty("Map");
+}
+
+void MozJSImplScope::installDBAccess() {
+ _cursorProto.install(_global);
+ _dbProto.install(_global);
+ _dbQueryProto.install(_global);
+ _dbCollectionProto.install(_global);
+}
+
+void MozJSImplScope::installFork() {
+ _countDownLatchProto.install(_global);
+ _jsThreadProto.install(_global);
+}
+
+bool MozJSImplScope::_checkErrorState(bool success, bool reportError, bool assertOnError) {
+ if (success)
+ return false;
+
+ if (_status.isOK()) {
+ _status = Status(ErrorCodes::UnknownError, "Unknown Failure from JSInterpreter");
+ }
+
+ _error = _status.reason();
+
+ if (reportError)
+ error() << _error << std::endl;
+
+ // Clear the status state
+ auto status = std::move(_status);
+
+ if (assertOnError) {
+ // Throw if necessary
+ uassertStatusOK(status);
+ }
+
+ return true;
+}
+
+
+void MozJSImplScope::setCompileOptions(JS::CompileOptions* co) {
+ co->setUTF8(true);
+}
+
+MozJSImplScope* MozJSImplScope::getThreadScope() {
+ return kCurrentScope;
+}
+
+void MozJSImplScope::setOOM() {
+ _status = Status(ErrorCodes::JSInterpreterFailure, "Out of memory");
+}
+
+} // namespace mozjs
+} // namespace mongo