2763 lines · 116.2 KB · C++
RawDownload
    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