History

* Update per 21114-part2 changes. * Add explicit thread safety guards. * Add Entities.queryPropertyMetdata for unit testing Entity script module support. * Cleanup / commenting pass.

humbletim committed 9 years ago

Commit 40ba8181 parent 9b096518 files · +439 −158Browse files

Showing the changes to libraries/script-engine/src/ScriptEngine.cpp (1 of 8 files). Show all

Unified · Split

libraries/script-engine/src/ScriptEngine.cpp+163−117
@@ -19,6 +19,9 @@ · 18 unmodified lines
1919 #include <QtCore/QThread>
2020 #include <QtCore/QRegularExpression>
2121
22+#include <QtCore/QFuture>
23+#include <QtConcurrent/QtConcurrentRun>
24+
2225 #include <QtWidgets/QMainWindow>
2326 #include <QtWidgets/QApplication>
2427
@@ -74,6 +77,10 @@ const QString ScriptEngine::_SETTINGS_ENABLE_EXTENDED_MODULE_COMPAT { · 49 unmodified lines
7477 "com.highfidelity.experimental.enableExtendedModuleCompatbility"
7578 };
7679
80+const QString ScriptEngine::_SETTINGS_ENABLE_EXTENDED_EXCEPTIONS {
81+ "com.highfidelity.experimental.enableExtendedJSExceptions"
82+};
83+
7784 static const int MAX_MODULE_ID_LENTGH { 4096 };
7885 static const int MAX_DEBUG_VALUE_LENGTH { 80 };
7986
@@ -146,7 +153,7 @@ QString encodeEntityIdIntoEntityUrl(const QString& url, const QString& entityID) · 66 unmodified lines
146153 }
147154
148155 QString ScriptEngine::logException(const QScriptValue& exception) {
149− auto message = formatException(exception);
156+ auto message = formatException(exception, _enableExtendedJSExceptions.get());
150157 scriptErrorMessage(qPrintable(message));
151158 return message;
152159 }
@@ -338,7 +345,11 @@ void ScriptEngine::runInThread() { · 185 unmodified lines
338345 // The thread interface cannot live on itself, and we want to move this into the thread, so
339346 // the thread cannot have this as a parent.
340347 QThread* workerThread = new QThread();
348+#ifdef Q_OS_LINUX
349+ workerThread->setObjectName(QString("js:") + getFilename());
350+#else
341351 workerThread->setObjectName(QString("Script Thread:") + getFilename());
352+#endif
342353 moveToThread(workerThread);
343354
344355 // NOTE: If you connect any essential signals for proper shutdown or cleanup of
@@ -539,41 +550,37 @@ static QScriptValue createScriptableResourcePrototype(QScriptEngine* engine) { · 194 unmodified lines
539550
540551 void ScriptEngine::resetModuleCache(bool deleteScriptCache) {
541552 if (QThread::currentThread() != thread()) {
542− QMetaObject::invokeMethod(this, "resetModuleCache");
553+ executeOnScriptThread([=](){ resetModuleCache(deleteScriptCache); });
543554 return;
544555 }
545− {
546− QMutexLocker locker(&_requireLock);
547− auto jsRequire = globalObject().property("Script").property("require");
548− auto cache = jsRequire.property("cache");
549− auto cacheMeta = jsRequire.data();
550−
551− if (deleteScriptCache) {
552− QScriptValueIterator it(cache);
553− while (it.hasNext()) {
554− it.next();
555− if (it.flags() & QScriptValue::SkipInEnumeration) {
556− continue;
557− }
558− //scriptCache->deleteScript(it.name());
559− qCDebug(scriptengine) << "resetModuleCache(true) -- staging " << it.name() << " for cache reset at next require";
560− cacheMeta.setProperty(it.name(), true);
556+ auto jsRequire = globalObject().property("Script").property("require");
557+ auto cache = jsRequire.property("cache");
558+ auto cacheMeta = jsRequire.data();
559+
560+ if (deleteScriptCache) {
561+ QScriptValueIterator it(cache);
562+ while (it.hasNext()) {
563+ it.next();
564+ if (it.flags() & QScriptValue::SkipInEnumeration) {
565+ continue;
561566 }
567+ //scriptCache->deleteScript(it.name());
568+ qCDebug(scriptengine) << "resetModuleCache(true) -- staging " << it.name() << " for cache reset at next require";
569+ cacheMeta.setProperty(it.name(), true);
562570 }
563− //_debugDump("cacheMeta", cacheMeta);
564− cache = newObject();
565− if (!cacheMeta.isObject()) {
566− cacheMeta = newObject();
567− cacheMeta.setProperty("id", "Script.require.cacheMeta");
568− cacheMeta.setProperty("type", "cacheMeta");
569− jsRequire.setData(cacheMeta);
570− }
571− cache.setProperty("__created__", (double)QDateTime::currentMSecsSinceEpoch(), QScriptValue::SkipInEnumeration);
571+ }
572+ cache = newObject();
573+ if (!cacheMeta.isObject()) {
574+ cacheMeta = newObject();
575+ cacheMeta.setProperty("id", "Script.require.cacheMeta");
576+ cacheMeta.setProperty("type", "cacheMeta");
577+ jsRequire.setData(cacheMeta);
578+ }
579+ cache.setProperty("__created__", (double)QDateTime::currentMSecsSinceEpoch(), QScriptValue::SkipInEnumeration);
572580 #if DEBUG_JS_MODULES
573− cache.setProperty("__meta__", cacheMeta, HIDDEN_PROP_FLAGS);
581+ cache.setProperty("__meta__", cacheMeta, READONLY_HIDDEN_PROP_FLAGS);
574582 #endif
575− jsRequire.setProperty("cache", cache, QScriptValue::ReadOnly | QScriptValue::Undeletable);
576− }
583+ jsRequire.setProperty("cache", cache, QScriptValue::ReadOnly | QScriptValue::Undeletable);
577584 }
578585
579586 void ScriptEngine::init() {
@@ -916,6 +923,11 @@ void ScriptEngine::addEventHandler(const EntityItemID& entityID, const QString& · 336 unmodified lines
916923 handlersForEvent << handlerData; // Note that the same handler can be added many times. See removeEntityEventHandler().
917924 }
918925
926+// this is not redundant -- the version in BaseScriptEngine is specifically not Q_INVOKABLE
927+QScriptValue ScriptEngine::evaluateInClosure(const QScriptValue& closure, const QScriptProgram& program) {
928+ return BaseScriptEngine::evaluateInClosure(closure, program);
929+}
930+
919931 QScriptValue ScriptEngine::evaluate(const QString& sourceCode, const QString& fileName, int lineNumber) {
920932 if (DependencyManager::get<ScriptEngines>()->isStopped()) {
921933 return QScriptValue(); // bail early
@@ -938,29 +950,26 @@ QScriptValue ScriptEngine::evaluate(const QString& sourceCode, const QString& fi · 16 unmodified lines
938950 // Check syntax
939951 auto syntaxError = lintScript(sourceCode, fileName);
940952 if (syntaxError.isError()) {
941− if (isEvaluating()) {
942− currentContext()->throwValue(syntaxError);
943− } else {
953+ if (!isEvaluating()) {
944954 syntaxError.setProperty("detail", "evaluate");
945− emit unhandledException(syntaxError);
946955 }
956+ raiseException(syntaxError);
957+ maybeEmitUncaughtException(__FUNCTION__);
947958 return syntaxError;
948959 }
949960 QScriptProgram program { sourceCode, fileName, lineNumber };
950961 if (program.isNull()) {
951962 // can this happen?
952963 auto err = makeError("could not create QScriptProgram for " + fileName);
953− emit unhandledException(err);
964+ raiseException(err);
965+ maybeEmitUncaughtException(__FUNCTION__);
954966 return err;
955967 }
956968
957969 QScriptValue result;
958970 {
959971 result = BaseScriptEngine::evaluate(program);
960− if (!isEvaluating() && hasUncaughtException()) {
961− emit unhandledException(cloneUncaughtException(__FUNCTION__));
962− clearExceptions();
963− }
972+ maybeEmitUncaughtException(__FUNCTION__);
964973 }
965974 return result;
966975 }
@@ -983,10 +992,7 @@ void ScriptEngine::run() { · 16 unmodified lines
983992
984993 {
985994 evaluate(_scriptContents, _fileNameString);
986− if (!isEvaluating() && hasUncaughtException()) {
987− emit unhandledException(cloneUncaughtException(__FUNCTION__));
988− clearExceptions();
989− }
995+ maybeEmitUncaughtException(__FUNCTION__);
990996 }
991997 #ifdef _WIN32
992998 // VS13 does not sleep_until unless it uses the system_clock, see:
@@ -1359,24 +1365,30 @@ void ScriptEngine::print(const QString& message) { · 366 unmodified lines
13591365
13601366 // Script.require.resolve -- like resolvePath, but performs more validation and throws exceptions on invalid module identifiers (for consistency with Node.js)
13611367 QString ScriptEngine::_requireResolve(const QString& moduleId, const QString& relativeTo) {
1368+ if (!IS_THREADSAFE_INVOCATION(thread(), __FUNCTION__)) {
1369+ return QString();
1370+ }
13621371 QUrl defaultScriptsLoc = defaultScriptsLocation();
13631372 QUrl url(moduleId);
1364− ExceptionEmitter tryCatcher(this, __FUNCTION__);
13651373
13661374 auto displayId = moduleId;
13671375 if (displayId.length() > MAX_DEBUG_VALUE_LENGTH) {
13681376 displayId = displayId.mid(0, MAX_DEBUG_VALUE_LENGTH) + "...";
13691377 }
13701378 auto message = QString("Cannot find module '%1' (%2)").arg(displayId);
1371− auto ctx = currentContext();
1379+
1380+ auto throwResolveError = [&](const QScriptValue& error) -> QString {
1381+ raiseException(error);
1382+ maybeEmitUncaughtException("require.resolve");
1383+ return nullptr;
1384+ };
13721385
13731386 // de-fuzz the input a little by restricting to rational sizes
13741387 auto idLength = url.toString().length();
13751388 if (idLength < 1 || idLength > MAX_MODULE_ID_LENTGH) {
13761389 auto details = QString("rejecting invalid module id size (%1 chars [1,%2])")
13771390 .arg(idLength).arg(MAX_MODULE_ID_LENTGH);
1378− ctx->throwValue(makeError(message.arg(details), "RangeError"));
1379− return nullptr;
1391+ return throwResolveError(makeError(message.arg(details), "RangeError"));
13801392 }
13811393
13821394 // this regex matches: absolute, dotted or path-like URLs
@@ -1396,14 +1408,12 @@ QString ScriptEngine::_requireResolve(const QString& moduleId, const QString& re · 13 unmodified lines
13961408 url = defaultScriptsLoc;
13971409 url.setPath(systemModulePath);
13981410 if (!QFileInfo(url.toLocalFile()).isFile()) {
1399− ctx->throwValue(makeError(message.arg("system module not found")));
1400− return nullptr;
1411+ return throwResolveError(makeError(message.arg("system module not found")));
14011412 }
14021413 }
14031414
14041415 if (url.isRelative()) {
1405− ctx->throwValue(makeError(message.arg("could not resolve module id")));
1406− return nullptr;
1416+ return throwResolveError(makeError(message.arg("could not resolve module id")));
14071417 }
14081418
14091419 // 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 · 6 unmodified lines
14161426
14171427 bool disallowOutsideFiles = !defaultScriptsLocation().isParentOf(canonical) && !currentSandboxURL.isLocalFile();
14181428 if (disallowOutsideFiles && !PathUtils::isDescendantOf(canonical, currentSandboxURL)) {
1419− ctx->throwValue(makeError(message.arg(
1429+ return throwResolveError(makeError(message.arg(
14201430 QString("path '%1' outside of origin script '%2' '%3'")
14211431 .arg(PathUtils::stripFilename(url))
14221432 .arg(PathUtils::stripFilename(currentSandboxURL))
14231433 .arg(canonical.toString())
14241434 )));
1425− return nullptr;
14261435 }
14271436 if (!file.exists()) {
1428− ctx->throwValue(makeError(message.arg("path does not exist: " + url.toLocalFile())));
1429− return nullptr;
1437+ return throwResolveError(makeError(message.arg("path does not exist: " + url.toLocalFile())));
14301438 }
14311439 if (!file.isFile()) {
1432− ctx->throwValue(makeError(message.arg("path is not a file: " + url.toLocalFile())));
1433− return nullptr;
1440+ return throwResolveError(makeError(message.arg("path is not a file: " + url.toLocalFile())));
14341441 }
14351442 }
14361443
1444+ maybeEmitUncaughtException(__FUNCTION__);
14371445 return url.toString();
14381446 }
14391447
14401448 // retrieves the current parent module from the JS scope chain
14411449 QScriptValue ScriptEngine::currentModule() {
1450+ if (!IS_THREADSAFE_INVOCATION(thread(), __FUNCTION__)) {
1451+ return unboundNullValue();
1452+ }
14421453 auto jsRequire = globalObject().property("Script").property("require");
14431454 auto cache = jsRequire.property("cache");
14441455 auto candidate = QScriptValue();
1445− for(auto ctx = currentContext(); ctx && !candidate.isObject(); ctx = ctx->parentContext()) {
1446− QScriptContextInfo contextInfo { ctx };
1456+ for (auto c = currentContext(); c && !candidate.isObject(); c = c->parentContext()) {
1457+ QScriptContextInfo contextInfo { c };
14471458 candidate = cache.property(contextInfo.fileName());
14481459 }
14491460 if (!candidate.isObject()) {
@@ -1459,7 +1470,7 @@ bool ScriptEngine::registerModuleWithParent(const QScriptValue& module, const QS · 9 unmodified lines
14591470 if (children.isArray()) {
14601471 auto key = module.property("id");
14611472 auto length = children.property("length").toInt32();
1462− for(int i=0; i < length; i++) {
1473+ for (int i=0; i < length; i++) {
14631474 if (children.property(i).property("id").strictlyEquals(key)) {
14641475 qCDebug(scriptengine_module) << key.toString() << " updating parent.children[" << i << "] = module";
14651476 children.setProperty(i, module);
@@ -1488,7 +1499,7 @@ QScriptValue ScriptEngine::newModule(const QString& modulePath, const QScriptVal · 22 unmodified lines
14881499 closure.setProperty("exports", exports);
14891500
14901501 // make the closure available to module instantiation
1491− module.setProperty("__closure__", closure, HIDDEN_PROP_FLAGS);
1502+ module.setProperty("__closure__", closure, READONLY_HIDDEN_PROP_FLAGS);
14921503
14931504 // for consistency with Node.js Module
14941505 module.setProperty("id", modulePath, READONLY_PROP_FLAGS);
@@ -1524,7 +1535,7 @@ QScriptValue ScriptEngine::fetchModuleSource(const QString& modulePath, const bo · 29 unmodified lines
15241535 req.setProperty("url", url);
15251536 req.setProperty("status", status);
15261537 req.setProperty("success", ScriptCache::isSuccessStatus(status));
1527− req.setProperty("contents", contents, HIDDEN_PROP_FLAGS);
1538+ req.setProperty("contents", contents, READONLY_HIDDEN_PROP_FLAGS);
15281539 }
15291540 };
15301541
@@ -1568,73 +1579,54 @@ QScriptValue ScriptEngine::instantiateModule(const QScriptValue& module, const Q · 37 unmodified lines
15681579 auto modulePath = module.property("filename").toString();
15691580 auto closure = module.property("__closure__");
15701581
1571− qCDebug(scriptengine_module) << QString("require.instantiateModule: %1 / %2 bytes").arg(QUrl(modulePath).fileName()).arg(sourceCode.length());
1582+ qCDebug(scriptengine_module) << QString("require.instantiateModule: %1 / %2 bytes")
1583+ .arg(QUrl(modulePath).fileName()).arg(sourceCode.length());
15721584
1573− {
1574− ExceptionEmitter tryCatch(this, __FUNCTION__);
1575−
1576− if (module.property("content-type").toString() == "application/json") {
1577− qCDebug(scriptengine_module) << "... parsing as JSON";
1578−#ifdef DEV_OTHER_MODULE_JSON_PARSE
1579− auto JSON = globalObject().property("JSON");
1580− auto parse = JSON.property("parse");
1581− if (!parse.isFunction()) {
1582− currentContext()->throwValue(makeError("global JSON.parse is not a function", "EvalError"));
1583− return nullValue();
1584− }
1585− result = parse.call(JSON, QScriptValueList({ sourceCode }));
1586− closure.property("module").setProperty("exports", result);
1587− return result;
1588−#endif
1589− closure.setProperty("__json", sourceCode);
1590− result = evaluateInClosure(closure, { "module.exports = JSON.parse(__json)", modulePath });
1591− } else {
1592− // scoped vars for consistency with Node.js
1593− closure.setProperty("require", module.property("require"));
1594− closure.setProperty("__filename", modulePath, HIDDEN_PROP_FLAGS);
1595− closure.setProperty("__dirname", QString(modulePath).replace(QRegExp("/[^/]*$"), ""), HIDDEN_PROP_FLAGS);
1596− result = evaluateInClosure(closure, { sourceCode, modulePath });
1597− }
1585+ if (module.property("content-type").toString() == "application/json") {
1586+ qCDebug(scriptengine_module) << "... parsing as JSON";
1587+ closure.setProperty("__json", sourceCode);
1588+ result = evaluateInClosure(closure, { "module.exports = JSON.parse(__json)", modulePath });
1589+ } else {
1590+ // scoped vars for consistency with Node.js
1591+ closure.setProperty("require", module.property("require"));
1592+ closure.setProperty("__filename", modulePath, READONLY_HIDDEN_PROP_FLAGS);
1593+ closure.setProperty("__dirname", QString(modulePath).replace(QRegExp("/[^/]*$"), ""), READONLY_HIDDEN_PROP_FLAGS);
1594+ result = evaluateInClosure(closure, { sourceCode, modulePath });
15981595 }
1596+ maybeEmitUncaughtException(__FUNCTION__);
15991597 return result;
16001598 }
16011599
16021600 // CommonJS/Node.js like require/module support
16031601 QScriptValue ScriptEngine::require(const QString& moduleId) {
16041602 qCDebug(scriptengine_module) << "ScriptEngine::require(" << moduleId.left(MAX_DEBUG_VALUE_LENGTH) << ")";
1605− if (QThread::currentThread() != thread()) {
1606− qCDebug(scriptengine_module) << moduleId << " threads mismatch";
1607− return nullValue();
1603+ if (!IS_THREADSAFE_INVOCATION(thread(), __FUNCTION__)) {
1604+ return unboundNullValue();
16081605 }
16091606
1610− // serialize require calls so the ordering/caching works correctly across multiple Entities
1611− // note: this is a Recursive mutex so nested require's should not affected
1612− QMutexLocker locker(&_requireLock);
1613− ExceptionEmitter tryCatcher(this, __FUNCTION__);
1614− // _requireDepth++;
1615− // QObject autoDecrement;connect(&autoDecrement, &QObject::destroyed, this, [this](){ _requireDepth--; });
1616−
16171607 auto jsRequire = globalObject().property("Script").property("require");
16181608 auto cacheMeta = jsRequire.data();
16191609 auto cache = jsRequire.property("cache");
16201610 auto parent = currentModule();
16211611
1622− auto throwModuleError = [this, &cache](const QString& modulePath, const QScriptValue& error) {
1612+ auto throwModuleError = [&](const QString& modulePath, const QScriptValue& error) {
16231613 cache.setProperty(modulePath, nullValue());
16241614 if (!error.isNull()) {
16251615 #ifdef DEBUG_JS_MODULES
16261616 qCWarning(scriptengine_module) << "throwing module error:" << error.toString() << modulePath << error.property("stack").toString();
16271617 #endif
1628− currentContext()->throwValue(makeError(error));
1618+ raiseException(error);
16291619 }
1630− return nullValue();
1620+ maybeEmitUncaughtException("module");
1621+ return unboundNullValue();
16311622 };
16321623
16331624 // start by resolving the moduleId into a fully-qualified path/URL
16341625 QString modulePath = _requireResolve(moduleId);
1635− if (modulePath.isNull() || tryCatcher.hasPending()) {
1626+ if (modulePath.isNull() || hasUncaughtException()) {
16361627 // the resolver already threw an exception -- bail early
1637− return nullValue();
1628+ maybeEmitUncaughtException(__FUNCTION__);
1629+ return unboundNullValue();
16381630 }
16391631
16401632 // check the resolved path against the cache
@@ -1656,6 +1648,7 @@ QScriptValue ScriptEngine::require(const QString& moduleId) { · 15 unmodified lines
16561648 qCDebug(scriptengine_module) << QString("require - using cached module '%1' for '%2' (loaded: %3)")
16571649 .arg(modulePath).arg(moduleId).arg(module.property("loaded").toString());
16581650 registerModuleWithParent(module, parent);
1651+ maybeEmitUncaughtException(__FUNCTION__);
16591652 return exports;
16601653 }
16611654
@@ -1705,7 +1698,7 @@ QScriptValue ScriptEngine::require(const QString& moduleId) { · 43 unmodified lines
17051698 #ifdef DEBUG_JS
17061699 _debugDump("this", thisObject);
17071700 #endif
1708− _applyUserOptions(module, thisObject);
1701+ applyUserOptions(module, thisObject);
17091702 }
17101703 }
17111704
@@ -1725,11 +1718,15 @@ QScriptValue ScriptEngine::require(const QString& moduleId) { · 13 unmodified lines
17251718
17261719 qCDebug(scriptengine_module) << "//ScriptEngine::require(" << moduleId << ")";
17271720
1721+ maybeEmitUncaughtException(__FUNCTION__);
17281722 return module.property("exports");
17291723 }
17301724
17311725 // User-configurable override options
1732−void ScriptEngine::_applyUserOptions(QScriptValue& module, QScriptValue& options) {
1726+void ScriptEngine::applyUserOptions(QScriptValue& module, QScriptValue& options) {
1727+ if (!IS_THREADSAFE_INVOCATION(thread(), __FUNCTION__)) {
1728+ return;
1729+ }
17331730 if (!options.isValid()) {
17341731 return;
17351732 }
@@ -1770,6 +1767,7 @@ void ScriptEngine::_applyUserOptions(QScriptValue& module, QScriptValue& options · 34 unmodified lines
17701767 qCDebug(scriptengine_module) << "module.closure['global'] = options.global";
17711768 }
17721769 }
1770+ maybeEmitUncaughtException(__FUNCTION__);
17731771 }
17741772
17751773 // 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 · 1 unmodified line
17771775 // If no callback is specified, the included files will be loaded synchronously and will block execution until
17781776 // all of the files have finished loading.
17791777 void ScriptEngine::include(const QStringList& includeFiles, QScriptValue callback) {
1778+ if (!IS_THREADSAFE_INVOCATION(thread(), __FUNCTION__)) {
1779+ return;
1780+ }
17801781 if (DependencyManager::get<ScriptEngines>()->isStopped()) {
17811782 scriptWarningMessage("Script.include() while shutting down is ignored... includeFiles:"
17821783 + includeFiles.join(",") + "parent script:" + getFilename());
@@ -1886,6 +1887,9 @@ void ScriptEngine::include(const QString& includeFile, QScriptValue callback) { · 103 unmodified lines
18861887 // as a stand-alone script. To accomplish this, the ScriptEngine class just emits a signal which
18871888 // the Application or other context will connect to in order to know to actually load the script
18881889 void ScriptEngine::load(const QString& loadFile) {
1890+ if (!IS_THREADSAFE_INVOCATION(thread(), __FUNCTION__)) {
1891+ return;
1892+ }
18891893 if (DependencyManager::get<ScriptEngines>()->isStopped()) {
18901894 scriptWarningMessage("Script.load() while shutting down is ignored... loadFile:"
18911895 + loadFile + "parent script:" + getFilename());
@@ -1955,6 +1959,52 @@ void ScriptEngine::updateEntityScriptStatus(const EntityItemID& entityID, const · 63 unmodified lines
19551959 emit entityScriptDetailsUpdated();
19561960 }
19571961
1962+QVariant ScriptEngine::cloneEntityScriptDetails(const EntityItemID& entityID) {
1963+ static const QVariant NULL_VARIANT { qVariantFromValue((QObject*)nullptr) };
1964+ QVariantMap map;
1965+ if (entityID.isNull()) {
1966+ // TODO: find better way to report JS Error across thread/process boundaries
1967+ map["isError"] = true;
1968+ map["errorInfo"] = "Error: getEntityScriptDetails -- invalid entityID";
1969+ } else {
1970+#ifdef DEBUG_ENTITY_STATES
1971+ qDebug() << "cloneEntityScriptDetails" << entityID << QThread::currentThread();
1972+#endif
1973+ EntityScriptDetails scriptDetails;
1974+ if (getEntityScriptDetails(entityID, scriptDetails)) {
1975+#ifdef DEBUG_ENTITY_STATES
1976+ qDebug() << "gotEntityScriptDetails" << scriptDetails.status << QThread::currentThread();
1977+#endif
1978+ map["isRunning"] = isEntityScriptRunning(entityID);
1979+ map["status"] = EntityScriptStatus_::valueToKey(scriptDetails.status).toLower();
1980+ map["errorInfo"] = scriptDetails.errorInfo;
1981+ map["entityID"] = entityID.toString();
1982+#ifdef DEBUG_ENTITY_STATES
1983+ {
1984+ auto debug = QVariantMap();
1985+ debug["script"] = scriptDetails.scriptText;
1986+ debug["scriptObject"] = scriptDetails.scriptObject.toVariant();
1987+ debug["lastModified"] = (qlonglong)scriptDetails.lastModified;
1988+ debug["sandboxURL"] = scriptDetails.definingSandboxURL;
1989+ map["debug"] = debug;
1990+ }
1991+#endif
1992+ } else {
1993+#ifdef DEBUG_ENTITY_STATES
1994+ qDebug() << "!gotEntityScriptDetails" << QThread::currentThread();
1995+#endif
1996+ map["isError"] = true;
1997+ map["errorInfo"] = "Entity script details unavailable";
1998+ map["entityID"] = entityID.toString();
1999+ }
2000+ }
2001+ return map;
2002+}
2003+
2004+QFuture<QVariant> ScriptEngine::getLocalEntityScriptDetails(const EntityItemID& entityID) {
2005+ return QtConcurrent::run(this, &ScriptEngine::cloneEntityScriptDetails, entityID);
2006+}
2007+
19582008 bool ScriptEngine::getEntityScriptDetails(const EntityItemID& entityID, EntityScriptDetails &details) const {
19592009 auto it = _entityScripts.constFind(entityID);
19602010 if (it == _entityScripts.constEnd()) {
@@ -2093,10 +2143,10 @@ void ScriptEngine::loadEntityScript(const EntityItemID& entityID, const QString& · 132 unmodified lines
20932143
20942144 auto scriptCache = DependencyManager::get<ScriptCache>();
20952145 // note: see EntityTreeRenderer.cpp for shared pointer lifecycle management
2096− QWeakPointer<ScriptEngine> weakRef(sharedFromThis());
2146+ QWeakPointer<BaseScriptEngine> weakRef(sharedFromThis());
20972147 scriptCache->getScriptContents(entityScript,
20982148 [this, weakRef, entityScript, entityID](const QString& url, const QString& contents, bool isURL, bool success, const QString& status) {
2099− QSharedPointer<ScriptEngine> strongRef(weakRef);
2149+ QSharedPointer<BaseScriptEngine> strongRef(weakRef);
21002150 if (!strongRef) {
21012151 qCWarning(scriptengine) << "loadEntityScript.contentAvailable -- ScriptEngine was deleted during getScriptContents!!";
21022152 return;
@@ -2215,13 +2265,12 @@ void ScriptEngine::entityScriptContentAvailable(const EntityItemID& entityID, co · 112 unmodified lines
22152265 timeout.setSingleShot(true);
22162266 timeout.start(SANDBOX_TIMEOUT);
22172267 connect(&timeout, &QTimer::timeout, [&sandbox, SANDBOX_TIMEOUT, scriptOrURL]{
2218− auto context = sandbox.currentContext();
2219− if (context) {
22202268 qCDebug(scriptengine) << "ScriptEngine::entityScriptContentAvailable timeout(" << scriptOrURL << ")";
22212269
22222270 // Guard against infinite loops and non-performant code
2223− context->throwError(QString("Timed out (entity constructors are limited to %1ms)").arg(SANDBOX_TIMEOUT));
2224− }
2271+ sandbox.raiseException(
2272+ sandbox.makeError(QString("Timed out (entity constructors are limited to %1ms)").arg(SANDBOX_TIMEOUT))
2273+ );
22252274 });
22262275
22272276 testConstructor = sandbox.evaluate(program);
@@ -2237,7 +2286,7 @@ void ScriptEngine::entityScriptContentAvailable(const EntityItemID& entityID, co · 9 unmodified lines
22372286 if (exception.isError()) {
22382287 // create a local copy using makeError to decouple from the sandbox engine
22392288 exception = makeError(exception);
2240− setError(formatException(exception), EntityScriptStatus::ERROR_RUNNING_SCRIPT);
2289+ setError(formatException(exception, _enableExtendedJSExceptions.get()), EntityScriptStatus::ERROR_RUNNING_SCRIPT);
22412290 emit unhandledException(exception);
22422291 return;
22432292 }
@@ -2288,7 +2337,7 @@ void ScriptEngine::entityScriptContentAvailable(const EntityItemID& entityID, co · 44 unmodified lines
22882337
22892338 if (entityScriptObject.isError()) {
22902339 auto exception = entityScriptObject;
2291− setError(formatException(exception), EntityScriptStatus::ERROR_RUNNING_SCRIPT);
2340+ setError(formatException(exception, _enableExtendedJSExceptions.get()), EntityScriptStatus::ERROR_RUNNING_SCRIPT);
22922341 emit unhandledException(exception);
22932342 return;
22942343 }
@@ -2420,10 +2469,7 @@ void ScriptEngine::doWithEnvironment(const EntityItemID& entityID, const QUrl& s · 125 unmodified lines
24202469 #else
24212470 operation();
24222471 #endif
2423− if (!isEvaluating() && hasUncaughtException()) {
2424− emit unhandledException(cloneUncaughtException(!entityID.isNull() ? entityID.toString() : __FUNCTION__));
2425− clearExceptions();
2426− }
2472+ maybeEmitUncaughtException(!entityID.isNull() ? entityID.toString() : __FUNCTION__);
24272473 currentEntityIdentifier = oldIdentifier;
24282474 currentSandboxURL = oldSandboxURL;
24292475 }