// // ScriptManager.h // libraries/script-engine/src // // Created by Brad Hefta-Gaub on 12/14/13. // Copyright 2013 High Fidelity, Inc. // Copyright 2020 Vircadia contributors. // Copyright 2022-2023 Overte e.V. // // Distributed under the Apache License, Version 2.0. // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // SPDX-License-Identifier: Apache-2.0 // #ifndef hifi_ScriptManager_h #define hifi_ScriptManager_h #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "EntityItemID.h" #include "EntitiesScriptEngineProvider.h" #include "EntityScriptUtils.h" #include #include #include "AssetScriptingInterface.h" #include "ConsoleScriptingInterface.h" #include "Mat4.h" #include "PointerEvent.h" #include "Quat.h" #include "ScriptUUID.h" #include "ScriptValue.h" #include "ScriptException.h" #include "Vec3.h" static const QString NO_SCRIPT(""); static const int SCRIPT_FPS = 60; static const int DEFAULT_MAX_ENTITY_PPS = 9000; static const int DEFAULT_ENTITY_PPS_PER_SCRIPT = 900; class ScriptEngine; class ScriptEngines; class ScriptManager; class ScriptManagerScriptingInterface; using ScriptEnginePointer = std::shared_ptr; using ScriptManagerPointer = std::shared_ptr; using ScriptManagerScriptingInterfacePointer = std::shared_ptr; using ScriptValueList = QList; Q_DECLARE_METATYPE(ScriptManagerPointer) const int QTREGISTER_QTimerStar = qRegisterMetaType(); /** * @brief Callback data for addEventHandler * */ class CallbackData { public: /** * @brief Function to call * */ ScriptValue function; /** * @brief Entity ID * */ EntityItemID definingEntityIdentifier; /** * @brief Sandbox URL for the script * */ QUrl definingSandboxURL; }; /** * @brief DeferredLoadEntity * @deprecated This appars unused * */ class DeferredLoadEntity { public: EntityItemID entityID; QString entityScript; //bool forceRedownload; }; /** * @brief Entity with available script contents * */ struct EntityScriptContentAvailable { /** * @brief Entity ID * */ EntityItemID entityID; /** * @brief URL to the script, or the actual script if embedded in the URL field * */ QString scriptOrURL; /** * @brief Contents of the script * */ QString contents; /** * @brief Whether scriptOrURL contains an URL * */ bool isURL; /** * @brief Whether the request has been successful * */ bool success; /** * @brief Status as text * */ QString status; }; typedef std::unordered_map EntityScriptContentAvailableMap; typedef QList CallbackList; typedef QHash RegisteredEventHandlers; /** * @brief Details about an entity script * */ class EntityScriptDetails { public: /** * @brief Current status * */ EntityScriptStatus status { EntityScriptStatus::PENDING }; /** * @brief Error information * * If status indicates an error, this contains a human-readable string giving more information about the error. * */ QString errorInfo { "" }; /** * @brief The source code of the script * */ QString scriptText { "" }; /** * @brief The return value of the script * */ ScriptValue scriptObject{ ScriptValue() }; /** * @brief Last modified time of the underlying script file * * This is used to determine if the script needs reloading when it changes on disk. */ int64_t lastModified { 0 }; /** * @brief URL under which the script is allowed to have access * * The script is allowed access below this URL (eg, sub-directories), but * not to the parent context. */ QUrl definingSandboxURL { QUrl("about:EntityScript") }; }; // declare a static script initializers #define STATIC_SCRIPT_TYPES_INITIALIZER(init) \ static ScriptManager::StaticTypesInitializerNode static_script_types_initializer_(init); #define STATIC_SCRIPT_INITIALIZER(init) \ static ScriptManager::StaticInitializerNode static_script_initializer_(init); /** * @brief Manages a single scripting engine * * This class manages and sets up a single scripting engine to make it execute scripts. * * It passes the objects needed to expose the public API, provides console access and error * reporting and event management. * * This manipulates a single underlying instance of ScriptEngine. * * Part of this class' functionality exists only to provide helper functions to the scripts that are * run by the scripting engine, and shouldn't be considered part of the C++ API. Those are the functions * in the "Script support methods", "Module support methods", "Entity Script methods", and "Scripting signals" sections. * * The script-facing interface is in ScriptManagerScriptingInterface and documented in JSDoc * as the Script class. * * The ScriptManager provides the following functionality to scripts: * * * A math library: Quat, Vec3, Mat4 * * UUID generation: Uuid * * Filesystem access: File * * Console access: console, print * * Resource access: Resource, Asset, Resources, ExternalPaths * * Scripting system management: Script * * Module loading: require * * Web access: XMLHttpRequest, WebSocket * * Other miscellaneous functionality. * * Example: * * @code {.cpp} * #include "ScriptManager.h" * * // Scripts only stop running when Script.stop() is called. * // In the normal environment this isn't needed, but for things like unit tests we need * // to use it to make the ScriptManager return from run(). * * QString scriptSource = "print(\"Hello, world!\"); Script.stop(true);"; * QString scriptFilename = "test.js"; * * ScriptManagerPointer sm = newScriptManager(ScriptManager::NETWORKLESS_TEST_SCRIPT, scriptSource, scriptFilename); * connect(sm.get(), &ScriptManager::printedMessage, [](const QString& message, const QString& engineName){ * qCDebug(scriptengine) << "Printed message from engine" << engineName << ": " << message; * }); * * qInfo() << "Running script!"; * sm->run(); * qInfo() << "Done!" * @endcode * * @note * Technically, the ScriptManager isn't generic enough -- it implements things that imitate * Node.js for examine in the module loading code, which makes it JS specific. This code * should probably be moved into the JS ScriptEngine class instead. * * The EntityScript functionality might also benefit from being split off into a separate * class, for better organization. * * Some more functionality can be shifted to ScriptManagerScriptingInterface, since * it only provides services to scripts and isn't called from C++. */ class ScriptManager : public QObject, public EntitiesScriptEngineProvider, public std::enable_shared_from_this { Q_OBJECT Q_PROPERTY(QString context READ getContext) Q_PROPERTY(QString type READ getTypeAsString) Q_PROPERTY(QString fileName MEMBER _fileNameString CONSTANT) public: static const QString SCRIPT_EXCEPTION_FORMAT; static const QString SCRIPT_BACKTRACE_SEP; /** * @brief Context of the script * */ enum Context { /** * @brief Client script. * Allowed to access local HTML files on UI created from C++ calls. * */ CLIENT_SCRIPT, /** * @brief Entity client script * */ ENTITY_CLIENT_SCRIPT, /** * @brief Entity server script * */ ENTITY_SERVER_SCRIPT, /** * @brief Agent script * */ AGENT_SCRIPT, /** * @brief Network-less test system context. * This is used for the QTest self-tests, and minimizes the API that is made available to * the running script. It removes the need for network access, which makes for much faster * test execution. * * * @warning This is a development-targeted bit of functionality. * * @warning This is going to break functionality like loadURL and require */ NETWORKLESS_TEST_SCRIPT }; /** * @brief Type of the script * */ enum Type { /** * @brief Client * */ CLIENT, /** * @brief Entity client * Receives the update event. */ ENTITY_CLIENT, /** * @brief Entity server * Receives the update event * */ ENTITY_SERVER, /** * @brief Agent script * */ AGENT, /** * @brief Avatar script * */ AVATAR, /** * @brief Test system script * * This is used for the QTest self-tests, and minimizes the API that is made available to * the running script. It removes the need for network access, which makes for much faster * test execution. * * @warning This is a development-targeted bit of functionality. */ NETWORKLESS_TEST }; Q_ENUM(Type); static int processLevelMaxRetries; ScriptManager(Context context, const QString& scriptContents = NO_SCRIPT, const QString& fileNameString = QString("about:ScriptEngine")); ~ScriptManager(); // static initialization support typedef void (*ScriptManagerInitializer)(ScriptManager*); class StaticInitializerNode { public: ScriptManagerInitializer init; StaticInitializerNode* prev; inline StaticInitializerNode(ScriptManagerInitializer&& pInit) : init(std::move(pInit)),prev(nullptr) { registerNewStaticInitializer(this); } }; static void registerNewStaticInitializer(StaticInitializerNode* dest); class StaticTypesInitializerNode { public: ScriptManagerInitializer init; StaticTypesInitializerNode* prev; inline StaticTypesInitializerNode(ScriptManagerInitializer&& pInit) : init(std::move(pInit)),prev(nullptr) { registerNewStaticTypesInitializer(this); } }; static void registerNewStaticTypesInitializer(StaticTypesInitializerNode* dest); /** * @brief Run the script in a dedicated thread * * This will have the side effect of evaluating the current script contents and calling run(). * Callers will likely want to register the script with external services before calling this. * * This function will return immediately, and work will continue on the newly created thread. * * @note Can't be called twice. * @note The underlying thread is not accessible. */ void runInThread(); /** * @brief Run the script in the caller's thread, exit when Script.stop() is called. * * Most scripts never stop running, so this function will never return for them. */ void run(); /** * @brief Get the filename of the running script, without the path. * * @return QString Filename */ QString getFilename() const; /** * @brief Underlying scripting engine * * @return ScriptEnginePointer Scripting engine */ inline ScriptEnginePointer engine() { return _engine; } QList getListOfEntityScriptIDs(); bool isStopped() const; /** * @name Script support functions * * These functions exist to support the scripting API */ ///@{ /** * @brief Stops and unloads the current script. * * @note This is part of the public scripting API for Agent scripts and local scripts, but not for EntityScripts * @param marshal Deprecated */ Q_INVOKABLE void stop(bool marshal = false); ///@} /** * @brief Stop any evaluating scripts and wait for the scripting thread to finish. * * @param shutdown True if we are currently shutting down. Setting this to true will allow * processing events emitted during the script's shutdown, such as scripts saving settings. * * @note This function has an internal timeout, and will forcefully abort the script if it * takes too long. */ void waitTillDoneRunning(bool shutdown = false); /** * @brief Load a script from a given URL * * If the script engine is not already running, this will download the URL and start the process of seting it up * to run. * * * @param scriptURL URL where to load the script from. Can be http, https, atp, or file protocol. The file extension * has to pass hasValidScriptSuffix(). * @param reload Load the script again even if it's in the cache. * * @note For file:// URLs, only URLs under the default scripts location are allowed. * @see PathUtils::defaultScriptsLocation */ void loadURL(const QUrl& scriptURL, bool reload); /** * @brief Determines whether a script filename has the right suffix * * * @param scriptFileName * @return true When the script has the right file extension (eg, .js) * @return false Otherwise */ bool hasValidScriptSuffix(const QString& scriptFileName); /** * @name Script support methods * * These functions exist to support the scripting API */ ///@{ /** * @brief Gets the context that the script is running in: Interface/avatar, client entity, server entity, or assignment client. * * @note This is part of the public JS API * @return QString */ Q_INVOKABLE QString getContext() const; /** * @brief Checks whether the script is running as an Interface or avatar script. * @note This is part of the public JS API * @return bool */ Q_INVOKABLE bool isClientScript() const { return _context == CLIENT_SCRIPT; } /** * @brief Checks whether the application was compiled as a debug build. * @note This is part of the public JS API * @return bool */ Q_INVOKABLE bool isDebugMode() const; /** * @brief Checks whether the script is running as a client entity script. * * @return bool */ Q_INVOKABLE bool isEntityClientScript() const { return _context == ENTITY_CLIENT_SCRIPT; } /** * @brief Checks whether the script is running as a server entity script. * * @return bool */ Q_INVOKABLE bool isEntityServerScript() const { return _context == ENTITY_SERVER_SCRIPT; } /** * @brief Checks whether the script is running as an assignment client script. * * @return bool */ Q_INVOKABLE bool isAgentScript() const { return _context == AGENT_SCRIPT; } /** * @brief Registers a global object by name. * * @param valueName * @param value */ Q_INVOKABLE void registerValue(const QString& valueName, ScriptValue value); //////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // NOTE - these are intended to be public interfaces available to scripts /** * @brief Format an exception and return it as a string * * @param exception Exception object, containing the exception information. * @param includeExtendedDetails Include additional troubleshooting information from the "detail" property, if there's one * @return QString A multi-line string containing the formatted exception */ Q_INVOKABLE QString formatException(const ScriptValue& exception, bool includeExtendedDetails); /** * @brief Adds a function to the list of functions called when a particular event occurs on a particular entity. * * @param entityID Entity ID * @param eventName Name of the event * @param handler Event handler * * @note The same handler can be added multiple times. */ Q_INVOKABLE void addEventHandler(const EntityItemID& entityID, const QString& eventName, const ScriptValue& handler); /** * @brief Removes a function from the list of functions called when an entity event occurs on a particular entity. * * @param entityID Entity ID * @param eventName Name if the event * @param handler Event handler */ Q_INVOKABLE void removeEventHandler(const EntityItemID& entityID, const QString& eventName, const ScriptValue& handler); /** * @brief Starts running another script in Interface, if it isn't already running. The script is not automatically loaded next * time Interface starts. * * The script is loaded as a stand-alone script. * * @param loadfile File to load * @warning In practice this seems equivalent to calling loadScript or reloadScript. It reacts to _isReloading in an odd-looking manner. * Is this function superfluous? */ Q_INVOKABLE void load(const QString& loadfile); /** * @brief Includes JavaScript from other files in the current script. * * 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. * * @param includeFiles List of files to include * @param callback Callback to call when the files have finished loading. */ Q_INVOKABLE void include(const QStringList& includeFiles, const ScriptValue& callback = ScriptValue()); /** * @brief Includes JavaScript from another file in the current script. * * 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. * * @param includeFile * @param callback */ Q_INVOKABLE void include(const QString& includeFile, const ScriptValue& callback = ScriptValue()); ///@} //////////////////////////////////////////////////////////////////////////////////////////////////////////////////// /** * @name Module support methods * */ ///@{ /** * @brief Provides access to methods or objects provided in an external JavaScript or JSON file. * * Implements CommonJS/Node.js like require/module support * * @param moduleId Module to load * @return ScriptValue */ Q_INVOKABLE ScriptValue require(const QString& moduleId); /** * @brief Resets the module cache * * @deprecated * @param deleteScriptCache */ Q_INVOKABLE void resetModuleCache(bool deleteScriptCache = false); /** * @brief The current parent module from the running JS script * * * @return ScriptValue Module. May be null or empty. */ ScriptValue currentModule(); /** * @brief Replaces or adds "module" to "parent.children[]" array * * This is an internal use function used as a part of the 'require' implementation. * * @param module Module to register * @param parent Parent * @return true Registration successful * @return false Registration failed, if the parent isn't a valid module */ bool registerModuleWithParent(const ScriptValue& module, const ScriptValue& parent); /** * @brief creates a new JS "module" Object with default metadata properties * * This imitates what is provided by https://nodejs.org/api/modules.html * * @param modulePath File path to the module * @param parent Parent module * @return ScriptValue Created module object */ ScriptValue newModule(const QString& modulePath, const ScriptValue& parent = ScriptValue()); /** * @brief Synchronously fetch a module's source code * * The return value is a map containing the following fields: * * * "status" -- A string indicating the status of the operation * * "success" -- A true or false value indicating success or failure * * "url" -- The URL of the source. May not be present. * * "contents" -- The contents of the source. May not be present. * * @param modulePath Path to the module's source code * @param forceDownload Force a redownload even if the source is already in the cache * @return QVariantMap The result of the operation */ QVariantMap fetchModuleSource(const QString& modulePath, const bool forceDownload = false); /** * @brief Evaluate a pending module object using the fetched source code * * @param module Module object * @param sourceCode Source code to evaluate * @return ScriptValue The result of evaluating the source code */ ScriptValue instantiateModule(const ScriptValue& module, const QString& sourceCode); /** * @brief Evaluate a program in the underlying scripting engine * * This simply calls to ScriptEngine::evaluate() * * @param program Source of the program * @param fileName Filename it was obtained from * @return ScriptValue Result of the evaluation */ ScriptValue evaluate(const QString& program, const QString& fileName = QString()); /** * @brief Calls a function repeatedly, at a set interval. * * @note This is a JS API service. * * @param function Function to call * @param intervalMS Interval at which to call the function, in ms * @return QTimer* A pointer to the timer */ Q_INVOKABLE QTimer* setInterval(const ScriptValue& function, int intervalMS); /** * @brief Calls a function once, after a delay. * * @note This is a JS API service. * * @param function Function to call * @param timeoutMS How long to wait before calling the function, in ms * @return QTimer* A pointer to the timer */ Q_INVOKABLE QTimer* setTimeout(const ScriptValue& function, int timeoutMS); /** * @brief Stops an interval timer * * @param timer Timer to stop */ Q_INVOKABLE void clearInterval(QTimer* timer) { stopTimer(timer); } /** * @brief Stops an interval timer * * Overloaded version is needed in case the timer has expired * * @param timer Timer to stop */ Q_INVOKABLE void clearInterval(QVariantMap timer) { ; } /** * @brief Stops a timeout timer * * @param timer Timer to stop */ Q_INVOKABLE void clearTimeout(QTimer* timer) { stopTimer(timer); } /** * @brief Stops a timeout timer * Overloaded version is needed in case the timer has expired * * @param timer Timer to stop */ Q_INVOKABLE void clearTimeout(QVariantMap timer) { ; } /** * @brief Prints a message to the program log * * @param message */ Q_INVOKABLE void print(const QString& message); /** * @brief Resolves a relative path to an absolute path. The relative path is relative to the script's location. * * @param path * @return QUrl */ Q_INVOKABLE QUrl resolvePath(const QString& path) const; /** * @brief Gets the path to the resources directory for QML files. * * @return QUrl */ Q_INVOKABLE QUrl resourcesPath() const; /** * @brief Starts timing a section of code in order to send usage data about it to Overte. Shouldn't be used outside of the * standard scripts. * @param label */ Q_INVOKABLE void beginProfileRange(const QString& label) const; /** * @brief Finishes timing a section of code in order to send usage data about it to Overte. Shouldn't be used outside of * the standard scripts * @param label */ Q_INVOKABLE void endProfileRange(const QString& label) const; ///@} //////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // Entity Script Related methods /** * @name Entity Script methods * */ ///@{ /** * @brief Checks whether an entity has an entity script running. * * @param entityID * @return bool */ Q_INVOKABLE bool isEntityScriptRunning(const EntityItemID& entityID) { QReadLocker locker { &_entityScriptsLock }; auto it = _entityScripts.constFind(entityID); return it != _entityScripts.constEnd() && it->status == EntityScriptStatus::RUNNING; } /** * @brief Clone the details of an entity script * * @param entityID Entity ID * @return QVariant Copy of the details */ QVariant cloneEntityScriptDetails(const EntityItemID& entityID); /** * @brief Get the details of a local entity script * * Same as cloneEntityScriptDetails, only as a QFuture. * * @param entityID Entity ID * @return QFuture */ QFuture getLocalEntityScriptDetails(const EntityItemID& entityID) override; /** * @brief Manually runs the JavaScript garbage collector which reclaims memory by disposing of objects that are no longer reachable * */ Q_INVOKABLE void requestGarbageCollection(); /** * @brief Prints out current backtrace to the log. * * @param title */ Q_INVOKABLE void logBacktrace(const QString &title); /** * @brief Load an entity script * * @param entityID * @param entityScript * @param forceRedownload */ Q_INVOKABLE void loadEntityScript(const EntityItemID& entityID, const QString& entityScript, bool forceRedownload); /** * @brief Unload an entity script * * @param entityID * @param shouldRemoveFromMap */ Q_INVOKABLE void unloadEntityScript(const EntityItemID& entityID, bool shouldRemoveFromMap = false); // will call unload method /** * @brief Unload all entity scripts * * @param blockingCall */ Q_INVOKABLE void unloadAllEntityScripts(bool blockingCall = false); /** * @brief Call a method on an entity script * * @param entityID * @param methodName * @param params * @param remoteCallerID */ Q_INVOKABLE void callEntityScriptMethod(const EntityItemID& entityID, const QString& methodName, const QStringList& params = QStringList(), const QUuid& remoteCallerID = QUuid()) override; /** * @brief Call a method on an entity script * * @param entityID * @param methodName * @param event */ Q_INVOKABLE void callEntityScriptMethod(const EntityItemID& entityID, const QString& methodName, const PointerEvent& event); /** * @brief Call a method on an entity script * * @param entityID * @param methodName * @param otherID * @param collision */ Q_INVOKABLE void callEntityScriptMethod(const EntityItemID& entityID, const QString& methodName, const EntityItemID& otherID, const Collision& collision); /** * @brief Set the script type * * @param type Type of this script */ void setType(Type type) { _type = type; }; /** * @brief Returns the script type * * @return Type of this script */ Type getType() { return _type; }; /** * @brief Returns the type of the script as a string * * @return QString A string describing the script's type */ QString getTypeAsString() const; /** * @brief Whether the script has finished running * * The finished status is set by stop() * @return true The script has finished * @return false The script is running */ bool isFinished() const { return _isFinished; } // used by Application and ScriptWidget /** * @brief Whether the script is running * * @return true The script is running * @return false The script is not running */ bool isRunning() const { return _isRunning; } // used by ScriptWidget // this is used by code in ScriptEngines.cpp during the "reload all" operation /** * @brief Whether this ScriptManager is stopping. Once this is true, it stays true. * * @return true We are stopping * @return false We are not stopping */ bool isStopping() const { return _isStopping; } /** * @brief Disconnect all signals, except essential ones * * This disconnects all signals, except the destroyed() and finished() handlers that * are needed for cleanup. */ void disconnectNonEssentialSignals(); ///@} //////////////////////////////////////////////////////////////////////////////////////////////////////////////////// /** * @brief Set whether this script was user-loaded * * This is used by Application to track if a script is user loaded or not. * @note Consider finding a solution inside of Application so that the ScriptManager class is not polluted by this notion * * @param isUserLoaded Script is user-loaded. */ void setUserLoaded(bool isUserLoaded) { _isUserLoaded = isUserLoaded; } /** * @brief Whether the script was user-loaded. * * This is used by Application to track if a script is user loaded or not. * @note Consider finding a solution inside of Application so that the ScriptManager class is not polluted by this notion * * @return true * @return false */ bool isUserLoaded() const { return _isUserLoaded; } /** * @brief Set whether to quit when finished * * This is used by ScriptEngines * * @param quitWhenFinished */ void setQuitWhenFinished(const bool quitWhenFinished) { _quitWhenFinished = quitWhenFinished; } /** * @brief Whether to quit when finished * * This is read by ScriptEngines * * @return true * @return false */ bool isQuitWhenFinished() const { return _quitWhenFinished; } /** * @brief Set a function that determines whether to emit update events * * Function func will be called from run() to determine whether update() will be issued. * The update() event will be called if func() returns true. * * @param func Function that determines whether update() events will be issued. */ void setEmitScriptUpdatesFunction(std::function func) { _emitScriptUpdates = func; } /** * @brief Logs a script error message and emits an errorMessage event * * Emits errorMessage() * * @param message Message to send to the log */ void scriptErrorMessage(const QString& message); /** * @brief Logs a script warning message and emits an warningMessage event * * Emits warningMessage() * * @param message Message to send to the log */ void scriptWarningMessage(const QString& message); /** * @brief Logs a script info message and emits an infoMessage event * * Emits infoMessage() * * @param message Message to send to the log */ void scriptInfoMessage(const QString& message); /** * @brief Logs a script printed message and emits an printedMessage event * * These are messages scripts provide by calling the print function. * Emits printedMessage() * * @param message Message to send to the log */ void scriptPrintedMessage(const QString& message); /** * @brief Clears the debug log window * * This only emits clearDebugWindow() * */ void clearDebugLogWindow(); /** * @brief Get the number of running entity scripts * * @return int Number of scripts with the status EntityScriptStatus::RUNNING */ int getNumRunningEntityScripts() const; /** * @brief Retrieves the details about an entity script * * @param entityID Entity ID * @param details Returned details * @return true If the entity ID was found * @return false If the entity ID wasn't found. details will be unmodified. */ bool getEntityScriptDetails(const EntityItemID& entityID, EntityScriptDetails &details) const; /** * @brief Whether there are script details for a given entity ID * * @param entityID Entity ID * @return true There is an entity script for this entity * @return false There's no entity script */ bool hasEntityScriptDetails(const EntityItemID& entityID) const; /** * @brief Set a shared pointer to the ScriptEngines class * * This is used to ask ScriptEngines whether the system is being stopped. * Setting this is optional. * * isStopped() is implemented by asking ScriptEngines. * * @param scriptEngines ScriptEngines class */ void setScriptEngines(QSharedPointer& scriptEngines) { _scriptEngines = scriptEngines; } /** * @brief Call all the registered event handlers on an entity for the specified name. * * Look up the handler associated with eventName and entityID. If found, evalute the argGenerator thunk and call the handler with those args * * @param entityID * @param eventName * @param eventHanderArgs */ void forwardHandlerCall(const EntityItemID& entityID, const QString& eventName, const ScriptValueList& eventHanderArgs); /** * @brief Remove all event handlers for the specified entityID (i.e. the entity is being removed) * * @param entityID Entity ID */ void removeAllEventHandlers(const EntityItemID& entityID); /** * @brief Return value of the script that finished running * * This should only be used after the script terminates. * * @return ScriptValue */ ScriptValue getReturnValue() const { return _returnValue; } /** * @brief Gets the URL for an asset in an external resource bucket. * * @param bucket * @param path * @return Q_INVOKABLE */ Q_INVOKABLE QString getExternalPath(ExternalResource::Bucket bucket, const QString& path); /** * @brief Get the uncaught exception from the underlying script engine * * @return std::shared_ptr Exception */ std::shared_ptr getUncaughtException() const; /** * @brief Whether this engine will abort on an uncaught exception * * @warning This probably should be refactored into a more comprehensive per-script flags system * @return true * @return false */ bool getAbortOnUncaughtException() const { return _abortOnUncaughtException; } /** * @brief Whether to abort on an uncaught exception * * @warning This probably should be refactored into a more comprehensive per-script flags system * @param value */ void setAbortOnUncaughtException(bool value) { _abortOnUncaughtException = value; } public slots: /** * @brief Script.updateMemoryCost * * Sends a memory cost update to the underlying scripting engine * * @param deltaSize Difference in memory usage * @deprecated Deprecated */ void updateMemoryCost(const qint64 &deltaSize); signals: /** * @name Scripting events * */ ///@{ /** * @brief Script.scriptLoaded * @deprecated * @param scriptFilename */ void scriptLoaded(const QString& scriptFilename); /** * @brief Script.errorLoadingScript * @deprecated * @param scriptFilename */ void errorLoadingScript(const QString& scriptFilename); /** * @brief Triggered frequently at a system-determined interval. * * @param deltaTime */ void update(float deltaTime); /** * @brief Triggered when the script is stopping. * */ void scriptEnding(); /** * @brief Script.finished * * @param fileNameString */ void finished(const QString& fileNameString, ScriptManagerPointer); /** * @brief Triggered when the script prints a message to the program log * * @param message * @param scriptName */ void printedMessage(const QString& message, const QString& scriptName); /** * @brief Triggered when the script generates an error * * @param message * @param scriptName */ void errorMessage(const QString& message, const QString& scriptName); /** * @brief Triggered when the script generates a warning * * @param message * @param scriptName */ void warningMessage(const QString& message, const QString& scriptName); /** * @brief Triggered when the script generates an information message * * @param message * @param scriptName */ void infoMessage(const QString& message, const QString& scriptName); /** * @brief Triggered when the running state of the script changes, e.g., from running to stopping. * */ void runningStateChanged(); /** * @brief Script.clearDebugWindow * @deprecated */ void clearDebugWindow(); /** * @brief Script.loadScript * @deprecated * @param scriptName * @param isUserLoaded */ void loadScript(const QString& scriptName, bool isUserLoaded); /** * @brief Script.reloadScript * * @param scriptName * @param isUserLoaded */ void reloadScript(const QString& scriptName, bool isUserLoaded); /** * @brief Triggered when the script has stopped. * */ void doneRunning(); /** * @brief Emitted when an entity script is added or removed, or when the status of an entity * script is updated (goes from RUNNING to ERROR_RUNNING_SCRIPT, for example) * */ void entityScriptDetailsUpdated(); /** * @brief Emitted when an entity script has finished running preload * * @param entityID */ void entityScriptPreloadFinished(const EntityItemID& entityID); /** * @brief Triggered when a script generates an unhandled exception. * * @param exception */ void unhandledException(std::shared_ptr exception); ///@} /** * @brief Triggered once before the first call to Script.addEventHandler happens on this ScriptManager * connections assumed to use Qt::DirectConnection * * @note not for use by scripts */ void attachDefaultEventHandlers(); /** * @brief Triggered repeatedly in the scripting loop to ensure entity edit messages get processed properly * connections assumed to use Qt::DirectConnection * * @note Not for use by scripts * @param wait */ void releaseEntityPacketSenderMessages(bool wait); protected: // Is called by the constructor, bceause all types need to be registered before method discovery with ScriptObjectV8Proxy::investigate() void initMetaTypes(); /** * @brief Initializes the underlying scripting engine * * This sets up the scripting engine with the default APIs */ void init(); /** * @brief executeOnScriptThread * * @deprecated * @param function * @param type */ Q_INVOKABLE void executeOnScriptThread(std::function function, const Qt::ConnectionType& type = Qt::QueuedConnection ); /** * @brief Script._requireResolve * * @note this is not meant to be called directly, but just to have QMetaObject take care of wiring it up in general; * then inside of init() we just have to do "Script.require.resolve = Script._requireResolve;" * * @deprecated * @param moduleId * @param relativeTo * @return QString */ Q_INVOKABLE QString _requireResolve(const QString& moduleId, const QString& relativeTo = QString()); /** * @brief Log an exception * * This both sends an exception to the log as an error message, and returns the formatted * text as a string. * * @param exception Exception * @return QString Exception formatted as a string */ QString logException(const ScriptValue& exception); void timerFired(); void stopAllTimers(); void stopAllTimersForEntityScript(const EntityItemID& entityID); void refreshFileScript(const EntityItemID& entityID); /** * @brief Updates the status of an entity script * * Emits entityScriptDetailsUpdated() * * @param entityID Entity ID * @param status Status * @param errorInfo Description of the error, if any */ void updateEntityScriptStatus(const EntityItemID& entityID, const EntityScriptStatus& status, const QString& errorInfo = QString()); /** * @brief Set the details for an entity script * * @param entityID Entity ID * @param details Details */ void setEntityScriptDetails(const EntityItemID& entityID, const EntityScriptDetails& details); /** * @brief Set the parent URL, used to resolve relative paths * * Relative paths are resolved respect of this URL * * @param parentURL Parent URL */ void setParentURL(const QString& parentURL) { _parentURL = parentURL; } /** * @brief Creates a timer with the specified interval * * @param function Function to call when the interval elapses * @param intervalMS Interval in milliseconds * @param isSingleShot Whether the timer happens continuously or a single time * @return QTimer* */ QTimer* setupTimerWithInterval(const ScriptValue& function, int intervalMS, bool isSingleShot); /** * @brief Stops a timer * * @param timer Timer to stop */ void stopTimer(QTimer* timer); QHash _registeredHandlers; /** * @brief Script.entityScriptContentAvailable * * @param entityID * @param scriptOrURL * @param contents * @param isURL * @param success * @param status */ Q_INVOKABLE void entityScriptContentAvailable(const EntityItemID& entityID, const QString& scriptOrURL, const QString& contents, bool isURL, bool success, const QString& status); EntityItemID currentEntityIdentifier; // Contains the defining entity script entity id during execution, if any. Empty for interface script execution. QUrl currentSandboxURL; // The toplevel url string for the entity script that loaded the code being executed, else empty. /** * @brief 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). * * @param entityID Entity ID, may be null * @param sandboxURL Sandbox URL * @param operation Operation to call */ void doWithEnvironment(const EntityItemID& entityID, const QUrl& sandboxURL, std::function operation); /** * @brief Execute operation in the appropriate context for (the possibly empty) entityID. * * This is equivalent to doWithEnvironment(), only with separate arguments for the function, this object and arguments. * * This is a convenience function, which performs: * * @code {.cpp} * auto operation = [&]() { * function.call(thisObject, args); * }; * doWithEnvironment(entityID, sandboxURL, operation); * @endcode * * @param entityID Entity ID, may be null * @param sandboxURL Sandbox URL * @param function Function to call * @param thisObject "this" object to use for the call * @param args Arguments */ void callWithEnvironment(const EntityItemID& entityID, const QUrl& sandboxURL, const ScriptValue& function, const ScriptValue& thisObject, const ScriptValueList& args); Context _context; Type _type; ScriptEnginePointer _engine; QString _scriptContents; QString _parentURL; std::atomic _isFinished { false }; std::atomic _isRunning { false }; std::atomic _isStopping { false }; bool _areMetaTypesInitialized { false }; bool _isInitialized { false }; QHash _timerFunctionMap; QSet _includedURLs; mutable QReadWriteLock _entityScriptsLock { QReadWriteLock::Recursive }; QHash _entityScripts; EntityScriptContentAvailableMap _contentAvailableQueue; ScriptValue _returnValue; bool _isThreaded { false }; qint64 _lastUpdate; QString _fileNameString; Quat _quatLibrary; Vec3 _vec3Library; Mat4 _mat4Library; ScriptUUID _uuidLibrary; ConsoleScriptingInterface _consoleScriptingInterface; std::atomic _isUserLoaded { false }; bool _isReloading { false }; std::atomic _quitWhenFinished; AssetScriptingInterface* _assetScriptingInterface; std::function _emitScriptUpdates{ []() { return true; } }; std::recursive_mutex _lock; std::chrono::microseconds _totalTimerExecution { 0 }; static const QString _SETTINGS_ENABLE_EXTENDED_EXCEPTIONS; Setting::Handle _enableExtendedJSExceptions { _SETTINGS_ENABLE_EXTENDED_EXCEPTIONS, true }; QWeakPointer _scriptEngines; // For debugging performance issues int _timerCallCounter{ 0 }; double _totalTimeInTimerEvents_s{ 0.0 }; ScriptManagerScriptingInterfacePointer _scriptingInterface; bool _abortOnUncaughtException{ false }; friend ScriptManagerPointer newScriptManager(Context context, const QString& scriptContents, const QString& fileNameString); friend class ScriptManagerScriptingInterface; }; /** * @brief Creates a new ScriptManager * * @param context Context in which scripts will run * @param scriptContents Contents of the script to run * @param fileNameString Filename for the script * @return ScriptManagerPointer */ ScriptManagerPointer newScriptManager(ScriptManager::Context context, const QString& scriptContents, const QString& fileNameString); /** * @brief Creates a new ScriptManager and adds it to ScriptEngines * * Same as newScriptManager, but it additionally registers the new * ScriptManager with ScriptEngines. * * @param context Context in which scripts will run * @param scriptContents Contents of the script * @param fileNameString Filename of the script * @return ScriptManagerPointer */ ScriptManagerPointer scriptManagerFactory(ScriptManager::Context context, const QString& scriptContents, const QString& fileNameString); #endif // hifi_ScriptManager_h