1653 lines · 70.8 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 <chrono>#include <thread> #include <QtCore/QCoreApplication>#include <QtCore/QEventLoop>#include <QtCore/QFileInfo>#include <QtCore/QTimer>#include <QtCore/QThread>#include <QtCore/QRegularExpression> #include <QtWidgets/QMainWindow>#include <QtWidgets/QApplication> #include <QtNetwork/QNetworkRequest>#include <QtNetwork/QNetworkReply> #include <QtScript/QScriptContextInfo>#include <QtScript/QScriptValue>#include <QtScript/QScriptValueIterator> #include <QtScriptTools/QScriptEngineDebugger> #include <AudioConstants.h>#include <AudioEffectOptions.h>#include <AvatarData.h>#include <EntityScriptingInterface.h>#include <MessagesClient.h>#include <NetworkAccessManager.h>#include <PathUtils.h>#include <ResourceScriptingInterface.h>#include <NodeList.h>#include <udt/PacketHeaders.h>#include <UUID.h>#include <ui/Menu.h> #include <controllers/ScriptingInterface.h>#include <AnimationObject.h> #include "ArrayBufferViewClass.h"#include "BatchLoader.h"#include "DataViewClass.h"#include "EventTypes.h"#include "FileScriptingInterface.h" // unzip project#include "MenuItemProperties.h"#include "ScriptAudioInjector.h"#include "ScriptCache.h"#include "ScriptEngineLogging.h"#include "ScriptEngine.h"#include "TypedArrays.h"#include "XMLHttpRequestClass.h"#include "WebSocketClass.h"#include "RecordingScriptingInterface.h"#include "ScriptEngines.h" #include "MIDIEvent.h" static const QString SCRIPT_EXCEPTION_FORMAT = "[UncaughtException] %1 in %2:%3"; Q_DECLARE_METATYPE(QScriptEngine::FunctionSignature)int functionSignatureMetaID = qRegisterMetaType<QScriptEngine::FunctionSignature>(); static QScriptValue debugPrint(QScriptContext* context, QScriptEngine* engine){    QString message = "";    for (int i = 0; i < context->argumentCount(); i++) {        if (i > 0) {            message += " ";        }        message += context->argument(i).toString();    }    qCDebug(scriptengine).noquote() << "script:print()<<" << message;  // noquote() so that \n is treated as newline     message = message.replace("\\", "\\\\")                     .replace("\n", "\\n")                     .replace("\r", "\\r")                     .replace("'", "\\'");    engine->evaluate("Script.print('" + message + "')");     return QScriptValue();} QScriptValue avatarDataToScriptValue(QScriptEngine* engine, AvatarData* const &in) {    return engine->newQObject(in);} void avatarDataFromScriptValue(const QScriptValue &object, AvatarData* &out) {    out = qobject_cast<AvatarData*>(object.toQObject());} Q_DECLARE_METATYPE(controller::InputController*)//static int inputControllerPointerId = qRegisterMetaType<controller::InputController*>(); QScriptValue inputControllerToScriptValue(QScriptEngine *engine, controller::InputController* const &in) {    return engine->newQObject(in);} 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(' ', QString::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 + "]";} static bool hasCorrectSyntax(const QScriptProgram& program) {    const auto syntaxCheck = QScriptEngine::checkSyntax(program.sourceCode());    if (syntaxCheck.state() != QScriptSyntaxCheckResult::Valid) {        const auto error = syntaxCheck.errorMessage();        const auto line = QString::number(syntaxCheck.errorLineNumber());        const auto column = QString::number(syntaxCheck.errorColumnNumber());        const auto message = QString("[SyntaxError] %1 in %2:%3(%4)").arg(error, program.fileName(), line, column);        qCWarning(scriptengine) << qPrintable(message);        return false;    }    return true;} static bool hadUncaughtExceptions(QScriptEngine& engine, const QString& fileName) {    if (engine.hasUncaughtException()) {        const auto backtrace = engine.uncaughtExceptionBacktrace();        const auto exception = engine.uncaughtException().toString();        const auto line = QString::number(engine.uncaughtExceptionLineNumber());        engine.clearExceptions();         auto message = QString(SCRIPT_EXCEPTION_FORMAT).arg(exception, fileName, line);        if (!backtrace.empty()) {            static const auto lineSeparator = "\n    ";            message += QString("\n[Backtrace]%1%2").arg(lineSeparator, backtrace.join(lineSeparator));        }        qCWarning(scriptengine) << qPrintable(message);        return true;    }    return false;} ScriptEngine::ScriptEngine(const QString& scriptContents, const QString& fileNameString) :    _scriptContents(scriptContents),    _timerFunctionMap(),    _fileNameString(fileNameString),    _arrayBufferClass(new ArrayBufferClass(this)){    DependencyManager::get<ScriptEngines>()->addScriptEngine(this);     connect(this, &QScriptEngine::signalHandlerException, this, [this](const QScriptValue& exception) {        hadUncaughtExceptions(*this, _fileNameString);    });        setProcessEventsInterval(MSECS_PER_SECOND);} ScriptEngine::~ScriptEngine() {    qCDebug(scriptengine) << "Script Engine shutting down:" << getFilename();     auto scriptEngines = DependencyManager::get<ScriptEngines>();    if (scriptEngines) {        scriptEngines->removeScriptEngine(this);    } else {        qCWarning(scriptengine) << "Script destroyed after ScriptEngines!";    }} void ScriptEngine::disconnectNonEssentialSignals() {    disconnect();    QThread* workerThread;    // Ensure the thread should be running, and does exist    if (_isRunning && _isThreaded && (workerThread = thread())) {        connect(this, &ScriptEngine::doneRunning, workerThread, &QThread::quit);        connect(workerThread, &QThread::finished, workerThread, &QObject::deleteLater);    }} void ScriptEngine::runDebuggable() {    static QMenuBar* menuBar { nullptr };    static QMenu* scriptDebugMenu { nullptr };    static size_t scriptMenuCount { 0 };    if (!scriptDebugMenu) {        for (auto window : qApp->topLevelWidgets()) {            auto mainWindow = qobject_cast<QMainWindow*>(window);            if (mainWindow) {                menuBar = mainWindow->menuBar();                break;            }        }        if (menuBar) {            scriptDebugMenu = menuBar->addMenu("Script Debug");        }    }     init();    _isRunning = true;    _debuggable = true;    _debugger = new QScriptEngineDebugger(this);    _debugger->attachTo(this);     QMenu* parentMenu = scriptDebugMenu;    QMenu* scriptMenu { nullptr };    if (parentMenu) {        ++scriptMenuCount;        scriptMenu = parentMenu->addMenu(_fileNameString);        scriptMenu->addMenu(_debugger->createStandardMenu(qApp->activeWindow()));    } else {        qWarning() << "Unable to add script debug menu";    }     QScriptValue result = evaluate(_scriptContents, _fileNameString);     _lastUpdate = usecTimestampNow();    QTimer* timer = new QTimer(this);    connect(this, &ScriptEngine::finished, [this, timer, parentMenu, scriptMenu] {        if (scriptMenu) {            parentMenu->removeAction(scriptMenu->menuAction());            --scriptMenuCount;            if (0 == scriptMenuCount) {                menuBar->removeAction(scriptDebugMenu->menuAction());                scriptDebugMenu = nullptr;            }        }        disconnect(timer);     });     connect(timer, &QTimer::timeout, [this, timer] {        if (_isFinished) {            if (!_isRunning) {                return;            }            stopAllTimers(); // make sure all our timers are stopped if the script is ending             emit scriptEnding();            emit finished(_fileNameString, this);            _isRunning = false;             emit runningStateChanged();            emit doneRunning();             timer->deleteLater();            return;        }         qint64 now = usecTimestampNow();        // we check for 'now' in the past in case people set their clock back        if (_lastUpdate < now) {            float deltaTime = (float)(now - _lastUpdate) / (float)USECS_PER_SECOND;            if (!_isFinished) {                emit update(deltaTime);            }        }        _lastUpdate = now;        // Debug and clear exceptions        hadUncaughtExceptions(*this, _fileNameString);    });     timer->start(10);}  void ScriptEngine::runInThread() {    Q_ASSERT_X(!_isThreaded, "ScriptEngine::runInThread()", "runInThread should not be called more than once");     if (_isThreaded) {        qCWarning(scriptengine) << "ScriptEngine already running in thread: " << getFilename();        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();    workerThread->setObjectName(QString("Script Thread:") + getFilename());    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, &ScriptEngine::run);    connect(this, &ScriptEngine::doneRunning, workerThread, &QThread::quit);    connect(workerThread, &QThread::finished, workerThread, &QObject::deleteLater);     workerThread->start();} void ScriptEngine::waitTillDoneRunning() {    auto workerThread = thread();     if (_isThreaded && workerThread) {        // We should never be waiting (blocking) on our own thread        assert(workerThread != QThread::currentThread());         // Engine should be stopped already, but be defensive        stop();         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 {                    qCWarning(scriptengine) << "Script Engine has been running too long, throwing:" << getFilename();                    auto context = currentContext();                    if (context) {                        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();                }            }             // NOTE: This will be called on the main application thread from stopAllScripts.            //       The application 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 the main application thread, allowing invokeMethod calls to pass between threads.            QCoreApplication::processEvents();            // In some cases (debugging), processEvents may give the thread enough time to shut down, so recheck it.            if (!thread()) {                break;            }             // Avoid a pure busy wait            QThread::yieldCurrentThread();        }         qCDebug(scriptengine) << "Script Engine has stopped:" << getFilename();    }} QString ScriptEngine::getFilename() const {    QStringList fileNameParts = _fileNameString.split("/");    QString lastPart;    if (!fileNameParts.isEmpty()) {        lastPart = fileNameParts.last();    }    return lastPart;}  // FIXME - switch this to the new model of ScriptCache callbacksvoid ScriptEngine::loadURL(const QUrl& scriptURL, bool reload) {    if (_isRunning) {        return;    }     QUrl url = expandScriptUrl(scriptURL);    _fileNameString = url.toString();    _isReloading = reload;     bool isPending;    auto scriptCache = DependencyManager::get<ScriptCache>();    scriptCache->getScript(url, this, isPending, reload);} // FIXME - switch this to the new model of ScriptCache callbacksvoid ScriptEngine::scriptContentsAvailable(const QUrl& url, const QString& scriptContents) {    _scriptContents = scriptContents;    static const QString DEBUG_FLAG("#debug");    if (QRegularExpression(DEBUG_FLAG).match(scriptContents).hasMatch()) {        _debuggable = true;    }    emit scriptLoaded(url.toString());} // FIXME - switch this to the new model of ScriptCache callbacksvoid ScriptEngine::errorInLoadingScript(const QUrl& url) {    qCDebug(scriptengine) << "ERROR Loading file:" << url.toString() << "line:" << __LINE__;    emit errorLoadingScript(_fileNameString); // ??} // Even though we never pass AnimVariantMap directly to and from javascript, the queued invokeMethod of// callAnimationStateHandler requires that the type be registered.// These two are meaningful, if we ever do want to use them...static QScriptValue animVarMapToScriptValue(QScriptEngine* engine, const AnimVariantMap& parameters) {    QStringList unused;    return parameters.animVariantMapToScriptValue(engine, unused, false);}static void animVarMapFromScriptValue(const QScriptValue& value, AnimVariantMap& parameters) {    parameters.animVariantMapFromScriptValue(value);}// ... while these two are not. But none of the four are ever used.static QScriptValue resultHandlerToScriptValue(QScriptEngine* engine, const AnimVariantResultHandler& resultHandler) {    qCCritical(scriptengine) << "Attempt to marshall result handler to javascript";    assert(false);    return QScriptValue();}static void resultHandlerFromScriptValue(const QScriptValue& value, AnimVariantResultHandler& resultHandler) {    qCCritical(scriptengine) << "Attempt to marshall result handler from javascript";    assert(false);} // Templated qScriptRegisterMetaType fails to compile with raw pointersusing ScriptableResourceRawPtr = ScriptableResource*; static QScriptValue scriptableResourceToScriptValue(QScriptEngine* engine, const ScriptableResourceRawPtr& resource) {    // The first script to encounter this resource will track its memory.    // In this way, it will be more likely to GC.    // This fails in the case that the resource is used across many scripts, but    // in that case it would be too difficult to tell which one should track the memory, and    // this serves the common case (use in a single script).    auto data = resource->getResource();    if (data && !resource->isInScript()) {        resource->setInScript(true);        QObject::connect(data.data(), SIGNAL(updateSize(qint64)), engine, SLOT(updateMemoryCost(qint64)));    }     auto object = engine->newQObject(        const_cast<ScriptableResourceRawPtr>(resource),        QScriptEngine::ScriptOwnership);    return object;} static void scriptableResourceFromScriptValue(const QScriptValue& value, ScriptableResourceRawPtr& resource) {    resource = static_cast<ScriptableResourceRawPtr>(value.toQObject());} static QScriptValue createScriptableResourcePrototype(QScriptEngine* engine) {    auto prototype = engine->newObject();     // Expose enum State to JS/QML via properties    QObject* state = new QObject(engine);    state->setObjectName("ResourceState");    auto metaEnum = QMetaEnum::fromType<ScriptableResource::State>();    for (int i = 0; i < metaEnum.keyCount(); ++i) {        state->setProperty(metaEnum.key(i), metaEnum.value(i));    }     auto prototypeState = engine->newQObject(state, QScriptEngine::QtOwnership, QScriptEngine::ExcludeSlots | QScriptEngine::ExcludeSuperClassMethods);    prototype.setProperty("State", prototypeState);     return prototype;} void ScriptEngine::init() {    if (_isInitialized) {        return; // only initialize once    }     _isInitialized = true;     auto entityScriptingInterface = DependencyManager::get<EntityScriptingInterface>();    entityScriptingInterface->init();     // register various meta-types    registerMetaTypes(this);    registerMIDIMetaTypes(this);    registerEventTypes(this);    registerMenuItemProperties(this);    registerAnimationTypes(this);    registerAvatarTypes(this);    registerAudioMetaTypes(this);     qScriptRegisterMetaType(this, EntityPropertyFlagsToScriptValue, EntityPropertyFlagsFromScriptValue);    qScriptRegisterMetaType(this, EntityItemPropertiesToScriptValue, EntityItemPropertiesFromScriptValueHonorReadOnly);    qScriptRegisterMetaType(this, EntityItemIDtoScriptValue, EntityItemIDfromScriptValue);    qScriptRegisterMetaType(this, RayToEntityIntersectionResultToScriptValue, RayToEntityIntersectionResultFromScriptValue);    qScriptRegisterMetaType(this, RayToAvatarIntersectionResultToScriptValue, RayToAvatarIntersectionResultFromScriptValue);    qScriptRegisterSequenceMetaType<QVector<QUuid>>(this);    qScriptRegisterSequenceMetaType<QVector<EntityItemID>>(this);     qScriptRegisterSequenceMetaType<QVector<glm::vec2> >(this);    qScriptRegisterSequenceMetaType<QVector<glm::quat> >(this);    qScriptRegisterSequenceMetaType<QVector<QString> >(this);     QScriptValue xmlHttpRequestConstructorValue = newFunction(XMLHttpRequestClass::constructor);    globalObject().setProperty("XMLHttpRequest", xmlHttpRequestConstructorValue);

Lines 1–500 of 1653.