diff --git a/assignment-client/src/scripts/EntityScriptServer.cpp b/assignment-client/src/scripts/EntityScriptServer.cpp index b16e4561d64..2b23434195b 100644 --- a/assignment-client/src/scripts/EntityScriptServer.cpp +++ b/assignment-client/src/scripts/EntityScriptServer.cpp @@ -44,16 +44,8 @@ using Mutex = std::mutex; using Lock = std::lock_guard; -static std::mutex logBufferMutex; -static std::string logBuffer; - void messageHandler(QtMsgType type, const QMessageLogContext& context, const QString& message) { auto logMessage = LogHandler::getInstance().printMessage((LogMsgType) type, context, message); - - if (!logMessage.isEmpty()) { - Lock lock(logBufferMutex); - logBuffer.append(logMessage.toStdString() + '\n'); - } } int EntityScriptServer::_entitiesScriptEngineCount = 0; @@ -217,10 +209,10 @@ void EntityScriptServer::handleEntityServerScriptLogPacket(QSharedPointer(); + QJsonDocument document; + document.setArray(buffer); + QString data(document.toJson()); + std::string string = data.toStdString(); + auto cstring = string.c_str(); for (auto uuid : _logListeners) { auto node = nodeList->nodeWithUUID(uuid); if (node && node->getActiveSocket()) { auto packet = NLPacketList::create(PacketType::EntityServerScriptLog, QByteArray(), true, true); - packet->write(buffer.data(), buffer.size()); + packet->write(cstring, strlen(cstring)); nodeList->sendPacketList(std::move(packet), *node); } } } +void EntityScriptServer::addLogEntry(const QString& message, const QString& fileName, int lineNumber, const EntityItemID& entityID, ScriptMessage::Severity severity) { + ScriptMessage entry(message, fileName, lineNumber, entityID, ScriptMessage::ScriptType::TYPE_ENTITY_SCRIPT, severity); + Lock lock(_logBufferMutex); + _logBuffer.append(entry.toJson()); +} + void EntityScriptServer::handleEntityScriptCallMethodPacket(QSharedPointer receivedMessage, SharedNodePointer senderNode) { if (_entitiesScriptManager && _entityViewer.getTree() && !_shuttingDown) { @@ -469,6 +472,29 @@ void EntityScriptServer::resetEntitiesScriptEngine() { connect(newManager.get(), &ScriptManager::warningMessage, scriptEngines, &ScriptEngines::onWarningMessage); connect(newManager.get(), &ScriptManager::infoMessage, scriptEngines, &ScriptEngines::onInfoMessage); + // Make script engine messages available through ScriptDiscoveryService + connect(newManager.get(), &ScriptManager::infoEntityMessage, scriptEngines, &ScriptEngines::infoEntityMessage); + connect(newManager.get(), &ScriptManager::printedEntityMessage, scriptEngines, &ScriptEngines::printedEntityMessage); + connect(newManager.get(), &ScriptManager::errorEntityMessage, scriptEngines, &ScriptEngines::errorEntityMessage); + connect(newManager.get(), &ScriptManager::warningEntityMessage, scriptEngines, &ScriptEngines::warningEntityMessage); + + connect(newManager.get(), &ScriptManager::infoEntityMessage, + [this](const QString& message, const QString& fileName, int lineNumber, const EntityItemID& entityID) { + addLogEntry(message, fileName, lineNumber, entityID, ScriptMessage::Severity::SEVERITY_INFO); + }); + connect(newManager.get(), &ScriptManager::printedEntityMessage, + [this](const QString& message, const QString& fileName, int lineNumber, const EntityItemID& entityID) { + addLogEntry(message, fileName, lineNumber, entityID, ScriptMessage::Severity::SEVERITY_PRINT); + }); + connect(newManager.get(), &ScriptManager::errorEntityMessage, + [this](const QString& message, const QString& fileName, int lineNumber, const EntityItemID& entityID) { + addLogEntry(message, fileName, lineNumber, entityID, ScriptMessage::Severity::SEVERITY_ERROR); + }); + connect(newManager.get(), &ScriptManager::warningEntityMessage, + [this](const QString& message, const QString& fileName, int lineNumber, const EntityItemID& entityID) { + addLogEntry(message, fileName, lineNumber, entityID, ScriptMessage::Severity::SEVERITY_WARNING); + }); + connect(newManager.get(), &ScriptManager::update, this, [this] { _entityViewer.queryOctree(); _entityViewer.getTree()->preUpdate(); diff --git a/assignment-client/src/scripts/EntityScriptServer.h b/assignment-client/src/scripts/EntityScriptServer.h index 3f15f5733c5..2fba985ef42 100644 --- a/assignment-client/src/scripts/EntityScriptServer.h +++ b/assignment-client/src/scripts/EntityScriptServer.h @@ -27,6 +27,8 @@ #include #include #include +#include +#include #include "../entities/EntityTreeHeadlessViewer.h" @@ -55,10 +57,32 @@ private slots: void handleSettings(); void updateEntityPPS(); + /** + * @brief Handles log subscribe/unsubscribe requests + * + * Clients can subscribe to logs by sending a script log packet. Entity Script Server keeps list of subscribers + * and sends them logs in JSON format. + */ + void handleEntityServerScriptLogPacket(QSharedPointer message, SharedNodePointer senderNode); + /** + * @brief Transmit logs + * + * This is called periodically through a timer to transmit logs from scripts. + */ + void pushLogs(); + /** + * @brief Adds log entry to the transmit buffer + * + * This is connected to entity script log events in the script manager and adds script log message to the buffer + * containing messages that will be sent to subscribed clients. + */ + + void addLogEntry(const QString& message, const QString& fileName, int lineNumber, const EntityItemID& entityID, ScriptMessage::Severity severity); + void handleEntityScriptCallMethodPacket(QSharedPointer message, SharedNodePointer senderNode); @@ -85,6 +109,9 @@ private slots: EntityEditPacketSender _entityEditSender; EntityTreeHeadlessViewer _entityViewer; + QJsonArray _logBuffer; + std::mutex _logBufferMutex; + int _maxEntityPPS { DEFAULT_MAX_ENTITY_PPS }; int _entityPPSPerScript { DEFAULT_ENTITY_PPS_PER_SCRIPT }; diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index aa9d0a2f2f6..137457c7222 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -902,7 +902,12 @@ bool setupEssentials(const QCommandLineParser& parser, bool runningMarkerExisted DependencyManager::set(); DependencyManager::set(); DependencyManager::set(); + DependencyManager::set(); + auto scriptEngines = DependencyManager::get(); + auto entityScriptServerLog = DependencyManager::get(); + QObject::connect(scriptEngines.data(), &ScriptEngines::requestingEntityScriptServerLog, entityScriptServerLog.data(), &EntityScriptServerLogClient::requestMessagesForScriptEngines); + DependencyManager::set(); DependencyManager::set(nullptr, qApp->getOcteeSceneStats()); DependencyManager::set(); diff --git a/libraries/entities-renderer/src/EntityTreeRenderer.cpp b/libraries/entities-renderer/src/EntityTreeRenderer.cpp index 4861bc6ecba..cdc31a3765f 100644 --- a/libraries/entities-renderer/src/EntityTreeRenderer.cpp +++ b/libraries/entities-renderer/src/EntityTreeRenderer.cpp @@ -232,6 +232,14 @@ void EntityTreeRenderer::resetPersistentEntitiesScriptEngine() { _persistentEntitiesScriptManager = scriptManagerFactory(ScriptManager::ENTITY_CLIENT_SCRIPT, NO_SCRIPT, QString("about:Entities %1").arg(++_entitiesScriptEngineCount)); DependencyManager::get()->runScriptInitializers(_persistentEntitiesScriptManager); + + // Make script engine messages available through ScriptDiscoveryService + auto scriptEngines = DependencyManager::get().data(); + connect(_persistentEntitiesScriptManager.get(), &ScriptManager::infoEntityMessage, scriptEngines, &ScriptEngines::infoEntityMessage); + connect(_persistentEntitiesScriptManager.get(), &ScriptManager::printedEntityMessage, scriptEngines, &ScriptEngines::printedEntityMessage); + connect(_persistentEntitiesScriptManager.get(), &ScriptManager::errorEntityMessage, scriptEngines, &ScriptEngines::errorEntityMessage); + connect(_persistentEntitiesScriptManager.get(), &ScriptManager::warningEntityMessage, scriptEngines, &ScriptEngines::warningEntityMessage); + _persistentEntitiesScriptManager->runInThread(); std::shared_ptr entitiesScriptEngineProvider = _persistentEntitiesScriptManager; auto entityScriptingInterface = DependencyManager::get(); @@ -255,6 +263,14 @@ void EntityTreeRenderer::resetNonPersistentEntitiesScriptEngine() { _nonPersistentEntitiesScriptManager = scriptManagerFactory(ScriptManager::ENTITY_CLIENT_SCRIPT, NO_SCRIPT, QString("about:Entities %1").arg(++_entitiesScriptEngineCount)); DependencyManager::get()->runScriptInitializers(_nonPersistentEntitiesScriptManager); + + // Make script engine messages available through ScriptDiscoveryService + auto scriptEngines = DependencyManager::get().data(); + connect(_nonPersistentEntitiesScriptManager.get(), &ScriptManager::infoEntityMessage, scriptEngines, &ScriptEngines::infoEntityMessage); + connect(_nonPersistentEntitiesScriptManager.get(), &ScriptManager::printedEntityMessage, scriptEngines, &ScriptEngines::printedEntityMessage); + connect(_nonPersistentEntitiesScriptManager.get(), &ScriptManager::errorEntityMessage, scriptEngines, &ScriptEngines::errorEntityMessage); + connect(_nonPersistentEntitiesScriptManager.get(), &ScriptManager::warningEntityMessage, scriptEngines, &ScriptEngines::warningEntityMessage); + _nonPersistentEntitiesScriptManager->runInThread(); std::shared_ptr entitiesScriptEngineProvider = _nonPersistentEntitiesScriptManager; DependencyManager::get()->setNonPersistentEntitiesScriptEngine(entitiesScriptEngineProvider); diff --git a/libraries/entities/src/EntityScriptServerLogClient.cpp b/libraries/entities/src/EntityScriptServerLogClient.cpp index 5d7d4017cda..7329cf1fdd2 100644 --- a/libraries/entities/src/EntityScriptServerLogClient.cpp +++ b/libraries/entities/src/EntityScriptServerLogClient.cpp @@ -9,7 +9,11 @@ // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // +#include +#include #include "EntityScriptServerLogClient.h" +#include "ScriptMessage.h" +#include "ScriptEngines.h" EntityScriptServerLogClient::EntityScriptServerLogClient() { auto nodeList = DependencyManager::get(); @@ -35,9 +39,9 @@ void EntityScriptServerLogClient::disconnectNotify(const QMetaMethod& signal) { void EntityScriptServerLogClient::connectionsChanged() { auto numReceivers = receivers(SIGNAL(receivedNewLogLines(QString))); - if (!_subscribed && numReceivers > 0) { + if (!_subscribed && (numReceivers > 0 || _areMessagesRequestedByScripts)) { enableToEntityServerScriptLog(DependencyManager::get()->getThisNodeCanRez()); - } else if (_subscribed && numReceivers == 0) { + } else if (_subscribed && (numReceivers == 0 && !_areMessagesRequestedByScripts)) { enableToEntityServerScriptLog(false); } } @@ -62,7 +66,59 @@ void EntityScriptServerLogClient::enableToEntityServerScriptLog(bool enable) { } void EntityScriptServerLogClient::handleEntityServerScriptLogPacket(QSharedPointer message, SharedNodePointer senderNode) { - emit receivedNewLogLines(QString::fromUtf8(message->readAll())); + QString messageText = QString::fromUtf8(message->readAll()); + QJsonParseError error; + QJsonDocument document = QJsonDocument::fromJson(messageText.toUtf8(), &error); + emit receivedNewLogLines(messageText); + if(document.isNull()) { + qWarning() << "EntityScriptServerLogClient::handleEntityServerScriptLogPacket: Cannot parse JSON: " << error.errorString() + << " Contents: " << messageText; + return; + } + // Iterate through contents and emit messages + if(!document.isArray()) { + qWarning() << "EntityScriptServerLogClient::handleEntityServerScriptLogPacket: JSON is not an array: " << messageText; + return; + } + + auto scriptEngines = DependencyManager::get().data(); + + auto array = document.array(); + for (int n = 0; n < array.size(); n++) { + if (!array[n].isObject()) { + qWarning() << "EntityScriptServerLogClient::handleEntityServerScriptLogPacket: message is not an object: " << messageText; + continue; + } + ScriptMessage scriptMessage; + if (!scriptMessage.fromJson(array[n].toObject())) { + qWarning() << "EntityScriptServerLogClient::handleEntityServerScriptLogPacket: message parsing failed: " << messageText; + continue; + } + switch (scriptMessage.getSeverity()) { + case ScriptMessage::Severity::SEVERITY_INFO: + emit scriptEngines->infoEntityMessage(scriptMessage.getMessage(), scriptMessage.getFileName(), + scriptMessage.getLineNumber(), scriptMessage.getEntityID(), true); + break; + + case ScriptMessage::Severity::SEVERITY_PRINT: + emit scriptEngines->printedEntityMessage(scriptMessage.getMessage(), scriptMessage.getFileName(), + scriptMessage.getLineNumber(), scriptMessage.getEntityID(), true); + break; + + case ScriptMessage::Severity::SEVERITY_WARNING: + emit scriptEngines->warningEntityMessage(scriptMessage.getMessage(), scriptMessage.getFileName(), + scriptMessage.getLineNumber(), scriptMessage.getEntityID(), true); + break; + + case ScriptMessage::Severity::SEVERITY_ERROR: + emit scriptEngines->errorEntityMessage(scriptMessage.getMessage(), scriptMessage.getFileName(), + scriptMessage.getLineNumber(), scriptMessage.getEntityID(), true); + break; + + default: + break; + } + } } void EntityScriptServerLogClient::nodeActivated(SharedNodePointer activatedNode) { @@ -84,3 +140,8 @@ void EntityScriptServerLogClient::canRezChanged(bool canRez) { enableToEntityServerScriptLog(canRez); } } + +void EntityScriptServerLogClient::requestMessagesForScriptEngines(bool areMessagesRequested) { + _areMessagesRequestedByScripts = areMessagesRequested; + connectionsChanged(); +} diff --git a/libraries/entities/src/EntityScriptServerLogClient.h b/libraries/entities/src/EntityScriptServerLogClient.h index 9eee5daed87..e388de917ad 100644 --- a/libraries/entities/src/EntityScriptServerLogClient.h +++ b/libraries/entities/src/EntityScriptServerLogClient.h @@ -33,6 +33,12 @@ class EntityScriptServerLogClient : public QObject, public Dependency { public: EntityScriptServerLogClient(); + /** + * @brief This is called by ScriptEngines when scripts need access to entity server script messages and when access + * is not needed anymore. + */ + void requestMessagesForScriptEngines(bool areMessagesRequested); + signals: /*@jsdoc @@ -66,7 +72,10 @@ private slots: void connectionsChanged(); private: + std::atomic _areMessagesRequestedByScripts {false}; bool _subscribed { false }; + + friend class ScriptEngines; }; #endif // hifi_EntityScriptServerLogClient_h diff --git a/libraries/script-engine/src/ConsoleScriptingInterface.cpp b/libraries/script-engine/src/ConsoleScriptingInterface.cpp index 2d9d5c7ad61..1c4c07dddd3 100644 --- a/libraries/script-engine/src/ConsoleScriptingInterface.cpp +++ b/libraries/script-engine/src/ConsoleScriptingInterface.cpp @@ -34,7 +34,7 @@ QList ConsoleScriptingInterface::_groupDetails = QList(); ScriptValue ConsoleScriptingInterface::info(ScriptContext* context, ScriptEngine* engine) { if (ScriptManager* scriptManager = engine->manager()) { - scriptManager->scriptInfoMessage(appendArguments(context)); + scriptManager->scriptInfoMessage(appendArguments(context), context->currentFileName(), context->currentLineNumber()); } return engine->nullValue(); } @@ -43,7 +43,7 @@ ScriptValue ConsoleScriptingInterface::log(ScriptContext* context, ScriptEngine* QString message = appendArguments(context); if (_groupDetails.count() == 0) { if (ScriptManager* scriptManager = engine->manager()) { - scriptManager->scriptPrintedMessage(message); + scriptManager->scriptPrintedMessage(message, context->currentFileName(), context->currentLineNumber()); } } else { logGroupMessage(message, engine); @@ -53,28 +53,28 @@ ScriptValue ConsoleScriptingInterface::log(ScriptContext* context, ScriptEngine* ScriptValue ConsoleScriptingInterface::debug(ScriptContext* context, ScriptEngine* engine) { if (ScriptManager* scriptManager = engine->manager()) { - scriptManager->scriptPrintedMessage(appendArguments(context)); + scriptManager->scriptPrintedMessage(appendArguments(context), context->currentFileName(), context->currentLineNumber()); } return engine->nullValue(); } ScriptValue ConsoleScriptingInterface::warn(ScriptContext* context, ScriptEngine* engine) { if (ScriptManager* scriptManager = engine->manager()) { - scriptManager->scriptWarningMessage(appendArguments(context)); + scriptManager->scriptWarningMessage(appendArguments(context), context->currentFileName(), context->currentLineNumber()); } return engine->nullValue(); } ScriptValue ConsoleScriptingInterface::error(ScriptContext* context, ScriptEngine* engine) { if (ScriptManager* scriptManager = engine->manager()) { - scriptManager->scriptErrorMessage(appendArguments(context)); + scriptManager->scriptErrorMessage(appendArguments(context), context->currentFileName(), context->currentLineNumber()); } return engine->nullValue(); } ScriptValue ConsoleScriptingInterface::exception(ScriptContext* context, ScriptEngine* engine) { if (ScriptManager* scriptManager = engine->manager()) { - scriptManager->scriptErrorMessage(appendArguments(context)); + scriptManager->scriptErrorMessage(appendArguments(context), context->currentFileName(), context->currentLineNumber()); } return engine->nullValue(); } @@ -84,7 +84,7 @@ void ConsoleScriptingInterface::time(QString labelName) { QString message = QString("%1: Timer started").arg(labelName); Q_ASSERT(engine); if (ScriptManager* scriptManager = engine()->manager()) { - scriptManager->scriptPrintedMessage(message); + scriptManager->scriptPrintedMessage(message, context()->currentFileName(), context()->currentLineNumber()); } } @@ -92,13 +92,13 @@ void ConsoleScriptingInterface::timeEnd(QString labelName) { Q_ASSERT(engine); if (ScriptManager* scriptManager = engine()->manager()) { if (!_timerDetails.contains(labelName)) { - scriptManager->scriptErrorMessage("No such label found " + labelName); + scriptManager->scriptErrorMessage("No such label found " + labelName, context()->currentFileName(), context()->currentLineNumber()); return; } if (_timerDetails.value(labelName).isNull()) { _timerDetails.remove(labelName); - scriptManager->scriptErrorMessage("Invalid start time for " + labelName); + scriptManager->scriptErrorMessage("Invalid start time for " + labelName, context()->currentFileName(), context()->currentLineNumber()); return; } QDateTime _startTime = _timerDetails.value(labelName); @@ -108,7 +108,7 @@ void ConsoleScriptingInterface::timeEnd(QString labelName) { QString message = QString("%1: %2ms").arg(labelName).arg(QString::number(diffInMS)); _timerDetails.remove(labelName); - scriptManager->scriptPrintedMessage(message); + scriptManager->scriptPrintedMessage(message, context()->currentFileName(), context()->currentLineNumber()); } } @@ -131,7 +131,7 @@ ScriptValue ConsoleScriptingInterface::assertion(ScriptContext* context, ScriptE assertionResult = QString("Assertion failed : %1").arg(message); } if (ScriptManager* scriptManager = engine->manager()) { - scriptManager->scriptErrorMessage(assertionResult); + scriptManager->scriptErrorMessage(assertionResult, context->currentFileName(), context->currentLineNumber()); } } return engine->nullValue(); @@ -143,7 +143,7 @@ void ConsoleScriptingInterface::trace() { if (ScriptManager* scriptManager = scriptEngine->manager()) { scriptManager->scriptPrintedMessage (QString(STACK_TRACE_FORMAT).arg(LINE_SEPARATOR, - scriptEngine->currentContext()->backtrace().join(LINE_SEPARATOR))); + scriptEngine->currentContext()->backtrace().join(LINE_SEPARATOR)), context()->currentFileName(), context()->currentLineNumber()); } } @@ -190,6 +190,6 @@ void ConsoleScriptingInterface::logGroupMessage(QString message, ScriptEngine* e } logMessage.append(message); if (ScriptManager* scriptManager = engine->manager()) { - scriptManager->scriptPrintedMessage(logMessage); + scriptManager->scriptPrintedMessage(logMessage, context()->currentFileName(), context()->currentLineNumber()); } } diff --git a/libraries/script-engine/src/ScriptContext.h b/libraries/script-engine/src/ScriptContext.h index 7bc70e1080a..8eb67c42474 100644 --- a/libraries/script-engine/src/ScriptContext.h +++ b/libraries/script-engine/src/ScriptContext.h @@ -57,6 +57,13 @@ class ScriptContext { virtual int argumentCount() const = 0; virtual ScriptValue argument(int index) const = 0; virtual QStringList backtrace() const = 0; + + // Name of the file in which message was generated. Empty string when no file name is available. + virtual int currentLineNumber() const = 0; + + // Number of the line on which message was generated. -1 if there line number is not available. + virtual QString currentFileName() const = 0; + virtual ScriptValue callee() const = 0; virtual ScriptEnginePointer engine() const = 0; virtual ScriptFunctionContextPointer functionContext() const = 0; diff --git a/libraries/script-engine/src/ScriptEngines.cpp b/libraries/script-engine/src/ScriptEngines.cpp index b47255ed24f..52d3b2753bd 100644 --- a/libraries/script-engine/src/ScriptEngines.cpp +++ b/libraries/script-engine/src/ScriptEngines.cpp @@ -158,8 +158,69 @@ void ScriptEngines::removeScriptEngine(ScriptManagerPointer manager) { QMutexLocker locker(&_allScriptsMutex); _allKnownScriptManagers.remove(manager); } + std::lock_guard lock(_subscriptionsToEntityScriptMessagesMutex); + _managersSubscribedToEntityScriptMessages.remove(manager.get()); + _entitiesSubscribedToEntityScriptMessages.remove(manager.get()); +} + +void ScriptEngines::requestServerEntityScriptMessages(ScriptManager *manager) { + std::lock_guard lock(_subscriptionsToEntityScriptMessagesMutex); + if (!_managersSubscribedToEntityScriptMessages.contains(manager)) { + _managersSubscribedToEntityScriptMessages.insert(manager); + // Emit a signal to inform EntityScriptServerLogClient about subscription request + emit requestingEntityScriptServerLog(true); + qDebug() << "ScriptEngines::requestServerEntityScriptMessages"; + } +} + +void ScriptEngines::requestServerEntityScriptMessages(ScriptManager *manager, const QUuid& entityID) { + std::lock_guard lock(_subscriptionsToEntityScriptMessagesMutex); + if (!_entitiesSubscribedToEntityScriptMessages.contains(manager)) { + _entitiesSubscribedToEntityScriptMessages.insert(manager,QSet()); + } + if (!_entitiesSubscribedToEntityScriptMessages[manager].contains(entityID)) { + _entitiesSubscribedToEntityScriptMessages[manager].insert(entityID); + // Emit a signal to inform EntityScriptServerLogClient about subscription request + emit requestingEntityScriptServerLog(true); + qDebug() << "ScriptEngines::requestServerEntityScriptMessages uuid"; + } +} + +void ScriptEngines::removeServerEntityScriptMessagesRequest(ScriptManager *manager) { + std::lock_guard lock(_subscriptionsToEntityScriptMessagesMutex); + if (_managersSubscribedToEntityScriptMessages.contains(manager)) { + _managersSubscribedToEntityScriptMessages.remove(manager); + } + if (_entitiesSubscribedToEntityScriptMessages.isEmpty() + && _managersSubscribedToEntityScriptMessages.isEmpty()) { + // No managers requiring entity script server messages remain, so we inform EntityScriptServerLogClient about this + // Emit a signal to inform EntityScriptServerLogClient about subscription request + emit requestingEntityScriptServerLog(false); + qDebug() << "ScriptEngines::removeServerEntityScriptMessagesRequest"; + } } +void ScriptEngines::removeServerEntityScriptMessagesRequest(ScriptManager *manager, const QUuid& entityID) { + std::lock_guard lock(_subscriptionsToEntityScriptMessagesMutex); + if (!_entitiesSubscribedToEntityScriptMessages.contains(manager)) { + return; + } + if (_entitiesSubscribedToEntityScriptMessages[manager].contains(entityID)) { + _entitiesSubscribedToEntityScriptMessages[manager].remove(entityID); + } + if (_entitiesSubscribedToEntityScriptMessages[manager].isEmpty()) { + _entitiesSubscribedToEntityScriptMessages.remove(manager); + } + if (_entitiesSubscribedToEntityScriptMessages.isEmpty() + && _managersSubscribedToEntityScriptMessages.isEmpty()) { + // No managers requiring entity script server messages remain, so we inform EntityScriptServerLogClient about this + // Emit a signal to inform EntityScriptServerLogClient about subscription request + emit requestingEntityScriptServerLog(false); + qDebug() << "ScriptEngines::removeServerEntityScriptMessagesRequest uuid"; + } +} + + void ScriptEngines::shutdownScripting() { _isStopped = true; QMutexLocker locker(&_allScriptsMutex); diff --git a/libraries/script-engine/src/ScriptEngines.h b/libraries/script-engine/src/ScriptEngines.h index 671789bd8e3..bafaa1322c1 100644 --- a/libraries/script-engine/src/ScriptEngines.h +++ b/libraries/script-engine/src/ScriptEngines.h @@ -186,6 +186,13 @@ class ScriptEngines : public QObject, public Dependency, public ScriptInitialize void removeScriptEngine(ScriptManagerPointer); + // Called by ScriptManagerScriptingInterface + void requestServerEntityScriptMessages(ScriptManager *manager); + void requestServerEntityScriptMessages(ScriptManager *manager, const QUuid& entityID); + + void removeServerEntityScriptMessagesRequest(ScriptManager *manager); + void removeServerEntityScriptMessagesRequest(ScriptManager *manager, const QUuid& entityID); + ScriptGatekeeper scriptGatekeeper; signals: @@ -251,11 +258,62 @@ class ScriptEngines : public QObject, public Dependency, public ScriptInitialize * Triggered when any script generates an information message or {@link console.info} is called. * @function ScriptDiscoveryService.infoMessage * @param {string} message - The information message. - * @param {string} scriptName - The name of the script that generated the informaton message. + * @param {string} scriptName - The name of the script that generated the information message. * @returns {Signal} */ void infoMessage(const QString& message, const QString& engineName); + /*@jsdoc + * Triggered when a client side entity script prints a message to the program log via {@link print}, {@link Script.print}, + * {@link console.log}, {@link console.debug}, {@link console.group}, {@link console.groupEnd}, {@link console.time}, or + * {@link console.timeEnd}. + * @function Script.printedMessage + * @param {string} message - The message. + * @param {string} fileName - Name of the file in which message was generated. Empty string when no file name is available. + * @param {number} lineNumber - Number of the line on which message was generated. -1 if there line number is not available. + * @param {Uuid} entityID - Entity ID. + * @param {boolean} isServerScript - true if entity script is server-side, false if it is client-side. + * @returns {Signal} + */ + void printedEntityMessage(const QString& message, const QString& fileName, int lineNumber, const EntityItemID& entityID, bool isServerScript); + + /*@jsdoc + * Triggered when a client side entity script generates an error, {@link console.error} or {@link console.exception} is called, or + * {@link console.assert} is called and fails. + * @function Script.errorMessage + * @param {string} message - The error message. + * @param {string} fileName - Name of the file in which message was generated. Empty string when no file name is available. + * @param {number} lineNumber - Number of the line on which message was generated. -1 if there line number is not available. + * @param {Uuid} entityID - Entity ID. + * @param {boolean} isServerScript - true if entity script is server-side, false if it is client-side. + * @returns {Signal} + */ + void errorEntityMessage(const QString& message, const QString& fileName, int lineNumber, const EntityItemID& entityID, bool isServerScript); + + /*@jsdoc + * Triggered when a client side entity script generates a warning or {@link console.warn} is called. + * @function Script.warningMessage + * @param {string} message - The warning message. + * @param {string} fileName - Name of the file in which message was generated. Empty string when no file name is available. + * @param {number} lineNumber - Number of the line on which message was generated. -1 if there line number is not available. + * @param {Uuid} entityID - Entity ID. + * @param {boolean} isServerScript - true if entity script is server-side, false if it is client-side. + * @returns {Signal} + */ + void warningEntityMessage(const QString& message, const QString& fileName, int lineNumber, const EntityItemID& entityID, bool isServerScript); + + /*@jsdoc + * Triggered when a client side entity script generates an information message or {@link console.info} is called. + * @function Script.infoMessage + * @param {string} message - The information message. + * @param {string} fileName - Name of the file in which message was generated. Empty string when no file name is available. + * @param {number} lineNumber - Number of the line on which message was generated. -1 if there line number is not available. + * @param {Uuid} entityID - Entity ID. + * @param {boolean} isServerScript - true if entity script is server-side, false if it is client-side. + * @returns {Signal} + */ + void infoEntityMessage(const QString& message, const QString& fileName, int lineNumber, const EntityItemID& entityID, bool isServerScript); + /*@jsdoc * @function ScriptDiscoveryService.errorLoadingScript * @param {string} url - URL. @@ -272,6 +330,12 @@ class ScriptEngines : public QObject, public Dependency, public ScriptInitialize */ void clearDebugWindow(); + /** + * @brief Fires when script engines need entity server script messages (areMessagesRequested == true) + * and when messages are not needed anymore (areMessagesRequested == false). + */ + void requestingEntityScriptServerLog(bool areMessagesRequested); + public slots: /*@jsdoc @@ -355,6 +419,12 @@ protected slots: bool _defaultScriptsLocationOverridden { false }; QString _debugScriptUrl; + // For subscriptions to server entity script messages + std::mutex _subscriptionsToEntityScriptMessagesMutex; + QSet _managersSubscribedToEntityScriptMessages; + // Since multiple entity scripts run in the same script engine, there's a need to track subscriptions per entity + QHash> _entitiesSubscribedToEntityScriptMessages; + // If this is set, defaultScripts.js will not be run if it is in the settings, // and this will be run instead. This script will not be persisted to settings. const QUrl _defaultScriptsOverride { }; diff --git a/libraries/script-engine/src/ScriptManager.cpp b/libraries/script-engine/src/ScriptManager.cpp index f74bb01b719..37ba7d04424 100644 --- a/libraries/script-engine/src/ScriptManager.cpp +++ b/libraries/script-engine/src/ScriptManager.cpp @@ -225,7 +225,12 @@ QString encodeEntityIdIntoEntityUrl(const QString& url, const QString& entityID) QString ScriptManager::logException(const ScriptValue& exception) { auto message = formatException(exception, _enableExtendedJSExceptions.get()); - scriptErrorMessage(message); + auto context = _engine->currentContext(); + if (context) { + scriptErrorMessage(message, context->currentFileName(), context->currentLineNumber()); + } else { + scriptErrorMessage(message, "", -1); + } return message; } @@ -330,6 +335,11 @@ ScriptManager::ScriptManager(Context context, const QString& scriptContents, con }); } + //Gather entity script messages for transmission when running server side. + if (_type == Type::ENTITY_SERVER) { + ; + } + if (!_areMetaTypesInitialized) { initMetaTypes(); } @@ -514,7 +524,7 @@ void ScriptManager::waitTillDoneRunning(bool shutdown) { } #endif - scriptInfoMessage("Script Engine has stopped:" + getFilename()); + scriptInfoMessage("Script Engine has stopped:" + getFilename(), "", -1); } } @@ -549,7 +559,7 @@ void ScriptManager::loadURL(const QUrl& scriptURL, bool 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"); + scriptErrorMessage("File extension of file: " + _fileNameString + " is not a currently supported script type", _fileNameString, -1); emit errorLoadingScript(_fileNameString); return; } @@ -559,7 +569,7 @@ void ScriptManager::loadURL(const QUrl& scriptURL, bool reload) { 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); + scriptErrorMessage("ERROR Loading file (" + status + "):" + url, url, -1); emit errorLoadingScript(_fileNameString); return; } @@ -570,24 +580,36 @@ void ScriptManager::loadURL(const QUrl& scriptURL, bool reload) { }, reload, maxRetries); } -void ScriptManager::scriptErrorMessage(const QString& message) { +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) { +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) { +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) { +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() { @@ -912,7 +934,7 @@ void ScriptManager::run() { return; // bail early - avoid setting state in init(), as evaluate() will bail too } - scriptInfoMessage("Script Engine starting:" + getFilename()); + scriptInfoMessage("Script Engine starting:" + getFilename(), getFilename(), -1); if (!_isInitialized) { init(); @@ -1064,7 +1086,7 @@ void ScriptManager::run() { _engine->clearExceptions(); } } - scriptInfoMessage("Script Engine stopping:" + getFilename()); + scriptInfoMessage("Script Engine stopping:" + getFilename(), getFilename(), -1); stopAllTimers(); // make sure all our timers are stopped if the script is ending emit scriptEnding(); @@ -1139,7 +1161,7 @@ void ScriptManager::updateMemoryCost(const qint64& deltaSize) { void ScriptManager::timerFired() { if (isStopped()) { - scriptWarningMessage("Script.timerFired() while shutting down is ignored... parent script:" + getFilename()); + scriptWarningMessage("Script.timerFired() while shutting down is ignored... parent script:" + getFilename(), getFilename(), -1); return; // bail early } @@ -1206,7 +1228,14 @@ QTimer* ScriptManager::setupTimerWithInterval(const ScriptValue& function, int i QTimer* ScriptManager::setInterval(const ScriptValue& function, int intervalMS) { if (isStopped()) { - scriptWarningMessage("Script.setInterval() while shutting down is ignored... parent script:" + getFilename()); + int lineNumber = -1; + QString fileName = getFilename(); + auto context = _engine->currentContext(); + if (context) { + lineNumber = context->currentLineNumber(); + fileName = context->currentFileName(); + } + scriptWarningMessage("Script.setInterval() while shutting down is ignored... parent script:" + getFilename(), fileName, lineNumber); return NULL; // bail early } @@ -1215,7 +1244,14 @@ QTimer* ScriptManager::setInterval(const ScriptValue& function, int intervalMS) QTimer* ScriptManager::setTimeout(const ScriptValue& function, int timeoutMS) { if (isStopped()) { - scriptWarningMessage("Script.setTimeout() while shutting down is ignored... parent script:" + getFilename()); + int lineNumber = -1; + QString fileName = getFilename(); + auto context = _engine->currentContext(); + if (context) { + lineNumber = context->currentLineNumber(); + fileName = context->currentFileName(); + } + scriptWarningMessage("Script.setTimeout() while shutting down is ignored... parent script:" + getFilename(), fileName, lineNumber); return NULL; // bail early } @@ -1281,7 +1317,7 @@ QUrl ScriptManager::resourcesPath() const { } void ScriptManager::print(const QString& message) { - emit printedMessage(message, getFilename()); + emit scriptPrintedMessage(message, getFilename(), engine()->currentContext()->currentLineNumber()); } @@ -1651,8 +1687,15 @@ void ScriptManager::include(const QStringList& includeFiles, const ScriptValue& return; } if (isStopped()) { + int lineNumber = -1; + QString fileName = getFilename(); + auto context = _engine->currentContext(); + if (context) { + lineNumber = context->currentLineNumber(); + fileName = context->currentFileName(); + } scriptWarningMessage("Script.include() while shutting down is ignored... includeFiles:" - + includeFiles.join(",") + "parent script:" + getFilename()); + + includeFiles.join(",") + "parent script:" + getFilename(), fileName, lineNumber); return; // bail early } QList urls; @@ -1665,8 +1708,15 @@ void ScriptManager::include(const QStringList& includeFiles, const ScriptValue& thisURL = expandScriptUrl(QUrl::fromLocalFile(expandScriptPath(file))); QUrl defaultScriptsLoc = PathUtils::defaultScriptsLocation(); if (!defaultScriptsLoc.isParentOf(thisURL)) { + int lineNumber = -1; + QString fileName = getFilename(); + auto context = _engine->currentContext(); + if (context) { + lineNumber = context->currentLineNumber(); + fileName = context->currentFileName(); + } //V8TODO this probably needs to be done per context, otherwise file cannot be included again in a module - scriptWarningMessage("Script.include() -- skipping" + file + "-- outside of standard libraries"); + scriptWarningMessage("Script.include() -- skipping" + file + "-- outside of standard libraries", fileName, lineNumber); continue; } isStandardLibrary = true; @@ -1676,8 +1726,15 @@ void ScriptManager::include(const QStringList& includeFiles, const ScriptValue& bool disallowOutsideFiles = thisURL.isLocalFile() && !isStandardLibrary && !currentSandboxURL.isLocalFile(); if (disallowOutsideFiles && !PathUtils::isDescendantOf(thisURL, currentSandboxURL)) { + int lineNumber = -1; + QString fileName = currentSandboxURL.toString(); + auto context = _engine->currentContext(); + if (context) { + lineNumber = context->currentLineNumber(); + fileName = context->currentFileName(); + } scriptWarningMessage("Script.include() ignoring file path" + thisURL.toString() - + "outside of original entity script" + currentSandboxURL.toString()); + + "outside of original entity script" + currentSandboxURL.toString(), fileName, lineNumber); } 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. @@ -1699,7 +1756,14 @@ void ScriptManager::include(const QStringList& includeFiles, const ScriptValue& for (QUrl url : urls) { QString contents = data[url]; if (contents.isNull()) { - scriptErrorMessage("Error loading file (" + status[url] +"): " + url.toString()); + int lineNumber = -1; + QString fileName = url.toString(); + auto context = _engine->currentContext(); + if (context) { + lineNumber = context->currentLineNumber(); + fileName = context->currentFileName(); + } + scriptErrorMessage("Error loading file (" + status[url] +"): " + url.toString(), fileName, lineNumber); } else { std::lock_guard lock(_lock); if (!_includedURLs.contains(url)) { @@ -1719,7 +1783,14 @@ void ScriptManager::include(const QStringList& includeFiles, const ScriptValue& _engine->clearExceptions(); } } else { - scriptPrintedMessage("Script.include() skipping evaluation of previously included url:" + url.toString()); + int lineNumber = -1; + QString fileName = url.toString(); + auto context = _engine->currentContext(); + if (context) { + lineNumber = context->currentLineNumber(); + fileName = context->currentFileName(); + } + scriptPrintedMessage("Script.include() skipping evaluation of previously included url:" + url.toString(), fileName, lineNumber); } } } @@ -1748,8 +1819,15 @@ void ScriptManager::include(const QStringList& includeFiles, const ScriptValue& void ScriptManager::include(const QString& includeFile, const ScriptValue& callback) { if (isStopped()) { + int lineNumber = -1; + QString fileName = currentSandboxURL.toString(); + auto context = _engine->currentContext(); + if (context) { + lineNumber = context->currentLineNumber(); + fileName = context->currentFileName(); + } scriptWarningMessage("Script.include() while shutting down is ignored... includeFile:" - + includeFile + "parent script:" + getFilename()); + + includeFile + "parent script:" + getFilename(), fileName, lineNumber); return; // bail early } @@ -1765,14 +1843,21 @@ void ScriptManager::load(const QString& loadFile) { if (!_engine->IS_THREADSAFE_INVOCATION(__FUNCTION__)) { return; } + int lineNumber = -1; + QString fileName = getFilename(); + auto context = _engine->currentContext(); + if (context) { + lineNumber = context->currentLineNumber(); + fileName = context->currentFileName(); + } if (isStopped()) { scriptWarningMessage("Script.load() while shutting down is ignored... loadFile:" - + loadFile + "parent script:" + getFilename()); + + loadFile + "parent script:" + getFilename(), fileName, lineNumber); return; // bail early } if (!currentEntityIdentifier.isInvalidID()) { scriptWarningMessage("Script.load() from entity script is ignored... loadFile:" - + loadFile + "parent script:" + getFilename() + "entity: " + currentEntityIdentifier.toString()); + + loadFile + "parent script:" + getFilename() + "entity: " + currentEntityIdentifier.toString(), fileName, lineNumber); return; // bail early } @@ -2440,7 +2525,7 @@ void ScriptManager::refreshFileScript(const EntityItemID& entityID) { QString filePath = QUrl(details.scriptText).toLocalFile(); auto lastModified = QFileInfo(filePath).lastModified().toMSecsSinceEpoch(); if (lastModified > details.lastModified) { - scriptInfoMessage("Reloading modified script " + details.scriptText); + scriptInfoMessage("Reloading modified script " + details.scriptText, filePath, -1); loadEntityScript(entityID, details.scriptText, true); } } diff --git a/libraries/script-engine/src/ScriptManager.h b/libraries/script-engine/src/ScriptManager.h index 01d0a1dbf08..623b51a43f1 100644 --- a/libraries/script-engine/src/ScriptManager.h +++ b/libraries/script-engine/src/ScriptManager.h @@ -1074,8 +1074,10 @@ class ScriptManager : public QObject, public EntitiesScriptEngineProvider, publi * Emits errorMessage() * * @param message Message to send to the log + * @param fileName Name of the file in which message was generated. Empty string when no file name is available. + * @param lineNumber Number of the line on which message was generated. -1 if there line number is not available. */ - void scriptErrorMessage(const QString& message); + void scriptErrorMessage(const QString& message, const QString& fileName, int lineNumber); /** * @brief Logs a script warning message and emits an warningMessage event @@ -1083,8 +1085,10 @@ class ScriptManager : public QObject, public EntitiesScriptEngineProvider, publi * Emits warningMessage() * * @param message Message to send to the log + * @param fileName Name of the file in which message was generated. Empty string when no file name is available. + * @param lineNumber Number of the line on which message was generated. -1 if there line number is not available. */ - void scriptWarningMessage(const QString& message); + void scriptWarningMessage(const QString& message, const QString& fileName, int lineNumber); /** * @brief Logs a script info message and emits an infoMessage event @@ -1092,8 +1096,10 @@ class ScriptManager : public QObject, public EntitiesScriptEngineProvider, publi * Emits infoMessage() * * @param message Message to send to the log + * @param fileName Name of the file in which message was generated. Empty string when no file name is available. + * @param lineNumber Number of the line on which message was generated. -1 if there line number is not available. */ - void scriptInfoMessage(const QString& message); + void scriptInfoMessage(const QString& message, const QString& fileName, int lineNumber); /** * @brief Logs a script printed message and emits an printedMessage event @@ -1102,9 +1108,11 @@ class ScriptManager : public QObject, public EntitiesScriptEngineProvider, publi * Emits printedMessage() * * @param message Message to send to the log + * @param fileName Name of the file in which message was generated. Empty string when no file name is available. + * @param lineNumber Number of the line on which message was generated. -1 if there line number is not available. */ - void scriptPrintedMessage(const QString& message); + void scriptPrintedMessage(const QString& message, const QString& fileName, int lineNumber); /** * @brief Clears the debug log window @@ -1321,6 +1329,54 @@ public slots: */ void infoMessage(const QString& message, const QString& scriptName); + /** + * @brief Triggered when a client side entity script prints a message to the program log + * + * @param message + * @param fileName Name of the file in which message was generated. + * @param lineNumber Number of the line on which message was generated. + * @param entityID + * @param isServerScript true if entity script is server-side, false if it is client-side. + */ + void printedEntityMessage(const QString& message, const QString& fileName, int lineNumber, const EntityItemID& entityID, bool isServerScript); + + + /** + * @brief Triggered when a client side entity script generates an error + * + * @param message + * @param fileName Name of the file in which message was generated. + * @param lineNumber Number of the line on which message was generated. + * @param entityID + * @param isServerScript true if entity script is server-side, false if it is client-side. + */ + void errorEntityMessage(const QString& message, const QString& fileName, int lineNumber, const EntityItemID& entityID, bool isServerScript); + + + + /** + * @brief Triggered when a client side entity script generates a warning + * + * @param message + * @param fileName Name of the file in which message was generated. + * @param lineNumber Number of the line on which message was generated. + * @param entityID + * @param isServerScript true if entity script is server-side, false if it is client-side. + */ + void warningEntityMessage(const QString& message, const QString& fileName, int lineNumber, const EntityItemID& entityID, bool isServerScript); + + + /** + * @brief Triggered when a client side entity script generates an information message + * + * @param message + * @param fileName Name of the file in which message was generated. + * @param lineNumber Number of the line on which message was generated. + * @param entityID + * @param isServerScript true if entity script is server-side, false if it is client-side. + */ + void infoEntityMessage(const QString& message, const QString& fileName, int lineNumber, const EntityItemID& entityID, bool isServerScript); + /** * @brief Triggered when the running state of the script changes, e.g., from running to stopping. diff --git a/libraries/script-engine/src/ScriptManagerScriptingInterface.cpp b/libraries/script-engine/src/ScriptManagerScriptingInterface.cpp index 7c9f264327c..8c2d2799f3d 100644 --- a/libraries/script-engine/src/ScriptManagerScriptingInterface.cpp +++ b/libraries/script-engine/src/ScriptManagerScriptingInterface.cpp @@ -12,6 +12,7 @@ #include "ScriptManager.h" #include "ScriptManagerScriptingInterface.h" +#include "ScriptEngines.h" #include "ScriptEngine.h" #include @@ -88,3 +89,39 @@ void ScriptManagerScriptingInterface::startProfiling() { void ScriptManagerScriptingInterface::stopProfilingAndSave() { _manager->engine()->stopProfilingAndSave(); } + +void ScriptManagerScriptingInterface::requestServerEntityScriptMessages() { + if (_manager->isEntityServerScript() || _manager->isEntityServerScript()) { + _manager->engine()->raiseException("Uuid needs to be specified when requestServerEntityScriptMessages is invoked from entity script"); + } else { + auto scriptEngines = DependencyManager::get().data(); + scriptEngines->requestServerEntityScriptMessages(_manager); + } +} + +void ScriptManagerScriptingInterface::requestServerEntityScriptMessages(const QUuid& entityID) { + if (_manager->isEntityServerScript() || _manager->isEntityServerScript()) { + auto scriptEngines = DependencyManager::get().data(); + scriptEngines->requestServerEntityScriptMessages(_manager, entityID); + } else { + _manager->engine()->raiseException("Uuid must not be specified when requestServerEntityScriptMessages is invoked from entity script"); + } +} + +void ScriptManagerScriptingInterface::removeServerEntityScriptMessagesRequest() { + if (_manager->isEntityServerScript() || _manager->isEntityServerScript()) { + _manager->engine()->raiseException("Uuid needs to be specified when removeServerEntityScriptMessagesRequest is invoked from entity script"); + } else { + auto scriptEngines = DependencyManager::get().data(); + scriptEngines->removeServerEntityScriptMessagesRequest(_manager); + } +} + +void ScriptManagerScriptingInterface::removeServerEntityScriptMessagesRequest(const QUuid& entityID) { + if (_manager->isEntityServerScript() || _manager->isEntityServerScript()) { + auto scriptEngines = DependencyManager::get().data(); + scriptEngines->removeServerEntityScriptMessagesRequest(_manager, entityID); + } else { + _manager->engine()->raiseException("Uuid must not be specified when removeServerEntityScriptMessagesRequest is invoked from entity script"); + } +} diff --git a/libraries/script-engine/src/ScriptManagerScriptingInterface.h b/libraries/script-engine/src/ScriptManagerScriptingInterface.h index 119cbadaa6f..c1d6bad360d 100644 --- a/libraries/script-engine/src/ScriptManagerScriptingInterface.h +++ b/libraries/script-engine/src/ScriptManagerScriptingInterface.h @@ -512,7 +512,7 @@ class ScriptManagerScriptingInterface : public QObject { /*@jsdoc * Start collecting object statistics that can later be reported with Script.dumpHeapObjectStatistics(). - * @function Script.dumpHeapObjectStatistics + * @function Script.startCollectingObjectStatistics */ Q_INVOKABLE void startCollectingObjectStatistics(); @@ -557,7 +557,31 @@ class ScriptManagerScriptingInterface : public QObject { */ Q_INVOKABLE void stopProfilingAndSave(); -signals: + /*@jsdoc + * After calling this function current script engine will start receiving server-side entity script messages + * through signals such as errorEntityMessage. This function can be invoked both from client-side entity scripts + * and from interface scripts. + * @function Script.subscribeToServerEntityScriptMessages + * @param {Uuid=} entityID - The ID of the entity that requests entity server script messages. Only needs to be specified + * for entity scripts, and must not be specified for other types of scripts. + */ + + Q_INVOKABLE void requestServerEntityScriptMessages(); + Q_INVOKABLE void requestServerEntityScriptMessages(const QUuid& entityID); + + /*@jsdoc + * Calling this function signalizes that current script doesn't require stop receiving server-side entity script messages + * through signals such as errorEntityMessage. This function can be invoked both from client-side entity scripts + * and from interface scripts. + * @function Script.unsubscribeFromServerEntityScriptMessages + * @param {Uuid=} entityID - The ID of the entity that requests entity server script messages. Only needs to be specified + * for entity scripts, and must not be specified for other types of scripts. + */ + + Q_INVOKABLE void removeServerEntityScriptMessagesRequest(); + Q_INVOKABLE void removeServerEntityScriptMessagesRequest(const QUuid& entityID); + + signals: /*@jsdoc * @function Script.scriptLoaded diff --git a/libraries/script-engine/src/ScriptMessage.cpp b/libraries/script-engine/src/ScriptMessage.cpp new file mode 100644 index 00000000000..0b3ea1abc86 --- /dev/null +++ b/libraries/script-engine/src/ScriptMessage.cpp @@ -0,0 +1,48 @@ +// +// ScriptMessage.h +// libraries/script-engine/src/v8/FastScriptValueUtils.cpp +// +// Created by dr Karol Suprynowicz on 2023/09/24. +// Copyright 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 +// + +#include "ScriptMessage.h" + +#include + +QJsonObject ScriptMessage::toJson() { + QJsonObject object; + object["message"] = _messageContent; + object["lineNumber"] = _lineNumber; + object["fileName"] = _fileName; + object["entityID"] = _entityID.toString(); + object["type"] = static_cast(_scriptType); + object["severity"] = static_cast(_severity); + return object; +} + +bool ScriptMessage::fromJson(const QJsonObject &object) { + if (object.isEmpty()) { + qDebug() << "ScriptMessage::fromJson object is empty"; + return false; + } + if (!object["message"].isString() + || !object["lineNumber"].isDouble() + || !object["fileName"].isString() + || !object["entityID"].isString() + || !object["type"].isDouble() + || !object["severity"].isDouble()) { + qDebug() << "ScriptMessage::fromJson failed to find required fields in JSON file"; + return false; + } + _messageContent = object["message"].toString(); + _lineNumber = object["lineNumber"].toInt(); + _fileName = object["fileName"].toInt(); + _entityID = QUuid::fromString(object["entityID"].toString()); + _scriptType = static_cast(object["type"].toInt()); + _severity = static_cast(object["severity"].toInt()); + return true; +} \ No newline at end of file diff --git a/libraries/script-engine/src/ScriptMessage.h b/libraries/script-engine/src/ScriptMessage.h new file mode 100644 index 00000000000..6fa7892d081 --- /dev/null +++ b/libraries/script-engine/src/ScriptMessage.h @@ -0,0 +1,61 @@ +// +// ScriptMessage.h +// libraries/script-engine/src/v8/FastScriptValueUtils.cpp +// +// Created by dr Karol Suprynowicz on 2023/09/24. +// Copyright 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 +// + +#ifndef OVERTE_SCRIPTMESSAGE_H +#define OVERTE_SCRIPTMESSAGE_H + +// Used to store script messages on entity script server before transmitting them to clients who subscribed to them. +// EntityServerScriptLog packet type is used. +// In the future will also be used for storing assignment client script messages before transmission + +#include +#include +#include "EntityItemID.h" + +class ScriptMessage { +public: + enum class ScriptType { + TYPE_NONE, + TYPE_ENTITY_SCRIPT + }; + enum class Severity { + SEVERITY_NONE, + SEVERITY_PRINT, + SEVERITY_INFO, + SEVERITY_DEBUG, + SEVERITY_WARNING, + SEVERITY_ERROR + }; + + ScriptMessage() {}; + ScriptMessage(const QString &messageContent, const QString &fileName, int lineNumber, const EntityItemID& entityID, ScriptType scriptType, Severity severity) + : _messageContent(messageContent), _fileName(fileName), _lineNumber(lineNumber), _entityID(entityID), _scriptType(scriptType), _severity(severity) {} + + QJsonObject toJson(); + bool fromJson(const QJsonObject &object); + + QString getMessage() { return _messageContent; } + QString getFileName() { return _fileName; } + int getLineNumber() { return _lineNumber; } + ScriptType getScriptType() { return _scriptType; } + Severity getSeverity() { return _severity; } + EntityItemID getEntityID() { return _entityID; } + +private: + QString _messageContent; + QString _fileName; + int _lineNumber {-1}; + EntityItemID _entityID; + ScriptType _scriptType {ScriptType::TYPE_NONE}; + Severity _severity {Severity::SEVERITY_NONE}; +}; + +#endif //OVERTE_SCRIPTMESSAGE_H diff --git a/libraries/script-engine/src/v8/ScriptContextV8Wrapper.cpp b/libraries/script-engine/src/v8/ScriptContextV8Wrapper.cpp index 2b92a9ae8a5..11e90b3a5ab 100644 --- a/libraries/script-engine/src/v8/ScriptContextV8Wrapper.cpp +++ b/libraries/script-engine/src/v8/ScriptContextV8Wrapper.cpp @@ -111,6 +111,37 @@ QStringList ScriptContextV8Wrapper::backtrace() const { return backTrace; } +int ScriptContextV8Wrapper::currentLineNumber() const { + auto isolate = _engine->getIsolate(); + v8::Locker locker(isolate); + v8::Isolate::Scope isolateScope(isolate); + v8::HandleScope handleScope(isolate); + v8::Context::Scope contextScope(_context.Get(isolate)); + v8::Local stackTrace = v8::StackTrace::CurrentStackTrace(isolate, 1); + if (stackTrace->GetFrameCount() > 0) { + v8::Local stackFrame = stackTrace->GetFrame(isolate, 0); + return stackFrame->GetLineNumber(); + } else { + return -1; + } +} + +QString ScriptContextV8Wrapper::currentFileName() const { + auto isolate = _engine->getIsolate(); + v8::Locker locker(isolate); + v8::Isolate::Scope isolateScope(isolate); + v8::HandleScope handleScope(isolate); + v8::Context::Scope contextScope(_context.Get(isolate)); + v8::Local stackTrace = v8::StackTrace::CurrentStackTrace(isolate, 1); + QStringList backTrace; + if (stackTrace->GetFrameCount() > 0) { + v8::Local stackFrame = stackTrace->GetFrame(isolate, 0); + return *v8::String::Utf8Value(isolate, stackFrame->GetScriptNameOrSourceURL()); + } else { + return ""; + } +} + ScriptValue ScriptContextV8Wrapper::callee() const { Q_ASSERT(false); //V8TODO diff --git a/libraries/script-engine/src/v8/ScriptContextV8Wrapper.h b/libraries/script-engine/src/v8/ScriptContextV8Wrapper.h index c410587c458..4512e728185 100644 --- a/libraries/script-engine/src/v8/ScriptContextV8Wrapper.h +++ b/libraries/script-engine/src/v8/ScriptContextV8Wrapper.h @@ -45,6 +45,13 @@ class ScriptContextV8Wrapper final : public ScriptContext { virtual int argumentCount() const override; virtual ScriptValue argument(int index) const override; virtual QStringList backtrace() const override; + + // Name of the file in which message was generated. Empty string when no file name is available. + virtual int currentLineNumber() const override; + + // Number of the line on which message was generated. -1 if there line number is not available. + virtual QString currentFileName() const override; + virtual ScriptValue callee() const override; virtual ScriptEnginePointer engine() const override; virtual ScriptFunctionContextPointer functionContext() const override; diff --git a/libraries/script-engine/src/v8/ScriptEngineV8.cpp b/libraries/script-engine/src/v8/ScriptEngineV8.cpp index 61dbdd00e08..d10b1c2d159 100644 --- a/libraries/script-engine/src/v8/ScriptEngineV8.cpp +++ b/libraries/script-engine/src/v8/ScriptEngineV8.cpp @@ -77,6 +77,17 @@ bool ScriptEngineV8::IS_THREADSAFE_INVOCATION(const QThread* thread, const QStri return false; } +QString getFileNameFromTryCatch(v8::TryCatch &tryCatch, v8::Isolate *isolate, v8::Local &context ) { + v8::Local exceptionMessage = tryCatch.Message(); + QString errorFileName; + auto resource = exceptionMessage->GetScriptResourceName(); + v8::Local v8resourceString; + if (resource->ToString(context).ToLocal(&v8resourceString)) { + errorFileName = QString(*v8::String::Utf8Value(isolate, v8resourceString)); + } + return errorFileName; +} + ScriptValue ScriptEngineV8::makeError(const ScriptValue& _other, const QString& type) { if (!IS_THREADSAFE_INVOCATION(thread(), __FUNCTION__)) { return nullValue(); @@ -726,7 +737,13 @@ ScriptValue ScriptEngineV8::evaluateInClosure(const ScriptValue& _closure, + "tryCatch details:" + formatErrorMessageFromTryCatch(tryCatch); v8Result = v8::Null(_v8Isolate); if (_manager) { - _manager->scriptErrorMessage(errorMessage); + v8::Local exceptionMessage = tryCatch.Message(); + int errorLineNumber = -1; + if (!exceptionMessage.IsEmpty()) { + errorLineNumber = exceptionMessage->GetLineNumber(closureContext).FromJust(); + } + _manager->scriptErrorMessage(errorMessage, getFileNameFromTryCatch(tryCatch, _v8Isolate, closureContext), + errorLineNumber); } else { qWarning(scriptengine_v8) << errorMessage; } @@ -781,7 +798,13 @@ ScriptValue ScriptEngineV8::evaluate(const QString& sourceCode, const QString& f if (!v8::Script::Compile(context, v8::String::NewFromUtf8(getIsolate(), sourceCode.toStdString().c_str()).ToLocalChecked(), &scriptOrigin).ToLocal(&script)) { QString errorMessage(QString("Error while compiling script: \"") + fileName + QString("\" ") + formatErrorMessageFromTryCatch(tryCatch)); if (_manager) { - _manager->scriptErrorMessage(errorMessage); + v8::Local exceptionMessage = tryCatch.Message(); + int errorLineNumber = -1; + if (!exceptionMessage.IsEmpty()) { + errorLineNumber = exceptionMessage->GetLineNumber(context).FromJust(); + } + _manager->scriptErrorMessage(errorMessage, getFileNameFromTryCatch(tryCatch, _v8Isolate, context), + errorLineNumber); } else { qDebug(scriptengine_v8) << errorMessage; } @@ -799,7 +822,13 @@ ScriptValue ScriptEngineV8::evaluate(const QString& sourceCode, const QString& f ScriptValue errorValue(new ScriptValueV8Wrapper(this, V8ScriptValue(this, runError->Get()))); QString errorMessage(QString("Running script: \"") + fileName + QString("\" ") + formatErrorMessageFromTryCatch(tryCatchRun)); if (_manager) { - _manager->scriptErrorMessage(errorMessage); + v8::Local exceptionMessage = tryCatchRun.Message(); + int errorLineNumber = -1; + if (!exceptionMessage.IsEmpty()) { + errorLineNumber = exceptionMessage->GetLineNumber(context).FromJust(); + } + _manager->scriptErrorMessage(errorMessage, getFileNameFromTryCatch(tryCatchRun, _v8Isolate, context), + errorLineNumber); } else { qDebug(scriptengine_v8) << errorMessage; } diff --git a/libraries/script-engine/src/v8/ScriptEngineV8.h b/libraries/script-engine/src/v8/ScriptEngineV8.h index 5badba271ef..02352c9ed78 100644 --- a/libraries/script-engine/src/v8/ScriptEngineV8.h +++ b/libraries/script-engine/src/v8/ScriptEngineV8.h @@ -309,6 +309,8 @@ class ContextScopeV8 { ScriptEngineV8* _engine; }; +QString getFileNameFromTryCatch(v8::TryCatch &tryCatch, v8::Isolate *isolate, v8::Local &context ); + #include "V8Types.h" #endif // hifi_ScriptEngineV8_h diff --git a/libraries/script-engine/src/v8/ScriptObjectV8Proxy.cpp b/libraries/script-engine/src/v8/ScriptObjectV8Proxy.cpp index 09e7cb59bb1..48572c30f97 100644 --- a/libraries/script-engine/src/v8/ScriptObjectV8Proxy.cpp +++ b/libraries/script-engine/src/v8/ScriptObjectV8Proxy.cpp @@ -56,6 +56,13 @@ class ScriptPropertyContextV8Wrapper final : public ScriptContext { virtual int argumentCount() const override { return _parent->argumentCount(); } virtual ScriptValue argument(int index) const override { return _parent->argument(index); } virtual QStringList backtrace() const override { return _parent->backtrace(); } + + // Name of the file in which message was generated. Empty string when no file name is available. + virtual int currentLineNumber() const override { return _parent->currentLineNumber(); } + + // Number of the line on which message was generated. -1 if there line number is not available. + virtual QString currentFileName() const override { return _parent->currentFileName(); } + virtual ScriptValue callee() const override { return _parent->callee(); } virtual ScriptEnginePointer engine() const override { return _parent->engine(); } virtual ScriptFunctionContextPointer functionContext() const override { return _parent->functionContext(); } @@ -1274,8 +1281,14 @@ int ScriptSignalV8Proxy::qt_metacall(QMetaObject::Call call, int id, void** argu QString errorMessage(QString("Signal proxy ") + fullName() + " connection call failed: \"" + _engine->formatErrorMessageFromTryCatch(tryCatch) + "\nThis provided: " + QString::number(conn.thisValue.get()->IsObject())); + v8::Local exceptionMessage = tryCatch.Message(); + int errorLineNumber = -1; + if (!exceptionMessage.IsEmpty()) { + errorLineNumber = exceptionMessage->GetLineNumber(context).FromJust(); + } if (_engine->_manager) { - _engine->_manager->scriptErrorMessage(errorMessage); + _engine->_manager->scriptErrorMessage(errorMessage, getFileNameFromTryCatch(tryCatch, isolate, context), + errorLineNumber); } else { qDebug(scriptengine_v8) << errorMessage; } diff --git a/libraries/script-engine/src/v8/ScriptValueV8Wrapper.cpp b/libraries/script-engine/src/v8/ScriptValueV8Wrapper.cpp index 40bfde08076..7f9faf21f20 100644 --- a/libraries/script-engine/src/v8/ScriptValueV8Wrapper.cpp +++ b/libraries/script-engine/src/v8/ScriptValueV8Wrapper.cpp @@ -104,7 +104,13 @@ ScriptValue ScriptValueV8Wrapper::call(const ScriptValue& thisObject, const Scri if (tryCatch.HasCaught()) { QString errorMessage(QString("Function call failed: \"") + _engine->formatErrorMessageFromTryCatch(tryCatch)); if (_engine->_manager) { - _engine->_manager->scriptErrorMessage(errorMessage); + v8::Local exceptionMessage = tryCatch.Message(); + int errorLineNumber = -1; + if (!exceptionMessage.IsEmpty()) { + errorLineNumber = exceptionMessage->GetLineNumber(context).FromJust(); + } + _engine->_manager->scriptErrorMessage(errorMessage, getFileNameFromTryCatch(tryCatch, isolate, context), + errorLineNumber); } else { qDebug(scriptengine_v8) << errorMessage; } @@ -114,9 +120,10 @@ ScriptValue ScriptValueV8Wrapper::call(const ScriptValue& thisObject, const Scri if (maybeResult.ToLocal(&result)) { return ScriptValue(new ScriptValueV8Wrapper(_engine, V8ScriptValue(_engine, result))); } else { - QString errorMessage("JS function call failed: " + _engine->currentContext()->backtrace().join("\n")); + auto currentContext = _engine->currentContext(); + QString errorMessage("JS function call failed: " + currentContext->backtrace().join("\n")); if (_engine->_manager) { - _engine->_manager->scriptErrorMessage(errorMessage); + _engine->_manager->scriptErrorMessage(errorMessage, currentContext->currentFileName(), currentContext->currentLineNumber()); } else { qDebug(scriptengine_v8) << errorMessage; }