Showing the changes to libraries/script-engine/src/ScriptEngine.cpp (1 of 8 files). Show all
Unified · Split
| @@ -19,6 +19,9 @@ · 18 unmodified lines | ||
| 19 | 19 | #include <QtCore/QThread> |
| 20 | 20 | #include <QtCore/QRegularExpression> |
| 21 | 21 | |
| 22 | +#include <QtCore/QFuture> | |
| 23 | +#include <QtConcurrent/QtConcurrentRun> | |
| 24 | + | |
| 22 | 25 | #include <QtWidgets/QMainWindow> |
| 23 | 26 | #include <QtWidgets/QApplication> |
| 24 | 27 | |
| @@ -74,6 +77,10 @@ const QString ScriptEngine::_SETTINGS_ENABLE_EXTENDED_MODULE_COMPAT { · 49 unmodified lines | ||
| 74 | 77 | "com.highfidelity.experimental.enableExtendedModuleCompatbility" |
| 75 | 78 | }; |
| 76 | 79 | |
| 80 | +const QString ScriptEngine::_SETTINGS_ENABLE_EXTENDED_EXCEPTIONS { | |
| 81 | + "com.highfidelity.experimental.enableExtendedJSExceptions" | |
| 82 | +}; | |
| 83 | + | |
| 77 | 84 | static const int MAX_MODULE_ID_LENTGH { 4096 }; |
| 78 | 85 | static 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 | |
| 148 | 155 | QString ScriptEngine::logException(const QScriptValue& exception) { |
| 149 | − auto message = formatException(exception); | |
| 156 | + auto message = formatException(exception, _enableExtendedJSExceptions.get()); | |
| 150 | 157 | scriptErrorMessage(qPrintable(message)); |
| 151 | 158 | return message; |
| 152 | 159 | } |
| @@ -338,7 +345,11 @@ void ScriptEngine::runInThread() { · 185 unmodified lines | ||
| 338 | 345 | // The thread interface cannot live on itself, and we want to move this into the thread, so |
| 339 | 346 | // the thread cannot have this as a parent. |
| 340 | 347 | QThread* workerThread = new QThread(); |
| 348 | +#ifdef Q_OS_LINUX | |
| 349 | + workerThread->setObjectName(QString("js:") + getFilename()); | |
| 350 | +#else | |
| 341 | 351 | workerThread->setObjectName(QString("Script Thread:") + getFilename()); |
| 352 | +#endif | |
| 342 | 353 | moveToThread(workerThread); |
| 343 | 354 | |
| 344 | 355 | // 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 | |
| 540 | 551 | void ScriptEngine::resetModuleCache(bool deleteScriptCache) { |
| 541 | 552 | if (QThread::currentThread() != thread()) { |
| 542 | − QMetaObject::invokeMethod(this, "resetModuleCache"); | |
| 553 | + executeOnScriptThread([=](){ resetModuleCache(deleteScriptCache); }); | |
| 543 | 554 | return; |
| 544 | 555 | } |
| 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; | |
| 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); | |
| 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); | |
| 572 | 580 | #if DEBUG_JS_MODULES |
| 573 | − cache.setProperty("__meta__", cacheMeta, HIDDEN_PROP_FLAGS); | |
| 581 | + cache.setProperty("__meta__", cacheMeta, READONLY_HIDDEN_PROP_FLAGS); | |
| 574 | 582 | #endif |
| 575 | − jsRequire.setProperty("cache", cache, QScriptValue::ReadOnly | QScriptValue::Undeletable); | |
| 576 | − } | |
| 583 | + jsRequire.setProperty("cache", cache, QScriptValue::ReadOnly | QScriptValue::Undeletable); | |
| 577 | 584 | } |
| 578 | 585 | |
| 579 | 586 | void ScriptEngine::init() { |
| @@ -916,6 +923,11 @@ void ScriptEngine::addEventHandler(const EntityItemID& entityID, const QString& · 336 unmodified lines | ||
| 916 | 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 | |
| 927 | +QScriptValue ScriptEngine::evaluateInClosure(const QScriptValue& closure, const QScriptProgram& program) { | |
| 928 | + return BaseScriptEngine::evaluateInClosure(closure, program); | |
| 929 | +} | |
| 930 | + | |
| 919 | 931 | QScriptValue ScriptEngine::evaluate(const QString& sourceCode, const QString& fileName, int lineNumber) { |
| 920 | 932 | if (DependencyManager::get<ScriptEngines>()->isStopped()) { |
| 921 | 933 | return QScriptValue(); // bail early |
| @@ -938,29 +950,26 @@ QScriptValue ScriptEngine::evaluate(const QString& sourceCode, const QString& fi · 16 unmodified lines | ||
| 938 | 950 | // Check syntax |
| 939 | 951 | auto syntaxError = lintScript(sourceCode, fileName); |
| 940 | 952 | if (syntaxError.isError()) { |
| 941 | − if (isEvaluating()) { | |
| 942 | − currentContext()->throwValue(syntaxError); | |
| 943 | − } else { | |
| 953 | + if (!isEvaluating()) { | |
| 944 | 954 | syntaxError.setProperty("detail", "evaluate"); |
| 945 | − emit unhandledException(syntaxError); | |
| 946 | 955 | } |
| 956 | + raiseException(syntaxError); | |
| 957 | + maybeEmitUncaughtException(__FUNCTION__); | |
| 947 | 958 | return syntaxError; |
| 948 | 959 | } |
| 949 | 960 | QScriptProgram program { sourceCode, fileName, lineNumber }; |
| 950 | 961 | if (program.isNull()) { |
| 951 | 962 | // can this happen? |
| 952 | 963 | auto err = makeError("could not create QScriptProgram for " + fileName); |
| 953 | − emit unhandledException(err); | |
| 964 | + raiseException(err); | |
| 965 | + maybeEmitUncaughtException(__FUNCTION__); | |
| 954 | 966 | return err; |
| 955 | 967 | } |
| 956 | 968 | |
| 957 | 969 | QScriptValue result; |
| 958 | 970 | { |
| 959 | 971 | result = BaseScriptEngine::evaluate(program); |
| 960 | − if (!isEvaluating() && hasUncaughtException()) { | |
| 961 | − emit unhandledException(cloneUncaughtException(__FUNCTION__)); | |
| 962 | − clearExceptions(); | |
| 963 | − } | |
| 972 | + maybeEmitUncaughtException(__FUNCTION__); | |
| 964 | 973 | } |
| 965 | 974 | return result; |
| 966 | 975 | } |
| @@ -983,10 +992,7 @@ void ScriptEngine::run() { · 16 unmodified lines | ||
| 983 | 992 | |
| 984 | 993 | { |
| 985 | 994 | evaluate(_scriptContents, _fileNameString); |
| 986 | − if (!isEvaluating() && hasUncaughtException()) { | |
| 987 | − emit unhandledException(cloneUncaughtException(__FUNCTION__)); | |
| 988 | − clearExceptions(); | |
| 989 | − } | |
| 995 | + maybeEmitUncaughtException(__FUNCTION__); | |
| 990 | 996 | } |
| 991 | 997 | #ifdef _WIN32 |
| 992 | 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 | 1366 | // Script.require.resolve -- like resolvePath, but performs more validation and throws exceptions on invalid module identifiers (for consistency with Node.js) |
| 1361 | 1367 | QString ScriptEngine::_requireResolve(const QString& moduleId, const QString& relativeTo) { |
| 1368 | + if (!IS_THREADSAFE_INVOCATION(thread(), __FUNCTION__)) { | |
| 1369 | + return QString(); | |
| 1370 | + } | |
| 1362 | 1371 | QUrl defaultScriptsLoc = defaultScriptsLocation(); |
| 1363 | 1372 | QUrl url(moduleId); |
| 1364 | − ExceptionEmitter tryCatcher(this, __FUNCTION__); | |
| 1365 | 1373 | |
| 1366 | 1374 | auto displayId = moduleId; |
| 1367 | 1375 | if (displayId.length() > MAX_DEBUG_VALUE_LENGTH) { |
| 1368 | 1376 | displayId = displayId.mid(0, MAX_DEBUG_VALUE_LENGTH) + "..."; |
| 1369 | 1377 | } |
| 1370 | 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 | 1386 | // de-fuzz the input a little by restricting to rational sizes |
| 1374 | 1387 | auto idLength = url.toString().length(); |
| 1375 | 1388 | if (idLength < 1 || idLength > MAX_MODULE_ID_LENTGH) { |
| 1376 | 1389 | auto details = QString("rejecting invalid module id size (%1 chars [1,%2])") |
| 1377 | 1390 | .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")); | |
| 1380 | 1392 | } |
| 1381 | 1393 | |
| 1382 | 1394 | // 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 | 1408 | url = defaultScriptsLoc; |
| 1397 | 1409 | url.setPath(systemModulePath); |
| 1398 | 1410 | 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"))); | |
| 1401 | 1412 | } |
| 1402 | 1413 | } |
| 1403 | 1414 | |
| 1404 | 1415 | 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"))); | |
| 1407 | 1417 | } |
| 1408 | 1418 | |
| 1409 | 1419 | // 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 | 1427 | bool disallowOutsideFiles = !defaultScriptsLocation().isParentOf(canonical) && !currentSandboxURL.isLocalFile(); |
| 1418 | 1428 | if (disallowOutsideFiles && !PathUtils::isDescendantOf(canonical, currentSandboxURL)) { |
| 1419 | − ctx->throwValue(makeError(message.arg( | |
| 1429 | + return throwResolveError(makeError(message.arg( | |
| 1420 | 1430 | QString("path '%1' outside of origin script '%2' '%3'") |
| 1421 | 1431 | .arg(PathUtils::stripFilename(url)) |
| 1422 | 1432 | .arg(PathUtils::stripFilename(currentSandboxURL)) |
| 1423 | 1433 | .arg(canonical.toString()) |
| 1424 | 1434 | ))); |
| 1425 | − return nullptr; | |
| 1426 | 1435 | } |
| 1427 | 1436 | 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()))); | |
| 1430 | 1438 | } |
| 1431 | 1439 | 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()))); | |
| 1434 | 1441 | } |
| 1435 | 1442 | } |
| 1436 | 1443 | |
| 1444 | + maybeEmitUncaughtException(__FUNCTION__); | |
| 1437 | 1445 | return url.toString(); |
| 1438 | 1446 | } |
| 1439 | 1447 | |
| 1440 | 1448 | // retrieves the current parent module from the JS scope chain |
| 1441 | 1449 | QScriptValue ScriptEngine::currentModule() { |
| 1450 | + if (!IS_THREADSAFE_INVOCATION(thread(), __FUNCTION__)) { | |
| 1451 | + return unboundNullValue(); | |
| 1452 | + } | |
| 1442 | 1453 | auto jsRequire = globalObject().property("Script").property("require"); |
| 1443 | 1454 | auto cache = jsRequire.property("cache"); |
| 1444 | 1455 | 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 }; | |
| 1447 | 1458 | candidate = cache.property(contextInfo.fileName()); |
| 1448 | 1459 | } |
| 1449 | 1460 | if (!candidate.isObject()) { |
| @@ -1459,7 +1470,7 @@ bool ScriptEngine::registerModuleWithParent(const QScriptValue& module, const QS · 9 unmodified lines | ||
| 1459 | 1470 | if (children.isArray()) { |
| 1460 | 1471 | auto key = module.property("id"); |
| 1461 | 1472 | auto length = children.property("length").toInt32(); |
| 1462 | − for(int i=0; i < length; i++) { | |
| 1473 | + for (int i=0; i < length; i++) { | |
| 1463 | 1474 | if (children.property(i).property("id").strictlyEquals(key)) { |
| 1464 | 1475 | qCDebug(scriptengine_module) << key.toString() << " updating parent.children[" << i << "] = module"; |
| 1465 | 1476 | children.setProperty(i, module); |
| @@ -1488,7 +1499,7 @@ QScriptValue ScriptEngine::newModule(const QString& modulePath, const QScriptVal · 22 unmodified lines | ||
| 1488 | 1499 | closure.setProperty("exports", exports); |
| 1489 | 1500 | |
| 1490 | 1501 | // 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 | 1504 | // for consistency with Node.js Module |
| 1494 | 1505 | module.setProperty("id", modulePath, READONLY_PROP_FLAGS); |
| @@ -1524,7 +1535,7 @@ QScriptValue ScriptEngine::fetchModuleSource(const QString& modulePath, const bo · 29 unmodified lines | ||
| 1524 | 1535 | req.setProperty("url", url); |
| 1525 | 1536 | req.setProperty("status", status); |
| 1526 | 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 | 1579 | auto modulePath = module.property("filename").toString(); |
| 1569 | 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 | − { | |
| 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 }); | |
| 1598 | 1595 | } |
| 1596 | + maybeEmitUncaughtException(__FUNCTION__); | |
| 1599 | 1597 | return result; |
| 1600 | 1598 | } |
| 1601 | 1599 | |
| 1602 | 1600 | // CommonJS/Node.js like require/module support |
| 1603 | 1601 | QScriptValue ScriptEngine::require(const QString& moduleId) { |
| 1604 | 1602 | 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(); | |
| 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 | 1607 | auto jsRequire = globalObject().property("Script").property("require"); |
| 1618 | 1608 | auto cacheMeta = jsRequire.data(); |
| 1619 | 1609 | auto cache = jsRequire.property("cache"); |
| 1620 | 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 | 1613 | cache.setProperty(modulePath, nullValue()); |
| 1624 | 1614 | if (!error.isNull()) { |
| 1625 | 1615 | #ifdef DEBUG_JS_MODULES |
| 1626 | 1616 | qCWarning(scriptengine_module) << "throwing module error:" << error.toString() << modulePath << error.property("stack").toString(); |
| 1627 | 1617 | #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 | 1624 | // start by resolving the moduleId into a fully-qualified path/URL |
| 1634 | 1625 | QString modulePath = _requireResolve(moduleId); |
| 1635 | − if (modulePath.isNull() || tryCatcher.hasPending()) { | |
| 1626 | + if (modulePath.isNull() || hasUncaughtException()) { | |
| 1636 | 1627 | // the resolver already threw an exception -- bail early |
| 1637 | − return nullValue(); | |
| 1628 | + maybeEmitUncaughtException(__FUNCTION__); | |
| 1629 | + return unboundNullValue(); | |
| 1638 | 1630 | } |
| 1639 | 1631 | |
| 1640 | 1632 | // check the resolved path against the cache |
| @@ -1656,6 +1648,7 @@ QScriptValue ScriptEngine::require(const QString& moduleId) { · 15 unmodified lines | ||
| 1656 | 1648 | qCDebug(scriptengine_module) << QString("require - using cached module '%1' for '%2' (loaded: %3)") |
| 1657 | 1649 | .arg(modulePath).arg(moduleId).arg(module.property("loaded").toString()); |
| 1658 | 1650 | registerModuleWithParent(module, parent); |
| 1651 | + maybeEmitUncaughtException(__FUNCTION__); | |
| 1659 | 1652 | return exports; |
| 1660 | 1653 | } |
| 1661 | 1654 | |
| @@ -1705,7 +1698,7 @@ QScriptValue ScriptEngine::require(const QString& moduleId) { · 43 unmodified lines | ||
| 1705 | 1698 | #ifdef DEBUG_JS |
| 1706 | 1699 | _debugDump("this", thisObject); |
| 1707 | 1700 | #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 | 1719 | qCDebug(scriptengine_module) << "//ScriptEngine::require(" << moduleId << ")"; |
| 1727 | 1720 | |
| 1721 | + maybeEmitUncaughtException(__FUNCTION__); | |
| 1728 | 1722 | return module.property("exports"); |
| 1729 | 1723 | } |
| 1730 | 1724 | |
| 1731 | 1725 | // 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 | + } | |
| 1733 | 1730 | if (!options.isValid()) { |
| 1734 | 1731 | return; |
| 1735 | 1732 | } |
| @@ -1770,6 +1767,7 @@ void ScriptEngine::_applyUserOptions(QScriptValue& module, QScriptValue& options · 34 unmodified lines | ||
| 1770 | 1767 | qCDebug(scriptengine_module) << "module.closure['global'] = options.global"; |
| 1771 | 1768 | } |
| 1772 | 1769 | } |
| 1770 | + maybeEmitUncaughtException(__FUNCTION__); | |
| 1773 | 1771 | } |
| 1774 | 1772 | |
| 1775 | 1773 | // 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 | 1775 | // If no callback is specified, the included files will be loaded synchronously and will block execution until |
| 1778 | 1776 | // all of the files have finished loading. |
| 1779 | 1777 | void ScriptEngine::include(const QStringList& includeFiles, QScriptValue callback) { |
| 1778 | + if (!IS_THREADSAFE_INVOCATION(thread(), __FUNCTION__)) { | |
| 1779 | + return; | |
| 1780 | + } | |
| 1780 | 1781 | if (DependencyManager::get<ScriptEngines>()->isStopped()) { |
| 1781 | 1782 | scriptWarningMessage("Script.include() while shutting down is ignored... includeFiles:" |
| 1782 | 1783 | + includeFiles.join(",") + "parent script:" + getFilename()); |
| @@ -1886,6 +1887,9 @@ void ScriptEngine::include(const QString& includeFile, QScriptValue callback) { · 103 unmodified lines | ||
| 1886 | 1887 | // as a stand-alone script. To accomplish this, the ScriptEngine class just emits a signal which |
| 1887 | 1888 | // the Application or other context will connect to in order to know to actually load the script |
| 1888 | 1889 | void ScriptEngine::load(const QString& loadFile) { |
| 1890 | + if (!IS_THREADSAFE_INVOCATION(thread(), __FUNCTION__)) { | |
| 1891 | + return; | |
| 1892 | + } | |
| 1889 | 1893 | if (DependencyManager::get<ScriptEngines>()->isStopped()) { |
| 1890 | 1894 | scriptWarningMessage("Script.load() while shutting down is ignored... loadFile:" |
| 1891 | 1895 | + loadFile + "parent script:" + getFilename()); |
| @@ -1955,6 +1959,52 @@ void ScriptEngine::updateEntityScriptStatus(const EntityItemID& entityID, const · 63 unmodified lines | ||
| 1955 | 1959 | emit entityScriptDetailsUpdated(); |
| 1956 | 1960 | } |
| 1957 | 1961 | |
| 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 | + | |
| 1958 | 2008 | bool ScriptEngine::getEntityScriptDetails(const EntityItemID& entityID, EntityScriptDetails &details) const { |
| 1959 | 2009 | auto it = _entityScripts.constFind(entityID); |
| 1960 | 2010 | if (it == _entityScripts.constEnd()) { |
| @@ -2093,10 +2143,10 @@ void ScriptEngine::loadEntityScript(const EntityItemID& entityID, const QString& · 132 unmodified lines | ||
| 2093 | 2143 | |
| 2094 | 2144 | auto scriptCache = DependencyManager::get<ScriptCache>(); |
| 2095 | 2145 | // note: see EntityTreeRenderer.cpp for shared pointer lifecycle management |
| 2096 | − QWeakPointer<ScriptEngine> weakRef(sharedFromThis()); | |
| 2146 | + QWeakPointer<BaseScriptEngine> weakRef(sharedFromThis()); | |
| 2097 | 2147 | scriptCache->getScriptContents(entityScript, |
| 2098 | 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 | 2150 | if (!strongRef) { |
| 2101 | 2151 | qCWarning(scriptengine) << "loadEntityScript.contentAvailable -- ScriptEngine was deleted during getScriptContents!!"; |
| 2102 | 2152 | return; |
| @@ -2215,13 +2265,12 @@ void ScriptEngine::entityScriptContentAvailable(const EntityItemID& entityID, co · 112 unmodified lines | ||
| 2215 | 2265 | timeout.setSingleShot(true); |
| 2216 | 2266 | timeout.start(SANDBOX_TIMEOUT); |
| 2217 | 2267 | connect(&timeout, &QTimer::timeout, [&sandbox, SANDBOX_TIMEOUT, scriptOrURL]{ |
| 2218 | − auto context = sandbox.currentContext(); | |
| 2219 | − if (context) { | |
| 2220 | 2268 | qCDebug(scriptengine) << "ScriptEngine::entityScriptContentAvailable timeout(" << scriptOrURL << ")"; |
| 2221 | 2269 | |
| 2222 | 2270 | // 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 | + ); | |
| 2225 | 2274 | }); |
| 2226 | 2275 | |
| 2227 | 2276 | testConstructor = sandbox.evaluate(program); |
| @@ -2237,7 +2286,7 @@ void ScriptEngine::entityScriptContentAvailable(const EntityItemID& entityID, co · 9 unmodified lines | ||
| 2237 | 2286 | if (exception.isError()) { |
| 2238 | 2287 | // create a local copy using makeError to decouple from the sandbox engine |
| 2239 | 2288 | exception = makeError(exception); |
| 2240 | − setError(formatException(exception), EntityScriptStatus::ERROR_RUNNING_SCRIPT); | |
| 2289 | + setError(formatException(exception, _enableExtendedJSExceptions.get()), EntityScriptStatus::ERROR_RUNNING_SCRIPT); | |
| 2241 | 2290 | emit unhandledException(exception); |
| 2242 | 2291 | return; |
| 2243 | 2292 | } |
| @@ -2288,7 +2337,7 @@ void ScriptEngine::entityScriptContentAvailable(const EntityItemID& entityID, co · 44 unmodified lines | ||
| 2288 | 2337 | |
| 2289 | 2338 | if (entityScriptObject.isError()) { |
| 2290 | 2339 | auto exception = entityScriptObject; |
| 2291 | − setError(formatException(exception), EntityScriptStatus::ERROR_RUNNING_SCRIPT); | |
| 2340 | + setError(formatException(exception, _enableExtendedJSExceptions.get()), EntityScriptStatus::ERROR_RUNNING_SCRIPT); | |
| 2292 | 2341 | emit unhandledException(exception); |
| 2293 | 2342 | return; |
| 2294 | 2343 | } |
| @@ -2420,10 +2469,7 @@ void ScriptEngine::doWithEnvironment(const EntityItemID& entityID, const QUrl& s · 125 unmodified lines | ||
| 2420 | 2469 | #else |
| 2421 | 2470 | operation(); |
| 2422 | 2471 | #endif |
| 2423 | − if (!isEvaluating() && hasUncaughtException()) { | |
| 2424 | − emit unhandledException(cloneUncaughtException(!entityID.isNull() ? entityID.toString() : __FUNCTION__)); | |
| 2425 | − clearExceptions(); | |
| 2426 | − } | |
| 2472 | + maybeEmitUncaughtException(!entityID.isNull() ? entityID.toString() : __FUNCTION__); | |
| 2427 | 2473 | currentEntityIdentifier = oldIdentifier; |
| 2428 | 2474 | currentSandboxURL = oldSandboxURL; |
| 2429 | 2475 | } |