//// ScriptEngine.cpp// libraries/script-engine/src//// Created by Brad Hefta-Gaub on 12/14/13.// Copyright 2013 High Fidelity, Inc.//// Distributed under the Apache License, Version 2.0.// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html// #include "ScriptEngine.h" #include <chrono>#include <thread> #include <QtCore/QCoreApplication>#include <QtCore/QEventLoop>#include <QtCore/QFileInfo>#include <QtCore/QTimer>#include <QtCore/QThread>#include <QtCore/QRegularExpression> #include <QtCore/QFuture>#include <QtConcurrent/QtConcurrentRun> #include <QtWidgets/QMainWindow>#include <QtWidgets/QApplication>#include <QtWidgets/QMenuBar>#include <QtWidgets/QMenu> #include <QtNetwork/QNetworkRequest>#include <QtNetwork/QNetworkReply> #include <QtScript/QScriptContextInfo>#include <QtScript/QScriptValue>#include <QtScript/QScriptValueIterator> #include <shared/LocalFileAccessGate.h>#include <shared/QtHelpers.h>#include <shared/AbstractLoggerInterface.h>#include <AudioConstants.h>#include <AudioEffectOptions.h>#include <AvatarData.h>#include <DebugDraw.h>#include <EntityScriptingInterface.h>#include <MessagesClient.h>#include <NetworkAccessManager.h>#include <PathUtils.h>#include <ResourceScriptingInterface.h>#include <UserActivityLoggerScriptingInterface.h>#include <NodeList.h>#include <ScriptAvatarData.h>#include <udt/PacketHeaders.h>#include <UUID.h> #include <controllers/ScriptingInterface.h>#include <AnimationObject.h> #include "ArrayBufferViewClass.h"#include "AssetScriptingInterface.h"#include "BatchLoader.h"#include "BaseScriptEngine.h"#include "DataViewClass.h"#include "EventTypes.h"#include "FileScriptingInterface.h" // unzip project#include "MenuItemProperties.h"#include "ScriptAudioInjector.h"#include "ScriptAvatarData.h"#include "ScriptCache.h"#include "ScriptEngineLogging.h"#include "TypedArrays.h"#include "XMLHttpRequestClass.h"#include "WebSocketClass.h"#include "RecordingScriptingInterface.h"#include "ScriptEngines.h"#include "StackTestScriptingInterface.h"#include "ModelScriptingInterface.h" #include <Profile.h> #include "../../midi/src/Midi.h" // FIXME why won't a simpler include work?#include "MIDIEvent.h" #include "SettingHandle.h"#include <AddressManager.h>#include <NetworkingConstants.h>#include <ThreadHelpers.h> const QString ScriptEngine::_SETTINGS_ENABLE_EXTENDED_EXCEPTIONS { "com.highfidelity.experimental.enableExtendedJSExceptions"}; static const int MAX_MODULE_ID_LENGTH { 4096 };static const int MAX_DEBUG_VALUE_LENGTH { 80 }; static const QScriptEngine::QObjectWrapOptions DEFAULT_QOBJECT_WRAP_OPTIONS = QScriptEngine::ExcludeDeleteLater | QScriptEngine::ExcludeChildObjects;static const QScriptValue::PropertyFlags READONLY_PROP_FLAGS { QScriptValue::ReadOnly | QScriptValue::Undeletable };static const QScriptValue::PropertyFlags READONLY_HIDDEN_PROP_FLAGS { READONLY_PROP_FLAGS | QScriptValue::SkipInEnumeration }; static const bool HIFI_AUTOREFRESH_FILE_SCRIPTS { true }; Q_DECLARE_METATYPE(QScriptEngine::FunctionSignature)int functionSignatureMetaID = qRegisterMetaType<QScriptEngine::FunctionSignature>(); int scriptEnginePointerMetaID = qRegisterMetaType<ScriptEnginePointer>(); Q_DECLARE_METATYPE(ExternalResource::Bucket); static QScriptValue debugPrint(QScriptContext* context, QScriptEngine* engine) { // assemble the message by concatenating our arguments QString message = ""; for (int i = 0; i < context->argumentCount(); i++) { if (i > 0) { message += " "; } message += context->argument(i).toString(); } // was this generated by a script engine? If we don't recognize it then send the message and exit ScriptEngine* scriptEngine = qobject_cast<ScriptEngine*>(engine); if (!scriptEngine) { qCDebug(scriptengine_script, "%s", qUtf8Printable(message)); return QScriptValue(); } // This message was sent by one of our script engines, let's try to see if we can find the source. // Note that the first entry in the backtrace should be "print" and is somewhat useless to us AbstractLoggerInterface* loggerInterface = AbstractLoggerInterface::get(); if (loggerInterface && loggerInterface->showSourceDebugging()) { QScriptContext* userContext = context; while (userContext && QScriptContextInfo(userContext).functionType() == QScriptContextInfo::NativeFunction) { userContext = userContext->parentContext(); } QString location; if (userContext) { QScriptContextInfo contextInfo(userContext); QString fileName = contextInfo.fileName(); int lineNumber = contextInfo.lineNumber(); QString functionName = contextInfo.functionName(); location = functionName; if (!fileName.isEmpty()) { if (location.isEmpty()) { location = fileName; } else { location = QString("%1 at %2").arg(location).arg(fileName); } } if (lineNumber != -1) { location = QString("%1:%2").arg(location).arg(lineNumber); } } if (location.isEmpty()) { location = scriptEngine->getFilename(); } // give the script engine a chance to notify the system about this message scriptEngine->print(message); // send the message to debug log qCDebug(scriptengine_script, "[%s] %s", qUtf8Printable(location), qUtf8Printable(message)); } else { scriptEngine->print(message); // prefix the script engine name to help disambiguate messages in the main debug log qCDebug(scriptengine_script, "[%s] %s", qUtf8Printable(scriptEngine->getFilename()), qUtf8Printable(message)); } return QScriptValue();} Q_DECLARE_METATYPE(controller::InputController*)//static int inputControllerPointerId = qRegisterMetaType<controller::InputController*>(); QScriptValue inputControllerToScriptValue(QScriptEngine *engine, controller::InputController* const &in) { return engine->newQObject(in, QScriptEngine::QtOwnership, DEFAULT_QOBJECT_WRAP_OPTIONS);} void inputControllerFromScriptValue(const QScriptValue &object, controller::InputController* &out) { out = qobject_cast<controller::InputController*>(object.toQObject());} // FIXME Come up with a way to properly encode entity IDs in filename// The purpose of the following two function is to embed entity ids into entity script filenames// so that they show up in stacktraces//// Extract the url portion of a url that has been encoded with encodeEntityIdIntoEntityUrl(...)QString extractUrlFromEntityUrl(const QString& url) { auto parts = url.split(' ', 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::runInThread() { Q_ASSERT_X(!_isThreaded, "ScriptEngine::runInThread()", "runInThread should not be called more than once"); if (_isThreaded) { return; } _isThreaded = true; // The thread interface cannot live on itself, and we want to move this into the thread, so // the thread cannot have this as a parent. QThread* workerThread = new QThread(); QString name = QString("js:") + getFilename().replace("about:",""); workerThread->setObjectName(name); moveToThread(workerThread); // NOTE: If you connect any essential signals for proper shutdown or cleanup of // the script engine, make sure to add code to "reconnect" them to the // disconnectNonEssentialSignals() method connect(workerThread, &QThread::started, this, [this, name] { setThreadName(name.toStdString()); run(); }); connect(this, &QObject::destroyed, workerThread, &QThread::quit); connect(workerThread, &QThread::finished, workerThread, &QObject::deleteLater); workerThread->start();} void ScriptEngine::executeOnScriptThread(std::function<void()> function, const Qt::ConnectionType& type ) { if (QThread::currentThread() != thread()) { QMetaObject::invokeMethod(this, "executeOnScriptThread", type, Q_ARG(std::function<void()>, function)); return; } function();} void ScriptEngine::waitTillDoneRunning(bool shutdown) { // Engine should be stopped already, but be defensive stop(); auto workerThread = thread(); if (workerThread == QThread::currentThread()) { qCWarning(scriptengine) << "ScriptEngine::waitTillDoneRunning called, but the script is on the same thread:" << getFilename(); return; } if (_isThreaded && workerThread) { // We should never be waiting (blocking) on our own thread assert(workerThread != QThread::currentThread()); #if 0 // 26 Feb 2021 - Disabled this OSX-specific code because it causes OSX to crash on shutdown; without this code, OSX // doesn't crash on shutdown. Qt 5.12.3 and Qt 5.15.2. // // On mac, don't call QCoreApplication::processEvents() here. This is to prevent // [NSApplication terminate:] from prematurely destroying the static destructors // while we are waiting for the scripts to shutdown. We will pump the message // queue later in the Application destructor. if (workerThread->isRunning()) { workerThread->quit(); if (isEvaluating()) { qCWarning(scriptengine) << "Script Engine has been running too long, aborting:" << getFilename(); abortEvaluation(); } else { auto context = currentContext(); if (context) { qCWarning(scriptengine) << "Script Engine has been running too long, throwing:" << getFilename(); context->throwError("Timed out during shutdown"); } } // Wait for the scripting thread to stop running, as // flooding it with aborts/exceptions will persist it longer static const auto MAX_SCRIPT_QUITTING_TIME = 0.5 * MSECS_PER_SECOND; if (!workerThread->wait(MAX_SCRIPT_QUITTING_TIME)) { workerThread->terminate(); } }#else auto startedWaiting = usecTimestampNow(); while (workerThread->isRunning()) { // If the final evaluation takes too long, then tell the script engine to stop running auto elapsedUsecs = usecTimestampNow() - startedWaiting; static const auto MAX_SCRIPT_EVALUATION_TIME = USECS_PER_SECOND; if (elapsedUsecs > MAX_SCRIPT_EVALUATION_TIME) { workerThread->quit(); if (isEvaluating()) { qCWarning(scriptengine) << "Script Engine has been running too long, aborting:" << getFilename(); abortEvaluation(); } else { auto context = currentContext(); if (context) { qCWarning(scriptengine) << "Script Engine has been running too long, throwing:" << getFilename(); context->throwError("Timed out during shutdown"); } } // Wait for the scripting thread to stop running, as // flooding it with aborts/exceptions will persist it longer static const auto MAX_SCRIPT_QUITTING_TIME = 0.5 * MSECS_PER_SECOND; if (!workerThread->wait(MAX_SCRIPT_QUITTING_TIME)) { workerThread->terminate(); } } if (shutdown) { // NOTE: This will be called on the main application thread (among other threads) from stopAllScripts. // The thread will need to continue to process events, because // the scripts will likely need to marshall messages across to the main thread, e.g. // if they access Settings or Menu in any of their shutdown code. So: // Process events for this thread, allowing invokeMethod calls to pass between threads. QCoreApplication::processEvents(); } // Avoid a pure busy wait QThread::yieldCurrentThread(); }#endif scriptInfoMessage("Script Engine has stopped:" + getFilename()); }} QString ScriptEngine::getFilename() const { QStringList fileNameParts = _fileNameString.split("/"); QString lastPart; if (!fileNameParts.isEmpty()) { lastPart = fileNameParts.last(); } return lastPart;} bool ScriptEngine::hasValidScriptSuffix(const QString& scriptFileName) { QFileInfo fileInfo(scriptFileName); QString scriptSuffixToLower = fileInfo.completeSuffix().toLower(); return scriptSuffixToLower.contains(QString("js"), Qt::CaseInsensitive);} void ScriptEngine::loadURL(const QUrl& scriptURL, bool reload) { if (_isRunning) { return; } QUrl url = expandScriptUrl(scriptURL); _fileNameString = url.toString(); _isReloading = reload; // Check that script has a supported file extension if (!hasValidScriptSuffix(_fileNameString)) { scriptErrorMessage("File extension of file: " + _fileNameString + " is not a currently supported script type"); emit errorLoadingScript(_fileNameString); return; } const auto maxRetries = 0; // for consistency with previous scriptCache->getScript() behavior auto scriptCache = DependencyManager::get<ScriptCache>(); scriptCache->getScriptContents(url.toString(), [this](const QString& url, const QString& scriptContents, bool isURL, bool success, const QString&status) { qCDebug(scriptengine) << "loadURL" << url << status << QThread::currentThread(); if (!success) { scriptErrorMessage("ERROR Loading file (" + status + "):" + url); emit errorLoadingScript(_fileNameString); return; } _scriptContents = scriptContents; emit scriptLoaded(url); }, reload, maxRetries);Lines 1–500 of 2844. 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 <shared/LocalFileAccessGate.h> #include <shared/QtHelpers.h> #include <shared/AbstractLoggerInterface.h> #include <AudioConstants.h> #include <AudioEffectOptions.h> #include <AvatarData.h> #include <DebugDraw.h> #include <EntityScriptingInterface.h> #include <MessagesClient.h> #include <NetworkAccessManager.h> #include <PathUtils.h> #include <ResourceScriptingInterface.h> #include <UserActivityLoggerScriptingInterface.h> #include <NodeList.h> #include <ScriptAvatarData.h> #include <udt/PacketHeaders.h> #include <UUID.h> #include <controllers/ScriptingInterface.h> #include <AnimationObject.h> #include "ArrayBufferViewClass.h" #include "AssetScriptingInterface.h" #include "BatchLoader.h" #include "BaseScriptEngine.h" #include "DataViewClass.h" #include "EventTypes.h" #include "FileScriptingInterface.h" // unzip project #include "MenuItemProperties.h" #include "ScriptAudioInjector.h" #include "ScriptAvatarData.h" #include "ScriptCache.h" #include "ScriptEngineLogging.h" #include "TypedArrays.h" #include "XMLHttpRequestClass.h" #include "WebSocketClass.h" #include "RecordingScriptingInterface.h" #include "ScriptEngines.h" #include "StackTestScriptingInterface.h" #include "ModelScriptingInterface.h" #include <Profile.h> #include "../../midi/src/Midi.h" // FIXME why won't a simpler include work? #include "MIDIEvent.h" #include "SettingHandle.h" #include <AddressManager.h> #include <NetworkingConstants.h> #include <ThreadHelpers.h> const QString ScriptEngine::_SETTINGS_ENABLE_EXTENDED_EXCEPTIONS { "com.highfidelity.experimental.enableExtendedJSExceptions" }; static const int MAX_MODULE_ID_LENGTH { 4096 }; static const int MAX_DEBUG_VALUE_LENGTH { 80 }; static const QScriptEngine::QObjectWrapOptions DEFAULT_QOBJECT_WRAP_OPTIONS = QScriptEngine::ExcludeDeleteLater | QScriptEngine::ExcludeChildObjects; static const QScriptValue::PropertyFlags READONLY_PROP_FLAGS { QScriptValue::ReadOnly | QScriptValue::Undeletable }; static const QScriptValue::PropertyFlags READONLY_HIDDEN_PROP_FLAGS { READONLY_PROP_FLAGS | QScriptValue::SkipInEnumeration }; static const bool HIFI_AUTOREFRESH_FILE_SCRIPTS { true }; Q_DECLARE_METATYPE(QScriptEngine::FunctionSignature) int functionSignatureMetaID = qRegisterMetaType<QScriptEngine::FunctionSignature>(); int scriptEnginePointerMetaID = qRegisterMetaType<ScriptEnginePointer>(); Q_DECLARE_METATYPE(ExternalResource::Bucket); static QScriptValue debugPrint(QScriptContext* context, QScriptEngine* engine) { // assemble the message by concatenating our arguments QString message = ""; for (int i = 0; i < context->argumentCount(); i++) { if (i > 0) { message += " "; } message += context->argument(i).toString(); } // was this generated by a script engine? If we don't recognize it then send the message and exit ScriptEngine* scriptEngine = qobject_cast<ScriptEngine*>(engine); if (!scriptEngine) { qCDebug(scriptengine_script, "%s", qUtf8Printable(message)); return QScriptValue(); } // This message was sent by one of our script engines, let's try to see if we can find the source. // Note that the first entry in the backtrace should be "print" and is somewhat useless to us AbstractLoggerInterface* loggerInterface = AbstractLoggerInterface::get(); if (loggerInterface && loggerInterface->showSourceDebugging()) { QScriptContext* userContext = context; while (userContext && QScriptContextInfo(userContext).functionType() == QScriptContextInfo::NativeFunction) { userContext = userContext->parentContext(); } QString location; if (userContext) { QScriptContextInfo contextInfo(userContext); QString fileName = contextInfo.fileName(); int lineNumber = contextInfo.lineNumber(); QString functionName = contextInfo.functionName(); location = functionName; if (!fileName.isEmpty()) { if (location.isEmpty()) { location = fileName; } else { location = QString("%1 at %2").arg(location).arg(fileName); } } if (lineNumber != -1) { location = QString("%1:%2").arg(location).arg(lineNumber); } } if (location.isEmpty()) { location = scriptEngine->getFilename(); } // give the script engine a chance to notify the system about this message scriptEngine->print(message); // send the message to debug log qCDebug(scriptengine_script, "[%s] %s", qUtf8Printable(location), qUtf8Printable(message)); } else { scriptEngine->print(message); // prefix the script engine name to help disambiguate messages in the main debug log qCDebug(scriptengine_script, "[%s] %s", qUtf8Printable(scriptEngine->getFilename()), qUtf8Printable(message)); } return QScriptValue(); } Q_DECLARE_METATYPE(controller::InputController*) //static int inputControllerPointerId = qRegisterMetaType<controller::InputController*>(); QScriptValue inputControllerToScriptValue(QScriptEngine *engine, controller::InputController* const &in) { return engine->newQObject(in, QScriptEngine::QtOwnership, DEFAULT_QOBJECT_WRAP_OPTIONS); } void inputControllerFromScriptValue(const QScriptValue &object, controller::InputController* &out) { out = qobject_cast<controller::InputController*>(object.toQObject()); } // FIXME Come up with a way to properly encode entity IDs in filename // The purpose of the following two function is to embed entity ids into entity script filenames // so that they show up in stacktraces // // Extract the url portion of a url that has been encoded with encodeEntityIdIntoEntityUrl(...) QString extractUrlFromEntityUrl(const QString& url) { auto parts = url.split(' ', 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::runInThread() { Q_ASSERT_X(!_isThreaded, "ScriptEngine::runInThread()", "runInThread should not be called more than once"); if (_isThreaded) { return; } _isThreaded = true; // The thread interface cannot live on itself, and we want to move this into the thread, so // the thread cannot have this as a parent. QThread* workerThread = new QThread(); QString name = QString("js:") + getFilename().replace("about:",""); workerThread->setObjectName(name); moveToThread(workerThread); // NOTE: If you connect any essential signals for proper shutdown or cleanup of // the script engine, make sure to add code to "reconnect" them to the // disconnectNonEssentialSignals() method connect(workerThread, &QThread::started, this, [this, name] { setThreadName(name.toStdString()); run(); }); connect(this, &QObject::destroyed, workerThread, &QThread::quit); connect(workerThread, &QThread::finished, workerThread, &QObject::deleteLater); workerThread->start(); } void ScriptEngine::executeOnScriptThread(std::function<void()> function, const Qt::ConnectionType& type ) { if (QThread::currentThread() != thread()) { QMetaObject::invokeMethod(this, "executeOnScriptThread", type, Q_ARG(std::function<void()>, function)); return; } function(); } void ScriptEngine::waitTillDoneRunning(bool shutdown) { // Engine should be stopped already, but be defensive stop(); auto workerThread = thread(); if (workerThread == QThread::currentThread()) { qCWarning(scriptengine) << "ScriptEngine::waitTillDoneRunning called, but the script is on the same thread:" << getFilename(); return; } if (_isThreaded && workerThread) { // We should never be waiting (blocking) on our own thread assert(workerThread != QThread::currentThread()); #if 0 // 26 Feb 2021 - Disabled this OSX-specific code because it causes OSX to crash on shutdown; without this code, OSX // doesn't crash on shutdown. Qt 5.12.3 and Qt 5.15.2. // // On mac, don't call QCoreApplication::processEvents() here. This is to prevent // [NSApplication terminate:] from prematurely destroying the static destructors // while we are waiting for the scripts to shutdown. We will pump the message // queue later in the Application destructor. if (workerThread->isRunning()) { workerThread->quit(); if (isEvaluating()) { qCWarning(scriptengine) << "Script Engine has been running too long, aborting:" << getFilename(); abortEvaluation(); } else { auto context = currentContext(); if (context) { qCWarning(scriptengine) << "Script Engine has been running too long, throwing:" << getFilename(); context->throwError("Timed out during shutdown"); } } // Wait for the scripting thread to stop running, as // flooding it with aborts/exceptions will persist it longer static const auto MAX_SCRIPT_QUITTING_TIME = 0.5 * MSECS_PER_SECOND; if (!workerThread->wait(MAX_SCRIPT_QUITTING_TIME)) { workerThread->terminate(); } } #else auto startedWaiting = usecTimestampNow(); while (workerThread->isRunning()) { // If the final evaluation takes too long, then tell the script engine to stop running auto elapsedUsecs = usecTimestampNow() - startedWaiting; static const auto MAX_SCRIPT_EVALUATION_TIME = USECS_PER_SECOND; if (elapsedUsecs > MAX_SCRIPT_EVALUATION_TIME) { workerThread->quit(); if (isEvaluating()) { qCWarning(scriptengine) << "Script Engine has been running too long, aborting:" << getFilename(); abortEvaluation(); } else { auto context = currentContext(); if (context) { qCWarning(scriptengine) << "Script Engine has been running too long, throwing:" << getFilename(); context->throwError("Timed out during shutdown"); } } // Wait for the scripting thread to stop running, as // flooding it with aborts/exceptions will persist it longer static const auto MAX_SCRIPT_QUITTING_TIME = 0.5 * MSECS_PER_SECOND; if (!workerThread->wait(MAX_SCRIPT_QUITTING_TIME)) { workerThread->terminate(); } } if (shutdown) { // NOTE: This will be called on the main application thread (among other threads) from stopAllScripts. // The thread will need to continue to process events, because // the scripts will likely need to marshall messages across to the main thread, e.g. // if they access Settings or Menu in any of their shutdown code. So: // Process events for this thread, allowing invokeMethod calls to pass between threads. QCoreApplication::processEvents(); } // Avoid a pure busy wait QThread::yieldCurrentThread(); } #endif scriptInfoMessage("Script Engine has stopped:" + getFilename()); } } QString ScriptEngine::getFilename() const { QStringList fileNameParts = _fileNameString.split("/"); QString lastPart; if (!fileNameParts.isEmpty()) { lastPart = fileNameParts.last(); } return lastPart; } bool ScriptEngine::hasValidScriptSuffix(const QString& scriptFileName) { QFileInfo fileInfo(scriptFileName); QString scriptSuffixToLower = fileInfo.completeSuffix().toLower(); return scriptSuffixToLower.contains(QString("js"), Qt::CaseInsensitive); } void ScriptEngine::loadURL(const QUrl& scriptURL, bool reload) { if (_isRunning) { return; } QUrl url = expandScriptUrl(scriptURL); _fileNameString = url.toString(); _isReloading = reload; // Check that script has a supported file extension if (!hasValidScriptSuffix(_fileNameString)) { scriptErrorMessage("File extension of file: " + _fileNameString + " is not a currently supported script type"); emit errorLoadingScript(_fileNameString); return; } const auto maxRetries = 0; // for consistency with previous scriptCache->getScript() behavior auto scriptCache = DependencyManager::get<ScriptCache>(); scriptCache->getScriptContents(url.toString(), [this](const QString& url, const QString& scriptContents, bool isURL, bool success, const QString&status) { qCDebug(scriptengine) << "loadURL" << url << status << QThread::currentThread(); if (!success) { scriptErrorMessage("ERROR Loading file (" + status + "):" + url); emit errorLoadingScript(_fileNameString); return; } _scriptContents = scriptContents; emit scriptLoaded(url); }, reload, maxRetries);