2844 lines · 124.3 KB · C++
RawDownload
////  ScriptEngine.cpp//  libraries/script-engine/src////  Created by Brad Hefta-Gaub on 12/14/13.//  Copyright 2013 High Fidelity, Inc.////  Distributed under the Apache License, Version 2.0.//  See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html// #include "ScriptEngine.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 <QtWidgets/QMainWindow>#include <QtWidgets/QApplication>#include <QtWidgets/QMenuBar>#include <QtWidgets/QMenu> #include <QtNetwork/QNetworkRequest>#include <QtNetwork/QNetworkReply> #include <QtScript/QScriptContextInfo>#include <QtScript/QScriptValue>#include <QtScript/QScriptValueIterator> #include <shared/LocalFileAccessGate.h>#include <shared/QtHelpers.h>#include <shared/AbstractLoggerInterface.h>#include <AudioConstants.h>#include <AudioEffectOptions.h>#include <AvatarData.h>#include <DebugDraw.h>#include <EntityScriptingInterface.h>#include <MessagesClient.h>#include <NetworkAccessManager.h>#include <PathUtils.h>#include <ResourceScriptingInterface.h>#include <UserActivityLoggerScriptingInterface.h>#include <NodeList.h>#include <ScriptAvatarData.h>#include <udt/PacketHeaders.h>#include <UUID.h> #include <controllers/ScriptingInterface.h>#include <AnimationObject.h> #include "ArrayBufferViewClass.h"#include "AssetScriptingInterface.h"#include "BatchLoader.h"#include "BaseScriptEngine.h"#include "DataViewClass.h"#include "EventTypes.h"#include "FileScriptingInterface.h" // unzip project#include "MenuItemProperties.h"#include "ScriptAudioInjector.h"#include "ScriptAvatarData.h"#include "ScriptCache.h"#include "ScriptEngineLogging.h"#include "TypedArrays.h"#include "XMLHttpRequestClass.h"#include "WebSocketClass.h"#include "RecordingScriptingInterface.h"#include "ScriptEngines.h"#include "StackTestScriptingInterface.h"#include "ModelScriptingInterface.h" #include <Profile.h> #include "../../midi/src/Midi.h"        // FIXME why won't a simpler include work?#include "MIDIEvent.h" #include "SettingHandle.h"#include <AddressManager.h>#include <NetworkingConstants.h>#include <ThreadHelpers.h> const QString ScriptEngine::_SETTINGS_ENABLE_EXTENDED_EXCEPTIONS {    "com.highfidelity.experimental.enableExtendedJSExceptions"}; static const int MAX_MODULE_ID_LENGTH { 4096 };static const int MAX_DEBUG_VALUE_LENGTH { 80 }; static const QScriptEngine::QObjectWrapOptions DEFAULT_QOBJECT_WRAP_OPTIONS =                QScriptEngine::ExcludeDeleteLater | QScriptEngine::ExcludeChildObjects;static const QScriptValue::PropertyFlags READONLY_PROP_FLAGS { QScriptValue::ReadOnly | QScriptValue::Undeletable };static const QScriptValue::PropertyFlags READONLY_HIDDEN_PROP_FLAGS { READONLY_PROP_FLAGS | QScriptValue::SkipInEnumeration }; static const bool HIFI_AUTOREFRESH_FILE_SCRIPTS { true }; Q_DECLARE_METATYPE(QScriptEngine::FunctionSignature)int functionSignatureMetaID = qRegisterMetaType<QScriptEngine::FunctionSignature>(); int scriptEnginePointerMetaID = qRegisterMetaType<ScriptEnginePointer>(); Q_DECLARE_METATYPE(ExternalResource::Bucket); static QScriptValue debugPrint(QScriptContext* context, QScriptEngine* 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    ScriptEngine* scriptEngine = qobject_cast<ScriptEngine*>(engine);    if (!scriptEngine) {        qCDebug(scriptengine_script, "%s", qUtf8Printable(message));        return QScriptValue();    }     // 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()) {        QScriptContext* userContext = context;        while (userContext && QScriptContextInfo(userContext).functionType() == QScriptContextInfo::NativeFunction) {            userContext = userContext->parentContext();        }        QString location;        if (userContext) {            QScriptContextInfo contextInfo(userContext);            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 = scriptEngine->getFilename();        }         // give the script engine a chance to notify the system about this message        scriptEngine->print(message);         // send the message to debug log        qCDebug(scriptengine_script, "[%s] %s", qUtf8Printable(location), qUtf8Printable(message));    } else {        scriptEngine->print(message);        // prefix the script engine name to help disambiguate messages in the main debug log        qCDebug(scriptengine_script, "[%s] %s", qUtf8Printable(scriptEngine->getFilename()), qUtf8Printable(message));    }     return QScriptValue();} Q_DECLARE_METATYPE(controller::InputController*)//static int inputControllerPointerId = qRegisterMetaType<controller::InputController*>(); QScriptValue inputControllerToScriptValue(QScriptEngine *engine, controller::InputController* const &in) {    return engine->newQObject(in, QScriptEngine::QtOwnership, DEFAULT_QOBJECT_WRAP_OPTIONS);} void inputControllerFromScriptValue(const QScriptValue &object, controller::InputController* &out) {    out = qobject_cast<controller::InputController*>(object.toQObject());} // 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 ScriptEngine::logException(const QScriptValue& exception) {    auto message = formatException(exception, _enableExtendedJSExceptions.get());    scriptErrorMessage(message);    return message;} ScriptEnginePointer scriptEngineFactory(ScriptEngine::Context context,                                                 const QString& scriptContents,                                                 const QString& fileNameString) {    ScriptEngine* engine = new ScriptEngine(context, scriptContents, fileNameString);    ScriptEnginePointer engineSP = ScriptEnginePointer(engine, &QObject::deleteLater);    auto scriptEngines = DependencyManager::get<ScriptEngines>();    scriptEngines->addScriptEngine(qSharedPointerCast<ScriptEngine>(engineSP));    engine->setScriptEngines(scriptEngines);    return engineSP;} int ScriptEngine::processLevelMaxRetries { ScriptRequest::MAX_RETRIES };ScriptEngine::ScriptEngine(Context context, const QString& scriptContents, const QString& fileNameString) :    BaseScriptEngine(),    _context(context),    _scriptContents(scriptContents),    _timerFunctionMap(),    _fileNameString(fileNameString),    _arrayBufferClass(new ArrayBufferClass(this)),    _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;    }     connect(this, &QScriptEngine::signalHandlerException, this, [this](const QScriptValue& exception) {        if (hasUncaughtException()) {            // the engine's uncaughtException() seems to produce much better stack traces here            emit unhandledException(cloneUncaughtException("signalHandlerException"));            clearExceptions();        } else {            // ... but may not always be available -- so if needed we fallback to the passed exception            emit unhandledException(exception);        }    }, Qt::DirectConnection);     setProcessEventsInterval(MSECS_PER_SECOND);    if (isEntityServerScript()) {        qCDebug(scriptengine) << "isEntityServerScript() -- limiting maxRetries to 1";        processLevelMaxRetries = 1;    }     // this is where all unhandled exceptions end up getting logged    connect(this, &BaseScriptEngine::unhandledException, this, [this](const QScriptValue& err) {        auto output = err.engine() == this ? err : 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, &ScriptEngine::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);                }            }        });    }} QString ScriptEngine::getTypeAsString() const {    auto value = QVariant::fromValue(_type).toString();    return value.isEmpty() ? "unknown" : value.toLower();} QString ScriptEngine::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 ScriptEngine::isDebugMode() const {#if defined(DEBUG)    return true;#else    return false;#endif} ScriptEngine::~ScriptEngine() {} void ScriptEngine::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 ScriptEngine::runInThread() {    Q_ASSERT_X(!_isThreaded, "ScriptEngine::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);    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 ScriptEngine::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 ScriptEngine::waitTillDoneRunning(bool shutdown) {    // Engine should be stopped already, but be defensive    stop();     auto workerThread = thread();     if (workerThread == QThread::currentThread()) {        qCWarning(scriptengine) << "ScriptEngine::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 (isEvaluating()) {                qCWarning(scriptengine) << "Script Engine has been running too long, aborting:" << getFilename();                abortEvaluation();            } else {                auto context = 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 (isEvaluating()) {                    qCWarning(scriptengine) << "Script Engine has been running too long, aborting:" << getFilename();                    abortEvaluation();                } else {                    auto context = 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 ScriptEngine::getFilename() const {    QStringList fileNameParts = _fileNameString.split("/");    QString lastPart;    if (!fileNameParts.isEmpty()) {        lastPart = fileNameParts.last();    }    return lastPart;} bool ScriptEngine::hasValidScriptSuffix(const QString& scriptFileName) {    QFileInfo fileInfo(scriptFileName);    QString scriptSuffixToLower = fileInfo.completeSuffix().toLower();    return scriptSuffixToLower.contains(QString("js"), Qt::CaseInsensitive);} void ScriptEngine::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)) {        scriptErrorMessage("File extension of file: " + _fileNameString + " is not a currently supported script type");        emit errorLoadingScript(_fileNameString);        return;    }     const auto maxRetries = 0; // for consistency with previous scriptCache->getScript() behavior    auto scriptCache = DependencyManager::get<ScriptCache>();    scriptCache->getScriptContents(url.toString(), [this](const QString& url, const QString& scriptContents, bool isURL, bool success, const QString&status) {        qCDebug(scriptengine) << "loadURL" << url << status << QThread::currentThread();        if (!success) {            scriptErrorMessage("ERROR Loading file (" + status + "):" + url);            emit errorLoadingScript(_fileNameString);            return;        }         _scriptContents = scriptContents;         emit scriptLoaded(url);    }, reload, maxRetries);

Lines 1–500 of 2844.