1574 lines · 67.4 KB · C++
RawDownload
  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);     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()) {        qCDebug(scriptengine) << "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()) {        qCDebug(scriptengine) << "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);    }     // we apparently weren't a fully qualified url, so, let's assume we're relative    // to the original URL of our script    QUrl parentURL;    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;} 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()) {        qCDebug(scriptengine) << "Script.include() while shutting down is ignored..."        << "includeFiles:" << includeFiles << "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 file : includeFiles) {        QUrl thisURL;        bool isStandardLibrary = false;        if (file.startsWith("/~/")) {            thisURL = expandScriptUrl(QUrl::fromLocalFile(expandScriptPath(file)));            QUrl defaultScriptsLoc = defaultScriptsLocation();            if (!defaultScriptsLoc.isParentOf(thisURL)) {                qDebug() << "ScriptEngine::include -- skipping" << file << "-- outside of standard libraries";                continue;            }            isStandardLibrary = true;        } else {            thisURL = resolvePath(file);        }         if (!_includedURLs.contains(thisURL)) {            if (!isStandardLibrary && !currentSandboxURL.isEmpty() && (thisURL.scheme() == "file") &&                (currentSandboxURL.scheme() != "file" ||                 !thisURL.toString(strippingFlags).startsWith(currentSandboxURL.toString(strippingFlags), getSensitivity()))) {                qCWarning(scriptengine) << "Script.include() ignoring file path"                                        << thisURL << "outside of original entity script" << currentSandboxURL;            } 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);                _includedURLs << thisURL;            }        } else {            qCDebug(scriptengine) << "Script.include() ignoring previously included url:" << thisURL;        }    }     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()) {                qCDebug(scriptengine) << "Error loading file: " << url << "line:" << __LINE__;            } else {                // 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);            }        }        _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()) {        qCDebug(scriptengine) << "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()) {        qCDebug(scriptengine) << "Script.load() while shutting down is ignored... "            << "loadFile:" << loadFile << "parent script:" << getFilename();        return; // bail early    }    if (!currentEntityIdentifier.isInvalidID()) {        qCWarning(scriptengine) << "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()) {        qDebug() << "*** 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            qDebug() << "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        qDebug() << "*** 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    qDebug() << "ScriptEngine::entityScriptContentAvailable() thread [" << QThread::currentThread() << "] expected thread [" << thread() << "]";#endif     auto scriptCache = DependencyManager::get<ScriptCache>();    bool isFileUrl = isURL && scriptOrURL.startsWith("file://");    auto fileName = QString("(EntityID:%1, %2)").arg(entityID.toString(), isURL ? scriptOrURL : "EmbededEntityScript");     QScriptProgram program(contents, fileName);    if (!hasCorrectSyntax(program)) {        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())) {        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) + "...";        }        qCDebug(scriptengine) << "Error -- ScriptEngine::loadEntityScript() entity:" << entityID                              << "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("");    }     // if we got this far, then call the preload method    callEntityScriptMethod(entityID, "preload");} void ScriptEngine::unloadEntityScript(const EntityItemID& entityID) {    if (QThread::currentThread() != thread()) {#ifdef THREAD_DEBUGGING        qDebug() << "*** WARNING *** ScriptEngine::unloadEntityScript() called on wrong thread [" << QThread::currentThread() << "], invoking on correct thread [" << thread() << "]  "            "entityID:" << entityID;#endif         QMetaObject::invokeMethod(this, "unloadEntityScript",                                  Q_ARG(const EntityItemID&, entityID));        return;    }#ifdef THREAD_DEBUGGING    qDebug() << "ScriptEngine::unloadEntityScript() called on correct thread [" << thread() << "]  "        "entityID:" << entityID;#endif     if (_entityScripts.contains(entityID)) {        callEntityScriptMethod(entityID, "unload");        _entityScripts.remove(entityID);        stopAllTimersForEntityScript(entityID);    }} void ScriptEngine::unloadAllEntityScripts() {    if (QThread::currentThread() != thread()) {#ifdef THREAD_DEBUGGING        qDebug() << "*** WARNING *** ScriptEngine::unloadAllEntityScripts() called on wrong thread [" << QThread::currentThread() << "], invoking on correct thread [" << thread() << "]";#endif         QMetaObject::invokeMethod(this, "unloadAllEntityScripts");        return;    }#ifdef THREAD_DEBUGGING    qDebug() << "ScriptEngine::unloadAllEntityScripts() called on correct thread [" << thread() << "]";#endif    foreach(const EntityItemID& entityID, _entityScripts.keys()) {        callEntityScriptMethod(entityID, "unload");    }    _entityScripts.clear(); #ifdef DEBUG_ENGINE_STATE    qDebug() << "---- CURRENT STATE OF ENGINE: --------------------------";    QScriptValueIterator it(globalObject());    while (it.hasNext()) {        it.next();        qDebug() << it.name() << ":" << it.value().toString();    }    qDebug() << "--------------------------------------------------------";#endif // DEBUG_ENGINE_STATE} void ScriptEngine::refreshFileScript(const EntityItemID& entityID) {    if (!_entityScripts.contains(entityID)) {        return;    }     static bool recurseGuard = false;    if (recurseGuard) {        return;    }    recurseGuard = true;     EntityScriptDetails details = _entityScripts[entityID];    // Check to see if a file based script needs to be reloaded (easier debugging)    if (details.lastModified > 0) {        QString filePath = QUrl(details.scriptText).toLocalFile();        auto lastModified = QFileInfo(filePath).lastModified().toMSecsSinceEpoch();        if (lastModified > details.lastModified) {            qCDebug(scriptengine) << "Reloading modified script " << details.scriptText;             QFile file(filePath);            file.open(QIODevice::ReadOnly);            QString scriptContents = QTextStream(&file).readAll();            this->unloadEntityScript(entityID);            this->entityScriptContentAvailable(entityID, details.scriptText, scriptContents, true, true);            if (!_entityScripts.contains(entityID)) {                qWarning() << "Reload script " << details.scriptText << " failed";            } else {                details = _entityScripts[entityID];            }        }    }    recurseGuard = false;} // Execute operation in the appropriate context for (the possibly empty) entityID.// Even if entityID is supplied as currentEntityIdentifier, this still documents the source// of the code being executed (e.g., if we ever sandbox different entity scripts, or provide different// global values for different entity scripts).void ScriptEngine::doWithEnvironment(const EntityItemID& entityID, const QUrl& sandboxURL, std::function<void()> operation) {    EntityItemID oldIdentifier = currentEntityIdentifier;    QUrl oldSandboxURL = currentSandboxURL;    currentEntityIdentifier = entityID;    currentSandboxURL = sandboxURL; #if DEBUG_CURRENT_ENTITY    QScriptValue oldData = this->globalObject().property("debugEntityID");    this->globalObject().setProperty("debugEntityID", entityID.toScriptValue(this)); // Make the entityID available to javascript as a global.    operation();    this->globalObject().setProperty("debugEntityID", oldData);#else    operation();#endif     currentEntityIdentifier = oldIdentifier;    currentSandboxURL = oldSandboxURL;}void ScriptEngine::callWithEnvironment(const EntityItemID& entityID, const QUrl& sandboxURL, QScriptValue function, QScriptValue thisObject, QScriptValueList args) {    auto operation = [&]() {        function.call(thisObject, args);    };    doWithEnvironment(entityID, sandboxURL, operation);} void ScriptEngine::callEntityScriptMethod(const EntityItemID& entityID, const QString& methodName, const QStringList& params) {    if (QThread::currentThread() != thread()) {#ifdef THREAD_DEBUGGING        qDebug() << "*** WARNING *** ScriptEngine::callEntityScriptMethod() called on wrong thread [" << QThread::currentThread() << "], invoking on correct thread [" << thread() << "]  "            "entityID:" << entityID << "methodName:" << methodName;#endif         QMetaObject::invokeMethod(this, "callEntityScriptMethod",                                  Q_ARG(const EntityItemID&, entityID),                                  Q_ARG(const QString&, methodName),                                  Q_ARG(const QStringList&, params));        return;    }#ifdef THREAD_DEBUGGING    qDebug() << "ScriptEngine::callEntityScriptMethod() called on correct thread [" << thread() << "]  "        "entityID:" << entityID << "methodName:" << methodName;#endif     refreshFileScript(entityID);    if (_entityScripts.contains(entityID)) {        EntityScriptDetails details = _entityScripts[entityID];        QScriptValue entityScript = details.scriptObject; // previously loaded

Lines 1001–1500 of 1574. Previous lines