mirror of
https://thingvellir.net/git/overte
synced 2025-03-27 23:52:03 +01:00
* 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:
parent
9b09651337
commit
40ba8185a0
8 changed files with 440 additions and 159 deletions
|
@ -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);
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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 {
|
||||
|
|
Loading…
Reference in a new issue