using UrlMap = QMap<QUrl, QString>; auto scriptCache = DependencyManager::get<ScriptCache>(); QVariantMap req; qCDebug(scriptengine_module) << "require.fetchModuleSource: " << QUrl(modulePath).fileName() << QThread::currentThread(); auto onload = [=, &req](const UrlMap& data, const UrlMap& _status) { auto url = modulePath; auto status = _status[url]; auto contents = data[url]; if (isStopping()) { req["status"] = "Stopped"; req["success"] = false; } else { req["url"] = url; req["status"] = status; req["success"] = ScriptCache::isSuccessStatus(status); req["contents"] = contents; } }; if (forceDownload) { qCDebug(scriptengine_module) << "require.requestScript -- clearing cache for" << modulePath; scriptCache->deleteScript(modulePath); } BatchLoader* loader = new BatchLoader(QList<QUrl>({ modulePath })); connect(loader, &BatchLoader::finished, this, onload); connect(this, &QObject::destroyed, loader, &QObject::deleteLater); // fail faster? (since require() blocks the engine thread while resolving dependencies) const int MAX_RETRIES = 1; loader->start(MAX_RETRIES); if (!loader->isFinished()) { // This lambda can get called AFTER this local scope has completed. // This is why we pass smart ptrs to the lambda instead of references to local variables. auto monitor = std::make_shared<QTimer>(); auto loop = std::make_shared<QEventLoop>(); QObject::connect(loader, &BatchLoader::finished, this, [monitor, loop] { monitor->stop(); loop->quit(); }); // this helps detect the case where stop() is invoked during the download // but not seen in time to abort processing in onload()... connect(monitor.get(), &QTimer::timeout, this, [this, loop] { if (isStopping()) { loop->exit(-1); } }); monitor->start(500); loop->exec(); } loader->deleteLater(); return req;} // evaluate a pending module object using the fetched source codeScriptValue ScriptManager::instantiateModule(const ScriptValue& module, const QString& sourceCode) { ScriptValue result; auto modulePath = module.property("filename").toString(); auto closure = module.property("__closure__"); qCDebug(scriptengine_module) << QString("require.instantiateModule: %1 / %2 bytes") .arg(QUrl(modulePath).fileName()).arg(sourceCode.length()); if (module.property("content-type").toString() == "application/json") { qCDebug(scriptengine_module) << "... parsing as JSON"; closure.setProperty("__json", sourceCode); result = _engine->evaluateInClosure(closure, _engine->newProgram( "module.exports = JSON.parse(__json)", modulePath )); } else { // scoped vars for consistency with Node.js closure.setProperty("require", module.property("require")); closure.setProperty("__filename", modulePath, READONLY_HIDDEN_PROP_FLAGS); closure.setProperty("__dirname", QString(modulePath).replace(QRegExp("/[^/]*$"), ""), READONLY_HIDDEN_PROP_FLAGS); //_engine->scriptValueDebugDetails(module); result = _engine->evaluateInClosure(closure, _engine->newProgram( sourceCode, modulePath )); } return result;} // CommonJS/Node.js like require/module supportScriptValue ScriptManager::require(const QString& moduleId) { qCDebug(scriptengine_module) << "ScriptManager::require(" << moduleId.left(MAX_DEBUG_VALUE_LENGTH) << ")"; if (!_engine->IS_THREADSAFE_INVOCATION(__FUNCTION__)) { return _engine->nullValue(); } auto jsRequire = _engine->globalObject().property("Script").property("require"); auto cacheMeta = jsRequire.data(); auto cache = jsRequire.property("cache"); auto parent = currentModule(); auto throwModuleError = [&](const QString& modulePath, const ScriptValue& error) { cache.setProperty(modulePath, _engine->nullValue()); if (!error.isNull()) {#ifdef DEBUG_JS_MODULES qCWarning(scriptengine_module) << "throwing module error:" << error.toString() << modulePath << error.property("stack").toString();#endif _engine->raiseException(error, "module error"); } return _engine->nullValue(); }; // start by resolving the moduleId into a fully-qualified path/URL QString modulePath = _requireResolve(moduleId); if (modulePath.isNull() || _engine->hasUncaughtException()) { // the resolver already threw an exception -- bail early return _engine->nullValue(); } // check the resolved path against the cache auto module = cache.property(modulePath); // modules get cached in `Script.require.cache` and (similar to Node.js) users can access it // to inspect particular entries and invalidate them by deleting the key: // `delete Script.require.cache[Script.require.resolve(moduleId)];` // Check to see if we should invalidate the cache based on a user setting. Setting::Handle<bool> getCachebustSetting {"cachebustScriptRequire", false }; // cacheMeta is just used right now to tell deleted keys apart from undefined ones bool invalidateCache = getCachebustSetting.get() || (module.isUndefined() && cacheMeta.property(moduleId).isValid()); // reset the cacheMeta record so invalidation won't apply next time, even if the module fails to load cacheMeta.setProperty(modulePath, ScriptValue()); auto exports = module.property("exports"); if (!invalidateCache && exports.isObject()) { // we have found a cached module -- just need to possibly register it with current parent qCDebug(scriptengine_module) << QString("require - using cached module for '%1' (loaded: %2)") .arg(moduleId).arg(module.property("loaded").toString()); registerModuleWithParent(module, parent); return exports; } // bootstrap / register new empty module module = newModule(modulePath, parent); registerModuleWithParent(module, parent); // add it to the cache (this is done early so any cyclic dependencies pick up) cache.setProperty(modulePath, module); // download the module source auto req = fetchModuleSource(modulePath, invalidateCache); if (!req.contains("success") || !req["success"].toBool()) { auto error = QString("error retrieving script (%1)").arg(req["status"].toString()); return throwModuleError(modulePath, _engine->newValue(error)); } #if DEBUG_JS_MODULES qCDebug(scriptengine_module) << "require.loaded: " << QUrl(req["url"].toString()).fileName() << req["status"].toString();#endif auto sourceCode = req["contents"].toString(); if (QUrl(modulePath).fileName().endsWith(".json", Qt::CaseInsensitive)) { module.setProperty("content-type", "application/json"); } else { module.setProperty("content-type", "application/javascript"); } // evaluate the module auto result = instantiateModule(module, sourceCode); if (result.isError() && !result.strictlyEquals(module.property("exports"))) { qCWarning(scriptengine_module) << "-- result.isError --" << result.toString(); return throwModuleError(modulePath, result); } // mark as fully-loaded module.setProperty("loaded", true, READONLY_PROP_FLAGS); // set up a new reference point for detecting cache key deletion cacheMeta.setProperty(modulePath, module); //qCDebug(scriptengine_module) << "//ScriptManager::require(" << moduleId << ")"; //qCDebug(scriptengine_module) << "Exports: " << _engine->scriptValueDebugDetails(module.property("exports")); return module.property("exports");} // 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 ScriptManager::include(const QStringList& includeFiles, const ScriptValue& callback) { if (!_engine->IS_THREADSAFE_INVOCATION(__FUNCTION__)) { return; } if (isStopped()) { int lineNumber = -1; QString fileName = getFilename(); auto context = _engine->currentContext(); if (context) { lineNumber = context->currentLineNumber(); fileName = context->currentFileName(); } scriptWarningMessage("Script.include() while shutting down is ignored... includeFiles:" + includeFiles.join(",") + "parent script:" + getFilename(), fileName, lineNumber); return; // bail early } QList<QUrl> urls; for (QString includeFile : includeFiles) { QString file = DependencyManager::get<ResourceManager>()->normalizeURL(includeFile); QUrl thisURL; bool isStandardLibrary = false; if (file.startsWith("/~/")) { thisURL = expandScriptUrl(QUrl::fromLocalFile(expandScriptPath(file))); QUrl defaultScriptsLoc = PathUtils::defaultScriptsLocation(); if (!defaultScriptsLoc.isParentOf(thisURL)) { int lineNumber = -1; QString fileName = getFilename(); auto context = _engine->currentContext(); if (context) { lineNumber = context->currentLineNumber(); fileName = context->currentFileName(); } //V8TODO this probably needs to be done per context, otherwise file cannot be included again in a module scriptWarningMessage("Script.include() -- skipping" + file + "-- outside of standard libraries", fileName, lineNumber); continue; } isStandardLibrary = true; } else { thisURL = resolvePath(file); } bool disallowOutsideFiles = thisURL.isLocalFile() && !isStandardLibrary && !currentSandboxURL.isLocalFile(); if (disallowOutsideFiles && !PathUtils::isDescendantOf(thisURL, currentSandboxURL)) { int lineNumber = -1; QString fileName = currentSandboxURL.toString(); auto context = _engine->currentContext(); if (context) { lineNumber = context->currentLineNumber(); fileName = context->currentFileName(); } scriptWarningMessage("Script.include() ignoring file path" + thisURL.toString() + "outside of original entity script" + currentSandboxURL.toString(), fileName, lineNumber); } else { // We could also check here for CORS, but we don't yet. // It turns out that QUrl.resolve will not change hosts and copy authority, so we don't need to check that here. urls.append(thisURL); } } // If there are no URLs left to download, don't bother attempting to download anything and return early if (urls.size() == 0) { return; } BatchLoader* loader = new BatchLoader(urls); EntityItemID capturedEntityIdentifier = currentEntityIdentifier; QUrl capturedSandboxURL = currentSandboxURL; auto evaluateScripts = [=](const QMap<QUrl, QString>& data, const QMap<QUrl, QString>& status) { auto parentURL = _parentURL; for (QUrl url : urls) { QString contents = data[url]; if (contents.isNull()) { int lineNumber = -1; QString fileName = url.toString(); auto context = _engine->currentContext(); if (context) { lineNumber = context->currentLineNumber(); fileName = context->currentFileName(); } scriptErrorMessage("Error loading file (" + status[url] +"): " + url.toString(), fileName, lineNumber); } else { std::lock_guard<std::recursive_mutex> lock(_lock); if (!_includedURLs.contains(url)) { _includedURLs << url; // Set the parent url so that path resolution will be relative // to this script's url during its initial evaluation _parentURL = url.toString(); auto operation = [&]() { _engine->evaluate(contents, url.toString()); }; doWithEnvironment(capturedEntityIdentifier, capturedSandboxURL, operation); if(_engine->hasUncaughtException()) { auto ex = _engine->uncaughtException(); ex->additionalInfo += "; evaluateInClosure"; emit unhandledException(ex); _engine->clearExceptions(); } } else { int lineNumber = -1; QString fileName = url.toString(); auto context = _engine->currentContext(); if (context) { lineNumber = context->currentLineNumber(); fileName = context->currentFileName(); } scriptPrintedMessage("Script.include() skipping evaluation of previously included url:" + url.toString(), fileName, lineNumber); } } } _parentURL = parentURL; if (callback.isFunction()) { callWithEnvironment(capturedEntityIdentifier, capturedSandboxURL, callback, ScriptValue(), ScriptValueList()); } 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(processLevelMaxRetries); if (!callback.isFunction() && !loader->isFinished()) { QEventLoop loop; QObject::connect(loader, &BatchLoader::finished, &loop, &QEventLoop::quit); loop.exec(); }} void ScriptManager::include(const QString& includeFile, const ScriptValue& callback) { if (isStopped()) { int lineNumber = -1; QString fileName = currentSandboxURL.toString(); auto context = _engine->currentContext(); if (context) { lineNumber = context->currentLineNumber(); fileName = context->currentFileName(); } scriptWarningMessage("Script.include() while shutting down is ignored... includeFile:" + includeFile + "parent script:" + getFilename(), fileName, lineNumber); 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 ScriptManager class just emits a signal which// the Application or other context will connect to in order to know to actually load the scriptvoid ScriptManager::load(const QString& loadFile) { if (!_engine->IS_THREADSAFE_INVOCATION(__FUNCTION__)) { return; } int lineNumber = -1; QString fileName = getFilename(); auto context = _engine->currentContext(); if (context) { lineNumber = context->currentLineNumber(); fileName = context->currentFileName(); } if (isStopped()) { scriptWarningMessage("Script.load() while shutting down is ignored... loadFile:" + loadFile + "parent script:" + getFilename(), fileName, lineNumber); return; // bail early } if (!currentEntityIdentifier.isInvalidID()) { scriptWarningMessage("Script.load() from entity script is ignored... loadFile:" + loadFile + "parent script:" + getFilename() + "entity: " + currentEntityIdentifier.toString(), fileName, lineNumber); 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 ScriptManager::forwardHandlerCall(const EntityItemID& entityID, const QString& eventName, const ScriptValueList& eventHandlerArgs) { if (QThread::currentThread() != thread()) { qCDebug(scriptengine) << "*** ERROR *** ScriptManager::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, ScriptValue(), eventHandlerArgs); } }} int ScriptManager::getNumRunningEntityScripts() const { QReadLocker locker { &_entityScriptsLock }; int sum = 0; for (const auto& st : _entityScripts) { if (st.status == EntityScriptStatus::RUNNING) { ++sum; } } return sum;} void ScriptManager::setEntityScriptDetails(const EntityItemID& entityID, const EntityScriptDetails& details) { { QWriteLocker locker { &_entityScriptsLock }; _entityScripts[entityID] = details; } emit entityScriptDetailsUpdated();} void ScriptManager::updateEntityScriptStatus(const EntityItemID& entityID, const EntityScriptStatus &status, const QString& errorInfo) { { QWriteLocker locker { &_entityScriptsLock }; EntityScriptDetails& details = _entityScripts[entityID]; details.status = status; details.errorInfo = errorInfo; } emit entityScriptDetailsUpdated();} QVariant ScriptManager::cloneEntityScriptDetails(const EntityItemID& entityID) { static const QVariant NULL_VARIANT = QVariant::fromValue(nullptr); QVariantMap map; if (entityID.isNull()) { // TODO: find better way to report JS Error across thread/process boundaries map["isError"] = true; map["errorInfo"] = "Error: getEntityScriptDetails -- invalid entityID"; } else {#ifdef DEBUG_ENTITY_STATES qCDebug(scriptengine) << "cloneEntityScriptDetails" << entityID << QThread::currentThread();#endif EntityScriptDetails scriptDetails; if (getEntityScriptDetails(entityID, scriptDetails)) {#ifdef DEBUG_ENTITY_STATES qCDebug(scriptengine) << "gotEntityScriptDetails" << scriptDetails.status << QThread::currentThread();#endif map["isRunning"] = isEntityScriptRunning(entityID); map["status"] = EntityScriptStatus_::valueToKey(scriptDetails.status).toLower(); map["errorInfo"] = scriptDetails.errorInfo; map["entityID"] = entityID.toString();#ifdef DEBUG_ENTITY_STATES { auto debug = QVariantMap(); debug["script"] = scriptDetails.scriptText; debug["scriptObject"] = scriptDetails.scriptObject.toVariant(); debug["lastModified"] = (qlonglong)scriptDetails.lastModified; debug["sandboxURL"] = scriptDetails.definingSandboxURL; map["debug"] = debug; }#endif } else {#ifdef DEBUG_ENTITY_STATES qCDebug(scriptengine) << "!gotEntityScriptDetails" << QThread::currentThread();#endif map["isError"] = true; map["errorInfo"] = "Entity script details unavailable"; map["entityID"] = entityID.toString(); } } return map;} QFuture<QVariant> ScriptManager::getLocalEntityScriptDetails(const EntityItemID& entityID) { return QtConcurrent::run(this, &ScriptManager::cloneEntityScriptDetails, entityID);} bool ScriptManager::getEntityScriptDetails(const EntityItemID& entityID, EntityScriptDetails &details) const { QReadLocker locker { &_entityScriptsLock }; auto it = _entityScripts.constFind(entityID); if (it == _entityScripts.constEnd()) { return false; } details = it.value(); return true;} bool ScriptManager::hasEntityScriptDetails(const EntityItemID& entityID) const { QReadLocker locker { &_entityScriptsLock }; return _entityScripts.contains(entityID);} void ScriptManager::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) );Lines 1501–2000 of 2763. Previous lines Load more
using UrlMap = QMap<QUrl, QString>; auto scriptCache = DependencyManager::get<ScriptCache>(); QVariantMap req; qCDebug(scriptengine_module) << "require.fetchModuleSource: " << QUrl(modulePath).fileName() << QThread::currentThread(); auto onload = [=, &req](const UrlMap& data, const UrlMap& _status) { auto url = modulePath; auto status = _status[url]; auto contents = data[url]; if (isStopping()) { req["status"] = "Stopped"; req["success"] = false; } else { req["url"] = url; req["status"] = status; req["success"] = ScriptCache::isSuccessStatus(status); req["contents"] = contents; } }; if (forceDownload) { qCDebug(scriptengine_module) << "require.requestScript -- clearing cache for" << modulePath; scriptCache->deleteScript(modulePath); } BatchLoader* loader = new BatchLoader(QList<QUrl>({ modulePath })); connect(loader, &BatchLoader::finished, this, onload); connect(this, &QObject::destroyed, loader, &QObject::deleteLater); // fail faster? (since require() blocks the engine thread while resolving dependencies) const int MAX_RETRIES = 1; loader->start(MAX_RETRIES); if (!loader->isFinished()) { // This lambda can get called AFTER this local scope has completed. // This is why we pass smart ptrs to the lambda instead of references to local variables. auto monitor = std::make_shared<QTimer>(); auto loop = std::make_shared<QEventLoop>(); QObject::connect(loader, &BatchLoader::finished, this, [monitor, loop] { monitor->stop(); loop->quit(); }); // this helps detect the case where stop() is invoked during the download // but not seen in time to abort processing in onload()... connect(monitor.get(), &QTimer::timeout, this, [this, loop] { if (isStopping()) { loop->exit(-1); } }); monitor->start(500); loop->exec(); } loader->deleteLater(); return req; } // evaluate a pending module object using the fetched source code ScriptValue ScriptManager::instantiateModule(const ScriptValue& module, const QString& sourceCode) { ScriptValue result; auto modulePath = module.property("filename").toString(); auto closure = module.property("__closure__"); qCDebug(scriptengine_module) << QString("require.instantiateModule: %1 / %2 bytes") .arg(QUrl(modulePath).fileName()).arg(sourceCode.length()); if (module.property("content-type").toString() == "application/json") { qCDebug(scriptengine_module) << "... parsing as JSON"; closure.setProperty("__json", sourceCode); result = _engine->evaluateInClosure(closure, _engine->newProgram( "module.exports = JSON.parse(__json)", modulePath )); } else { // scoped vars for consistency with Node.js closure.setProperty("require", module.property("require")); closure.setProperty("__filename", modulePath, READONLY_HIDDEN_PROP_FLAGS); closure.setProperty("__dirname", QString(modulePath).replace(QRegExp("/[^/]*$"), ""), READONLY_HIDDEN_PROP_FLAGS); //_engine->scriptValueDebugDetails(module); result = _engine->evaluateInClosure(closure, _engine->newProgram( sourceCode, modulePath )); } return result; } // CommonJS/Node.js like require/module support ScriptValue ScriptManager::require(const QString& moduleId) { qCDebug(scriptengine_module) << "ScriptManager::require(" << moduleId.left(MAX_DEBUG_VALUE_LENGTH) << ")"; if (!_engine->IS_THREADSAFE_INVOCATION(__FUNCTION__)) { return _engine->nullValue(); } auto jsRequire = _engine->globalObject().property("Script").property("require"); auto cacheMeta = jsRequire.data(); auto cache = jsRequire.property("cache"); auto parent = currentModule(); auto throwModuleError = [&](const QString& modulePath, const ScriptValue& error) { cache.setProperty(modulePath, _engine->nullValue()); if (!error.isNull()) { #ifdef DEBUG_JS_MODULES qCWarning(scriptengine_module) << "throwing module error:" << error.toString() << modulePath << error.property("stack").toString(); #endif _engine->raiseException(error, "module error"); } return _engine->nullValue(); }; // start by resolving the moduleId into a fully-qualified path/URL QString modulePath = _requireResolve(moduleId); if (modulePath.isNull() || _engine->hasUncaughtException()) { // the resolver already threw an exception -- bail early return _engine->nullValue(); } // check the resolved path against the cache auto module = cache.property(modulePath); // modules get cached in `Script.require.cache` and (similar to Node.js) users can access it // to inspect particular entries and invalidate them by deleting the key: // `delete Script.require.cache[Script.require.resolve(moduleId)];` // Check to see if we should invalidate the cache based on a user setting. Setting::Handle<bool> getCachebustSetting {"cachebustScriptRequire", false }; // cacheMeta is just used right now to tell deleted keys apart from undefined ones bool invalidateCache = getCachebustSetting.get() || (module.isUndefined() && cacheMeta.property(moduleId).isValid()); // reset the cacheMeta record so invalidation won't apply next time, even if the module fails to load cacheMeta.setProperty(modulePath, ScriptValue()); auto exports = module.property("exports"); if (!invalidateCache && exports.isObject()) { // we have found a cached module -- just need to possibly register it with current parent qCDebug(scriptengine_module) << QString("require - using cached module for '%1' (loaded: %2)") .arg(moduleId).arg(module.property("loaded").toString()); registerModuleWithParent(module, parent); return exports; } // bootstrap / register new empty module module = newModule(modulePath, parent); registerModuleWithParent(module, parent); // add it to the cache (this is done early so any cyclic dependencies pick up) cache.setProperty(modulePath, module); // download the module source auto req = fetchModuleSource(modulePath, invalidateCache); if (!req.contains("success") || !req["success"].toBool()) { auto error = QString("error retrieving script (%1)").arg(req["status"].toString()); return throwModuleError(modulePath, _engine->newValue(error)); } #if DEBUG_JS_MODULES qCDebug(scriptengine_module) << "require.loaded: " << QUrl(req["url"].toString()).fileName() << req["status"].toString(); #endif auto sourceCode = req["contents"].toString(); if (QUrl(modulePath).fileName().endsWith(".json", Qt::CaseInsensitive)) { module.setProperty("content-type", "application/json"); } else { module.setProperty("content-type", "application/javascript"); } // evaluate the module auto result = instantiateModule(module, sourceCode); if (result.isError() && !result.strictlyEquals(module.property("exports"))) { qCWarning(scriptengine_module) << "-- result.isError --" << result.toString(); return throwModuleError(modulePath, result); } // mark as fully-loaded module.setProperty("loaded", true, READONLY_PROP_FLAGS); // set up a new reference point for detecting cache key deletion cacheMeta.setProperty(modulePath, module); //qCDebug(scriptengine_module) << "//ScriptManager::require(" << moduleId << ")"; //qCDebug(scriptengine_module) << "Exports: " << _engine->scriptValueDebugDetails(module.property("exports")); return module.property("exports"); } // 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 ScriptManager::include(const QStringList& includeFiles, const ScriptValue& callback) { if (!_engine->IS_THREADSAFE_INVOCATION(__FUNCTION__)) { return; } if (isStopped()) { int lineNumber = -1; QString fileName = getFilename(); auto context = _engine->currentContext(); if (context) { lineNumber = context->currentLineNumber(); fileName = context->currentFileName(); } scriptWarningMessage("Script.include() while shutting down is ignored... includeFiles:" + includeFiles.join(",") + "parent script:" + getFilename(), fileName, lineNumber); return; // bail early } QList<QUrl> urls; for (QString includeFile : includeFiles) { QString file = DependencyManager::get<ResourceManager>()->normalizeURL(includeFile); QUrl thisURL; bool isStandardLibrary = false; if (file.startsWith("/~/")) { thisURL = expandScriptUrl(QUrl::fromLocalFile(expandScriptPath(file))); QUrl defaultScriptsLoc = PathUtils::defaultScriptsLocation(); if (!defaultScriptsLoc.isParentOf(thisURL)) { int lineNumber = -1; QString fileName = getFilename(); auto context = _engine->currentContext(); if (context) { lineNumber = context->currentLineNumber(); fileName = context->currentFileName(); } //V8TODO this probably needs to be done per context, otherwise file cannot be included again in a module scriptWarningMessage("Script.include() -- skipping" + file + "-- outside of standard libraries", fileName, lineNumber); continue; } isStandardLibrary = true; } else { thisURL = resolvePath(file); } bool disallowOutsideFiles = thisURL.isLocalFile() && !isStandardLibrary && !currentSandboxURL.isLocalFile(); if (disallowOutsideFiles && !PathUtils::isDescendantOf(thisURL, currentSandboxURL)) { int lineNumber = -1; QString fileName = currentSandboxURL.toString(); auto context = _engine->currentContext(); if (context) { lineNumber = context->currentLineNumber(); fileName = context->currentFileName(); } scriptWarningMessage("Script.include() ignoring file path" + thisURL.toString() + "outside of original entity script" + currentSandboxURL.toString(), fileName, lineNumber); } else { // We could also check here for CORS, but we don't yet. // It turns out that QUrl.resolve will not change hosts and copy authority, so we don't need to check that here. urls.append(thisURL); } } // If there are no URLs left to download, don't bother attempting to download anything and return early if (urls.size() == 0) { return; } BatchLoader* loader = new BatchLoader(urls); EntityItemID capturedEntityIdentifier = currentEntityIdentifier; QUrl capturedSandboxURL = currentSandboxURL; auto evaluateScripts = [=](const QMap<QUrl, QString>& data, const QMap<QUrl, QString>& status) { auto parentURL = _parentURL; for (QUrl url : urls) { QString contents = data[url]; if (contents.isNull()) { int lineNumber = -1; QString fileName = url.toString(); auto context = _engine->currentContext(); if (context) { lineNumber = context->currentLineNumber(); fileName = context->currentFileName(); } scriptErrorMessage("Error loading file (" + status[url] +"): " + url.toString(), fileName, lineNumber); } else { std::lock_guard<std::recursive_mutex> lock(_lock); if (!_includedURLs.contains(url)) { _includedURLs << url; // Set the parent url so that path resolution will be relative // to this script's url during its initial evaluation _parentURL = url.toString(); auto operation = [&]() { _engine->evaluate(contents, url.toString()); }; doWithEnvironment(capturedEntityIdentifier, capturedSandboxURL, operation); if(_engine->hasUncaughtException()) { auto ex = _engine->uncaughtException(); ex->additionalInfo += "; evaluateInClosure"; emit unhandledException(ex); _engine->clearExceptions(); } } else { int lineNumber = -1; QString fileName = url.toString(); auto context = _engine->currentContext(); if (context) { lineNumber = context->currentLineNumber(); fileName = context->currentFileName(); } scriptPrintedMessage("Script.include() skipping evaluation of previously included url:" + url.toString(), fileName, lineNumber); } } } _parentURL = parentURL; if (callback.isFunction()) { callWithEnvironment(capturedEntityIdentifier, capturedSandboxURL, callback, ScriptValue(), ScriptValueList()); } 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(processLevelMaxRetries); if (!callback.isFunction() && !loader->isFinished()) { QEventLoop loop; QObject::connect(loader, &BatchLoader::finished, &loop, &QEventLoop::quit); loop.exec(); } } void ScriptManager::include(const QString& includeFile, const ScriptValue& callback) { if (isStopped()) { int lineNumber = -1; QString fileName = currentSandboxURL.toString(); auto context = _engine->currentContext(); if (context) { lineNumber = context->currentLineNumber(); fileName = context->currentFileName(); } scriptWarningMessage("Script.include() while shutting down is ignored... includeFile:" + includeFile + "parent script:" + getFilename(), fileName, lineNumber); 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 ScriptManager class just emits a signal which // the Application or other context will connect to in order to know to actually load the script void ScriptManager::load(const QString& loadFile) { if (!_engine->IS_THREADSAFE_INVOCATION(__FUNCTION__)) { return; } int lineNumber = -1; QString fileName = getFilename(); auto context = _engine->currentContext(); if (context) { lineNumber = context->currentLineNumber(); fileName = context->currentFileName(); } if (isStopped()) { scriptWarningMessage("Script.load() while shutting down is ignored... loadFile:" + loadFile + "parent script:" + getFilename(), fileName, lineNumber); return; // bail early } if (!currentEntityIdentifier.isInvalidID()) { scriptWarningMessage("Script.load() from entity script is ignored... loadFile:" + loadFile + "parent script:" + getFilename() + "entity: " + currentEntityIdentifier.toString(), fileName, lineNumber); 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 args void ScriptManager::forwardHandlerCall(const EntityItemID& entityID, const QString& eventName, const ScriptValueList& eventHandlerArgs) { if (QThread::currentThread() != thread()) { qCDebug(scriptengine) << "*** ERROR *** ScriptManager::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, ScriptValue(), eventHandlerArgs); } } } int ScriptManager::getNumRunningEntityScripts() const { QReadLocker locker { &_entityScriptsLock }; int sum = 0; for (const auto& st : _entityScripts) { if (st.status == EntityScriptStatus::RUNNING) { ++sum; } } return sum; } void ScriptManager::setEntityScriptDetails(const EntityItemID& entityID, const EntityScriptDetails& details) { { QWriteLocker locker { &_entityScriptsLock }; _entityScripts[entityID] = details; } emit entityScriptDetailsUpdated(); } void ScriptManager::updateEntityScriptStatus(const EntityItemID& entityID, const EntityScriptStatus &status, const QString& errorInfo) { { QWriteLocker locker { &_entityScriptsLock }; EntityScriptDetails& details = _entityScripts[entityID]; details.status = status; details.errorInfo = errorInfo; } emit entityScriptDetailsUpdated(); } QVariant ScriptManager::cloneEntityScriptDetails(const EntityItemID& entityID) { static const QVariant NULL_VARIANT = QVariant::fromValue(nullptr); QVariantMap map; if (entityID.isNull()) { // TODO: find better way to report JS Error across thread/process boundaries map["isError"] = true; map["errorInfo"] = "Error: getEntityScriptDetails -- invalid entityID"; } else { #ifdef DEBUG_ENTITY_STATES qCDebug(scriptengine) << "cloneEntityScriptDetails" << entityID << QThread::currentThread(); #endif EntityScriptDetails scriptDetails; if (getEntityScriptDetails(entityID, scriptDetails)) { #ifdef DEBUG_ENTITY_STATES qCDebug(scriptengine) << "gotEntityScriptDetails" << scriptDetails.status << QThread::currentThread(); #endif map["isRunning"] = isEntityScriptRunning(entityID); map["status"] = EntityScriptStatus_::valueToKey(scriptDetails.status).toLower(); map["errorInfo"] = scriptDetails.errorInfo; map["entityID"] = entityID.toString(); #ifdef DEBUG_ENTITY_STATES { auto debug = QVariantMap(); debug["script"] = scriptDetails.scriptText; debug["scriptObject"] = scriptDetails.scriptObject.toVariant(); debug["lastModified"] = (qlonglong)scriptDetails.lastModified; debug["sandboxURL"] = scriptDetails.definingSandboxURL; map["debug"] = debug; } #endif } else { #ifdef DEBUG_ENTITY_STATES qCDebug(scriptengine) << "!gotEntityScriptDetails" << QThread::currentThread(); #endif map["isError"] = true; map["errorInfo"] = "Entity script details unavailable"; map["entityID"] = entityID.toString(); } } return map; } QFuture<QVariant> ScriptManager::getLocalEntityScriptDetails(const EntityItemID& entityID) { return QtConcurrent::run(this, &ScriptManager::cloneEntityScriptDetails, entityID); } bool ScriptManager::getEntityScriptDetails(const EntityItemID& entityID, EntityScriptDetails &details) const { QReadLocker locker { &_entityScriptsLock }; auto it = _entityScripts.constFind(entityID); if (it == _entityScripts.constEnd()) { return false; } details = it.value(); return true; } bool ScriptManager::hasEntityScriptDetails(const EntityItemID& entityID) const { QReadLocker locker { &_entityScriptsLock }; return _entityScripts.contains(entityID); } void ScriptManager::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) );