QString systemModulePath = QString("%1/modules/%2.js").arg(systemPath).arg(moduleId); url = defaultScriptsLoc; url.setPath(systemModulePath); if (!QFileInfo(url.toLocalFile()).isFile()) { if (!moduleId.contains("./")) { // the user might be trying to refer to a relative file without anchoring it // let's do them a favor and test for that case -- offering specific advice if detected auto unanchoredUrl = resolvePath("./" + moduleId); if (QFileInfo(unanchoredUrl.toLocalFile()).isFile()) { auto msg = QString("relative module ids must be anchored; use './%1' instead") .arg(moduleId); return throwResolveError(makeError(message.arg(msg))); } } return throwResolveError(makeError(message.arg("system module not found"))); } } if (url.isRelative()) { return throwResolveError(makeError(message.arg("could not resolve module id"))); } // if it looks like a local file, verify that it's an allowed path and really a file if (url.isLocalFile()) { QFileInfo file(url.toLocalFile()); QUrl canonical = url; if (file.exists()) { canonical.setPath(file.canonicalFilePath()); } bool disallowOutsideFiles = !PathUtils::defaultScriptsLocation().isParentOf(canonical) && !currentSandboxURL.isLocalFile(); if (disallowOutsideFiles && !PathUtils::isDescendantOf(canonical, currentSandboxURL)) { return throwResolveError(makeError(message.arg( QString("path '%1' outside of origin script '%2' '%3'") .arg(PathUtils::stripFilename(url)) .arg(PathUtils::stripFilename(currentSandboxURL)) .arg(canonical.toString()) ))); } if (!file.exists()) { return throwResolveError(makeError(message.arg("path does not exist: " + url.toLocalFile()))); } if (!file.isFile()) { return throwResolveError(makeError(message.arg("path is not a file: " + url.toLocalFile()))); } } maybeEmitUncaughtException(__FUNCTION__); return url.toString();} // retrieves the current parent module from the JS scope chainQScriptValue ScriptEngine::currentModule() { if (!IS_THREADSAFE_INVOCATION(thread(), __FUNCTION__)) { return unboundNullValue(); } auto jsRequire = globalObject().property("Script").property("require"); auto cache = jsRequire.property("cache"); auto candidate = QScriptValue(); for (auto c = currentContext(); c && !candidate.isObject(); c = c->parentContext()) { QScriptContextInfo contextInfo { c }; candidate = cache.property(contextInfo.fileName()); } if (!candidate.isObject()) { return QScriptValue(); } return candidate;} // replaces or adds "module" to "parent.children[]" array// (for consistency with Node.js and userscript cache invalidation without "cache busters")bool ScriptEngine::registerModuleWithParent(const QScriptValue& module, const QScriptValue& parent) { auto children = parent.property("children"); if (children.isArray()) { auto key = module.property("id"); auto length = children.property("length").toInt32(); for (int i = 0; i < length; i++) { if (children.property(i).property("id").strictlyEquals(key)) { qCDebug(scriptengine_module) << key.toString() << " updating parent.children[" << i << "] = module"; children.setProperty(i, module); return true; } } qCDebug(scriptengine_module) << key.toString() << " appending parent.children[" << length << "] = module"; children.setProperty(length, module); return true; } else if (parent.isValid()) { qCDebug(scriptengine_module) << "registerModuleWithParent -- unrecognized parent" << parent.toVariant().toString(); } return false;} // creates a new JS "module" Object with default metadata propertiesQScriptValue ScriptEngine::newModule(const QString& modulePath, const QScriptValue& parent) { auto closure = newObject(); auto exports = newObject(); auto module = newObject(); qCDebug(scriptengine_module) << "newModule" << modulePath << parent.property("filename").toString(); closure.setProperty("module", module, READONLY_PROP_FLAGS); // note: this becomes the "exports" free variable, so should not be set read only closure.setProperty("exports", exports); // make the closure available to module instantiation module.setProperty("__closure__", closure, READONLY_HIDDEN_PROP_FLAGS); // for consistency with Node.js Module module.setProperty("id", modulePath, READONLY_PROP_FLAGS); module.setProperty("filename", modulePath, READONLY_PROP_FLAGS); module.setProperty("exports", exports); // not readonly module.setProperty("loaded", false, READONLY_PROP_FLAGS); module.setProperty("parent", parent, READONLY_PROP_FLAGS); module.setProperty("children", newArray(), READONLY_PROP_FLAGS); // module.require is a bound version of require that always resolves relative to that module's path auto boundRequire = QScriptEngine::evaluate("(function(id) { return Script.require(Script.require.resolve(id, this.filename)); })", "(boundRequire)"); module.setProperty("require", boundRequire, READONLY_PROP_FLAGS); return module;} // synchronously fetch a module's source code using BatchLoaderQVariantMap ScriptEngine::fetchModuleSource(const QString& modulePath, const bool forceDownload) { 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]; qCDebug(scriptengine_module) << "require.fetchModuleSource.onload: " << QUrl(url).fileName() << status << QThread::currentThread(); 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 codeQScriptValue ScriptEngine::instantiateModule(const QScriptValue& module, const QString& sourceCode) { QScriptValue 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 = evaluateInClosure(closure, { "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); result = evaluateInClosure(closure, { sourceCode, modulePath }); } maybeEmitUncaughtException(__FUNCTION__); return result;} // CommonJS/Node.js like require/module supportQScriptValue ScriptEngine::require(const QString& moduleId) { qCDebug(scriptengine_module) << "ScriptEngine::require(" << moduleId.left(MAX_DEBUG_VALUE_LENGTH) << ")"; if (!IS_THREADSAFE_INVOCATION(thread(), __FUNCTION__)) { return unboundNullValue(); } auto jsRequire = globalObject().property("Script").property("require"); auto cacheMeta = jsRequire.data(); auto cache = jsRequire.property("cache"); auto parent = currentModule(); auto throwModuleError = [&](const QString& modulePath, const QScriptValue& error) { cache.setProperty(modulePath, nullValue()); if (!error.isNull()) {#ifdef DEBUG_JS_MODULES qCWarning(scriptengine_module) << "throwing module error:" << error.toString() << modulePath << error.property("stack").toString();#endif raiseException(error); } maybeEmitUncaughtException("module"); return unboundNullValue(); }; // start by resolving the moduleId into a fully-qualified path/URL QString modulePath = _requireResolve(moduleId); if (modulePath.isNull() || hasUncaughtException()) { // the resolver already threw an exception -- bail early maybeEmitUncaughtException(__FUNCTION__); return unboundNullValue(); } // 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)];` // cacheMeta is just used right now to tell deleted keys apart from undefined ones bool invalidateCache = 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, QScriptValue()); 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 '%1' for '%2' (loaded: %3)") .arg(modulePath).arg(moduleId).arg(module.property("loaded").toString()); registerModuleWithParent(module, parent); maybeEmitUncaughtException("cached module"); 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, 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) << "//ScriptEngine::require(" << moduleId << ")"; maybeEmitUncaughtException(__FUNCTION__); 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 ScriptEngine::include(const QStringList& includeFiles, QScriptValue callback) { if (!IS_THREADSAFE_INVOCATION(thread(), __FUNCTION__)) { return; } if (DependencyManager::get<ScriptEngines>()->isStopped()) { scriptWarningMessage("Script.include() while shutting down is ignored... includeFiles:" + includeFiles.join(",") + "parent script:" + getFilename()); 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)) { scriptWarningMessage("Script.include() -- skipping" + file + "-- outside of standard libraries"); continue; } isStandardLibrary = true; } else { thisURL = resolvePath(file); } bool disallowOutsideFiles = thisURL.isLocalFile() && !isStandardLibrary && !currentSandboxURL.isLocalFile(); if (disallowOutsideFiles && !PathUtils::isDescendantOf(thisURL, currentSandboxURL)) { scriptWarningMessage("Script.include() ignoring file path" + thisURL.toString() + "outside of original entity script" + currentSandboxURL.toString()); } 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()) { scriptErrorMessage("Error loading file (" + status[url] +"): " + url.toString()); } 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 = [&]() { evaluate(contents, url.toString()); }; doWithEnvironment(capturedEntityIdentifier, capturedSandboxURL, operation); if (hasUncaughtException()) { emit unhandledException(cloneUncaughtException("evaluateInclude")); clearExceptions(); } } else { scriptPrintedMessage("Script.include() skipping evaluation of previously included url:" + url.toString()); } } } _parentURL = parentURL; if (callback.isFunction()) { callWithEnvironment(capturedEntityIdentifier, capturedSandboxURL, QScriptValue(callback), QScriptValue(), QScriptValueList()); } loader->deleteLater(); }; connect(loader, &BatchLoader::finished, this, evaluateScripts); // If we are destroyed before the loader completes, make sure to clean it up connect(this, &QObject::destroyed, loader, &QObject::deleteLater); loader->start(processLevelMaxRetries); if (!callback.isFunction() && !loader->isFinished()) { QEventLoop loop; QObject::connect(loader, &BatchLoader::finished, &loop, &QEventLoop::quit); loop.exec(); }} void ScriptEngine::include(const QString& includeFile, QScriptValue callback) { if (DependencyManager::get<ScriptEngines>()->isStopped()) { scriptWarningMessage("Script.include() while shutting down is ignored... includeFile:" + includeFile + "parent script:" + getFilename()); return; // bail early } QStringList urls; urls.append(includeFile); include(urls, callback);} // NOTE: The load() command is similar to the include() command except that it loads the script// as a stand-alone script. To accomplish this, the ScriptEngine class just emits a signal which// the Application or other context will connect to in order to know to actually load the scriptvoid ScriptEngine::load(const QString& loadFile) { if (!IS_THREADSAFE_INVOCATION(thread(), __FUNCTION__)) { return; } if (DependencyManager::get<ScriptEngines>()->isStopped()) { scriptWarningMessage("Script.load() while shutting down is ignored... loadFile:" + loadFile + "parent script:" + getFilename()); return; // bail early } if (!currentEntityIdentifier.isInvalidID()) { scriptWarningMessage("Script.load() from entity script is ignored... loadFile:" + loadFile + "parent script:" + getFilename() + "entity: " + currentEntityIdentifier.toString()); return; // bail early } QUrl url = resolvePath(loadFile); if (_isReloading) { auto scriptCache = DependencyManager::get<ScriptCache>(); scriptCache->deleteScript(url.toString()); emit reloadScript(url.toString(), false); } else { emit loadScript(url.toString(), false); }} // Look up the handler associated with eventName and entityID. If found, evalute the argGenerator thunk and call the handler with those argsvoid ScriptEngine::forwardHandlerCall(const EntityItemID& entityID, const QString& eventName, QScriptValueList eventHandlerArgs) { if (QThread::currentThread() != thread()) { qCDebug(scriptengine) << "*** ERROR *** ScriptEngine::forwardHandlerCall() called on wrong thread [" << QThread::currentThread() << "], invoking on correct thread [" << thread() << "]"; assert(false); return ; } if (!_registeredHandlers.contains(entityID)) { return; } const RegisteredEventHandlers& handlersOnEntity = _registeredHandlers[entityID]; if (!handlersOnEntity.contains(eventName)) { return; } CallbackList handlersForEvent = handlersOnEntity[eventName]; if (!handlersForEvent.isEmpty()) { for (int i = 0; i < handlersForEvent.count(); ++i) { // handlersForEvent[i] can contain many handlers that may have each been added by different interface or entity scripts, // and the entity scripts may be for entities other than the one this is a handler for. // Fortunately, the definingEntityIdentifier captured the entity script id (if any) when the handler was added. CallbackData& handler = handlersForEvent[i]; callWithEnvironment(handler.definingEntityIdentifier, handler.definingSandboxURL, handler.function, QScriptValue(), eventHandlerArgs); } }} int ScriptEngine::getNumRunningEntityScripts() const { QReadLocker locker { &_entityScriptsLock }; int sum = 0; for (const auto& st : _entityScripts) { if (st.status == EntityScriptStatus::RUNNING) { ++sum; } } return sum;} void ScriptEngine::setEntityScriptDetails(const EntityItemID& entityID, const EntityScriptDetails& details) { { QWriteLocker locker { &_entityScriptsLock }; _entityScripts[entityID] = details; } emit entityScriptDetailsUpdated();} void ScriptEngine::updateEntityScriptStatus(const EntityItemID& entityID, const EntityScriptStatus &status, const QString& errorInfo) { { QWriteLocker locker { &_entityScriptsLock }; EntityScriptDetails& details = _entityScripts[entityID]; details.status = status; details.errorInfo = errorInfo;Lines 1501–2000 of 2746. Previous lines Load more
QString systemModulePath = QString("%1/modules/%2.js").arg(systemPath).arg(moduleId); url = defaultScriptsLoc; url.setPath(systemModulePath); if (!QFileInfo(url.toLocalFile()).isFile()) { if (!moduleId.contains("./")) { // the user might be trying to refer to a relative file without anchoring it // let's do them a favor and test for that case -- offering specific advice if detected auto unanchoredUrl = resolvePath("./" + moduleId); if (QFileInfo(unanchoredUrl.toLocalFile()).isFile()) { auto msg = QString("relative module ids must be anchored; use './%1' instead") .arg(moduleId); return throwResolveError(makeError(message.arg(msg))); } } return throwResolveError(makeError(message.arg("system module not found"))); } } if (url.isRelative()) { return throwResolveError(makeError(message.arg("could not resolve module id"))); } // if it looks like a local file, verify that it's an allowed path and really a file if (url.isLocalFile()) { QFileInfo file(url.toLocalFile()); QUrl canonical = url; if (file.exists()) { canonical.setPath(file.canonicalFilePath()); } bool disallowOutsideFiles = !PathUtils::defaultScriptsLocation().isParentOf(canonical) && !currentSandboxURL.isLocalFile(); if (disallowOutsideFiles && !PathUtils::isDescendantOf(canonical, currentSandboxURL)) { return throwResolveError(makeError(message.arg( QString("path '%1' outside of origin script '%2' '%3'") .arg(PathUtils::stripFilename(url)) .arg(PathUtils::stripFilename(currentSandboxURL)) .arg(canonical.toString()) ))); } if (!file.exists()) { return throwResolveError(makeError(message.arg("path does not exist: " + url.toLocalFile()))); } if (!file.isFile()) { return throwResolveError(makeError(message.arg("path is not a file: " + url.toLocalFile()))); } } maybeEmitUncaughtException(__FUNCTION__); return url.toString(); } // retrieves the current parent module from the JS scope chain QScriptValue ScriptEngine::currentModule() { if (!IS_THREADSAFE_INVOCATION(thread(), __FUNCTION__)) { return unboundNullValue(); } auto jsRequire = globalObject().property("Script").property("require"); auto cache = jsRequire.property("cache"); auto candidate = QScriptValue(); for (auto c = currentContext(); c && !candidate.isObject(); c = c->parentContext()) { QScriptContextInfo contextInfo { c }; candidate = cache.property(contextInfo.fileName()); } if (!candidate.isObject()) { return QScriptValue(); } return candidate; } // replaces or adds "module" to "parent.children[]" array // (for consistency with Node.js and userscript cache invalidation without "cache busters") bool ScriptEngine::registerModuleWithParent(const QScriptValue& module, const QScriptValue& parent) { auto children = parent.property("children"); if (children.isArray()) { auto key = module.property("id"); auto length = children.property("length").toInt32(); for (int i = 0; i < length; i++) { if (children.property(i).property("id").strictlyEquals(key)) { qCDebug(scriptengine_module) << key.toString() << " updating parent.children[" << i << "] = module"; children.setProperty(i, module); return true; } } qCDebug(scriptengine_module) << key.toString() << " appending parent.children[" << length << "] = module"; children.setProperty(length, module); return true; } else if (parent.isValid()) { qCDebug(scriptengine_module) << "registerModuleWithParent -- unrecognized parent" << parent.toVariant().toString(); } return false; } // creates a new JS "module" Object with default metadata properties QScriptValue ScriptEngine::newModule(const QString& modulePath, const QScriptValue& parent) { auto closure = newObject(); auto exports = newObject(); auto module = newObject(); qCDebug(scriptengine_module) << "newModule" << modulePath << parent.property("filename").toString(); closure.setProperty("module", module, READONLY_PROP_FLAGS); // note: this becomes the "exports" free variable, so should not be set read only closure.setProperty("exports", exports); // make the closure available to module instantiation module.setProperty("__closure__", closure, READONLY_HIDDEN_PROP_FLAGS); // for consistency with Node.js Module module.setProperty("id", modulePath, READONLY_PROP_FLAGS); module.setProperty("filename", modulePath, READONLY_PROP_FLAGS); module.setProperty("exports", exports); // not readonly module.setProperty("loaded", false, READONLY_PROP_FLAGS); module.setProperty("parent", parent, READONLY_PROP_FLAGS); module.setProperty("children", newArray(), READONLY_PROP_FLAGS); // module.require is a bound version of require that always resolves relative to that module's path auto boundRequire = QScriptEngine::evaluate("(function(id) { return Script.require(Script.require.resolve(id, this.filename)); })", "(boundRequire)"); module.setProperty("require", boundRequire, READONLY_PROP_FLAGS); return module; } // synchronously fetch a module's source code using BatchLoader QVariantMap ScriptEngine::fetchModuleSource(const QString& modulePath, const bool forceDownload) { 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]; qCDebug(scriptengine_module) << "require.fetchModuleSource.onload: " << QUrl(url).fileName() << status << QThread::currentThread(); 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 QScriptValue ScriptEngine::instantiateModule(const QScriptValue& module, const QString& sourceCode) { QScriptValue 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 = evaluateInClosure(closure, { "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); result = evaluateInClosure(closure, { sourceCode, modulePath }); } maybeEmitUncaughtException(__FUNCTION__); return result; } // CommonJS/Node.js like require/module support QScriptValue ScriptEngine::require(const QString& moduleId) { qCDebug(scriptengine_module) << "ScriptEngine::require(" << moduleId.left(MAX_DEBUG_VALUE_LENGTH) << ")"; if (!IS_THREADSAFE_INVOCATION(thread(), __FUNCTION__)) { return unboundNullValue(); } auto jsRequire = globalObject().property("Script").property("require"); auto cacheMeta = jsRequire.data(); auto cache = jsRequire.property("cache"); auto parent = currentModule(); auto throwModuleError = [&](const QString& modulePath, const QScriptValue& error) { cache.setProperty(modulePath, nullValue()); if (!error.isNull()) { #ifdef DEBUG_JS_MODULES qCWarning(scriptengine_module) << "throwing module error:" << error.toString() << modulePath << error.property("stack").toString(); #endif raiseException(error); } maybeEmitUncaughtException("module"); return unboundNullValue(); }; // start by resolving the moduleId into a fully-qualified path/URL QString modulePath = _requireResolve(moduleId); if (modulePath.isNull() || hasUncaughtException()) { // the resolver already threw an exception -- bail early maybeEmitUncaughtException(__FUNCTION__); return unboundNullValue(); } // 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)];` // cacheMeta is just used right now to tell deleted keys apart from undefined ones bool invalidateCache = 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, QScriptValue()); 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 '%1' for '%2' (loaded: %3)") .arg(modulePath).arg(moduleId).arg(module.property("loaded").toString()); registerModuleWithParent(module, parent); maybeEmitUncaughtException("cached module"); 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, 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) << "//ScriptEngine::require(" << moduleId << ")"; maybeEmitUncaughtException(__FUNCTION__); 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 ScriptEngine::include(const QStringList& includeFiles, QScriptValue callback) { if (!IS_THREADSAFE_INVOCATION(thread(), __FUNCTION__)) { return; } if (DependencyManager::get<ScriptEngines>()->isStopped()) { scriptWarningMessage("Script.include() while shutting down is ignored... includeFiles:" + includeFiles.join(",") + "parent script:" + getFilename()); 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)) { scriptWarningMessage("Script.include() -- skipping" + file + "-- outside of standard libraries"); continue; } isStandardLibrary = true; } else { thisURL = resolvePath(file); } bool disallowOutsideFiles = thisURL.isLocalFile() && !isStandardLibrary && !currentSandboxURL.isLocalFile(); if (disallowOutsideFiles && !PathUtils::isDescendantOf(thisURL, currentSandboxURL)) { scriptWarningMessage("Script.include() ignoring file path" + thisURL.toString() + "outside of original entity script" + currentSandboxURL.toString()); } 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()) { scriptErrorMessage("Error loading file (" + status[url] +"): " + url.toString()); } 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 = [&]() { evaluate(contents, url.toString()); }; doWithEnvironment(capturedEntityIdentifier, capturedSandboxURL, operation); if (hasUncaughtException()) { emit unhandledException(cloneUncaughtException("evaluateInclude")); clearExceptions(); } } else { scriptPrintedMessage("Script.include() skipping evaluation of previously included url:" + url.toString()); } } } _parentURL = parentURL; if (callback.isFunction()) { callWithEnvironment(capturedEntityIdentifier, capturedSandboxURL, QScriptValue(callback), QScriptValue(), QScriptValueList()); } loader->deleteLater(); }; connect(loader, &BatchLoader::finished, this, evaluateScripts); // If we are destroyed before the loader completes, make sure to clean it up connect(this, &QObject::destroyed, loader, &QObject::deleteLater); loader->start(processLevelMaxRetries); if (!callback.isFunction() && !loader->isFinished()) { QEventLoop loop; QObject::connect(loader, &BatchLoader::finished, &loop, &QEventLoop::quit); loop.exec(); } } void ScriptEngine::include(const QString& includeFile, QScriptValue callback) { if (DependencyManager::get<ScriptEngines>()->isStopped()) { scriptWarningMessage("Script.include() while shutting down is ignored... includeFile:" + includeFile + "parent script:" + getFilename()); return; // bail early } QStringList urls; urls.append(includeFile); include(urls, callback); } // NOTE: The load() command is similar to the include() command except that it loads the script // as a stand-alone script. To accomplish this, the ScriptEngine class just emits a signal which // the Application or other context will connect to in order to know to actually load the script void ScriptEngine::load(const QString& loadFile) { if (!IS_THREADSAFE_INVOCATION(thread(), __FUNCTION__)) { return; } if (DependencyManager::get<ScriptEngines>()->isStopped()) { scriptWarningMessage("Script.load() while shutting down is ignored... loadFile:" + loadFile + "parent script:" + getFilename()); return; // bail early } if (!currentEntityIdentifier.isInvalidID()) { scriptWarningMessage("Script.load() from entity script is ignored... loadFile:" + loadFile + "parent script:" + getFilename() + "entity: " + currentEntityIdentifier.toString()); 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 ScriptEngine::forwardHandlerCall(const EntityItemID& entityID, const QString& eventName, QScriptValueList eventHandlerArgs) { if (QThread::currentThread() != thread()) { qCDebug(scriptengine) << "*** ERROR *** ScriptEngine::forwardHandlerCall() called on wrong thread [" << QThread::currentThread() << "], invoking on correct thread [" << thread() << "]"; assert(false); return ; } if (!_registeredHandlers.contains(entityID)) { return; } const RegisteredEventHandlers& handlersOnEntity = _registeredHandlers[entityID]; if (!handlersOnEntity.contains(eventName)) { return; } CallbackList handlersForEvent = handlersOnEntity[eventName]; if (!handlersForEvent.isEmpty()) { for (int i = 0; i < handlersForEvent.count(); ++i) { // handlersForEvent[i] can contain many handlers that may have each been added by different interface or entity scripts, // and the entity scripts may be for entities other than the one this is a handler for. // Fortunately, the definingEntityIdentifier captured the entity script id (if any) when the handler was added. CallbackData& handler = handlersForEvent[i]; callWithEnvironment(handler.definingEntityIdentifier, handler.definingSandboxURL, handler.function, QScriptValue(), eventHandlerArgs); } } } int ScriptEngine::getNumRunningEntityScripts() const { QReadLocker locker { &_entityScriptsLock }; int sum = 0; for (const auto& st : _entityScripts) { if (st.status == EntityScriptStatus::RUNNING) { ++sum; } } return sum; } void ScriptEngine::setEntityScriptDetails(const EntityItemID& entityID, const EntityScriptDetails& details) { { QWriteLocker locker { &_entityScriptsLock }; _entityScripts[entityID] = details; } emit entityScriptDetailsUpdated(); } void ScriptEngine::updateEntityScriptStatus(const EntityItemID& entityID, const EntityScriptStatus &status, const QString& errorInfo) { { QWriteLocker locker { &_entityScriptsLock }; EntityScriptDetails& details = _entityScripts[entityID]; details.status = status; details.errorInfo = errorInfo;