//// ScriptManager.cpp// libraries/script-engine/src//// Created by Brad Hefta-Gaub on 12/14/13.// Copyright 2013 High Fidelity, Inc.// Copyright 2022-2023 Overte e.V.//// Distributed under the Apache License, Version 2.0.// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html// SPDX-License-Identifier: Apache-2.0// #include "ScriptManager.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 <shared/LocalFileAccessGate.h>#include <shared/AbstractLoggerInterface.h>#include <DebugDraw.h>#include <MessagesClient.h>#include <OctreeConstants.h>#include <PathUtils.h>#include <PortableHighResolutionClock.h>#include <ResourceCache.h>#include <ResourceManager.h>#include <ResourceScriptingInterface.h>#include <UserActivityLoggerScriptingInterface.h>#include <NodeList.h> #include "AssetScriptingInterface.h"#include "BatchLoader.h"#include "EventTypes.h"#include "MenuItemProperties.h"#include "ScriptCache.h"#include "ScriptContext.h"#include "XMLHttpRequestClass.h"#include "WebSocketClass.h"#include "ScriptEngine.h"#include "ScriptEngineCast.h"#include "ScriptEngineLogging.h"#include "ScriptEngines.h"#include "StackTestScriptingInterface.h"#include "ScriptValue.h"#include "ScriptProgram.h"#include "ScriptValueIterator.h"#include "ScriptValueUtils.h"#include "ScriptManagerScriptingInterface.h" #include <Profile.h> #include "MIDIEvent.h" #include "SettingHandle.h"#include <AddressManager.h>#include <NetworkingConstants.h>#include <ThreadHelpers.h>#include <iostream> const QString ScriptManager::_SETTINGS_ENABLE_EXTENDED_EXCEPTIONS { "com.highfidelity.experimental.enableExtendedJSExceptions"}; const QString ScriptManager::SCRIPT_EXCEPTION_FORMAT{ "[%0] %1 in %2:%3" };const QString ScriptManager::SCRIPT_BACKTRACE_SEP{ "\n " }; static const int MAX_MODULE_ID_LENGTH { 4096 };static const int MAX_DEBUG_VALUE_LENGTH { 80 }; static const ScriptValue::PropertyFlags READONLY_PROP_FLAGS{ ScriptValue::ReadOnly | ScriptValue::Undeletable };static const ScriptValue::PropertyFlags READONLY_HIDDEN_PROP_FLAGS{ READONLY_PROP_FLAGS | ScriptValue::SkipInEnumeration }; static const bool HIFI_AUTOREFRESH_FILE_SCRIPTS { true }; int scriptManagerPointerMetaID = qRegisterMetaType<ScriptManagerPointer>(); Q_DECLARE_METATYPE(ExternalResource::Bucket); Q_DECLARE_METATYPE(ScriptValue); // --- Static script initialization registry static ScriptManager::StaticInitializerNode* rootInitializer = nullptr;static ScriptManager::StaticTypesInitializerNode* rootTypesInitializer = nullptr; using ScriptableResourceRawPtr = ScriptableResource*;ScriptValue externalResourceBucketToScriptValue(ScriptEngine* engine, ExternalResource::Bucket const& in);bool externalResourceBucketFromScriptValue(const ScriptValue& object, ExternalResource::Bucket& out);static ScriptValue scriptableResourceToScriptValue(ScriptEngine* engine, const ScriptableResourceRawPtr& resource);static bool scriptableResourceFromScriptValue(const ScriptValue& value, ScriptableResourceRawPtr& resource);STATIC_SCRIPT_TYPES_INITIALIZER((+[](ScriptManager* manager){ auto scriptEngine = manager->engine().get(); registerEventTypes(scriptEngine); scriptRegisterMetaType<ExternalResource::Bucket, externalResourceBucketToScriptValue, externalResourceBucketFromScriptValue>(scriptEngine); scriptRegisterMetaType<ScriptableResourceRawPtr, scriptableResourceToScriptValue, scriptableResourceFromScriptValue>(scriptEngine);})); void ScriptManager::registerNewStaticInitializer(StaticInitializerNode* dest) { // this function is assumed to be called on LoadLibrary, where we are explicitly operating in single-threaded mode // Therefore there is no mutex or threadsafety here and the structure is assumed not to change after loading dest->prev = rootInitializer; rootInitializer = dest;}static void runStaticInitializers(ScriptManager* manager) { ScriptManager::StaticInitializerNode* here = rootInitializer; while (here != nullptr) { (*here->init)(manager); here = here->prev; }}void ScriptManager::registerNewStaticTypesInitializer(StaticTypesInitializerNode* dest) { // this function is assumed to be called on LoadLibrary, where we are explicitly operating in single-threaded mode // Therefore there is no mutex or threadsafety here and the structure is assumed not to change after loading dest->prev = rootTypesInitializer; rootTypesInitializer = dest;}static void runStaticTypesInitializers(ScriptManager* manager) { ScriptManager::StaticTypesInitializerNode* here = rootTypesInitializer; while (here != nullptr) { (*here->init)(manager); here = here->prev; }} // --- static ScriptValue debugPrint(ScriptContext* context, ScriptEngine* 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 ScriptManager* scriptManager = engine->manager(); if (!scriptManager) { qCDebug(scriptengine_script, "%s", qUtf8Printable(message)); return ScriptValue(); } // 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()) { ScriptContext* userContext = context; ScriptContextPointer parentContext; // using this variable to maintain parent variable lifespan while (userContext && userContext->functionContext()->functionType() == ScriptFunctionContext::NativeFunction) { parentContext = userContext->parentContext(); userContext = parentContext.get(); } QString location; if (userContext) { auto contextInfo = userContext->functionContext(); 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 = scriptManager->getFilename(); } // give the script engine a chance to notify the system about this message scriptManager->print(message); // send the message to debug log qCDebug(scriptengine_script, "[%s] %s", qUtf8Printable(location), qUtf8Printable(message)); } else { scriptManager->print(message); // prefix the script engine name to help disambiguate messages in the main debug log qCDebug(scriptengine_script, "[%s] %s", qUtf8Printable(scriptManager->getFilename()), qUtf8Printable(message)); } return ScriptValue();} // 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(' ', Qt::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 ScriptManager::logException(const ScriptValue& exception) { auto message = formatException(exception, _enableExtendedJSExceptions.get()); auto context = _engine->currentContext(); if (context) { scriptErrorMessage(message, context->currentFileName(), context->currentLineNumber()); } else { scriptErrorMessage(message, "", -1); } return message;} std::shared_ptr<ScriptException> ScriptManager::getUncaughtException() const { return _engine->uncaughtException();} ScriptManagerPointer scriptManagerFactory(ScriptManager::Context context, const QString& scriptContents, const QString& fileNameString) { ScriptManagerPointer manager = newScriptManager(context, scriptContents, fileNameString); auto scriptEngines = DependencyManager::get<ScriptEngines>(); scriptEngines->addScriptEngine(manager); manager->setScriptEngines(scriptEngines); return manager;} ScriptManagerPointer newScriptManager(ScriptManager::Context context, const QString& scriptContents, const QString& fileNameString) { ScriptManagerPointer manager(new ScriptManager(context, scriptContents, fileNameString), [](ScriptManager* obj) { obj->deleteLater(); }); return manager;} int ScriptManager::processLevelMaxRetries { ScriptRequest::MAX_RETRIES }; ScriptManager::ScriptManager(Context context, const QString& scriptContents, const QString& fileNameString) : QObject(), _context(context), _engine(newScriptEngine(this)), _scriptContents(scriptContents), _timerFunctionMap(), _fileNameString(fileNameString), _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; case Context::NETWORKLESS_TEST_SCRIPT: _type = Type::NETWORKLESS_TEST; break; } qRegisterMetaType<ScriptValue>(); qRegisterMetaType<std::function<void()>>(); _scriptingInterface = std::make_shared<ScriptManagerScriptingInterface>(this); if (isEntityServerScript()) { qCDebug(scriptengine) << "isEntityServerScript() -- limiting maxRetries to 1"; processLevelMaxRetries = 1; } #if 0 // V8TODO: Is this actually needed? Exceptions will already be logged on the // script engine side. // this is where all unhandled exceptions end up getting logged connect(this, &ScriptManager::unhandledException, this, [this](const ScriptValue& err) { qCCritical(scriptengine) << "Caught exception"; auto output = err.engine() == _engine ? err : _engine->makeError(err); if (!output.property("detail").isValid()) { output.setProperty("detail", "UnhandledException"); } // Unhandled exception kills the running script if (_abortOnUncaughtException) { stop(false); logException(output); } });#endif // Forward exceptions from the scripting engine connect(_engine.get(), &ScriptEngine::exception, this, &ScriptManager::unhandledException); if (_type == Type::ENTITY_CLIENT || _type == Type::ENTITY_SERVER) { QObject::connect(this, &ScriptManager::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); } } }); } //Gather entity script messages for transmission when running server side. if (_type == Type::ENTITY_SERVER) { ; } if (!_areMetaTypesInitialized) { initMetaTypes(); }} QString ScriptManager::getTypeAsString() const { auto value = QVariant::fromValue(_type).toString(); return value.isEmpty() ? "unknown" : value.toLower();} QString ScriptManager::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"; case NETWORKLESS_TEST_SCRIPT: return "networkless_test"; default: return "unknown"; } return "unknown";} bool ScriptManager::isDebugMode() const {#if defined(DEBUG) return true;#else return false;#endif} ScriptManager::~ScriptManager() { qDebug() << "ScriptManager::~ScriptManager() : Script manager deleted, type: " << _type << " name: " << _fileNameString; if (_type == ScriptManager::Type::ENTITY_CLIENT) { printf("ScriptManager::~ScriptManager"); }} void ScriptManager::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 ScriptManager::runInThread() { Q_ASSERT_X(!_isThreaded, "ScriptManager::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); _engine->setThread(workerThread); moveToThread(workerThread); _assetScriptingInterface->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 ScriptManager::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 ScriptManager::waitTillDoneRunning(bool shutdown) { // Engine should be stopped already, but be defensive stop(); auto workerThread = thread(); if (workerThread == QThread::currentThread()) { qCWarning(scriptengine) << "ScriptManager::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 (_engine->isEvaluating()) { qCWarning(scriptengine) << "Script Engine has been running too long, aborting:" << getFilename(); _engine->abortEvaluation(); } else { auto context = _engine->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 (!_isDoneRunning) { // If the final evaluation takes too long, then tell the script engine to stop running auto elapsedUsecs = usecTimestampNow() - startedWaiting; // TODO: This part was very unsafe and was causing crashes all the time. // I disabled it for now until we find a safer solution. // With it disabled now we get clean shutdowns and restarts. // V8TODO: temporarily increased script timeout. Maybe use different timeouts for release and unoptimized debug? /*static const auto MAX_SCRIPT_EVALUATION_TIME = 10 * USECS_PER_SECOND; if (elapsedUsecs > MAX_SCRIPT_EVALUATION_TIME) { workerThread->quit(); if (_engine->isEvaluating()) { qCWarning(scriptengine) << "Script Engine has been running too long (evaluation), aborting:" << getFilename(); _engine->abortEvaluation(); } else { auto context = _engine->currentContext(); if (context) { qCWarning(scriptengine) << "Script Engine has been running too long (event loop), throwing:" << getFilename(); context->throwError("Timed out during shutdown");Lines 1–500 of 2763. Load more
// // ScriptManager.cpp // libraries/script-engine/src // // Created by Brad Hefta-Gaub on 12/14/13. // Copyright 2013 High Fidelity, Inc. // Copyright 2022-2023 Overte e.V. // // Distributed under the Apache License, Version 2.0. // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // SPDX-License-Identifier: Apache-2.0 // #include "ScriptManager.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 <shared/LocalFileAccessGate.h> #include <shared/AbstractLoggerInterface.h> #include <DebugDraw.h> #include <MessagesClient.h> #include <OctreeConstants.h> #include <PathUtils.h> #include <PortableHighResolutionClock.h> #include <ResourceCache.h> #include <ResourceManager.h> #include <ResourceScriptingInterface.h> #include <UserActivityLoggerScriptingInterface.h> #include <NodeList.h> #include "AssetScriptingInterface.h" #include "BatchLoader.h" #include "EventTypes.h" #include "MenuItemProperties.h" #include "ScriptCache.h" #include "ScriptContext.h" #include "XMLHttpRequestClass.h" #include "WebSocketClass.h" #include "ScriptEngine.h" #include "ScriptEngineCast.h" #include "ScriptEngineLogging.h" #include "ScriptEngines.h" #include "StackTestScriptingInterface.h" #include "ScriptValue.h" #include "ScriptProgram.h" #include "ScriptValueIterator.h" #include "ScriptValueUtils.h" #include "ScriptManagerScriptingInterface.h" #include <Profile.h> #include "MIDIEvent.h" #include "SettingHandle.h" #include <AddressManager.h> #include <NetworkingConstants.h> #include <ThreadHelpers.h> #include <iostream> const QString ScriptManager::_SETTINGS_ENABLE_EXTENDED_EXCEPTIONS { "com.highfidelity.experimental.enableExtendedJSExceptions" }; const QString ScriptManager::SCRIPT_EXCEPTION_FORMAT{ "[%0] %1 in %2:%3" }; const QString ScriptManager::SCRIPT_BACKTRACE_SEP{ "\n " }; static const int MAX_MODULE_ID_LENGTH { 4096 }; static const int MAX_DEBUG_VALUE_LENGTH { 80 }; static const ScriptValue::PropertyFlags READONLY_PROP_FLAGS{ ScriptValue::ReadOnly | ScriptValue::Undeletable }; static const ScriptValue::PropertyFlags READONLY_HIDDEN_PROP_FLAGS{ READONLY_PROP_FLAGS | ScriptValue::SkipInEnumeration }; static const bool HIFI_AUTOREFRESH_FILE_SCRIPTS { true }; int scriptManagerPointerMetaID = qRegisterMetaType<ScriptManagerPointer>(); Q_DECLARE_METATYPE(ExternalResource::Bucket); Q_DECLARE_METATYPE(ScriptValue); // --- Static script initialization registry static ScriptManager::StaticInitializerNode* rootInitializer = nullptr; static ScriptManager::StaticTypesInitializerNode* rootTypesInitializer = nullptr; using ScriptableResourceRawPtr = ScriptableResource*; ScriptValue externalResourceBucketToScriptValue(ScriptEngine* engine, ExternalResource::Bucket const& in); bool externalResourceBucketFromScriptValue(const ScriptValue& object, ExternalResource::Bucket& out); static ScriptValue scriptableResourceToScriptValue(ScriptEngine* engine, const ScriptableResourceRawPtr& resource); static bool scriptableResourceFromScriptValue(const ScriptValue& value, ScriptableResourceRawPtr& resource); STATIC_SCRIPT_TYPES_INITIALIZER((+[](ScriptManager* manager){ auto scriptEngine = manager->engine().get(); registerEventTypes(scriptEngine); scriptRegisterMetaType<ExternalResource::Bucket, externalResourceBucketToScriptValue, externalResourceBucketFromScriptValue>(scriptEngine); scriptRegisterMetaType<ScriptableResourceRawPtr, scriptableResourceToScriptValue, scriptableResourceFromScriptValue>(scriptEngine); })); void ScriptManager::registerNewStaticInitializer(StaticInitializerNode* dest) { // this function is assumed to be called on LoadLibrary, where we are explicitly operating in single-threaded mode // Therefore there is no mutex or threadsafety here and the structure is assumed not to change after loading dest->prev = rootInitializer; rootInitializer = dest; } static void runStaticInitializers(ScriptManager* manager) { ScriptManager::StaticInitializerNode* here = rootInitializer; while (here != nullptr) { (*here->init)(manager); here = here->prev; } } void ScriptManager::registerNewStaticTypesInitializer(StaticTypesInitializerNode* dest) { // this function is assumed to be called on LoadLibrary, where we are explicitly operating in single-threaded mode // Therefore there is no mutex or threadsafety here and the structure is assumed not to change after loading dest->prev = rootTypesInitializer; rootTypesInitializer = dest; } static void runStaticTypesInitializers(ScriptManager* manager) { ScriptManager::StaticTypesInitializerNode* here = rootTypesInitializer; while (here != nullptr) { (*here->init)(manager); here = here->prev; } } // --- static ScriptValue debugPrint(ScriptContext* context, ScriptEngine* 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 ScriptManager* scriptManager = engine->manager(); if (!scriptManager) { qCDebug(scriptengine_script, "%s", qUtf8Printable(message)); return ScriptValue(); } // 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()) { ScriptContext* userContext = context; ScriptContextPointer parentContext; // using this variable to maintain parent variable lifespan while (userContext && userContext->functionContext()->functionType() == ScriptFunctionContext::NativeFunction) { parentContext = userContext->parentContext(); userContext = parentContext.get(); } QString location; if (userContext) { auto contextInfo = userContext->functionContext(); 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 = scriptManager->getFilename(); } // give the script engine a chance to notify the system about this message scriptManager->print(message); // send the message to debug log qCDebug(scriptengine_script, "[%s] %s", qUtf8Printable(location), qUtf8Printable(message)); } else { scriptManager->print(message); // prefix the script engine name to help disambiguate messages in the main debug log qCDebug(scriptengine_script, "[%s] %s", qUtf8Printable(scriptManager->getFilename()), qUtf8Printable(message)); } return ScriptValue(); } // 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(' ', Qt::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 ScriptManager::logException(const ScriptValue& exception) { auto message = formatException(exception, _enableExtendedJSExceptions.get()); auto context = _engine->currentContext(); if (context) { scriptErrorMessage(message, context->currentFileName(), context->currentLineNumber()); } else { scriptErrorMessage(message, "", -1); } return message; } std::shared_ptr<ScriptException> ScriptManager::getUncaughtException() const { return _engine->uncaughtException(); } ScriptManagerPointer scriptManagerFactory(ScriptManager::Context context, const QString& scriptContents, const QString& fileNameString) { ScriptManagerPointer manager = newScriptManager(context, scriptContents, fileNameString); auto scriptEngines = DependencyManager::get<ScriptEngines>(); scriptEngines->addScriptEngine(manager); manager->setScriptEngines(scriptEngines); return manager; } ScriptManagerPointer newScriptManager(ScriptManager::Context context, const QString& scriptContents, const QString& fileNameString) { ScriptManagerPointer manager(new ScriptManager(context, scriptContents, fileNameString), [](ScriptManager* obj) { obj->deleteLater(); }); return manager; } int ScriptManager::processLevelMaxRetries { ScriptRequest::MAX_RETRIES }; ScriptManager::ScriptManager(Context context, const QString& scriptContents, const QString& fileNameString) : QObject(), _context(context), _engine(newScriptEngine(this)), _scriptContents(scriptContents), _timerFunctionMap(), _fileNameString(fileNameString), _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; case Context::NETWORKLESS_TEST_SCRIPT: _type = Type::NETWORKLESS_TEST; break; } qRegisterMetaType<ScriptValue>(); qRegisterMetaType<std::function<void()>>(); _scriptingInterface = std::make_shared<ScriptManagerScriptingInterface>(this); if (isEntityServerScript()) { qCDebug(scriptengine) << "isEntityServerScript() -- limiting maxRetries to 1"; processLevelMaxRetries = 1; } #if 0 // V8TODO: Is this actually needed? Exceptions will already be logged on the // script engine side. // this is where all unhandled exceptions end up getting logged connect(this, &ScriptManager::unhandledException, this, [this](const ScriptValue& err) { qCCritical(scriptengine) << "Caught exception"; auto output = err.engine() == _engine ? err : _engine->makeError(err); if (!output.property("detail").isValid()) { output.setProperty("detail", "UnhandledException"); } // Unhandled exception kills the running script if (_abortOnUncaughtException) { stop(false); logException(output); } }); #endif // Forward exceptions from the scripting engine connect(_engine.get(), &ScriptEngine::exception, this, &ScriptManager::unhandledException); if (_type == Type::ENTITY_CLIENT || _type == Type::ENTITY_SERVER) { QObject::connect(this, &ScriptManager::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); } } }); } //Gather entity script messages for transmission when running server side. if (_type == Type::ENTITY_SERVER) { ; } if (!_areMetaTypesInitialized) { initMetaTypes(); } } QString ScriptManager::getTypeAsString() const { auto value = QVariant::fromValue(_type).toString(); return value.isEmpty() ? "unknown" : value.toLower(); } QString ScriptManager::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"; case NETWORKLESS_TEST_SCRIPT: return "networkless_test"; default: return "unknown"; } return "unknown"; } bool ScriptManager::isDebugMode() const { #if defined(DEBUG) return true; #else return false; #endif } ScriptManager::~ScriptManager() { qDebug() << "ScriptManager::~ScriptManager() : Script manager deleted, type: " << _type << " name: " << _fileNameString; if (_type == ScriptManager::Type::ENTITY_CLIENT) { printf("ScriptManager::~ScriptManager"); } } void ScriptManager::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 ScriptManager::runInThread() { Q_ASSERT_X(!_isThreaded, "ScriptManager::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); _engine->setThread(workerThread); moveToThread(workerThread); _assetScriptingInterface->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 ScriptManager::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 ScriptManager::waitTillDoneRunning(bool shutdown) { // Engine should be stopped already, but be defensive stop(); auto workerThread = thread(); if (workerThread == QThread::currentThread()) { qCWarning(scriptengine) << "ScriptManager::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 (_engine->isEvaluating()) { qCWarning(scriptengine) << "Script Engine has been running too long, aborting:" << getFilename(); _engine->abortEvaluation(); } else { auto context = _engine->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 (!_isDoneRunning) { // If the final evaluation takes too long, then tell the script engine to stop running auto elapsedUsecs = usecTimestampNow() - startedWaiting; // TODO: This part was very unsafe and was causing crashes all the time. // I disabled it for now until we find a safer solution. // With it disabled now we get clean shutdowns and restarts. // V8TODO: temporarily increased script timeout. Maybe use different timeouts for release and unoptimized debug? /*static const auto MAX_SCRIPT_EVALUATION_TIME = 10 * USECS_PER_SECOND; if (elapsedUsecs > MAX_SCRIPT_EVALUATION_TIME) { workerThread->quit(); if (_engine->isEvaluating()) { qCWarning(scriptengine) << "Script Engine has been running too long (evaluation), aborting:" << getFilename(); _engine->abortEvaluation(); } else { auto context = _engine->currentContext(); if (context) { qCWarning(scriptengine) << "Script Engine has been running too long (event loop), throwing:" << getFilename(); context->throwError("Timed out during shutdown");