1715 lines · 74.2 KB · C++
RawDownload
            // 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