} } // 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 = 50 * MSECS_PER_SECOND; if (!workerThread->wait(MAX_SCRIPT_QUITTING_TIME)) { Q_ASSERT(false); workerThread->terminate(); } }*/ if (shutdown) { // NOTE: This will be called on the main application thread (among other threads) from stopAllScripts. // The thread will need to continue to process events, because // the scripts will likely need to marshall messages across to the main thread, e.g. // if they access Settings or Menu in any of their shutdown code. So: // Process events for this thread, allowing invokeMethod calls to pass between threads. QCoreApplication::processEvents(); } // Avoid a pure busy wait QThread::msleep(1); }#endif scriptInfoMessage("Script Engine has stopped:" + getFilename(), "", -1); }} void ScriptManager::removeFromScriptEngines() { Q_ASSERT(_scriptEngines); _scriptEngines.toStrongRef()->removeScriptEngine(shared_from_this());} QString ScriptManager::getFilename() const { QStringList fileNameParts = _fileNameString.split("/"); QString lastPart; if (!fileNameParts.isEmpty()) { lastPart = fileNameParts.last(); } return lastPart;} QString ScriptManager::getAbsoluteFilename() const { return _fileNameString;} bool ScriptManager::hasValidScriptSuffix(const QString& scriptFileName) { QFileInfo fileInfo(scriptFileName); QString scriptSuffixToLower = fileInfo.completeSuffix().toLower(); return scriptSuffixToLower.contains(QString("js"), Qt::CaseInsensitive);} void ScriptManager::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", _fileNameString, -1); emit errorLoadingScript(_fileNameString); return; } const auto maxRetries = 0; // for consistency with previous scriptCache->getScript() behavior auto scriptCache = DependencyManager::get<ScriptCache>(); scriptCache->getScriptContents(url.toString(), [this](const QString& url, const QString& scriptContents, bool isURL, bool success, const QString&status) { qCDebug(scriptengine) << "loadURL" << url << status << QThread::currentThread(); if (!success) { scriptErrorMessage("ERROR Loading file (" + status + "):" + url, url, -1); emit errorLoadingScript(_fileNameString); return; } _scriptContents = scriptContents; emit scriptLoaded(url); }, reload, maxRetries);} void ScriptManager::scriptErrorMessage(const QString& message, const QString& fileName, int lineNumber) { qCCritical(scriptengine, "[%s] %s", qUtf8Printable(getFilename()), qUtf8Printable(message)); emit errorMessage(message, getFilename()); if (!currentEntityIdentifier.isInvalidID()) { emit errorEntityMessage(message, fileName, lineNumber, currentEntityIdentifier, isEntityServerScript()); }} void ScriptManager::scriptWarningMessage(const QString& message, const QString& fileName, int lineNumber) { qCWarning(scriptengine, "[%s] %s", qUtf8Printable(getFilename()), qUtf8Printable(message)); emit warningMessage(message, getFilename()); if (!currentEntityIdentifier.isInvalidID()) { emit warningEntityMessage(message, fileName, lineNumber, currentEntityIdentifier, isEntityServerScript()); }} void ScriptManager::scriptInfoMessage(const QString& message, const QString& fileName, int lineNumber) { qCInfo(scriptengine, "[%s] %s", qUtf8Printable(getFilename()), qUtf8Printable(message)); emit infoMessage(message, getFilename()); if (!currentEntityIdentifier.isInvalidID()) { emit infoEntityMessage(message, fileName, lineNumber, currentEntityIdentifier, isEntityServerScript()); }} void ScriptManager::scriptPrintedMessage(const QString& message, const QString& fileName, int lineNumber) { qCDebug(scriptengine, "[%s] %s", qUtf8Printable(getFilename()), qUtf8Printable(message)); emit printedMessage(message, getFilename()); if (!currentEntityIdentifier.isInvalidID()) { emit printedEntityMessage(message, fileName, lineNumber, currentEntityIdentifier, isEntityServerScript()); }} void ScriptManager::clearDebugLogWindow() { emit clearDebugWindow();} // Templated qScriptRegisterMetaType fails to compile with raw pointersusing ScriptableResourceRawPtr = ScriptableResource*; static ScriptValue scriptableResourceToScriptValue(ScriptEngine* engine, const ScriptableResourceRawPtr& resource) { if (!resource) { return ScriptValue(); // 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(); auto manager = engine->manager(); if (data && manager && !resource->isInScript()) { resource->setInScript(true); QObject::connect(data.data(), &Resource::updateSize, manager, &ScriptManager::updateMemoryCost); } auto object = engine->newQObject(const_cast<ScriptableResourceRawPtr>(resource), ScriptEngine::ScriptOwnership); return object;} static bool scriptableResourceFromScriptValue(const ScriptValue& value, ScriptableResourceRawPtr& resource) { resource = static_cast<ScriptableResourceRawPtr>(value.toQObject()); return true;} /*@jsdoc * The <code>Resource</code> 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. <em>Read-only.</em> */static ScriptValue createScriptableResourcePrototype(ScriptManagerPointer manager) { auto engine = manager->engine(); auto prototype = engine->newObject(); auto state = engine->newObject(); auto metaEnum = QMetaEnum::fromType<ScriptableResource::State>(); for (int i = 0; i < metaEnum.keyCount(); ++i) { state.setProperty(metaEnum.key(i), metaEnum.value(i)); } prototype.setProperty("State", state); return prototype;} ScriptValue externalResourceBucketToScriptValue(ScriptEngine* engine, ExternalResource::Bucket const& in) { return engine->newValue((int)in);} bool externalResourceBucketFromScriptValue(const ScriptValue& object, ExternalResource::Bucket& out) { out = static_cast<ExternalResource::Bucket>(object.toInt32()); return true;} void ScriptManager::resetModuleCache(bool deleteScriptCache) { if (QThread::currentThread() != thread()) { executeOnScriptThread([=]() { resetModuleCache(deleteScriptCache); }); return; } auto jsRequire = _engine->globalObject().property("Script").property("require"); auto cache = jsRequire.property("cache"); auto cacheMeta = jsRequire.data(); if (deleteScriptCache) { auto it = cache.newIterator(); while (it->hasNext()) { it->next(); if (it->flags() & ScriptValue::SkipInEnumeration) { continue; } qCDebug(scriptengine) << "resetModuleCache(true) -- staging " << it->name() << " for cache reset at next require"; cacheMeta.setProperty(it->name(), true); } } cache = _engine->newObject(); if (!cacheMeta.isObject()) { cacheMeta = _engine->newObject(); cacheMeta.setProperty("id", "Script.require.cacheMeta"); cacheMeta.setProperty("type", "cacheMeta"); jsRequire.setData(cacheMeta); } cache.setProperty("__created__", (double)QDateTime::currentMSecsSinceEpoch(), ScriptValue::SkipInEnumeration);#if DEBUG_JS_MODULES cache.setProperty("__meta__", cacheMeta, READONLY_HIDDEN_PROP_FLAGS);#endif jsRequire.setProperty("cache", cache, READONLY_PROP_FLAGS);} void ScriptManager::initMetaTypes() { if (_areMetaTypesInitialized) { return; } _areMetaTypesInitialized = true; runStaticTypesInitializers(this);} void ScriptManager::init() { if (_isInitialized) { return; // only initialize once } _isInitialized = true; if (_context != NETWORKLESS_TEST_SCRIPT) { // This initializes a bunch of systems that want network access. We // want to avoid it in test script mode. runStaticInitializers(this); } auto scriptEngine = _engine.get(); if (_context != NETWORKLESS_TEST_SCRIPT) { // For test scripts we want to minimize the amount of functionality available, for the least // amount of dependencies and faster test system startup. ScriptValue xmlHttpRequestConstructorValue = scriptEngine->newFunction(XMLHttpRequestClass::constructor); scriptEngine->globalObject().setProperty("XMLHttpRequest", xmlHttpRequestConstructorValue); ScriptValue webSocketConstructorValue = scriptEngine->newFunction(WebSocketClass::constructor); scriptEngine->globalObject().setProperty("WebSocket", webSocketConstructorValue); } /*@jsdoc * Prints a message to the program log and emits {@link Script.printedMessage}. * The message logged is the message values separated by spaces. * <p>Alternatively, you can use {@link Script.print} or one of the {@link console} API methods.</p> * @function print * @param {...*} [message] - The message values to print. */ scriptEngine->globalObject().setProperty("print", scriptEngine->newFunction(debugPrint)); // NOTE: You do not want to end up creating new instances of singletons here. They will be on the ScriptManager thread // and are likely to be unusable if we "reset" the ScriptManager by creating a new one (on a whole new thread). scriptEngine->registerGlobalObject("Script", _scriptingInterface.get()); { // set up Script.require.resolve and Script.require.cache auto Script = scriptEngine->globalObject().property("Script"); auto require = Script.property("require"); auto resolve = Script.property("_requireResolve"); require.setProperty("resolve", resolve, READONLY_PROP_FLAGS); scriptEngine->globalObject().setProperty("require", require, READONLY_PROP_FLAGS); resetModuleCache(); } scriptEngine->registerEnum("Script.ExternalPaths", QMetaEnum::fromType<ExternalResource::Bucket>()); scriptEngine->registerGlobalObject("Quat", &_quatLibrary); scriptEngine->registerGlobalObject("Vec3", &_vec3Library); scriptEngine->registerGlobalObject("Mat4", &_mat4Library); scriptEngine->registerGlobalObject("Uuid", &_uuidLibrary); if (_context != NETWORKLESS_TEST_SCRIPT) { // This requires networking, we want to avoid the need for it in test scripts scriptEngine->registerGlobalObject("Messages", DependencyManager::get<MessagesClient>().data()); } scriptEngine->registerGlobalObject("console", &_consoleScriptingInterface); scriptEngine->registerFunction("console", "info", ConsoleScriptingInterface::info, scriptEngine->currentContext()->argumentCount()); scriptEngine->registerFunction("console", "log", ConsoleScriptingInterface::log, scriptEngine->currentContext()->argumentCount()); scriptEngine->registerFunction("console", "debug", ConsoleScriptingInterface::debug, scriptEngine->currentContext()->argumentCount()); scriptEngine->registerFunction("console", "warn", ConsoleScriptingInterface::warn, scriptEngine->currentContext()->argumentCount()); scriptEngine->registerFunction("console", "error", ConsoleScriptingInterface::error, scriptEngine->currentContext()->argumentCount()); scriptEngine->registerFunction("console", "exception", ConsoleScriptingInterface::exception, scriptEngine->currentContext()->argumentCount()); scriptEngine->registerFunction("console", "assert", ConsoleScriptingInterface::assertion, scriptEngine->currentContext()->argumentCount()); scriptEngine->registerFunction("console", "group", ConsoleScriptingInterface::group, 1); scriptEngine->registerFunction("console", "groupCollapsed", ConsoleScriptingInterface::groupCollapsed, 1); scriptEngine->registerFunction("console", "groupEnd", ConsoleScriptingInterface::groupEnd, 0); // constants scriptEngine->globalObject().setProperty("TREE_SCALE", scriptEngine->newValue(TREE_SCALE)); if (_context != NETWORKLESS_TEST_SCRIPT) { // Scriptable cache access auto resourcePrototype = createScriptableResourcePrototype(shared_from_this()); scriptEngine->globalObject().setProperty("Resource", resourcePrototype); scriptEngine->setDefaultPrototype(qMetaTypeId<ScriptableResource*>(), resourcePrototype); scriptEngine->registerGlobalObject("Assets", _assetScriptingInterface); scriptEngine->registerGlobalObject("Resources", DependencyManager::get<ResourceScriptingInterface>().data()); scriptEngine->registerGlobalObject("DebugDraw", &DebugDraw::getInstance()); scriptEngine->registerGlobalObject("UserActivityLogger", DependencyManager::get<UserActivityLoggerScriptingInterface>().data()); } #if DEV_BUILD || PR_BUILD scriptEngine->registerGlobalObject("StackTest", new StackTestScriptingInterface(this));#endif qCDebug(scriptengine) << "Engine initialized";} // registers a global object by namevoid ScriptManager::registerValue(const QString& valueName, ScriptValue value) { _engine->globalObject().setProperty(valueName, value);} // Unregister the handlers for this eventName and entityID.void ScriptManager::removeEventHandler(const EntityItemID& entityID, const QString& eventName, const ScriptValue& handler) { if (QThread::currentThread() != thread()) {#ifdef THREAD_DEBUGGING qCDebug(scriptengine) << "*** WARNING *** ScriptManager::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(const ScriptValue&, handler)); return; }#ifdef THREAD_DEBUGGING qCDebug(scriptengine) << "ScriptManager::removeEventHandler() called on thread [" << QThread::currentThread() << "] entityID:" << entityID << " eventName : " << eventName;#endif if (!_registeredHandlers.contains(entityID)) { return; } RegisteredEventHandlers& handlersOnEntity = _registeredHandlers[entityID]; CallbackList& handlersForEvent = handlersOnEntity[eventName]; // ScriptValue 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. } }} // Unregister all event handlers for the specified entityID (i.e. the entity is being removed)void ScriptManager::removeAllEventHandlers(const EntityItemID& entityID) { if (QThread::currentThread() != thread()) {#ifdef THREAD_DEBUGGING qCDebug(scriptengine) << "*** WARNING *** ScriptManager::removeAllEventHandlers() called on wrong thread [" << QThread::currentThread() << ", correct thread is " << thread() << " ], ignoring " "entityID:" << entityID;#endif return; } if (_registeredHandlers.contains(entityID)) { _registeredHandlers.remove(entityID); }} // Register the handler.void ScriptManager::addEventHandler(const EntityItemID& entityID, const QString& eventName, const ScriptValue& handler) { if (QThread::currentThread() != thread()) {#ifdef THREAD_DEBUGGING qCDebug(scriptengine) << "*** WARNING *** ScriptManager::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(const ScriptValue&, handler)); return; }#ifdef THREAD_DEBUGGING qCDebug(scriptengine) << "ScriptManager::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... emit attachDefaultEventHandlers(); } 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().} bool ScriptManager::isStopped() const { if (_context == NETWORKLESS_TEST_SCRIPT) { return false; } QSharedPointer<ScriptEngines> scriptEngines(_scriptEngines); return !scriptEngines || scriptEngines->isStopped();} void ScriptManager::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); } if (QThread::currentThread() != _assetScriptingInterface->thread()) { _assetScriptingInterface->moveToThread(QThread::currentThread()); } auto filenameParts = _fileNameString.split("/"); auto name = filenameParts.size() > 0 ? filenameParts[filenameParts.size() - 1] : "unknown"; PROFILE_SET_THREAD_NAME("Script: " + name); if (isStopped()) { qCCritical(scriptengine) << "ScriptManager is stopped or ScriptEngines is not available, refusing to run script"; return; // bail early - avoid setting state in init(), as evaluate() will bail too } scriptInfoMessage("Script Engine starting:" + getFilename(), getFilename(), -1); if (!_isInitialized) { init(); } _isRunning = true; emit runningStateChanged(); { PROFILE_RANGE(script, _fileNameString); _returnValue = _engine->evaluate(_scriptContents, _fileNameString); if (_engine->hasUncaughtException() && _abortOnUncaughtException) { qCWarning(scriptengine) << "Engine has uncaught exception, stopping"; stop(); // V8TODO: Is clearing needed here? //_engine->clearExceptions(); } }#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; _lastUpdate = usecTimestampNow(); std::chrono::microseconds totalUpdates(0); qCDebug(scriptengine) << "Waiting for finish"; // TODO: Integrate this with signals/slots instead of reimplementing throttling for ScriptManager while (!_isFinished) { //qCDebug(scriptengine) << "In script event loop"; 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;Lines 501–1000 of 2763. Previous lines Load more
} } // 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 = 50 * MSECS_PER_SECOND; if (!workerThread->wait(MAX_SCRIPT_QUITTING_TIME)) { Q_ASSERT(false); workerThread->terminate(); } }*/ if (shutdown) { // NOTE: This will be called on the main application thread (among other threads) from stopAllScripts. // The thread will need to continue to process events, because // the scripts will likely need to marshall messages across to the main thread, e.g. // if they access Settings or Menu in any of their shutdown code. So: // Process events for this thread, allowing invokeMethod calls to pass between threads. QCoreApplication::processEvents(); } // Avoid a pure busy wait QThread::msleep(1); } #endif scriptInfoMessage("Script Engine has stopped:" + getFilename(), "", -1); } } void ScriptManager::removeFromScriptEngines() { Q_ASSERT(_scriptEngines); _scriptEngines.toStrongRef()->removeScriptEngine(shared_from_this()); } QString ScriptManager::getFilename() const { QStringList fileNameParts = _fileNameString.split("/"); QString lastPart; if (!fileNameParts.isEmpty()) { lastPart = fileNameParts.last(); } return lastPart; } QString ScriptManager::getAbsoluteFilename() const { return _fileNameString; } bool ScriptManager::hasValidScriptSuffix(const QString& scriptFileName) { QFileInfo fileInfo(scriptFileName); QString scriptSuffixToLower = fileInfo.completeSuffix().toLower(); return scriptSuffixToLower.contains(QString("js"), Qt::CaseInsensitive); } void ScriptManager::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", _fileNameString, -1); emit errorLoadingScript(_fileNameString); return; } const auto maxRetries = 0; // for consistency with previous scriptCache->getScript() behavior auto scriptCache = DependencyManager::get<ScriptCache>(); scriptCache->getScriptContents(url.toString(), [this](const QString& url, const QString& scriptContents, bool isURL, bool success, const QString&status) { qCDebug(scriptengine) << "loadURL" << url << status << QThread::currentThread(); if (!success) { scriptErrorMessage("ERROR Loading file (" + status + "):" + url, url, -1); emit errorLoadingScript(_fileNameString); return; } _scriptContents = scriptContents; emit scriptLoaded(url); }, reload, maxRetries); } void ScriptManager::scriptErrorMessage(const QString& message, const QString& fileName, int lineNumber) { qCCritical(scriptengine, "[%s] %s", qUtf8Printable(getFilename()), qUtf8Printable(message)); emit errorMessage(message, getFilename()); if (!currentEntityIdentifier.isInvalidID()) { emit errorEntityMessage(message, fileName, lineNumber, currentEntityIdentifier, isEntityServerScript()); } } void ScriptManager::scriptWarningMessage(const QString& message, const QString& fileName, int lineNumber) { qCWarning(scriptengine, "[%s] %s", qUtf8Printable(getFilename()), qUtf8Printable(message)); emit warningMessage(message, getFilename()); if (!currentEntityIdentifier.isInvalidID()) { emit warningEntityMessage(message, fileName, lineNumber, currentEntityIdentifier, isEntityServerScript()); } } void ScriptManager::scriptInfoMessage(const QString& message, const QString& fileName, int lineNumber) { qCInfo(scriptengine, "[%s] %s", qUtf8Printable(getFilename()), qUtf8Printable(message)); emit infoMessage(message, getFilename()); if (!currentEntityIdentifier.isInvalidID()) { emit infoEntityMessage(message, fileName, lineNumber, currentEntityIdentifier, isEntityServerScript()); } } void ScriptManager::scriptPrintedMessage(const QString& message, const QString& fileName, int lineNumber) { qCDebug(scriptengine, "[%s] %s", qUtf8Printable(getFilename()), qUtf8Printable(message)); emit printedMessage(message, getFilename()); if (!currentEntityIdentifier.isInvalidID()) { emit printedEntityMessage(message, fileName, lineNumber, currentEntityIdentifier, isEntityServerScript()); } } void ScriptManager::clearDebugLogWindow() { emit clearDebugWindow(); } // Templated qScriptRegisterMetaType fails to compile with raw pointers using ScriptableResourceRawPtr = ScriptableResource*; static ScriptValue scriptableResourceToScriptValue(ScriptEngine* engine, const ScriptableResourceRawPtr& resource) { if (!resource) { return ScriptValue(); // 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(); auto manager = engine->manager(); if (data && manager && !resource->isInScript()) { resource->setInScript(true); QObject::connect(data.data(), &Resource::updateSize, manager, &ScriptManager::updateMemoryCost); } auto object = engine->newQObject(const_cast<ScriptableResourceRawPtr>(resource), ScriptEngine::ScriptOwnership); return object; } static bool scriptableResourceFromScriptValue(const ScriptValue& value, ScriptableResourceRawPtr& resource) { resource = static_cast<ScriptableResourceRawPtr>(value.toQObject()); return true; } /*@jsdoc * The <code>Resource</code> 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. <em>Read-only.</em> */ static ScriptValue createScriptableResourcePrototype(ScriptManagerPointer manager) { auto engine = manager->engine(); auto prototype = engine->newObject(); auto state = engine->newObject(); auto metaEnum = QMetaEnum::fromType<ScriptableResource::State>(); for (int i = 0; i < metaEnum.keyCount(); ++i) { state.setProperty(metaEnum.key(i), metaEnum.value(i)); } prototype.setProperty("State", state); return prototype; } ScriptValue externalResourceBucketToScriptValue(ScriptEngine* engine, ExternalResource::Bucket const& in) { return engine->newValue((int)in); } bool externalResourceBucketFromScriptValue(const ScriptValue& object, ExternalResource::Bucket& out) { out = static_cast<ExternalResource::Bucket>(object.toInt32()); return true; } void ScriptManager::resetModuleCache(bool deleteScriptCache) { if (QThread::currentThread() != thread()) { executeOnScriptThread([=]() { resetModuleCache(deleteScriptCache); }); return; } auto jsRequire = _engine->globalObject().property("Script").property("require"); auto cache = jsRequire.property("cache"); auto cacheMeta = jsRequire.data(); if (deleteScriptCache) { auto it = cache.newIterator(); while (it->hasNext()) { it->next(); if (it->flags() & ScriptValue::SkipInEnumeration) { continue; } qCDebug(scriptengine) << "resetModuleCache(true) -- staging " << it->name() << " for cache reset at next require"; cacheMeta.setProperty(it->name(), true); } } cache = _engine->newObject(); if (!cacheMeta.isObject()) { cacheMeta = _engine->newObject(); cacheMeta.setProperty("id", "Script.require.cacheMeta"); cacheMeta.setProperty("type", "cacheMeta"); jsRequire.setData(cacheMeta); } cache.setProperty("__created__", (double)QDateTime::currentMSecsSinceEpoch(), ScriptValue::SkipInEnumeration); #if DEBUG_JS_MODULES cache.setProperty("__meta__", cacheMeta, READONLY_HIDDEN_PROP_FLAGS); #endif jsRequire.setProperty("cache", cache, READONLY_PROP_FLAGS); } void ScriptManager::initMetaTypes() { if (_areMetaTypesInitialized) { return; } _areMetaTypesInitialized = true; runStaticTypesInitializers(this); } void ScriptManager::init() { if (_isInitialized) { return; // only initialize once } _isInitialized = true; if (_context != NETWORKLESS_TEST_SCRIPT) { // This initializes a bunch of systems that want network access. We // want to avoid it in test script mode. runStaticInitializers(this); } auto scriptEngine = _engine.get(); if (_context != NETWORKLESS_TEST_SCRIPT) { // For test scripts we want to minimize the amount of functionality available, for the least // amount of dependencies and faster test system startup. ScriptValue xmlHttpRequestConstructorValue = scriptEngine->newFunction(XMLHttpRequestClass::constructor); scriptEngine->globalObject().setProperty("XMLHttpRequest", xmlHttpRequestConstructorValue); ScriptValue webSocketConstructorValue = scriptEngine->newFunction(WebSocketClass::constructor); scriptEngine->globalObject().setProperty("WebSocket", webSocketConstructorValue); } /*@jsdoc * Prints a message to the program log and emits {@link Script.printedMessage}. * The message logged is the message values separated by spaces. * <p>Alternatively, you can use {@link Script.print} or one of the {@link console} API methods.</p> * @function print * @param {...*} [message] - The message values to print. */ scriptEngine->globalObject().setProperty("print", scriptEngine->newFunction(debugPrint)); // NOTE: You do not want to end up creating new instances of singletons here. They will be on the ScriptManager thread // and are likely to be unusable if we "reset" the ScriptManager by creating a new one (on a whole new thread). scriptEngine->registerGlobalObject("Script", _scriptingInterface.get()); { // set up Script.require.resolve and Script.require.cache auto Script = scriptEngine->globalObject().property("Script"); auto require = Script.property("require"); auto resolve = Script.property("_requireResolve"); require.setProperty("resolve", resolve, READONLY_PROP_FLAGS); scriptEngine->globalObject().setProperty("require", require, READONLY_PROP_FLAGS); resetModuleCache(); } scriptEngine->registerEnum("Script.ExternalPaths", QMetaEnum::fromType<ExternalResource::Bucket>()); scriptEngine->registerGlobalObject("Quat", &_quatLibrary); scriptEngine->registerGlobalObject("Vec3", &_vec3Library); scriptEngine->registerGlobalObject("Mat4", &_mat4Library); scriptEngine->registerGlobalObject("Uuid", &_uuidLibrary); if (_context != NETWORKLESS_TEST_SCRIPT) { // This requires networking, we want to avoid the need for it in test scripts scriptEngine->registerGlobalObject("Messages", DependencyManager::get<MessagesClient>().data()); } scriptEngine->registerGlobalObject("console", &_consoleScriptingInterface); scriptEngine->registerFunction("console", "info", ConsoleScriptingInterface::info, scriptEngine->currentContext()->argumentCount()); scriptEngine->registerFunction("console", "log", ConsoleScriptingInterface::log, scriptEngine->currentContext()->argumentCount()); scriptEngine->registerFunction("console", "debug", ConsoleScriptingInterface::debug, scriptEngine->currentContext()->argumentCount()); scriptEngine->registerFunction("console", "warn", ConsoleScriptingInterface::warn, scriptEngine->currentContext()->argumentCount()); scriptEngine->registerFunction("console", "error", ConsoleScriptingInterface::error, scriptEngine->currentContext()->argumentCount()); scriptEngine->registerFunction("console", "exception", ConsoleScriptingInterface::exception, scriptEngine->currentContext()->argumentCount()); scriptEngine->registerFunction("console", "assert", ConsoleScriptingInterface::assertion, scriptEngine->currentContext()->argumentCount()); scriptEngine->registerFunction("console", "group", ConsoleScriptingInterface::group, 1); scriptEngine->registerFunction("console", "groupCollapsed", ConsoleScriptingInterface::groupCollapsed, 1); scriptEngine->registerFunction("console", "groupEnd", ConsoleScriptingInterface::groupEnd, 0); // constants scriptEngine->globalObject().setProperty("TREE_SCALE", scriptEngine->newValue(TREE_SCALE)); if (_context != NETWORKLESS_TEST_SCRIPT) { // Scriptable cache access auto resourcePrototype = createScriptableResourcePrototype(shared_from_this()); scriptEngine->globalObject().setProperty("Resource", resourcePrototype); scriptEngine->setDefaultPrototype(qMetaTypeId<ScriptableResource*>(), resourcePrototype); scriptEngine->registerGlobalObject("Assets", _assetScriptingInterface); scriptEngine->registerGlobalObject("Resources", DependencyManager::get<ResourceScriptingInterface>().data()); scriptEngine->registerGlobalObject("DebugDraw", &DebugDraw::getInstance()); scriptEngine->registerGlobalObject("UserActivityLogger", DependencyManager::get<UserActivityLoggerScriptingInterface>().data()); } #if DEV_BUILD || PR_BUILD scriptEngine->registerGlobalObject("StackTest", new StackTestScriptingInterface(this)); #endif qCDebug(scriptengine) << "Engine initialized"; } // registers a global object by name void ScriptManager::registerValue(const QString& valueName, ScriptValue value) { _engine->globalObject().setProperty(valueName, value); } // Unregister the handlers for this eventName and entityID. void ScriptManager::removeEventHandler(const EntityItemID& entityID, const QString& eventName, const ScriptValue& handler) { if (QThread::currentThread() != thread()) { #ifdef THREAD_DEBUGGING qCDebug(scriptengine) << "*** WARNING *** ScriptManager::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(const ScriptValue&, handler)); return; } #ifdef THREAD_DEBUGGING qCDebug(scriptengine) << "ScriptManager::removeEventHandler() called on thread [" << QThread::currentThread() << "] entityID:" << entityID << " eventName : " << eventName; #endif if (!_registeredHandlers.contains(entityID)) { return; } RegisteredEventHandlers& handlersOnEntity = _registeredHandlers[entityID]; CallbackList& handlersForEvent = handlersOnEntity[eventName]; // ScriptValue 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. } } } // Unregister all event handlers for the specified entityID (i.e. the entity is being removed) void ScriptManager::removeAllEventHandlers(const EntityItemID& entityID) { if (QThread::currentThread() != thread()) { #ifdef THREAD_DEBUGGING qCDebug(scriptengine) << "*** WARNING *** ScriptManager::removeAllEventHandlers() called on wrong thread [" << QThread::currentThread() << ", correct thread is " << thread() << " ], ignoring " "entityID:" << entityID; #endif return; } if (_registeredHandlers.contains(entityID)) { _registeredHandlers.remove(entityID); } } // Register the handler. void ScriptManager::addEventHandler(const EntityItemID& entityID, const QString& eventName, const ScriptValue& handler) { if (QThread::currentThread() != thread()) { #ifdef THREAD_DEBUGGING qCDebug(scriptengine) << "*** WARNING *** ScriptManager::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(const ScriptValue&, handler)); return; } #ifdef THREAD_DEBUGGING qCDebug(scriptengine) << "ScriptManager::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... emit attachDefaultEventHandlers(); } 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(). } bool ScriptManager::isStopped() const { if (_context == NETWORKLESS_TEST_SCRIPT) { return false; } QSharedPointer<ScriptEngines> scriptEngines(_scriptEngines); return !scriptEngines || scriptEngines->isStopped(); } void ScriptManager::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); } if (QThread::currentThread() != _assetScriptingInterface->thread()) { _assetScriptingInterface->moveToThread(QThread::currentThread()); } auto filenameParts = _fileNameString.split("/"); auto name = filenameParts.size() > 0 ? filenameParts[filenameParts.size() - 1] : "unknown"; PROFILE_SET_THREAD_NAME("Script: " + name); if (isStopped()) { qCCritical(scriptengine) << "ScriptManager is stopped or ScriptEngines is not available, refusing to run script"; return; // bail early - avoid setting state in init(), as evaluate() will bail too } scriptInfoMessage("Script Engine starting:" + getFilename(), getFilename(), -1); if (!_isInitialized) { init(); } _isRunning = true; emit runningStateChanged(); { PROFILE_RANGE(script, _fileNameString); _returnValue = _engine->evaluate(_scriptContents, _fileNameString); if (_engine->hasUncaughtException() && _abortOnUncaughtException) { qCWarning(scriptengine) << "Engine has uncaught exception, stopping"; stop(); // V8TODO: Is clearing needed here? //_engine->clearExceptions(); } } #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; _lastUpdate = usecTimestampNow(); std::chrono::microseconds totalUpdates(0); qCDebug(scriptengine) << "Waiting for finish"; // TODO: Integrate this with signals/slots instead of reimplementing throttling for ScriptManager while (!_isFinished) { //qCDebug(scriptengine) << "In script event loop"; 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;