//// 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. Load more
// // 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 callbacks void 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 callbacks void 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 callbacks void 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 pointers using 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);