3033 lines · 129.3 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-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 "ScriptManager.h" #include <chrono>#include <memory>#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 "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 "ScriptProgram.h"#include "ScriptValueIterator.h"#include "ScriptValueUtils.h"#include "ScriptManagerScriptingInterface.h" #include <Profile.h> #include "MIDIEvent.h" #include "SettingHandle.h"#include <AddressManager.h>#include <NetworkingConstants.h>#include <ThreadHelpers.h>#include <iostream> 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();     registerEventTypes(scriptEngine);    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).repr();    }     // 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());    auto context = _engine->currentContext();    if (context) {        scriptErrorMessage(message, context->currentFileName(), context->currentLineNumber());    } else {        scriptErrorMessage(message, "", -1);    }    return message;} std::shared_ptr<ScriptException> ScriptManager::getUncaughtException() const {    return _engine->uncaughtException();} void ScriptManager::registerConsoleScriptingInterface(ScriptEngine::ScriptEngineScopeGuard* scopeGuard, ScriptEngine* scriptEngine) {    scriptEngine->registerGlobalObject(scopeGuard, "console", _consoleScriptingInterface.get());    scriptEngine->registerFunction(scopeGuard, "console", "info", ConsoleScriptingInterface::info, scriptEngine->currentContext()->argumentCount());    scriptEngine->registerFunction(scopeGuard, "console", "log", ConsoleScriptingInterface::log, scriptEngine->currentContext()->argumentCount());    scriptEngine->registerFunction(scopeGuard, "console", "debug", ConsoleScriptingInterface::debug, scriptEngine->currentContext()->argumentCount());    scriptEngine->registerFunction(scopeGuard, "console", "warn", ConsoleScriptingInterface::warn, scriptEngine->currentContext()->argumentCount());    scriptEngine->registerFunction(scopeGuard, "console", "error", ConsoleScriptingInterface::error, scriptEngine->currentContext()->argumentCount());    scriptEngine->registerFunction(scopeGuard, "console", "exception", ConsoleScriptingInterface::exception, scriptEngine->currentContext()->argumentCount());    scriptEngine->registerFunction(scopeGuard, "console", "assert", ConsoleScriptingInterface::assertion, scriptEngine->currentContext()->argumentCount());    scriptEngine->registerFunction(scopeGuard, "console", "group", ConsoleScriptingInterface::group, 1);    scriptEngine->registerFunction(scopeGuard, "console", "groupCollapsed", ConsoleScriptingInterface::groupCollapsed, 1);    scriptEngine->registerFunction(scopeGuard, "console", "groupEnd", ConsoleScriptingInterface::groupEnd, 0);} 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),    _quatLibrary(new Quat()),    _vec3Library(new Vec3()),    _mat4Library(new Mat4()),    _uuidLibrary(new ScriptUUID()),    _consoleScriptingInterface(new ConsoleScriptingInterface()),    _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;        case Context::NETWORKLESS_TEST_SCRIPT:            _type = Type::NETWORKLESS_TEST;            break;    }     qRegisterMetaType<ScriptValue>();    qRegisterMetaType<std::function<void()>>();     _scriptingInterface = std::make_shared<ScriptManagerScriptingInterface>(this);     if (isEntityServerScript()) {        qCDebug(scriptengine) << "isEntityServerScript() -- limiting maxRetries to 1";        processLevelMaxRetries = 1;    }  #if 0    // V8TODO: Is this actually needed? Exceptions will already be logged on the    // script engine side.     // this is where all unhandled exceptions end up getting logged    connect(this, &ScriptManager::unhandledException, this, [this](const ScriptValue& err) {        qCCritical(scriptengine) << "Caught exception";         auto output = err.engine() == _engine ? err : _engine->makeError(err);        if (!output.property("detail").isValid()) {            output.setProperty("detail", "UnhandledException");        }         // Unhandled exception kills the running script        if (_abortOnUncaughtException) {            stop(false);            logException(output);        }    });#endif     // Forward exceptions from the scripting engine    connect(_engine.get(), &ScriptEngine::exception, this, &ScriptManager::unhandledException);     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);                }            }        });    }     //Gather entity script messages for transmission when running server side.    if (_type == Type::ENTITY_SERVER) {        ;    }     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";        case NETWORKLESS_TEST_SCRIPT:            return "networkless_test";        default:            return "unknown";    }    return "unknown";} bool ScriptManager::isDebugMode() const {#if defined(DEBUG)    return true;#else    return false;#endif} ScriptManager::~ScriptManager() {    auto scopeGuard = engine()->getScopeGuard();    qDebug() << "ScriptManager::~ScriptManager() : Script manager deleted, type: " << _type << " name: " << _fileNameString;    if (_type == ScriptManager::Type::ENTITY_CLIENT) {        printf("ScriptManager::~ScriptManager");    }    // We need to make sure that these get deleted before the script engine.    _scriptingInterface.reset();    _quatLibrary.reset();    _vec3Library.reset();    _mat4Library.reset();    _uuidLibrary.reset();    _consoleScriptingInterface.reset();    _assetScriptingInterface.reset();    _returnValue = ScriptValue();    _isDeleted = true;} 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);    _assetScriptingInterface->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()) {        // Lambda is necessary there to keep shared_ptr counter above zero        QMetaObject::invokeMethod(this, [=, manager = shared_from_this()]{            manager->executeOnScriptThread(function, type);        });        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();

Lines 1–500 of 3033.