2551 lines · 109.7 KB · C++
RawDownload
        const auto leaderDetails = _entityScripts[leaderID];        if (leaderDetails.status != EntityScriptStatus::RUNNING) {            qCDebug(scriptengine) << QString("... pending load of %1 cancelled (leader: %2 status: %3)")                .arg(retry.entityID.toString()).arg(leaderID.toString()).arg(leaderDetails.status);             auto extraDetail = QString("\n(propagated from %1)").arg(leaderID.toString());            if (leaderDetails.status == EntityScriptStatus::ERROR_LOADING_SCRIPT ||                leaderDetails.status == EntityScriptStatus::ERROR_RUNNING_SCRIPT) {                // propagate same error so User doesn't have to hunt down stampede's leader                updateEntityScriptStatus(retry.entityID, leaderDetails.status, leaderDetails.errorInfo + extraDetail);            } else {                // the leader Entity somehow ended up in some other state (rapid-fire delete or unload could cause)                updateEntityScriptStatus(retry.entityID, EntityScriptStatus::ERROR_LOADING_SCRIPT,                    "A previous Entity failed to load using this script URL; reload to try again." + extraDetail);            }            continue;        }         if (_occupiedScriptURLs.contains(retry.entityScript)) {            qCWarning(scriptengine) << "--- SHOULD NOT HAPPEN -- recursive call into processDeferredEntityLoads" << retry.entityScript;            continue;        }         // if we made it here then the leading entity was successful so proceed with normal load        loadEntityScript(retry.entityID, retry.entityScript, false);    }} void ScriptEngine::loadEntityScript(const EntityItemID& entityID, const QString& entityScript, bool forceRedownload) {    if (QThread::currentThread() != thread()) {        QMetaObject::invokeMethod(this, "loadEntityScript",            Q_ARG(const EntityItemID&, entityID),            Q_ARG(const QString&, entityScript),            Q_ARG(bool, forceRedownload)        );        return;    }    PROFILE_RANGE(script, __FUNCTION__);     if (isStopping() || DependencyManager::get<ScriptEngines>()->isStopped()) {        qCDebug(scriptengine) << "loadEntityScript.start " << entityScript << entityID.toString()                                     << " but isStopping==" << isStopping()                                     << " || engines->isStopped==" << DependencyManager::get<ScriptEngines>()->isStopped();        return;    }     if (!_entityScripts.contains(entityID)) {        // make sure EntityScriptDetails has an entry for this UUID right away        // (which allows bailing from the loading/provisioning process early if the Entity gets deleted mid-flight)        updateEntityScriptStatus(entityID, EntityScriptStatus::PENDING, "...pending...");    }     // This "occupied" approach allows multiple Entities to boot from the same script URL while still taking    // full advantage of cacheable require modules.  This only affects Entities literally coming in back-to-back    // before the first one has time to finish loading.    if (_occupiedScriptURLs.contains(entityScript)) {        auto currentEntityID = _occupiedScriptURLs[entityScript];        if (currentEntityID == BAD_SCRIPT_UUID_PLACEHOLDER) {            if (forceRedownload) {                // script was previously marked unusable, but we're reloading so reset it                _occupiedScriptURLs.remove(entityScript);            } else {                // since not reloading, assume that the exact same input would produce the exact same output again                // note: this state gets reset with "reload all scripts," leaving/returning to a Domain, clear cache, etc.#ifdef DEBUG_ENTITY_STATES                qCDebug(scriptengine) << QString("loadEntityScript.cancelled entity: %1 script: %2 (previous script failure)")                    .arg(entityID.toString()).arg(entityScript);#endif                updateEntityScriptStatus(entityID, EntityScriptStatus::ERROR_LOADING_SCRIPT,                                         "A previous Entity failed to load using this script URL; reload to try again.");                return;            }        } else {            // another entity is busy loading from this script URL so wait for them to finish#ifdef DEBUG_ENTITY_STATES            qCDebug(scriptengine) << QString("loadEntityScript.deferring[%0] entity: %1 script: %2 (waiting on %3)")                .arg(_deferredEntityLoads.size()).arg(entityID.toString()).arg(entityScript).arg(currentEntityID.toString());#endif            _deferredEntityLoads.push_back({ entityID, entityScript });            return;        }    }     // the scriptURL slot is available; flag as in-use    _occupiedScriptURLs[entityScript] = entityID; #ifdef DEBUG_ENTITY_STATES    auto previousStatus = _entityScripts.contains(entityID) ? _entityScripts[entityID].status : EntityScriptStatus::PENDING;    qCDebug(scriptengine) << "loadEntityScript.LOADING: " << entityScript << entityID.toString()                                 << "(previous: " << previousStatus << ")";#endif     EntityScriptDetails newDetails;    newDetails.scriptText = entityScript;    newDetails.status = EntityScriptStatus::LOADING;    newDetails.definingSandboxURL = currentSandboxURL;    setEntityScriptDetails(entityID, newDetails);     auto scriptCache = DependencyManager::get<ScriptCache>();    // note: see EntityTreeRenderer.cpp for shared pointer lifecycle management    QWeakPointer<BaseScriptEngine> weakRef(sharedFromThis());    scriptCache->getScriptContents(entityScript,        [this, weakRef, entityScript, entityID](const QString& url, const QString& contents, bool isURL, bool success, const QString& status) {            QSharedPointer<BaseScriptEngine> strongRef(weakRef);            if (!strongRef) {                qCWarning(scriptengine) << "loadEntityScript.contentAvailable -- ScriptEngine was deleted during getScriptContents!!";                return;            }            if (isStopping()) {#ifdef DEBUG_ENTITY_STATES                qCDebug(scriptengine) << "loadEntityScript.contentAvailable -- stopping";#endif                return;            }            executeOnScriptThread([=]{#ifdef DEBUG_ENTITY_STATES                qCDebug(scriptengine) << "loadEntityScript.contentAvailable" << status << QUrl(url).fileName() << entityID.toString();#endif                if (!isStopping() && _entityScripts.contains(entityID)) {                    entityScriptContentAvailable(entityID, url, contents, isURL, success, status);                } else {#ifdef DEBUG_ENTITY_STATES                    qCDebug(scriptengine) << "loadEntityScript.contentAvailable -- aborting";#endif                }                // recheck whether us since may have been set to BAD_SCRIPT_UUID_PLACEHOLDER in entityScriptContentAvailable                if (_occupiedScriptURLs.contains(entityScript) && _occupiedScriptURLs[entityScript] == entityID) {                    _occupiedScriptURLs.remove(entityScript);                }            });    }, 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 , const QString& status) {    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),                                  Q_ARG(const QString&, status));        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 : "about:EmbeddedEntityScript";     const EntityScriptDetails &oldDetails = _entityScripts[entityID];    const QString entityScript = oldDetails.scriptText;     EntityScriptDetails newDetails;    newDetails.scriptText = scriptOrURL;     // If an error happens below, we want to update newDetails with the new status info    // and also abort any pending Entity loads that are waiting on the exact same script URL.    auto setError = [&](const QString &errorInfo, const EntityScriptStatus& status) {        newDetails.errorInfo = errorInfo;        newDetails.status = status;        setEntityScriptDetails(entityID, newDetails); #ifdef DEBUG_ENTITY_STATES        qCDebug(scriptengine) << "entityScriptContentAvailable -- flagging " << entityScript << " as BAD_SCRIPT_UUID_PLACEHOLDER";#endif        // flag the original entityScript as unusuable        _occupiedScriptURLs[entityScript] = BAD_SCRIPT_UUID_PLACEHOLDER;        processDeferredEntityLoads(entityScript, entityID);    };     // NETWORK / FILESYSTEM ERRORS    if (!success) {        setError("Failed to load script (" + status + ")", EntityScriptStatus::ERROR_LOADING_SCRIPT);        return;    }     // SYNTAX ERRORS    auto syntaxError = lintScript(contents, fileName);    if (syntaxError.isError()) {        auto message = syntaxError.property("formatted").toString();        if (message.isEmpty()) {            message = syntaxError.toString();        }        setError(QString("Bad syntax (%1)").arg(message), EntityScriptStatus::ERROR_RUNNING_SCRIPT);        syntaxError.setProperty("detail", entityID.toString());        emit unhandledException(syntaxError);        return;    }    QScriptProgram program { contents, fileName };    if (program.isNull()) {        setError("Bad program (isNull)", EntityScriptStatus::ERROR_RUNNING_SCRIPT);        emit unhandledException(makeError("program.isNull"));        return; // done processing script    }     if (isURL) {        setParentURL(scriptOrURL);    }     // SANITY/PERFORMANCE CHECK USING SANDBOX    const int SANDBOX_TIMEOUT = 0.25 * MSECS_PER_SECOND;    BaseScriptEngine sandbox;    sandbox.setProcessEventsInterval(SANDBOX_TIMEOUT);    QScriptValue testConstructor, exception;    {        QTimer timeout;        timeout.setSingleShot(true);        timeout.start(SANDBOX_TIMEOUT);        connect(&timeout, &QTimer::timeout, [&sandbox, SANDBOX_TIMEOUT, scriptOrURL]{                qCDebug(scriptengine) << "ScriptEngine::entityScriptContentAvailable timeout(" << scriptOrURL << ")";                 // Guard against infinite loops and non-performant code                sandbox.raiseException(                    sandbox.makeError(QString("Timed out (entity constructors are limited to %1ms)").arg(SANDBOX_TIMEOUT))                );        });         testConstructor = sandbox.evaluate(program);         if (sandbox.hasUncaughtException()) {            exception = sandbox.cloneUncaughtException(QString("(preflight %1)").arg(entityID.toString()));            sandbox.clearExceptions();        } else if (testConstructor.isError()) {            exception = testConstructor;        }    }     if (exception.isError()) {        // create a local copy using makeError to decouple from the sandbox engine        exception = makeError(exception);        setError(formatException(exception, _enableExtendedJSExceptions.get()), EntityScriptStatus::ERROR_RUNNING_SCRIPT);        emit unhandledException(exception);        return;    }     // CONSTRUCTOR VIABILITY    if (!testConstructor.isFunction()) {        QString testConstructorType = QString(testConstructor.toVariant().typeName());        if (testConstructorType == "") {            testConstructorType = "empty";        }        QString testConstructorValue = testConstructor.toString();        if (testConstructorValue.size() > MAX_DEBUG_VALUE_LENGTH) {            testConstructorValue = testConstructorValue.mid(0, MAX_DEBUG_VALUE_LENGTH) + "...";        }        auto message = QString("failed to load entity script -- expected a function, got %1, %2")            .arg(testConstructorType).arg(testConstructorValue);         auto err = makeError(message);        err.setProperty("fileName", scriptOrURL);        err.setProperty("detail", "(constructor " + entityID.toString() + ")");         setError("Could not find constructor (" + testConstructorType + ")", EntityScriptStatus::ERROR_RUNNING_SCRIPT);        emit unhandledException(err);        return; // done processing script    }     // (this feeds into refreshFileScript)    int64_t lastModified = 0;    if (isFileUrl) {        QString file = QUrl(scriptOrURL).toLocalFile();        lastModified = (quint64)QFileInfo(file).lastModified().toMSecsSinceEpoch();    }     // THE ACTUAL EVALUATION AND CONSTRUCTION    QScriptValue entityScriptConstructor, entityScriptObject;    QUrl sandboxURL = currentSandboxURL.isEmpty() ? scriptOrURL : currentSandboxURL;    auto initialization = [&]{        entityScriptConstructor = evaluate(contents, fileName);        entityScriptObject = entityScriptConstructor.construct();         if (hasUncaughtException()) {            entityScriptObject = cloneUncaughtException("(construct " + entityID.toString() + ")");            clearExceptions();        }    };     doWithEnvironment(entityID, sandboxURL, initialization);     if (entityScriptObject.isError()) {        auto exception = entityScriptObject;        setError(formatException(exception, _enableExtendedJSExceptions.get()), EntityScriptStatus::ERROR_RUNNING_SCRIPT);        emit unhandledException(exception);        return;    }     // ... AND WE HAVE LIFTOFF    newDetails.status = EntityScriptStatus::RUNNING;    newDetails.scriptObject = entityScriptObject;    newDetails.lastModified = lastModified;    newDetails.definingSandboxURL = sandboxURL;    setEntityScriptDetails(entityID, newDetails);     if (isURL) {        setParentURL("");    }     // if we got this far, then call the preload method    callEntityScriptMethod(entityID, "preload");     _occupiedScriptURLs.remove(entityScript);    processDeferredEntityLoads(entityScript, entityID);} void ScriptEngine::unloadEntityScript(const EntityItemID& entityID, bool shouldRemoveFromMap) {    if (QThread::currentThread() != thread()) {#ifdef THREAD_DEBUGGING        qCDebug(scriptengine) << "*** 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),                                  Q_ARG(bool, shouldRemoveFromMap));        return;    }#ifdef THREAD_DEBUGGING    qCDebug(scriptengine) << "ScriptEngine::unloadEntityScript() called on correct thread [" << thread() << "]  "        "entityID:" << entityID;#endif     if (_entityScripts.contains(entityID)) {        const EntityScriptDetails &oldDetails = _entityScripts[entityID];        auto scriptText = oldDetails.scriptText;         if (isEntityScriptRunning(entityID)) {            callEntityScriptMethod(entityID, "unload");        }#ifdef DEBUG_ENTITY_STATES        else {            qCDebug(scriptengine) << "unload called while !running" << entityID << oldDetails.status;        }#endif        if (shouldRemoveFromMap) {            // this was a deleted entity, we've been asked to remove it from the map            _entityScripts.remove(entityID);            emit entityScriptDetailsUpdated();        } else if (oldDetails.status != EntityScriptStatus::UNLOADED) {            EntityScriptDetails newDetails;            newDetails.status = EntityScriptStatus::UNLOADED;            newDetails.lastModified = QDateTime::currentMSecsSinceEpoch();            // keep scriptText populated for the current need to "debouce" duplicate calls to unloadEntityScript            newDetails.scriptText = scriptText;            setEntityScriptDetails(entityID, newDetails);        }         stopAllTimersForEntityScript(entityID);        {            // FIXME: shouldn't have to do this here, but currently something seems to be firing unloads moments after firing initial load requests            processDeferredEntityLoads(scriptText, entityID);        }    }} void ScriptEngine::unloadAllEntityScripts() {    if (QThread::currentThread() != thread()) {#ifdef THREAD_DEBUGGING        qCDebug(scriptengine) << "*** WARNING *** ScriptEngine::unloadAllEntityScripts() called on wrong thread [" << QThread::currentThread() << "], invoking on correct thread [" << thread() << "]";#endif         QMetaObject::invokeMethod(this, "unloadAllEntityScripts");        return;    }#ifdef THREAD_DEBUGGING    qCDebug(scriptengine) << "ScriptEngine::unloadAllEntityScripts() called on correct thread [" << thread() << "]";#endif    foreach(const EntityItemID& entityID, _entityScripts.keys()) {        unloadEntityScript(entityID);    }    _entityScripts.clear();    emit entityScriptDetailsUpdated();    _occupiedScriptURLs.clear(); #ifdef DEBUG_ENGINE_STATE    _debugDump(        "---- CURRENT STATE OF ENGINE: --------------------------",        globalObject(),        "--------------------------------------------------------"    );#endif // DEBUG_ENGINE_STATE} void ScriptEngine::refreshFileScript(const EntityItemID& entityID) {    if (!HIFI_AUTOREFRESH_FILE_SCRIPTS || !_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) {            scriptInfoMessage("Reloading modified script " + details.scriptText);            loadEntityScript(entityID, details.scriptText, true);        }    }    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    maybeEmitUncaughtException(!entityID.isNull() ? entityID.toString() : __FUNCTION__);    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        qCDebug(scriptengine) << "*** 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    qCDebug(scriptengine) << "ScriptEngine::callEntityScriptMethod() called on correct thread [" << thread() << "]  "        "entityID:" << entityID << "methodName:" << methodName;#endif     if (HIFI_AUTOREFRESH_FILE_SCRIPTS && methodName != "unload") {        refreshFileScript(entityID);    }    if (isEntityScriptRunning(entityID)) {        EntityScriptDetails details = _entityScripts[entityID];        QScriptValue entityScript = details.scriptObject; // previously loaded        if (entityScript.property(methodName).isFunction()) {            QScriptValueList args;            args << entityID.toScriptValue(this);            args << qScriptValueFromSequence(this, params);            callWithEnvironment(entityID, details.definingSandboxURL, entityScript.property(methodName), entityScript, args);        }     }} void ScriptEngine::callEntityScriptMethod(const EntityItemID& entityID, const QString& methodName, const PointerEvent& event) {    if (QThread::currentThread() != thread()) {#ifdef THREAD_DEBUGGING        qCDebug(scriptengine) << "*** WARNING *** ScriptEngine::callEntityScriptMethod() called on wrong thread [" << QThread::currentThread() << "], invoking on correct thread [" << thread() << "]  "            "entityID:" << entityID << "methodName:" << methodName << "event: mouseEvent";#endif         QMetaObject::invokeMethod(this, "callEntityScriptMethod",                                  Q_ARG(const EntityItemID&, entityID),                                  Q_ARG(const QString&, methodName),                                  Q_ARG(const PointerEvent&, event));        return;    }#ifdef THREAD_DEBUGGING    qCDebug(scriptengine) << "ScriptEngine::callEntityScriptMethod() called on correct thread [" << thread() << "]  "        "entityID:" << entityID << "methodName:" << methodName << "event: pointerEvent";#endif

Lines 2001–2500 of 2551. Previous lines