ksuprynowiczFixes for code review586bd7f4 years agoHistory
2494 lines · 104.4 KB · C++
RawDownload
////  ScriptManager.cpp//  libraries/script-engine/src////  Created by Brad Hefta-Gaub on 12/14/13.//  Copyright 2013 High Fidelity, Inc.//  Copyright 2022 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// #include "ScriptManager.h" #include <chrono>#include <thread> #include <QtCore/QCoreApplication>#include <QtCore/QEventLoop>#include <QtCore/QFileInfo>#include <QtCore/QTimer>#include <QtCore/QThread>#include <QtCore/QRegularExpression> #include <QtCore/QFuture>#include <QtConcurrent/QtConcurrentRun> #include <shared/LocalFileAccessGate.h>#include <shared/AbstractLoggerInterface.h>#include <DebugDraw.h>#include <MessagesClient.h>#include <OctreeConstants.h>#include <PathUtils.h>#include <PortableHighResolutionClock.h>#include <ResourceCache.h>#include <ResourceManager.h>#include <ResourceScriptingInterface.h>#include <UserActivityLoggerScriptingInterface.h>#include <NodeList.h> #include "AssetScriptingInterface.h"#include "BatchLoader.h"#include "EventTypes.h"#include "FileScriptingInterface.h" // unzip project#include "MenuItemProperties.h"#include "ScriptCache.h"#include "ScriptContext.h"#include "XMLHttpRequestClass.h"#include "WebSocketClass.h"#include "ScriptEngine.h"#include "ScriptEngineCast.h"#include "ScriptEngineLogging.h"#include "ScriptEngines.h"#include "StackTestScriptingInterface.h"#include "ScriptValue.h"#include "ScriptValueIterator.h"#include "ScriptValueUtils.h" #include <Profile.h> #include "MIDIEvent.h" #include "SettingHandle.h"#include <AddressManager.h>#include <NetworkingConstants.h>#include <ThreadHelpers.h> const QString ScriptManager::_SETTINGS_ENABLE_EXTENDED_EXCEPTIONS {    "com.highfidelity.experimental.enableExtendedJSExceptions"}; const QString ScriptManager::SCRIPT_EXCEPTION_FORMAT{ "[%0] %1 in %2:%3" };const QString ScriptManager::SCRIPT_BACKTRACE_SEP{ "\n    " }; static const int MAX_MODULE_ID_LENGTH { 4096 };static const int MAX_DEBUG_VALUE_LENGTH { 80 }; static const ScriptValue::PropertyFlags READONLY_PROP_FLAGS{ ScriptValue::ReadOnly | ScriptValue::Undeletable };static const ScriptValue::PropertyFlags READONLY_HIDDEN_PROP_FLAGS{ READONLY_PROP_FLAGS | ScriptValue::SkipInEnumeration }; static const bool HIFI_AUTOREFRESH_FILE_SCRIPTS { true }; int scriptManagerPointerMetaID = qRegisterMetaType<ScriptManagerPointer>(); Q_DECLARE_METATYPE(ExternalResource::Bucket); Q_DECLARE_METATYPE(ScriptValue); // --- Static script initialization registry static ScriptManager::StaticInitializerNode* rootInitializer = nullptr;static ScriptManager::StaticTypesInitializerNode* rootTypesInitializer = nullptr;  using ScriptableResourceRawPtr = ScriptableResource*;ScriptValue externalResourceBucketToScriptValue(ScriptEngine* engine, ExternalResource::Bucket const& in);bool externalResourceBucketFromScriptValue(const ScriptValue& object, ExternalResource::Bucket& out);static ScriptValue scriptableResourceToScriptValue(ScriptEngine* engine,                                                    const ScriptableResourceRawPtr& resource);static bool scriptableResourceFromScriptValue(const ScriptValue& value, ScriptableResourceRawPtr& resource);STATIC_SCRIPT_TYPES_INITIALIZER((+[](ScriptManager* manager){    auto scriptEngine = manager->engine().get();     scriptRegisterMetaType<ExternalResource::Bucket, externalResourceBucketToScriptValue, externalResourceBucketFromScriptValue>(scriptEngine);     scriptRegisterMetaType<ScriptableResourceRawPtr, scriptableResourceToScriptValue, scriptableResourceFromScriptValue>(scriptEngine);})); void ScriptManager::registerNewStaticInitializer(StaticInitializerNode* dest) {    // this function is assumed to be called on LoadLibrary, where we are explicitly operating in single-threaded mode    // Therefore there is no mutex or threadsafety here and the structure is assumed not to change after loading    dest->prev = rootInitializer;    rootInitializer = dest;}static void runStaticInitializers(ScriptManager* manager) {    ScriptManager::StaticInitializerNode* here = rootInitializer;    while (here != nullptr) {        (*here->init)(manager);        here = here->prev;    }}void ScriptManager::registerNewStaticTypesInitializer(StaticTypesInitializerNode* dest) {    // this function is assumed to be called on LoadLibrary, where we are explicitly operating in single-threaded mode    // Therefore there is no mutex or threadsafety here and the structure is assumed not to change after loading    dest->prev = rootTypesInitializer;    rootTypesInitializer = dest;}static void runStaticTypesInitializers(ScriptManager* manager) {    ScriptManager::StaticTypesInitializerNode* here = rootTypesInitializer;    while (here != nullptr) {        (*here->init)(manager);        here = here->prev;    }} // --- static ScriptValue debugPrint(ScriptContext* context, ScriptEngine* engine) {    // assemble the message by concatenating our arguments    QString message = "";    for (int i = 0; i < context->argumentCount(); i++) {        if (i > 0) {            message += " ";        }        message += context->argument(i).toString();    }     // was this generated by a script engine? If we don't recognize it then send the message and exit    ScriptManager* scriptManager = engine->manager();    if (!scriptManager) {        qCDebug(scriptengine_script, "%s", qUtf8Printable(message));        return ScriptValue();    }     // This message was sent by one of our script engines, let's try to see if we can find the source.    // Note that the first entry in the backtrace should be "print" and is somewhat useless to us    AbstractLoggerInterface* loggerInterface = AbstractLoggerInterface::get();    if (loggerInterface && loggerInterface->showSourceDebugging()) {        ScriptContext* userContext = context;        ScriptContextPointer parentContext; // using this variable to maintain parent variable lifespan        while (userContext && userContext->functionContext()->functionType() == ScriptFunctionContext::NativeFunction) {            parentContext = userContext->parentContext();            userContext = parentContext.get();        }        QString location;        if (userContext) {            auto contextInfo = userContext->functionContext();            QString fileName = contextInfo->fileName();            int lineNumber = contextInfo->lineNumber();            QString functionName = contextInfo->functionName();             location = functionName;            if (!fileName.isEmpty()) {                if (location.isEmpty()) {                    location = fileName;                } else {                    location = QString("%1 at %2").arg(location).arg(fileName);                }            }            if (lineNumber != -1) {                location = QString("%1:%2").arg(location).arg(lineNumber);            }        }        if (location.isEmpty()) {            location = scriptManager->getFilename();        }         // give the script engine a chance to notify the system about this message        scriptManager->print(message);         // send the message to debug log        qCDebug(scriptengine_script, "[%s] %s", qUtf8Printable(location), qUtf8Printable(message));    } else {        scriptManager->print(message);        // prefix the script engine name to help disambiguate messages in the main debug log        qCDebug(scriptengine_script, "[%s] %s", qUtf8Printable(scriptManager->getFilename()), qUtf8Printable(message));    }     return ScriptValue();} // FIXME Come up with a way to properly encode entity IDs in filename// The purpose of the following two function is to embed entity ids into entity script filenames// so that they show up in stacktraces//// Extract the url portion of a url that has been encoded with encodeEntityIdIntoEntityUrl(...)QString extractUrlFromEntityUrl(const QString& url) {    auto parts = url.split(' ', Qt::SkipEmptyParts);    if (parts.length() > 0) {        return parts[0];    } else {        return "";    }} // Encode an entity id into an entity url// Example: http://www.example.com/some/path.js [EntityID:{9fdd355f-d226-4887-9484-44432d29520e}]QString encodeEntityIdIntoEntityUrl(const QString& url, const QString& entityID) {    return url + " [EntityID:" + entityID + "]";} QString ScriptManager::logException(const ScriptValue& exception) {    auto message = formatException(exception, _enableExtendedJSExceptions.get());    scriptErrorMessage(message);    return message;} ScriptManagerPointer scriptManagerFactory(ScriptManager::Context context,                                                 const QString& scriptContents,                                                 const QString& fileNameString) {    ScriptManagerPointer manager = newScriptManager(context, scriptContents, fileNameString);    auto scriptEngines = DependencyManager::get<ScriptEngines>();    scriptEngines->addScriptEngine(manager);    manager->setScriptEngines(scriptEngines);    return manager;} ScriptManagerPointer newScriptManager(ScriptManager::Context context,                                      const QString& scriptContents,                                      const QString& fileNameString) {    ScriptManagerPointer manager(new ScriptManager(context, scriptContents, fileNameString),                                 [](ScriptManager* obj) { obj->deleteLater(); });    return manager;} int ScriptManager::processLevelMaxRetries { ScriptRequest::MAX_RETRIES }; ScriptManager::ScriptManager(Context context, const QString& scriptContents, const QString& fileNameString) :    QObject(),    _context(context),    _engine(newScriptEngine(this)),    _scriptContents(scriptContents),    _timerFunctionMap(),    _fileNameString(fileNameString),    _assetScriptingInterface(new AssetScriptingInterface(this)){    switch (_context) {        case Context::CLIENT_SCRIPT:            _type = Type::CLIENT;            break;        case Context::ENTITY_CLIENT_SCRIPT:            _type = Type::ENTITY_CLIENT;            break;        case Context::ENTITY_SERVER_SCRIPT:            _type = Type::ENTITY_SERVER;            break;        case Context::AGENT_SCRIPT:            _type = Type::AGENT;            break;    }     if (isEntityServerScript()) {        qCDebug(scriptengine) << "isEntityServerScript() -- limiting maxRetries to 1";        processLevelMaxRetries = 1;    }     // this is where all unhandled exceptions end up getting logged    connect(this, &ScriptManager::unhandledException, this, [this](const ScriptValue& err) {        auto output = err.engine() == _engine ? err : _engine->makeError(err);        if (!output.property("detail").isValid()) {            output.setProperty("detail", "UnhandledException");        }        logException(output);    });     if (_type == Type::ENTITY_CLIENT || _type == Type::ENTITY_SERVER) {        QObject::connect(this, &ScriptManager::update, this, [this]() {            // process pending entity script content            if (!_contentAvailableQueue.empty() && !(_isFinished || _isStopping)) {                EntityScriptContentAvailableMap pending;                std::swap(_contentAvailableQueue, pending);                for (auto& pair : pending) {                    auto& args = pair.second;                    entityScriptContentAvailable(args.entityID, args.scriptOrURL, args.contents, args.isURL, args.success, args.status);                }            }        });    }        if (!_areMetaTypesInitialized) {        initMetaTypes();    }    } QString ScriptManager::getTypeAsString() const {    auto value = QVariant::fromValue(_type).toString();    return value.isEmpty() ? "unknown" : value.toLower();} QString ScriptManager::getContext() const {    switch (_context) {        case CLIENT_SCRIPT:            return "client";        case ENTITY_CLIENT_SCRIPT:            return "entity_client";        case ENTITY_SERVER_SCRIPT:            return "entity_server";        case AGENT_SCRIPT:            return "agent";        default:            return "unknown";    }    return "unknown";} bool ScriptManager::isDebugMode() const {#if defined(DEBUG)    return true;#else    return false;#endif} ScriptManager::~ScriptManager() {} void ScriptManager::disconnectNonEssentialSignals() {    disconnect();    QThread* workerThread;    // Ensure the thread should be running, and does exist    if (_isRunning && _isThreaded && (workerThread = thread())) {        connect(this, &QObject::destroyed, workerThread, &QThread::quit);        connect(workerThread, &QThread::finished, workerThread, &QObject::deleteLater);    }} void ScriptManager::runInThread() {    Q_ASSERT_X(!_isThreaded, "ScriptManager::runInThread()", "runInThread should not be called more than once");     if (_isThreaded) {        return;    }     _isThreaded = true;     // The thread interface cannot live on itself, and we want to move this into the thread, so    // the thread cannot have this as a parent.    QThread* workerThread = new QThread();    QString name = QString("js:") + getFilename().replace("about:","");    workerThread->setObjectName(name);    _engine->setThread(workerThread);    moveToThread(workerThread);     // NOTE: If you connect any essential signals for proper shutdown or cleanup of    // the script engine, make sure to add code to "reconnect" them to the    // disconnectNonEssentialSignals() method    connect(workerThread, &QThread::started, this, [this, name] {        setThreadName(name.toStdString());        run();    });    connect(this, &QObject::destroyed, workerThread, &QThread::quit);    connect(workerThread, &QThread::finished, workerThread, &QObject::deleteLater);     workerThread->start();} void ScriptManager::executeOnScriptThread(std::function<void()> function, const Qt::ConnectionType& type ) {    if (QThread::currentThread() != thread()) {        QMetaObject::invokeMethod(this, "executeOnScriptThread", type, Q_ARG(std::function<void()>, function));        return;    }     function();} void ScriptManager::waitTillDoneRunning(bool shutdown) {    // Engine should be stopped already, but be defensive    stop();     auto workerThread = thread();     if (workerThread == QThread::currentThread()) {        qCWarning(scriptengine) << "ScriptManager::waitTillDoneRunning called, but the script is on the same thread:" << getFilename();        return;    }     if (_isThreaded && workerThread) {        // We should never be waiting (blocking) on our own thread        assert(workerThread != QThread::currentThread()); #if 0        // 26 Feb 2021 - Disabled this OSX-specific code because it causes OSX to crash on shutdown; without this code, OSX        // doesn't crash on shutdown. Qt 5.12.3 and Qt 5.15.2.        //        // On mac, don't call QCoreApplication::processEvents() here. This is to prevent        // [NSApplication terminate:] from prematurely destroying the static destructors        // while we are waiting for the scripts to shutdown. We will pump the message        // queue later in the Application destructor.        if (workerThread->isRunning()) {            workerThread->quit();             if (_engine->isEvaluating()) {                qCWarning(scriptengine) << "Script Engine has been running too long, aborting:" << getFilename();                _engine->abortEvaluation();            } else {                auto context = _engine->currentContext();                if (context) {                    qCWarning(scriptengine) << "Script Engine has been running too long, throwing:" << getFilename();                    context->throwError("Timed out during shutdown");                }            }             // Wait for the scripting thread to stop running, as            // flooding it with aborts/exceptions will persist it longer            static const auto MAX_SCRIPT_QUITTING_TIME = 0.5 * MSECS_PER_SECOND;            if (!workerThread->wait(MAX_SCRIPT_QUITTING_TIME)) {                workerThread->terminate();            }        }#else        auto startedWaiting = usecTimestampNow();        while (workerThread->isRunning()) {            // If the final evaluation takes too long, then tell the script engine to stop running            auto elapsedUsecs = usecTimestampNow() - startedWaiting;            static const auto MAX_SCRIPT_EVALUATION_TIME = USECS_PER_SECOND;            if (elapsedUsecs > MAX_SCRIPT_EVALUATION_TIME) {                workerThread->quit();                 if (_engine->isEvaluating()) {                    qCWarning(scriptengine) << "Script Engine has been running too long, aborting:" << getFilename();                    _engine->abortEvaluation();                } else {                    auto context = _engine->currentContext();                    if (context) {                        qCWarning(scriptengine) << "Script Engine has been running too long, throwing:" << getFilename();                        context->throwError("Timed out during shutdown");                    }                }                 // Wait for the scripting thread to stop running, as                // flooding it with aborts/exceptions will persist it longer                static const auto MAX_SCRIPT_QUITTING_TIME = 0.5 * MSECS_PER_SECOND;                if (!workerThread->wait(MAX_SCRIPT_QUITTING_TIME)) {                    workerThread->terminate();                }            }             if (shutdown) {                // NOTE: This will be called on the main application thread (among other threads) from stopAllScripts.                //       The thread will need to continue to process events, because                //       the scripts will likely need to marshall messages across to the main thread, e.g.                //       if they access Settings or Menu in any of their shutdown code. So:                // Process events for this thread, allowing invokeMethod calls to pass between threads.                QCoreApplication::processEvents();            }             // Avoid a pure busy wait            QThread::yieldCurrentThread();        }#endif         scriptInfoMessage("Script Engine has stopped:" + getFilename());    }} QString ScriptManager::getFilename() const {    QStringList fileNameParts = _fileNameString.split("/");    QString lastPart;    if (!fileNameParts.isEmpty()) {        lastPart = fileNameParts.last();    }    return lastPart;} bool ScriptManager::hasValidScriptSuffix(const QString& scriptFileName) {    QFileInfo fileInfo(scriptFileName);    QString scriptSuffixToLower = fileInfo.completeSuffix().toLower();    return scriptSuffixToLower.contains(QString("js"), Qt::CaseInsensitive);} void ScriptManager::loadURL(const QUrl& scriptURL, bool reload) {    if (_isRunning) {        return;    }     QUrl url = expandScriptUrl(scriptURL);    _fileNameString = url.toString();    _isReloading = reload;     // Check that script has a supported file extension    if (!hasValidScriptSuffix(_fileNameString)) {

Lines 1–500 of 2494.