2746 lines · 117.8 KB · C++
RawDownload
        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