* Update per 21114-part2 changes.

* Add explicit thread safety guards.
* Add Entities.queryPropertyMetdata for unit testing Entity script module support.
* Cleanup / commenting pass.
This commit is contained in:
humbletim 2017-03-01 09:14:19 -05:00
parent 9b09651337
commit 40ba8185a0
8 changed files with 440 additions and 159 deletions

View file

@ -146,6 +146,7 @@ void EntityTreeRenderer::clear() {
void EntityTreeRenderer::reloadEntityScripts() {
_entitiesScriptEngine->unloadAllEntityScripts();
_entitiesScriptEngine->resetModuleCache();
foreach(auto entity, _entitiesInScene) {
if (!entity->getScript().isEmpty()) {
_entitiesScriptEngine->loadEntityScript(entity->getEntityItemID(), entity->getScript(), true);

View file

@ -15,11 +15,13 @@
#define hifi_EntitiesScriptEngineProvider_h
#include <QtCore/QString>
#include <QFuture>
#include "EntityItemID.h"
class EntitiesScriptEngineProvider {
public:
virtual void callEntityScriptMethod(const EntityItemID& entityID, const QString& methodName, const QStringList& params = QStringList()) = 0;
virtual QFuture<QVariant> getLocalEntityScriptDetails(const EntityItemID& entityID) = 0;
};
#endif // hifi_EntitiesScriptEngineProvider_h
#endif // hifi_EntitiesScriptEngineProvider_h

View file

@ -10,6 +10,9 @@
//
#include "EntityScriptingInterface.h"
#include <QFutureWatcher>
#include <QtConcurrent/QtConcurrentRun>
#include "EntityItemID.h"
#include <VariantMapToScriptValue.h>
#include <SharedUtil.h>
@ -24,6 +27,7 @@
#include "ModelEntityItem.h"
#include "QVariantGLM.h"
#include "SimulationOwner.h"
#include "BaseScriptEngine.h"
#include "ZoneEntityItem.h"
#include "WebEntityItem.h"
#include <EntityScriptClient.h>
@ -680,6 +684,111 @@ bool EntityScriptingInterface::reloadServerScripts(QUuid entityID) {
return client->reloadServerScript(entityID);
}
bool EntityScriptingInterface::queryPropertyMetadata(QUuid entityID, QScriptValue property, QScriptValue scopeOrCallback, QScriptValue methodOrName) {
auto name = property.toString();
auto handler = makeScopedHandlerObject(scopeOrCallback, methodOrName);
QPointer<BaseScriptEngine> engine = dynamic_cast<BaseScriptEngine*>(handler.engine());
if (!engine) {
qCDebug(entities) << "queryPropertyMetadata without detectable engine" << entityID << name;
return false;
}
connect(engine, &QObject::destroyed, this, [=]() {
qDebug() << "queryPropertyMetadata -- engine destroyed!" << (!engine ? "nullptr" : "engine");
});
if (!handler.property("callback").isFunction()) {
qDebug() << "!handler.callback.isFunction" << engine;
engine->raiseException(engine->makeError("callback is not a function", "TypeError"));
return false;
}
if (name == "userData") {
EntityItemPointer entity = _entityTree->findEntityByEntityItemID(entityID);
QScriptValue err, result;
if (entity) {
auto JSON = engine->globalObject().property("JSON");
auto parsed = JSON.property("parse").call(JSON, QScriptValueList({ entity->getUserData() }));
if (engine->hasUncaughtException()) {
err = engine->cloneUncaughtException(__FUNCTION__);
engine->clearExceptions();
} else {
result = parsed;
}
} else {
err = engine->makeError("entity not found");
}
QFutureWatcher<QVariant> *request = new QFutureWatcher<QVariant>;
connect(request, &QFutureWatcher<QVariant>::finished, engine, [=]() mutable {
if (!engine) {
qCDebug(entities) << "queryPropertyMetadata -- engine destroyed while inflight" << entityID << name;
return;
}
callScopedHandlerObject(handler, err, result);
request->deleteLater();
});
request->setFuture(QtConcurrent::run([]() -> QVariant {
QThread::sleep(1);
return 1;
}));
return true;
} else if (name == "script") {
using LocalScriptStatusRequest = QFutureWatcher<QVariant>;
LocalScriptStatusRequest *request = new LocalScriptStatusRequest;
connect(request, &LocalScriptStatusRequest::finished, engine, [=]() mutable {
if (!engine) {
qCDebug(entities) << "queryPropertyMetadata -- engine destroyed while inflight" << entityID << name;
return;
}
auto details = request->result().toMap();
QScriptValue err, result;
if (details.contains("isError")) {
if (!details.contains("message")) {
details["message"] = details["errorInfo"];
}
err = engine->makeError(engine->toScriptValue(details));
} else {
details["success"] = true;
result = engine->toScriptValue(details);
}
callScopedHandlerObject(handler, err, result);
request->deleteLater();
});
request->setFuture(_entitiesScriptEngine->getLocalEntityScriptDetails(entityID));
return true;
} else if (name == "serverScripts") {
auto client = DependencyManager::get<EntityScriptClient>();
auto request = client->createScriptStatusRequest(entityID);
connect(request, &GetScriptStatusRequest::finished, engine, [=](GetScriptStatusRequest* request) mutable {
if (!engine) {
qCDebug(entities) << "queryPropertyMetadata -- engine destroyed while inflight" << entityID << name;
return;
}
QVariantMap details;
details["success"] = request->getResponseReceived();
details["isRunning"] = request->getIsRunning();
details["status"] = EntityScriptStatus_::valueToKey(request->getStatus()).toLower();
details["errorInfo"] = request->getErrorInfo();
QScriptValue err, result;
if (!details["success"].toBool()) {
if (!details.contains("message") && details.contains("errorInfo")) {
details["message"] = details["errorInfo"];
}
if (details["message"].toString().isEmpty()) {
details["message"] = "entity server script details not found";
}
err = engine->makeError(engine->toScriptValue(details));
} else {
result = engine->toScriptValue(details);
}
callScopedHandlerObject(handler, err, result);
request->deleteLater();
});
request->start();
return true;
}
engine->raiseException(engine->makeError("property has no mapped metadata: " + name));
return false;
}
bool EntityScriptingInterface::getServerScriptStatus(QUuid entityID, QScriptValue callback) {
auto client = DependencyManager::get<EntityScriptClient>();
auto request = client->createScriptStatusRequest(entityID);

View file

@ -211,6 +211,26 @@ public slots:
Q_INVOKABLE RayToEntityIntersectionResult findRayIntersectionBlocking(const PickRay& ray, bool precisionPicking = false, const QScriptValue& entityIdsToInclude = QScriptValue(), const QScriptValue& entityIdsToDiscard = QScriptValue());
Q_INVOKABLE bool reloadServerScripts(QUuid entityID);
/**jsdoc
* Query for the available metadata behind one of an Entity's "magic" properties (eg: `script` and `serverScripts`).
*
* @function Entities.queryPropertyMetadata
* @param {EntityID} entityID The ID of the entity.
* @param {string} property The name of the property extended metadata is wanted for.
* @param {ResultCallback} callback Executes callback(err, result) with the query results.
*/
/**jsdoc
* Query for the available metadata behind one of an Entity's "magic" properties (eg: `script` and `serverScripts`).
*
* @function Entities.queryPropertyMetadata
* @param {EntityID} entityID The ID of the entity.
* @param {string} property The name of the property extended metadata is wanted for.
* @param {Object} thisObject The scoping "this" context that callback will be executed within.
* @param {ResultCallback} callbackOrMethodName Executes thisObject[callbackOrMethodName](err, result) with the query results.
*/
Q_INVOKABLE bool queryPropertyMetadata(QUuid entityID, QScriptValue property, QScriptValue scopeOrCallback, QScriptValue methodOrName = QScriptValue());
Q_INVOKABLE bool getServerScriptStatus(QUuid entityID, QScriptValue callback);
Q_INVOKABLE void setLightsArePickable(bool value);

View file

@ -19,6 +19,9 @@
#include <QtCore/QThread>
#include <QtCore/QRegularExpression>
#include <QtCore/QFuture>
#include <QtConcurrent/QtConcurrentRun>
#include <QtWidgets/QMainWindow>
#include <QtWidgets/QApplication>
@ -74,6 +77,10 @@ const QString ScriptEngine::_SETTINGS_ENABLE_EXTENDED_MODULE_COMPAT {
"com.highfidelity.experimental.enableExtendedModuleCompatbility"
};
const QString ScriptEngine::_SETTINGS_ENABLE_EXTENDED_EXCEPTIONS {
"com.highfidelity.experimental.enableExtendedJSExceptions"
};
static const int MAX_MODULE_ID_LENTGH { 4096 };
static const int MAX_DEBUG_VALUE_LENGTH { 80 };
@ -146,7 +153,7 @@ QString encodeEntityIdIntoEntityUrl(const QString& url, const QString& entityID)
}
QString ScriptEngine::logException(const QScriptValue& exception) {
auto message = formatException(exception);
auto message = formatException(exception, _enableExtendedJSExceptions.get());
scriptErrorMessage(qPrintable(message));
return message;
}
@ -338,7 +345,11 @@ void ScriptEngine::runInThread() {
// The thread interface cannot live on itself, and we want to move this into the thread, so
// the thread cannot have this as a parent.
QThread* workerThread = new QThread();
#ifdef Q_OS_LINUX
workerThread->setObjectName(QString("js:") + getFilename());
#else
workerThread->setObjectName(QString("Script Thread:") + getFilename());
#endif
moveToThread(workerThread);
// NOTE: If you connect any essential signals for proper shutdown or cleanup of
@ -539,41 +550,37 @@ static QScriptValue createScriptableResourcePrototype(QScriptEngine* engine) {
void ScriptEngine::resetModuleCache(bool deleteScriptCache) {
if (QThread::currentThread() != thread()) {
QMetaObject::invokeMethod(this, "resetModuleCache");
executeOnScriptThread([=](){ resetModuleCache(deleteScriptCache); });
return;
}
{
QMutexLocker locker(&_requireLock);
auto jsRequire = globalObject().property("Script").property("require");
auto cache = jsRequire.property("cache");
auto cacheMeta = jsRequire.data();
auto jsRequire = globalObject().property("Script").property("require");
auto cache = jsRequire.property("cache");
auto cacheMeta = jsRequire.data();
if (deleteScriptCache) {
QScriptValueIterator it(cache);
while (it.hasNext()) {
it.next();
if (it.flags() & QScriptValue::SkipInEnumeration) {
continue;
}
//scriptCache->deleteScript(it.name());
qCDebug(scriptengine) << "resetModuleCache(true) -- staging " << it.name() << " for cache reset at next require";
cacheMeta.setProperty(it.name(), true);
if (deleteScriptCache) {
QScriptValueIterator it(cache);
while (it.hasNext()) {
it.next();
if (it.flags() & QScriptValue::SkipInEnumeration) {
continue;
}
//scriptCache->deleteScript(it.name());
qCDebug(scriptengine) << "resetModuleCache(true) -- staging " << it.name() << " for cache reset at next require";
cacheMeta.setProperty(it.name(), true);
}
//_debugDump("cacheMeta", cacheMeta);
cache = newObject();
if (!cacheMeta.isObject()) {
cacheMeta = newObject();
cacheMeta.setProperty("id", "Script.require.cacheMeta");
cacheMeta.setProperty("type", "cacheMeta");
jsRequire.setData(cacheMeta);
}
cache.setProperty("__created__", (double)QDateTime::currentMSecsSinceEpoch(), QScriptValue::SkipInEnumeration);
#if DEBUG_JS_MODULES
cache.setProperty("__meta__", cacheMeta, HIDDEN_PROP_FLAGS);
#endif
jsRequire.setProperty("cache", cache, QScriptValue::ReadOnly | QScriptValue::Undeletable);
}
cache = newObject();
if (!cacheMeta.isObject()) {
cacheMeta = newObject();
cacheMeta.setProperty("id", "Script.require.cacheMeta");
cacheMeta.setProperty("type", "cacheMeta");
jsRequire.setData(cacheMeta);
}
cache.setProperty("__created__", (double)QDateTime::currentMSecsSinceEpoch(), QScriptValue::SkipInEnumeration);
#if DEBUG_JS_MODULES
cache.setProperty("__meta__", cacheMeta, READONLY_HIDDEN_PROP_FLAGS);
#endif
jsRequire.setProperty("cache", cache, QScriptValue::ReadOnly | QScriptValue::Undeletable);
}
void ScriptEngine::init() {
@ -916,6 +923,11 @@ void ScriptEngine::addEventHandler(const EntityItemID& entityID, const QString&
handlersForEvent << handlerData; // Note that the same handler can be added many times. See removeEntityEventHandler().
}
// this is not redundant -- the version in BaseScriptEngine is specifically not Q_INVOKABLE
QScriptValue ScriptEngine::evaluateInClosure(const QScriptValue& closure, const QScriptProgram& program) {
return BaseScriptEngine::evaluateInClosure(closure, program);
}
QScriptValue ScriptEngine::evaluate(const QString& sourceCode, const QString& fileName, int lineNumber) {
if (DependencyManager::get<ScriptEngines>()->isStopped()) {
return QScriptValue(); // bail early
@ -938,29 +950,26 @@ QScriptValue ScriptEngine::evaluate(const QString& sourceCode, const QString& fi
// Check syntax
auto syntaxError = lintScript(sourceCode, fileName);
if (syntaxError.isError()) {
if (isEvaluating()) {
currentContext()->throwValue(syntaxError);
} else {
if (!isEvaluating()) {
syntaxError.setProperty("detail", "evaluate");
emit unhandledException(syntaxError);
}
raiseException(syntaxError);
maybeEmitUncaughtException(__FUNCTION__);
return syntaxError;
}
QScriptProgram program { sourceCode, fileName, lineNumber };
if (program.isNull()) {
// can this happen?
auto err = makeError("could not create QScriptProgram for " + fileName);
emit unhandledException(err);
raiseException(err);
maybeEmitUncaughtException(__FUNCTION__);
return err;
}
QScriptValue result;
{
result = BaseScriptEngine::evaluate(program);
if (!isEvaluating() && hasUncaughtException()) {
emit unhandledException(cloneUncaughtException(__FUNCTION__));
clearExceptions();
}
maybeEmitUncaughtException(__FUNCTION__);
}
return result;
}
@ -983,10 +992,7 @@ void ScriptEngine::run() {
{
evaluate(_scriptContents, _fileNameString);
if (!isEvaluating() && hasUncaughtException()) {
emit unhandledException(cloneUncaughtException(__FUNCTION__));
clearExceptions();
}
maybeEmitUncaughtException(__FUNCTION__);
}
#ifdef _WIN32
// VS13 does not sleep_until unless it uses the system_clock, see:
@ -1359,24 +1365,30 @@ void ScriptEngine::print(const QString& message) {
// Script.require.resolve -- like resolvePath, but performs more validation and throws exceptions on invalid module identifiers (for consistency with Node.js)
QString ScriptEngine::_requireResolve(const QString& moduleId, const QString& relativeTo) {
if (!IS_THREADSAFE_INVOCATION(thread(), __FUNCTION__)) {
return QString();
}
QUrl defaultScriptsLoc = defaultScriptsLocation();
QUrl url(moduleId);
ExceptionEmitter tryCatcher(this, __FUNCTION__);
auto displayId = moduleId;
if (displayId.length() > MAX_DEBUG_VALUE_LENGTH) {
displayId = displayId.mid(0, MAX_DEBUG_VALUE_LENGTH) + "...";
}
auto message = QString("Cannot find module '%1' (%2)").arg(displayId);
auto ctx = currentContext();
auto throwResolveError = [&](const QScriptValue& error) -> QString {
raiseException(error);
maybeEmitUncaughtException("require.resolve");
return nullptr;
};
// de-fuzz the input a little by restricting to rational sizes
auto idLength = url.toString().length();
if (idLength < 1 || idLength > MAX_MODULE_ID_LENTGH) {
auto details = QString("rejecting invalid module id size (%1 chars [1,%2])")
.arg(idLength).arg(MAX_MODULE_ID_LENTGH);
ctx->throwValue(makeError(message.arg(details), "RangeError"));
return nullptr;
return throwResolveError(makeError(message.arg(details), "RangeError"));
}
// this regex matches: absolute, dotted or path-like URLs
@ -1396,14 +1408,12 @@ QString ScriptEngine::_requireResolve(const QString& moduleId, const QString& re
url = defaultScriptsLoc;
url.setPath(systemModulePath);
if (!QFileInfo(url.toLocalFile()).isFile()) {
ctx->throwValue(makeError(message.arg("system module not found")));
return nullptr;
return throwResolveError(makeError(message.arg("system module not found")));
}
}
if (url.isRelative()) {
ctx->throwValue(makeError(message.arg("could not resolve module id")));
return nullptr;
return throwResolveError(makeError(message.arg("could not resolve module id")));
}
// if it looks like a local file, verify that it's an allowed path and really a file
@ -1416,34 +1426,35 @@ QString ScriptEngine::_requireResolve(const QString& moduleId, const QString& re
bool disallowOutsideFiles = !defaultScriptsLocation().isParentOf(canonical) && !currentSandboxURL.isLocalFile();
if (disallowOutsideFiles && !PathUtils::isDescendantOf(canonical, currentSandboxURL)) {
ctx->throwValue(makeError(message.arg(
return throwResolveError(makeError(message.arg(
QString("path '%1' outside of origin script '%2' '%3'")
.arg(PathUtils::stripFilename(url))
.arg(PathUtils::stripFilename(currentSandboxURL))
.arg(canonical.toString())
)));
return nullptr;
}
if (!file.exists()) {
ctx->throwValue(makeError(message.arg("path does not exist: " + url.toLocalFile())));
return nullptr;
return throwResolveError(makeError(message.arg("path does not exist: " + url.toLocalFile())));
}
if (!file.isFile()) {
ctx->throwValue(makeError(message.arg("path is not a file: " + url.toLocalFile())));
return nullptr;
return throwResolveError(makeError(message.arg("path is not a file: " + url.toLocalFile())));
}
}
maybeEmitUncaughtException(__FUNCTION__);
return url.toString();
}
// retrieves the current parent module from the JS scope chain
QScriptValue ScriptEngine::currentModule() {
if (!IS_THREADSAFE_INVOCATION(thread(), __FUNCTION__)) {
return unboundNullValue();
}
auto jsRequire = globalObject().property("Script").property("require");
auto cache = jsRequire.property("cache");
auto candidate = QScriptValue();
for(auto ctx = currentContext(); ctx && !candidate.isObject(); ctx = ctx->parentContext()) {
QScriptContextInfo contextInfo { ctx };
for (auto c = currentContext(); c && !candidate.isObject(); c = c->parentContext()) {
QScriptContextInfo contextInfo { c };
candidate = cache.property(contextInfo.fileName());
}
if (!candidate.isObject()) {
@ -1459,7 +1470,7 @@ bool ScriptEngine::registerModuleWithParent(const QScriptValue& module, const QS
if (children.isArray()) {
auto key = module.property("id");
auto length = children.property("length").toInt32();
for(int i=0; i < length; i++) {
for (int i=0; i < length; i++) {
if (children.property(i).property("id").strictlyEquals(key)) {
qCDebug(scriptengine_module) << key.toString() << " updating parent.children[" << i << "] = module";
children.setProperty(i, module);
@ -1488,7 +1499,7 @@ QScriptValue ScriptEngine::newModule(const QString& modulePath, const QScriptVal
closure.setProperty("exports", exports);
// make the closure available to module instantiation
module.setProperty("__closure__", closure, HIDDEN_PROP_FLAGS);
module.setProperty("__closure__", closure, READONLY_HIDDEN_PROP_FLAGS);
// for consistency with Node.js Module
module.setProperty("id", modulePath, READONLY_PROP_FLAGS);
@ -1524,7 +1535,7 @@ QScriptValue ScriptEngine::fetchModuleSource(const QString& modulePath, const bo
req.setProperty("url", url);
req.setProperty("status", status);
req.setProperty("success", ScriptCache::isSuccessStatus(status));
req.setProperty("contents", contents, HIDDEN_PROP_FLAGS);
req.setProperty("contents", contents, READONLY_HIDDEN_PROP_FLAGS);
}
};
@ -1568,73 +1579,54 @@ QScriptValue ScriptEngine::instantiateModule(const QScriptValue& module, const Q
auto modulePath = module.property("filename").toString();
auto closure = module.property("__closure__");
qCDebug(scriptengine_module) << QString("require.instantiateModule: %1 / %2 bytes").arg(QUrl(modulePath).fileName()).arg(sourceCode.length());
qCDebug(scriptengine_module) << QString("require.instantiateModule: %1 / %2 bytes")
.arg(QUrl(modulePath).fileName()).arg(sourceCode.length());
{
ExceptionEmitter tryCatch(this, __FUNCTION__);
if (module.property("content-type").toString() == "application/json") {
qCDebug(scriptengine_module) << "... parsing as JSON";
#ifdef DEV_OTHER_MODULE_JSON_PARSE
auto JSON = globalObject().property("JSON");
auto parse = JSON.property("parse");
if (!parse.isFunction()) {
currentContext()->throwValue(makeError("global JSON.parse is not a function", "EvalError"));
return nullValue();
}
result = parse.call(JSON, QScriptValueList({ sourceCode }));
closure.property("module").setProperty("exports", result);
return result;
#endif
closure.setProperty("__json", sourceCode);
result = evaluateInClosure(closure, { "module.exports = JSON.parse(__json)", modulePath });
} else {
// scoped vars for consistency with Node.js
closure.setProperty("require", module.property("require"));
closure.setProperty("__filename", modulePath, HIDDEN_PROP_FLAGS);
closure.setProperty("__dirname", QString(modulePath).replace(QRegExp("/[^/]*$"), ""), HIDDEN_PROP_FLAGS);
result = evaluateInClosure(closure, { sourceCode, modulePath });
}
if (module.property("content-type").toString() == "application/json") {
qCDebug(scriptengine_module) << "... parsing as JSON";
closure.setProperty("__json", sourceCode);
result = evaluateInClosure(closure, { "module.exports = JSON.parse(__json)", modulePath });
} else {
// scoped vars for consistency with Node.js
closure.setProperty("require", module.property("require"));
closure.setProperty("__filename", modulePath, READONLY_HIDDEN_PROP_FLAGS);
closure.setProperty("__dirname", QString(modulePath).replace(QRegExp("/[^/]*$"), ""), READONLY_HIDDEN_PROP_FLAGS);
result = evaluateInClosure(closure, { sourceCode, modulePath });
}
maybeEmitUncaughtException(__FUNCTION__);
return result;
}
// CommonJS/Node.js like require/module support
QScriptValue ScriptEngine::require(const QString& moduleId) {
qCDebug(scriptengine_module) << "ScriptEngine::require(" << moduleId.left(MAX_DEBUG_VALUE_LENGTH) << ")";
if (QThread::currentThread() != thread()) {
qCDebug(scriptengine_module) << moduleId << " threads mismatch";
return nullValue();
if (!IS_THREADSAFE_INVOCATION(thread(), __FUNCTION__)) {
return unboundNullValue();
}
// serialize require calls so the ordering/caching works correctly across multiple Entities
// note: this is a Recursive mutex so nested require's should not affected
QMutexLocker locker(&_requireLock);
ExceptionEmitter tryCatcher(this, __FUNCTION__);
// _requireDepth++;
// QObject autoDecrement;connect(&autoDecrement, &QObject::destroyed, this, [this](){ _requireDepth--; });
auto jsRequire = globalObject().property("Script").property("require");
auto cacheMeta = jsRequire.data();
auto cache = jsRequire.property("cache");
auto parent = currentModule();
auto throwModuleError = [this, &cache](const QString& modulePath, const QScriptValue& error) {
auto throwModuleError = [&](const QString& modulePath, const QScriptValue& error) {
cache.setProperty(modulePath, nullValue());
if (!error.isNull()) {
#ifdef DEBUG_JS_MODULES
qCWarning(scriptengine_module) << "throwing module error:" << error.toString() << modulePath << error.property("stack").toString();
#endif
currentContext()->throwValue(makeError(error));
raiseException(error);
}
return nullValue();
maybeEmitUncaughtException("module");
return unboundNullValue();
};
// start by resolving the moduleId into a fully-qualified path/URL
QString modulePath = _requireResolve(moduleId);
if (modulePath.isNull() || tryCatcher.hasPending()) {
if (modulePath.isNull() || hasUncaughtException()) {
// the resolver already threw an exception -- bail early
return nullValue();
maybeEmitUncaughtException(__FUNCTION__);
return unboundNullValue();
}
// check the resolved path against the cache
@ -1656,6 +1648,7 @@ QScriptValue ScriptEngine::require(const QString& moduleId) {
qCDebug(scriptengine_module) << QString("require - using cached module '%1' for '%2' (loaded: %3)")
.arg(modulePath).arg(moduleId).arg(module.property("loaded").toString());
registerModuleWithParent(module, parent);
maybeEmitUncaughtException(__FUNCTION__);
return exports;
}
@ -1705,7 +1698,7 @@ QScriptValue ScriptEngine::require(const QString& moduleId) {
#ifdef DEBUG_JS
_debugDump("this", thisObject);
#endif
_applyUserOptions(module, thisObject);
applyUserOptions(module, thisObject);
}
}
@ -1725,11 +1718,15 @@ QScriptValue ScriptEngine::require(const QString& moduleId) {
qCDebug(scriptengine_module) << "//ScriptEngine::require(" << moduleId << ")";
maybeEmitUncaughtException(__FUNCTION__);
return module.property("exports");
}
// User-configurable override options
void ScriptEngine::_applyUserOptions(QScriptValue& module, QScriptValue& options) {
void ScriptEngine::applyUserOptions(QScriptValue& module, QScriptValue& options) {
if (!IS_THREADSAFE_INVOCATION(thread(), __FUNCTION__)) {
return;
}
if (!options.isValid()) {
return;
}
@ -1770,6 +1767,7 @@ void ScriptEngine::_applyUserOptions(QScriptValue& module, QScriptValue& options
qCDebug(scriptengine_module) << "module.closure['global'] = options.global";
}
}
maybeEmitUncaughtException(__FUNCTION__);
}
// If a callback is specified, the included files will be loaded asynchronously and the callback will be called
@ -1777,6 +1775,9 @@ void ScriptEngine::_applyUserOptions(QScriptValue& module, QScriptValue& options
// If no callback is specified, the included files will be loaded synchronously and will block execution until
// all of the files have finished loading.
void ScriptEngine::include(const QStringList& includeFiles, QScriptValue callback) {
if (!IS_THREADSAFE_INVOCATION(thread(), __FUNCTION__)) {
return;
}
if (DependencyManager::get<ScriptEngines>()->isStopped()) {
scriptWarningMessage("Script.include() while shutting down is ignored... includeFiles:"
+ includeFiles.join(",") + "parent script:" + getFilename());
@ -1886,6 +1887,9 @@ void ScriptEngine::include(const QString& includeFile, QScriptValue callback) {
// as a stand-alone script. To accomplish this, the ScriptEngine class just emits a signal which
// the Application or other context will connect to in order to know to actually load the script
void ScriptEngine::load(const QString& loadFile) {
if (!IS_THREADSAFE_INVOCATION(thread(), __FUNCTION__)) {
return;
}
if (DependencyManager::get<ScriptEngines>()->isStopped()) {
scriptWarningMessage("Script.load() while shutting down is ignored... loadFile:"
+ loadFile + "parent script:" + getFilename());
@ -1955,6 +1959,52 @@ void ScriptEngine::updateEntityScriptStatus(const EntityItemID& entityID, const
emit entityScriptDetailsUpdated();
}
QVariant ScriptEngine::cloneEntityScriptDetails(const EntityItemID& entityID) {
static const QVariant NULL_VARIANT { qVariantFromValue((QObject*)nullptr) };
QVariantMap map;
if (entityID.isNull()) {
// TODO: find better way to report JS Error across thread/process boundaries
map["isError"] = true;
map["errorInfo"] = "Error: getEntityScriptDetails -- invalid entityID";
} else {
#ifdef DEBUG_ENTITY_STATES
qDebug() << "cloneEntityScriptDetails" << entityID << QThread::currentThread();
#endif
EntityScriptDetails scriptDetails;
if (getEntityScriptDetails(entityID, scriptDetails)) {
#ifdef DEBUG_ENTITY_STATES
qDebug() << "gotEntityScriptDetails" << scriptDetails.status << QThread::currentThread();
#endif
map["isRunning"] = isEntityScriptRunning(entityID);
map["status"] = EntityScriptStatus_::valueToKey(scriptDetails.status).toLower();
map["errorInfo"] = scriptDetails.errorInfo;
map["entityID"] = entityID.toString();
#ifdef DEBUG_ENTITY_STATES
{
auto debug = QVariantMap();
debug["script"] = scriptDetails.scriptText;
debug["scriptObject"] = scriptDetails.scriptObject.toVariant();
debug["lastModified"] = (qlonglong)scriptDetails.lastModified;
debug["sandboxURL"] = scriptDetails.definingSandboxURL;
map["debug"] = debug;
}
#endif
} else {
#ifdef DEBUG_ENTITY_STATES
qDebug() << "!gotEntityScriptDetails" << QThread::currentThread();
#endif
map["isError"] = true;
map["errorInfo"] = "Entity script details unavailable";
map["entityID"] = entityID.toString();
}
}
return map;
}
QFuture<QVariant> ScriptEngine::getLocalEntityScriptDetails(const EntityItemID& entityID) {
return QtConcurrent::run(this, &ScriptEngine::cloneEntityScriptDetails, entityID);
}
bool ScriptEngine::getEntityScriptDetails(const EntityItemID& entityID, EntityScriptDetails &details) const {
auto it = _entityScripts.constFind(entityID);
if (it == _entityScripts.constEnd()) {
@ -2093,10 +2143,10 @@ void ScriptEngine::loadEntityScript(const EntityItemID& entityID, const QString&
auto scriptCache = DependencyManager::get<ScriptCache>();
// note: see EntityTreeRenderer.cpp for shared pointer lifecycle management
QWeakPointer<ScriptEngine> weakRef(sharedFromThis());
QWeakPointer<BaseScriptEngine> weakRef(sharedFromThis());
scriptCache->getScriptContents(entityScript,
[this, weakRef, entityScript, entityID](const QString& url, const QString& contents, bool isURL, bool success, const QString& status) {
QSharedPointer<ScriptEngine> strongRef(weakRef);
QSharedPointer<BaseScriptEngine> strongRef(weakRef);
if (!strongRef) {
qCWarning(scriptengine) << "loadEntityScript.contentAvailable -- ScriptEngine was deleted during getScriptContents!!";
return;
@ -2215,13 +2265,12 @@ void ScriptEngine::entityScriptContentAvailable(const EntityItemID& entityID, co
timeout.setSingleShot(true);
timeout.start(SANDBOX_TIMEOUT);
connect(&timeout, &QTimer::timeout, [&sandbox, SANDBOX_TIMEOUT, scriptOrURL]{
auto context = sandbox.currentContext();
if (context) {
qCDebug(scriptengine) << "ScriptEngine::entityScriptContentAvailable timeout(" << scriptOrURL << ")";
// Guard against infinite loops and non-performant code
context->throwError(QString("Timed out (entity constructors are limited to %1ms)").arg(SANDBOX_TIMEOUT));
}
sandbox.raiseException(
sandbox.makeError(QString("Timed out (entity constructors are limited to %1ms)").arg(SANDBOX_TIMEOUT))
);
});
testConstructor = sandbox.evaluate(program);
@ -2237,7 +2286,7 @@ void ScriptEngine::entityScriptContentAvailable(const EntityItemID& entityID, co
if (exception.isError()) {
// create a local copy using makeError to decouple from the sandbox engine
exception = makeError(exception);
setError(formatException(exception), EntityScriptStatus::ERROR_RUNNING_SCRIPT);
setError(formatException(exception, _enableExtendedJSExceptions.get()), EntityScriptStatus::ERROR_RUNNING_SCRIPT);
emit unhandledException(exception);
return;
}
@ -2288,7 +2337,7 @@ void ScriptEngine::entityScriptContentAvailable(const EntityItemID& entityID, co
if (entityScriptObject.isError()) {
auto exception = entityScriptObject;
setError(formatException(exception), EntityScriptStatus::ERROR_RUNNING_SCRIPT);
setError(formatException(exception, _enableExtendedJSExceptions.get()), EntityScriptStatus::ERROR_RUNNING_SCRIPT);
emit unhandledException(exception);
return;
}
@ -2420,10 +2469,7 @@ void ScriptEngine::doWithEnvironment(const EntityItemID& entityID, const QUrl& s
#else
operation();
#endif
if (!isEvaluating() && hasUncaughtException()) {
emit unhandledException(cloneUncaughtException(!entityID.isNull() ? entityID.toString() : __FUNCTION__));
clearExceptions();
}
maybeEmitUncaughtException(!entityID.isNull() ? entityID.toString() : __FUNCTION__);
currentEntityIdentifier = oldIdentifier;
currentSandboxURL = oldSandboxURL;
}

View file

@ -79,7 +79,7 @@ public:
QUrl definingSandboxURL { QUrl("about:EntityScript") };
};
class ScriptEngine : public BaseScriptEngine, public EntitiesScriptEngineProvider, public QEnableSharedFromThis<ScriptEngine> {
class ScriptEngine : public BaseScriptEngine, public EntitiesScriptEngineProvider {
Q_OBJECT
Q_PROPERTY(QString context READ getContext)
public:
@ -138,6 +138,8 @@ public:
/// evaluate some code in the context of the ScriptEngine and return the result
Q_INVOKABLE QScriptValue evaluate(const QString& program, const QString& fileName, int lineNumber = 1); // this is also used by the script tool widget
Q_INVOKABLE QScriptValue evaluateInClosure(const QScriptValue& locals, const QScriptProgram& program);
/// if the script engine is not already running, this will download the URL and start the process of seting it up
/// to run... NOTE - this is used by Application currently to load the url. We don't really want it to be exposed
/// to scripts. we may not need this to be invokable
@ -181,6 +183,8 @@ public:
Q_INVOKABLE bool isEntityScriptRunning(const EntityItemID& entityID) {
return _entityScripts.contains(entityID) && _entityScripts[entityID].status == EntityScriptStatus::RUNNING;
}
QVariant cloneEntityScriptDetails(const EntityItemID& entityID);
QFuture<QVariant> getLocalEntityScriptDetails(const EntityItemID& entityID) override;
Q_INVOKABLE void loadEntityScript(const EntityItemID& entityID, const QString& entityScript, bool forceRedownload);
Q_INVOKABLE void unloadEntityScript(const EntityItemID& entityID); // will call unload method
Q_INVOKABLE void unloadAllEntityScripts();
@ -286,7 +290,6 @@ protected:
QHash<EntityItemID, EntityScriptDetails> _entityScripts;
QHash<QString, EntityItemID> _occupiedScriptURLs;
QList<DeferredLoadEntity> _deferredEntityLoads;
QMutex _requireLock { QMutex::Recursive };
bool _isThreaded { false };
QScriptEngineDebugger* _debugger { nullptr };
@ -312,8 +315,12 @@ protected:
std::chrono::microseconds _totalTimerExecution { 0 };
static const QString _SETTINGS_ENABLE_EXTENDED_MODULE_COMPAT;
static const QString _SETTINGS_ENABLE_EXTENDED_EXCEPTIONS;
Setting::Handle<bool> _enableExtendedModuleCompatbility { _SETTINGS_ENABLE_EXTENDED_MODULE_COMPAT, false };
void _applyUserOptions(QScriptValue& module, QScriptValue& options);
Setting::Handle<bool> _enableExtendedJSExceptions { _SETTINGS_ENABLE_EXTENDED_EXCEPTIONS, true };
void applyUserOptions(QScriptValue& module, QScriptValue& options);
};
#endif // hifi_ScriptEngine_h

View file

@ -10,6 +10,7 @@
//
#include "BaseScriptEngine.h"
#include "SharedLogging.h"
#include <QtCore/QString>
#include <QtCore/QThread>
@ -18,18 +19,26 @@
#include <QtScript/QScriptValueIterator>
#include <QtScript/QScriptContextInfo>
#include "ScriptEngineLogging.h"
#include "Profile.h"
const QString BaseScriptEngine::_SETTINGS_ENABLE_EXTENDED_EXCEPTIONS {
"com.highfidelity.experimental.enableExtendedJSExceptions"
};
const QString BaseScriptEngine::SCRIPT_EXCEPTION_FORMAT { "[%0] %1 in %2:%3" };
const QString BaseScriptEngine::SCRIPT_BACKTRACE_SEP { "\n " };
bool BaseScriptEngine::IS_THREADSAFE_INVOCATION(const QThread *thread, const QString& method) {
if (QThread::currentThread() == thread) {
return true;
}
qCCritical(shared) << QString("Scripting::%1 @ %2 -- ignoring thread-unsafe call from %3")
.arg(method).arg(thread ? thread->objectName() : "(!thread)").arg(QThread::currentThread()->objectName());
qCDebug(shared) << "(please resolve on the calling side by using invokeMethod, executeOnScriptThread, etc.)";
return false;
}
// engine-aware JS Error copier and factory
QScriptValue BaseScriptEngine::makeError(const QScriptValue& _other, const QString& type) {
if (!IS_THREADSAFE_INVOCATION(thread(), __FUNCTION__)) {
return unboundNullValue();
}
auto other = _other;
if (other.isString()) {
other = newObject();
@ -41,7 +50,7 @@ QScriptValue BaseScriptEngine::makeError(const QScriptValue& _other, const QStri
}
if (!proto.isFunction()) {
#ifdef DEBUG_JS_EXCEPTIONS
qCDebug(scriptengine) << "BaseScriptEngine::makeError -- couldn't find constructor for" << type << " -- using Error instead";
qCDebug(shared) << "BaseScriptEngine::makeError -- couldn't find constructor for" << type << " -- using Error instead";
#endif
proto = globalObject().property("Error");
}
@ -64,6 +73,9 @@ QScriptValue BaseScriptEngine::makeError(const QScriptValue& _other, const QStri
// check syntax and when there are issues returns an actual "SyntaxError" with the details
QScriptValue BaseScriptEngine::lintScript(const QString& sourceCode, const QString& fileName, const int lineNumber) {
if (!IS_THREADSAFE_INVOCATION(thread(), __FUNCTION__)) {
return unboundNullValue();
}
const auto syntaxCheck = checkSyntax(sourceCode);
if (syntaxCheck.state() != QScriptSyntaxCheckResult::Valid) {
auto err = globalObject().property("SyntaxError")
@ -82,13 +94,16 @@ QScriptValue BaseScriptEngine::lintScript(const QString& sourceCode, const QStri
}
return err;
}
return undefinedValue();
return QScriptValue();
}
// this pulls from the best available information to create a detailed snapshot of the current exception
QScriptValue BaseScriptEngine::cloneUncaughtException(const QString& extraDetail) {
if (!IS_THREADSAFE_INVOCATION(thread(), __FUNCTION__)) {
return unboundNullValue();
}
if (!hasUncaughtException()) {
return QScriptValue();
return unboundNullValue();
}
auto exception = uncaughtException();
// ensure the error object is engine-local
@ -144,7 +159,10 @@ QScriptValue BaseScriptEngine::cloneUncaughtException(const QString& extraDetail
return err;
}
QString BaseScriptEngine::formatException(const QScriptValue& exception) {
QString BaseScriptEngine::formatException(const QScriptValue& exception, bool includeExtendedDetails) {
if (!IS_THREADSAFE_INVOCATION(thread(), __FUNCTION__)) {
return QString();
}
QString note { "UncaughtException" };
QString result;
@ -156,8 +174,8 @@ QString BaseScriptEngine::formatException(const QScriptValue& exception) {
const auto lineNumber = exception.property("lineNumber").toString();
const auto stacktrace = exception.property("stack").toString();
if (_enableExtendedJSExceptions.get()) {
// This setting toggles display of the hints now being added during the loading process.
if (includeExtendedDetails) {
// Display additional exception / troubleshooting hints that can be added via the custom Error .detail property
// Example difference:
// [UncaughtExceptions] Error: Can't find variable: foobar in atp:/myentity.js\n...
// [UncaughtException (construct {1eb5d3fa-23b1-411c-af83-163af7220e3f})] Error: Can't find variable: foobar in atp:/myentity.js\n...
@ -173,14 +191,39 @@ QString BaseScriptEngine::formatException(const QScriptValue& exception) {
return result;
}
bool BaseScriptEngine::raiseException(const QScriptValue& exception) {
if (!IS_THREADSAFE_INVOCATION(thread(), __FUNCTION__)) {
return false;
}
if (currentContext()) {
// we have an active context / JS stack frame so throw the exception per usual
currentContext()->throwValue(makeError(exception));
return true;
} else {
// we are within a pure C++ stack frame (ie: being called directly by other C++ code)
// in this case no context information is available so just emit the exception for reporting
emit unhandledException(makeError(exception));
}
return false;
}
bool BaseScriptEngine::maybeEmitUncaughtException(const QString& debugHint) {
if (!IS_THREADSAFE_INVOCATION(thread(), __FUNCTION__)) {
return false;
}
if (!isEvaluating() && hasUncaughtException()) {
emit unhandledException(cloneUncaughtException(debugHint));
clearExceptions();
return true;
}
return false;
}
QScriptValue BaseScriptEngine::evaluateInClosure(const QScriptValue& closure, const QScriptProgram& program) {
PROFILE_RANGE(script, "evaluateInClosure");
if (QThread::currentThread() != thread()) {
qCCritical(scriptengine) << "*** CRITICAL *** ScriptEngine::evaluateInClosure() is meant to be called from engine thread only.";
// note: a recursive mutex might be needed around below code if this method ever becomes Q_INVOKABLE
return QScriptValue();
if (!IS_THREADSAFE_INVOCATION(thread(), __FUNCTION__)) {
return unboundNullValue();
}
const auto fileName = program.fileName();
const auto shortName = QUrl(fileName).fileName();
@ -189,7 +232,7 @@ QScriptValue BaseScriptEngine::evaluateInClosure(const QScriptValue& closure, co
auto global = closure.property("global");
if (global.isObject()) {
#ifdef DEBUG_JS
qCDebug(scriptengine) << " setting global = closure.global" << shortName;
qCDebug(shared) << " setting global = closure.global" << shortName;
#endif
oldGlobal = globalObject();
setGlobalObject(global);
@ -200,34 +243,34 @@ QScriptValue BaseScriptEngine::evaluateInClosure(const QScriptValue& closure, co
auto thiz = closure.property("this");
if (thiz.isObject()) {
#ifdef DEBUG_JS
qCDebug(scriptengine) << " setting this = closure.this" << shortName;
qCDebug(shared) << " setting this = closure.this" << shortName;
#endif
context->setThisObject(thiz);
}
context->pushScope(closure);
#ifdef DEBUG_JS
qCDebug(scriptengine) << QString("[%1] evaluateInClosure %2").arg(isEvaluating()).arg(shortName);
qCDebug(shared) << QString("[%1] evaluateInClosure %2").arg(isEvaluating()).arg(shortName);
#endif
{
result = BaseScriptEngine::evaluate(program);
if (hasUncaughtException()) {
auto err = cloneUncaughtException(__FUNCTION__);
#ifdef DEBUG_JS_EXCEPTIONS
qCWarning(scriptengine) << __FUNCTION__ << "---------- hasCaught:" << err.toString() << result.toString();
qCWarning(shared) << __FUNCTION__ << "---------- hasCaught:" << err.toString() << result.toString();
err.setProperty("_result", result);
#endif
result = err;
}
}
#ifdef DEBUG_JS
qCDebug(scriptengine) << QString("[%1] //evaluateInClosure %2").arg(isEvaluating()).arg(shortName);
qCDebug(shared) << QString("[%1] //evaluateInClosure %2").arg(isEvaluating()).arg(shortName);
#endif
popContext();
if (oldGlobal.isValid()) {
#ifdef DEBUG_JS
qCDebug(scriptengine) << " restoring global" << shortName;
qCDebug(shared) << " restoring global" << shortName;
#endif
setGlobalObject(oldGlobal);
}
@ -236,7 +279,6 @@ QScriptValue BaseScriptEngine::evaluateInClosure(const QScriptValue& closure, co
}
// Lambda
QScriptValue BaseScriptEngine::newLambdaFunction(std::function<QScriptValue(QScriptContext *, QScriptEngine*)> operation, const QScriptValue& data, const QScriptEngine::ValueOwnership& ownership) {
auto lambda = new Lambda(this, operation, data);
auto object = newQObject(lambda, ownership);
@ -262,26 +304,57 @@ Lambda::Lambda(QScriptEngine *engine, std::function<QScriptValue(QScriptContext
#endif
}
QScriptValue Lambda::call() {
if (!BaseScriptEngine::IS_THREADSAFE_INVOCATION(engine->thread(), __FUNCTION__)) {
return BaseScriptEngine::unboundNullValue();
}
return operation(engine->currentContext(), engine);
}
QScriptValue makeScopedHandlerObject(QScriptValue scopeOrCallback, QScriptValue methodOrName) {
auto engine = scopeOrCallback.engine();
if (!engine) {
return scopeOrCallback;
}
auto scope = QScriptValue();
auto callback = scopeOrCallback;
if (scopeOrCallback.isObject()) {
if (methodOrName.isString()) {
scope = scopeOrCallback;
callback = scope.property(methodOrName.toString());
} else if (methodOrName.isFunction()) {
scope = scopeOrCallback;
callback = methodOrName;
}
}
auto handler = engine->newObject();
handler.setProperty("scope", scope);
handler.setProperty("callback", callback);
return handler;
}
QScriptValue callScopedHandlerObject(QScriptValue handler, QScriptValue err, QScriptValue result) {
return handler.property("callback").call(handler.property("scope"), QScriptValueList({ err, result }));
}
#ifdef DEBUG_JS
void BaseScriptEngine::_debugDump(const QString& header, const QScriptValue& object, const QString& footer) {
if (!IS_THREADSAFE_INVOCATION(thread(), __FUNCTION__)) {
return;
}
if (!header.isEmpty()) {
qCDebug(scriptengine) << header;
qCDebug(shared) << header;
}
if (!object.isObject()) {
qCDebug(scriptengine) << "(!isObject)" << object.toVariant().toString() << object.toString();
qCDebug(shared) << "(!isObject)" << object.toVariant().toString() << object.toString();
return;
}
QScriptValueIterator it(object);
while (it.hasNext()) {
it.next();
qCDebug(scriptengine) << it.name() << ":" << it.value().toString();
qCDebug(shared) << it.name() << ":" << it.value().toString();
}
if (!footer.isEmpty()) {
qCDebug(scriptengine) << footer;
qCDebug(shared) << footer;
}
}
#endif

View file

@ -16,38 +16,61 @@
#include <QtCore/QDebug>
#include <QtScript/QScriptEngine>
#include "SettingHandle.h"
// common base class for extending QScriptEngine itself
class BaseScriptEngine : public QScriptEngine {
class BaseScriptEngine : public QScriptEngine, public QEnableSharedFromThis<BaseScriptEngine> {
Q_OBJECT
public:
static const QString SCRIPT_EXCEPTION_FORMAT;
static const QString SCRIPT_BACKTRACE_SEP;
BaseScriptEngine() {}
// threadsafe "unbound" version of QScriptEngine::nullValue()
static const QScriptValue unboundNullValue() { return QScriptValue(0, QScriptValue::NullValue); }
Q_INVOKABLE QScriptValue evaluateInClosure(const QScriptValue& locals, const QScriptProgram& program);
BaseScriptEngine() {}
Q_INVOKABLE QScriptValue lintScript(const QString& sourceCode, const QString& fileName, const int lineNumber = 1);
Q_INVOKABLE QScriptValue makeError(const QScriptValue& other = QScriptValue(), const QString& type = "Error");
Q_INVOKABLE QString formatException(const QScriptValue& exception);
QScriptValue cloneUncaughtException(const QString& detail = QString());
Q_INVOKABLE QString formatException(const QScriptValue& exception, bool includeExtendedDetails);
QScriptValue cloneUncaughtException(const QString& detail = QString());
QScriptValue evaluateInClosure(const QScriptValue& locals, const QScriptProgram& program);
// if there is a pending exception and we are at the top level (non-recursive) stack frame, this emits and resets it
bool maybeEmitUncaughtException(const QString& debugHint = QString());
// if the currentContext() is valid then throw the passed exception; otherwise, immediately emit it.
// note: this is used in cases where C++ code might call into JS API methods directly
bool raiseException(const QScriptValue& exception);
// helper to detect and log warnings when other code invokes QScriptEngine/BaseScriptEngine in thread-unsafe ways
static bool IS_THREADSAFE_INVOCATION(const QThread *thread, const QString& method);
signals:
void unhandledException(const QScriptValue& exception);
protected:
void _emitUnhandledException(const QScriptValue& exception);
// like `newFunction`, but allows mapping inline C++ lambdas with captures as callable QScriptValues
// even though the context/engine parameters are redundant in most cases, the function signature matches `newFunction`
// anyway so that newLambdaFunction can be used to rapidly prototype / test utility APIs and then if becoming
// permanent more easily promoted into regular static newFunction scenarios.
QScriptValue newLambdaFunction(std::function<QScriptValue(QScriptContext *context, QScriptEngine* engine)> operation, const QScriptValue& data = QScriptValue(), const QScriptEngine::ValueOwnership& ownership = QScriptEngine::AutoOwnership);
static const QString _SETTINGS_ENABLE_EXTENDED_EXCEPTIONS;
Setting::Handle<bool> _enableExtendedJSExceptions { _SETTINGS_ENABLE_EXTENDED_EXCEPTIONS, true };
#ifdef DEBUG_JS
static void _debugDump(const QString& header, const QScriptValue& object, const QString& footer = QString());
#endif
};
// Standardized CPS callback helpers (see: http://fredkschott.com/post/2014/03/understanding-error-first-callbacks-in-node-js/)
// These two helpers allow async JS APIs that use a callback parameter to be more friendly to scripters by accepting thisObject
// context and adopting a consistent and intuitable callback signature:
// function callback(err, result) { if (err) { ... } else { /* do stuff with result */ } }
//
// To use, first pass the user-specified callback args in the same order used with optionally-scoped Qt signal connections:
// auto handler = makeScopedHandlerObject(scopeOrCallback, optionalMethodOrName);
// And then invoke the scoped handler later per CPS conventions:
// auto result = callScopedHandlerObject(handler, err, result);
QScriptValue makeScopedHandlerObject(QScriptValue scopeOrCallback, QScriptValue methodOrName);
QScriptValue callScopedHandlerObject(QScriptValue handler, QScriptValue err, QScriptValue result);
// Lambda helps create callable QScriptValues out of std::functions:
// (just meant for use from within the script engine itself)
class Lambda : public QObject {