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
19#include <QtCore/QThread>19#include <QtCore/QThread>
20#include <QtCore/QRegularExpression>20#include <QtCore/QRegularExpression>
21 21
22#include <QtCore/QFuture>
23#include <QtConcurrent/QtConcurrentRun>
24
22#include <QtWidgets/QMainWindow>25#include <QtWidgets/QMainWindow>
23#include <QtWidgets/QApplication>26#include <QtWidgets/QApplication>
24 27
@@ -74,6 +77,10 @@ const QString ScriptEngine::_SETTINGS_ENABLE_EXTENDED_MODULE_COMPAT { · 49 unmodified lines
74 "com.highfidelity.experimental.enableExtendedModuleCompatbility"77 "com.highfidelity.experimental.enableExtendedModuleCompatbility"
75};78};
76 79
80const QString ScriptEngine::_SETTINGS_ENABLE_EXTENDED_EXCEPTIONS {
81 "com.highfidelity.experimental.enableExtendedJSExceptions"
82};
83
77static const int MAX_MODULE_ID_LENTGH { 4096 };84static const int MAX_MODULE_ID_LENTGH { 4096 };
78static const int MAX_DEBUG_VALUE_LENGTH { 80 };85static const int MAX_DEBUG_VALUE_LENGTH { 80 };
79 86
@@ -146,7 +153,7 @@ QString encodeEntityIdIntoEntityUrl(const QString& url, const QString& entityID) · 66 unmodified lines
146}153}
147 154
148QString ScriptEngine::logException(const QScriptValue& exception) {155QString ScriptEngine::logException(const QScriptValue& exception) {
149 auto message = formatException(exception);156 auto message = formatException(exception, _enableExtendedJSExceptions.get());
150 scriptErrorMessage(qPrintable(message));157 scriptErrorMessage(qPrintable(message));
151 return message;158 return message;
152}159}
@@ -338,7 +345,11 @@ void ScriptEngine::runInThread() { · 185 unmodified lines
338 // The thread interface cannot live on itself, and we want to move this into the thread, so345 // The thread interface cannot live on itself, and we want to move this into the thread, so
339 // the thread cannot have this as a parent.346 // the thread cannot have this as a parent.
340 QThread* workerThread = new QThread();347 QThread* workerThread = new QThread();
348#ifdef Q_OS_LINUX
349 workerThread->setObjectName(QString("js:") + getFilename());
350#else
341 workerThread->setObjectName(QString("Script Thread:") + getFilename());351 workerThread->setObjectName(QString("Script Thread:") + getFilename());
352#endif
342 moveToThread(workerThread);353 moveToThread(workerThread);
343 354
344 // NOTE: If you connect any essential signals for proper shutdown or cleanup of355 // 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
539 550
540void ScriptEngine::resetModuleCache(bool deleteScriptCache) {551void ScriptEngine::resetModuleCache(bool deleteScriptCache) {
541 if (QThread::currentThread() != thread()) {552 if (QThread::currentThread() != thread()) {
542 QMetaObject::invokeMethod(this, "resetModuleCache");553 executeOnScriptThread([=](){ resetModuleCache(deleteScriptCache); });
543 return;554 return;
544 }555 }
545 {556 auto jsRequire = globalObject().property("Script").property("require");
546 QMutexLocker locker(&_requireLock);557 auto cache = jsRequire.property("cache");
547 auto jsRequire = globalObject().property("Script").property("require");558 auto cacheMeta = jsRequire.data();
548 auto cache = jsRequire.property("cache");559
549 auto cacheMeta = jsRequire.data();560 if (deleteScriptCache) {
550 561 QScriptValueIterator it(cache);
551 if (deleteScriptCache) {562 while (it.hasNext()) {
552 QScriptValueIterator it(cache);563 it.next();
553 while (it.hasNext()) {564 if (it.flags() & QScriptValue::SkipInEnumeration) {
554 it.next();565 continue;
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);
561 }566 }
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);
562 }570 }
563 //_debugDump("cacheMeta", cacheMeta);571 }
564 cache = newObject();572 cache = newObject();
565 if (!cacheMeta.isObject()) {573 if (!cacheMeta.isObject()) {
566 cacheMeta = newObject();574 cacheMeta = newObject();
567 cacheMeta.setProperty("id", "Script.require.cacheMeta");575 cacheMeta.setProperty("id", "Script.require.cacheMeta");
568 cacheMeta.setProperty("type", "cacheMeta");576 cacheMeta.setProperty("type", "cacheMeta");
569 jsRequire.setData(cacheMeta);577 jsRequire.setData(cacheMeta);
570 }578 }
571 cache.setProperty("__created__", (double)QDateTime::currentMSecsSinceEpoch(), QScriptValue::SkipInEnumeration);579 cache.setProperty("__created__", (double)QDateTime::currentMSecsSinceEpoch(), QScriptValue::SkipInEnumeration);
572#if DEBUG_JS_MODULES580#if DEBUG_JS_MODULES
573 cache.setProperty("__meta__", cacheMeta, HIDDEN_PROP_FLAGS);581 cache.setProperty("__meta__", cacheMeta, READONLY_HIDDEN_PROP_FLAGS);
574#endif582#endif
575 jsRequire.setProperty("cache", cache, QScriptValue::ReadOnly | QScriptValue::Undeletable);583 jsRequire.setProperty("cache", cache, QScriptValue::ReadOnly | QScriptValue::Undeletable);
576 }
577}584}
578 585
579void ScriptEngine::init() {586void ScriptEngine::init() {
@@ -916,6 +923,11 @@ void ScriptEngine::addEventHandler(const EntityItemID& entityID, const QString& · 336 unmodified lines
916 handlersForEvent << handlerData; // Note that the same handler can be added many times. See removeEntityEventHandler().923 handlersForEvent << handlerData; // Note that the same handler can be added many times. See removeEntityEventHandler().
917}924}
918 925
926// this is not redundant -- the version in BaseScriptEngine is specifically not Q_INVOKABLE
927QScriptValue ScriptEngine::evaluateInClosure(const QScriptValue& closure, const QScriptProgram& program) {
928 return BaseScriptEngine::evaluateInClosure(closure, program);
929}
930
919QScriptValue ScriptEngine::evaluate(const QString& sourceCode, const QString& fileName, int lineNumber) {931QScriptValue ScriptEngine::evaluate(const QString& sourceCode, const QString& fileName, int lineNumber) {
920 if (DependencyManager::get<ScriptEngines>()->isStopped()) {932 if (DependencyManager::get<ScriptEngines>()->isStopped()) {
921 return QScriptValue(); // bail early933 return QScriptValue(); // bail early
@@ -938,29 +950,26 @@ QScriptValue ScriptEngine::evaluate(const QString& sourceCode, const QString& fi · 16 unmodified lines
938 // Check syntax950 // Check syntax
939 auto syntaxError = lintScript(sourceCode, fileName);951 auto syntaxError = lintScript(sourceCode, fileName);
940 if (syntaxError.isError()) {952 if (syntaxError.isError()) {
941 if (isEvaluating()) {953 if (!isEvaluating()) {
942 currentContext()->throwValue(syntaxError);
943 } else {
944 syntaxError.setProperty("detail", "evaluate");954 syntaxError.setProperty("detail", "evaluate");
945 emit unhandledException(syntaxError);
946 }955 }
956 raiseException(syntaxError);
957 maybeEmitUncaughtException(__FUNCTION__);
947 return syntaxError;958 return syntaxError;
948 }959 }
949 QScriptProgram program { sourceCode, fileName, lineNumber };960 QScriptProgram program { sourceCode, fileName, lineNumber };
950 if (program.isNull()) {961 if (program.isNull()) {
951 // can this happen?962 // can this happen?
952 auto err = makeError("could not create QScriptProgram for " + fileName);963 auto err = makeError("could not create QScriptProgram for " + fileName);
953 emit unhandledException(err);964 raiseException(err);
965 maybeEmitUncaughtException(__FUNCTION__);
954 return err;966 return err;
955 }967 }
956 968
957 QScriptValue result;969 QScriptValue result;
958 {970 {
959 result = BaseScriptEngine::evaluate(program);971 result = BaseScriptEngine::evaluate(program);
960 if (!isEvaluating() && hasUncaughtException()) {972 maybeEmitUncaughtException(__FUNCTION__);
961 emit unhandledException(cloneUncaughtException(__FUNCTION__));
962 clearExceptions();
963 }
964 }973 }
965 return result;974 return result;
966}975}
@@ -983,10 +992,7 @@ void ScriptEngine::run() { · 16 unmodified lines
983 992
984 {993 {
985 evaluate(_scriptContents, _fileNameString);994 evaluate(_scriptContents, _fileNameString);
986 if (!isEvaluating() && hasUncaughtException()) {995 maybeEmitUncaughtException(__FUNCTION__);
987 emit unhandledException(cloneUncaughtException(__FUNCTION__));
988 clearExceptions();
989 }
990 }996 }
991#ifdef _WIN32997#ifdef _WIN32
992 // VS13 does not sleep_until unless it uses the system_clock, see:998 // 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
1359 1365
1360// Script.require.resolve -- like resolvePath, but performs more validation and throws exceptions on invalid module identifiers (for consistency with Node.js)1366// Script.require.resolve -- like resolvePath, but performs more validation and throws exceptions on invalid module identifiers (for consistency with Node.js)
1361QString ScriptEngine::_requireResolve(const QString& moduleId, const QString& relativeTo) {1367QString ScriptEngine::_requireResolve(const QString& moduleId, const QString& relativeTo) {
1368 if (!IS_THREADSAFE_INVOCATION(thread(), __FUNCTION__)) {
1369 return QString();
1370 }
1362 QUrl defaultScriptsLoc = defaultScriptsLocation();1371 QUrl defaultScriptsLoc = defaultScriptsLocation();
1363 QUrl url(moduleId);1372 QUrl url(moduleId);
1364 ExceptionEmitter tryCatcher(this, __FUNCTION__);
1365 1373
1366 auto displayId = moduleId;1374 auto displayId = moduleId;
1367 if (displayId.length() > MAX_DEBUG_VALUE_LENGTH) {1375 if (displayId.length() > MAX_DEBUG_VALUE_LENGTH) {
1368 displayId = displayId.mid(0, MAX_DEBUG_VALUE_LENGTH) + "...";1376 displayId = displayId.mid(0, MAX_DEBUG_VALUE_LENGTH) + "...";
1369 }1377 }
1370 auto message = QString("Cannot find module '%1' (%2)").arg(displayId);1378 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 };
1372 1385
1373 // de-fuzz the input a little by restricting to rational sizes1386 // de-fuzz the input a little by restricting to rational sizes
1374 auto idLength = url.toString().length();1387 auto idLength = url.toString().length();
1375 if (idLength < 1 || idLength > MAX_MODULE_ID_LENTGH) {1388 if (idLength < 1 || idLength > MAX_MODULE_ID_LENTGH) {
1376 auto details = QString("rejecting invalid module id size (%1 chars [1,%2])")1389 auto details = QString("rejecting invalid module id size (%1 chars [1,%2])")
1377 .arg(idLength).arg(MAX_MODULE_ID_LENTGH);1390 .arg(idLength).arg(MAX_MODULE_ID_LENTGH);
1378 ctx->throwValue(makeError(message.arg(details), "RangeError"));1391 return throwResolveError(makeError(message.arg(details), "RangeError"));
1379 return nullptr;
1380 }1392 }
1381 1393
1382 // this regex matches: absolute, dotted or path-like URLs1394 // 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
1396 url = defaultScriptsLoc;1408 url = defaultScriptsLoc;
1397 url.setPath(systemModulePath);1409 url.setPath(systemModulePath);
1398 if (!QFileInfo(url.toLocalFile()).isFile()) {1410 if (!QFileInfo(url.toLocalFile()).isFile()) {
1399 ctx->throwValue(makeError(message.arg("system module not found")));1411 return throwResolveError(makeError(message.arg("system module not found")));
1400 return nullptr;
1401 }1412 }
1402 }1413 }
1403 1414
1404 if (url.isRelative()) {1415 if (url.isRelative()) {
1405 ctx->throwValue(makeError(message.arg("could not resolve module id")));1416 return throwResolveError(makeError(message.arg("could not resolve module id")));
1406 return nullptr;
1407 }1417 }
1408 1418
1409 // if it looks like a local file, verify that it's an allowed path and really a file1419 // 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
1416 1426
1417 bool disallowOutsideFiles = !defaultScriptsLocation().isParentOf(canonical) && !currentSandboxURL.isLocalFile();1427 bool disallowOutsideFiles = !defaultScriptsLocation().isParentOf(canonical) && !currentSandboxURL.isLocalFile();
1418 if (disallowOutsideFiles && !PathUtils::isDescendantOf(canonical, currentSandboxURL)) {1428 if (disallowOutsideFiles && !PathUtils::isDescendantOf(canonical, currentSandboxURL)) {
1419 ctx->throwValue(makeError(message.arg(1429 return throwResolveError(makeError(message.arg(
1420 QString("path '%1' outside of origin script '%2' '%3'")1430 QString("path '%1' outside of origin script '%2' '%3'")
1421 .arg(PathUtils::stripFilename(url))1431 .arg(PathUtils::stripFilename(url))
1422 .arg(PathUtils::stripFilename(currentSandboxURL))1432 .arg(PathUtils::stripFilename(currentSandboxURL))
1423 .arg(canonical.toString())1433 .arg(canonical.toString())
1424 )));1434 )));
1425 return nullptr;
1426 }1435 }
1427 if (!file.exists()) {1436 if (!file.exists()) {
1428 ctx->throwValue(makeError(message.arg("path does not exist: " + url.toLocalFile())));1437 return throwResolveError(makeError(message.arg("path does not exist: " + url.toLocalFile())));
1429 return nullptr;
1430 }1438 }
1431 if (!file.isFile()) {1439 if (!file.isFile()) {
1432 ctx->throwValue(makeError(message.arg("path is not a file: " + url.toLocalFile())));1440 return throwResolveError(makeError(message.arg("path is not a file: " + url.toLocalFile())));
1433 return nullptr;
1434 }1441 }
1435 }1442 }
1436 1443
1444 maybeEmitUncaughtException(__FUNCTION__);
1437 return url.toString();1445 return url.toString();
1438}1446}
1439 1447
1440// retrieves the current parent module from the JS scope chain1448// retrieves the current parent module from the JS scope chain
1441QScriptValue ScriptEngine::currentModule() {1449QScriptValue ScriptEngine::currentModule() {
1450 if (!IS_THREADSAFE_INVOCATION(thread(), __FUNCTION__)) {
1451 return unboundNullValue();
1452 }
1442 auto jsRequire = globalObject().property("Script").property("require");1453 auto jsRequire = globalObject().property("Script").property("require");
1443 auto cache = jsRequire.property("cache");1454 auto cache = jsRequire.property("cache");
1444 auto candidate = QScriptValue();1455 auto candidate = QScriptValue();
1445 for(auto ctx = currentContext(); ctx && !candidate.isObject(); ctx = ctx->parentContext()) {1456 for (auto c = currentContext(); c && !candidate.isObject(); c = c->parentContext()) {
1446 QScriptContextInfo contextInfo { ctx };1457 QScriptContextInfo contextInfo { c };
1447 candidate = cache.property(contextInfo.fileName());1458 candidate = cache.property(contextInfo.fileName());
1448 }1459 }
1449 if (!candidate.isObject()) {1460 if (!candidate.isObject()) {
@@ -1459,7 +1470,7 @@ bool ScriptEngine::registerModuleWithParent(const QScriptValue& module, const QS · 9 unmodified lines
1459 if (children.isArray()) {1470 if (children.isArray()) {
1460 auto key = module.property("id");1471 auto key = module.property("id");
1461 auto length = children.property("length").toInt32();1472 auto length = children.property("length").toInt32();
1462 for(int i=0; i < length; i++) {1473 for (int i=0; i < length; i++) {
1463 if (children.property(i).property("id").strictlyEquals(key)) {1474 if (children.property(i).property("id").strictlyEquals(key)) {
1464 qCDebug(scriptengine_module) << key.toString() << " updating parent.children[" << i << "] = module";1475 qCDebug(scriptengine_module) << key.toString() << " updating parent.children[" << i << "] = module";
1465 children.setProperty(i, module);1476 children.setProperty(i, module);
@@ -1488,7 +1499,7 @@ QScriptValue ScriptEngine::newModule(const QString& modulePath, const QScriptVal · 22 unmodified lines
1488 closure.setProperty("exports", exports);1499 closure.setProperty("exports", exports);
1489 1500
1490 // make the closure available to module instantiation1501 // make the closure available to module instantiation
1491 module.setProperty("__closure__", closure, HIDDEN_PROP_FLAGS);1502 module.setProperty("__closure__", closure, READONLY_HIDDEN_PROP_FLAGS);
1492 1503
1493 // for consistency with Node.js Module1504 // for consistency with Node.js Module
1494 module.setProperty("id", modulePath, READONLY_PROP_FLAGS);1505 module.setProperty("id", modulePath, READONLY_PROP_FLAGS);
@@ -1524,7 +1535,7 @@ QScriptValue ScriptEngine::fetchModuleSource(const QString& modulePath, const bo · 29 unmodified lines
1524 req.setProperty("url", url);1535 req.setProperty("url", url);
1525 req.setProperty("status", status);1536 req.setProperty("status", status);
1526 req.setProperty("success", ScriptCache::isSuccessStatus(status));1537 req.setProperty("success", ScriptCache::isSuccessStatus(status));
1527 req.setProperty("contents", contents, HIDDEN_PROP_FLAGS);1538 req.setProperty("contents", contents, READONLY_HIDDEN_PROP_FLAGS);
1528 }1539 }
1529 };1540 };
1530 1541
@@ -1568,73 +1579,54 @@ QScriptValue ScriptEngine::instantiateModule(const QScriptValue& module, const Q · 37 unmodified lines
1568 auto modulePath = module.property("filename").toString();1579 auto modulePath = module.property("filename").toString();
1569 auto closure = module.property("__closure__");1580 auto closure = module.property("__closure__");
1570 1581
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());
1572 1584
1573 {1585 if (module.property("content-type").toString() == "application/json") {
1574 ExceptionEmitter tryCatch(this, __FUNCTION__);1586 qCDebug(scriptengine_module) << "... parsing as JSON";
1575 1587 closure.setProperty("__json", sourceCode);
1576 if (module.property("content-type").toString() == "application/json") {1588 result = evaluateInClosure(closure, { "module.exports = JSON.parse(__json)", modulePath });
1577 qCDebug(scriptengine_module) << "... parsing as JSON";1589 } else {
1578#ifdef DEV_OTHER_MODULE_JSON_PARSE1590 // scoped vars for consistency with Node.js
1579 auto JSON = globalObject().property("JSON");1591 closure.setProperty("require", module.property("require"));
1580 auto parse = JSON.property("parse");1592 closure.setProperty("__filename", modulePath, READONLY_HIDDEN_PROP_FLAGS);
1581 if (!parse.isFunction()) {1593 closure.setProperty("__dirname", QString(modulePath).replace(QRegExp("/[^/]*$"), ""), READONLY_HIDDEN_PROP_FLAGS);
1582 currentContext()->throwValue(makeError("global JSON.parse is not a function", "EvalError"));1594 result = evaluateInClosure(closure, { sourceCode, modulePath });
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 }
1598 }1595 }
1596 maybeEmitUncaughtException(__FUNCTION__);
1599 return result;1597 return result;
1600}1598}
1601 1599
1602// CommonJS/Node.js like require/module support1600// CommonJS/Node.js like require/module support
1603QScriptValue ScriptEngine::require(const QString& moduleId) {1601QScriptValue ScriptEngine::require(const QString& moduleId) {
1604 qCDebug(scriptengine_module) << "ScriptEngine::require(" << moduleId.left(MAX_DEBUG_VALUE_LENGTH) << ")";1602 qCDebug(scriptengine_module) << "ScriptEngine::require(" << moduleId.left(MAX_DEBUG_VALUE_LENGTH) << ")";
1605 if (QThread::currentThread() != thread()) {1603 if (!IS_THREADSAFE_INVOCATION(thread(), __FUNCTION__)) {
1606 qCDebug(scriptengine_module) << moduleId << " threads mismatch";1604 return unboundNullValue();
1607 return nullValue();
1608 }1605 }
1609 1606
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
1617 auto jsRequire = globalObject().property("Script").property("require");1607 auto jsRequire = globalObject().property("Script").property("require");
1618 auto cacheMeta = jsRequire.data();1608 auto cacheMeta = jsRequire.data();
1619 auto cache = jsRequire.property("cache");1609 auto cache = jsRequire.property("cache");
1620 auto parent = currentModule();1610 auto parent = currentModule();
1621 1611
1622 auto throwModuleError = [this, &cache](const QString& modulePath, const QScriptValue& error) {1612 auto throwModuleError = [&](const QString& modulePath, const QScriptValue& error) {
1623 cache.setProperty(modulePath, nullValue());1613 cache.setProperty(modulePath, nullValue());
1624 if (!error.isNull()) {1614 if (!error.isNull()) {
1625#ifdef DEBUG_JS_MODULES1615#ifdef DEBUG_JS_MODULES
1626 qCWarning(scriptengine_module) << "throwing module error:" << error.toString() << modulePath << error.property("stack").toString();1616 qCWarning(scriptengine_module) << "throwing module error:" << error.toString() << modulePath << error.property("stack").toString();
1627#endif1617#endif
1628 currentContext()->throwValue(makeError(error));1618 raiseException(error);
1629 }1619 }
1630 return nullValue();1620 maybeEmitUncaughtException("module");
1621 return unboundNullValue();
1631 };1622 };
1632 1623
1633 // start by resolving the moduleId into a fully-qualified path/URL1624 // start by resolving the moduleId into a fully-qualified path/URL
1634 QString modulePath = _requireResolve(moduleId);1625 QString modulePath = _requireResolve(moduleId);
1635 if (modulePath.isNull() || tryCatcher.hasPending()) {1626 if (modulePath.isNull() || hasUncaughtException()) {
1636 // the resolver already threw an exception -- bail early1627 // the resolver already threw an exception -- bail early
1637 return nullValue();1628 maybeEmitUncaughtException(__FUNCTION__);
1629 return unboundNullValue();
1638 }1630 }
1639 1631
1640 // check the resolved path against the cache1632 // check the resolved path against the cache
@@ -1656,6 +1648,7 @@ QScriptValue ScriptEngine::require(const QString& moduleId) { · 15 unmodified lines
1656 qCDebug(scriptengine_module) << QString("require - using cached module '%1' for '%2' (loaded: %3)")1648 qCDebug(scriptengine_module) << QString("require - using cached module '%1' for '%2' (loaded: %3)")
1657 .arg(modulePath).arg(moduleId).arg(module.property("loaded").toString());1649 .arg(modulePath).arg(moduleId).arg(module.property("loaded").toString());
1658 registerModuleWithParent(module, parent);1650 registerModuleWithParent(module, parent);
1651 maybeEmitUncaughtException(__FUNCTION__);
1659 return exports;1652 return exports;
1660 }1653 }
1661 1654
@@ -1705,7 +1698,7 @@ QScriptValue ScriptEngine::require(const QString& moduleId) { · 43 unmodified lines
1705#ifdef DEBUG_JS1698#ifdef DEBUG_JS
1706 _debugDump("this", thisObject);1699 _debugDump("this", thisObject);
1707#endif1700#endif
1708 _applyUserOptions(module, thisObject);1701 applyUserOptions(module, thisObject);
1709 }1702 }
1710 }1703 }
1711 1704
@@ -1725,11 +1718,15 @@ QScriptValue ScriptEngine::require(const QString& moduleId) { · 13 unmodified lines
1725 1718
1726 qCDebug(scriptengine_module) << "//ScriptEngine::require(" << moduleId << ")";1719 qCDebug(scriptengine_module) << "//ScriptEngine::require(" << moduleId << ")";
1727 1720
1721 maybeEmitUncaughtException(__FUNCTION__);
1728 return module.property("exports");1722 return module.property("exports");
1729}1723}
1730 1724
1731// User-configurable override options1725// User-configurable override options
1732void ScriptEngine::_applyUserOptions(QScriptValue& module, QScriptValue& options) {1726void ScriptEngine::applyUserOptions(QScriptValue& module, QScriptValue& options) {
1727 if (!IS_THREADSAFE_INVOCATION(thread(), __FUNCTION__)) {
1728 return;
1729 }
1733 if (!options.isValid()) {1730 if (!options.isValid()) {
1734 return;1731 return;
1735 }1732 }
@@ -1770,6 +1767,7 @@ void ScriptEngine::_applyUserOptions(QScriptValue& module, QScriptValue& options · 34 unmodified lines
1770 qCDebug(scriptengine_module) << "module.closure['global'] = options.global";1767 qCDebug(scriptengine_module) << "module.closure['global'] = options.global";
1771 }1768 }
1772 }1769 }
1770 maybeEmitUncaughtException(__FUNCTION__);
1773}1771}
1774 1772
1775// If a callback is specified, the included files will be loaded asynchronously and the callback will be called1773// 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
1777// If no callback is specified, the included files will be loaded synchronously and will block execution until1775// If no callback is specified, the included files will be loaded synchronously and will block execution until
1778// all of the files have finished loading.1776// all of the files have finished loading.
1779void ScriptEngine::include(const QStringList& includeFiles, QScriptValue callback) {1777void ScriptEngine::include(const QStringList& includeFiles, QScriptValue callback) {
1778 if (!IS_THREADSAFE_INVOCATION(thread(), __FUNCTION__)) {
1779 return;
1780 }
1780 if (DependencyManager::get<ScriptEngines>()->isStopped()) {1781 if (DependencyManager::get<ScriptEngines>()->isStopped()) {
1781 scriptWarningMessage("Script.include() while shutting down is ignored... includeFiles:" 1782 scriptWarningMessage("Script.include() while shutting down is ignored... includeFiles:"
1782 + includeFiles.join(",") + "parent script:" + getFilename());1783 + includeFiles.join(",") + "parent script:" + getFilename());
@@ -1886,6 +1887,9 @@ void ScriptEngine::include(const QString& includeFile, QScriptValue callback) { · 103 unmodified lines
1886// as a stand-alone script. To accomplish this, the ScriptEngine class just emits a signal which1887// as a stand-alone script. To accomplish this, the ScriptEngine class just emits a signal which
1887// the Application or other context will connect to in order to know to actually load the script1888// the Application or other context will connect to in order to know to actually load the script
1888void ScriptEngine::load(const QString& loadFile) {1889void ScriptEngine::load(const QString& loadFile) {
1890 if (!IS_THREADSAFE_INVOCATION(thread(), __FUNCTION__)) {
1891 return;
1892 }
1889 if (DependencyManager::get<ScriptEngines>()->isStopped()) {1893 if (DependencyManager::get<ScriptEngines>()->isStopped()) {
1890 scriptWarningMessage("Script.load() while shutting down is ignored... loadFile:" 1894 scriptWarningMessage("Script.load() while shutting down is ignored... loadFile:"
1891 + loadFile + "parent script:" + getFilename());1895 + loadFile + "parent script:" + getFilename());
@@ -1955,6 +1959,52 @@ void ScriptEngine::updateEntityScriptStatus(const EntityItemID& entityID, const · 63 unmodified lines
1955 emit entityScriptDetailsUpdated();1959 emit entityScriptDetailsUpdated();
1956}1960}
1957 1961
1962QVariant 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
2004QFuture<QVariant> ScriptEngine::getLocalEntityScriptDetails(const EntityItemID& entityID) {
2005 return QtConcurrent::run(this, &ScriptEngine::cloneEntityScriptDetails, entityID);
2006}
2007
1958bool ScriptEngine::getEntityScriptDetails(const EntityItemID& entityID, EntityScriptDetails &details) const {2008bool ScriptEngine::getEntityScriptDetails(const EntityItemID& entityID, EntityScriptDetails &details) const {
1959 auto it = _entityScripts.constFind(entityID);2009 auto it = _entityScripts.constFind(entityID);
1960 if (it == _entityScripts.constEnd()) {2010 if (it == _entityScripts.constEnd()) {
@@ -2093,10 +2143,10 @@ void ScriptEngine::loadEntityScript(const EntityItemID& entityID, const QString& · 132 unmodified lines
2093 2143
2094 auto scriptCache = DependencyManager::get<ScriptCache>();2144 auto scriptCache = DependencyManager::get<ScriptCache>();
2095 // note: see EntityTreeRenderer.cpp for shared pointer lifecycle management2145 // note: see EntityTreeRenderer.cpp for shared pointer lifecycle management
2096 QWeakPointer<ScriptEngine> weakRef(sharedFromThis());2146 QWeakPointer<BaseScriptEngine> weakRef(sharedFromThis());
2097 scriptCache->getScriptContents(entityScript,2147 scriptCache->getScriptContents(entityScript,
2098 [this, weakRef, entityScript, entityID](const QString& url, const QString& contents, bool isURL, bool success, const QString& status) {2148 [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);
2100 if (!strongRef) {2150 if (!strongRef) {
2101 qCWarning(scriptengine) << "loadEntityScript.contentAvailable -- ScriptEngine was deleted during getScriptContents!!";2151 qCWarning(scriptengine) << "loadEntityScript.contentAvailable -- ScriptEngine was deleted during getScriptContents!!";
2102 return;2152 return;
@@ -2215,13 +2265,12 @@ void ScriptEngine::entityScriptContentAvailable(const EntityItemID& entityID, co · 112 unmodified lines
2215 timeout.setSingleShot(true);2265 timeout.setSingleShot(true);
2216 timeout.start(SANDBOX_TIMEOUT);2266 timeout.start(SANDBOX_TIMEOUT);
2217 connect(&timeout, &QTimer::timeout, [&sandbox, SANDBOX_TIMEOUT, scriptOrURL]{2267 connect(&timeout, &QTimer::timeout, [&sandbox, SANDBOX_TIMEOUT, scriptOrURL]{
2218 auto context = sandbox.currentContext();
2219 if (context) {
2220 qCDebug(scriptengine) << "ScriptEngine::entityScriptContentAvailable timeout(" << scriptOrURL << ")";2268 qCDebug(scriptengine) << "ScriptEngine::entityScriptContentAvailable timeout(" << scriptOrURL << ")";
2221 2269
2222 // Guard against infinite loops and non-performant code2270 // Guard against infinite loops and non-performant code
2223 context->throwError(QString("Timed out (entity constructors are limited to %1ms)").arg(SANDBOX_TIMEOUT));2271 sandbox.raiseException(
2224 }2272 sandbox.makeError(QString("Timed out (entity constructors are limited to %1ms)").arg(SANDBOX_TIMEOUT))
2273 );
2225 });2274 });
2226 2275
2227 testConstructor = sandbox.evaluate(program);2276 testConstructor = sandbox.evaluate(program);
@@ -2237,7 +2286,7 @@ void ScriptEngine::entityScriptContentAvailable(const EntityItemID& entityID, co · 9 unmodified lines
2237 if (exception.isError()) {2286 if (exception.isError()) {
2238 // create a local copy using makeError to decouple from the sandbox engine2287 // create a local copy using makeError to decouple from the sandbox engine
2239 exception = makeError(exception);2288 exception = makeError(exception);
2240 setError(formatException(exception), EntityScriptStatus::ERROR_RUNNING_SCRIPT);2289 setError(formatException(exception, _enableExtendedJSExceptions.get()), EntityScriptStatus::ERROR_RUNNING_SCRIPT);
2241 emit unhandledException(exception);2290 emit unhandledException(exception);
2242 return;2291 return;
2243 }2292 }
@@ -2288,7 +2337,7 @@ void ScriptEngine::entityScriptContentAvailable(const EntityItemID& entityID, co · 44 unmodified lines
2288 2337
2289 if (entityScriptObject.isError()) {2338 if (entityScriptObject.isError()) {
2290 auto exception = entityScriptObject;2339 auto exception = entityScriptObject;
2291 setError(formatException(exception), EntityScriptStatus::ERROR_RUNNING_SCRIPT);2340 setError(formatException(exception, _enableExtendedJSExceptions.get()), EntityScriptStatus::ERROR_RUNNING_SCRIPT);
2292 emit unhandledException(exception);2341 emit unhandledException(exception);
2293 return;2342 return;
2294 }2343 }
@@ -2420,10 +2469,7 @@ void ScriptEngine::doWithEnvironment(const EntityItemID& entityID, const QUrl& s · 125 unmodified lines
2420#else2469#else
2421 operation();2470 operation();
2422#endif2471#endif
2423 if (!isEvaluating() && hasUncaughtException()) {2472 maybeEmitUncaughtException(!entityID.isNull() ? entityID.toString() : __FUNCTION__);
2424 emit unhandledException(cloneUncaughtException(!entityID.isNull() ? entityID.toString() : __FUNCTION__));
2425 clearExceptions();
2426 }
2427 currentEntityIdentifier = oldIdentifier;2473 currentEntityIdentifier = oldIdentifier;
2428 currentSandboxURL = oldSandboxURL;2474 currentSandboxURL = oldSandboxURL;
2429}2475}