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";#endifLines 2001–2500 of 2551. Previous lines Load more
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 download void 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