// FIXME - do we need to have a similar "wait here" loop for non-threaded packet senders? } } emit finished(_fileNameString, this); _isRunning = false; emit runningStateChanged(); emit doneRunning();} // NOTE: This is private because it must be called on the same thread that created the timers, which is why// we want to only call it in our own run "shutdown" processing.void ScriptEngine::stopAllTimers() { QMutableHashIterator<QTimer*, CallbackData> i(_timerFunctionMap); while (i.hasNext()) { i.next(); QTimer* timer = i.key(); stopTimer(timer); }} void ScriptEngine::stopAllTimersForEntityScript(const EntityItemID& entityID) { // We could maintain a separate map of entityID => QTimer, but someone will have to prove to me that it's worth the complexity. -HRS QVector<QTimer*> toDelete; QMutableHashIterator<QTimer*, CallbackData> i(_timerFunctionMap); while (i.hasNext()) { i.next(); if (i.value().definingEntityIdentifier != entityID) { continue; } QTimer* timer = i.key(); toDelete << timer; // don't delete while we're iterating. save it. } for (auto timer:toDelete) { // now reap 'em stopTimer(timer); } } void ScriptEngine::stop(bool marshal) { _isStopping = true; // this can be done on any thread if (marshal) { QMetaObject::invokeMethod(this, "stop"); return; } if (!_isFinished) { _isFinished = true; emit runningStateChanged(); }} // Other threads can invoke this through invokeMethod, which causes the callback to be asynchronously executed in this script's thread.void ScriptEngine::callAnimationStateHandler(QScriptValue callback, AnimVariantMap parameters, QStringList names, bool useNames, AnimVariantResultHandler resultHandler) { if (QThread::currentThread() != thread()) {#ifdef THREAD_DEBUGGING qCDebug(scriptengine) << "*** WARNING *** ScriptEngine::callAnimationStateHandler() called on wrong thread [" << QThread::currentThread() << "], invoking on correct thread [" << thread() << "] name:" << name;#endif QMetaObject::invokeMethod(this, "callAnimationStateHandler", Q_ARG(QScriptValue, callback), Q_ARG(AnimVariantMap, parameters), Q_ARG(QStringList, names), Q_ARG(bool, useNames), Q_ARG(AnimVariantResultHandler, resultHandler)); return; } QScriptValue javascriptParameters = parameters.animVariantMapToScriptValue(this, names, useNames); QScriptValueList callingArguments; callingArguments << javascriptParameters; assert(currentEntityIdentifier.isInvalidID()); // No animation state handlers from entity scripts. QScriptValue result = callback.call(QScriptValue(), callingArguments); // validate result from callback function. if (result.isValid() && result.isObject()) { resultHandler(result); } else { qCWarning(scriptengine) << "ScriptEngine::callAnimationStateHandler invalid return argument from callback, expected an object"; }} void ScriptEngine::updateMemoryCost(const qint64& deltaSize) { if (deltaSize > 0) { reportAdditionalMemoryCost(deltaSize); }} void ScriptEngine::timerFired() { { auto engine = DependencyManager::get<ScriptEngines>(); if (!engine || engine->isStopped()) { scriptWarningMessage("Script.timerFired() while shutting down is ignored... parent script:" + getFilename()); return; // bail early } } QTimer* callingTimer = reinterpret_cast<QTimer*>(sender()); CallbackData timerData = _timerFunctionMap.value(callingTimer); if (!callingTimer->isActive()) { // this timer is done, we can kill it _timerFunctionMap.remove(callingTimer); delete callingTimer; } // call the associated JS function, if it exists if (timerData.function.isValid()) { auto preTimer = p_high_resolution_clock::now(); callWithEnvironment(timerData.definingEntityIdentifier, timerData.definingSandboxURL, timerData.function, timerData.function, QScriptValueList()); auto postTimer = p_high_resolution_clock::now(); auto elapsed = (postTimer - preTimer); _totalTimerExecution += std::chrono::duration_cast<std::chrono::microseconds>(elapsed); }} QObject* ScriptEngine::setupTimerWithInterval(const QScriptValue& function, int intervalMS, bool isSingleShot) { // create the timer, add it to the map, and start it QTimer* newTimer = new QTimer(this); newTimer->setSingleShot(isSingleShot); // The default timer type is not very accurate below about 200ms http://doc.qt.io/qt-5/qt.html#TimerType-enum static const int MIN_TIMEOUT_FOR_COARSE_TIMER = 200; if (intervalMS < MIN_TIMEOUT_FOR_COARSE_TIMER) { newTimer->setTimerType(Qt::PreciseTimer); } connect(newTimer, &QTimer::timeout, this, &ScriptEngine::timerFired); // make sure the timer stops when the script does connect(this, &ScriptEngine::scriptEnding, newTimer, &QTimer::stop); CallbackData timerData = {function, currentEntityIdentifier, currentSandboxURL }; _timerFunctionMap.insert(newTimer, timerData); newTimer->start(intervalMS); return newTimer;} QObject* ScriptEngine::setInterval(const QScriptValue& function, int intervalMS) { if (DependencyManager::get<ScriptEngines>()->isStopped()) { scriptWarningMessage("Script.setInterval() while shutting down is ignored... parent script:" + getFilename()); return NULL; // bail early } return setupTimerWithInterval(function, intervalMS, false);} QObject* ScriptEngine::setTimeout(const QScriptValue& function, int timeoutMS) { if (DependencyManager::get<ScriptEngines>()->isStopped()) { scriptWarningMessage("Script.setTimeout() while shutting down is ignored... parent script:" + getFilename()); return NULL; // bail early } return setupTimerWithInterval(function, timeoutMS, true);} void ScriptEngine::stopTimer(QTimer *timer) { if (_timerFunctionMap.contains(timer)) { timer->stop(); _timerFunctionMap.remove(timer); delete timer; }} QUrl ScriptEngine::resolvePath(const QString& include) const { QUrl url(include); // first lets check to see if it's already a full URL if (!url.scheme().isEmpty()) { return expandScriptUrl(url); } QScriptContextInfo contextInfo { currentContext()->parentContext() }; // we apparently weren't a fully qualified url, so, let's assume we're relative // to the original URL of our script QUrl parentURL = contextInfo.fileName(); if (parentURL.isEmpty()) { if (_parentURL.isEmpty()) { parentURL = QUrl(_fileNameString); } else { parentURL = QUrl(_parentURL); } } // if the parent URL's scheme is empty, then this is probably a local file... if (parentURL.scheme().isEmpty()) { parentURL = QUrl::fromLocalFile(_fileNameString); } // at this point we should have a legitimate fully qualified URL for our parent url = expandScriptUrl(parentURL.resolved(url)); return url;} QUrl ScriptEngine::resourcesPath() const { return QUrl::fromLocalFile(PathUtils::resourcesPath());} void ScriptEngine::print(const QString& message) { emit printedMessage(message);} // If a callback is specified, the included files will be loaded asynchronously and the callback will be called// when all of the files have finished loading.// If no callback is specified, the included files will be loaded synchronously and will block execution until// all of the files have finished loading.void ScriptEngine::include(const QStringList& includeFiles, QScriptValue callback) { if (DependencyManager::get<ScriptEngines>()->isStopped()) { scriptWarningMessage("Script.include() while shutting down is ignored... includeFiles:" + includeFiles.join(",") + "parent script:" + getFilename()); return; // bail early } QList<QUrl> urls; bool knowsSensitivity = false; Qt::CaseSensitivity sensitivity { Qt::CaseSensitive }; auto getSensitivity = [&]() { if (!knowsSensitivity) { QString path = currentSandboxURL.path(); QFileInfo upperFI(path.toUpper()); QFileInfo lowerFI(path.toLower()); sensitivity = (upperFI == lowerFI) ? Qt::CaseInsensitive : Qt::CaseSensitive; knowsSensitivity = true; } return sensitivity; }; // Guard against meaningless query and fragment parts. // Do NOT use PreferLocalFile as its behavior is unpredictable (e.g., on defaultScriptsLocation()) const auto strippingFlags = QUrl::RemoveFilename | QUrl::RemoveQuery | QUrl::RemoveFragment; for (QString includeFile : includeFiles) { QString file = ResourceManager::normalizeURL(includeFile); QUrl thisURL; bool isStandardLibrary = false; if (file.startsWith("/~/")) { thisURL = expandScriptUrl(QUrl::fromLocalFile(expandScriptPath(file))); QUrl defaultScriptsLoc = defaultScriptsLocation(); if (!defaultScriptsLoc.isParentOf(thisURL)) { scriptWarningMessage("Script.include() -- skipping" + file + "-- outside of standard libraries"); continue; } isStandardLibrary = true; } else { thisURL = resolvePath(file); } if (!isStandardLibrary && !currentSandboxURL.isEmpty() && (thisURL.scheme() == "file") && (currentSandboxURL.scheme() != "file" || !thisURL.toString(strippingFlags).startsWith(currentSandboxURL.toString(strippingFlags), getSensitivity()))) { scriptWarningMessage("Script.include() ignoring file path" + thisURL.toString() + "outside of original entity script" + currentSandboxURL.toString()); } else { // We could also check here for CORS, but we don't yet. // It turns out that QUrl.resolve will not change hosts and copy authority, so we don't need to check that here. urls.append(thisURL); } } // If there are no URLs left to download, don't bother attempting to download anything and return early if (urls.size() == 0) { return; } BatchLoader* loader = new BatchLoader(urls); EntityItemID capturedEntityIdentifier = currentEntityIdentifier; QUrl capturedSandboxURL = currentSandboxURL; auto evaluateScripts = [=](const QMap<QUrl, QString>& data) { auto parentURL = _parentURL; for (QUrl url : urls) { QString contents = data[url]; if (contents.isNull()) { scriptErrorMessage("Error loading file: " + url.toString()); } else { std::lock_guard<std::recursive_mutex> lock(_lock); if (!_includedURLs.contains(url)) { _includedURLs << url; // Set the parent url so that path resolution will be relative // to this script's url during its initial evaluation _parentURL = url.toString(); auto operation = [&]() { evaluate(contents, url.toString()); }; doWithEnvironment(capturedEntityIdentifier, capturedSandboxURL, operation); } else { scriptWarningMessage("Script.include() skipping evaluation of previously included url:" + url.toString()); } } } _parentURL = parentURL; if (callback.isFunction()) { callWithEnvironment(capturedEntityIdentifier, capturedSandboxURL, QScriptValue(callback), QScriptValue(), QScriptValueList()); } loader->deleteLater(); }; connect(loader, &BatchLoader::finished, this, evaluateScripts); // If we are destroyed before the loader completes, make sure to clean it up connect(this, &QObject::destroyed, loader, &QObject::deleteLater); loader->start(); if (!callback.isFunction() && !loader->isFinished()) { QEventLoop loop; QObject::connect(loader, &BatchLoader::finished, &loop, &QEventLoop::quit); loop.exec(); }} void ScriptEngine::include(const QString& includeFile, QScriptValue callback) { if (DependencyManager::get<ScriptEngines>()->isStopped()) { scriptWarningMessage("Script.include() while shutting down is ignored... includeFile:" + includeFile + "parent script:" + getFilename()); return; // bail early } QStringList urls; urls.append(includeFile); include(urls, callback);} // NOTE: The load() command is similar to the include() command except that it loads the script// as a stand-alone script. To accomplish this, the ScriptEngine class just emits a signal which// the Application or other context will connect to in order to know to actually load the scriptvoid ScriptEngine::load(const QString& loadFile) { if (DependencyManager::get<ScriptEngines>()->isStopped()) { scriptWarningMessage("Script.load() while shutting down is ignored... loadFile:" + loadFile + "parent script:" + getFilename()); return; // bail early } if (!currentEntityIdentifier.isInvalidID()) { scriptWarningMessage("Script.load() from entity script is ignored... loadFile:" + loadFile + "parent script:" + getFilename()); return; // bail early } QUrl url = resolvePath(loadFile); if (_isReloading) { auto scriptCache = DependencyManager::get<ScriptCache>(); scriptCache->deleteScript(url.toString()); emit reloadScript(url.toString(), false); } else { emit loadScript(url.toString(), false); }} // Look up the handler associated with eventName and entityID. If found, evalute the argGenerator thunk and call the handler with those argsvoid ScriptEngine::forwardHandlerCall(const EntityItemID& entityID, const QString& eventName, QScriptValueList eventHandlerArgs) { if (QThread::currentThread() != thread()) { qCDebug(scriptengine) << "*** ERROR *** ScriptEngine::forwardHandlerCall() called on wrong thread [" << QThread::currentThread() << "], invoking on correct thread [" << thread() << "]"; assert(false); return ; } if (!_registeredHandlers.contains(entityID)) { return; } const RegisteredEventHandlers& handlersOnEntity = _registeredHandlers[entityID]; if (!handlersOnEntity.contains(eventName)) { return; } CallbackList handlersForEvent = handlersOnEntity[eventName]; if (!handlersForEvent.isEmpty()) { for (int i = 0; i < handlersForEvent.count(); ++i) { // handlersForEvent[i] can contain many handlers that may have each been added by different interface or entity scripts, // and the entity scripts may be for entities other than the one this is a handler for. // Fortunately, the definingEntityIdentifier captured the entity script id (if any) when the handler was added. CallbackData& handler = handlersForEvent[i]; callWithEnvironment(handler.definingEntityIdentifier, handler.definingSandboxURL, handler.function, QScriptValue(), eventHandlerArgs); } }} // since all of these operations can be asynch we will always do the actual work in the response handler// for the downloadvoid ScriptEngine::loadEntityScript(QWeakPointer<ScriptEngine> theEngine, const EntityItemID& entityID, const QString& entityScript, bool forceRedownload) { // NOTE: If the script content is not currently in the cache, the LAMBDA here will be called on the Main Thread // which means we're guaranteed that it's not the correct thread for the ScriptEngine. This means // when we get into entityScriptContentAvailable() we will likely invokeMethod() to get it over // to the "Entities" ScriptEngine thread. DependencyManager::get<ScriptCache>()->getScriptContents(entityScript, [theEngine, entityID](const QString& scriptOrURL, const QString& contents, bool isURL, bool success) { QSharedPointer<ScriptEngine> strongEngine = theEngine.toStrongRef(); if (strongEngine) {#ifdef THREAD_DEBUGGING qCDebug(scriptengine) << "ScriptEngine::entityScriptContentAvailable() IN LAMBDA contentAvailable on thread [" << QThread::currentThread() << "] expected thread [" << strongEngine->thread() << "]";#endif strongEngine->entityScriptContentAvailable(entityID, scriptOrURL, contents, isURL, success); } }, forceRedownload);} // since all of these operations can be asynch we will always do the actual work in the response handler// for the downloadvoid ScriptEngine::entityScriptContentAvailable(const EntityItemID& entityID, const QString& scriptOrURL, const QString& contents, bool isURL, bool success) { if (QThread::currentThread() != thread()) {#ifdef THREAD_DEBUGGING qCDebug(scriptengine) << "*** WARNING *** ScriptEngine::entityScriptContentAvailable() called on wrong thread [" << QThread::currentThread() << "], invoking on correct thread [" << thread() << "] " "entityID:" << entityID << "scriptOrURL:" << scriptOrURL << "contents:" << contents << "isURL:" << isURL << "success:" << success;#endif QMetaObject::invokeMethod(this, "entityScriptContentAvailable", Q_ARG(const EntityItemID&, entityID), Q_ARG(const QString&, scriptOrURL), Q_ARG(const QString&, contents), Q_ARG(bool, isURL), Q_ARG(bool, success)); return; } #ifdef THREAD_DEBUGGING qCDebug(scriptengine) << "ScriptEngine::entityScriptContentAvailable() thread [" << QThread::currentThread() << "] expected thread [" << thread() << "]";#endif auto scriptCache = DependencyManager::get<ScriptCache>(); bool isFileUrl = isURL && scriptOrURL.startsWith("file://"); auto fileName = isURL ? scriptOrURL : "EmbeddedEntityScript"; QScriptProgram program(contents, fileName); if (!hasCorrectSyntax(program, this)) { if (!isFileUrl) { scriptCache->addScriptToBadScriptList(scriptOrURL); } return; // done processing script } if (isURL) { setParentURL(scriptOrURL); } const int SANDBOX_TIMEOUT = 0.25 * MSECS_PER_SECOND; QScriptEngine sandbox; sandbox.setProcessEventsInterval(SANDBOX_TIMEOUT); QScriptValue testConstructor; { QTimer timeout; timeout.setSingleShot(true); timeout.start(SANDBOX_TIMEOUT); connect(&timeout, &QTimer::timeout, [&sandbox, SANDBOX_TIMEOUT]{ auto context = sandbox.currentContext(); if (context) { // Guard against infinite loops and non-performant code context->throwError(QString("Timed out (entity constructors are limited to %1ms)").arg(SANDBOX_TIMEOUT)); } }); testConstructor = sandbox.evaluate(program); } if (hadUncaughtExceptions(sandbox, program.fileName(), this)) { return; } if (!testConstructor.isFunction()) { QString testConstructorType = QString(testConstructor.toVariant().typeName()); if (testConstructorType == "") { testConstructorType = "empty"; } QString testConstructorValue = testConstructor.toString(); const int maxTestConstructorValueSize = 80; if (testConstructorValue.size() > maxTestConstructorValueSize) { testConstructorValue = testConstructorValue.mid(0, maxTestConstructorValueSize) + "..."; } scriptErrorMessage("Error -- ScriptEngine::loadEntityScript() entity:" + entityID.toString() + "failed to load entity script -- expected a function, got " + testConstructorType + "," + testConstructorValue + "," + scriptOrURL); if (!isFileUrl) { scriptCache->addScriptToBadScriptList(scriptOrURL); } return; // done processing script } int64_t lastModified = 0; if (isFileUrl) { QString file = QUrl(scriptOrURL).toLocalFile(); lastModified = (quint64)QFileInfo(file).lastModified().toMSecsSinceEpoch(); } QScriptValue entityScriptConstructor, entityScriptObject; QUrl sandboxURL = currentSandboxURL.isEmpty() ? scriptOrURL : currentSandboxURL; auto initialization = [&]{ entityScriptConstructor = evaluate(contents, fileName); entityScriptObject = entityScriptConstructor.construct(); }; doWithEnvironment(entityID, sandboxURL, initialization); EntityScriptDetails newDetails = { scriptOrURL, entityScriptObject, lastModified, sandboxURL }; _entityScripts[entityID] = newDetails; if (isURL) { setParentURL(""); } Lines 1001–1500 of 1715. Previous lines Load more
// FIXME - do we need to have a similar "wait here" loop for non-threaded packet senders? } } emit finished(_fileNameString, this); _isRunning = false; emit runningStateChanged(); emit doneRunning(); } // NOTE: This is private because it must be called on the same thread that created the timers, which is why // we want to only call it in our own run "shutdown" processing. void ScriptEngine::stopAllTimers() { QMutableHashIterator<QTimer*, CallbackData> i(_timerFunctionMap); while (i.hasNext()) { i.next(); QTimer* timer = i.key(); stopTimer(timer); } } void ScriptEngine::stopAllTimersForEntityScript(const EntityItemID& entityID) { // We could maintain a separate map of entityID => QTimer, but someone will have to prove to me that it's worth the complexity. -HRS QVector<QTimer*> toDelete; QMutableHashIterator<QTimer*, CallbackData> i(_timerFunctionMap); while (i.hasNext()) { i.next(); if (i.value().definingEntityIdentifier != entityID) { continue; } QTimer* timer = i.key(); toDelete << timer; // don't delete while we're iterating. save it. } for (auto timer:toDelete) { // now reap 'em stopTimer(timer); } } void ScriptEngine::stop(bool marshal) { _isStopping = true; // this can be done on any thread if (marshal) { QMetaObject::invokeMethod(this, "stop"); return; } if (!_isFinished) { _isFinished = true; emit runningStateChanged(); } } // Other threads can invoke this through invokeMethod, which causes the callback to be asynchronously executed in this script's thread. void ScriptEngine::callAnimationStateHandler(QScriptValue callback, AnimVariantMap parameters, QStringList names, bool useNames, AnimVariantResultHandler resultHandler) { if (QThread::currentThread() != thread()) { #ifdef THREAD_DEBUGGING qCDebug(scriptengine) << "*** WARNING *** ScriptEngine::callAnimationStateHandler() called on wrong thread [" << QThread::currentThread() << "], invoking on correct thread [" << thread() << "] name:" << name; #endif QMetaObject::invokeMethod(this, "callAnimationStateHandler", Q_ARG(QScriptValue, callback), Q_ARG(AnimVariantMap, parameters), Q_ARG(QStringList, names), Q_ARG(bool, useNames), Q_ARG(AnimVariantResultHandler, resultHandler)); return; } QScriptValue javascriptParameters = parameters.animVariantMapToScriptValue(this, names, useNames); QScriptValueList callingArguments; callingArguments << javascriptParameters; assert(currentEntityIdentifier.isInvalidID()); // No animation state handlers from entity scripts. QScriptValue result = callback.call(QScriptValue(), callingArguments); // validate result from callback function. if (result.isValid() && result.isObject()) { resultHandler(result); } else { qCWarning(scriptengine) << "ScriptEngine::callAnimationStateHandler invalid return argument from callback, expected an object"; } } void ScriptEngine::updateMemoryCost(const qint64& deltaSize) { if (deltaSize > 0) { reportAdditionalMemoryCost(deltaSize); } } void ScriptEngine::timerFired() { { auto engine = DependencyManager::get<ScriptEngines>(); if (!engine || engine->isStopped()) { scriptWarningMessage("Script.timerFired() while shutting down is ignored... parent script:" + getFilename()); return; // bail early } } QTimer* callingTimer = reinterpret_cast<QTimer*>(sender()); CallbackData timerData = _timerFunctionMap.value(callingTimer); if (!callingTimer->isActive()) { // this timer is done, we can kill it _timerFunctionMap.remove(callingTimer); delete callingTimer; } // call the associated JS function, if it exists if (timerData.function.isValid()) { auto preTimer = p_high_resolution_clock::now(); callWithEnvironment(timerData.definingEntityIdentifier, timerData.definingSandboxURL, timerData.function, timerData.function, QScriptValueList()); auto postTimer = p_high_resolution_clock::now(); auto elapsed = (postTimer - preTimer); _totalTimerExecution += std::chrono::duration_cast<std::chrono::microseconds>(elapsed); } } QObject* ScriptEngine::setupTimerWithInterval(const QScriptValue& function, int intervalMS, bool isSingleShot) { // create the timer, add it to the map, and start it QTimer* newTimer = new QTimer(this); newTimer->setSingleShot(isSingleShot); // The default timer type is not very accurate below about 200ms http://doc.qt.io/qt-5/qt.html#TimerType-enum static const int MIN_TIMEOUT_FOR_COARSE_TIMER = 200; if (intervalMS < MIN_TIMEOUT_FOR_COARSE_TIMER) { newTimer->setTimerType(Qt::PreciseTimer); } connect(newTimer, &QTimer::timeout, this, &ScriptEngine::timerFired); // make sure the timer stops when the script does connect(this, &ScriptEngine::scriptEnding, newTimer, &QTimer::stop); CallbackData timerData = {function, currentEntityIdentifier, currentSandboxURL }; _timerFunctionMap.insert(newTimer, timerData); newTimer->start(intervalMS); return newTimer; } QObject* ScriptEngine::setInterval(const QScriptValue& function, int intervalMS) { if (DependencyManager::get<ScriptEngines>()->isStopped()) { scriptWarningMessage("Script.setInterval() while shutting down is ignored... parent script:" + getFilename()); return NULL; // bail early } return setupTimerWithInterval(function, intervalMS, false); } QObject* ScriptEngine::setTimeout(const QScriptValue& function, int timeoutMS) { if (DependencyManager::get<ScriptEngines>()->isStopped()) { scriptWarningMessage("Script.setTimeout() while shutting down is ignored... parent script:" + getFilename()); return NULL; // bail early } return setupTimerWithInterval(function, timeoutMS, true); } void ScriptEngine::stopTimer(QTimer *timer) { if (_timerFunctionMap.contains(timer)) { timer->stop(); _timerFunctionMap.remove(timer); delete timer; } } QUrl ScriptEngine::resolvePath(const QString& include) const { QUrl url(include); // first lets check to see if it's already a full URL if (!url.scheme().isEmpty()) { return expandScriptUrl(url); } QScriptContextInfo contextInfo { currentContext()->parentContext() }; // we apparently weren't a fully qualified url, so, let's assume we're relative // to the original URL of our script QUrl parentURL = contextInfo.fileName(); if (parentURL.isEmpty()) { if (_parentURL.isEmpty()) { parentURL = QUrl(_fileNameString); } else { parentURL = QUrl(_parentURL); } } // if the parent URL's scheme is empty, then this is probably a local file... if (parentURL.scheme().isEmpty()) { parentURL = QUrl::fromLocalFile(_fileNameString); } // at this point we should have a legitimate fully qualified URL for our parent url = expandScriptUrl(parentURL.resolved(url)); return url; } QUrl ScriptEngine::resourcesPath() const { return QUrl::fromLocalFile(PathUtils::resourcesPath()); } void ScriptEngine::print(const QString& message) { emit printedMessage(message); } // If a callback is specified, the included files will be loaded asynchronously and the callback will be called // when all of the files have finished loading. // If no callback is specified, the included files will be loaded synchronously and will block execution until // all of the files have finished loading. void ScriptEngine::include(const QStringList& includeFiles, QScriptValue callback) { if (DependencyManager::get<ScriptEngines>()->isStopped()) { scriptWarningMessage("Script.include() while shutting down is ignored... includeFiles:" + includeFiles.join(",") + "parent script:" + getFilename()); return; // bail early } QList<QUrl> urls; bool knowsSensitivity = false; Qt::CaseSensitivity sensitivity { Qt::CaseSensitive }; auto getSensitivity = [&]() { if (!knowsSensitivity) { QString path = currentSandboxURL.path(); QFileInfo upperFI(path.toUpper()); QFileInfo lowerFI(path.toLower()); sensitivity = (upperFI == lowerFI) ? Qt::CaseInsensitive : Qt::CaseSensitive; knowsSensitivity = true; } return sensitivity; }; // Guard against meaningless query and fragment parts. // Do NOT use PreferLocalFile as its behavior is unpredictable (e.g., on defaultScriptsLocation()) const auto strippingFlags = QUrl::RemoveFilename | QUrl::RemoveQuery | QUrl::RemoveFragment; for (QString includeFile : includeFiles) { QString file = ResourceManager::normalizeURL(includeFile); QUrl thisURL; bool isStandardLibrary = false; if (file.startsWith("/~/")) { thisURL = expandScriptUrl(QUrl::fromLocalFile(expandScriptPath(file))); QUrl defaultScriptsLoc = defaultScriptsLocation(); if (!defaultScriptsLoc.isParentOf(thisURL)) { scriptWarningMessage("Script.include() -- skipping" + file + "-- outside of standard libraries"); continue; } isStandardLibrary = true; } else { thisURL = resolvePath(file); } if (!isStandardLibrary && !currentSandboxURL.isEmpty() && (thisURL.scheme() == "file") && (currentSandboxURL.scheme() != "file" || !thisURL.toString(strippingFlags).startsWith(currentSandboxURL.toString(strippingFlags), getSensitivity()))) { scriptWarningMessage("Script.include() ignoring file path" + thisURL.toString() + "outside of original entity script" + currentSandboxURL.toString()); } else { // We could also check here for CORS, but we don't yet. // It turns out that QUrl.resolve will not change hosts and copy authority, so we don't need to check that here. urls.append(thisURL); } } // If there are no URLs left to download, don't bother attempting to download anything and return early if (urls.size() == 0) { return; } BatchLoader* loader = new BatchLoader(urls); EntityItemID capturedEntityIdentifier = currentEntityIdentifier; QUrl capturedSandboxURL = currentSandboxURL; auto evaluateScripts = [=](const QMap<QUrl, QString>& data) { auto parentURL = _parentURL; for (QUrl url : urls) { QString contents = data[url]; if (contents.isNull()) { scriptErrorMessage("Error loading file: " + url.toString()); } else { std::lock_guard<std::recursive_mutex> lock(_lock); if (!_includedURLs.contains(url)) { _includedURLs << url; // Set the parent url so that path resolution will be relative // to this script's url during its initial evaluation _parentURL = url.toString(); auto operation = [&]() { evaluate(contents, url.toString()); }; doWithEnvironment(capturedEntityIdentifier, capturedSandboxURL, operation); } else { scriptWarningMessage("Script.include() skipping evaluation of previously included url:" + url.toString()); } } } _parentURL = parentURL; if (callback.isFunction()) { callWithEnvironment(capturedEntityIdentifier, capturedSandboxURL, QScriptValue(callback), QScriptValue(), QScriptValueList()); } loader->deleteLater(); }; connect(loader, &BatchLoader::finished, this, evaluateScripts); // If we are destroyed before the loader completes, make sure to clean it up connect(this, &QObject::destroyed, loader, &QObject::deleteLater); loader->start(); if (!callback.isFunction() && !loader->isFinished()) { QEventLoop loop; QObject::connect(loader, &BatchLoader::finished, &loop, &QEventLoop::quit); loop.exec(); } } void ScriptEngine::include(const QString& includeFile, QScriptValue callback) { if (DependencyManager::get<ScriptEngines>()->isStopped()) { scriptWarningMessage("Script.include() while shutting down is ignored... includeFile:" + includeFile + "parent script:" + getFilename()); return; // bail early } QStringList urls; urls.append(includeFile); include(urls, callback); } // NOTE: The load() command is similar to the include() command except that it loads the script // as a stand-alone script. To accomplish this, the ScriptEngine class just emits a signal which // the Application or other context will connect to in order to know to actually load the script void ScriptEngine::load(const QString& loadFile) { if (DependencyManager::get<ScriptEngines>()->isStopped()) { scriptWarningMessage("Script.load() while shutting down is ignored... loadFile:" + loadFile + "parent script:" + getFilename()); return; // bail early } if (!currentEntityIdentifier.isInvalidID()) { scriptWarningMessage("Script.load() from entity script is ignored... loadFile:" + loadFile + "parent script:" + getFilename()); return; // bail early } QUrl url = resolvePath(loadFile); if (_isReloading) { auto scriptCache = DependencyManager::get<ScriptCache>(); scriptCache->deleteScript(url.toString()); emit reloadScript(url.toString(), false); } else { emit loadScript(url.toString(), false); } } // Look up the handler associated with eventName and entityID. If found, evalute the argGenerator thunk and call the handler with those args void ScriptEngine::forwardHandlerCall(const EntityItemID& entityID, const QString& eventName, QScriptValueList eventHandlerArgs) { if (QThread::currentThread() != thread()) { qCDebug(scriptengine) << "*** ERROR *** ScriptEngine::forwardHandlerCall() called on wrong thread [" << QThread::currentThread() << "], invoking on correct thread [" << thread() << "]"; assert(false); return ; } if (!_registeredHandlers.contains(entityID)) { return; } const RegisteredEventHandlers& handlersOnEntity = _registeredHandlers[entityID]; if (!handlersOnEntity.contains(eventName)) { return; } CallbackList handlersForEvent = handlersOnEntity[eventName]; if (!handlersForEvent.isEmpty()) { for (int i = 0; i < handlersForEvent.count(); ++i) { // handlersForEvent[i] can contain many handlers that may have each been added by different interface or entity scripts, // and the entity scripts may be for entities other than the one this is a handler for. // Fortunately, the definingEntityIdentifier captured the entity script id (if any) when the handler was added. CallbackData& handler = handlersForEvent[i]; callWithEnvironment(handler.definingEntityIdentifier, handler.definingSandboxURL, handler.function, QScriptValue(), eventHandlerArgs); } } } // since all of these operations can be asynch we will always do the actual work in the response handler // for the download void ScriptEngine::loadEntityScript(QWeakPointer<ScriptEngine> theEngine, const EntityItemID& entityID, const QString& entityScript, bool forceRedownload) { // NOTE: If the script content is not currently in the cache, the LAMBDA here will be called on the Main Thread // which means we're guaranteed that it's not the correct thread for the ScriptEngine. This means // when we get into entityScriptContentAvailable() we will likely invokeMethod() to get it over // to the "Entities" ScriptEngine thread. DependencyManager::get<ScriptCache>()->getScriptContents(entityScript, [theEngine, entityID](const QString& scriptOrURL, const QString& contents, bool isURL, bool success) { QSharedPointer<ScriptEngine> strongEngine = theEngine.toStrongRef(); if (strongEngine) { #ifdef THREAD_DEBUGGING qCDebug(scriptengine) << "ScriptEngine::entityScriptContentAvailable() IN LAMBDA contentAvailable on thread [" << QThread::currentThread() << "] expected thread [" << strongEngine->thread() << "]"; #endif strongEngine->entityScriptContentAvailable(entityID, scriptOrURL, contents, isURL, success); } }, forceRedownload); } // since all of these operations can be asynch we will always do the actual work in the response handler // for the download void ScriptEngine::entityScriptContentAvailable(const EntityItemID& entityID, const QString& scriptOrURL, const QString& contents, bool isURL, bool success) { if (QThread::currentThread() != thread()) { #ifdef THREAD_DEBUGGING qCDebug(scriptengine) << "*** WARNING *** ScriptEngine::entityScriptContentAvailable() called on wrong thread [" << QThread::currentThread() << "], invoking on correct thread [" << thread() << "] " "entityID:" << entityID << "scriptOrURL:" << scriptOrURL << "contents:" << contents << "isURL:" << isURL << "success:" << success; #endif QMetaObject::invokeMethod(this, "entityScriptContentAvailable", Q_ARG(const EntityItemID&, entityID), Q_ARG(const QString&, scriptOrURL), Q_ARG(const QString&, contents), Q_ARG(bool, isURL), Q_ARG(bool, success)); return; } #ifdef THREAD_DEBUGGING qCDebug(scriptengine) << "ScriptEngine::entityScriptContentAvailable() thread [" << QThread::currentThread() << "] expected thread [" << thread() << "]"; #endif auto scriptCache = DependencyManager::get<ScriptCache>(); bool isFileUrl = isURL && scriptOrURL.startsWith("file://"); auto fileName = isURL ? scriptOrURL : "EmbeddedEntityScript"; QScriptProgram program(contents, fileName); if (!hasCorrectSyntax(program, this)) { if (!isFileUrl) { scriptCache->addScriptToBadScriptList(scriptOrURL); } return; // done processing script } if (isURL) { setParentURL(scriptOrURL); } const int SANDBOX_TIMEOUT = 0.25 * MSECS_PER_SECOND; QScriptEngine sandbox; sandbox.setProcessEventsInterval(SANDBOX_TIMEOUT); QScriptValue testConstructor; { QTimer timeout; timeout.setSingleShot(true); timeout.start(SANDBOX_TIMEOUT); connect(&timeout, &QTimer::timeout, [&sandbox, SANDBOX_TIMEOUT]{ auto context = sandbox.currentContext(); if (context) { // Guard against infinite loops and non-performant code context->throwError(QString("Timed out (entity constructors are limited to %1ms)").arg(SANDBOX_TIMEOUT)); } }); testConstructor = sandbox.evaluate(program); } if (hadUncaughtExceptions(sandbox, program.fileName(), this)) { return; } if (!testConstructor.isFunction()) { QString testConstructorType = QString(testConstructor.toVariant().typeName()); if (testConstructorType == "") { testConstructorType = "empty"; } QString testConstructorValue = testConstructor.toString(); const int maxTestConstructorValueSize = 80; if (testConstructorValue.size() > maxTestConstructorValueSize) { testConstructorValue = testConstructorValue.mid(0, maxTestConstructorValueSize) + "..."; } scriptErrorMessage("Error -- ScriptEngine::loadEntityScript() entity:" + entityID.toString() + "failed to load entity script -- expected a function, got " + testConstructorType + "," + testConstructorValue + "," + scriptOrURL); if (!isFileUrl) { scriptCache->addScriptToBadScriptList(scriptOrURL); } return; // done processing script } int64_t lastModified = 0; if (isFileUrl) { QString file = QUrl(scriptOrURL).toLocalFile(); lastModified = (quint64)QFileInfo(file).lastModified().toMSecsSinceEpoch(); } QScriptValue entityScriptConstructor, entityScriptObject; QUrl sandboxURL = currentSandboxURL.isEmpty() ? scriptOrURL : currentSandboxURL; auto initialization = [&]{ entityScriptConstructor = evaluate(contents, fileName); entityScriptObject = entityScriptConstructor.construct(); }; doWithEnvironment(entityID, sandboxURL, initialization); EntityScriptDetails newDetails = { scriptOrURL, entityScriptObject, lastModified, sandboxURL }; _entityScripts[entityID] = newDetails; if (isURL) { setParentURL(""); }