/**
* 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 .
*
* 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/jsthread.h"
#include
#include "vm/PosixNSPR.h"
#include "mongo/db/jsobj.h"
#include "mongo/scripting/mozjs/implscope.h"
#include "mongo/scripting/mozjs/valuereader.h"
#include "mongo/scripting/mozjs/valuewriter.h"
#include "mongo/stdx/condition_variable.h"
#include "mongo/stdx/memory.h"
#include "mongo/stdx/mutex.h"
#include "mongo/stdx/thread.h"
#include "mongo/util/log.h"
#include "mongo/util/stacktrace.h"
namespace mongo {
namespace mozjs {
// These are all executed on some object that owns a js thread, rather than a
// jsthread itself, so CONSTRAINED_METHOD doesn't do the job here.
const JSFunctionSpec JSThreadInfo::threadMethods[6] = {
MONGO_ATTACH_JS_FUNCTION(init),
MONGO_ATTACH_JS_FUNCTION(start),
MONGO_ATTACH_JS_FUNCTION(join),
MONGO_ATTACH_JS_FUNCTION(hasFailed),
MONGO_ATTACH_JS_FUNCTION(returnData),
JS_FS_END,
};
const JSFunctionSpec JSThreadInfo::freeFunctions[3] = {
MONGO_ATTACH_JS_FUNCTION(_threadInject),
MONGO_ATTACH_JS_FUNCTION(_scopedThreadInject),
JS_FS_END,
};
const char* const JSThreadInfo::className = "JSThread";
/**
* Holder for JSThreads as exposed by fork() in the shell.
*
* The idea here is that we create a jsthread by taking a js function and its
* parameters and encoding them into a single bson object. Then we spawn a
* thread, have that thread do the work and join() it before checking it's
* result (serialized through bson). We can check errors at any time by
* checking a mutex guarded hasError().
*/
class JSThreadConfig {
public:
JSThreadConfig(JSContext* cx, JS::CallArgs args)
: _started(false), _done(false), _sharedData(new SharedData()), _jsthread(*this) {
auto scope = getScope(cx);
uassert(ErrorCodes::JSInterpreterFailure, "need at least one argument", args.length() > 0);
uassert(ErrorCodes::JSInterpreterFailure,
"first argument must be a function",
args.get(0).isObject() && JS_ObjectIsFunction(cx, args.get(0).toObjectOrNull()));
JS::RootedObject robj(cx, JS_NewArrayObject(cx, args));
if (!robj) {
uasserted(ErrorCodes::JSInterpreterFailure, "Failed to JS_NewArrayObject");
}
_sharedData->_args = ObjectWrapper(cx, robj).toBSON();
_sharedData->_stack = currentJSStackToString(cx);
if (!scope->getParentStack().empty()) {
_sharedData->_stack = _sharedData->_stack + scope->getParentStack();
}
}
void start() {
uassert(ErrorCodes::JSInterpreterFailure, "Thread already started", !_started);
// Despite calling PR_CreateThread, we're actually using our own
// implementation of PosixNSPR.cpp in this directory. So these threads
// are actually hosted on top of stdx::threads and most of the flags
// don't matter.
_thread = PR_CreateThread(PR_USER_THREAD,
JSThread::run,
&_jsthread,
PR_PRIORITY_NORMAL,
PR_LOCAL_THREAD,
PR_JOINABLE_THREAD,
0);
_started = true;
}
void join() {
uassert(ErrorCodes::JSInterpreterFailure, "Thread not running", _started && !_done);
PR_JoinThread(_thread);
_done = true;
}
/**
* Returns true if the JSThread terminated as a result of an error
* during its execution, and false otherwise. This operation does
* not block, nor does it require join() to have been called.
*/
bool hasFailed() const {
uassert(ErrorCodes::JSInterpreterFailure, "Thread not started", _started);
return _sharedData->getErrored();
}
BSONObj returnData() {
if (!_done)
join();
return _sharedData->_returnData;
}
private:
/**
* SharedData between the calling thread and the callee
*
* JSThreadConfig doesn't always outlive its JSThread (for example, if the parent thread
* garbage collects the JSThreadConfig before the JSThread has finished running), so any
* data shared between them has to go in a shared_ptr.
*/
class SharedData {
public:
SharedData() : _errored(false) {}
void setErrored(bool value) {
stdx::lock_guard lck(_erroredMutex);
_errored = value;
}
bool getErrored() {
stdx::lock_guard lck(_erroredMutex);
return _errored;
}
/**
* These three members aren't protected in any way, so you have to be
* mindful about how they're used. I.e. _args/_stack need to be set
* before start() and _returnData can't be touched until after join().
*/
BSONObj _args;
BSONObj _returnData;
std::string _stack;
private:
stdx::mutex _erroredMutex;
bool _errored;
};
/**
* The callable object used by stdx::thread
*/
class JSThread {
public:
JSThread(JSThreadConfig& config) : _sharedData(config._sharedData) {}
static void run(void* priv) {
auto thisv = static_cast(priv);
try {
MozJSImplScope scope(static_cast(globalScriptEngine));
scope.setParentStack(thisv->_sharedData->_stack);
thisv->_sharedData->_returnData = scope.callThreadArgs(thisv->_sharedData->_args);
} catch (...) {
auto status = exceptionToStatus();
log() << "js thread raised js exception: " << status.reason()
<< thisv->_sharedData->_stack;
thisv->_sharedData->setErrored(true);
thisv->_sharedData->_returnData = BSON("ret" << BSONUndefined);
}
}
private:
std::shared_ptr _sharedData;
};
bool _started;
bool _done;
PRThread* _thread = nullptr;
std::shared_ptr _sharedData;
JSThread _jsthread;
};
namespace {
JSThreadConfig* getConfig(JSContext* cx, JS::CallArgs args) {
JS::RootedValue value(cx);
ObjectWrapper(cx, args.thisv()).getValue(InternedString::_JSThreadConfig, &value);
if (!value.isObject())
uasserted(ErrorCodes::BadValue, "_JSThreadConfig not an object");
if (!getScope(cx)->getProto().instanceOf(value))
uasserted(ErrorCodes::BadValue, "_JSThreadConfig is not a JSThread");
return static_cast(JS_GetPrivate(value.toObjectOrNull()));
}
} // namespace
void JSThreadInfo::finalize(JSFreeOp* fop, JSObject* obj) {
auto config = static_cast(JS_GetPrivate(obj));
if (!config)
return;
delete config;
}
void JSThreadInfo::Functions::init::call(JSContext* cx, JS::CallArgs args) {
auto scope = getScope(cx);
JS::RootedObject obj(cx);
scope->getProto().newObject(&obj);
JSThreadConfig* config = new JSThreadConfig(cx, args);
JS_SetPrivate(obj, config);
ObjectWrapper(cx, args.thisv()).setObject(InternedString::_JSThreadConfig, obj);
args.rval().setUndefined();
}
void JSThreadInfo::Functions::start::call(JSContext* cx, JS::CallArgs args) {
getConfig(cx, args)->start();
args.rval().setUndefined();
}
void JSThreadInfo::Functions::join::call(JSContext* cx, JS::CallArgs args) {
getConfig(cx, args)->join();
args.rval().setUndefined();
}
void JSThreadInfo::Functions::hasFailed::call(JSContext* cx, JS::CallArgs args) {
args.rval().setBoolean(getConfig(cx, args)->hasFailed());
}
void JSThreadInfo::Functions::returnData::call(JSContext* cx, JS::CallArgs args) {
auto obj = getConfig(cx, args)->returnData();
ValueReader(cx, args.rval()).fromBSONElement(obj.firstElement(), obj, true);
}
void JSThreadInfo::Functions::_threadInject::call(JSContext* cx, JS::CallArgs args) {
uassert(ErrorCodes::JSInterpreterFailure,
"threadInject takes exactly 1 argument",
args.length() == 1);
uassert(ErrorCodes::JSInterpreterFailure,
"threadInject needs to be passed a prototype",
args.get(0).isObject());
JS::RootedObject o(cx, args.get(0).toObjectOrNull());
if (!JS_DefineFunctions(cx, o, JSThreadInfo::threadMethods))
throwCurrentJSException(cx, ErrorCodes::JSInterpreterFailure, "Failed to define functions");
args.rval().setUndefined();
}
void JSThreadInfo::Functions::_scopedThreadInject::call(JSContext* cx, JS::CallArgs args) {
_threadInject::call(cx, args);
}
} // namespace mozjs
} // namespace mongo