From 7af436c43d8bb7027c146e9a57bb19c3c89c5d3e Mon Sep 17 00:00:00 2001 From: Rider Linden Date: Mon, 17 Aug 2026 17:24:06 -0700 Subject: Issue https://github.com/secondlife/sl-vscode-plugin/issues/98: Refactor message passing into unified format. --- indra/newview/llfloaterimnearbychathandler.cpp | 36 ++- indra/newview/llpreviewscript.cpp | 10 +- indra/newview/llpublishedobjectmgr.cpp | 381 ++++++++++++++++++++++++- indra/newview/llpublishedobjectmgr.h | 127 ++++++++- indra/newview/llscripteditorws.cpp | 376 +++++++++++------------- indra/newview/llscripteditorws.h | 41 +-- 6 files changed, 704 insertions(+), 267 deletions(-) (limited to 'indra/newview') diff --git a/indra/newview/llfloaterimnearbychathandler.cpp b/indra/newview/llfloaterimnearbychathandler.cpp index e92672f684..34570725ce 100644 --- a/indra/newview/llfloaterimnearbychathandler.cpp +++ b/indra/newview/llfloaterimnearbychathandler.cpp @@ -564,6 +564,23 @@ void LLFloaterIMNearbyChatHandler::processChat(const LLChat& chat_msg, return; } + if (LLScriptEditorWSServer::isEnabled() && + gSavedSettings.getBOOL("ExternalWebsocketForwardDebug") && + (chat_msg.mChatType == CHAT_TYPE_DEBUG_MSG || + chat_msg.mChatType == CHAT_TYPE_OWNER)) + { + LLScriptEditorWSServer::ptr_t server = + LLScriptEditorWSServer::getServer(); + if (server) + { + const auto channel = + chat_msg.mChatType == CHAT_TYPE_OWNER + ? LLPublishedObjectMgr::RuntimeEventAggregator::Channel::OWNER_SAY + : LLPublishedObjectMgr::RuntimeEventAggregator::Channel::DEBUG; + server->forwardChatToIDE(chat_msg, channel); + } + } + // don't show toast and add message to chat history on receive debug message // with disabled setting showing script errors or enabled setting to show script // errors in separate window. @@ -575,15 +592,6 @@ void LLFloaterIMNearbyChatHandler::processChat(const LLChat& chat_msg, if (!gSavedSettings.getBOOL("ShowScriptErrors")) return; - if (LLScriptEditorWSServer::isEnabled() && gSavedSettings.getBOOL("ExternalWebsocketForwardDebug")) - { - LLScriptEditorWSServer::ptr_t server = LLScriptEditorWSServer::getServer(); - if (server) - { - server->forwardChatToIDE(chat_msg); - } - } - // don't process debug messages from not owned objects, see EXT-7762 if (gAgentID != chat_msg.mOwnerID) { @@ -603,16 +611,6 @@ void LLFloaterIMNearbyChatHandler::processChat(const LLChat& chat_msg, return; } } - else if ((chat_msg.mChatType == CHAT_TYPE_OWNER) && LLScriptEditorWSServer::isEnabled() && - gSavedSettings.getBOOL("ExternalWebsocketForwardDebug")) - { - LLScriptEditorWSServer::ptr_t server = LLScriptEditorWSServer::getServer(); - if (server) - { - server->forwardChatToIDE(chat_msg); - } - } - nearby_chat->addMessage(chat_msg, true, args); if (chat_msg.mSourceType == CHAT_SOURCE_AGENT diff --git a/indra/newview/llpreviewscript.cpp b/indra/newview/llpreviewscript.cpp index 10bacf53b7..354147336a 100644 --- a/indra/newview/llpreviewscript.cpp +++ b/indra/newview/llpreviewscript.cpp @@ -40,7 +40,6 @@ #include "llinventorymodel.h" #include "llkeyboard.h" #include "lllineeditor.h" -#include "llmd5.h" #include "llhelp.h" #include "llnotificationsutil.h" #include "llresmgr.h" @@ -1613,14 +1612,7 @@ std::string LLScriptEdContainer::getUniqueHash() const // Take script inventory item id (within the object inventory) // to consideration so that it's possible to edit multiple scripts // in the same object inventory simultaneously (STORM-781). - std::string script_id = mObjectUUID.asString() + "_" + mItemUUID.asString(); - - // Use MD5 sum to make the file name shorter and not exceed maximum path length. - char script_id_hash_str[33]; /* Flawfinder: ignore */ - LLMD5 script_id_hash((const U8*)script_id.c_str()); - script_id_hash.hex_digest(script_id_hash_str); - - return std::string(script_id_hash_str); + return LLScriptEditorWSServer::buildScriptSubscriptionId(mObjectUUID, mItemUUID); } std::string LLScriptEdContainer::getErrorLogFileName(const std::string& script_path) diff --git a/indra/newview/llpublishedobjectmgr.cpp b/indra/newview/llpublishedobjectmgr.cpp index e9d13c1213..d92bb382de 100644 --- a/indra/newview/llpublishedobjectmgr.cpp +++ b/indra/newview/llpublishedobjectmgr.cpp @@ -29,7 +29,9 @@ #include "llscripteditorws.h" +#include "llchat.h" #include "llinventorydefines.h" +#include "llregex.h" #include "llselectmgr.h" #include "llviewerinventory.h" #include "llviewerobject.h" @@ -39,6 +41,12 @@ namespace { + static const boost::regex LUAU_LOCATION_PATTERN( + R"(^([^:]*):([0-9]+):\s*(.*)$)"); + + static const boost::regex LSL_LOCATION_PATTERN( + R"(\((\d+), (\d+)\) : ([^:]+) : (.+))"); + std::string nv_string(LLViewerObject* obj, const char* key) { if (!obj) @@ -122,13 +130,382 @@ private: LLUUID mPrimID; }; -LLPublishedObjectMgr::LLPublishedObjectMgr(LLScriptEditorWSServer* server) - : mServer(server) +LLPublishedObjectMgr::LLPublishedObjectMgr( + LLScriptEditorWSServer* server, + RuntimeEventCallback runtime_event_callback): + mServer(server), + mRuntimeEventCallback(std::move(runtime_event_callback)), + mRuntimeEventAggregator(std::make_unique( + [this](const RuntimeEventAggregator::RuntimeEvent& runtime_event) + { + std::optional event = + buildRuntimeChatEvent(runtime_event); + if (event && mRuntimeEventCallback) + { + mRuntimeEventCallback(*event); + } + })), + mRuntimeFlushTimer( + std::unique_ptr( + LLEventTimer::run_every( + RUNTIME_FLUSH_INTERVAL, + [this]() + { + flushExpiredRuntimeFragments(); + }))) { } LLPublishedObjectMgr::~LLPublishedObjectMgr() = default; +void LLPublishedObjectMgr::ingestRuntimeChat( + const LLChat& chat_msg, + RuntimeEventAggregator::Channel channel) +{ + static const boost::regex runtime_error_header( + R"(^(.+?)\s+\[script:([^\]]+)\]\s+Script run-time error)"); + + std::vector lines = + LLStringUtil::getTokens(chat_msg.mText, "\n"); + boost::smatch match; + bool is_error_header = + !lines.empty() && + boost::regex_match(lines.front(), match, runtime_error_header); + + if (!is_error_header && !mRuntimeEventAggregator->hasPending()) + { + RuntimeEventAggregator::RuntimeEvent runtime_event; + runtime_event.mVM = RuntimeEventAggregator::VM::LSL2; + runtime_event.mChannel = channel; + + RuntimeEventAggregator::Fragment fragment; + fragment.mFromID = chat_msg.mFromID; + fragment.mFromName = chat_msg.mFromName; + fragment.mText = chat_msg.mText; + runtime_event.mFragments.push_back(std::move(fragment)); + + std::optional event = + buildRuntimeChatEvent(runtime_event); + if (event && mRuntimeEventCallback) + { + mRuntimeEventCallback(*event); + } + return; + } + + RuntimeEventAggregator::VM vm = RuntimeEventAggregator::VM::LSL2; + LLViewerObject* prim = gObjectList.findObject(chat_msg.mFromID); + if (prim) + { + std::vector lines = + LLStringUtil::getTokens(chat_msg.mText, "\n"); + static const boost::regex runtime_error_header( + R"(^(.+?)\s+\[script:([^\]]+)\]\s+Script run-time error)"); + boost::smatch match; + + if (!lines.empty() && + boost::regex_match(lines.front(), match, runtime_error_header)) + { + const std::string script_name = match[2].str(); + LLInventoryObject::object_list_t inventory; + prim->getInventoryContents(inventory); + for (const auto& inventory_object : inventory) + { + LLInventoryItem* item = + dynamic_cast(inventory_object.get()); + if (item && item->getName() == script_name && + item->getRuntime() == "luau") + { + vm = RuntimeEventAggregator::VM::LUAU; + break; + } + } + } + } + + mRuntimeEventAggregator->ingest( + chat_msg.mFromID, + chat_msg.mFromName, + chat_msg.mText, + vm, + channel); +} + +void LLPublishedObjectMgr::flushExpiredRuntimeFragments() +{ + mRuntimeEventAggregator->flushExpired(); +} + +std::optional +LLPublishedObjectMgr::buildRuntimeChatEvent( + const RuntimeEventAggregator::RuntimeEvent& runtime_event) const +{ + if (runtime_event.mFragments.empty()) + { + return std::nullopt; + } + + const RuntimeEventAggregator::Fragment& source = + runtime_event.mFragments.front(); + LLViewerObject* prim = gObjectList.findObject(source.mFromID); + if (!prim) + { + return std::nullopt; + } + + LLViewerObject* root = prim->getRootEdit(); + if (!root) + { + return std::nullopt; + } + + RuntimeChatEvent event; + event.mChannel = runtime_event.mChannel; + event.mVM = runtime_event.mVM; + event.mRootID = root->getID(); + event.mPrimID = prim->getID(); + event.mObjectName = source.mFromName; + for (const auto& fragment : runtime_event.mFragments) + { + if (!event.mMessage.empty()) + { + event.mMessage += "\n"; + } + event.mMessage += fragment.mText; + } + + std::vector lines = + LLStringUtil::getTokens(event.mMessage, "\n"); + static const std::string runtime_error_marker = "Script run-time error"; + static const boost::regex runtime_error_header( + R"(^(.+?)\s+\[script:([^\]]+)\]\s+Script run-time error)"); + + auto ends_with = [](const std::string& value, const std::string& suffix) + { + return value.size() >= suffix.size() && + std::equal(suffix.rbegin(), suffix.rend(), value.rbegin()); + }; + + if (!lines.empty() && ends_with(lines.front(), runtime_error_marker)) + { + event.mIsError = true; + boost::smatch match; + if (boost::regex_match(lines.front(), match, runtime_error_header)) + { + event.mObjectName = match[1].str(); + event.mScriptName = match[2].str(); + lines.erase(lines.begin()); + } + else + { + lines.clear(); + } + } + + LLInventoryObject::object_list_t inventory; + prim->getInventoryContents(inventory); + for (const auto& inventory_object : inventory) + { + LLInventoryItem* item = + dynamic_cast(inventory_object.get()); + if (item && !event.mScriptName.empty() && + item->getName() == event.mScriptName) + { + event.mItemID = item->getUUID(); + break; + } + } + + if (event.mIsError && !lines.empty()) + { + RuntimeEventAggregator::VM vm = runtime_event.mVM; + if (event.mItemID.notNull()) + { + LLInventoryItem* item = + dynamic_cast(prim->getInventoryObject(event.mItemID)); + if (item && item->getRuntime() == "luau") + { + vm = RuntimeEventAggregator::VM::LUAU; + } + } + + RuntimeEventAggregator::ParsedError parsed = + mRuntimeEventAggregator->parseError(vm, runtime_event.mFragments); + event.mError = parsed.mError; + event.mLine = parsed.mLine; + event.mColumn = parsed.mColumn; + event.mStack = lines; + } + + return event; +} + +LLPublishedObjectMgr::RuntimeEventAggregator::RuntimeEventAggregator( + FlushCallback flush_callback): + mFlushCallback(std::move(flush_callback)) +{ +} + +void LLPublishedObjectMgr::RuntimeEventAggregator::ingest( + const LLUUID& from_id, + const std::string& from_name, + const std::string& text, + VM vm, + Channel channel) +{ + if (mPending && + isNewBurst(from_id, from_name, vm, channel)) + { + flushPending(); + } + + if (!mPending) + { + mPending = std::make_unique(); + mPending->mFromID = from_id; + mPending->mFromName = from_name; + mPending->mVM = vm; + mPending->mChannel = channel; + } + + Fragment fragment; + fragment.mFromID = from_id; + fragment.mFromName = from_name; + fragment.mText = text; + mPending->mFragments.push_back(std::move(fragment)); + mPending->mTimer.setTimerExpirySec(FRAGMENT_TIMEOUT); +} + +void LLPublishedObjectMgr::RuntimeEventAggregator::flushExpired() +{ + if (mPending && mPending->mTimer.hasExpired()) + { + flushPending(); + } +} + +void LLPublishedObjectMgr::RuntimeEventAggregator::flush() +{ + flushPending(); +} + +bool LLPublishedObjectMgr::RuntimeEventAggregator::hasPending() const +{ + return mPending != nullptr; +} + +LLPublishedObjectMgr::RuntimeEventAggregator::ParsedError +LLPublishedObjectMgr::RuntimeEventAggregator::parseError( + VM vm, + const fragments_t& fragments) const +{ + ParsedError parsed; + const boost::regex* location_pattern = nullptr; + bool lsl_coordinates = false; + + switch (vm) + { + case VM::LUAU: + location_pattern = &LUAU_LOCATION_PATTERN; + break; + case VM::LSL2: + location_pattern = &LSL_LOCATION_PATTERN; + lsl_coordinates = true; + break; + } + + for (const auto& fragment : fragments) + { + std::vector lines = + LLStringUtil::getTokens(fragment.mText, "\n"); + for (const auto& line : lines) + { + boost::smatch match; + if (!boost::regex_match(line, match, *location_pattern)) + { + continue; + } + + if (lsl_coordinates) + { + parsed.mLine = static_cast( + std::strtol(match[1].str().c_str(), nullptr, 10)) + 1; + parsed.mColumn = static_cast( + std::strtol(match[2].str().c_str(), nullptr, 10)) + 1; + parsed.mError = match[4].str(); + } + else + { + parsed.mSource = match[1].str(); + parsed.mLine = static_cast( + std::strtol(match[2].str().c_str(), nullptr, 10)); + parsed.mColumn = 0; + parsed.mError = match[3].str(); + } + return parsed; + } + } + + // LSL runtime errors commonly arrive as plain text without source + // location metadata, for example: "Math Error". + if (vm == VM::LSL2) + { + for (const auto& fragment : fragments) + { + const std::vector lines = + LLStringUtil::getTokens(fragment.mText, "\n"); + for (const auto& line : lines) + { + if (line.empty() || + line.find("Script run-time error") != std::string::npos) + { + continue; + } + + parsed.mError = line; + return parsed; + } + } + } + + return parsed; +} + +void LLPublishedObjectMgr::RuntimeEventAggregator::flushPending() +{ + if (!mPending) + { + return; + } + + if (!mFlushCallback) + { + mPending.reset(); + return; + } + + RuntimeEvent event; + event.mVM = mPending->mVM; + event.mChannel = mPending->mChannel; + event.mFragments = mPending->mFragments; + event.mError = parseError(mPending->mVM, mPending->mFragments); + mFlushCallback(event); + + mPending.reset(); +} + +bool LLPublishedObjectMgr::RuntimeEventAggregator::isNewBurst( + const LLUUID& from_id, + const std::string& from_name, + VM vm, + Channel channel) const +{ + return mPending->mFromID != from_id || + mPending->mFromName != from_name || + mPending->mVM != vm || + mPending->mChannel != channel; +} + LLPublishedObjectMgr::PublishedObjectInfo::PublishedObjectInfo() = default; LLPublishedObjectMgr::PublishedObjectInfo::~PublishedObjectInfo() = default; LLPublishedObjectMgr::PublishedObjectInfo::PublishedObjectInfo(PublishedObjectInfo&&) noexcept = default; diff --git a/indra/newview/llpublishedobjectmgr.h b/indra/newview/llpublishedobjectmgr.h index 99ffa34b39..51af129dcb 100644 --- a/indra/newview/llpublishedobjectmgr.h +++ b/indra/newview/llpublishedobjectmgr.h @@ -27,17 +27,21 @@ #pragma once #include "llsd.h" +#include "lltimer.h" #include "lluuid.h" #include "lleventtimer.h" #include "stdtypes.h" #include +#include #include +#include #include #include #include class LLScriptEditorWSServer; +class LLChat; class LLPublishedPrimListener; class LLViewerObject; @@ -117,9 +121,124 @@ public: std::string mPendingItemCreatePump; }; - explicit LLPublishedObjectMgr(LLScriptEditorWSServer* server = nullptr); + class RuntimeEventAggregator + { + public: + enum class Channel + { + DEBUG, + OWNER_SAY + }; + + enum class VM + { + LSL2, + LUAU + }; + + struct StackFrame + { + S32 mLine{ 0 }; + std::string mFunction; + std::string mSource; + }; + + struct ParsedError + { + std::string mSource; + std::string mError; + S32 mLine{ 0 }; + S32 mColumn{ 0 }; + std::vector mStack; + }; + + struct Fragment + { + LLUUID mFromID; + std::string mFromName; + std::string mText; + }; + + using fragments_t = std::vector; + + struct RuntimeEvent + { + VM mVM; + Channel mChannel; + fragments_t mFragments; + ParsedError mError; + }; + + using FlushCallback = std::function; + + explicit RuntimeEventAggregator(FlushCallback flush_callback); + + void ingest(const LLUUID& from_id, + const std::string& from_name, + const std::string& text, + VM vm, + Channel channel); + void flushExpired(); + void flush(); + ParsedError parseError(VM vm, + const fragments_t& fragments) const; + bool hasPending() const; + + private: + struct PendingBurst + { + LLUUID mFromID; + std::string mFromName; + VM mVM{ VM::LSL2 }; + Channel mChannel{ Channel::DEBUG }; + fragments_t mFragments; + LLTimer mTimer; + }; + + static constexpr F32 FRAGMENT_TIMEOUT = 1.0f; + + void flushPending(); + bool isNewBurst(const LLUUID& from_id, + const std::string& from_name, + VM vm, + Channel channel) const; + + FlushCallback mFlushCallback; + std::unique_ptr mPending; + }; + + struct RuntimeChatEvent + { + RuntimeEventAggregator::Channel mChannel; + RuntimeEventAggregator::VM mVM; + LLUUID mRootID; + LLUUID mPrimID; + LLUUID mItemID; + std::string mObjectName; + std::string mScriptName; + std::string mMessage; + std::string mError; + S32 mLine{ 0 }; + S32 mColumn{ 0 }; + std::vector mStack; + bool mIsError{ false }; + }; + + using RuntimeEventCallback = + std::function; + + explicit LLPublishedObjectMgr( + LLScriptEditorWSServer* server = nullptr, + RuntimeEventCallback runtime_event_callback = {}); ~LLPublishedObjectMgr(); + void flushExpiredRuntimeFragments(); + void ingestRuntimeChat( + const LLChat& chat_msg, + RuntimeEventAggregator::Channel channel); + std::optional buildRuntimeChatEvent( + const RuntimeEventAggregator::RuntimeEvent& event) const; + bool hasPublished(const LLUUID& object_id) const { return mPublishedObjects.find(object_id) != mPublishedObjects.end(); } void erasePublished(const LLUUID& object_id) { mPublishedObjects.erase(object_id); } PublishedObjectInfo* getPublished(const LLUUID& object_id); @@ -190,6 +309,12 @@ public: private: LLScriptEditorWSServer* mServer{ nullptr }; + std::unique_ptr mRuntimeEventAggregator; + RuntimeEventCallback mRuntimeEventCallback; + std::unique_ptr mRuntimeFlushTimer; + + static constexpr F32 RUNTIME_FLUSH_INTERVAL = 0.25f; + void cancelPendingPublishWithCleanup(const LLUUID& object_id); void clearPublishedListeners(const LLUUID& object_id); diff --git a/indra/newview/llscripteditorws.cpp b/indra/newview/llscripteditorws.cpp index 0487ebd06d..e33e18bcd4 100644 --- a/indra/newview/llscripteditorws.cpp +++ b/indra/newview/llscripteditorws.cpp @@ -50,6 +50,7 @@ #include "llpreviewscript.h" #include "llprocess.h" #include "llregex.h" +#include "llmd5.h" #include "llsdjson.h" #include "llselectmgr.h" #include "lltrans.h" @@ -68,6 +69,8 @@ #include "llvoinventorylistener.h" #include "roles_constants.h" +#include + namespace { // Per-operation timeouts (seconds) for coroutine-based async RPC handlers. @@ -80,6 +83,12 @@ namespace constexpr F32 LINKSET_ADD_FLUSH_DELAY = 5.0f; constexpr F32 LINKSET_REMOVE_FLUSH_DELAY = 0.2f; + static const boost::regex LUAU_LOCATION_PATTERN( + R"(^([^:]*):([0-9]+):\s*(.*)$)"); + + static const boost::regex LSL_LOCATION_PATTERN( + R"(\((\d+), (\d+)\) : ([^:]+) : (.+))"); + // Creates a uniquely-named LLEventMailDrop under ".", passes // its name to kickoff (which arranges for one post to that pump), then // suspends the current coroutine up to imeout seconds for the result. @@ -165,7 +174,12 @@ namespace //======================================================================== LLScriptEditorWSServer::LLScriptEditorWSServer(const std::string& name, U16 port, bool local_only): LLJSONRPCServer(name, port, local_only), - mPublishedObjectManager(this) + mPublishedObjectManager( + this, + [this](const LLPublishedObjectMgr::RuntimeChatEvent& event) + { + sendRuntimeEvent(event); + }) { LL_INFOS("ScriptEditorWS") << "Created JSON-RPC script editor server: " << name << " on port " << port << LL_ENDL; @@ -198,11 +212,8 @@ LLScriptEditorWSServer::LLScriptEditorWSServer(const std::string& name, U16 port if (!handle_zoom_to_object(object_id)) { - LLSD response; - response["success"] = false; - response["error_code"] = WSCommandError::ExecutionError; - response["message"] = "Object not found or not reachable"; - return response; + throw LLJSONRPCConnection::InternalError( + "Object not found or not reachable"); } LLSD response; @@ -279,6 +290,18 @@ LLScriptEditorWSServer::ptr_t LLScriptEditorWSServer::ensureServerRunning() return server; } +std::string LLScriptEditorWSServer::buildScriptSubscriptionId(const LLUUID& object_id, + const LLUUID& item_id) +{ + std::string script_id = object_id.asString() + "_" + item_id.asString(); + + std::array script_id_hash_str = {}; + LLMD5 script_id_hash((const U8*)script_id.c_str()); + script_id_hash.hex_digest(script_id_hash_str.data()); + + return std::string(script_id_hash_str.data()); +} + std::string LLScriptEditorWSServer::buildVSCodeURI(const LLUUID& object_id, const LLUUID& script_id) { @@ -431,8 +454,12 @@ bool LLScriptEditorWSServer::subscribeScriptEditor(const LLUUID& object_id, cons if (it == mSubscriptions.end()) { // New subscription - mSubscriptions.emplace(script_id, - EditorSubscription(object_id, item_id, script_name, editor_handle)); + ItemRef item_ref; + item_ref.mPrimID = object_id; + item_ref.mItemID = item_id; + item_ref.mScriptName = script_name; + mSubscriptions.emplace(script_id, + EditorSubscription(item_ref, editor_handle)); } else { @@ -451,8 +478,6 @@ void LLScriptEditorWSServer::unsubscribeEditor(const std::string &script_id) auto connection = it->second.mConnection.lock(); mSubscriptions.erase(it); - // Maintain per-connection count; erase entry when it hits zero. - bool last_for_connection = false; if (connection_id != 0) { auto cit = mConnectionSubscriptionCounts.find(connection_id); @@ -461,21 +486,8 @@ void LLScriptEditorWSServer::unsubscribeEditor(const std::string &script_id) if (--cit->second <= 0) { mConnectionSubscriptionCounts.erase(cit); - last_for_connection = true; } } - else - { - // No counter entry means no other subs referenced this connection. - last_for_connection = true; - } - } - - if (connection && last_for_connection) - { // We have removed the last subscription, close the connection - LL_DEBUGS("ScriptEditorWS") << "Closing connection ID " << connection_id << - " as last subscription was removed" << LL_ENDL; - connection->sendDisconnect(LLScriptEditorWSConnection::DisconnectReason::EDITOR_CLOSED, "Editor closed"); } } @@ -597,7 +609,7 @@ void LLScriptEditorWSServer::setupConnectionMethods(LLJSONRPCConnection::ptr_t c return s.handleSyntaxCacheFileRequest(params); })); - script_connection->registerMethod("script.subscribe", + script_connection->registerAsyncMethod("script.subscribe", bindHandler([connection_id](LLScriptEditorWSServer& s, auto&, auto&, const LLSD& params) { return s.handleScriptSubscribe(connection_id, params); @@ -609,7 +621,7 @@ void LLScriptEditorWSServer::setupConnectionMethods(LLJSONRPCConnection::ptr_t c return s.handleFileWatcherFileListRequest(); })); - script_connection->registerMethod("object.unpublish", + script_connection->registerAsyncMethod("object.unpublish", bindHandler([connection_id](LLScriptEditorWSServer& s, auto&, auto&, const LLSD& params) { return s.handleObjectUnpublish(connection_id, params); @@ -946,39 +958,27 @@ LLSD LLScriptEditorWSServer::handleSaveBackToObjectContents(U32 connection_id, c mPublishedObjectManager.getPublished(object_id); if (!published_info) { - LLSD response; - response["success"] = false; - response["error_code"] = WSCommandError::InvalidParams; - response["message"] = "Object is not published"; - return response; + throw LLJSONRPCConnection::InvalidParams( + "Object is not published"); } if (!published_info->mCanSaveBackToContents || published_info->mSourceTaskID.isNull()) { - LLSD response; - response["success"] = false; - response["error_code"] = WSCommandError::NotPermitted; - response["message"] = "Save back is not available for this object"; - return response; + throw LLJSONRPCConnection::ForbiddenError( + "Save back is not available for this object"); } LLViewerObject* root = gObjectList.findObject(object_id); if (!root) { - LLSD response; - response["success"] = false; - response["error_code"] = WSCommandError::InvalidParams; - response["message"] = "object_id not found"; - return response; + throw LLJSONRPCConnection::InvalidParams( + "object_id not found"); } if (!save_object_back_to_contents(root, published_info->mSourceTaskID)) { - LLSD response; - response["success"] = false; - response["error_code"] = WSCommandError::ExecutionError; - response["message"] = "Failed to save object back to contents"; - return response; + throw LLJSONRPCConnection::InternalError( + "Failed to save object back to contents"); } LL_DEBUGS("ScriptEditorWS") << "Save-back requested via command for object " @@ -1004,11 +1004,8 @@ LLSD LLScriptEditorWSServer::handleCommandExecute(U32 connection_id, const LLSD& auto it = mCommandRegistry.find(command); if (it == mCommandRegistry.end()) { - LLSD response; - response["success"] = false; - response["error_code"] = WSCommandError::UnknownCommand; - response["message"] = "Unknown command: " + command; - return response; + throw LLJSONRPCConnection::InvalidParams( + "Unknown command: " + command); } return it->second.second(connection_id, params["params"]); @@ -1088,27 +1085,32 @@ LLSD LLScriptEditorWSServer::handleSyntaxRequest(const LLSD& params) const if (category.empty()) { - response["error"] = "No syntax category specified"; - response["success"] = false; - return response; + throw LLJSONRPCConnection::InvalidParams( + "No syntax category specified"); } response["id"] = mLastSyntaxId; if (category == "defs.lua") { response["defs"] = LLSyntaxDefCache::instance().getLuaKeywords(); - response["success"] = response["defs"].isDefined(); } else if (category == "defs.lsl") { response["defs"] = LLSyntaxDefCache::instance().getLSLKeywords(); - response["success"] = response["defs"].isDefined(); } else { - response["error"] = "Unknown syntax category requested"; - response["success"] = false; + throw LLJSONRPCConnection::InvalidParams( + "Unknown syntax category requested"); + } + + if (!response["defs"].isDefined()) + { + throw LLJSONRPCConnection::InternalError( + "Syntax definitions are unavailable"); } + + response["success"] = true; return response; } @@ -1136,28 +1138,25 @@ LLSD LLScriptEditorWSServer::handleSyntaxCacheFileRequest(const LLSD& params) co if (filename.empty()) { - response["error"] = "No filename specified"; - response["success"] = false; - return response; + throw LLJSONRPCConnection::InvalidParams( + "No filename specified"); } if (!cache.hasCacheFile(filename)) { - response["error"] = "Requested syntax cache file not found"; - response["success"] = false; - return response; + throw LLJSONRPCConnection::InvalidParams( + "Requested syntax cache file not found"); } - bool success = false; if (as_json) { LLSD file_content = cache.loadCacheFileAsLLSD(filename); if (file_content.isDefined()) { response["content"] = file_content; - success = true; } else { - response["error"] = "Failed to load and format syntax cache file."; + throw LLJSONRPCConnection::InternalError( + "Failed to load and format syntax cache file."); } } else @@ -1166,14 +1165,14 @@ LLSD LLScriptEditorWSServer::handleSyntaxCacheFileRequest(const LLSD& params) co if (!content.empty()) { response["content"] = content; - success = true; } else { - response["error"] = "Failed to load syntax cache file"; + throw LLJSONRPCConnection::InternalError( + "Failed to load syntax cache file"); } } - response["success"] = success; + response["success"] = true; return response; } @@ -1217,10 +1216,22 @@ LLSD LLScriptEditorWSServer::handleScriptSubscribe(U32 connection_id, const LLSD auto it = mSubscriptions.find(script_id); if (it != mSubscriptions.end()) { - LLViewerObject* object = gObjectList.findObject((*it).second.mObjectID); - response["object_id"] = (*it).second.mObjectID; + LLUUID prim_id = (*it).second.mItemRef.mPrimID; + LLUUID root_id = prim_id; + LLViewerObject* object = gObjectList.findObject(prim_id); + if (object) + { + LLViewerObject* root = object->getRootEdit(); + if (root) + { + root_id = root->getID(); + } + } + + response["object_id"] = prim_id; + response["root_id"] = root_id; //response["object_name"] = object ? object->getName() : "Unknown"; - response["item_id"] = (*it).second.mItemID; + response["item_id"] = (*it).second.mItemRef.mItemID; } } @@ -1265,32 +1276,31 @@ LLSD LLScriptEditorWSServer::handleObjectRequest(U32 connection_id, const LLSD& if (object_id.isNull()) { - response["success"] = false; - response["message"] = "No object_id specified"; - return response; + throw LLJSONRPCConnection::InvalidParams( + "No object_id specified"); } LLViewerObject* object = gObjectList.findObject(object_id); if (!object) { - response["success"] = false; - response["message"] = "Object not found"; - return response; + throw LLJSONRPCConnection::InvalidParams( + "Object not found"); } if (!object->permModify()) { - response["success"] = false; - response["message"] = "Permission denied"; - return response; + throw LLJSONRPCConnection::ForbiddenError( + "Permission denied"); } bool accepted = publishObject(object_id); - response["success"] = accepted; if (!accepted) { - response["message"] = "Failed to initiate publish"; + throw LLJSONRPCConnection::InternalError( + "Failed to initiate publish"); } + + response["success"] = true; return response; } @@ -1531,7 +1541,16 @@ LLSD LLScriptEditorWSServer::saveScript(LLViewerObject* prim, LLInventoryItem* i response["compiled"] = cb_result["compiled"]; if (!cb_result["compiled"].asBoolean() && cb_result.has("errors")) { - response["errors"] = cb_result["errors"]; + response["diagnostics"] = LLSD::emptyArray(); + for (const auto& error : llsd::inArray(cb_result["errors"])) + { + LLSD diagnostic; + diagnostic["row"] = 0; + diagnostic["column"] = 0; + diagnostic["level"] = "ERROR"; + diagnostic["message"] = error.asString(); + response["diagnostics"].append(diagnostic); + } } // If the script is open in the viewer's editor, update it @@ -1934,12 +1953,10 @@ void LLScriptEditorWSServer::sendCompileResults(const std::string &script_id, co params["running"] = results["is_running"].asBoolean(); if (results.has("errors")) { - params["errors"] = LLSD::emptyArray(); + params["diagnostics"] = LLSD::emptyArray(); if (is_lua) { // lua errors: ":line: message", line is 1-based - const static boost::regex lua_err_regex(R"(^[^:]*:(\d+): (.+)$)"); - for (const auto& err : llsd::inArray(results["errors"])) { boost::smatch match; @@ -1948,10 +1965,10 @@ void LLScriptEditorWSServer::sendCompileResults(const std::string &script_id, co err_entry["column"] = 0; // TODO: Lua compiler does not provide column info err_entry["level"] = "ERROR"; - if (boost::regex_match(err.asString(), match, lua_err_regex)) + if (boost::regex_match(err.asString(), match, LUAU_LOCATION_PATTERN)) { - S32 line_number = std::stoi(match[1].str()); - std::string message = match[2].str(); + S32 line_number = std::stoi(match[2].str()); + std::string message = match[3].str(); err_entry["row"] = line_number; err_entry["message"] = message; @@ -1961,19 +1978,17 @@ void LLScriptEditorWSServer::sendCompileResults(const std::string &script_id, co err_entry["row"] = 0; err_entry["message"] = err.asString(); } - params["errors"].append(err_entry); + params["diagnostics"].append(err_entry); } } else { // lsl errors: "(line, column) : SEVERITY : message", line and column are 0-based - static const boost::regex lsl_err_regex(R"(\((\d+), (\d+)\) : ([^:]+) : (.+))"); - for (const auto& err : llsd::inArray(results["errors"])) { boost::smatch match; LLSD err_entry; - if (boost::regex_match(err.asString(), match, lsl_err_regex)) + if (boost::regex_match(err.asString(), match, LSL_LOCATION_PATTERN)) { S32 line_number = std::stoi(match[1].str()); S32 col_number = std::stoi(match[2].str()); @@ -1994,7 +2009,7 @@ void LLScriptEditorWSServer::sendCompileResults(const std::string &script_id, co err_entry["message"] = err.asString(); err_entry["format"] = "lsl"; } - params["errors"].append(err_entry); + params["diagnostics"].append(err_entry); } } } @@ -2002,138 +2017,73 @@ void LLScriptEditorWSServer::sendCompileResults(const std::string &script_id, co notifyScript(script_id, "script.compiled", params); } -void LLScriptEditorWSServer::forwardChatToIDE(const LLChat& chat_msg) const +void LLScriptEditorWSServer::forwardChatToIDE( + const LLChat& chat_msg, + LLPublishedObjectMgr::RuntimeEventAggregator::Channel channel) const { LL_PROFILE_ZONE_SCOPED_CATEGORY_SCRIPTDEV; - LLUUID object_id = chat_msg.mFromID; - bool tracking = false; - bool publish = false; + mPublishedObjectManager.ingestRuntimeChat(chat_msg, channel); +} - LLUUID publish_id = object_id; - LLViewerObject* objectp = gObjectList.findObject(object_id); - if (objectp) - { - LLViewerObject* root = objectp->getRootEdit(); - if (root) - { - publish_id = root->getID(); - } - } +void LLScriptEditorWSServer::sendRuntimeEvent( + const LLPublishedObjectMgr::RuntimeChatEvent& event) const +{ - const EditorSubscription* subinfo = nullptr; - std::string script_id; - // have we either published or subscribed to this object? - if (isObjectPublished(publish_id)) + std::string script_id; + if (event.mItemID.notNull()) { - tracking = true; - publish = true; - } - else - { - // If the object is not published, we may still be tracking it if it is a script we are subscribed to - auto it = std::find_if(mSubscriptions.begin(), mSubscriptions.end(), - [&object_id](const auto& pair) { return (pair.second.mObjectID == object_id); }); - if (it != mSubscriptions.end()) - { - tracking = true; - subinfo = &it->second; - script_id = it->first; - } + script_id = buildScriptSubscriptionId(event.mPrimID, event.mItemID); } - if (!tracking) - { // Not a script we are tracking - return; - } - - bool is_error = false; - std::string error_message; - std::string object_name = chat_msg.mFromName; - std::string script_name; - S32 line_number = 0; - // We have at least one script from this object, we will forward the message to the IDE - // but first we need to see if it is a runtime error - std::vector lines = LLStringUtil::getTokens(chat_msg.mText, "\n"); - // If this is a runtime error, the first line will look like: " [script: