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 | #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 | ||
| 80 | const QString ScriptEngine::_SETTINGS_ENABLE_EXTENDED_EXCEPTIONS { | ||
| 81 | "com.highfidelity.experimental.enableExtendedJSExceptions" | ||
| 82 | }; | ||
| 83 | |||
| 77 | static const int MAX_MODULE_ID_LENTGH { 4096 }; | 84 | static const int MAX_MODULE_ID_LENTGH { 4096 }; |
| 78 | static const int MAX_DEBUG_VALUE_LENGTH { 80 }; | 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 | QString ScriptEngine::logException(const QScriptValue& exception) { | 155 | QString 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, so | 345 | // 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 of | 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 | void ScriptEngine::resetModuleCache(bool deleteScriptCache) { | 551 | void 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_MODULES | 580 | #if DEBUG_JS_MODULES |
| 573 | cache.setProperty("__meta__", cacheMeta, HIDDEN_PROP_FLAGS); | 581 | cache.setProperty("__meta__", cacheMeta, READONLY_HIDDEN_PROP_FLAGS); |
| 574 | #endif | 582 | #endif |
| 575 | jsRequire.setProperty("cache", cache, QScriptValue::ReadOnly | QScriptValue::Undeletable); | 583 | jsRequire.setProperty("cache", cache, QScriptValue::ReadOnly | QScriptValue::Undeletable); |
| 576 | } | ||
| 577 | } | 584 | } |
| 578 | 585 | ||
| 579 | void ScriptEngine::init() { | 586 | void 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 | ||
| 927 | QScriptValue ScriptEngine::evaluateInClosure(const QScriptValue& closure, const QScriptProgram& program) { | ||
| 928 | return BaseScriptEngine::evaluateInClosure(closure, program); | ||
| 929 | } | ||
| 930 | |||
| 919 | QScriptValue ScriptEngine::evaluate(const QString& sourceCode, const QString& fileName, int lineNumber) { | 931 | QScriptValue 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 early | 933 | return QScriptValue(); // bail early |
| @@ -938,29 +950,26 @@ QScriptValue ScriptEngine::evaluate(const QString& sourceCode, const QString& fi · 16 unmodified lines | |||
| 938 | // Check syntax | 950 | // 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 _WIN32 | 997 | #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) |
| 1361 | QString ScriptEngine::_requireResolve(const QString& moduleId, const QString& relativeTo) { | 1367 | QString 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 sizes | 1386 | // 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 URLs | 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 | 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 file | 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 | 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 chain | 1448 | // retrieves the current parent module from the JS scope chain |
| 1441 | QScriptValue ScriptEngine::currentModule() { | 1449 | QScriptValue 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 instantiation | 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 | // for consistency with Node.js Module | 1504 | // 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_PARSE | 1590 | // 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 support | 1600 | // CommonJS/Node.js like require/module support |
| 1603 | QScriptValue ScriptEngine::require(const QString& moduleId) { | 1601 | QScriptValue 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_MODULES | 1615 | #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 | #endif | 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 | // start by resolving the moduleId into a fully-qualified path/URL | 1624 | // 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 early | 1627 | // 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 cache | 1632 | // 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_JS | 1698 | #ifdef DEBUG_JS |
| 1706 | _debugDump("this", thisObject); | 1699 | _debugDump("this", thisObject); |
| 1707 | #endif | 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 | 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 options | 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 | 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 called | 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 | // If no callback is specified, the included files will be loaded synchronously and will block execution until | 1775 | // 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. |
| 1779 | void ScriptEngine::include(const QStringList& includeFiles, QScriptValue callback) { | 1777 | void 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 which | 1887 | // 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 script | 1888 | // the Application or other context will connect to in order to know to actually load the script |
| 1888 | void ScriptEngine::load(const QString& loadFile) { | 1889 | void 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 | ||
| 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 | bool ScriptEngine::getEntityScriptDetails(const EntityItemID& entityID, EntityScriptDetails &details) const { | 2008 | bool 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 management | 2145 | // 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 code | 2270 | // 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 engine | 2287 | // 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 | #else | 2469 | #else |
| 2421 | operation(); | 2470 | operation(); |
| 2422 | #endif | 2471 | #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 | } |