2800 lines · 120.0 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 <QtScriptTools/QScriptEngineDebugger> #include <shared/LocalFileAccessGate.h>#include <shared/QtHelpers.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 "SettingManager.h"// #include "SettingInterface.h"// #include "SettingHelpers.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>(); 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();    }     if (ScriptEngine *scriptEngine = qobject_cast<ScriptEngine*>(engine)) {        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));    } else {        qCDebug(scriptengine_script, "%s", 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(' ', 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 + "]";} 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::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, qSharedPointerCast<ScriptEngine>(sharedFromThis()));            _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 || _isStopping)) {                emit update(deltaTime);            }        }        _lastUpdate = now;         // only clear exceptions if we are not in the middle of evaluating        if (!isEvaluating() && hasUncaughtException()) {            qCWarning(scriptengine) << __FUNCTION__ << "---------- UNCAUGHT EXCEPTION --------";            qCWarning(scriptengine) << "runDebuggable" << uncaughtException().toString();            logException(__FUNCTION__);            clearExceptions();        }    });     timer->start(10);}  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();    workerThread->setObjectName(QString("js:") + getFilename().replace("about:",""));    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, &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() {    // 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()); #ifdef Q_OS_MAC        // 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();                }            }             // 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();    }

Lines 1–500 of 2800.