705 lines · 28.0 KB · C++
RawDownload
////  Created by Bradley Austin Davis on 2016/01/08//  Copyright 2013-2016 High Fidelity, Inc.//  Copyright 2023 Overte e.V:////  Distributed under the Apache License, Version 2.0.//  See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html//  SPDX-License-Identifier: Apache-2.0// #include "ScriptEngines.h" #include <QtCore/QStandardPaths>#include <QtCore/QSharedPointer> #include <QtWidgets/QApplication>#include <QtCore/QThread> #include <shared/QtHelpers.h>#include <SettingHandle.h>#include <UserActivityLogger.h>#include <PathUtils.h>#include <shared/FileUtils.h>#include <QtConcurrent/QtConcurrent> #include "ScriptCache.h"#include "ScriptEngine.h"#include "ScriptEngineLogging.h" #define __STR2__(x) #x#define __STR1__(x) __STR2__(x)#define __LOC__ __FILE__ "(" __STR1__(__LINE__) ") : Warning Msg: " static const QString DESKTOP_LOCATION = QStandardPaths::writableLocation(QStandardPaths::DesktopLocation);static const QString SETTINGS_KEY { "RunningScripts" };static const QUrl DEFAULT_SCRIPTS_LOCATION { "file:///~//defaultScripts.js" }; // Using a QVariantList so this is human-readable in the settings filestatic Setting::Handle<QVariantList> runningScriptsHandle(SETTINGS_KEY, { QVariant(DEFAULT_SCRIPTS_LOCATION) }); const int RELOAD_ALL_SCRIPTS_TIMEOUT = 1000;  ScriptsModel& getScriptsModel() {    static ScriptsModel scriptsModel;    return scriptsModel;} void ScriptEngines::onPrintedMessage(const QString& message, const QString& scriptName) {    emit printedMessage(message, scriptName);} void ScriptEngines::onErrorMessage(const QString& message, const QString& scriptName) {    emit errorMessage(message, scriptName);} void ScriptEngines::onWarningMessage(const QString& message, const QString& scriptName) {    emit warningMessage(message, scriptName);} void ScriptEngines::onInfoMessage(const QString& message, const QString& scriptName) {    emit infoMessage(message, scriptName);} void ScriptEngines::onClearDebugWindow() {    emit clearDebugWindow();} void ScriptEngines::onErrorLoadingScript(const QString& url) {    emit errorLoadingScript(url);} ScriptEngines::ScriptEngines(ScriptManager::Context context, const QUrl& defaultScriptsOverride)    : _context(context), _defaultScriptsOverride(defaultScriptsOverride){    scriptGatekeeper.initialize();     _scriptsModelFilter.setSourceModel(&_scriptsModel);    _scriptsModelFilter.sort(0, Qt::AscendingOrder);    _scriptsModelFilter.setDynamicSortFilter(true);} QUrl normalizeScriptURL(const QUrl& rawScriptURL) {    if (rawScriptURL.scheme() == "file") {        QUrl fullNormal = rawScriptURL;        QUrl defaultScriptLoc = PathUtils::defaultScriptsLocation();         // if this url is something "beneath" the default script url, replace the local path with ~        if (fullNormal.scheme() == defaultScriptLoc.scheme() &&            fullNormal.host() == defaultScriptLoc.host() &&            fullNormal.path().startsWith(defaultScriptLoc.path())) {            fullNormal.setPath("/~/" + fullNormal.path().mid(defaultScriptLoc.path().size()));        }        return fullNormal;    } else if (rawScriptURL.scheme() == "http" || rawScriptURL.scheme() == "https" || rawScriptURL.scheme() == "atp") {        return rawScriptURL;    } else {        // don't accidently support gopher        return QUrl("");    }} QString expandScriptPath(const QString& rawPath) {    QStringList splitPath = rawPath.split("/");    QUrl defaultScriptsLoc = PathUtils::defaultScriptsLocation();    return defaultScriptsLoc.path() + "/" + splitPath.mid(2).join("/"); // 2 to skip the slashes in /~/} QUrl expandScriptUrl(const QUrl& rawScriptURL) {    QUrl normalizedScriptURL = normalizeScriptURL(rawScriptURL);    if (normalizedScriptURL.scheme() == "http" ||        normalizedScriptURL.scheme() == "https" ||        normalizedScriptURL.scheme() == "atp") {        return normalizedScriptURL;    } else if (normalizedScriptURL.scheme() == "file") {        if (normalizedScriptURL.path().startsWith("/~/")) {            QUrl url = normalizedScriptURL;            url.setPath(expandScriptPath(url.path()));             // stop something like Script.include(["/~/../Desktop/naughty.js"]); from working            QFileInfo fileInfo(url.toLocalFile());            url = QUrl::fromLocalFile(fileInfo.canonicalFilePath());             QUrl defaultScriptsLoc = PathUtils::defaultScriptsLocation();            if (!defaultScriptsLoc.isParentOf(url) && defaultScriptsLoc != url) {                qCWarning(scriptengine) << "Script.include() ignoring file path"                                        << "-- outside of standard libraries: "                                        << url.path()                                        << defaultScriptsLoc.path();                return rawScriptURL;            }            if (rawScriptURL.path().endsWith("/") && !url.path().endsWith("/")) {                url.setPath(url.path() + "/");            }            return url;        }        return normalizedScriptURL;    } else {        return QUrl("");    }}  QObject* scriptsModel(); void ScriptEngines::addScriptEngine(ScriptManagerPointer manager) {    if (!_isStopped) {        QMutexLocker locker(&_allScriptsMutex);        _allKnownScriptManagers.insert(manager);    }} void ScriptEngines::removeScriptEngine(ScriptManagerPointer manager) {    // If we're not already in the middle of stopping all scripts, then we should remove ourselves    // from the list of running scripts. We don't do this if we're in the process of stopping all scripts    // because that method removes scripts from its list as it iterates them    if (!_isStopped) {        QMutexLocker locker(&_allScriptsMutex);        _allKnownScriptManagers.remove(manager);    }    std::lock_guard<std::mutex> lock(_subscriptionsToEntityScriptMessagesMutex);    _managersSubscribedToEntityScriptMessages.remove(manager.get());    _entitiesSubscribedToEntityScriptMessages.remove(manager.get());} void ScriptEngines::requestServerEntityScriptMessages(ScriptManager *manager) {    std::lock_guard<std::mutex> lock(_subscriptionsToEntityScriptMessagesMutex);    if (!_managersSubscribedToEntityScriptMessages.contains(manager)) {        _managersSubscribedToEntityScriptMessages.insert(manager);        // Emit a signal to inform EntityScriptServerLogClient about subscription request        emit requestingEntityScriptServerLog(true);        qDebug() << "ScriptEngines::requestServerEntityScriptMessages";    }} void ScriptEngines::requestServerEntityScriptMessages(ScriptManager *manager, const QUuid& entityID) {    std::lock_guard<std::mutex> lock(_subscriptionsToEntityScriptMessagesMutex);    if (!_entitiesSubscribedToEntityScriptMessages.contains(manager)) {        _entitiesSubscribedToEntityScriptMessages.insert(manager,QSet<QUuid>());    }    if (!_entitiesSubscribedToEntityScriptMessages[manager].contains(entityID)) {        _entitiesSubscribedToEntityScriptMessages[manager].insert(entityID);        // Emit a signal to inform EntityScriptServerLogClient about subscription request        emit requestingEntityScriptServerLog(true);        qDebug() << "ScriptEngines::requestServerEntityScriptMessages uuid";    }} void ScriptEngines::removeServerEntityScriptMessagesRequest(ScriptManager *manager) {    std::lock_guard<std::mutex> lock(_subscriptionsToEntityScriptMessagesMutex);    if (_managersSubscribedToEntityScriptMessages.contains(manager)) {        _managersSubscribedToEntityScriptMessages.remove(manager);    }    if (_entitiesSubscribedToEntityScriptMessages.isEmpty()        && _managersSubscribedToEntityScriptMessages.isEmpty()) {        // No managers requiring entity script server messages remain, so we inform EntityScriptServerLogClient about this        // Emit a signal to inform EntityScriptServerLogClient about subscription request        emit requestingEntityScriptServerLog(false);        qDebug() << "ScriptEngines::removeServerEntityScriptMessagesRequest";    }} void ScriptEngines::removeServerEntityScriptMessagesRequest(ScriptManager *manager, const QUuid& entityID) {    std::lock_guard<std::mutex> lock(_subscriptionsToEntityScriptMessagesMutex);    if (!_entitiesSubscribedToEntityScriptMessages.contains(manager)) {        return;    }    if (_entitiesSubscribedToEntityScriptMessages[manager].contains(entityID)) {        _entitiesSubscribedToEntityScriptMessages[manager].remove(entityID);    }    if (_entitiesSubscribedToEntityScriptMessages[manager].isEmpty()) {        _entitiesSubscribedToEntityScriptMessages.remove(manager);    }    if (_entitiesSubscribedToEntityScriptMessages.isEmpty()        && _managersSubscribedToEntityScriptMessages.isEmpty()) {        // No managers requiring entity script server messages remain, so we inform EntityScriptServerLogClient about this        // Emit a signal to inform EntityScriptServerLogClient about subscription request        emit requestingEntityScriptServerLog(false);        qDebug() << "ScriptEngines::removeServerEntityScriptMessagesRequest uuid";    }}  void ScriptEngines::shutdownScripting() {    _isStopped = true;    QMutexLocker locker(&_allScriptsMutex);    qCDebug(scriptengine) << "Stopping all scripts.... currently known scripts:" << _allKnownScriptManagers.size();     QMutableSetIterator<ScriptManagerPointer> i(_allKnownScriptManagers);    while (i.hasNext()) {        ScriptManagerPointer scriptManager = i.next();        QString scriptName = scriptManager->getFilename();         // NOTE: typically all script engines are running. But there's at least one known exception to this, the        // "entities sandbox" which is only used to evaluate entities scripts to test their validity before using        // them. We don't need to stop scripts that aren't running.        // TODO: Scripts could be shut down faster if we spread them across a threadpool.        if (scriptManager->isRunning()) {            qCDebug(scriptengine) << "about to shutdown script:" << scriptName;             // We disconnect any script engine signals from the application because we don't want to do any            // extra stopScript/loadScript processing that the Application normally does when scripts start            // and stop. We can safely short circuit this because we know we're in the "quitting" process            scriptManager->disconnect(this);             // Gracefully stop the engine's scripting thread            scriptManager->stop();             // We need to wait for the engine to be done running before we proceed, because we don't            // want any of the scripts final "scriptEnding()" or pending "update()" methods from accessing            // any application state after we leave this stopAllScripts() method            qCDebug(scriptengine) << "waiting on script:" << scriptName;            scriptManager->waitTillDoneRunning(true);            qCDebug(scriptengine) << "done waiting on script:" << scriptName;        }        // Once the script is stopped, we can remove it from our set        i.remove();    }    qCDebug(scriptengine) << "DONE Stopping all scripts....";} /*@jsdoc * Information on a public script, i.e., a script that's included in the Interface installation. * @typedef {object} ScriptDiscoveryService.PublicScript * @property {string} name - The script's file name. * @property {string} type - <code>"script"</code> or <code>"folder"</code>. *     <p class="important">Deprecated: This property is deprecated and will be removed. It currently always has the value, *     <code>"script"</code>.</p> * @property {ScriptDiscoveryService.PublicScript[]} [children] - Only present if <code>type == "folder"</code>. *     <p class="important">Deprecated: This property is deprecated and will be removed. It currently is never present. * @property {string} [url] - The full URL of the script &mdash; including the <code>"file:///"</code> scheme at the start. *     <p>Only present if <code>type == "script"</code>.</p> */QVariantList getPublicChildNodes(TreeNodeFolder* parent) {    QVariantList result;    QList<TreeNodeBase*> treeNodes = getScriptsModel().getFolderNodes(parent);    for (int i = 0; i < treeNodes.size(); i++) {        TreeNodeBase* node = treeNodes.at(i);        if (node->getType() == TREE_NODE_TYPE_FOLDER) {            TreeNodeFolder* folder = static_cast<TreeNodeFolder*>(node);            QVariantMap resultNode;            resultNode.insert("name", node->getName());            resultNode.insert("type", "folder");            resultNode.insert("children", getPublicChildNodes(folder));            result.append(resultNode);            continue;        }        TreeNodeScript* script = static_cast<TreeNodeScript*>(node);        if (script->getOrigin() == ScriptOrigin::SCRIPT_ORIGIN_LOCAL) {            continue;        }        QVariantMap resultNode;        resultNode.insert("name", node->getName());        resultNode.insert("type", "script");        resultNode.insert("url", script->getFullPath());        result.append(resultNode);    }    return result;} QVariantList ScriptEngines::getPublic() {    return getPublicChildNodes(NULL);} /*@jsdoc * Information on a local script. * @typedef {object} ScriptDiscoveryService.LocalScript * @property {string} name - The script's file name. * @property {string} path - The script's path. * @deprecated This type is deprecated and will be removed. */QVariantList ScriptEngines::getLocal() {    QVariantList result;    QList<TreeNodeBase*> treeNodes = getScriptsModel().getFolderNodes(NULL);    for (int i = 0; i < treeNodes.size(); i++) {        TreeNodeBase* node = treeNodes.at(i);        if (node->getType() != TREE_NODE_TYPE_SCRIPT) {            continue;        }        TreeNodeScript* script = static_cast<TreeNodeScript*>(node);        if (script->getOrigin() != ScriptOrigin::SCRIPT_ORIGIN_LOCAL) {            continue;        }        QVariantMap resultNode;        resultNode.insert("name", node->getName());        resultNode.insert("path", script->getFullPath());        result.append(resultNode);    }    return result;} /*@jsdoc * Information on a running script. * @typedef {object} ScriptDiscoveryService.RunningScript * @property {boolean} local - <code>true</code> if the script is a local file (i.e., the scheme is "file"), <code>false</code> *     if it isn't (e.g., the scheme is "http"). * @property {string} name - The script's file name. * @property {string} path - The script's path and file name &mdash; excluding the scheme if a local file. * @property {string} url - The full URL of the script &mdash; including the scheme if a local file. */QVariantList ScriptEngines::getRunning() {    QVariantList result;    auto runningScripts = getRunningScripts();    foreach(const QString& runningScript, runningScripts) {        QUrl runningScriptURL = QUrl(runningScript);        if (!runningScriptURL.isValid()) {            runningScriptURL = QUrl::fromLocalFile(runningScriptURL.toDisplayString(QUrl::FormattingOptions(QUrl::FullyEncoded)));        }        QVariantMap resultNode;        resultNode.insert("name", runningScriptURL.fileName());        QUrl displayURL = expandScriptUrl(runningScriptURL);        QString displayURLString;        if (displayURL.isLocalFile()) {            displayURLString = displayURL.toLocalFile();        } else {            displayURLString = displayURL.toDisplayString(QUrl::FormattingOptions(QUrl::FullyEncoded));        }        // The path contains the exact path/URL of the script, which also is used in the stopScript function.        resultNode.insert("path", displayURLString);        resultNode.insert("url", normalizeScriptURL(runningScript).toString());        resultNode.insert("local", runningScriptURL.isLocalFile());        result.append(resultNode);    }    return result;} void ScriptEngines::loadDefaultScripts() {    loadScript(DEFAULT_SCRIPTS_LOCATION);} void ScriptEngines::loadOneScript(const QString& scriptFilename) {    loadScript(scriptFilename);} void ScriptEngines::loadScripts() {    // START BACKWARD COMPATIBILITY CODE    // The following code makes sure people don't lose all their scripts    // This should be removed after a reasonable ammount of time went by    // Load old setting format if present    bool foundDeprecatedSetting = false;    Settings settings;    int size = settings.beginReadArray(SETTINGS_KEY);    for (int i = 0; i < size; ++i) {        settings.setArrayIndex(i);        QString string = settings.value("script").toString();        if (!string.isEmpty()) {            loadScript(string);            foundDeprecatedSetting = true;        }    }    settings.endArray();    if (foundDeprecatedSetting) {        // Remove old settings found and return        settings.beginWriteArray(SETTINGS_KEY);        settings.remove("");        settings.endArray();        settings.remove(SETTINGS_KEY + "/size");        return;    }    // END BACKWARD COMPATIBILITY CODE     // loads all saved scripts    auto runningScripts = runningScriptsHandle.get();    bool defaultScriptsOverrideSet = !_defaultScriptsOverride.isEmpty();     for (auto script : runningScripts) {        auto url = script.toUrl();        if (!url.isEmpty()) {            if (defaultScriptsOverrideSet && url == DEFAULT_SCRIPTS_LOCATION) {                _defaultScriptsWasRunning = true;            } else {                loadScript(url);            }        }    }     if (defaultScriptsOverrideSet) {        loadScript(_defaultScriptsOverride, false);    }} void ScriptEngines::saveScripts() {    // Do not save anything if we are in the process of shutting down    if (qApp->closingDown()) {        qCWarning(scriptengine) << "Trying to save scripts during shutdown.";        return;    }     // don't save scripts if we started with --scripts, as we would overwrite    // the scripts that the user expects to be there when launched without the    // --scripts override.    if (_defaultScriptsLocationOverridden) {        runningScriptsHandle.set(QVariantList{ DEFAULT_SCRIPTS_LOCATION });        return;    }     // Saves all currently running user-loaded scripts    QVariantList list;     {        QReadLocker lock(&_scriptManagersHashLock);        for (auto it = _scriptManagersHash.begin(); it != _scriptManagersHash.end(); ++it) {            // Save user-loaded scripts, only if they are set to quit when finished            if (it.value() && it.value()->isUserLoaded()  && !it.value()->isQuitWhenFinished()) {                auto normalizedUrl = normalizeScriptURL(it.key());                list.append(normalizedUrl.toString());            }        }    }     if (_defaultScriptsWasRunning) {        list.append(DEFAULT_SCRIPTS_LOCATION);    }     runningScriptsHandle.set(list);} QStringList ScriptEngines::getRunningScripts() {    QReadLocker lock(&_scriptManagersHashLock);    QList<QUrl> urls = _scriptManagersHash.keys();    QStringList result;    for (auto url : urls) {        result.append(url.toString());    }    return result;} void ScriptEngines::stopAllScripts(bool restart) {    QtConcurrent::run([this, restart] {        QHash<QUrl, ScriptManagerPointer> scriptManagersHashCopy;         {            QReadLocker lock(&_scriptManagersHashLock);            scriptManagersHashCopy = _scriptManagersHash;        }         if (_isReloading) {            return;        }         for (QHash<QUrl, ScriptManagerPointer>::const_iterator it = scriptManagersHashCopy.constBegin();                it != scriptManagersHashCopy.constEnd(); it++) {            ScriptManagerPointer scriptManager = it.value();            // skip already stopped scripts            if (scriptManager->isFinished() || scriptManager->isStopping()) {                continue;            }             bool isOverrideScript = it.key().toString().compare(this->_defaultScriptsOverride.toString()) == 0;            // queue user scripts if restarting            if (restart && (scriptManager->isUserLoaded() || isOverrideScript)) {                _isReloading = true;                ScriptManager::Type type = scriptManager->getType();                 connect(scriptManager.get(), &ScriptManager::finished, this,                        [this, type, isOverrideScript](QString scriptName) {                            reloadScript(scriptName, !isOverrideScript)->setType(type);                        });            } 

Lines 1–500 of 705.