// // 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 #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #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 #include "../../midi/src/Midi.h" // FIXME why won't a simpler include work? #include "MIDIEvent.h" #include "SettingHandle.h" #include #include 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(); int scriptEnginePointerMetaID = qRegisterMetaType(); 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(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(); 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(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->addScriptEngine(qSharedPointerCast(engineSP)); engine->setScriptEngines(scriptEngines); return engineSP; } int ScriptEngine::processLevelMaxRetries { ScriptRequest::MAX_RETRIES }; ScriptEngine::ScriptEngine(Context context, const QString& scriptContents, const QString& fileNameString) : BaseScriptEngine(), _context(context), _scriptContents(scriptContents), _timerFunctionMap(), _fileNameString(fileNameString), _arrayBufferClass(new ArrayBufferClass(this)), _assetScriptingInterface(new AssetScriptingInterface(this)) { switch (_context) { case Context::CLIENT_SCRIPT: _type = Type::CLIENT; break; case Context::ENTITY_CLIENT_SCRIPT: _type = Type::ENTITY_CLIENT; break; case Context::ENTITY_SERVER_SCRIPT: _type = Type::ENTITY_SERVER; break; case Context::AGENT_SCRIPT: _type = Type::AGENT; break; } connect(this, &QScriptEngine::signalHandlerException, this, [this](const QScriptValue& exception) { if (hasUncaughtException()) { // the engine's uncaughtException() seems to produce much better stack traces here emit unhandledException(cloneUncaughtException("signalHandlerException")); clearExceptions(); } else { // ... but may not always be available -- so if needed we fallback to the passed exception emit unhandledException(exception); } }, Qt::DirectConnection); setProcessEventsInterval(MSECS_PER_SECOND); if (isEntityServerScript()) { qCDebug(scriptengine) << "isEntityServerScript() -- limiting maxRetries to 1"; processLevelMaxRetries = 1; } // this is where all unhandled exceptions end up getting logged connect(this, &BaseScriptEngine::unhandledException, this, [this](const QScriptValue& err) { auto output = err.engine() == this ? err : makeError(err); if (!output.property("detail").isValid()) { output.setProperty("detail", "UnhandledException"); } logException(output); }); if (_type == Type::ENTITY_CLIENT || _type == Type::ENTITY_SERVER) { QObject::connect(this, &ScriptEngine::update, this, [this]() { // process pending entity script content if (!_contentAvailableQueue.empty() && !(_isFinished || _isStopping)) { EntityScriptContentAvailableMap pending; std::swap(_contentAvailableQueue, pending); for (auto& pair : pending) { auto& args = pair.second; entityScriptContentAvailable(args.entityID, args.scriptOrURL, args.contents, args.isURL, args.success, args.status); } } }); } } QString ScriptEngine::getTypeAsString() const { auto value = QVariant::fromValue(_type).toString(); return value.isEmpty() ? "unknown" : value.toLower(); } QString ScriptEngine::getContext() const { switch (_context) { case CLIENT_SCRIPT: return "client"; case ENTITY_CLIENT_SCRIPT: return "entity_client"; case ENTITY_SERVER_SCRIPT: return "entity_server"; case AGENT_SCRIPT: return "agent"; default: return "unknown"; } return "unknown"; } bool ScriptEngine::isDebugMode() const { #if defined(DEBUG) return true; #else return false; #endif } ScriptEngine::~ScriptEngine() {} void ScriptEngine::disconnectNonEssentialSignals() { disconnect(); QThread* workerThread; // Ensure the thread should be running, and does exist if (_isRunning && _isThreaded && (workerThread = thread())) { connect(this, &QObject::destroyed, workerThread, &QThread::quit); connect(workerThread, &QThread::finished, workerThread, &QObject::deleteLater); } } void ScriptEngine::runDebuggable() { static QMenuBar* menuBar { nullptr }; static QMenu* scriptDebugMenu { nullptr }; static size_t scriptMenuCount { 0 }; if (!scriptDebugMenu) { for (auto window : qApp->topLevelWidgets()) { auto mainWindow = qobject_cast(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(sharedFromThis())); _isRunning = false; emit runningStateChanged(); emit doneRunning(); timer->deleteLater(); return; } qint64 now = usecTimestampNow(); // we check for 'now' in the past in case people set their clock back if (_lastUpdate < now) { float deltaTime = (float)(now - _lastUpdate) / (float)USECS_PER_SECOND; if (!(_isFinished || _isStopping)) { emit update(deltaTime); } } _lastUpdate = now; // only clear exceptions if we are not in the middle of evaluating if (!isEvaluating() && hasUncaughtException()) { qCWarning(scriptengine) << __FUNCTION__ << "---------- UNCAUGHT EXCEPTION --------"; qCWarning(scriptengine) << "runDebuggable" << uncaughtException().toString(); logException(__FUNCTION__); clearExceptions(); } }); timer->start(10); } void ScriptEngine::runInThread() { Q_ASSERT_X(!_isThreaded, "ScriptEngine::runInThread()", "runInThread should not be called more than once"); if (_isThreaded) { return; } _isThreaded = true; // The thread interface cannot live on itself, and we want to move this into the thread, so // the thread cannot have this as a parent. QThread* workerThread = new QThread(); workerThread->setObjectName(QString("js:") + getFilename().replace("about:","")); moveToThread(workerThread); // NOTE: If you connect any essential signals for proper shutdown or cleanup of // the script engine, make sure to add code to "reconnect" them to the // disconnectNonEssentialSignals() method connect(workerThread, &QThread::started, this, &ScriptEngine::run); connect(this, &QObject::destroyed, workerThread, &QThread::quit); connect(workerThread, &QThread::finished, workerThread, &QObject::deleteLater); workerThread->start(); } void ScriptEngine::executeOnScriptThread(std::function function, const Qt::ConnectionType& type ) { if (QThread::currentThread() != thread()) { QMetaObject::invokeMethod(this, "executeOnScriptThread", type, Q_ARG(std::function, function)); return; } function(); } void ScriptEngine::waitTillDoneRunning() { // Engine should be stopped already, but be defensive stop(); auto workerThread = thread(); if (workerThread == QThread::currentThread()) { qCWarning(scriptengine) << "ScriptEngine::waitTillDoneRunning called, but the script is on the same thread:" << getFilename(); return; } if (_isThreaded && workerThread) { // We should never be waiting (blocking) on our own thread assert(workerThread != QThread::currentThread()); #ifdef Q_OS_MAC // On mac, don't call QCoreApplication::processEvents() here. This is to prevent // [NSApplication terminate:] from prematurely destroying the static destructors // while we are waiting for the scripts to shutdown. We will pump the message // queue later in the Application destructor. if (workerThread->isRunning()) { workerThread->quit(); if (isEvaluating()) { qCWarning(scriptengine) << "Script Engine has been running too long, aborting:" << getFilename(); abortEvaluation(); } else { auto context = currentContext(); if (context) { qCWarning(scriptengine) << "Script Engine has been running too long, throwing:" << getFilename(); context->throwError("Timed out during shutdown"); } } // Wait for the scripting thread to stop running, as // flooding it with aborts/exceptions will persist it longer static const auto MAX_SCRIPT_QUITTING_TIME = 0.5 * MSECS_PER_SECOND; if (!workerThread->wait(MAX_SCRIPT_QUITTING_TIME)) { workerThread->terminate(); } } #else auto startedWaiting = usecTimestampNow(); while (workerThread->isRunning()) { // If the final evaluation takes too long, then tell the script engine to stop running auto elapsedUsecs = usecTimestampNow() - startedWaiting; static const auto MAX_SCRIPT_EVALUATION_TIME = USECS_PER_SECOND; if (elapsedUsecs > MAX_SCRIPT_EVALUATION_TIME) { workerThread->quit(); if (isEvaluating()) { qCWarning(scriptengine) << "Script Engine has been running too long, aborting:" << getFilename(); abortEvaluation(); } else { auto context = currentContext(); if (context) { qCWarning(scriptengine) << "Script Engine has been running too long, throwing:" << getFilename(); context->throwError("Timed out during shutdown"); } } // Wait for the scripting thread to stop running, as // flooding it with aborts/exceptions will persist it longer static const auto MAX_SCRIPT_QUITTING_TIME = 0.5 * MSECS_PER_SECOND; if (!workerThread->wait(MAX_SCRIPT_QUITTING_TIME)) { workerThread->terminate(); } } // NOTE: This will be called on the main application thread (among other threads) from stopAllScripts. // The thread will need to continue to process events, because // the scripts will likely need to marshall messages across to the main thread, e.g. // if they access Settings or Menu in any of their shutdown code. So: // Process events for this thread, allowing invokeMethod calls to pass between threads. QCoreApplication::processEvents(); // Avoid a pure busy wait QThread::yieldCurrentThread(); } #endif scriptInfoMessage("Script Engine has stopped:" + getFilename()); } } QString ScriptEngine::getFilename() const { QStringList fileNameParts = _fileNameString.split("/"); QString lastPart; if (!fileNameParts.isEmpty()) { lastPart = fileNameParts.last(); } 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->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()) { _debuggable = true; } } emit scriptLoaded(url); }, reload, maxRetries); } void ScriptEngine::scriptErrorMessage(const QString& message) { qCCritical(scriptengine, "[%s] %s", qUtf8Printable(getFilename()), qUtf8Printable(message)); emit errorMessage(message, getFilename()); } void ScriptEngine::scriptWarningMessage(const QString& message) { qCWarning(scriptengine, "[%s] %s", qUtf8Printable(getFilename()), qUtf8Printable(message)); emit warningMessage(message, getFilename()); } void ScriptEngine::scriptInfoMessage(const QString& message) { qCInfo(scriptengine, "[%s] %s", qUtf8Printable(getFilename()), qUtf8Printable(message)); emit infoMessage(message, getFilename()); } void ScriptEngine::scriptPrintedMessage(const QString& message) { qCDebug(scriptengine, "[%s] %s", qUtf8Printable(getFilename()), qUtf8Printable(message)); emit printedMessage(message, getFilename()); } void ScriptEngine::clearDebugLogWindow() { emit clearDebugWindow(); } // Even though we never pass AnimVariantMap directly to and from javascript, the queued invokeMethod of // callAnimationStateHandler requires that the type be registered. // These two are meaningful, if we ever do want to use them... static QScriptValue animVarMapToScriptValue(QScriptEngine* engine, const AnimVariantMap& parameters) { QStringList unused; return parameters.animVariantMapToScriptValue(engine, unused, false); } static void animVarMapFromScriptValue(const QScriptValue& value, AnimVariantMap& parameters) { parameters.animVariantMapFromScriptValue(value); } // ... while these two are not. But none of the four are ever used. static QScriptValue resultHandlerToScriptValue(QScriptEngine* engine, const AnimVariantResultHandler& resultHandler) { qCCritical(scriptengine) << "Attempt to marshall result handler to javascript"; assert(false); return QScriptValue(); } static void resultHandlerFromScriptValue(const QScriptValue& value, AnimVariantResultHandler& resultHandler) { qCCritical(scriptengine) << "Attempt to marshall result handler from javascript"; assert(false); } // Templated qScriptRegisterMetaType fails to compile with raw pointers using ScriptableResourceRawPtr = ScriptableResource*; static QScriptValue scriptableResourceToScriptValue(QScriptEngine* engine, const ScriptableResourceRawPtr& resource) { if (!resource) { return QScriptValue(); // probably shutting down } // The first script to encounter this resource will track its memory. // In this way, it will be more likely to GC. // This fails in the case that the resource is used across many scripts, but // in that case it would be too difficult to tell which one should track the memory, and // this serves the common case (use in a single script). auto data = resource->getResource(); if (data && !resource->isInScript()) { resource->setInScript(true); QObject::connect(data.data(), SIGNAL(updateSize(qint64)), engine, SLOT(updateMemoryCost(qint64))); } auto object = engine->newQObject( const_cast(resource), QScriptEngine::ScriptOwnership, DEFAULT_QOBJECT_WRAP_OPTIONS); return object; } static void scriptableResourceFromScriptValue(const QScriptValue& value, ScriptableResourceRawPtr& resource) { resource = static_cast(value.toQObject()); } /**jsdoc * The Resource API provides values that define the possible loading states of a resource. * * @namespace Resource * * @hifi-interface * @hifi-client-entity * @hifi-avatar * @hifi-server-entity * @hifi-assignment-client * * @property {Resource.State} State - The possible loading states of a resource. Read-only. */ static QScriptValue createScriptableResourcePrototype(ScriptEnginePointer engine) { auto prototype = engine->newObject(); // Expose enum State to JS/QML via properties QObject* state = new QObject(engine.data()); state->setObjectName("ResourceState"); auto metaEnum = QMetaEnum::fromType(); for (int i = 0; i < metaEnum.keyCount(); ++i) { state->setProperty(metaEnum.key(i), metaEnum.value(i)); } auto prototypeState = engine->newQObject(state, QScriptEngine::QtOwnership, QScriptEngine::ExcludeDeleteLater | QScriptEngine::ExcludeSlots | QScriptEngine::ExcludeSuperClassMethods); prototype.setProperty("State", prototypeState); return prototype; } QScriptValue avatarDataToScriptValue(QScriptEngine* engine, ScriptAvatarData* const& in) { return engine->newQObject(in, QScriptEngine::ScriptOwnership, DEFAULT_QOBJECT_WRAP_OPTIONS); } void avatarDataFromScriptValue(const QScriptValue& object, ScriptAvatarData*& out) { // This is not implemented because there are no slots/properties that take an AvatarSharedPointer from a script assert(false); out = nullptr; } void ScriptEngine::resetModuleCache(bool deleteScriptCache) { if (QThread::currentThread() != thread()) { executeOnScriptThread([=]() { resetModuleCache(deleteScriptCache); }); return; } auto jsRequire = globalObject().property("Script").property("require"); auto cache = jsRequire.property("cache"); auto cacheMeta = jsRequire.data(); if (deleteScriptCache) { QScriptValueIterator it(cache); while (it.hasNext()) { it.next(); if (it.flags() & QScriptValue::SkipInEnumeration) { continue; } qCDebug(scriptengine) << "resetModuleCache(true) -- staging " << it.name() << " for cache reset at next require"; cacheMeta.setProperty(it.name(), true); } } cache = newObject(); if (!cacheMeta.isObject()) { cacheMeta = newObject(); cacheMeta.setProperty("id", "Script.require.cacheMeta"); cacheMeta.setProperty("type", "cacheMeta"); jsRequire.setData(cacheMeta); } cache.setProperty("__created__", (double)QDateTime::currentMSecsSinceEpoch(), QScriptValue::SkipInEnumeration); #if DEBUG_JS_MODULES cache.setProperty("__meta__", cacheMeta, READONLY_HIDDEN_PROP_FLAGS); #endif jsRequire.setProperty("cache", cache, READONLY_PROP_FLAGS); } void ScriptEngine::init() { if (_isInitialized) { return; // only initialize once } _isInitialized = true; auto entityScriptingInterface = DependencyManager::get(); entityScriptingInterface->init(); // register various meta-types registerMetaTypes(this); registerMIDIMetaTypes(this); registerEventTypes(this); registerMenuItemProperties(this); registerAnimationTypes(this); registerAvatarTypes(this); registerAudioMetaTypes(this); qScriptRegisterMetaType(this, EntityPropertyFlagsToScriptValue, EntityPropertyFlagsFromScriptValue); qScriptRegisterMetaType(this, EntityItemPropertiesToScriptValue, EntityItemPropertiesFromScriptValueHonorReadOnly); qScriptRegisterMetaType(this, EntityPropertyInfoToScriptValue, EntityPropertyInfoFromScriptValue); qScriptRegisterMetaType(this, EntityItemIDtoScriptValue, EntityItemIDfromScriptValue); qScriptRegisterMetaType(this, RayToEntityIntersectionResultToScriptValue, RayToEntityIntersectionResultFromScriptValue); qScriptRegisterMetaType(this, RayToAvatarIntersectionResultToScriptValue, RayToAvatarIntersectionResultFromScriptValue); qScriptRegisterMetaType(this, AvatarEntityMapToScriptValue, AvatarEntityMapFromScriptValue); qScriptRegisterSequenceMetaType>(this); qScriptRegisterSequenceMetaType>(this); qScriptRegisterSequenceMetaType>(this); qScriptRegisterSequenceMetaType>(this); qScriptRegisterSequenceMetaType>(this); QScriptValue xmlHttpRequestConstructorValue = newFunction(XMLHttpRequestClass::constructor); globalObject().setProperty("XMLHttpRequest", xmlHttpRequestConstructorValue); QScriptValue webSocketConstructorValue = newFunction(WebSocketClass::constructor); globalObject().setProperty("WebSocket", webSocketConstructorValue); globalObject().setProperty("print", newFunction(debugPrint)); QScriptValue audioEffectOptionsConstructorValue = newFunction(AudioEffectOptions::constructor); globalObject().setProperty("AudioEffectOptions", audioEffectOptionsConstructorValue); qScriptRegisterMetaType(this, injectorToScriptValue, injectorFromScriptValue); qScriptRegisterMetaType(this, inputControllerToScriptValue, inputControllerFromScriptValue); qScriptRegisterMetaType(this, avatarDataToScriptValue, avatarDataFromScriptValue); qScriptRegisterMetaType(this, animationDetailsToScriptValue, animationDetailsFromScriptValue); qScriptRegisterMetaType(this, webSocketToScriptValue, webSocketFromScriptValue); qScriptRegisterMetaType(this, qWSCloseCodeToScriptValue, qWSCloseCodeFromScriptValue); qScriptRegisterMetaType(this, wscReadyStateToScriptValue, wscReadyStateFromScriptValue); // NOTE: You do not want to end up creating new instances of singletons here. They will be on the ScriptEngine thread // and are likely to be unusable if we "reset" the ScriptEngine by creating a new one (on a whole new thread). registerGlobalObject("Script", this); { // set up Script.require.resolve and Script.require.cache auto Script = globalObject().property("Script"); auto require = Script.property("require"); auto resolve = Script.property("_requireResolve"); require.setProperty("resolve", resolve, READONLY_PROP_FLAGS); resetModuleCache(); } registerGlobalObject("Audio", DependencyManager::get().data()); registerGlobalObject("Midi", DependencyManager::get().data()); registerGlobalObject("Entities", entityScriptingInterface.data()); registerFunction("Entities", "getMultipleEntityProperties", EntityScriptingInterface::getMultipleEntityProperties); registerGlobalObject("Quat", &_quatLibrary); registerGlobalObject("Vec3", &_vec3Library); registerGlobalObject("Mat4", &_mat4Library); registerGlobalObject("Uuid", &_uuidLibrary); registerGlobalObject("Messages", DependencyManager::get().data()); registerGlobalObject("File", new FileScriptingInterface(this)); registerGlobalObject("console", &_consoleScriptingInterface); registerFunction("console", "info", ConsoleScriptingInterface::info, currentContext()->argumentCount()); registerFunction("console", "log", ConsoleScriptingInterface::log, currentContext()->argumentCount()); registerFunction("console", "debug", ConsoleScriptingInterface::debug, currentContext()->argumentCount()); registerFunction("console", "warn", ConsoleScriptingInterface::warn, currentContext()->argumentCount()); registerFunction("console", "error", ConsoleScriptingInterface::error, currentContext()->argumentCount()); registerFunction("console", "exception", ConsoleScriptingInterface::exception, currentContext()->argumentCount()); registerFunction("console", "assert", ConsoleScriptingInterface::assertion, currentContext()->argumentCount()); registerFunction("console", "group", ConsoleScriptingInterface::group, 1); registerFunction("console", "groupCollapsed", ConsoleScriptingInterface::groupCollapsed, 1); registerFunction("console", "groupEnd", ConsoleScriptingInterface::groupEnd, 0); qScriptRegisterMetaType(this, animVarMapToScriptValue, animVarMapFromScriptValue); qScriptRegisterMetaType(this, resultHandlerToScriptValue, resultHandlerFromScriptValue); // Scriptable cache access auto resourcePrototype = createScriptableResourcePrototype(qSharedPointerCast(sharedFromThis())); globalObject().setProperty("Resource", resourcePrototype); setDefaultPrototype(qMetaTypeId(), resourcePrototype); qScriptRegisterMetaType(this, scriptableResourceToScriptValue, scriptableResourceFromScriptValue); // constants globalObject().setProperty("TREE_SCALE", newVariant(QVariant(TREE_SCALE))); registerGlobalObject("Assets", _assetScriptingInterface); registerGlobalObject("Resources", DependencyManager::get().data()); registerGlobalObject("DebugDraw", &DebugDraw::getInstance()); registerGlobalObject("Model", new ModelScriptingInterface(this)); qScriptRegisterMetaType(this, meshToScriptValue, meshFromScriptValue); qScriptRegisterMetaType(this, meshesToScriptValue, meshesFromScriptValue); registerGlobalObject("UserActivityLogger", DependencyManager::get().data()); #if DEV_BUILD || PR_BUILD registerGlobalObject("StackTest", new StackTestScriptingInterface(this)); #endif globalObject().setProperty("KALILA", "isWaifu"); globalObject().setProperty("Kute", newFunction([](QScriptContext* context, QScriptEngine* engine) -> QScriptValue { return context->argument(0).toString().toLower() == "kalila" ? true : false; })); } void ScriptEngine::registerValue(const QString& valueName, QScriptValue value) { if (QThread::currentThread() != thread()) { #ifdef THREAD_DEBUGGING qCDebug(scriptengine) << "*** WARNING *** ScriptEngine::registerValue() called on wrong thread [" << QThread::currentThread() << "], invoking on correct thread [" << thread() << "]"; #endif QMetaObject::invokeMethod(this, "registerValue", Q_ARG(const QString&, valueName), Q_ARG(QScriptValue, value)); return; } QStringList pathToValue = valueName.split("."); int partsToGo = pathToValue.length(); QScriptValue partObject = globalObject(); for (const auto& pathPart : pathToValue) { partsToGo--; if (!partObject.property(pathPart).isValid()) { if (partsToGo > 0) { //QObject *object = new QObject; QScriptValue partValue = newArray(); //newQObject(object, QScriptEngine::ScriptOwnership); partObject.setProperty(pathPart, partValue); } else { partObject.setProperty(pathPart, value); } } partObject = partObject.property(pathPart); } } void ScriptEngine::registerGlobalObject(const QString& name, QObject* object) { if (QThread::currentThread() != thread()) { #ifdef THREAD_DEBUGGING qCDebug(scriptengine) << "*** WARNING *** ScriptEngine::registerGlobalObject() called on wrong thread [" << QThread::currentThread() << "], invoking on correct thread [" << thread() << "] name:" << name; #endif QMetaObject::invokeMethod(this, "registerGlobalObject", Q_ARG(const QString&, name), Q_ARG(QObject*, object)); return; } #ifdef THREAD_DEBUGGING qCDebug(scriptengine) << "ScriptEngine::registerGlobalObject() called on thread [" << QThread::currentThread() << "] name:" << name; #endif if (!globalObject().property(name).isValid()) { if (object) { QScriptValue value = newQObject(object, QScriptEngine::QtOwnership, DEFAULT_QOBJECT_WRAP_OPTIONS); globalObject().setProperty(name, value); } else { globalObject().setProperty(name, QScriptValue()); } } } void ScriptEngine::registerFunction(const QString& name, QScriptEngine::FunctionSignature functionSignature, int numArguments) { if (QThread::currentThread() != thread()) { #ifdef THREAD_DEBUGGING qCDebug(scriptengine) << "*** WARNING *** ScriptEngine::registerFunction() called on wrong thread [" << QThread::currentThread() << "], invoking on correct thread [" << thread() << "] name:" << name; #endif QMetaObject::invokeMethod(this, "registerFunction", Q_ARG(const QString&, name), Q_ARG(QScriptEngine::FunctionSignature, functionSignature), Q_ARG(int, numArguments)); return; } #ifdef THREAD_DEBUGGING qCDebug(scriptengine) << "ScriptEngine::registerFunction() called on thread [" << QThread::currentThread() << "] name:" << name; #endif QScriptValue scriptFun = newFunction(functionSignature, numArguments); globalObject().setProperty(name, scriptFun); } void ScriptEngine::registerFunction(const QString& parent, const QString& name, QScriptEngine::FunctionSignature functionSignature, int numArguments) { if (QThread::currentThread() != thread()) { #ifdef THREAD_DEBUGGING qCDebug(scriptengine) << "*** WARNING *** ScriptEngine::registerFunction() called on wrong thread [" << QThread::currentThread() << "], invoking on correct thread [" << thread() << "] parent:" << parent << "name:" << name; #endif QMetaObject::invokeMethod(this, "registerFunction", Q_ARG(const QString&, name), Q_ARG(QScriptEngine::FunctionSignature, functionSignature), Q_ARG(int, numArguments)); return; } #ifdef THREAD_DEBUGGING qCDebug(scriptengine) << "ScriptEngine::registerFunction() called on thread [" << QThread::currentThread() << "] parent:" << parent << "name:" << name; #endif QScriptValue object = globalObject().property(parent); if (object.isValid()) { QScriptValue scriptFun = newFunction(functionSignature, numArguments); object.setProperty(name, scriptFun); } } void ScriptEngine::registerGetterSetter(const QString& name, QScriptEngine::FunctionSignature getter, QScriptEngine::FunctionSignature setter, const QString& parent) { if (QThread::currentThread() != thread()) { #ifdef THREAD_DEBUGGING qCDebug(scriptengine) << "*** WARNING *** ScriptEngine::registerGetterSetter() called on wrong thread [" << QThread::currentThread() << "], invoking on correct thread [" << thread() << "] " " name:" << name << "parent:" << parent; #endif QMetaObject::invokeMethod(this, "registerGetterSetter", Q_ARG(const QString&, name), Q_ARG(QScriptEngine::FunctionSignature, getter), Q_ARG(QScriptEngine::FunctionSignature, setter), Q_ARG(const QString&, parent)); return; } #ifdef THREAD_DEBUGGING qCDebug(scriptengine) << "ScriptEngine::registerGetterSetter() called on thread [" << QThread::currentThread() << "] name:" << name << "parent:" << parent; #endif QScriptValue setterFunction = newFunction(setter, 1); QScriptValue getterFunction = newFunction(getter); if (!parent.isNull() && !parent.isEmpty()) { QScriptValue object = globalObject().property(parent); if (object.isValid()) { object.setProperty(name, setterFunction, QScriptValue::PropertySetter); object.setProperty(name, getterFunction, QScriptValue::PropertyGetter); } } else { globalObject().setProperty(name, setterFunction, QScriptValue::PropertySetter); globalObject().setProperty(name, getterFunction, QScriptValue::PropertyGetter); } } // Unregister the handlers for this eventName and entityID. void ScriptEngine::removeEventHandler(const EntityItemID& entityID, const QString& eventName, QScriptValue handler) { if (QThread::currentThread() != thread()) { #ifdef THREAD_DEBUGGING qCDebug(scriptengine) << "*** WARNING *** ScriptEngine::removeEventHandler() called on wrong thread [" << QThread::currentThread() << "], invoking on correct thread [" << thread() << "] " "entityID:" << entityID << " eventName:" << eventName; #endif QMetaObject::invokeMethod(this, "removeEventHandler", Q_ARG(const EntityItemID&, entityID), Q_ARG(const QString&, eventName), Q_ARG(QScriptValue, handler)); return; } #ifdef THREAD_DEBUGGING qCDebug(scriptengine) << "ScriptEngine::removeEventHandler() called on thread [" << QThread::currentThread() << "] entityID:" << entityID << " eventName : " << eventName; #endif if (!_registeredHandlers.contains(entityID)) { return; } RegisteredEventHandlers& handlersOnEntity = _registeredHandlers[entityID]; CallbackList& handlersForEvent = handlersOnEntity[eventName]; // QScriptValue does not have operator==(), so we can't use QList::removeOne and friends. So iterate. for (int i = 0; i < handlersForEvent.count(); ++i) { if (handlersForEvent[i].function.equals(handler)) { handlersForEvent.removeAt(i); return; // Design choice: since comparison is relatively expensive, just remove the first matching handler. } } } // Register the handler. void ScriptEngine::addEventHandler(const EntityItemID& entityID, const QString& eventName, QScriptValue handler) { if (QThread::currentThread() != thread()) { #ifdef THREAD_DEBUGGING qCDebug(scriptengine) << "*** WARNING *** ScriptEngine::addEventHandler() called on wrong thread [" << QThread::currentThread() << "], invoking on correct thread [" << thread() << "] " "entityID:" << entityID << " eventName:" << eventName; #endif QMetaObject::invokeMethod(this, "addEventHandler", Q_ARG(const EntityItemID&, entityID), Q_ARG(const QString&, eventName), Q_ARG(QScriptValue, handler)); return; } #ifdef THREAD_DEBUGGING qCDebug(scriptengine) << "ScriptEngine::addEventHandler() called on thread [" << QThread::currentThread() << "] entityID:" << entityID << " eventName : " << eventName; #endif if (_registeredHandlers.count() == 0) { // First time any per-entity handler has been added in this script... // Connect up ALL the handlers to the global entities object's signals. // (We could go signal by signal, or even handler by handler, but I don't think the efficiency is worth the complexity.) auto entities = DependencyManager::get(); // Bug? These handlers are deleted when entityID is deleted, which is nice. // But if they are created by an entity script on a different entity, should they also be deleted when the entity script unloads? // E.g., suppose a bow has an entity script that causes arrows to be created with a potential lifetime greater than the bow, // and that the entity script adds (e.g., collision) handlers to the arrows. Should those handlers fire if the bow is unloaded? // Also, what about when the entity script is REloaded? // For now, we are leaving them around. Changing that would require some non-trivial digging around to find the // handlers that were added while a given currentEntityIdentifier was in place. I don't think this is dangerous. Just perhaps unexpected. -HRS connect(entities.data(), &EntityScriptingInterface::deletingEntity, this, [this](const EntityItemID& entityID) { _registeredHandlers.remove(entityID); }); // Two common cases of event handler, differing only in argument signature. using SingleEntityHandler = std::function; auto makeSingleEntityHandler = [this](QString eventName) -> SingleEntityHandler { return [this, eventName](const EntityItemID& entityItemID) { forwardHandlerCall(entityItemID, eventName, { entityItemID.toScriptValue(this) }); }; }; using PointerHandler = std::function; auto makePointerHandler = [this](QString eventName) -> PointerHandler { return [this, eventName](const EntityItemID& entityItemID, const PointerEvent& event) { if (!EntityTree::areEntityClicksCaptured()) { forwardHandlerCall(entityItemID, eventName, { entityItemID.toScriptValue(this), event.toScriptValue(this) }); } }; }; using CollisionHandler = std::function; auto makeCollisionHandler = [this](QString eventName) -> CollisionHandler { return [this, eventName](const EntityItemID& idA, const EntityItemID& idB, const Collision& collision) { forwardHandlerCall(idA, eventName, { idA.toScriptValue(this), idB.toScriptValue(this), collisionToScriptValue(this, collision) }); }; }; /**jsdoc *

The name of an entity event. When the entity event occurs, any function that has been registered for that event via * {@link Script.addEventHandler} is called with parameters per the entity event.

* * * * * * * * * * * * * * * * * * *
Event NameEntity Event
"enterEntity"{@link Entities.enterEntity}
"leaveEntity"{@link Entities.leaveEntity}
"mousePressOnEntity"{@link Entities.mousePressOnEntity}
"mouseMoveOnEntity"{@link Entities.mouseMoveOnEntity}
"mouseReleaseOnEntity"{@link Entities.mouseReleaseOnEntity}
"clickDownOnEntity"{@link Entities.clickDownOnEntity}
"holdingClickOnEntity"{@link Entities.holdingClickOnEntity}
"clickReleaseOnEntity"{@link Entities.clickReleaseOnEntity}
"hoverEnterEntity"{@link Entities.hoverEnterEntity}
"hoverOverEntity"{@link Entities.hoverOverEntity}
"hoverLeaveEntity"{@link Entities.hoverLeaveEntity}
"collisionWithEntity"{@link Entities.collisionWithEntity}
* * @typedef {string} Script.EntityEvent */ connect(entities.data(), &EntityScriptingInterface::enterEntity, this, makeSingleEntityHandler("enterEntity")); connect(entities.data(), &EntityScriptingInterface::leaveEntity, this, makeSingleEntityHandler("leaveEntity")); connect(entities.data(), &EntityScriptingInterface::mousePressOnEntity, this, makePointerHandler("mousePressOnEntity")); connect(entities.data(), &EntityScriptingInterface::mouseMoveOnEntity, this, makePointerHandler("mouseMoveOnEntity")); connect(entities.data(), &EntityScriptingInterface::mouseReleaseOnEntity, this, makePointerHandler("mouseReleaseOnEntity")); connect(entities.data(), &EntityScriptingInterface::clickDownOnEntity, this, makePointerHandler("clickDownOnEntity")); connect(entities.data(), &EntityScriptingInterface::holdingClickOnEntity, this, makePointerHandler("holdingClickOnEntity")); connect(entities.data(), &EntityScriptingInterface::clickReleaseOnEntity, this, makePointerHandler("clickReleaseOnEntity")); connect(entities.data(), &EntityScriptingInterface::hoverEnterEntity, this, makePointerHandler("hoverEnterEntity")); connect(entities.data(), &EntityScriptingInterface::hoverOverEntity, this, makePointerHandler("hoverOverEntity")); connect(entities.data(), &EntityScriptingInterface::hoverLeaveEntity, this, makePointerHandler("hoverLeaveEntity")); connect(entities.data(), &EntityScriptingInterface::collisionWithEntity, this, makeCollisionHandler("collisionWithEntity")); } if (!_registeredHandlers.contains(entityID)) { _registeredHandlers[entityID] = RegisteredEventHandlers(); } CallbackList& handlersForEvent = _registeredHandlers[entityID][eventName]; CallbackData handlerData = { handler, currentEntityIdentifier, currentSandboxURL }; handlersForEvent << handlerData; // Note that the same handler can be added many times. See removeEntityEventHandler(). } // this is not redundant -- the version in BaseScriptEngine is specifically not Q_INVOKABLE QScriptValue ScriptEngine::evaluateInClosure(const QScriptValue& closure, const QScriptProgram& program) { return BaseScriptEngine::evaluateInClosure(closure, program); } QScriptValue ScriptEngine::evaluate(const QString& sourceCode, const QString& fileName, int lineNumber) { QSharedPointer scriptEngines(_scriptEngines); if (!scriptEngines || scriptEngines->isStopped()) { return QScriptValue(); // bail early } if (QThread::currentThread() != thread()) { QScriptValue result; #ifdef THREAD_DEBUGGING qCDebug(scriptengine) << "*** WARNING *** ScriptEngine::evaluate() called on wrong thread [" << QThread::currentThread() << "], invoking on correct thread [" << thread() << "] " "sourceCode:" << sourceCode << " fileName:" << fileName << "lineNumber:" << lineNumber; #endif BLOCKING_INVOKE_METHOD(this, "evaluate", Q_RETURN_ARG(QScriptValue, result), Q_ARG(const QString&, sourceCode), Q_ARG(const QString&, fileName), Q_ARG(int, lineNumber)); return result; } // Check syntax auto syntaxError = lintScript(sourceCode, fileName); if (syntaxError.isError()) { if (!isEvaluating()) { syntaxError.setProperty("detail", "evaluate"); } raiseException(syntaxError); maybeEmitUncaughtException("lint"); return syntaxError; } QScriptProgram program { sourceCode, fileName, lineNumber }; if (program.isNull()) { // can this happen? auto err = makeError("could not create QScriptProgram for " + fileName); raiseException(err); maybeEmitUncaughtException("compile"); return err; } QScriptValue result; { result = BaseScriptEngine::evaluate(program); maybeEmitUncaughtException("evaluate"); } return result; } void ScriptEngine::run() { if (QThread::currentThread() != qApp->thread() && _context == Context::CLIENT_SCRIPT) { // Flag that we're allowed to access local HTML files on UI created from C++ calls on this thread // (because we're a client script) hifi::scripting::setLocalAccessSafeThread(true); } auto filenameParts = _fileNameString.split("/"); auto name = filenameParts.size() > 0 ? filenameParts[filenameParts.size() - 1] : "unknown"; PROFILE_SET_THREAD_NAME("Script: " + name); QSharedPointer scriptEngines(_scriptEngines); if (!scriptEngines || scriptEngines->isStopped()) { return; // bail early - avoid setting state in init(), as evaluate() will bail too } scriptInfoMessage("Script Engine starting:" + getFilename()); if (!_isInitialized) { init(); } _isRunning = true; emit runningStateChanged(); { PROFILE_RANGE(script, _fileNameString); evaluate(_scriptContents, _fileNameString); maybeEmitUncaughtException(__FUNCTION__); } #ifdef _WIN32 // VS13 does not sleep_until unless it uses the system_clock, see: // https://www.reddit.com/r/cpp_questions/comments/3o71ic/sleep_until_not_working_with_a_time_pointsteady/ using clock = std::chrono::system_clock; #else using clock = std::chrono::high_resolution_clock; #endif clock::time_point startTime = clock::now(); int thisFrame = 0; auto nodeList = DependencyManager::get(); auto entityScriptingInterface = DependencyManager::get(); _lastUpdate = usecTimestampNow(); std::chrono::microseconds totalUpdates(0); // TODO: Integrate this with signals/slots instead of reimplementing throttling for ScriptEngine while (!_isFinished) { auto beforeSleep = clock::now(); // Throttle to SCRIPT_FPS // We'd like to try to keep the script at a solid SCRIPT_FPS update rate. And so we will // calculate a sleepUntil to be the time from our start time until the original target // sleepUntil for this frame. This approach will allow us to "catch up" in the event // that some of our script udpates/frames take a little bit longer than the target average // to execute. // NOTE: if we go to variable SCRIPT_FPS, then we will need to reconsider this approach const std::chrono::microseconds TARGET_SCRIPT_FRAME_DURATION(USECS_PER_SECOND / SCRIPT_FPS + 1); clock::time_point targetSleepUntil(startTime + (thisFrame++ * TARGET_SCRIPT_FRAME_DURATION)); // However, if our sleepUntil is not at least our average update and timer execution time // into the future it means our script is taking too long in its updates, and we want to // punish the script a little bit. So we will force the sleepUntil to be at least our // averageUpdate + averageTimerPerFrame time into the future. auto averageUpdate = totalUpdates / thisFrame; auto averageTimerPerFrame = _totalTimerExecution / thisFrame; auto averageTimerAndUpdate = averageUpdate + averageTimerPerFrame; auto sleepUntil = std::max(targetSleepUntil, beforeSleep + averageTimerAndUpdate); // We don't want to actually sleep for too long, because it causes our scripts to hang // on shutdown and stop... so we want to loop and sleep until we've spent our time in // purgatory, constantly checking to see if our script was asked to end bool processedEvents = false; if (!_isFinished) { PROFILE_RANGE(script, "processEvents-sleep"); std::chrono::milliseconds sleepFor = std::chrono::duration_cast(sleepUntil - clock::now()); if (sleepFor > std::chrono::milliseconds(0)) { QEventLoop loop; QTimer timer; timer.setSingleShot(true); connect(&timer, SIGNAL(timeout()), &loop, SLOT(quit())); timer.start(sleepFor.count()); loop.exec(); } else { QCoreApplication::processEvents(); } processedEvents = true; } PROFILE_RANGE(script, "ScriptMainLoop"); #ifdef SCRIPT_DELAY_DEBUG { auto actuallySleptUntil = clock::now(); uint64_t seconds = std::chrono::duration_cast(actuallySleptUntil - startTime).count(); if (seconds > 0) { // avoid division by zero and time travel uint64_t fps = thisFrame / seconds; // Overreporting artificially reduces the reported rate if (thisFrame % SCRIPT_FPS == 0) { qCDebug(scriptengine) << "Frame:" << thisFrame << "Slept (us):" << std::chrono::duration_cast(actuallySleptUntil - beforeSleep).count() << "Avg Updates (us):" << averageUpdate.count() << "FPS:" << fps; } } } #endif if (_isFinished) { break; } // Only call this if we didn't processEvents as part of waiting for next frame if (!processedEvents) { PROFILE_RANGE(script, "processEvents"); QCoreApplication::processEvents(); } if (_isFinished) { break; } if (!_isFinished && entityScriptingInterface->getEntityPacketSender()->serversExist()) { // release the queue of edit entity messages. entityScriptingInterface->getEntityPacketSender()->releaseQueuedMessages(); // since we're in non-threaded mode, call process so that the packets are sent if (!entityScriptingInterface->getEntityPacketSender()->isThreaded()) { entityScriptingInterface->getEntityPacketSender()->process(); } } qint64 now = usecTimestampNow(); // we check for 'now' in the past in case people set their clock back if (_emitScriptUpdates() && _lastUpdate < now) { float deltaTime = (float) (now - _lastUpdate) / (float) USECS_PER_SECOND; if (!_isFinished) { auto preUpdate = clock::now(); { PROFILE_RANGE(script, "ScriptUpdate"); emit update(deltaTime); } auto postUpdate = clock::now(); auto elapsed = (postUpdate - preUpdate); totalUpdates += std::chrono::duration_cast(elapsed); } } _lastUpdate = now; // only clear exceptions if we are not in the middle of evaluating if (!isEvaluating() && hasUncaughtException()) { qCWarning(scriptengine) << __FUNCTION__ << "---------- UNCAUGHT EXCEPTION --------"; qCWarning(scriptengine) << "runInThread" << uncaughtException().toString(); emit unhandledException(cloneUncaughtException(__FUNCTION__)); clearExceptions(); } } scriptInfoMessage("Script Engine stopping:" + getFilename()); stopAllTimers(); // make sure all our timers are stopped if the script is ending emit scriptEnding(); if (entityScriptingInterface->getEntityPacketSender()->serversExist()) { // release the queue of edit entity messages. entityScriptingInterface->getEntityPacketSender()->releaseQueuedMessages(); // since we're in non-threaded mode, call process so that the packets are sent if (!entityScriptingInterface->getEntityPacketSender()->isThreaded()) { // wait here till the edit packet sender is completely done sending while (entityScriptingInterface->getEntityPacketSender()->hasPacketsToSend()) { entityScriptingInterface->getEntityPacketSender()->process(); QCoreApplication::processEvents(); } } else { // FIXME - do we need to have a similar "wait here" loop for non-threaded packet senders? } } emit finished(_fileNameString, qSharedPointerCast(sharedFromThis())); // Don't leave our local-file-access flag laying around, reset it to false when the scriptengine // thread is finished hifi::scripting::setLocalAccessSafeThread(false); _isRunning = false; emit runningStateChanged(); emit doneRunning(); } // NOTE: This is private because it must be called on the same thread that created the timers, which is why // we want to only call it in our own run "shutdown" processing. void ScriptEngine::stopAllTimers() { QMutableHashIterator i(_timerFunctionMap); int j {0}; while (i.hasNext()) { i.next(); QTimer* timer = i.key(); qCDebug(scriptengine) << getFilename() << "stopAllTimers[" << j++ << "]"; stopTimer(timer); } } void ScriptEngine::stopAllTimersForEntityScript(const EntityItemID& entityID) { // We could maintain a separate map of entityID => QTimer, but someone will have to prove to me that it's worth the complexity. -HRS QVector toDelete; QMutableHashIterator i(_timerFunctionMap); while (i.hasNext()) { i.next(); if (i.value().definingEntityIdentifier != entityID) { continue; } QTimer* timer = i.key(); toDelete << timer; // don't delete while we're iterating. save it. } for (auto timer:toDelete) { // now reap 'em stopTimer(timer); } } void ScriptEngine::stop(bool marshal) { _isStopping = true; // this can be done on any thread if (marshal) { QMetaObject::invokeMethod(this, "stop"); return; } if (!_isFinished) { _isFinished = true; emit runningStateChanged(); } } // Other threads can invoke this through invokeMethod, which causes the callback to be asynchronously executed in this script's thread. void ScriptEngine::callAnimationStateHandler(QScriptValue callback, AnimVariantMap parameters, QStringList names, bool useNames, AnimVariantResultHandler resultHandler) { if (QThread::currentThread() != thread()) { #ifdef THREAD_DEBUGGING qCDebug(scriptengine) << "*** WARNING *** ScriptEngine::callAnimationStateHandler() called on wrong thread [" << QThread::currentThread() << "], invoking on correct thread [" << thread() << "] name:" << name; #endif QMetaObject::invokeMethod(this, "callAnimationStateHandler", Q_ARG(QScriptValue, callback), Q_ARG(AnimVariantMap, parameters), Q_ARG(QStringList, names), Q_ARG(bool, useNames), Q_ARG(AnimVariantResultHandler, resultHandler)); return; } QScriptValue javascriptParameters = parameters.animVariantMapToScriptValue(this, names, useNames); QScriptValueList callingArguments; callingArguments << javascriptParameters; assert(currentEntityIdentifier.isInvalidID()); // No animation state handlers from entity scripts. QScriptValue result = callback.call(QScriptValue(), callingArguments); // validate result from callback function. if (result.isValid() && result.isObject()) { resultHandler(result); } else { qCWarning(scriptengine) << "ScriptEngine::callAnimationStateHandler invalid return argument from callback, expected an object"; } } void ScriptEngine::updateMemoryCost(const qint64& deltaSize) { if (deltaSize > 0) { // We've patched qt to fix https://highfidelity.atlassian.net/browse/BUGZ-46 on mac and windows only. #if defined(Q_OS_WIN) || defined(Q_OS_MAC) reportAdditionalMemoryCost(deltaSize); #endif } } void ScriptEngine::timerFired() { { QSharedPointer scriptEngines(_scriptEngines); if (!scriptEngines || scriptEngines->isStopped()) { scriptWarningMessage("Script.timerFired() while shutting down is ignored... parent script:" + getFilename()); return; // bail early } } QTimer* callingTimer = reinterpret_cast(sender()); CallbackData timerData = _timerFunctionMap.value(callingTimer); if (!callingTimer->isActive()) { // this timer is done, we can kill it _timerFunctionMap.remove(callingTimer); delete callingTimer; } // call the associated JS function, if it exists if (timerData.function.isValid()) { PROFILE_RANGE(script, __FUNCTION__); auto preTimer = p_high_resolution_clock::now(); callWithEnvironment(timerData.definingEntityIdentifier, timerData.definingSandboxURL, timerData.function, timerData.function, QScriptValueList()); auto postTimer = p_high_resolution_clock::now(); auto elapsed = (postTimer - preTimer); _totalTimerExecution += std::chrono::duration_cast(elapsed); } else { qCWarning(scriptengine) << "timerFired -- invalid function" << timerData.function.toVariant().toString(); } } QObject* ScriptEngine::setupTimerWithInterval(const QScriptValue& function, int intervalMS, bool isSingleShot) { // create the timer, add it to the map, and start it QTimer* newTimer = new QTimer(this); newTimer->setSingleShot(isSingleShot); // The default timer type is not very accurate below about 200ms http://doc.qt.io/qt-5/qt.html#TimerType-enum static const int MIN_TIMEOUT_FOR_COARSE_TIMER = 200; if (intervalMS < MIN_TIMEOUT_FOR_COARSE_TIMER) { newTimer->setTimerType(Qt::PreciseTimer); } connect(newTimer, &QTimer::timeout, this, &ScriptEngine::timerFired); // make sure the timer stops when the script does connect(this, &ScriptEngine::scriptEnding, newTimer, &QTimer::stop); CallbackData timerData = { function, currentEntityIdentifier, currentSandboxURL }; _timerFunctionMap.insert(newTimer, timerData); newTimer->start(intervalMS); return newTimer; } QObject* ScriptEngine::setInterval(const QScriptValue& function, int intervalMS) { QSharedPointer scriptEngines(_scriptEngines); if (!scriptEngines || scriptEngines->isStopped()) { scriptWarningMessage("Script.setInterval() while shutting down is ignored... parent script:" + getFilename()); return NULL; // bail early } return setupTimerWithInterval(function, intervalMS, false); } QObject* ScriptEngine::setTimeout(const QScriptValue& function, int timeoutMS) { QSharedPointer scriptEngines(_scriptEngines); if (!scriptEngines || scriptEngines->isStopped()) { scriptWarningMessage("Script.setTimeout() while shutting down is ignored... parent script:" + getFilename()); return NULL; // bail early } return setupTimerWithInterval(function, timeoutMS, true); } void ScriptEngine::stopTimer(QTimer *timer) { if (_timerFunctionMap.contains(timer)) { timer->stop(); _timerFunctionMap.remove(timer); delete timer; } else { qCDebug(scriptengine) << "stopTimer -- not in _timerFunctionMap" << timer; } } QUrl ScriptEngine::resolvePath(const QString& include) const { QUrl url(include); // first lets check to see if it's already a full URL -- or a Windows path like "c:/" if (include.startsWith("/") || url.scheme().length() == 1) { url = QUrl::fromLocalFile(include); } if (!url.isRelative()) { return expandScriptUrl(url); } // we apparently weren't a fully qualified url, so, let's assume we're relative // to the first absolute URL in the JS scope chain QUrl parentURL; auto context = currentContext(); do { QScriptContextInfo contextInfo { context }; parentURL = QUrl(contextInfo.fileName()); context = context->parentContext(); } while (parentURL.isRelative() && context); if (parentURL.isRelative()) { // fallback to the "include" parent (if defined, this will already be absolute) parentURL = QUrl(_parentURL); } if (parentURL.isRelative()) { // fallback to the original script engine URL parentURL = QUrl(_fileNameString); // if still relative and path-like, then this is probably a local file... if (parentURL.isRelative() && url.path().contains("/")) { parentURL = QUrl::fromLocalFile(_fileNameString); } } // at this point we should have a legitimate fully qualified URL for our parent url = expandScriptUrl(parentURL.resolved(url)); return url; } QUrl ScriptEngine::resourcesPath() const { return QUrl(PathUtils::resourcesUrl()); } void ScriptEngine::print(const QString& message) { emit printedMessage(message, getFilename()); } void ScriptEngine::beginProfileRange(const QString& label) const { PROFILE_SYNC_BEGIN(script, label.toStdString().c_str(), label.toStdString().c_str()); } void ScriptEngine::endProfileRange(const QString& label) const { PROFILE_SYNC_END(script, label.toStdString().c_str(), label.toStdString().c_str()); } // Script.require.resolve -- like resolvePath, but performs more validation and throws exceptions on invalid module identifiers (for consistency with Node.js) QString ScriptEngine::_requireResolve(const QString& moduleId, const QString& relativeTo) { if (!IS_THREADSAFE_INVOCATION(thread(), __FUNCTION__)) { return QString(); } QUrl defaultScriptsLoc = PathUtils::defaultScriptsLocation(); QUrl url(moduleId); auto displayId = moduleId; if (displayId.length() > MAX_DEBUG_VALUE_LENGTH) { displayId = displayId.mid(0, MAX_DEBUG_VALUE_LENGTH) + "..."; } auto message = QString("Cannot find module '%1' (%2)").arg(displayId); auto throwResolveError = [&](const QScriptValue& error) -> QString { raiseException(error); maybeEmitUncaughtException("require.resolve"); return QString(); }; // de-fuzz the input a little by restricting to rational sizes auto idLength = url.toString().length(); if (idLength < 1 || idLength > MAX_MODULE_ID_LENGTH) { auto details = QString("rejecting invalid module id size (%1 chars [1,%2])") .arg(idLength).arg(MAX_MODULE_ID_LENGTH); return throwResolveError(makeError(message.arg(details), "RangeError")); } // this regex matches: absolute, dotted or path-like URLs // (ie: the kind of stuff ScriptEngine::resolvePath already handles) QRegularExpression qualified ("^\\w+:|^/|^[.]{1,2}(/|$)"); // this is for module.require (which is a bound version of require that's always relative to the module path) if (!relativeTo.isEmpty()) { url = QUrl(relativeTo).resolved(moduleId); url = resolvePath(url.toString()); } else if (qualified.match(moduleId).hasMatch()) { url = resolvePath(moduleId); } else { // check if the moduleId refers to a "system" module QString systemPath = defaultScriptsLoc.path(); QString systemModulePath = QString("%1/modules/%2.js").arg(systemPath).arg(moduleId); url = defaultScriptsLoc; url.setPath(systemModulePath); if (!QFileInfo(url.toLocalFile()).isFile()) { if (!moduleId.contains("./")) { // the user might be trying to refer to a relative file without anchoring it // let's do them a favor and test for that case -- offering specific advice if detected auto unanchoredUrl = resolvePath("./" + moduleId); if (QFileInfo(unanchoredUrl.toLocalFile()).isFile()) { auto msg = QString("relative module ids must be anchored; use './%1' instead") .arg(moduleId); return throwResolveError(makeError(message.arg(msg))); } } return throwResolveError(makeError(message.arg("system module not found"))); } } if (url.isRelative()) { return throwResolveError(makeError(message.arg("could not resolve module id"))); } // if it looks like a local file, verify that it's an allowed path and really a file if (url.isLocalFile()) { QFileInfo file(url.toLocalFile()); QUrl canonical = url; if (file.exists()) { canonical.setPath(file.canonicalFilePath()); } bool disallowOutsideFiles = !PathUtils::defaultScriptsLocation().isParentOf(canonical) && !currentSandboxURL.isLocalFile(); if (disallowOutsideFiles && !PathUtils::isDescendantOf(canonical, currentSandboxURL)) { return throwResolveError(makeError(message.arg( QString("path '%1' outside of origin script '%2' '%3'") .arg(PathUtils::stripFilename(url)) .arg(PathUtils::stripFilename(currentSandboxURL)) .arg(canonical.toString()) ))); } if (!file.exists()) { return throwResolveError(makeError(message.arg("path does not exist: " + url.toLocalFile()))); } if (!file.isFile()) { return throwResolveError(makeError(message.arg("path is not a file: " + url.toLocalFile()))); } } maybeEmitUncaughtException(__FUNCTION__); return url.toString(); } // retrieves the current parent module from the JS scope chain QScriptValue ScriptEngine::currentModule() { if (!IS_THREADSAFE_INVOCATION(thread(), __FUNCTION__)) { return unboundNullValue(); } auto jsRequire = globalObject().property("Script").property("require"); auto cache = jsRequire.property("cache"); auto candidate = QScriptValue(); for (auto c = currentContext(); c && !candidate.isObject(); c = c->parentContext()) { QScriptContextInfo contextInfo { c }; candidate = cache.property(contextInfo.fileName()); } if (!candidate.isObject()) { return QScriptValue(); } return candidate; } // replaces or adds "module" to "parent.children[]" array // (for consistency with Node.js and userscript cache invalidation without "cache busters") bool ScriptEngine::registerModuleWithParent(const QScriptValue& module, const QScriptValue& parent) { auto children = parent.property("children"); if (children.isArray()) { auto key = module.property("id"); auto length = children.property("length").toInt32(); for (int i = 0; i < length; i++) { if (children.property(i).property("id").strictlyEquals(key)) { qCDebug(scriptengine_module) << key.toString() << " updating parent.children[" << i << "] = module"; children.setProperty(i, module); return true; } } qCDebug(scriptengine_module) << key.toString() << " appending parent.children[" << length << "] = module"; children.setProperty(length, module); return true; } else if (parent.isValid()) { qCDebug(scriptengine_module) << "registerModuleWithParent -- unrecognized parent" << parent.toVariant().toString(); } return false; } // creates a new JS "module" Object with default metadata properties QScriptValue ScriptEngine::newModule(const QString& modulePath, const QScriptValue& parent) { auto closure = newObject(); auto exports = newObject(); auto module = newObject(); qCDebug(scriptengine_module) << "newModule" << parent.property("filename").toString(); closure.setProperty("module", module, READONLY_PROP_FLAGS); // note: this becomes the "exports" free variable, so should not be set read only closure.setProperty("exports", exports); // make the closure available to module instantiation module.setProperty("__closure__", closure, READONLY_HIDDEN_PROP_FLAGS); // for consistency with Node.js Module module.setProperty("id", modulePath, READONLY_PROP_FLAGS); module.setProperty("filename", modulePath, READONLY_PROP_FLAGS); module.setProperty("exports", exports); // not readonly module.setProperty("loaded", false, READONLY_PROP_FLAGS); module.setProperty("parent", parent, READONLY_PROP_FLAGS); module.setProperty("children", newArray(), READONLY_PROP_FLAGS); // module.require is a bound version of require that always resolves relative to that module's path auto boundRequire = QScriptEngine::evaluate("(function(id) { return Script.require(Script.require.resolve(id, this.filename)); })", "(boundRequire)"); module.setProperty("require", boundRequire, READONLY_PROP_FLAGS); return module; } // synchronously fetch a module's source code using BatchLoader QVariantMap ScriptEngine::fetchModuleSource(const QString& modulePath, const bool forceDownload) { using UrlMap = QMap; auto scriptCache = DependencyManager::get(); QVariantMap req; qCDebug(scriptengine_module) << "require.fetchModuleSource: " << QUrl(modulePath).fileName() << QThread::currentThread(); auto onload = [=, &req](const UrlMap& data, const UrlMap& _status) { auto url = modulePath; auto status = _status[url]; auto contents = data[url]; if (isStopping()) { req["status"] = "Stopped"; req["success"] = false; } else { req["url"] = url; req["status"] = status; req["success"] = ScriptCache::isSuccessStatus(status); req["contents"] = contents; } }; if (forceDownload) { qCDebug(scriptengine_module) << "require.requestScript -- clearing cache for" << modulePath; scriptCache->deleteScript(modulePath); } BatchLoader* loader = new BatchLoader(QList({ modulePath })); connect(loader, &BatchLoader::finished, this, onload); connect(this, &QObject::destroyed, loader, &QObject::deleteLater); // fail faster? (since require() blocks the engine thread while resolving dependencies) const int MAX_RETRIES = 1; loader->start(MAX_RETRIES); if (!loader->isFinished()) { // This lambda can get called AFTER this local scope has completed. // This is why we pass smart ptrs to the lambda instead of references to local variables. auto monitor = std::make_shared(); auto loop = std::make_shared(); QObject::connect(loader, &BatchLoader::finished, this, [monitor, loop] { monitor->stop(); loop->quit(); }); // this helps detect the case where stop() is invoked during the download // but not seen in time to abort processing in onload()... connect(monitor.get(), &QTimer::timeout, this, [this, loop] { if (isStopping()) { loop->exit(-1); } }); monitor->start(500); loop->exec(); } loader->deleteLater(); return req; } // evaluate a pending module object using the fetched source code QScriptValue ScriptEngine::instantiateModule(const QScriptValue& module, const QString& sourceCode) { QScriptValue result; auto modulePath = module.property("filename").toString(); auto closure = module.property("__closure__"); qCDebug(scriptengine_module) << QString("require.instantiateModule: %1 / %2 bytes") .arg(QUrl(modulePath).fileName()).arg(sourceCode.length()); if (module.property("content-type").toString() == "application/json") { qCDebug(scriptengine_module) << "... parsing as JSON"; closure.setProperty("__json", sourceCode); result = evaluateInClosure(closure, { "module.exports = JSON.parse(__json)", modulePath }); } else { // scoped vars for consistency with Node.js closure.setProperty("require", module.property("require")); closure.setProperty("__filename", modulePath, READONLY_HIDDEN_PROP_FLAGS); closure.setProperty("__dirname", QString(modulePath).replace(QRegExp("/[^/]*$"), ""), READONLY_HIDDEN_PROP_FLAGS); result = evaluateInClosure(closure, { sourceCode, modulePath }); } maybeEmitUncaughtException(__FUNCTION__); return result; } // CommonJS/Node.js like require/module support QScriptValue ScriptEngine::require(const QString& moduleId) { qCDebug(scriptengine_module) << "ScriptEngine::require(" << moduleId.left(MAX_DEBUG_VALUE_LENGTH) << ")"; if (!IS_THREADSAFE_INVOCATION(thread(), __FUNCTION__)) { return unboundNullValue(); } auto jsRequire = globalObject().property("Script").property("require"); auto cacheMeta = jsRequire.data(); auto cache = jsRequire.property("cache"); auto parent = currentModule(); auto throwModuleError = [&](const QString& modulePath, const QScriptValue& error) { cache.setProperty(modulePath, nullValue()); if (!error.isNull()) { #ifdef DEBUG_JS_MODULES qCWarning(scriptengine_module) << "throwing module error:" << error.toString() << modulePath << error.property("stack").toString(); #endif raiseException(error); } maybeEmitUncaughtException("module"); return unboundNullValue(); }; // start by resolving the moduleId into a fully-qualified path/URL QString modulePath = _requireResolve(moduleId); if (modulePath.isNull() || hasUncaughtException()) { // the resolver already threw an exception -- bail early maybeEmitUncaughtException(__FUNCTION__); return unboundNullValue(); } // check the resolved path against the cache auto module = cache.property(modulePath); // modules get cached in `Script.require.cache` and (similar to Node.js) users can access it // to inspect particular entries and invalidate them by deleting the key: // `delete Script.require.cache[Script.require.resolve(moduleId)];` // cacheMeta is just used right now to tell deleted keys apart from undefined ones bool invalidateCache = module.isUndefined() && cacheMeta.property(moduleId).isValid(); // reset the cacheMeta record so invalidation won't apply next time, even if the module fails to load cacheMeta.setProperty(modulePath, QScriptValue()); auto exports = module.property("exports"); if (!invalidateCache && exports.isObject()) { // we have found a cached module -- just need to possibly register it with current parent qCDebug(scriptengine_module) << QString("require - using cached module for '%1' (loaded: %2)") .arg(moduleId).arg(module.property("loaded").toString()); registerModuleWithParent(module, parent); maybeEmitUncaughtException("cached module"); return exports; } // bootstrap / register new empty module module = newModule(modulePath, parent); registerModuleWithParent(module, parent); // add it to the cache (this is done early so any cyclic dependencies pick up) cache.setProperty(modulePath, module); // download the module source auto req = fetchModuleSource(modulePath, invalidateCache); if (!req.contains("success") || !req["success"].toBool()) { auto error = QString("error retrieving script (%1)").arg(req["status"].toString()); return throwModuleError(modulePath, error); } #if DEBUG_JS_MODULES qCDebug(scriptengine_module) << "require.loaded: " << QUrl(req["url"].toString()).fileName() << req["status"].toString(); #endif auto sourceCode = req["contents"].toString(); if (QUrl(modulePath).fileName().endsWith(".json", Qt::CaseInsensitive)) { module.setProperty("content-type", "application/json"); } else { module.setProperty("content-type", "application/javascript"); } // evaluate the module auto result = instantiateModule(module, sourceCode); if (result.isError() && !result.strictlyEquals(module.property("exports"))) { qCWarning(scriptengine_module) << "-- result.isError --" << result.toString(); return throwModuleError(modulePath, result); } // mark as fully-loaded module.setProperty("loaded", true, READONLY_PROP_FLAGS); // set up a new reference point for detecting cache key deletion cacheMeta.setProperty(modulePath, module); qCDebug(scriptengine_module) << "//ScriptEngine::require(" << moduleId << ")"; maybeEmitUncaughtException(__FUNCTION__); return module.property("exports"); } // If a callback is specified, the included files will be loaded asynchronously and the callback will be called // when all of the files have finished loading. // If no callback is specified, the included files will be loaded synchronously and will block execution until // all of the files have finished loading. void ScriptEngine::include(const QStringList& includeFiles, QScriptValue callback) { if (!IS_THREADSAFE_INVOCATION(thread(), __FUNCTION__)) { return; } QSharedPointer scriptEngines(_scriptEngines); if (!scriptEngines || scriptEngines->isStopped()) { scriptWarningMessage("Script.include() while shutting down is ignored... includeFiles:" + includeFiles.join(",") + "parent script:" + getFilename()); return; // bail early } QList urls; for (QString includeFile : includeFiles) { QString file = DependencyManager::get()->normalizeURL(includeFile); QUrl thisURL; bool isStandardLibrary = false; if (file.startsWith("/~/")) { thisURL = expandScriptUrl(QUrl::fromLocalFile(expandScriptPath(file))); QUrl defaultScriptsLoc = PathUtils::defaultScriptsLocation(); if (!defaultScriptsLoc.isParentOf(thisURL)) { scriptWarningMessage("Script.include() -- skipping" + file + "-- outside of standard libraries"); continue; } isStandardLibrary = true; } else { thisURL = resolvePath(file); } bool disallowOutsideFiles = thisURL.isLocalFile() && !isStandardLibrary && !currentSandboxURL.isLocalFile(); if (disallowOutsideFiles && !PathUtils::isDescendantOf(thisURL, currentSandboxURL)) { scriptWarningMessage("Script.include() ignoring file path" + thisURL.toString() + "outside of original entity script" + currentSandboxURL.toString()); } else { // We could also check here for CORS, but we don't yet. // It turns out that QUrl.resolve will not change hosts and copy authority, so we don't need to check that here. urls.append(thisURL); } } // If there are no URLs left to download, don't bother attempting to download anything and return early if (urls.size() == 0) { return; } BatchLoader* loader = new BatchLoader(urls); EntityItemID capturedEntityIdentifier = currentEntityIdentifier; QUrl capturedSandboxURL = currentSandboxURL; auto evaluateScripts = [=](const QMap& data, const QMap& status) { auto parentURL = _parentURL; for (QUrl url : urls) { QString contents = data[url]; if (contents.isNull()) { scriptErrorMessage("Error loading file (" + status[url] +"): " + url.toString()); } else { std::lock_guard lock(_lock); if (!_includedURLs.contains(url)) { _includedURLs << url; // Set the parent url so that path resolution will be relative // to this script's url during its initial evaluation _parentURL = url.toString(); auto operation = [&]() { evaluate(contents, url.toString()); }; doWithEnvironment(capturedEntityIdentifier, capturedSandboxURL, operation); if (hasUncaughtException()) { emit unhandledException(cloneUncaughtException("evaluateInclude")); clearExceptions(); } } else { scriptPrintedMessage("Script.include() skipping evaluation of previously included url:" + url.toString()); } } } _parentURL = parentURL; if (callback.isFunction()) { callWithEnvironment(capturedEntityIdentifier, capturedSandboxURL, QScriptValue(callback), QScriptValue(), QScriptValueList()); } loader->deleteLater(); }; connect(loader, &BatchLoader::finished, this, evaluateScripts); // If we are destroyed before the loader completes, make sure to clean it up connect(this, &QObject::destroyed, loader, &QObject::deleteLater); loader->start(processLevelMaxRetries); if (!callback.isFunction() && !loader->isFinished()) { QEventLoop loop; QObject::connect(loader, &BatchLoader::finished, &loop, &QEventLoop::quit); loop.exec(); } } void ScriptEngine::include(const QString& includeFile, QScriptValue callback) { QSharedPointer scriptEngines(_scriptEngines); if (!scriptEngines || scriptEngines->isStopped()) { scriptWarningMessage("Script.include() while shutting down is ignored... includeFile:" + includeFile + "parent script:" + getFilename()); return; // bail early } QStringList urls; urls.append(includeFile); include(urls, callback); } // NOTE: The load() command is similar to the include() command except that it loads the script // as a stand-alone script. To accomplish this, the ScriptEngine class just emits a signal which // the Application or other context will connect to in order to know to actually load the script void ScriptEngine::load(const QString& loadFile) { if (!IS_THREADSAFE_INVOCATION(thread(), __FUNCTION__)) { return; } QSharedPointer scriptEngines(_scriptEngines); if (!scriptEngines || scriptEngines->isStopped()) { scriptWarningMessage("Script.load() while shutting down is ignored... loadFile:" + loadFile + "parent script:" + getFilename()); return; // bail early } if (!currentEntityIdentifier.isInvalidID()) { scriptWarningMessage("Script.load() from entity script is ignored... loadFile:" + loadFile + "parent script:" + getFilename() + "entity: " + currentEntityIdentifier.toString()); return; // bail early } QUrl url = resolvePath(loadFile); if (_isReloading) { auto scriptCache = DependencyManager::get(); scriptCache->deleteScript(url.toString()); emit reloadScript(url.toString(), false); } else { emit loadScript(url.toString(), false); } } // Look up the handler associated with eventName and entityID. If found, evalute the argGenerator thunk and call the handler with those args void ScriptEngine::forwardHandlerCall(const EntityItemID& entityID, const QString& eventName, QScriptValueList eventHandlerArgs) { if (QThread::currentThread() != thread()) { qCDebug(scriptengine) << "*** ERROR *** ScriptEngine::forwardHandlerCall() called on wrong thread [" << QThread::currentThread() << "], invoking on correct thread [" << thread() << "]"; assert(false); return ; } if (!_registeredHandlers.contains(entityID)) { return; } const RegisteredEventHandlers& handlersOnEntity = _registeredHandlers[entityID]; if (!handlersOnEntity.contains(eventName)) { return; } CallbackList handlersForEvent = handlersOnEntity[eventName]; if (!handlersForEvent.isEmpty()) { for (int i = 0; i < handlersForEvent.count(); ++i) { // handlersForEvent[i] can contain many handlers that may have each been added by different interface or entity scripts, // and the entity scripts may be for entities other than the one this is a handler for. // Fortunately, the definingEntityIdentifier captured the entity script id (if any) when the handler was added. CallbackData& handler = handlersForEvent[i]; callWithEnvironment(handler.definingEntityIdentifier, handler.definingSandboxURL, handler.function, QScriptValue(), eventHandlerArgs); } } } int ScriptEngine::getNumRunningEntityScripts() const { QReadLocker locker { &_entityScriptsLock }; int sum = 0; for (const auto& st : _entityScripts) { if (st.status == EntityScriptStatus::RUNNING) { ++sum; } } return sum; } void ScriptEngine::setEntityScriptDetails(const EntityItemID& entityID, const EntityScriptDetails& details) { { QWriteLocker locker { &_entityScriptsLock }; _entityScripts[entityID] = details; } emit entityScriptDetailsUpdated(); } void ScriptEngine::updateEntityScriptStatus(const EntityItemID& entityID, const EntityScriptStatus &status, const QString& errorInfo) { { QWriteLocker locker { &_entityScriptsLock }; EntityScriptDetails& details = _entityScripts[entityID]; details.status = status; details.errorInfo = errorInfo; } emit entityScriptDetailsUpdated(); } QVariant ScriptEngine::cloneEntityScriptDetails(const EntityItemID& entityID) { static const QVariant NULL_VARIANT { qVariantFromValue((QObject*)nullptr) }; QVariantMap map; if (entityID.isNull()) { // TODO: find better way to report JS Error across thread/process boundaries map["isError"] = true; map["errorInfo"] = "Error: getEntityScriptDetails -- invalid entityID"; } else { #ifdef DEBUG_ENTITY_STATES qDebug() << "cloneEntityScriptDetails" << entityID << QThread::currentThread(); #endif EntityScriptDetails scriptDetails; if (getEntityScriptDetails(entityID, scriptDetails)) { #ifdef DEBUG_ENTITY_STATES qDebug() << "gotEntityScriptDetails" << scriptDetails.status << QThread::currentThread(); #endif map["isRunning"] = isEntityScriptRunning(entityID); map["status"] = EntityScriptStatus_::valueToKey(scriptDetails.status).toLower(); map["errorInfo"] = scriptDetails.errorInfo; map["entityID"] = entityID.toString(); #ifdef DEBUG_ENTITY_STATES { auto debug = QVariantMap(); debug["script"] = scriptDetails.scriptText; debug["scriptObject"] = scriptDetails.scriptObject.toVariant(); debug["lastModified"] = (qlonglong)scriptDetails.lastModified; debug["sandboxURL"] = scriptDetails.definingSandboxURL; map["debug"] = debug; } #endif } else { #ifdef DEBUG_ENTITY_STATES qDebug() << "!gotEntityScriptDetails" << QThread::currentThread(); #endif map["isError"] = true; map["errorInfo"] = "Entity script details unavailable"; map["entityID"] = entityID.toString(); } } return map; } QFuture ScriptEngine::getLocalEntityScriptDetails(const EntityItemID& entityID) { return QtConcurrent::run(this, &ScriptEngine::cloneEntityScriptDetails, entityID); } bool ScriptEngine::getEntityScriptDetails(const EntityItemID& entityID, EntityScriptDetails &details) const { QReadLocker locker { &_entityScriptsLock }; auto it = _entityScripts.constFind(entityID); if (it == _entityScripts.constEnd()) { return false; } details = it.value(); return true; } bool ScriptEngine::hasEntityScriptDetails(const EntityItemID& entityID) const { QReadLocker locker { &_entityScriptsLock }; return _entityScripts.contains(entityID); } void ScriptEngine::loadEntityScript(const EntityItemID& entityID, const QString& entityScript, bool forceRedownload) { if (QThread::currentThread() != thread()) { QMetaObject::invokeMethod(this, "loadEntityScript", Q_ARG(const EntityItemID&, entityID), Q_ARG(const QString&, entityScript), Q_ARG(bool, forceRedownload) ); return; } PROFILE_RANGE(script, __FUNCTION__); QSharedPointer scriptEngines(_scriptEngines); if (isStopping() || !scriptEngines || scriptEngines->isStopped()) { qCDebug(scriptengine) << "loadEntityScript.start " << entityID.toString() << " but isStopping==" << isStopping() << " || engines->isStopped==" << scriptEngines->isStopped(); return; } if (!hasEntityScriptDetails(entityID)) { // make sure EntityScriptDetails has an entry for this UUID right away // (which allows bailing from the loading/provisioning process early if the Entity gets deleted mid-flight) updateEntityScriptStatus(entityID, EntityScriptStatus::PENDING, "...pending..."); } #ifdef DEBUG_ENTITY_STATES { EntityScriptDetails details; bool hasEntityScript = getEntityScriptDetails(entityID, details); qCDebug(scriptengine) << "loadEntityScript.LOADING: " << entityID.toString() << "(previous: " << (hasEntityScript ? details.status : EntityScriptStatus::PENDING) << ")"; } #endif EntityScriptDetails newDetails; newDetails.scriptText = entityScript; newDetails.status = EntityScriptStatus::LOADING; newDetails.definingSandboxURL = currentSandboxURL; setEntityScriptDetails(entityID, newDetails); auto scriptCache = DependencyManager::get(); // note: see EntityTreeRenderer.cpp for shared pointer lifecycle management QWeakPointer weakRef(sharedFromThis()); scriptCache->getScriptContents(entityScript, [this, weakRef, entityScript, entityID](const QString& url, const QString& contents, bool isURL, bool success, const QString& status) { QSharedPointer strongRef(weakRef); if (!strongRef) { qCWarning(scriptengine) << "loadEntityScript.contentAvailable -- ScriptEngine was deleted during getScriptContents!!"; return; } if (isStopping()) { #ifdef DEBUG_ENTITY_STATES qCDebug(scriptengine) << "loadEntityScript.contentAvailable -- stopping"; #endif return; } executeOnScriptThread([=]{ #ifdef DEBUG_ENTITY_STATES qCDebug(scriptengine) << "loadEntityScript.contentAvailable" << status << entityID.toString(); #endif if (!isStopping() && hasEntityScriptDetails(entityID)) { _contentAvailableQueue[entityID] = { entityID, url, contents, isURL, success, status }; } else { #ifdef DEBUG_ENTITY_STATES qCDebug(scriptengine) << "loadEntityScript.contentAvailable -- aborting"; #endif } }); }, forceRedownload); } /**jsdoc * Triggered when the script starts for a user. See also, {@link Script.entityScriptPreloadFinished}. *

Note: Can only be connected to via this.preload = function (...) { ... } in the entity script.

*

Supported Script Types: Client Entity Scripts • Server Entity Scripts

* @function Entities.preload * @param {Uuid} entityID - The ID of the entity that the script is running in. * @returns {Signal} * @example Get the ID of the entity that a client entity script is running in. * var entityScript = (function () { * this.entityID = Uuid.NULL; * * this.preload = function (entityID) { * this.entityID = entityID; * print("Entity ID: " + this.entityID); * }; * }); * * var entityID = Entities.addEntity({ * type: "Box", * position: Vec3.sum(MyAvatar.position, Vec3.multiplyQbyV(MyAvatar.orientation, { x: 0, y: 0, z: -5 })), * dimensions: { x: 0.5, y: 0.5, z: 0.5 }, * color: { red: 255, green: 0, blue: 0 }, * script: "(" + entityScript + ")", // Could host the script on a Web server instead. * lifetime: 300 // Delete after 5 minutes. * }); */ // The JSDoc is for the callEntityScriptMethod() call in this method. // since all of these operations can be asynch we will always do the actual work in the response handler // for the download void ScriptEngine::entityScriptContentAvailable(const EntityItemID& entityID, const QString& scriptOrURL, const QString& contents, bool isURL, bool success , const QString& status) { if (QThread::currentThread() != thread()) { #ifdef THREAD_DEBUGGING qCDebug(scriptengine) << "*** WARNING *** ScriptEngine::entityScriptContentAvailable() called on wrong thread [" << QThread::currentThread() << "], invoking on correct thread [" << thread() << "] " "entityID:" << entityID << "scriptOrURL:" << scriptOrURL << "contents:" << contents << "isURL:" << isURL << "success:" << success; #endif QMetaObject::invokeMethod(this, "entityScriptContentAvailable", Q_ARG(const EntityItemID&, entityID), Q_ARG(const QString&, scriptOrURL), Q_ARG(const QString&, contents), Q_ARG(bool, isURL), Q_ARG(bool, success), Q_ARG(const QString&, status)); return; } #ifdef THREAD_DEBUGGING qCDebug(scriptengine) << "ScriptEngine::entityScriptContentAvailable() thread [" << QThread::currentThread() << "] expected thread [" << thread() << "]"; #endif auto scriptCache = DependencyManager::get(); bool isFileUrl = isURL && scriptOrURL.startsWith("file://"); auto fileName = isURL ? scriptOrURL : "about:EmbeddedEntityScript"; QString entityScript; { QWriteLocker locker { &_entityScriptsLock }; entityScript = _entityScripts[entityID].scriptText; } EntityScriptDetails newDetails; newDetails.scriptText = scriptOrURL; // If an error happens below, we want to update newDetails with the new status info // and also abort any pending Entity loads that are waiting on the exact same script URL. auto setError = [&](const QString &errorInfo, const EntityScriptStatus& status) { newDetails.errorInfo = errorInfo; newDetails.status = status; setEntityScriptDetails(entityID, newDetails); }; // NETWORK / FILESYSTEM ERRORS if (!success) { setError("Failed to load script (" + status + ")", EntityScriptStatus::ERROR_LOADING_SCRIPT); return; } // SYNTAX ERRORS auto syntaxError = lintScript(contents, fileName); if (syntaxError.isError()) { auto message = syntaxError.property("formatted").toString(); if (message.isEmpty()) { message = syntaxError.toString(); } setError(QString("Bad syntax (%1)").arg(message), EntityScriptStatus::ERROR_RUNNING_SCRIPT); syntaxError.setProperty("detail", entityID.toString()); emit unhandledException(syntaxError); return; } QScriptProgram program { contents, fileName }; if (program.isNull()) { setError("Bad program (isNull)", EntityScriptStatus::ERROR_RUNNING_SCRIPT); emit unhandledException(makeError("program.isNull")); return; // done processing script } if (isURL) { setParentURL(scriptOrURL); } // SANITY/PERFORMANCE CHECK USING SANDBOX const int SANDBOX_TIMEOUT = 0.25 * MSECS_PER_SECOND; BaseScriptEngine sandbox; sandbox.setProcessEventsInterval(SANDBOX_TIMEOUT); QScriptValue testConstructor, exception; if (atoi(getenv("UNSAFE_ENTITY_SCRIPTS") ? getenv("UNSAFE_ENTITY_SCRIPTS") : "0")) { QTimer timeout; timeout.setSingleShot(true); timeout.start(SANDBOX_TIMEOUT); connect(&timeout, &QTimer::timeout, [=, &sandbox]{ qCDebug(scriptengine) << "ScriptEngine::entityScriptContentAvailable timeout"; // Guard against infinite loops and non-performant code sandbox.raiseException( sandbox.makeError(QString("Timed out (entity constructors are limited to %1ms)").arg(SANDBOX_TIMEOUT)) ); }); testConstructor = sandbox.evaluate(program); if (sandbox.hasUncaughtException()) { exception = sandbox.cloneUncaughtException(QString("(preflight %1)").arg(entityID.toString())); sandbox.clearExceptions(); } else if (testConstructor.isError()) { exception = testConstructor; } } else { // ENTITY SCRIPT WHITELIST STARTS HERE auto nodeList = DependencyManager::get(); bool passList = false; // assume unsafe QString whitelistPrefix = "[WHITELIST ENTITY SCRIPTS]"; QList safeURLPrefixes = { "file:///", "atp:", "cache:" }; safeURLPrefixes += qEnvironmentVariable("EXTRA_WHITELIST").trimmed().split(QRegExp("\\s*,\\s*"), QString::SkipEmptyParts); // IF WHITELIST IS DISABLED IN SETTINGS bool whitelistEnabled = Setting::Handle("private/whitelistEnabled", true).get(); if (!whitelistEnabled) { passList = true; } // PULL SAFEURLS FROM INTERFACE.JSON Settings QVariant raw = Setting::Handle("private/settingsSafeURLS").get(); QStringList settingsSafeURLS = raw.toString().trimmed().split(QRegExp("\\s*[,\r\n]+\\s*"), QString::SkipEmptyParts); safeURLPrefixes += settingsSafeURLS; // END PULL SAFEURLS FROM INTERFACE.JSON Settings // GET CURRENT DOMAIN WHITELIST BYPASS, IN CASE AN ENTIRE DOMAIN IS WHITELISTED QString currentDomain = DependencyManager::get()->getDomainURL().host(); QString domainSafeIP = nodeList->getDomainHandler().getHostname(); QString domainSafeURL = URL_SCHEME_HIFI + "://" + currentDomain; for (const auto& str : safeURLPrefixes) { if (domainSafeURL.startsWith(str) || domainSafeIP.startsWith(str)) { qCDebug(scriptengine) << whitelistPrefix << "Whitelist Bypassed. Current Domain Host: " << nodeList->getDomainHandler().getHostname() << "Current Domain: " << currentDomain; passList = true; } } // END CURRENT DOMAIN WHITELIST BYPASS // START CHECKING AGAINST THE WHITELIST if (ScriptEngine::getContext() == "entity_server") { // If running on the server, do not engage whitelist. passList = true; } else if (!passList) { // If waved through, do not engage whitelist. for (const auto& str : safeURLPrefixes) { qCDebug(scriptengine) << whitelistPrefix << "Script URL: " << scriptOrURL << "TESTING AGAINST" << str << "RESULTS IN" << scriptOrURL.startsWith(str); if (!str.isEmpty() && scriptOrURL.startsWith(str)) { passList = true; qCDebug(scriptengine) << whitelistPrefix << "Script approved."; break; // bail early since we found a match } } } // END CHECKING AGAINST THE WHITELIST if (!passList) { // If the entity failed to pass for any reason, it's blocked and an error is thrown. qCDebug(scriptengine) << whitelistPrefix << "(disabled entity script)" << entityID.toString() << scriptOrURL; exception = makeError("UNSAFE_ENTITY_SCRIPTS == 0"); } else { QTimer timeout; timeout.setSingleShot(true); timeout.start(SANDBOX_TIMEOUT); connect(&timeout, &QTimer::timeout, [=, &sandbox] { qCDebug(scriptengine) << "ScriptEngine::entityScriptContentAvailable timeout"; // Guard against infinite loops and non-performant code sandbox.raiseException( sandbox.makeError(QString("Timed out (entity constructors are limited to %1ms)").arg(SANDBOX_TIMEOUT))); }); testConstructor = sandbox.evaluate(program); if (sandbox.hasUncaughtException()) { exception = sandbox.cloneUncaughtException(QString("(preflight %1)").arg(entityID.toString())); sandbox.clearExceptions(); } else if (testConstructor.isError()) { exception = testConstructor; } } // ENTITY SCRIPT WHITELIST ENDS HERE, uncomment below for original full disabling. // qDebug() << "(disabled entity script)" << entityID.toString() << scriptOrURL; // exception = makeError("UNSAFE_ENTITY_SCRIPTS == 0"); } if (exception.isError()) { // create a local copy using makeError to decouple from the sandbox engine exception = makeError(exception); setError(formatException(exception, _enableExtendedJSExceptions.get()), EntityScriptStatus::ERROR_RUNNING_SCRIPT); emit unhandledException(exception); return; } // CONSTRUCTOR VIABILITY if (!testConstructor.isFunction()) { QString testConstructorType = QString(testConstructor.toVariant().typeName()); if (testConstructorType == "") { testConstructorType = "empty"; } QString testConstructorValue = testConstructor.toString(); if (testConstructorValue.size() > MAX_DEBUG_VALUE_LENGTH) { testConstructorValue = testConstructorValue.mid(0, MAX_DEBUG_VALUE_LENGTH) + "..."; } auto message = QString("failed to load entity script -- expected a function, got %1, %2") .arg(testConstructorType).arg(testConstructorValue); auto err = makeError(message); err.setProperty("fileName", scriptOrURL); err.setProperty("detail", "(constructor " + entityID.toString() + ")"); setError("Could not find constructor (" + testConstructorType + ")", EntityScriptStatus::ERROR_RUNNING_SCRIPT); emit unhandledException(err); return; // done processing script } // (this feeds into refreshFileScript) int64_t lastModified = 0; if (isFileUrl) { QString file = QUrl(scriptOrURL).toLocalFile(); lastModified = (quint64)QFileInfo(file).lastModified().toMSecsSinceEpoch(); } // THE ACTUAL EVALUATION AND CONSTRUCTION QScriptValue entityScriptConstructor, entityScriptObject; QUrl sandboxURL = currentSandboxURL.isEmpty() ? scriptOrURL : currentSandboxURL; auto initialization = [&]{ entityScriptConstructor = evaluate(contents, fileName); entityScriptObject = entityScriptConstructor.construct(); if (hasUncaughtException()) { entityScriptObject = cloneUncaughtException("(construct " + entityID.toString() + ")"); clearExceptions(); } }; doWithEnvironment(entityID, sandboxURL, initialization); if (entityScriptObject.isError()) { auto exception = entityScriptObject; setError(formatException(exception, _enableExtendedJSExceptions.get()), EntityScriptStatus::ERROR_RUNNING_SCRIPT); emit unhandledException(exception); return; } // ... AND WE HAVE LIFTOFF newDetails.status = EntityScriptStatus::RUNNING; newDetails.scriptObject = entityScriptObject; newDetails.lastModified = lastModified; newDetails.definingSandboxURL = sandboxURL; setEntityScriptDetails(entityID, newDetails); if (isURL) { setParentURL(""); } // if we got this far, then call the preload method callEntityScriptMethod(entityID, "preload"); emit entityScriptPreloadFinished(entityID); } /**jsdoc * Triggered when the script terminates for a user. *

Note: Can only be connected to via this.unoad = function () { ... } in the entity script.

*

Supported Script Types: Client Entity Scripts • Server Entity Scripts

* @function Entities.unload * @param {Uuid} entityID - The ID of the entity that the script is running in. * @returns {Signal} */ // The JSDoc is for the callEntityScriptMethod() call in this method. void ScriptEngine::unloadEntityScript(const EntityItemID& entityID, bool shouldRemoveFromMap) { if (QThread::currentThread() != thread()) { #ifdef THREAD_DEBUGGING qCDebug(scriptengine) << "*** WARNING *** ScriptEngine::unloadEntityScript() called on wrong thread [" << QThread::currentThread() << "], invoking on correct thread [" << thread() << "] " "entityID:" << entityID; #endif QMetaObject::invokeMethod(this, "unloadEntityScript", Q_ARG(const EntityItemID&, entityID), Q_ARG(bool, shouldRemoveFromMap)); return; } #ifdef THREAD_DEBUGGING qCDebug(scriptengine) << "ScriptEngine::unloadEntityScript() called on correct thread [" << thread() << "] " "entityID:" << entityID; #endif EntityScriptDetails oldDetails; if (getEntityScriptDetails(entityID, oldDetails)) { auto scriptText = oldDetails.scriptText; if (isEntityScriptRunning(entityID)) { callEntityScriptMethod(entityID, "unload"); } #ifdef DEBUG_ENTITY_STATES else { qCDebug(scriptengine) << "unload called while !running" << entityID << oldDetails.status; } #endif if (shouldRemoveFromMap) { // this was a deleted entity, we've been asked to remove it from the map { QWriteLocker locker { &_entityScriptsLock }; _entityScripts.remove(entityID); } emit entityScriptDetailsUpdated(); } else if (oldDetails.status != EntityScriptStatus::UNLOADED) { EntityScriptDetails newDetails; newDetails.status = EntityScriptStatus::UNLOADED; newDetails.lastModified = QDateTime::currentMSecsSinceEpoch(); // keep scriptText populated for the current need to "debouce" duplicate calls to unloadEntityScript newDetails.scriptText = scriptText; setEntityScriptDetails(entityID, newDetails); } stopAllTimersForEntityScript(entityID); } } QList ScriptEngine::getListOfEntityScriptIDs() { QReadLocker locker{ &_entityScriptsLock }; return _entityScripts.keys(); } void ScriptEngine::unloadAllEntityScripts(bool blockingCall) { if (QThread::currentThread() != thread()) { #ifdef THREAD_DEBUGGING qCDebug(scriptengine) << "*** WARNING *** ScriptEngine::unloadAllEntityScripts() called on wrong thread [" << QThread::currentThread() << "], invoking on correct thread [" << thread() << "]"; #endif QMetaObject::invokeMethod(this, "unloadAllEntityScripts", blockingCall ? Qt::BlockingQueuedConnection : Qt::QueuedConnection); return; } #ifdef THREAD_DEBUGGING qCDebug(scriptengine) << "ScriptEngine::unloadAllEntityScripts() called on correct thread [" << thread() << "]"; #endif QList keys; { QReadLocker locker{ &_entityScriptsLock }; keys = _entityScripts.keys(); } foreach(const EntityItemID& entityID, keys) { unloadEntityScript(entityID); } { QWriteLocker locker{ &_entityScriptsLock }; _entityScripts.clear(); } emit entityScriptDetailsUpdated(); #ifdef DEBUG_ENGINE_STATE _debugDump( "---- CURRENT STATE OF ENGINE: --------------------------", globalObject(), "--------------------------------------------------------" ); #endif // DEBUG_ENGINE_STATE } void ScriptEngine::refreshFileScript(const EntityItemID& entityID) { if (!HIFI_AUTOREFRESH_FILE_SCRIPTS || !hasEntityScriptDetails(entityID)) { return; } static bool recurseGuard = false; if (recurseGuard) { return; } recurseGuard = true; EntityScriptDetails details; { QWriteLocker locker { &_entityScriptsLock }; details = _entityScripts[entityID]; } // Check to see if a file based script needs to be reloaded (easier debugging) if (details.lastModified > 0) { QString filePath = QUrl(details.scriptText).toLocalFile(); auto lastModified = QFileInfo(filePath).lastModified().toMSecsSinceEpoch(); if (lastModified > details.lastModified) { scriptInfoMessage("Reloading modified script " + details.scriptText); loadEntityScript(entityID, details.scriptText, true); } } recurseGuard = false; } // Execute operation in the appropriate context for (the possibly empty) entityID. // Even if entityID is supplied as currentEntityIdentifier, this still documents the source // of the code being executed (e.g., if we ever sandbox different entity scripts, or provide different // global values for different entity scripts). void ScriptEngine::doWithEnvironment(const EntityItemID& entityID, const QUrl& sandboxURL, std::function operation) { EntityItemID oldIdentifier = currentEntityIdentifier; QUrl oldSandboxURL = currentSandboxURL; currentEntityIdentifier = entityID; currentSandboxURL = sandboxURL; #if DEBUG_CURRENT_ENTITY QScriptValue oldData = this->globalObject().property("debugEntityID"); this->globalObject().setProperty("debugEntityID", entityID.toScriptValue(this)); // Make the entityID available to javascript as a global. operation(); this->globalObject().setProperty("debugEntityID", oldData); #else operation(); #endif maybeEmitUncaughtException(!entityID.isNull() ? entityID.toString() : __FUNCTION__); currentEntityIdentifier = oldIdentifier; currentSandboxURL = oldSandboxURL; } void ScriptEngine::callWithEnvironment(const EntityItemID& entityID, const QUrl& sandboxURL, QScriptValue function, QScriptValue thisObject, QScriptValueList args) { auto operation = [&]() { function.call(thisObject, args); }; doWithEnvironment(entityID, sandboxURL, operation); } void ScriptEngine::callEntityScriptMethod(const EntityItemID& entityID, const QString& methodName, const QStringList& params, const QUuid& remoteCallerID) { if (QThread::currentThread() != thread()) { #ifdef THREAD_DEBUGGING qCDebug(scriptengine) << "*** WARNING *** ScriptEngine::callEntityScriptMethod() called on wrong thread [" << QThread::currentThread() << "], invoking on correct thread [" << thread() << "] " "entityID:" << entityID << "methodName:" << methodName; #endif QMetaObject::invokeMethod(this, "callEntityScriptMethod", Q_ARG(const EntityItemID&, entityID), Q_ARG(const QString&, methodName), Q_ARG(const QStringList&, params), Q_ARG(const QUuid&, remoteCallerID)); return; } #ifdef THREAD_DEBUGGING qCDebug(scriptengine) << "ScriptEngine::callEntityScriptMethod() called on correct thread [" << thread() << "] " "entityID:" << entityID << "methodName:" << methodName; #endif if (HIFI_AUTOREFRESH_FILE_SCRIPTS && methodName != "unload") { refreshFileScript(entityID); } if (isEntityScriptRunning(entityID)) { EntityScriptDetails details; { QWriteLocker locker { &_entityScriptsLock }; details = _entityScripts[entityID]; } QScriptValue entityScript = details.scriptObject; // previously loaded // If this is a remote call, we need to check to see if the function is remotely callable // we do this by checking for the existance of the 'remotelyCallable' property on the // entityScript. And we confirm that the method name is included. If this fails, the // function will not be called. bool callAllowed = false; if (remoteCallerID == QUuid()) { callAllowed = true; } else { if (entityScript.property("remotelyCallable").isArray()) { auto callables = entityScript.property("remotelyCallable"); auto callableCount = callables.property("length").toInteger(); for (int i = 0; i < callableCount; i++) { auto callable = callables.property(i).toString(); if (callable == methodName) { callAllowed = true; break; } } } if (!callAllowed) { qDebug() << "Method [" << methodName << "] not remotely callable."; } } if (callAllowed && entityScript.property(methodName).isFunction()) { QScriptValueList args; args << entityID.toScriptValue(this); args << qScriptValueFromSequence(this, params); QScriptValue oldData = this->globalObject().property("Script").property("remoteCallerID"); this->globalObject().property("Script").setProperty("remoteCallerID", remoteCallerID.toString()); // Make the remoteCallerID available to javascript as a global. callWithEnvironment(entityID, details.definingSandboxURL, entityScript.property(methodName), entityScript, args); this->globalObject().property("Script").setProperty("remoteCallerID", oldData); } } } void ScriptEngine::callEntityScriptMethod(const EntityItemID& entityID, const QString& methodName, const PointerEvent& event) { if (QThread::currentThread() != thread()) { #ifdef THREAD_DEBUGGING qCDebug(scriptengine) << "*** WARNING *** ScriptEngine::callEntityScriptMethod() called on wrong thread [" << QThread::currentThread() << "], invoking on correct thread [" << thread() << "] " "entityID:" << entityID << "methodName:" << methodName << "event: mouseEvent"; #endif QMetaObject::invokeMethod(this, "callEntityScriptMethod", Q_ARG(const EntityItemID&, entityID), Q_ARG(const QString&, methodName), Q_ARG(const PointerEvent&, event)); return; } #ifdef THREAD_DEBUGGING qCDebug(scriptengine) << "ScriptEngine::callEntityScriptMethod() called on correct thread [" << thread() << "] " "entityID:" << entityID << "methodName:" << methodName << "event: pointerEvent"; #endif if (HIFI_AUTOREFRESH_FILE_SCRIPTS) { refreshFileScript(entityID); } if (isEntityScriptRunning(entityID)) { EntityScriptDetails details; { QWriteLocker locker { &_entityScriptsLock }; details = _entityScripts[entityID]; } QScriptValue entityScript = details.scriptObject; // previously loaded if (entityScript.property(methodName).isFunction()) { QScriptValueList args; args << entityID.toScriptValue(this); args << event.toScriptValue(this); callWithEnvironment(entityID, details.definingSandboxURL, entityScript.property(methodName), entityScript, args); } } } void ScriptEngine::callEntityScriptMethod(const EntityItemID& entityID, const QString& methodName, const EntityItemID& otherID, const Collision& collision) { if (QThread::currentThread() != thread()) { #ifdef THREAD_DEBUGGING qCDebug(scriptengine) << "*** WARNING *** ScriptEngine::callEntityScriptMethod() called on wrong thread [" << QThread::currentThread() << "], invoking on correct thread [" << thread() << "] " "entityID:" << entityID << "methodName:" << methodName << "otherID:" << otherID << "collision: collision"; #endif QMetaObject::invokeMethod(this, "callEntityScriptMethod", Q_ARG(const EntityItemID&, entityID), Q_ARG(const QString&, methodName), Q_ARG(const EntityItemID&, otherID), Q_ARG(const Collision&, collision)); return; } #ifdef THREAD_DEBUGGING qCDebug(scriptengine) << "ScriptEngine::callEntityScriptMethod() called on correct thread [" << thread() << "] " "entityID:" << entityID << "methodName:" << methodName << "otherID:" << otherID << "collision: collision"; #endif if (HIFI_AUTOREFRESH_FILE_SCRIPTS) { refreshFileScript(entityID); } if (isEntityScriptRunning(entityID)) { EntityScriptDetails details; { QWriteLocker locker { &_entityScriptsLock }; details = _entityScripts[entityID]; } QScriptValue entityScript = details.scriptObject; // previously loaded if (entityScript.property(methodName).isFunction()) { QScriptValueList args; args << entityID.toScriptValue(this); args << otherID.toScriptValue(this); args << collisionToScriptValue(this, collision); callWithEnvironment(entityID, details.definingSandboxURL, entityScript.property(methodName), entityScript, args); } } }