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/llscripteditorws.cpp | 376 ++++++++++++++++--------------------- 1 file changed, 159 insertions(+), 217 deletions(-) (limited to 'indra/newview/llscripteditorws.cpp') 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: