2763 lines · 116.2 KB · C++
RawDownload
        return;    }    PROFILE_RANGE(script, __FUNCTION__);     QSharedPointer<ScriptEngines> scriptEngines(_scriptEngines);    if (isStopping() || !scriptEngines || scriptEngines->isStopped()) {        qCDebug(scriptengine) << "loadEntityScript.start " << entityID.toString()                                     << " but isStopping==" << isStopping()                                     << " || engines->isStopped==" << scriptEngines->isStopped();        return;    }     if (!hasEntityScriptDetails(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...");    } #ifdef DEBUG_ENTITY_STATES    {        EntityScriptDetails details;        bool hasEntityScript = getEntityScriptDetails(entityID, details);        qCDebug(scriptengine) << "loadEntityScript.LOADING: " << entityID.toString()            << "(previous: " << (hasEntityScript ? details.status : EntityScriptStatus::PENDING) << ")";    }#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    std::weak_ptr<ScriptManager> weakRef(shared_from_this());    scriptCache->getScriptContents(entityScript,        [this, weakRef, entityScript, entityID](const QString& url, const QString& contents, bool isURL, bool success, const QString& status) {            std::shared_ptr<ScriptManager> strongRef(weakRef);            if (!strongRef) {                qCWarning(scriptengine) << "loadEntityScript.contentAvailable -- ScriptManager 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 << entityID.toString();#endif                if (!isStopping() && hasEntityScriptDetails(entityID)) {                    _contentAvailableQueue[entityID] = { entityID, url, contents, isURL, success, status };                } else {#ifdef DEBUG_ENTITY_STATES                    qCDebug(scriptengine) << "loadEntityScript.contentAvailable -- aborting";#endif                }            });    }, forceRedownload);} // The JSDoc is for the callEntityScriptMethod() call in this method.// since all of these operations can be asynch we will always do the actual work in the response handler// for the downloadvoid ScriptManager::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 *** ScriptManager::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) << "ScriptManager::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";     QString entityScript;    {        QWriteLocker locker { &_entityScriptsLock };        entityScript = _entityScripts[entityID].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);    };     // NETWORK / FILESYSTEM ERRORS    if (!success) {        setError("Failed to load script (" + status + ")", EntityScriptStatus::ERROR_LOADING_SCRIPT);        return;    }     // SYNTAX ERRORS    //auto syntaxError = _engine->lintScript(contents, fileName);    auto program = _engine->newProgram( contents, fileName );    auto syntaxCheck = program->checkSyntax();    if (syntaxCheck->state() != ScriptSyntaxCheckResult::Valid) {        auto message = syntaxCheck->errorMessage();        //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());        //V8TODO        //emit unhandledException(syntaxError);         return;    }    if (!program) {        setError("Bad program (isNull)", EntityScriptStatus::ERROR_RUNNING_SCRIPT);        std::shared_ptr<ScriptException> ex = std::make_shared<ScriptEngineException>("Program is Null", "Bad program in entityScriptContentAvailable");        emit unhandledException(ex);         return; // done processing script    }     if (isURL) {        setParentURL(scriptOrURL);    }     // SANITY/PERFORMANCE CHECK USING SANDBOX    // V8TODO: can be skipped for now should we implement it before merging V8 branch?     /*const int SANDBOX_TIMEOUT = 0.25 * MSECS_PER_SECOND;    ScriptEnginePointer sandbox = newScriptEngine();    sandbox->setProcessEventsInterval(SANDBOX_TIMEOUT);    ScriptValue testConstructor, exception;    if (atoi(getenv("UNSAFE_ENTITY_SCRIPTS") ? getenv("UNSAFE_ENTITY_SCRIPTS") : "0"))    {        QTimer timeout;        timeout.setSingleShot(true);        timeout.start(SANDBOX_TIMEOUT);        connect(&timeout, &QTimer::timeout, [=, &sandbox]{                qCDebug(scriptengine) << "ScriptManager::entityScriptContentAvailable timeout";                 // Guard against infinite loops and non-performant code                sandbox->raiseException(                    sandbox->makeError(sandbox->newValue(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;        }    } else {        // ENTITY SCRIPT WHITELIST STARTS HERE        auto nodeList = DependencyManager::get<NodeList>();        bool passList = false;  // assume unsafe        QString whitelistPrefix = "[WHITELIST ENTITY SCRIPTS]";        QList<QString> safeURLPrefixes = { "file:///", "atp:", "cache:" };        safeURLPrefixes += qEnvironmentVariable("EXTRA_WHITELIST").trimmed().split(QRegExp("\\s*,\\s*"), Qt::SkipEmptyParts);         // Entity Script Whitelist toggle check.        Setting::Handle<bool> whitelistEnabled {"private/whitelistEnabled", false };         if (!whitelistEnabled.get()) {            passList = true;        }         // Pull SAFEURLS from the Interface.JSON settings.        QVariant raw = Setting::Handle<QVariant>("private/settingsSafeURLS").get();        QStringList settingsSafeURLS = raw.toString().trimmed().split(QRegExp("\\s*[,\r\n]+\\s*"), Qt::SkipEmptyParts);        safeURLPrefixes += settingsSafeURLS;        // END Pull SAFEURLS from the Interface.JSON settings.         // Get current domain whitelist bypass, in case an entire domain is whitelisted.        QString currentDomain = DependencyManager::get<AddressManager>()->getDomainURL().host();         QString domainSafeIP = nodeList->getDomainHandler().getHostname();        QString domainSafeURL = URL_SCHEME_OVERTE + "://" + currentDomain;        for (const auto& str : safeURLPrefixes) {            if (domainSafeURL.startsWith(str) || domainSafeIP.startsWith(str)) {                qCDebug(scriptengine) << whitelistPrefix << "Whitelist Bypassed, entire domain is whitelisted. Current Domain Host: "                    << nodeList->getDomainHandler().getHostname()                    << "Current Domain: " << currentDomain;                passList = true;            }        }        // END bypass whitelist based on current domain.         // Start processing scripts through the whitelist.        if (ScriptManager::getContext() == "entity_server") { // If running on the server, do not engage whitelist.            passList = true;        } else if (!passList) { // If waved through, do not engage whitelist.            for (const auto& str : safeURLPrefixes) {                qCDebug(scriptengine) << whitelistPrefix << "Script URL: " << scriptOrURL << "TESTING AGAINST" << str << "RESULTS IN"                    << scriptOrURL.startsWith(str);                if (!str.isEmpty() && scriptOrURL.startsWith(str)) {                    passList = true;                    qCDebug(scriptengine) << whitelistPrefix << "Script approved.";                    break; // Bail early since we found a match.                }            }        }        // END processing of scripts through the whitelist.         if (!passList) { // If the entity failed to pass for any reason, it's blocked and an error is thrown.            qCDebug(scriptengine) << whitelistPrefix << "(disabled entity script)" << entityID.toString() << scriptOrURL;            exception = _engine->makeError(_engine->newValue("UNSAFE_ENTITY_SCRIPTS == 0"));        } else {            QTimer timeout;            timeout.setSingleShot(true);            timeout.start(SANDBOX_TIMEOUT);            connect(&timeout, &QTimer::timeout, [=, &sandbox] {                qCDebug(scriptengine) << "ScriptManager::entityScriptContentAvailable timeout";                 // Guard against infinite loops and non-performant code                sandbox->raiseException(                    sandbox->makeError(sandbox->newValue(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;            }        }      // ENTITY SCRIPT WHITELIST ENDS HERE, uncomment below for original full disabling.       // qCDebug(scriptengine) << "(disabled entity script)" << entityID.toString() << scriptOrURL;      // exception = makeError("UNSAFE_ENTITY_SCRIPTS == 0");    }     if (exception.isError()) {      // create a local copy using makeError to decouple from the sandbox engine      exception = _engine->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 = _engine->makeError(_engine->newValue(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    }*/     // ENTITY SCRIPT WHITELIST STARTS HERE    auto nodeList = DependencyManager::get<NodeList>();    bool passList = false;  // assume unsafe    QString whitelistPrefix = "[WHITELIST ENTITY SCRIPTS]";    QList<QString> safeURLPrefixes = { "file:///", "atp:", "cache:" };    safeURLPrefixes += qEnvironmentVariable("EXTRA_WHITELIST").trimmed().split(QRegExp("\\s*,\\s*"), Qt::SkipEmptyParts);     // Entity Script Whitelist toggle check.    Setting::Handle<bool> whitelistEnabled {"private/whitelistEnabled", false };     if (!whitelistEnabled.get()) {        passList = true;    }     // Pull SAFEURLS from the Interface.JSON settings.    QVariant raw = Setting::Handle<QVariant>("private/settingsSafeURLS").get();    QStringList settingsSafeURLS = raw.toString().trimmed().split(QRegExp("\\s*[,\r\n]+\\s*"), Qt::SkipEmptyParts);    safeURLPrefixes += settingsSafeURLS;    // END Pull SAFEURLS from the Interface.JSON settings.     // Get current domain whitelist bypass, in case an entire domain is whitelisted.    QString currentDomain = DependencyManager::get<AddressManager>()->getDomainURL().host();     QString domainSafeIP = nodeList->getDomainHandler().getHostname();    QString domainSafeURL = URL_SCHEME_OVERTE + "://" + currentDomain;    for (const auto& str : safeURLPrefixes) {        if (domainSafeURL.startsWith(str) || domainSafeIP.startsWith(str)) {            qCDebug(scriptengine) << whitelistPrefix << "Whitelist Bypassed, entire domain is whitelisted. Current Domain Host: "                                  << nodeList->getDomainHandler().getHostname()                                  << "Current Domain: " << currentDomain;            passList = true;        }    }    // END bypass whitelist based on current domain.     // Start processing scripts through the whitelist.    if (ScriptManager::getContext() == "entity_server") { // If running on the server, do not engage whitelist.        passList = true;    } else if (!passList) { // If waved through, do not engage whitelist.        for (const auto& str : safeURLPrefixes) {            qCDebug(scriptengine) << whitelistPrefix << "Script URL: " << scriptOrURL << "TESTING AGAINST" << str << "RESULTS IN"                                  << scriptOrURL.startsWith(str);            if (!str.isEmpty() && scriptOrURL.startsWith(str)) {                passList = true;                qCDebug(scriptengine) << whitelistPrefix << "Script approved.";                break; // Bail early since we found a match.            }        }    }    // END processing of scripts through the whitelist.     ScriptValue exception;    if (!passList) { // If the entity failed to pass for any reason, it's blocked and an error is thrown.        qCDebug(scriptengine) << whitelistPrefix << "(disabled entity script)" << entityID.toString() << scriptOrURL;        exception = _engine->makeError(_engine->newValue("UNSAFE_ENTITY_SCRIPTS == 0"));    }     if (exception.isError()) {        setError(formatException(exception, _enableExtendedJSExceptions.get()), EntityScriptStatus::ERROR_RUNNING_SCRIPT);        auto scriptRuntimeException = std::make_shared<ScriptRuntimeException>();        ScriptValue message = exception.property("stack"); //This contains more details along with the error message        scriptRuntimeException->errorMessage = message.toString();        scriptRuntimeException->thrownValue = exception;        emit unhandledException(scriptRuntimeException);        return;    }    // ENTITY SCRIPT WHITELIST ENDS HERE, uncomment below for original full disabling.     // qCDebug(scriptengine) << "(disabled entity script)" << entityID.toString() << scriptOrURL;    // exception = makeError("UNSAFE_ENTITY_SCRIPTS == 0");     // (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    ScriptValue entityScriptConstructor, entityScriptObject;    QUrl sandboxURL = currentSandboxURL.isEmpty() ? scriptOrURL : currentSandboxURL;    auto initialization = [&]{        entityScriptConstructor = _engine->evaluate(contents, fileName);        //V8TODO: check if entityScriptConstructor is a function or not        //Throw V8 exception if it's not?        entityScriptObject = entityScriptConstructor.construct();         if (_engine->hasUncaughtException()) {            // V8TODO: Why were we copying the uncaught exception here? Does anything            // actually make use of that?            //  entityScriptObject = _engine->cloneUncaughtException("(construct " + entityID.toString() + ")");            entityScriptObject = _engine->nullValue();            _engine->clearExceptions();        }    };     doWithEnvironment(entityID, sandboxURL, initialization);     if (entityScriptObject.isError()) {       // auto exception = entityScriptObject;       // setError(formatException(exception, _enableExtendedJSExceptions.get()), EntityScriptStatus::ERROR_RUNNING_SCRIPT);       // emit unhandledException(exception);       // V8TODO: Is this needed? Wouldn't the ScriptManager have already emitted the 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");     emit entityScriptPreloadFinished(entityID);} /*@jsdoc * Triggered when the script terminates for a user. * <p>Note: Can only be connected to via <code>this.unoad = function () { ... }</code> in the entity script.</p> * <p class="availableIn"><strong>Supported Script Types:</strong> Client Entity Scripts &bull; Server Entity Scripts</p> * @function Entities.unload * @param {Uuid} entityID - The ID of the entity that the script is running in. * @returns {Signal} */// The JSDoc is for the callEntityScriptMethod() call in this method.void ScriptManager::unloadEntityScript(const EntityItemID& entityID, bool shouldRemoveFromMap) {    if (QThread::currentThread() != thread()) {#ifdef THREAD_DEBUGGING        qCDebug(scriptengine) << "*** WARNING *** ScriptManager::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) << "ScriptManager::unloadEntityScript() called on correct thread [" << thread() << "]  "        "entityID:" << entityID;#endif     EntityScriptDetails oldDetails;    if (getEntityScriptDetails(entityID, oldDetails)) {        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            {                QWriteLocker locker { &_entityScriptsLock };                _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);    }} QList<EntityItemID> ScriptManager::getListOfEntityScriptIDs() {    QReadLocker locker{ &_entityScriptsLock };    return _entityScripts.keys();} void ScriptManager::unloadAllEntityScripts(bool blockingCall) {    if (QThread::currentThread() != thread()) {#ifdef THREAD_DEBUGGING        qCDebug(scriptengine) << "*** WARNING *** ScriptManager::unloadAllEntityScripts() called on wrong thread [" << QThread::currentThread() << "], invoking on correct thread [" << thread() << "]";#endif         QMetaObject::invokeMethod(this, "unloadAllEntityScripts",            blockingCall ? Qt::BlockingQueuedConnection : Qt::QueuedConnection);        return;    }#ifdef THREAD_DEBUGGING    qCDebug(scriptengine) << "ScriptManager::unloadAllEntityScripts() called on correct thread [" << thread() << "]";#endif     QList<EntityItemID> keys;    {        QReadLocker locker{ &_entityScriptsLock };        keys = _entityScripts.keys();    }    foreach(const EntityItemID& entityID, keys) {        unloadEntityScript(entityID);    }    {        QWriteLocker locker{ &_entityScriptsLock };        _entityScripts.clear();    }    emit entityScriptDetailsUpdated();

Lines 2001–2500 of 2763. Previous lines