//// 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/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" 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); DependencyManager::get<ScriptEngines>()->addScriptEngine(qSharedPointerCast<ScriptEngine>(engineSP)); 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()) { 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::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() { auto scriptEngines = DependencyManager::get<ScriptEngines>(); if (scriptEngines) { scriptEngines->removeScriptEngine(qSharedPointerCast<ScriptEngine>(sharedFromThis())); }} 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, 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) { 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) { 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("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, &ScriptEngine::doneRunning, 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() { 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 (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(); } 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; { static const QString DEBUG_FLAG("#debug"); if (QRegularExpression(DEBUG_FLAG).match(scriptContents).hasMatch()) {Lines 1–500 of 2749. 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 "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/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" 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); DependencyManager::get<ScriptEngines>()->addScriptEngine(qSharedPointerCast<ScriptEngine>(engineSP)); 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()) { 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::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() { auto scriptEngines = DependencyManager::get<ScriptEngines>(); if (scriptEngines) { scriptEngines->removeScriptEngine(qSharedPointerCast<ScriptEngine>(sharedFromThis())); } } 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, 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) { 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) { 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("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, &ScriptEngine::doneRunning, 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() { 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 (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(); } 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; { static const QString DEBUG_FLAG("#debug"); if (QRegularExpression(DEBUG_FLAG).match(scriptContents).hasMatch()) {