CR 21114 -- Implement modules/require support into hifi
Showing the changes to libraries/script-engine/src/ScriptEngine.cpp (1 of 29 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 | ||
| @@ -72,13 +75,18 @@ · 47 unmodified lines | |||
| 72 | 75 | ||
| 73 | #include "MIDIEvent.h" | 76 | #include "MIDIEvent.h" |
| 74 | 77 | ||
| 78 | const QString ScriptEngine::_SETTINGS_ENABLE_EXTENDED_EXCEPTIONS { | ||
| 79 | "com.highfidelity.experimental.enableExtendedJSExceptions" | ||
| 80 | }; | ||
| 81 | |||
| 82 | static const int MAX_MODULE_ID_LENGTH { 4096 }; | ||
| 83 | static const int MAX_DEBUG_VALUE_LENGTH { 80 }; | ||
| 84 | |||
| 75 | static const QScriptEngine::QObjectWrapOptions DEFAULT_QOBJECT_WRAP_OPTIONS = | 85 | static const QScriptEngine::QObjectWrapOptions DEFAULT_QOBJECT_WRAP_OPTIONS = |
| 76 | QScriptEngine::ExcludeDeleteLater | QScriptEngine::ExcludeChildObjects; | 86 | QScriptEngine::ExcludeDeleteLater | QScriptEngine::ExcludeChildObjects; |
| 77 | static const QScriptValue::PropertyFlags READONLY_PROP_FLAGS { QScriptValue::ReadOnly | QScriptValue::Undeletable }; | 87 | static const QScriptValue::PropertyFlags READONLY_PROP_FLAGS { QScriptValue::ReadOnly | QScriptValue::Undeletable }; |
| 78 | static const QScriptValue::PropertyFlags READONLY_HIDDEN_PROP_FLAGS { READONLY_PROP_FLAGS | QScriptValue::SkipInEnumeration }; | 88 | static const QScriptValue::PropertyFlags READONLY_HIDDEN_PROP_FLAGS { READONLY_PROP_FLAGS | QScriptValue::SkipInEnumeration }; |
| 79 | 89 | ||
| 80 | |||
| 81 | |||
| 82 | static const bool HIFI_AUTOREFRESH_FILE_SCRIPTS { true }; | 90 | static const bool HIFI_AUTOREFRESH_FILE_SCRIPTS { true }; |
| 83 | 91 | ||
| 84 | Q_DECLARE_METATYPE(QScriptEngine::FunctionSignature) | 92 | Q_DECLARE_METATYPE(QScriptEngine::FunctionSignature) |
| @@ -86,7 +94,7 @@ int functionSignatureMetaID = qRegisterMetaType<QScriptEngine::FunctionSignature · 1 unmodified line | |||
| 86 | 94 | ||
| 87 | Q_LOGGING_CATEGORY(scriptengineScript, "hifi.scriptengine.script") | 95 | Q_LOGGING_CATEGORY(scriptengineScript, "hifi.scriptengine.script") |
| 88 | 96 | ||
| 89 | static QScriptValue debugPrint(QScriptContext* context, QScriptEngine* engine){ | 97 | static QScriptValue debugPrint(QScriptContext* context, QScriptEngine* engine) { |
| 90 | QString message = ""; | 98 | QString message = ""; |
| 91 | for (int i = 0; i < context->argumentCount(); i++) { | 99 | for (int i = 0; i < context->argumentCount(); i++) { |
| 92 | if (i > 0) { | 100 | if (i > 0) { |
| @@ -143,7 +151,7 @@ QString encodeEntityIdIntoEntityUrl(const QString& url, const QString& entityID) · 50 unmodified lines | |||
| 143 | } | 151 | } |
| 144 | 152 | ||
| 145 | QString ScriptEngine::logException(const QScriptValue& exception) { | 153 | QString ScriptEngine::logException(const QScriptValue& exception) { |
| 146 | auto message = formatException(exception); | 154 | auto message = formatException(exception, _enableExtendedJSExceptions.get()); |
| 147 | scriptErrorMessage(message); | 155 | scriptErrorMessage(message); |
| 148 | return message; | 156 | return message; |
| 149 | } | 157 | } |
| @@ -335,7 +343,7 @@ void ScriptEngine::runInThread() { · 185 unmodified lines | |||
| 335 | // The thread interface cannot live on itself, and we want to move this into the thread, so | 343 | // The thread interface cannot live on itself, and we want to move this into the thread, so |
| 336 | // the thread cannot have this as a parent. | 344 | // the thread cannot have this as a parent. |
| 337 | QThread* workerThread = new QThread(); | 345 | QThread* workerThread = new QThread(); |
| 338 | workerThread->setObjectName(QString("Script Thread:") + getFilename()); | 346 | workerThread->setObjectName(QString("js:") + getFilename().replace("about:","")); |
| 339 | moveToThread(workerThread); | 347 | moveToThread(workerThread); |
| 340 | 348 | ||
| 341 | // NOTE: If you connect any essential signals for proper shutdown or cleanup of | 349 | // NOTE: If you connect any essential signals for proper shutdown or cleanup of |
| @@ -534,6 +542,40 @@ static QScriptValue createScriptableResourcePrototype(QScriptEngine* engine) { · 192 unmodified lines | |||
| 534 | return prototype; | 542 | return prototype; |
| 535 | } | 543 | } |
| 536 | 544 | ||
| 545 | void ScriptEngine::resetModuleCache(bool deleteScriptCache) { | ||
| 546 | if (QThread::currentThread() != thread()) { | ||
| 547 | executeOnScriptThread([=]() { resetModuleCache(deleteScriptCache); }); | ||
| 548 | return; | ||
| 549 | } | ||
| 550 | auto jsRequire = globalObject().property("Script").property("require"); | ||
| 551 | auto cache = jsRequire.property("cache"); | ||
| 552 | auto cacheMeta = jsRequire.data(); | ||
| 553 | |||
| 554 | if (deleteScriptCache) { | ||
| 555 | QScriptValueIterator it(cache); | ||
| 556 | while (it.hasNext()) { | ||
| 557 | it.next(); | ||
| 558 | if (it.flags() & QScriptValue::SkipInEnumeration) { | ||
| 559 | continue; | ||
| 560 | } | ||
| 561 | qCDebug(scriptengine) << "resetModuleCache(true) -- staging " << it.name() << " for cache reset at next require"; | ||
| 562 | cacheMeta.setProperty(it.name(), true); | ||
| 563 | } | ||
| 564 | } | ||
| 565 | cache = newObject(); | ||
| 566 | if (!cacheMeta.isObject()) { | ||
| 567 | cacheMeta = newObject(); | ||
| 568 | cacheMeta.setProperty("id", "Script.require.cacheMeta"); | ||
| 569 | cacheMeta.setProperty("type", "cacheMeta"); | ||
| 570 | jsRequire.setData(cacheMeta); | ||
| 571 | } | ||
| 572 | cache.setProperty("__created__", (double)QDateTime::currentMSecsSinceEpoch(), QScriptValue::SkipInEnumeration); | ||
| 573 | #if DEBUG_JS_MODULES | ||
| 574 | cache.setProperty("__meta__", cacheMeta, READONLY_HIDDEN_PROP_FLAGS); | ||
| 575 | #endif | ||
| 576 | jsRequire.setProperty("cache", cache, READONLY_PROP_FLAGS); | ||
| 577 | } | ||
| 578 | |||
| 537 | void ScriptEngine::init() { | 579 | void ScriptEngine::init() { |
| 538 | if (_isInitialized) { | 580 | if (_isInitialized) { |
| 539 | return; // only initialize once | 581 | return; // only initialize once |
| @@ -587,6 +629,15 @@ void ScriptEngine::init() { · 47 unmodified lines | |||
| 587 | 629 | ||
| 588 | registerGlobalObject("Script", this); | 630 | registerGlobalObject("Script", this); |
| 589 | 631 | ||
| 632 | { | ||
| 633 | // set up Script.require.resolve and Script.require.cache | ||
| 634 | auto Script = globalObject().property("Script"); | ||
| 635 | auto require = Script.property("require"); | ||
| 636 | auto resolve = Script.property("_requireResolve"); | ||
| 637 | require.setProperty("resolve", resolve, READONLY_PROP_FLAGS); | ||
| 638 | resetModuleCache(); | ||
| 639 | } | ||
| 640 | |||
| 590 | registerGlobalObject("Audio", &AudioScriptingInterface::getInstance()); | 641 | registerGlobalObject("Audio", &AudioScriptingInterface::getInstance()); |
| 591 | registerGlobalObject("Entities", entityScriptingInterface.data()); | 642 | registerGlobalObject("Entities", entityScriptingInterface.data()); |
| 592 | registerGlobalObject("Quat", &_quatLibrary); | 643 | registerGlobalObject("Quat", &_quatLibrary); |
| @@ -859,6 +910,11 @@ void ScriptEngine::addEventHandler(const EntityItemID& entityID, const QString& · 266 unmodified lines | |||
| 859 | handlersForEvent << handlerData; // Note that the same handler can be added many times. See removeEntityEventHandler(). | 910 | handlersForEvent << handlerData; // Note that the same handler can be added many times. See removeEntityEventHandler(). |
| 860 | } | 911 | } |
| 861 | 912 | ||
| 913 | // this is not redundant -- the version in BaseScriptEngine is specifically not Q_INVOKABLE | ||
| 914 | QScriptValue ScriptEngine::evaluateInClosure(const QScriptValue& closure, const QScriptProgram& program) { | ||
| 915 | return BaseScriptEngine::evaluateInClosure(closure, program); | ||
| 916 | } | ||
| 917 | |||
| 862 | QScriptValue ScriptEngine::evaluate(const QString& sourceCode, const QString& fileName, int lineNumber) { | 918 | QScriptValue ScriptEngine::evaluate(const QString& sourceCode, const QString& fileName, int lineNumber) { |
| 863 | if (DependencyManager::get<ScriptEngines>()->isStopped()) { | 919 | if (DependencyManager::get<ScriptEngines>()->isStopped()) { |
| 864 | return QScriptValue(); // bail early | 920 | return QScriptValue(); // bail early |
| @@ -881,29 +937,26 @@ QScriptValue ScriptEngine::evaluate(const QString& sourceCode, const QString& fi · 16 unmodified lines | |||
| 881 | // Check syntax | 937 | // Check syntax |
| 882 | auto syntaxError = lintScript(sourceCode, fileName); | 938 | auto syntaxError = lintScript(sourceCode, fileName); |
| 883 | if (syntaxError.isError()) { | 939 | if (syntaxError.isError()) { |
| 884 | if (isEvaluating()) { | 940 | if (!isEvaluating()) { |
| 885 | currentContext()->throwValue(syntaxError); | ||
| 886 | } else { | ||
| 887 | syntaxError.setProperty("detail", "evaluate"); | 941 | syntaxError.setProperty("detail", "evaluate"); |
| 888 | emit unhandledException(syntaxError); | ||
| 889 | } | 942 | } |
| 943 | raiseException(syntaxError); | ||
| 944 | maybeEmitUncaughtException("lint"); | ||
| 890 | return syntaxError; | 945 | return syntaxError; |
| 891 | } | 946 | } |
| 892 | QScriptProgram program { sourceCode, fileName, lineNumber }; | 947 | QScriptProgram program { sourceCode, fileName, lineNumber }; |
| 893 | if (program.isNull()) { | 948 | if (program.isNull()) { |
| 894 | // can this happen? | 949 | // can this happen? |
| 895 | auto err = makeError("could not create QScriptProgram for " + fileName); | 950 | auto err = makeError("could not create QScriptProgram for " + fileName); |
| 896 | emit unhandledException(err); | 951 | raiseException(err); |
| 952 | maybeEmitUncaughtException("compile"); | ||
| 897 | return err; | 953 | return err; |
| 898 | } | 954 | } |
| 899 | 955 | ||
| 900 | QScriptValue result; | 956 | QScriptValue result; |
| 901 | { | 957 | { |
| 902 | result = BaseScriptEngine::evaluate(program); | 958 | result = BaseScriptEngine::evaluate(program); |
| 903 | if (!isEvaluating() && hasUncaughtException()) { | 959 | maybeEmitUncaughtException("evaluate"); |
| 904 | emit unhandledException(cloneUncaughtException(__FUNCTION__)); | ||
| 905 | clearExceptions(); | ||
| 906 | } | ||
| 907 | } | 960 | } |
| 908 | return result; | 961 | return result; |
| 909 | } | 962 | } |
| @@ -926,10 +979,7 @@ void ScriptEngine::run() { · 16 unmodified lines | |||
| 926 | 979 | ||
| 927 | { | 980 | { |
| 928 | evaluate(_scriptContents, _fileNameString); | 981 | evaluate(_scriptContents, _fileNameString); |
| 929 | if (!isEvaluating() && hasUncaughtException()) { | 982 | maybeEmitUncaughtException(__FUNCTION__); |
| 930 | emit unhandledException(cloneUncaughtException(__FUNCTION__)); | ||
| 931 | clearExceptions(); | ||
| 932 | } | ||
| 933 | } | 983 | } |
| 934 | #ifdef _WIN32 | 984 | #ifdef _WIN32 |
| 935 | // VS13 does not sleep_until unless it uses the system_clock, see: | 985 | // VS13 does not sleep_until unless it uses the system_clock, see: |
| @@ -1300,11 +1350,361 @@ void ScriptEngine::print(const QString& message) { · 364 unmodified lines | |||
| 1300 | emit printedMessage(message); | 1350 | emit printedMessage(message); |
| 1301 | } | 1351 | } |
| 1302 | 1352 | ||
| 1353 | // Script.require.resolve -- like resolvePath, but performs more validation and throws exceptions on invalid module identifiers (for consistency with Node.js) | ||
| 1354 | QString ScriptEngine::_requireResolve(const QString& moduleId, const QString& relativeTo) { | ||
| 1355 | if (!IS_THREADSAFE_INVOCATION(thread(), __FUNCTION__)) { | ||
| 1356 | return QString(); | ||
| 1357 | } | ||
| 1358 | QUrl defaultScriptsLoc = defaultScriptsLocation(); | ||
| 1359 | QUrl url(moduleId); | ||
| 1360 | |||
| 1361 | auto displayId = moduleId; | ||
| 1362 | if (displayId.length() > MAX_DEBUG_VALUE_LENGTH) { | ||
| 1363 | displayId = displayId.mid(0, MAX_DEBUG_VALUE_LENGTH) + "..."; | ||
| 1364 | } | ||
| 1365 | auto message = QString("Cannot find module '%1' (%2)").arg(displayId); | ||
| 1366 | |||
| 1367 | auto throwResolveError = [&](const QScriptValue& error) -> QString { | ||
| 1368 | raiseException(error); | ||
| 1369 | maybeEmitUncaughtException("require.resolve"); | ||
| 1370 | return QString(); | ||
| 1371 | }; | ||
| 1372 | |||
| 1373 | // de-fuzz the input a little by restricting to rational sizes | ||
| 1374 | auto idLength = url.toString().length(); | ||
| 1375 | if (idLength < 1 || idLength > MAX_MODULE_ID_LENGTH) { | ||
| 1376 | auto details = QString("rejecting invalid module id size (%1 chars [1,%2])") | ||
| 1377 | .arg(idLength).arg(MAX_MODULE_ID_LENGTH); | ||
| 1378 | return throwResolveError(makeError(message.arg(details), "RangeError")); | ||
| 1379 | } | ||
| 1380 | |||
| 1381 | // this regex matches: absolute, dotted or path-like URLs | ||
| 1382 | // (ie: the kind of stuff ScriptEngine::resolvePath already handles) | ||
| 1383 | QRegularExpression qualified ("^\\w+:|^/|^[.]{1,2}(/|$)"); | ||
| 1384 | |||
| 1385 | // this is for module.require (which is a bound version of require that's always relative to the module path) | ||
| 1386 | if (!relativeTo.isEmpty()) { | ||
| 1387 | url = QUrl(relativeTo).resolved(moduleId); | ||
| 1388 | url = resolvePath(url.toString()); | ||
| 1389 | } else if (qualified.match(moduleId).hasMatch()) { | ||
| 1390 | url = resolvePath(moduleId); | ||
| 1391 | } else { | ||
| 1392 | // check if the moduleId refers to a "system" module | ||
| 1393 | QString systemPath = defaultScriptsLoc.path(); | ||
| 1394 | QString systemModulePath = QString("%1/modules/%2.js").arg(systemPath).arg(moduleId); | ||
| 1395 | url = defaultScriptsLoc; | ||
| 1396 | url.setPath(systemModulePath); | ||
| 1397 | if (!QFileInfo(url.toLocalFile()).isFile()) { | ||
| 1398 | if (!moduleId.contains("./")) { | ||
| 1399 | // the user might be trying to refer to a relative file without anchoring it | ||
| 1400 | // let's do them a favor and test for that case -- offering specific advice if detected | ||
| 1401 | auto unanchoredUrl = resolvePath("./" + moduleId); | ||
| 1402 | if (QFileInfo(unanchoredUrl.toLocalFile()).isFile()) { | ||
| 1403 | auto msg = QString("relative module ids must be anchored; use './%1' instead") | ||
| 1404 | .arg(moduleId); | ||
| 1405 | return throwResolveError(makeError(message.arg(msg))); | ||
| 1406 | } | ||
| 1407 | } | ||
| 1408 | return throwResolveError(makeError(message.arg("system module not found"))); | ||
| 1409 | } | ||
| 1410 | } | ||
| 1411 | |||
| 1412 | if (url.isRelative()) { | ||
| 1413 | return throwResolveError(makeError(message.arg("could not resolve module id"))); | ||
| 1414 | } | ||
| 1415 | |||
| 1416 | // if it looks like a local file, verify that it's an allowed path and really a file | ||
| 1417 | if (url.isLocalFile()) { | ||
| 1418 | QFileInfo file(url.toLocalFile()); | ||
| 1419 | QUrl canonical = url; | ||
| 1420 | if (file.exists()) { | ||
| 1421 | canonical.setPath(file.canonicalFilePath()); | ||
| 1422 | } | ||
| 1423 | |||
| 1424 | bool disallowOutsideFiles = !defaultScriptsLocation().isParentOf(canonical) && !currentSandboxURL.isLocalFile(); | ||
| 1425 | if (disallowOutsideFiles && !PathUtils::isDescendantOf(canonical, currentSandboxURL)) { | ||
| 1426 | return throwResolveError(makeError(message.arg( | ||
| 1427 | QString("path '%1' outside of origin script '%2' '%3'") | ||
| 1428 | .arg(PathUtils::stripFilename(url)) | ||
| 1429 | .arg(PathUtils::stripFilename(currentSandboxURL)) | ||
| 1430 | .arg(canonical.toString()) | ||
| 1431 | ))); | ||
| 1432 | } | ||
| 1433 | if (!file.exists()) { | ||
| 1434 | return throwResolveError(makeError(message.arg("path does not exist: " + url.toLocalFile()))); | ||
| 1435 | } | ||
| 1436 | if (!file.isFile()) { | ||
| 1437 | return throwResolveError(makeError(message.arg("path is not a file: " + url.toLocalFile()))); | ||
| 1438 | } | ||
| 1439 | } | ||
| 1440 | |||
| 1441 | maybeEmitUncaughtException(__FUNCTION__); | ||
| 1442 | return url.toString(); | ||
| 1443 | } | ||
| 1444 | |||
| 1445 | // retrieves the current parent module from the JS scope chain | ||
| 1446 | QScriptValue ScriptEngine::currentModule() { | ||
| 1447 | if (!IS_THREADSAFE_INVOCATION(thread(), __FUNCTION__)) { | ||
| 1448 | return unboundNullValue(); | ||
| 1449 | } | ||
| 1450 | auto jsRequire = globalObject().property("Script").property("require"); | ||
| 1451 | auto cache = jsRequire.property("cache"); | ||
| 1452 | auto candidate = QScriptValue(); | ||
| 1453 | for (auto c = currentContext(); c && !candidate.isObject(); c = c->parentContext()) { | ||
| 1454 | QScriptContextInfo contextInfo { c }; | ||
| 1455 | candidate = cache.property(contextInfo.fileName()); | ||
| 1456 | } | ||
| 1457 | if (!candidate.isObject()) { | ||
| 1458 | return QScriptValue(); | ||
| 1459 | } | ||
| 1460 | return candidate; | ||
| 1461 | } | ||
| 1462 | |||
| 1463 | // replaces or adds "module" to "parent.children[]" array | ||
| 1464 | // (for consistency with Node.js and userscript cache invalidation without "cache busters") | ||
| 1465 | bool ScriptEngine::registerModuleWithParent(const QScriptValue& module, const QScriptValue& parent) { | ||
| 1466 | auto children = parent.property("children"); | ||
| 1467 | if (children.isArray()) { | ||
| 1468 | auto key = module.property("id"); | ||
| 1469 | auto length = children.property("length").toInt32(); | ||
| 1470 | for (int i = 0; i < length; i++) { | ||
| 1471 | if (children.property(i).property("id").strictlyEquals(key)) { | ||
| 1472 | qCDebug(scriptengine_module) << key.toString() << " updating parent.children[" << i << "] = module"; | ||
| 1473 | children.setProperty(i, module); | ||
| 1474 | return true; | ||
| 1475 | } | ||
| 1476 | } | ||
| 1477 | qCDebug(scriptengine_module) << key.toString() << " appending parent.children[" << length << "] = module"; | ||
| 1478 | children.setProperty(length, module); | ||
| 1479 | return true; | ||
| 1480 | } else if (parent.isValid()) { | ||
| 1481 | qCDebug(scriptengine_module) << "registerModuleWithParent -- unrecognized parent" << parent.toVariant().toString(); | ||
| 1482 | } | ||
| 1483 | return false; | ||
| 1484 | } | ||
| 1485 | |||
| 1486 | // creates a new JS "module" Object with default metadata properties | ||
| 1487 | QScriptValue ScriptEngine::newModule(const QString& modulePath, const QScriptValue& parent) { | ||
| 1488 | auto closure = newObject(); | ||
| 1489 | auto exports = newObject(); | ||
| 1490 | auto module = newObject(); | ||
| 1491 | qCDebug(scriptengine_module) << "newModule" << modulePath << parent.property("filename").toString(); | ||
| 1492 | |||
| 1493 | closure.setProperty("module", module, READONLY_PROP_FLAGS); | ||
| 1494 | |||
| 1495 | // note: this becomes the "exports" free variable, so should not be set read only | ||
| 1496 | closure.setProperty("exports", exports); | ||
| 1497 | |||
| 1498 | // make the closure available to module instantiation | ||
| 1499 | module.setProperty("__closure__", closure, READONLY_HIDDEN_PROP_FLAGS); | ||
| 1500 | |||
| 1501 | // for consistency with Node.js Module | ||
| 1502 | module.setProperty("id", modulePath, READONLY_PROP_FLAGS); | ||
| 1503 | module.setProperty("filename", modulePath, READONLY_PROP_FLAGS); | ||
| 1504 | module.setProperty("exports", exports); // not readonly | ||
| 1505 | module.setProperty("loaded", false, READONLY_PROP_FLAGS); | ||
| 1506 | module.setProperty("parent", parent, READONLY_PROP_FLAGS); | ||
| 1507 | module.setProperty("children", newArray(), READONLY_PROP_FLAGS); | ||
| 1508 | |||
| 1509 | // module.require is a bound version of require that always resolves relative to that module's path | ||
| 1510 | auto boundRequire = QScriptEngine::evaluate("(function(id) { return Script.require(Script.require.resolve(id, this.filename)); })", "(boundRequire)"); | ||
| 1511 | module.setProperty("require", boundRequire, READONLY_PROP_FLAGS); | ||
| 1512 | |||
| 1513 | return module; | ||
| 1514 | } | ||
| 1515 | |||
| 1516 | // synchronously fetch a module's source code using BatchLoader | ||
| 1517 | QVariantMap ScriptEngine::fetchModuleSource(const QString& modulePath, const bool forceDownload) { | ||
| 1518 | using UrlMap = QMap<QUrl, QString>; | ||
| 1519 | auto scriptCache = DependencyManager::get<ScriptCache>(); | ||
| 1520 | QVariantMap req; | ||
| 1521 | qCDebug(scriptengine_module) << "require.fetchModuleSource: " << QUrl(modulePath).fileName() << QThread::currentThread(); | ||
| 1522 | |||
| 1523 | auto onload = [=, &req](const UrlMap& data, const UrlMap& _status) { | ||
| 1524 | auto url = modulePath; | ||
| 1525 | auto status = _status[url]; | ||
| 1526 | auto contents = data[url]; | ||
| 1527 | qCDebug(scriptengine_module) << "require.fetchModuleSource.onload: " << QUrl(url).fileName() << status << QThread::currentThread(); | ||
| 1528 | if (isStopping()) { | ||
| 1529 | req["status"] = "Stopped"; | ||
| 1530 | req["success"] = false; | ||
| 1531 | } else { | ||
| 1532 | req["url"] = url; | ||
| 1533 | req["status"] = status; | ||
| 1534 | req["success"] = ScriptCache::isSuccessStatus(status); | ||
| 1535 | req["contents"] = contents; | ||
| 1536 | } | ||
| 1537 | }; | ||
| 1538 | |||
| 1539 | if (forceDownload) { | ||
| 1540 | qCDebug(scriptengine_module) << "require.requestScript -- clearing cache for" << modulePath; | ||
| 1541 | scriptCache->deleteScript(modulePath); | ||
| 1542 | } | ||
| 1543 | BatchLoader* loader = new BatchLoader(QList<QUrl>({ modulePath })); | ||
| 1544 | connect(loader, &BatchLoader::finished, this, onload); | ||
| 1545 | connect(this, &QObject::destroyed, loader, &QObject::deleteLater); | ||
| 1546 | // fail faster? (since require() blocks the engine thread while resolving dependencies) | ||
| 1547 | const int MAX_RETRIES = 1; | ||
| 1548 | |||
| 1549 | loader->start(MAX_RETRIES); | ||
| 1550 | |||
| 1551 | if (!loader->isFinished()) { | ||
| 1552 | QTimer monitor; | ||
| 1553 | QEventLoop loop; | ||
| 1554 | QObject::connect(loader, &BatchLoader::finished, this, [this, &monitor, &loop]{ | ||
| 1555 | monitor.stop(); | ||
| 1556 | loop.quit(); | ||
| 1557 | }); | ||
| 1558 | |||
| 1559 | // this helps detect the case where stop() is invoked during the download | ||
| 1560 | // but not seen in time to abort processing in onload()... | ||
| 1561 | connect(&monitor, &QTimer::timeout, this, [this, &loop, &loader]{ | ||
| 1562 | if (isStopping()) { | ||
| 1563 | loop.exit(-1); | ||
| 1564 | } | ||
| 1565 | }); | ||
| 1566 | monitor.start(500); | ||
| 1567 | loop.exec(); | ||
| 1568 | } | ||
| 1569 | loader->deleteLater(); | ||
| 1570 | return req; | ||
| 1571 | } | ||
| 1572 | |||
| 1573 | // evaluate a pending module object using the fetched source code | ||
| 1574 | QScriptValue ScriptEngine::instantiateModule(const QScriptValue& module, const QString& sourceCode) { | ||
| 1575 | QScriptValue result; | ||
| 1576 | auto modulePath = module.property("filename").toString(); | ||
| 1577 | auto closure = module.property("__closure__"); | ||
| 1578 | |||
| 1579 | qCDebug(scriptengine_module) << QString("require.instantiateModule: %1 / %2 bytes") | ||
| 1580 | .arg(QUrl(modulePath).fileName()).arg(sourceCode.length()); | ||
| 1581 | |||
| 1582 | if (module.property("content-type").toString() == "application/json") { | ||
| 1583 | qCDebug(scriptengine_module) << "... parsing as JSON"; | ||
| 1584 | closure.setProperty("__json", sourceCode); | ||
| 1585 | result = evaluateInClosure(closure, { "module.exports = JSON.parse(__json)", modulePath }); | ||
| 1586 | } else { | ||
| 1587 | // scoped vars for consistency with Node.js | ||
| 1588 | closure.setProperty("require", module.property("require")); | ||
| 1589 | closure.setProperty("__filename", modulePath, READONLY_HIDDEN_PROP_FLAGS); | ||
| 1590 | closure.setProperty("__dirname", QString(modulePath).replace(QRegExp("/[^/]*$"), ""), READONLY_HIDDEN_PROP_FLAGS); | ||
| 1591 | result = evaluateInClosure(closure, { sourceCode, modulePath }); | ||
| 1592 | } | ||
| 1593 | maybeEmitUncaughtException(__FUNCTION__); | ||
| 1594 | return result; | ||
| 1595 | } | ||
| 1596 | |||
| 1597 | // CommonJS/Node.js like require/module support | ||
| 1598 | QScriptValue ScriptEngine::require(const QString& moduleId) { | ||
| 1599 | qCDebug(scriptengine_module) << "ScriptEngine::require(" << moduleId.left(MAX_DEBUG_VALUE_LENGTH) << ")"; | ||
| 1600 | if (!IS_THREADSAFE_INVOCATION(thread(), __FUNCTION__)) { | ||
| 1601 | return unboundNullValue(); | ||
| 1602 | } | ||
| 1603 | |||
| 1604 | auto jsRequire = globalObject().property("Script").property("require"); | ||
| 1605 | auto cacheMeta = jsRequire.data(); | ||
| 1606 | auto cache = jsRequire.property("cache"); | ||
| 1607 | auto parent = currentModule(); | ||
| 1608 | |||
| 1609 | auto throwModuleError = [&](const QString& modulePath, const QScriptValue& error) { | ||
| 1610 | cache.setProperty(modulePath, nullValue()); | ||
| 1611 | if (!error.isNull()) { | ||
| 1612 | #ifdef DEBUG_JS_MODULES | ||
| 1613 | qCWarning(scriptengine_module) << "throwing module error:" << error.toString() << modulePath << error.property("stack").toString(); | ||
| 1614 | #endif | ||
| 1615 | raiseException(error); | ||
| 1616 | } | ||
| 1617 | maybeEmitUncaughtException("module"); | ||
| 1618 | return unboundNullValue(); | ||
| 1619 | }; | ||
| 1620 | |||
| 1621 | // start by resolving the moduleId into a fully-qualified path/URL | ||
| 1622 | QString modulePath = _requireResolve(moduleId); | ||
| 1623 | if (modulePath.isNull() || hasUncaughtException()) { | ||
| 1624 | // the resolver already threw an exception -- bail early | ||
| 1625 | maybeEmitUncaughtException(__FUNCTION__); | ||
| 1626 | return unboundNullValue(); | ||
| 1627 | } | ||
| 1628 | |||
| 1629 | // check the resolved path against the cache | ||
| 1630 | auto module = cache.property(modulePath); | ||
| 1631 | |||
| 1632 | // modules get cached in `Script.require.cache` and (similar to Node.js) users can access it | ||
| 1633 | // to inspect particular entries and invalidate them by deleting the key: | ||
| 1634 | // `delete Script.require.cache[Script.require.resolve(moduleId)];` | ||
| 1635 | |||
| 1636 | // cacheMeta is just used right now to tell deleted keys apart from undefined ones | ||
| 1637 | bool invalidateCache = module.isUndefined() && cacheMeta.property(moduleId).isValid(); | ||
| 1638 | |||
| 1639 | // reset the cacheMeta record so invalidation won't apply next time, even if the module fails to load | ||
| 1640 | cacheMeta.setProperty(modulePath, QScriptValue()); | ||
| 1641 | |||
| 1642 | auto exports = module.property("exports"); | ||
| 1643 | if (!invalidateCache && exports.isObject()) { | ||
| 1644 | // we have found a cached module -- just need to possibly register it with current parent | ||
| 1645 | qCDebug(scriptengine_module) << QString("require - using cached module '%1' for '%2' (loaded: %3)") | ||
| 1646 | .arg(modulePath).arg(moduleId).arg(module.property("loaded").toString()); | ||
| 1647 | registerModuleWithParent(module, parent); | ||
| 1648 | maybeEmitUncaughtException("cached module"); | ||
| 1649 | return exports; | ||
| 1650 | } | ||
| 1651 | |||
| 1652 | // bootstrap / register new empty module | ||
| 1653 | module = newModule(modulePath, parent); | ||
| 1654 | registerModuleWithParent(module, parent); | ||
| 1655 | |||
| 1656 | // add it to the cache (this is done early so any cyclic dependencies pick up) | ||
| 1657 | cache.setProperty(modulePath, module); | ||
| 1658 | |||
| 1659 | // download the module source | ||
| 1660 | auto req = fetchModuleSource(modulePath, invalidateCache); | ||
| 1661 | |||
| 1662 | if (!req.contains("success") || !req["success"].toBool()) { | ||
| 1663 | auto error = QString("error retrieving script (%1)").arg(req["status"].toString()); | ||
| 1664 | return throwModuleError(modulePath, error); | ||
| 1665 | } | ||
| 1666 | |||
| 1667 | #if DEBUG_JS_MODULES | ||
| 1668 | qCDebug(scriptengine_module) << "require.loaded: " << | ||
| 1669 | QUrl(req["url"].toString()).fileName() << req["status"].toString(); | ||
| 1670 | #endif | ||
| 1671 | |||
| 1672 | auto sourceCode = req["contents"].toString(); | ||
| 1673 | |||
| 1674 | if (QUrl(modulePath).fileName().endsWith(".json", Qt::CaseInsensitive)) { | ||
| 1675 | module.setProperty("content-type", "application/json"); | ||
| 1676 | } else { | ||
| 1677 | module.setProperty("content-type", "application/javascript"); | ||
| 1678 | } | ||
| 1679 | |||
| 1680 | // evaluate the module | ||
| 1681 | auto result = instantiateModule(module, sourceCode); | ||
| 1682 | |||
| 1683 | if (result.isError() && !result.strictlyEquals(module.property("exports"))) { | ||
| 1684 | qCWarning(scriptengine_module) << "-- result.isError --" << result.toString(); | ||
| 1685 | return throwModuleError(modulePath, result); | ||
| 1686 | } | ||
| 1687 | |||
| 1688 | // mark as fully-loaded | ||
| 1689 | module.setProperty("loaded", true, READONLY_PROP_FLAGS); | ||
| 1690 | |||
| 1691 | // set up a new reference point for detecting cache key deletion | ||
| 1692 | cacheMeta.setProperty(modulePath, module); | ||
| 1693 | |||
| 1694 | qCDebug(scriptengine_module) << "//ScriptEngine::require(" << moduleId << ")"; | ||
| 1695 | |||
| 1696 | maybeEmitUncaughtException(__FUNCTION__); | ||
| 1697 | return module.property("exports"); | ||
| 1698 | } | ||
| 1699 | |||
| 1303 | // If a callback is specified, the included files will be loaded asynchronously and the callback will be called | 1700 | // If a callback is specified, the included files will be loaded asynchronously and the callback will be called |
| 1304 | // when all of the files have finished loading. | 1701 | // when all of the files have finished loading. |
| 1305 | // If no callback is specified, the included files will be loaded synchronously and will block execution until | 1702 | // If no callback is specified, the included files will be loaded synchronously and will block execution until |
| 1306 | // all of the files have finished loading. | 1703 | // all of the files have finished loading. |
| 1307 | void ScriptEngine::include(const QStringList& includeFiles, QScriptValue callback) { | 1704 | void ScriptEngine::include(const QStringList& includeFiles, QScriptValue callback) { |
| 1705 | if (!IS_THREADSAFE_INVOCATION(thread(), __FUNCTION__)) { | ||
| 1706 | return; | ||
| 1707 | } | ||
| 1308 | if (DependencyManager::get<ScriptEngines>()->isStopped()) { | 1708 | if (DependencyManager::get<ScriptEngines>()->isStopped()) { |
| 1309 | scriptWarningMessage("Script.include() while shutting down is ignored... includeFiles:" | 1709 | scriptWarningMessage("Script.include() while shutting down is ignored... includeFiles:" |
| 1310 | + includeFiles.join(",") + "parent script:" + getFilename()); | 1710 | + includeFiles.join(",") + "parent script:" + getFilename()); |
| @@ -1367,7 +1767,7 @@ void ScriptEngine::include(const QStringList& includeFiles, QScriptValue callbac · 56 unmodified lines | |||
| 1367 | 1767 | ||
| 1368 | doWithEnvironment(capturedEntityIdentifier, capturedSandboxURL, operation); | 1768 | doWithEnvironment(capturedEntityIdentifier, capturedSandboxURL, operation); |
| 1369 | if (hasUncaughtException()) { | 1769 | if (hasUncaughtException()) { |
| 1370 | emit unhandledException(cloneUncaughtException(__FUNCTION__)); | 1770 | emit unhandledException(cloneUncaughtException("evaluateInclude")); |
| 1371 | clearExceptions(); | 1771 | clearExceptions(); |
| 1372 | } | 1772 | } |
| 1373 | } else { | 1773 | } else { |
| @@ -1414,6 +1814,9 @@ void ScriptEngine::include(const QString& includeFile, QScriptValue callback) { · 40 unmodified lines | |||
| 1414 | // as a stand-alone script. To accomplish this, the ScriptEngine class just emits a signal which | 1814 | // as a stand-alone script. To accomplish this, the ScriptEngine class just emits a signal which |
| 1415 | // the Application or other context will connect to in order to know to actually load the script | 1815 | // the Application or other context will connect to in order to know to actually load the script |
| 1416 | void ScriptEngine::load(const QString& loadFile) { | 1816 | void ScriptEngine::load(const QString& loadFile) { |
| 1817 | if (!IS_THREADSAFE_INVOCATION(thread(), __FUNCTION__)) { | ||
| 1818 | return; | ||
| 1819 | } | ||
| 1417 | if (DependencyManager::get<ScriptEngines>()->isStopped()) { | 1820 | if (DependencyManager::get<ScriptEngines>()->isStopped()) { |
| 1418 | scriptWarningMessage("Script.load() while shutting down is ignored... loadFile:" | 1821 | scriptWarningMessage("Script.load() while shutting down is ignored... loadFile:" |
| 1419 | + loadFile + "parent script:" + getFilename()); | 1822 | + loadFile + "parent script:" + getFilename()); |
| @@ -1483,6 +1886,52 @@ void ScriptEngine::updateEntityScriptStatus(const EntityItemID& entityID, const · 63 unmodified lines | |||
| 1483 | emit entityScriptDetailsUpdated(); | 1886 | emit entityScriptDetailsUpdated(); |
| 1484 | } | 1887 | } |
| 1485 | 1888 | ||
| 1889 | QVariant ScriptEngine::cloneEntityScriptDetails(const EntityItemID& entityID) { | ||
| 1890 | static const QVariant NULL_VARIANT { qVariantFromValue((QObject*)nullptr) }; | ||
| 1891 | QVariantMap map; | ||
| 1892 | if (entityID.isNull()) { | ||
| 1893 | // TODO: find better way to report JS Error across thread/process boundaries | ||
| 1894 | map["isError"] = true; | ||
| 1895 | map["errorInfo"] = "Error: getEntityScriptDetails -- invalid entityID"; | ||
| 1896 | } else { | ||
| 1897 | #ifdef DEBUG_ENTITY_STATES | ||
| 1898 | qDebug() << "cloneEntityScriptDetails" << entityID << QThread::currentThread(); | ||
| 1899 | #endif | ||
| 1900 | EntityScriptDetails scriptDetails; | ||
| 1901 | if (getEntityScriptDetails(entityID, scriptDetails)) { | ||
| 1902 | #ifdef DEBUG_ENTITY_STATES | ||
| 1903 | qDebug() << "gotEntityScriptDetails" << scriptDetails.status << QThread::currentThread(); | ||
| 1904 | #endif | ||
| 1905 | map["isRunning"] = isEntityScriptRunning(entityID); | ||
| 1906 | map["status"] = EntityScriptStatus_::valueToKey(scriptDetails.status).toLower(); | ||
| 1907 | map["errorInfo"] = scriptDetails.errorInfo; | ||
| 1908 | map["entityID"] = entityID.toString(); | ||
| 1909 | #ifdef DEBUG_ENTITY_STATES | ||
| 1910 | { | ||
| 1911 | auto debug = QVariantMap(); | ||
| 1912 | debug["script"] = scriptDetails.scriptText; | ||
| 1913 | debug["scriptObject"] = scriptDetails.scriptObject.toVariant(); | ||
| 1914 | debug["lastModified"] = (qlonglong)scriptDetails.lastModified; | ||
| 1915 | debug["sandboxURL"] = scriptDetails.definingSandboxURL; | ||
| 1916 | map["debug"] = debug; | ||
| 1917 | } | ||
| 1918 | #endif | ||
| 1919 | } else { | ||
| 1920 | #ifdef DEBUG_ENTITY_STATES | ||
| 1921 | qDebug() << "!gotEntityScriptDetails" << QThread::currentThread(); | ||
| 1922 | #endif | ||
| 1923 | map["isError"] = true; | ||
| 1924 | map["errorInfo"] = "Entity script details unavailable"; | ||
| 1925 | map["entityID"] = entityID.toString(); | ||
| 1926 | } | ||
| 1927 | } | ||
| 1928 | return map; | ||
| 1929 | } | ||
| 1930 | |||
| 1931 | QFuture<QVariant> ScriptEngine::getLocalEntityScriptDetails(const EntityItemID& entityID) { | ||
| 1932 | return QtConcurrent::run(this, &ScriptEngine::cloneEntityScriptDetails, entityID); | ||
| 1933 | } | ||
| 1934 | |||
| 1486 | bool ScriptEngine::getEntityScriptDetails(const EntityItemID& entityID, EntityScriptDetails &details) const { | 1935 | bool ScriptEngine::getEntityScriptDetails(const EntityItemID& entityID, EntityScriptDetails &details) const { |
| 1487 | auto it = _entityScripts.constFind(entityID); | 1936 | auto it = _entityScripts.constFind(entityID); |
| 1488 | if (it == _entityScripts.constEnd()) { | 1937 | if (it == _entityScripts.constEnd()) { |
| @@ -1621,10 +2070,10 @@ void ScriptEngine::loadEntityScript(const EntityItemID& entityID, const QString& · 132 unmodified lines | |||
| 1621 | 2070 | ||
| 1622 | auto scriptCache = DependencyManager::get<ScriptCache>(); | 2071 | auto scriptCache = DependencyManager::get<ScriptCache>(); |
| 1623 | // note: see EntityTreeRenderer.cpp for shared pointer lifecycle management | 2072 | // note: see EntityTreeRenderer.cpp for shared pointer lifecycle management |
| 1624 | QWeakPointer<ScriptEngine> weakRef(sharedFromThis()); | 2073 | QWeakPointer<BaseScriptEngine> weakRef(sharedFromThis()); |
| 1625 | scriptCache->getScriptContents(entityScript, | 2074 | scriptCache->getScriptContents(entityScript, |
| 1626 | [this, weakRef, entityScript, entityID](const QString& url, const QString& contents, bool isURL, bool success, const QString& status) { | 2075 | [this, weakRef, entityScript, entityID](const QString& url, const QString& contents, bool isURL, bool success, const QString& status) { |
| 1627 | QSharedPointer<ScriptEngine> strongRef(weakRef); | 2076 | QSharedPointer<BaseScriptEngine> strongRef(weakRef); |
| 1628 | if (!strongRef) { | 2077 | if (!strongRef) { |
| 1629 | qCWarning(scriptengine) << "loadEntityScript.contentAvailable -- ScriptEngine was deleted during getScriptContents!!"; | 2078 | qCWarning(scriptengine) << "loadEntityScript.contentAvailable -- ScriptEngine was deleted during getScriptContents!!"; |
| 1630 | return; | 2079 | return; |
| @@ -1743,13 +2192,12 @@ void ScriptEngine::entityScriptContentAvailable(const EntityItemID& entityID, co · 112 unmodified lines | |||
| 1743 | timeout.setSingleShot(true); | 2192 | timeout.setSingleShot(true); |
| 1744 | timeout.start(SANDBOX_TIMEOUT); | 2193 | timeout.start(SANDBOX_TIMEOUT); |
| 1745 | connect(&timeout, &QTimer::timeout, [&sandbox, SANDBOX_TIMEOUT, scriptOrURL]{ | 2194 | connect(&timeout, &QTimer::timeout, [&sandbox, SANDBOX_TIMEOUT, scriptOrURL]{ |
| 1746 | auto context = sandbox.currentContext(); | ||
| 1747 | if (context) { | ||
| 1748 | qCDebug(scriptengine) << "ScriptEngine::entityScriptContentAvailable timeout(" << scriptOrURL << ")"; | 2195 | qCDebug(scriptengine) << "ScriptEngine::entityScriptContentAvailable timeout(" << scriptOrURL << ")"; |
| 1749 | 2196 | ||
| 1750 | // Guard against infinite loops and non-performant code | 2197 | // Guard against infinite loops and non-performant code |
| 1751 | context->throwError(QString("Timed out (entity constructors are limited to %1ms)").arg(SANDBOX_TIMEOUT)); | 2198 | sandbox.raiseException( |
| 1752 | } | 2199 | sandbox.makeError(QString("Timed out (entity constructors are limited to %1ms)").arg(SANDBOX_TIMEOUT)) |
| 2200 | ); | ||
| 1753 | }); | 2201 | }); |
| 1754 | 2202 | ||
| 1755 | testConstructor = sandbox.evaluate(program); | 2203 | testConstructor = sandbox.evaluate(program); |
| @@ -1765,7 +2213,7 @@ void ScriptEngine::entityScriptContentAvailable(const EntityItemID& entityID, co · 9 unmodified lines | |||
| 1765 | if (exception.isError()) { | 2213 | if (exception.isError()) { |
| 1766 | // create a local copy using makeError to decouple from the sandbox engine | 2214 | // create a local copy using makeError to decouple from the sandbox engine |
| 1767 | exception = makeError(exception); | 2215 | exception = makeError(exception); |
| 1768 | setError(formatException(exception), EntityScriptStatus::ERROR_RUNNING_SCRIPT); | 2216 | setError(formatException(exception, _enableExtendedJSExceptions.get()), EntityScriptStatus::ERROR_RUNNING_SCRIPT); |
| 1769 | emit unhandledException(exception); | 2217 | emit unhandledException(exception); |
| 1770 | return; | 2218 | return; |
| 1771 | } | 2219 | } |
| @@ -1777,9 +2225,8 @@ void ScriptEngine::entityScriptContentAvailable(const EntityItemID& entityID, co · 5 unmodified lines | |||
| 1777 | testConstructorType = "empty"; | 2225 | testConstructorType = "empty"; |
| 1778 | } | 2226 | } |
| 1779 | QString testConstructorValue = testConstructor.toString(); | 2227 | QString testConstructorValue = testConstructor.toString(); |
| 1780 | const int maxTestConstructorValueSize = 80; | 2228 | if (testConstructorValue.size() > MAX_DEBUG_VALUE_LENGTH) { |
| 1781 | if (testConstructorValue.size() > maxTestConstructorValueSize) { | 2229 | testConstructorValue = testConstructorValue.mid(0, MAX_DEBUG_VALUE_LENGTH) + "..."; |
| 1782 | testConstructorValue = testConstructorValue.mid(0, maxTestConstructorValueSize) + "..."; | ||
| 1783 | } | 2230 | } |
| 1784 | auto message = QString("failed to load entity script -- expected a function, got %1, %2") | 2231 | auto message = QString("failed to load entity script -- expected a function, got %1, %2") |
| 1785 | .arg(testConstructorType).arg(testConstructorValue); | 2232 | .arg(testConstructorType).arg(testConstructorValue); |
| @@ -1817,7 +2264,7 @@ void ScriptEngine::entityScriptContentAvailable(const EntityItemID& entityID, co · 31 unmodified lines | |||
| 1817 | 2264 | ||
| 1818 | if (entityScriptObject.isError()) { | 2265 | if (entityScriptObject.isError()) { |
| 1819 | auto exception = entityScriptObject; | 2266 | auto exception = entityScriptObject; |
| 1820 | setError(formatException(exception), EntityScriptStatus::ERROR_RUNNING_SCRIPT); | 2267 | setError(formatException(exception, _enableExtendedJSExceptions.get()), EntityScriptStatus::ERROR_RUNNING_SCRIPT); |
| 1821 | emit unhandledException(exception); | 2268 | emit unhandledException(exception); |
| 1822 | return; | 2269 | return; |
| 1823 | } | 2270 | } |
| @@ -1861,10 +2308,12 @@ void ScriptEngine::unloadEntityScript(const EntityItemID& entityID, bool shouldR · 37 unmodified lines | |||
| 1861 | const EntityScriptDetails &oldDetails = _entityScripts[entityID]; | 2308 | const EntityScriptDetails &oldDetails = _entityScripts[entityID]; |
| 1862 | if (isEntityScriptRunning(entityID)) { | 2309 | if (isEntityScriptRunning(entityID)) { |
| 1863 | callEntityScriptMethod(entityID, "unload"); | 2310 | callEntityScriptMethod(entityID, "unload"); |
| 1864 | } else { | 2311 | } |
| 2312 | #ifdef DEBUG_ENTITY_STATES | ||
| 2313 | else { | ||
| 1865 | qCDebug(scriptengine) << "unload called while !running" << entityID << oldDetails.status; | 2314 | qCDebug(scriptengine) << "unload called while !running" << entityID << oldDetails.status; |
| 1866 | } | 2315 | } |
| 1867 | 2316 | #endif | |
| 1868 | if (shouldRemoveFromMap) { | 2317 | if (shouldRemoveFromMap) { |
| 1869 | // this was a deleted entity, we've been asked to remove it from the map | 2318 | // this was a deleted entity, we've been asked to remove it from the map |
| 1870 | _entityScripts.remove(entityID); | 2319 | _entityScripts.remove(entityID); |
| @@ -1956,10 +2405,7 @@ void ScriptEngine::doWithEnvironment(const EntityItemID& entityID, const QUrl& s · 85 unmodified lines | |||
| 1956 | #else | 2405 | #else |
| 1957 | operation(); | 2406 | operation(); |
| 1958 | #endif | 2407 | #endif |
| 1959 | if (!isEvaluating() && hasUncaughtException()) { | 2408 | maybeEmitUncaughtException(!entityID.isNull() ? entityID.toString() : __FUNCTION__); |
| 1960 | emit unhandledException(cloneUncaughtException(!entityID.isNull() ? entityID.toString() : __FUNCTION__)); | ||
| 1961 | clearExceptions(); | ||
| 1962 | } | ||
| 1963 | currentEntityIdentifier = oldIdentifier; | 2409 | currentEntityIdentifier = oldIdentifier; |
| 1964 | currentSandboxURL = oldSandboxURL; | 2410 | currentSandboxURL = oldSandboxURL; |
| 1965 | } | 2411 | } |