History

Merge pull request #9686 from humbletim/21114-part3

CR 21114 -- Implement modules/require support into hifi

Chris Collins committed 9 years ago

Commit 308c1342 parents 1723549 87d62ad29 files · +1538 −138Browse files

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

Unified · Split

libraries/script-engine/src/ScriptEngine.cpp+482−36
@@ -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
78const QString ScriptEngine::_SETTINGS_ENABLE_EXTENDED_EXCEPTIONS {
79 "com.highfidelity.experimental.enableExtendedJSExceptions"
80};
81
82static const int MAX_MODULE_ID_LENGTH { 4096 };
83static const int MAX_DEBUG_VALUE_LENGTH { 80 };
84
75static const QScriptEngine::QObjectWrapOptions DEFAULT_QOBJECT_WRAP_OPTIONS =85static const QScriptEngine::QObjectWrapOptions DEFAULT_QOBJECT_WRAP_OPTIONS =
76 QScriptEngine::ExcludeDeleteLater | QScriptEngine::ExcludeChildObjects;86 QScriptEngine::ExcludeDeleteLater | QScriptEngine::ExcludeChildObjects;
77static const QScriptValue::PropertyFlags READONLY_PROP_FLAGS { QScriptValue::ReadOnly | QScriptValue::Undeletable };87static const QScriptValue::PropertyFlags READONLY_PROP_FLAGS { QScriptValue::ReadOnly | QScriptValue::Undeletable };
78static const QScriptValue::PropertyFlags READONLY_HIDDEN_PROP_FLAGS { READONLY_PROP_FLAGS | QScriptValue::SkipInEnumeration };88static const QScriptValue::PropertyFlags READONLY_HIDDEN_PROP_FLAGS { READONLY_PROP_FLAGS | QScriptValue::SkipInEnumeration };
79 89
80
81
82static const bool HIFI_AUTOREFRESH_FILE_SCRIPTS { true };90static const bool HIFI_AUTOREFRESH_FILE_SCRIPTS { true };
83 91
84Q_DECLARE_METATYPE(QScriptEngine::FunctionSignature)92Q_DECLARE_METATYPE(QScriptEngine::FunctionSignature)
@@ -86,7 +94,7 @@ int functionSignatureMetaID = qRegisterMetaType<QScriptEngine::FunctionSignature · 1 unmodified line
86 94
87Q_LOGGING_CATEGORY(scriptengineScript, "hifi.scriptengine.script")95Q_LOGGING_CATEGORY(scriptengineScript, "hifi.scriptengine.script")
88 96
89static QScriptValue debugPrint(QScriptContext* context, QScriptEngine* engine){97static 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
145QString ScriptEngine::logException(const QScriptValue& exception) {153QString 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, so343 // 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 of349 // 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
545void 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
537void ScriptEngine::init() {579void ScriptEngine::init() {
538 if (_isInitialized) {580 if (_isInitialized) {
539 return; // only initialize once581 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
914QScriptValue ScriptEngine::evaluateInClosure(const QScriptValue& closure, const QScriptProgram& program) {
915 return BaseScriptEngine::evaluateInClosure(closure, program);
916}
917
862QScriptValue ScriptEngine::evaluate(const QString& sourceCode, const QString& fileName, int lineNumber) {918QScriptValue 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 early920 return QScriptValue(); // bail early
@@ -881,29 +937,26 @@ QScriptValue ScriptEngine::evaluate(const QString& sourceCode, const QString& fi · 16 unmodified lines
881 // Check syntax937 // 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 _WIN32984#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)
1354QString 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
1446QScriptValue 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")
1465bool 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
1487QScriptValue 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
1517QVariantMap 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
1574QScriptValue 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
1598QScriptValue 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 called1700// 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 until1702// 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.
1307void ScriptEngine::include(const QStringList& includeFiles, QScriptValue callback) {1704void 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 which1814// 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 script1815// the Application or other context will connect to in order to know to actually load the script
1416void ScriptEngine::load(const QString& loadFile) {1816void 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
1889QVariant 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
1931QFuture<QVariant> ScriptEngine::getLocalEntityScriptDetails(const EntityItemID& entityID) {
1932 return QtConcurrent::run(this, &ScriptEngine::cloneEntityScriptDetails, entityID);
1933}
1934
1486bool ScriptEngine::getEntityScriptDetails(const EntityItemID& entityID, EntityScriptDetails &details) const {1935bool 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 management2072 // 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 code2197 // 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 engine2214 // 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 map2318 // 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#else2405#else
1957 operation();2406 operation();
1958#endif2407#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}