diff --git a/libraries/entities-renderer/src/EntityTreeRenderer.cpp b/libraries/entities-renderer/src/EntityTreeRenderer.cpp index bd25bcf905..2c07c368f9 100644 --- a/libraries/entities-renderer/src/EntityTreeRenderer.cpp +++ b/libraries/entities-renderer/src/EntityTreeRenderer.cpp @@ -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); diff --git a/libraries/entities/src/EntitiesScriptEngineProvider.h b/libraries/entities/src/EntitiesScriptEngineProvider.h index 69bf73e688..d87dd105c2 100644 --- a/libraries/entities/src/EntitiesScriptEngineProvider.h +++ b/libraries/entities/src/EntitiesScriptEngineProvider.h @@ -15,11 +15,13 @@ #define hifi_EntitiesScriptEngineProvider_h #include +#include #include "EntityItemID.h" class EntitiesScriptEngineProvider { public: virtual void callEntityScriptMethod(const EntityItemID& entityID, const QString& methodName, const QStringList& params = QStringList()) = 0; + virtual QFuture getLocalEntityScriptDetails(const EntityItemID& entityID) = 0; }; -#endif // hifi_EntitiesScriptEngineProvider_h \ No newline at end of file +#endif // hifi_EntitiesScriptEngineProvider_h diff --git a/libraries/entities/src/EntityScriptingInterface.cpp b/libraries/entities/src/EntityScriptingInterface.cpp index 540eba4511..df88194f9f 100644 --- a/libraries/entities/src/EntityScriptingInterface.cpp +++ b/libraries/entities/src/EntityScriptingInterface.cpp @@ -10,6 +10,9 @@ // #include "EntityScriptingInterface.h" +#include +#include + #include "EntityItemID.h" #include #include @@ -24,6 +27,7 @@ #include "ModelEntityItem.h" #include "QVariantGLM.h" #include "SimulationOwner.h" +#include "BaseScriptEngine.h" #include "ZoneEntityItem.h" #include "WebEntityItem.h" #include @@ -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 engine = dynamic_cast(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 *request = new QFutureWatcher; + connect(request, &QFutureWatcher::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; + 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(); + 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(); auto request = client->createScriptStatusRequest(entityID); diff --git a/libraries/entities/src/EntityScriptingInterface.h b/libraries/entities/src/EntityScriptingInterface.h index e9f0637830..2c3c654528 100644 --- a/libraries/entities/src/EntityScriptingInterface.h +++ b/libraries/entities/src/EntityScriptingInterface.h @@ -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); diff --git a/libraries/script-engine/src/ScriptEngine.cpp b/libraries/script-engine/src/ScriptEngine.cpp index 09d267f3ba..c7364450a1 100644 --- a/libraries/script-engine/src/ScriptEngine.cpp +++ b/libraries/script-engine/src/ScriptEngine.cpp @@ -19,6 +19,9 @@ #include #include +#include +#include + #include #include @@ -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()->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()->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()->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 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(); // note: see EntityTreeRenderer.cpp for shared pointer lifecycle management - QWeakPointer weakRef(sharedFromThis()); + QWeakPointer weakRef(sharedFromThis()); scriptCache->getScriptContents(entityScript, [this, weakRef, entityScript, entityID](const QString& url, const QString& contents, bool isURL, bool success, const QString& status) { - QSharedPointer strongRef(weakRef); + QSharedPointer 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; } diff --git a/libraries/script-engine/src/ScriptEngine.h b/libraries/script-engine/src/ScriptEngine.h index f64dec44c7..ef6f3b6896 100644 --- a/libraries/script-engine/src/ScriptEngine.h +++ b/libraries/script-engine/src/ScriptEngine.h @@ -79,7 +79,7 @@ public: QUrl definingSandboxURL { QUrl("about:EntityScript") }; }; -class ScriptEngine : public BaseScriptEngine, public EntitiesScriptEngineProvider, public QEnableSharedFromThis { +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 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 _entityScripts; QHash _occupiedScriptURLs; QList _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 _enableExtendedModuleCompatbility { _SETTINGS_ENABLE_EXTENDED_MODULE_COMPAT, false }; - void _applyUserOptions(QScriptValue& module, QScriptValue& options); + Setting::Handle _enableExtendedJSExceptions { _SETTINGS_ENABLE_EXTENDED_EXCEPTIONS, true }; + + void applyUserOptions(QScriptValue& module, QScriptValue& options); }; #endif // hifi_ScriptEngine_h diff --git a/libraries/shared/src/BaseScriptEngine.cpp b/libraries/shared/src/BaseScriptEngine.cpp index 16308c0650..d803e85ed6 100644 --- a/libraries/shared/src/BaseScriptEngine.cpp +++ b/libraries/shared/src/BaseScriptEngine.cpp @@ -10,6 +10,7 @@ // #include "BaseScriptEngine.h" +#include "SharedLogging.h" #include #include @@ -18,18 +19,26 @@ #include #include -#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 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::functionthread(), __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 - diff --git a/libraries/shared/src/BaseScriptEngine.h b/libraries/shared/src/BaseScriptEngine.h index 27a6eff33d..138e46fafa 100644 --- a/libraries/shared/src/BaseScriptEngine.h +++ b/libraries/shared/src/BaseScriptEngine.h @@ -16,38 +16,61 @@ #include #include -#include "SettingHandle.h" - // common base class for extending QScriptEngine itself -class BaseScriptEngine : public QScriptEngine { +class BaseScriptEngine : public QScriptEngine, public QEnableSharedFromThis { 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 operation, const QScriptValue& data = QScriptValue(), const QScriptEngine::ValueOwnership& ownership = QScriptEngine::AutoOwnership); - static const QString _SETTINGS_ENABLE_EXTENDED_EXCEPTIONS; - Setting::Handle _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 {