diff options
| author | Rider Linden <rider@lindenlab.com> | 2026-08-19 13:53:16 -0700 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2026-08-19 13:53:16 -0700 |
| commit | f26e024aee3957193a3bd7ffdd614c004c243eaf (patch) | |
| tree | 30c12980f211cb46ce4ea21c09f9b0bdfec3e230 | |
| parent | 305bebbdb3b16b10b6011b5b3b5d63ee7c7334be (diff) | |
| parent | ece8b732d963afe257b4ba7396c9deb340dbf47c (diff) | |
Merge pull request #6156 from secondlife/rider/runtime_msging
Rider/runtime msging
| -rw-r--r-- | doc/external-editor-json-rpc.md | 38 | ||||
| -rw-r--r-- | indra/llcorehttp/CMakeLists.txt | 1 | ||||
| -rw-r--r-- | indra/llcorehttp/lljsonrpcws.cpp | 18 | ||||
| -rw-r--r-- | indra/llcorehttp/lljsonrpcws.h | 5 | ||||
| -rw-r--r-- | indra/llcorehttp/tests/llcorehttp_test.cpp | 1 | ||||
| -rw-r--r-- | indra/llcorehttp/tests/test_jsonrpcws.hpp | 146 | ||||
| -rw-r--r-- | indra/newview/llfloaterimnearbychathandler.cpp | 36 | ||||
| -rw-r--r-- | indra/newview/llpreviewscript.cpp | 10 | ||||
| -rw-r--r-- | indra/newview/llpublishedobjectmgr.cpp | 381 | ||||
| -rw-r--r-- | indra/newview/llpublishedobjectmgr.h | 127 | ||||
| -rw-r--r-- | indra/newview/llscripteditorws.cpp | 408 | ||||
| -rw-r--r-- | indra/newview/llscripteditorws.h | 41 |
12 files changed, 937 insertions, 275 deletions
diff --git a/doc/external-editor-json-rpc.md b/doc/external-editor-json-rpc.md index 6c041cf70b..5c7d70185e 100644 --- a/doc/external-editor-json-rpc.md +++ b/doc/external-editor-json-rpc.md @@ -25,7 +25,7 @@ This document describes all the message interfaces defined for WebSocket communi - [ScriptUnsubscribe](#scriptunsubscribe) - [ScriptList](#scriptlist) - [Compilation Interfaces](#compilation-interfaces) - - [CompilationError](#compilationerror) + - [Diagnostic](#diagnostic) - [CompilationResult](#compilationresult) - [Runtime Event Interfaces](#runtime-event-interfaces) - [RuntimeDebug](#runtimedebug) @@ -601,12 +601,12 @@ interface ScriptList { ## Compilation Interfaces -### CompilationError +### Diagnostic Individual compilation error record. ```typescript -interface CompilationError { +interface Diagnostic { row: number; column: number; level: string; @@ -631,10 +631,11 @@ Result of a compilation operation in the viewer. ```typescript interface CompilationResult { + /** Deprecated: retained during migration to item-based routing. */ script_id: string; success: boolean; running: boolean; - errors?: CompilationError[]; + diagnostics?: Diagnostic[]; } ``` @@ -643,7 +644,7 @@ interface CompilationResult { - `script_id`: Unique identifier for the script that was compiled - `success`: Whether the compilation was successful - `running`: Whether the compiled script is currently running -- `errors` (optional): Array of compilation errors if any occurred +- `diagnostics` (optional): Array of `Diagnostic` records if any occurred ## Runtime Event Interfaces @@ -655,10 +656,15 @@ Debug message notification sent by the viewer during script execution. ```typescript interface RuntimeDebug { + /** Deprecated: use item.item_id when available. */ script_id: string; object_id: string; + prim_id?: string; + item_id?: string; object_name: string; message: string; + channel?: "debug" | "owner_say"; + item?: ItemRef; } ``` @@ -677,13 +683,27 @@ Runtime error notification sent by the viewer when a script encounters an error ```typescript interface RuntimeError { + /** Deprecated: use item.item_id when available. */ script_id: string; object_id: string; + prim_id?: string; + item_id?: string; object_name: string; message: string; error: string; line: number; + column?: number; stack?: string[]; + channel?: "debug" | "owner_say"; + item?: ItemRef; +} + +interface ItemRef { + root_id: string; + prim_id?: string; + item_id?: string; + name?: string; + language?: "lsl" | "luau"; } ``` @@ -693,8 +713,8 @@ interface RuntimeError { - `object_id`: Unique identifier for the object containing the script - `object_name`: Human-readable name of the object - `message`: The full raw chat text of the runtime error message as received from the simulator -- `error`: Extracted error description. Currently always an empty string - runtime error extraction from the simulator's multi-message format is not yet fully implemented. -- `line`: Line number where the error occurred. Currently always `0` for the same reason. +- `error`: Extracted runtime error description. This remains a top-level compatibility field while the protocol stays on version `1.0`. +- `line`: Line number where the error occurred when the runtime format can be parsed; otherwise `0`. - `stack` (optional): Stack trace lines if they could be extracted from the error message ## Handler and Configuration Interfaces @@ -973,7 +993,7 @@ interface ObjectContentSaveResponse { prim_id?: string; item_id?: string; compiled?: boolean; - errors?: string[]; + diagnostics?: Diagnostic[]; message?: string; } ``` @@ -986,7 +1006,7 @@ interface ObjectContentSaveResponse { - `vm` (optional): Scripts only compile target. Accepted values are `"mono"`, `"lsl2"`, `"luau"`. When `"luau"` is specified for an LSL script (as opposed to a native Luau script), the viewer automatically selects the correct LSL-on-Luau compile path. If omitted, inferred from item metadata or content analysis. - `success`: Whether the upload/save operation succeeded. - `compiled` (optional): Scripts only. `true` when compilation succeeded, `false` when source saved but compile failed. -- `errors` (optional): Scripts only. Compiler diagnostics when `compiled` is `false`. +- `diagnostics` (optional): Scripts only. Array of `Diagnostic` records when `compiled` is `false`. - `message` (optional): Error description on failure. --- diff --git a/indra/llcorehttp/CMakeLists.txt b/indra/llcorehttp/CMakeLists.txt index fbc25a7cd5..c2c4078ff0 100644 --- a/indra/llcorehttp/CMakeLists.txt +++ b/indra/llcorehttp/CMakeLists.txt @@ -119,6 +119,7 @@ if (LL_TESTS AND LLCOREHTTP_TESTS) tests/test_httpheaders.hpp tests/test_bufferarray.hpp tests/test_bufferstream.hpp + tests/test_jsonrpcws.hpp ) list(APPEND llcorehttp_TEST_SOURCE_FILES ${llcorehttp_TEST_HEADER_FILES}) diff --git a/indra/llcorehttp/lljsonrpcws.cpp b/indra/llcorehttp/lljsonrpcws.cpp index 3a0d3d1f26..d6c5b0eb97 100644 --- a/indra/llcorehttp/lljsonrpcws.cpp +++ b/indra/llcorehttp/lljsonrpcws.cpp @@ -457,6 +457,24 @@ void LLJSONRPCConnection::sweepTimeouts() } } +void LLJSONRPCConnection::testInjectPendingRequest(const std::string& id, F64 deadline, ResponseCallback callback) +{ + LLMutexLock lock(&mMutex); + mPendingRequests[id] = std::move(callback); + mPendingDeadlines.push({ deadline, id }); +} + +void LLJSONRPCConnection::testSweepTimeouts() +{ + sweepTimeouts(); +} + +size_t LLJSONRPCConnection::testPendingRequestCount() const +{ + LLMutexLock lock(&mMutex); + return mPendingRequests.size(); +} + LLSD LLJSONRPCConnection::generateId() { // Server-wide atomic counter for efficient unique ID generation. diff --git a/indra/llcorehttp/lljsonrpcws.h b/indra/llcorehttp/lljsonrpcws.h index cd71473a47..7cb26b1fb2 100644 --- a/indra/llcorehttp/lljsonrpcws.h +++ b/indra/llcorehttp/lljsonrpcws.h @@ -401,6 +401,11 @@ private: /// Invoked by the sweep timer; fires the timeout callback for any /// request whose deadline has passed. Safe to call from the main thread. void sweepTimeouts(); + +public: + void testInjectPendingRequest(const std::string& id, F64 deadline, ResponseCallback callback); + void testSweepTimeouts(); + size_t testPendingRequestCount() const; }; /** diff --git a/indra/llcorehttp/tests/llcorehttp_test.cpp b/indra/llcorehttp/tests/llcorehttp_test.cpp index c7c50e6166..65d7fe93a9 100644 --- a/indra/llcorehttp/tests/llcorehttp_test.cpp +++ b/indra/llcorehttp/tests/llcorehttp_test.cpp @@ -44,6 +44,7 @@ #include "test_httprequest.hpp" #include "test_httpheaders.hpp" #include "test_httprequestqueue.hpp" +#include "test_jsonrpcws.hpp" #include "_httpservice.h" #include "llproxy.h" diff --git a/indra/llcorehttp/tests/test_jsonrpcws.hpp b/indra/llcorehttp/tests/test_jsonrpcws.hpp new file mode 100644 index 0000000000..6cf7932aa0 --- /dev/null +++ b/indra/llcorehttp/tests/test_jsonrpcws.hpp @@ -0,0 +1,146 @@ +/** + * @file test_jsonrpcws.hpp + * @brief unit tests for LLJSONRPCConnection helpers and dispatch behavior + */ + +#ifndef TEST_LLCORE_JSONRPCWS_H_ +#define TEST_LLCORE_JSONRPCWS_H_ + +#include "lljsonrpcws.h" +#include "lltimer.h" + +namespace +{ +class TestJSONRPCConnection : public LLJSONRPCConnection +{ +public: + TestJSONRPCConnection() + : LLJSONRPCConnection(LLWebsocketMgr::WSServer::ptr_t(), LLWebsocketMgr::connection_h()) + { + } + + using LLJSONRPCConnection::processMessage; + using LLJSONRPCConnection::validateMessage; + using LLJSONRPCConnection::testInjectPendingRequest; + using LLJSONRPCConnection::testPendingRequestCount; + using LLJSONRPCConnection::testSweepTimeouts; +}; +} + +namespace tut +{ + struct JSONRPCWSTestData + { + }; + + typedef test_group<JSONRPCWSTestData> JSONRPCWSTestGroupType; + typedef JSONRPCWSTestGroupType::object JSONRPCWSTestObjectType; + JSONRPCWSTestGroupType JSONRPCWSTestGroup("LLJSONRPCConnection Tests"); + + template<> template<> + void JSONRPCWSTestObjectType::test<1>() + { + set_test_name("makeEnvelope notification omits id"); + + LLSD params; + params["value"] = 42; + LLSD env = LLJSONRPCConnection::makeEnvelope(LLSD(), "runtime.debug", params, LLSD(), LLSD()); + + ensure("jsonrpc field should exist", env.has("jsonrpc")); + ensure_equals("jsonrpc version", env["jsonrpc"].asString(), "2.0"); + ensure("notification should omit id", !env.has("id")); + ensure_equals("method", env["method"].asString(), "runtime.debug"); + ensure_equals("param round trip", env["params"]["value"].asInteger(), 42); + } + + template<> template<> + void JSONRPCWSTestObjectType::test<2>() + { + set_test_name("makeEnvelope response keeps id slot"); + + LLSD env = LLJSONRPCConnection::makeEnvelope(LLSD(), std::string(), LLSD(), LLSD("ok"), LLSD()); + + ensure("response should include id", env.has("id")); + ensure("response should not include method", !env.has("method")); + ensure_equals("result", env["result"].asString(), "ok"); + } + + template<> template<> + void JSONRPCWSTestObjectType::test<3>() + { + set_test_name("validateMessage accepts valid request and rejects invalid params"); + + TestJSONRPCConnection conn; + + LLSD valid; + valid["jsonrpc"] = "2.0"; + valid["method"] = "session.ping"; + LLSD params = LLSD::emptyMap(); + params["timestamp"] = 123; + valid["params"] = params; + ensure("valid request should pass", conn.validateMessage(valid, true)); + + LLSD invalid = valid; + invalid["params"] = "not-an-array-or-object"; + ensure("invalid params type should fail", !conn.validateMessage(invalid, true)); + } + + template<> template<> + void JSONRPCWSTestObjectType::test<4>() + { + set_test_name("processMessage dispatches notification handler"); + + TestJSONRPCConnection conn; + + bool called = false; + LLSD seen_id; + LLSD seen_params; + conn.registerMethod("runtime.debug", + [&](const std::string& method, const LLSD& id, const LLSD& params) -> LLSD + { + called = true; + ensure_equals("method propagated", method, "runtime.debug"); + seen_id = id; + seen_params = params; + return LLSD(); + }); + + LLSD params; + params["message"] = "hello"; + LLSD notification = LLJSONRPCConnection::makeEnvelope(LLSD(), "runtime.debug", params, LLSD(), LLSD()); + conn.processMessage(notification); + + ensure("handler should be called", called); + ensure("notification id should be undefined", seen_id.isUndefined()); + ensure_equals("payload should be forwarded", seen_params["message"].asString(), "hello"); + } + + template<> template<> + void JSONRPCWSTestObjectType::test<5>() + { + set_test_name("sweepTimeouts expires overdue callbacks"); + + TestJSONRPCConnection conn; + + bool callback_called = false; + conn.testInjectPendingRequest( + "req_1", + LLTimer::getTotalSeconds() - 1.0, + [&](const LLSD& result, const LLSD& error) + { + callback_called = true; + ensure("timed out result should be undefined", result.isUndefined()); + ensure_equals( + "timeout code", + error["code"].asInteger(), + LLJSONRPCConnection::RPCError::REQUEST_TIMEOUT); + }); + + ensure_equals("pending request should be tracked", conn.testPendingRequestCount(), (size_t)1); + conn.testSweepTimeouts(); + ensure("timeout callback should be called", callback_called); + ensure_equals("pending request should be removed", conn.testPendingRequestCount(), (size_t)0); + } +} + +#endif
\ No newline at end of file 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<RuntimeEventAggregator>( + [this](const RuntimeEventAggregator::RuntimeEvent& runtime_event) + { + std::optional<RuntimeChatEvent> event = + buildRuntimeChatEvent(runtime_event); + if (event && mRuntimeEventCallback) + { + mRuntimeEventCallback(*event); + } + })), + mRuntimeFlushTimer( + std::unique_ptr<LLEventTimer>( + 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<std::string> 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<RuntimeChatEvent> 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<std::string> 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<LLInventoryItem*>(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::RuntimeChatEvent> +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<std::string> 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<LLInventoryItem*>(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<LLInventoryItem*>(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<PendingBurst>(); + 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<std::string> 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<S32>( + std::strtol(match[1].str().c_str(), nullptr, 10)) + 1; + parsed.mColumn = static_cast<S32>( + 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<S32>( + 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<std::string> 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 <map> +#include <functional> #include <memory> +#include <optional> #include <set> #include <string> #include <vector> 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<StackFrame> mStack; + }; + + struct Fragment + { + LLUUID mFromID; + std::string mFromName; + std::string mText; + }; + + using fragments_t = std::vector<Fragment>; + + struct RuntimeEvent + { + VM mVM; + Channel mChannel; + fragments_t mFragments; + ParsedError mError; + }; + + using FlushCallback = std::function<void(const RuntimeEvent&)>; + + 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<PendingBurst> 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<std::string> mStack; + bool mIsError{ false }; + }; + + using RuntimeEventCallback = + std::function<void(const RuntimeChatEvent&)>; + + explicit LLPublishedObjectMgr( + LLScriptEditorWSServer* server = nullptr, + RuntimeEventCallback runtime_event_callback = {}); ~LLPublishedObjectMgr(); + void flushExpiredRuntimeFragments(); + void ingestRuntimeChat( + const LLChat& chat_msg, + RuntimeEventAggregator::Channel channel); + std::optional<RuntimeChatEvent> 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<RuntimeEventAggregator> mRuntimeEventAggregator; + RuntimeEventCallback mRuntimeEventCallback; + std::unique_ptr<LLEventTimer> 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..f886be819f 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 <array> + 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 "<prefix>.<uuid>", 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<char, MD5HEX_STR_SIZE> 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,51 @@ 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(); + + const bool is_lua = + compile_target == "luau" || + compile_target == "lsl-luau"; + + for (const auto& error : llsd::inArray(cb_result["errors"])) + { + boost::smatch match; + LLSD diagnostic; + diagnostic["level"] = "ERROR"; + + if (is_lua && + boost::regex_match( + error.asString(), + match, + LUAU_LOCATION_PATTERN)) + { + diagnostic["row"] = std::stoi(match[2].str()); + diagnostic["column"] = 0; + diagnostic["message"] = match[3].str(); + } + else if (!is_lua && + boost::regex_match( + error.asString(), + match, + LSL_LOCATION_PATTERN)) + { + diagnostic["row"] = + std::stoi(match[1].str()) + 1; + diagnostic["column"] = + std::stoi(match[2].str()) + 1; + diagnostic["level"] = match[3].str(); + diagnostic["message"] = match[4].str(); + diagnostic["format"] = "lsl"; + } + else + { + diagnostic["row"] = 0; + diagnostic["column"] = 0; + diagnostic["message"] = error.asString(); + } + + response["diagnostics"].append(diagnostic); + } } // If the script is open in the viewer's editor, update it @@ -1934,12 +1988,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 +2000,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 +2013,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 +2044,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 +2052,72 @@ 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)) - { - tracking = true; - publish = true; - } - else + std::string script_id; + if (event.mItemID.notNull()) { - // 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 + if (!isObjectPublished(event.mRootID) && + (script_id.empty() || mSubscriptions.find(script_id) == mSubscriptions.end())) + { 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<std::string> lines = LLStringUtil::getTokens(chat_msg.mText, "\n"); - // If this is a runtime error, the first line will look like: "<Object Name> [script:<Script Name>] Script run-time error" - static const std::string runtime_error_marker = "Script run-time error"; - auto ends_with = [](const std::string& s, const std::string& suffix) - { - return s.size() >= suffix.size() && - std::equal(suffix.rbegin(), suffix.rend(), s.rbegin()); - }; - - if (!lines.empty() && ends_with(lines.front(), runtime_error_marker)) - { - is_error = true; - std::string first_line = lines.front(); - - // Extract the object and script name from the first line - static const boost::regex RUNTIME_ERR_REGEX_FLEX(R"(^(.+?)\s+\[script:([^\]]+)\]\s+Script run-time error)"); - boost::smatch m; - - S32 remove_count = 0; - if (boost::regex_match(first_line, m, RUNTIME_ERR_REGEX_FLEX)) - { - object_name = m[1].str(); - script_name = m[2].str(); - remove_count++; - } + LLSD message; + message["object_id"] = event.mRootID; + message["prim_id"] = event.mPrimID; + message["item_id"] = event.mItemID; + message["object_name"] = event.mObjectName; + message["message"] = event.mMessage; - // TODO: Build an actual error message to forward to the external editor. - // The complete error message arrives as two or three separate chat - // messages from the server (2 for LSL / non-owner Lua, 3 for owner Lua): - // Message 1: <Object Name> [script:<Script Name>] Script run-time error - // Message 2: <runtime error> - // Message 3: <script>:<line>: <actual error message>\n<call stack> - // These need to be composited into a single error message for the IDE. - if (lines.size() > remove_count) - { // The rest of the lines may contain a stack trace - lines.erase(lines.begin(), lines.begin() + remove_count); - } - else - { - lines.clear(); - } + LLSD item; + item["root_id"] = event.mRootID; + item["prim_id"] = event.mPrimID; + item["item_id"] = event.mItemID; + item["name"] = event.mScriptName; + item["language"] = + event.mVM == LLPublishedObjectMgr::RuntimeEventAggregator::VM::LUAU + ? "luau" + : "lsl"; + message["item"] = item; - if (subinfo) - { - // We should also check that the script name matches one of our subscriptions - if (!script_name.empty() && (subinfo->mScriptName != script_name)) - { // right object, wrong script - auto sit = - std::find_if(mSubscriptions.begin(), mSubscriptions.end(), [&chat_msg, &script_name](const auto& pair) - { return (pair.second.mScriptName == script_name) && (pair.second.mObjectID == chat_msg.mFromID); }); - if (sit != mSubscriptions.end()) - { // We have a better match - subinfo = &sit->second; - script_id = sit->first; - } - } - } + switch (event.mChannel) + { + case LLPublishedObjectMgr::RuntimeEventAggregator::Channel::DEBUG: + message["channel"] = "debug"; + break; + case LLPublishedObjectMgr::RuntimeEventAggregator::Channel::OWNER_SAY: + message["channel"] = "owner_say"; + break; } - LLSD message; - message["script_id"] = script_id; - message["object_id"] = object_id; - message["object_name"] = object_name; - message["message"] = chat_msg.mText; - - if (is_error) + if (event.mIsError) { - message["error"] = error_message; - message["line"] = line_number; - if (!lines.empty()) + message["error"] = event.mError; + message["line"] = event.mLine; + message["column"] = event.mColumn; + message["stack"] = LLSD::emptyArray(); + for (const auto& line : event.mStack) { - message["stack"] = LLSD::emptyArray(); - for (const auto& line : lines) - { - message["stack"].append(line); - } + message["stack"].append(line); } } - notifyAll(is_error ? "runtime.error" : "runtime.debug", message); + notifyAll(event.mIsError ? "runtime.error" : "runtime.debug", message); } void LLScriptEditorWSServer::notifyConnection(U32 connection_id, const std::string& method, const LLSD& params) const @@ -2172,15 +2156,6 @@ void LLScriptEditorWSServer::notifyAll(const std::string& method, const LLSD& pa // static -LLSD LLScriptEditorWSServer::errorResponse(const std::string& message) -{ - LLSD response; - response["success"] = false; - response["message"] = message; - return response; -} - -// static std::string LLScriptEditorWSServer::getPrimName(LLViewerObject* obj) { std::string name = nv_string(obj, "Name"); @@ -2599,6 +2574,7 @@ void LLScriptEditorWSConnection::onOpen() features["compilation"] = true; features["syntax_cache"] = true; features["commands"] = true; + features["unified_diagnostics"] = true; handshake["features"] = features; wptr_t that = weak_from_this(); diff --git a/indra/newview/llscripteditorws.h b/indra/newview/llscripteditorws.h index ded83e6369..1dc218853b 100644 --- a/indra/newview/llscripteditorws.h +++ b/indra/newview/llscripteditorws.h @@ -36,6 +36,7 @@ #include <memory> #include <string> +#include <vector> #include <map> #include <set> #include <atomic> @@ -157,6 +158,14 @@ private: class LLScriptEditorWSServer : public LLJSONRPCServer { public: + struct ItemRef + { + LLUUID mRootID; + LLUUID mPrimID; + LLUUID mItemID; + std::string mScriptName; + }; + static constexpr U32 ALL_CONNECTIONS = 0xFFFFFFFF; enum class SubscriptionError { @@ -179,6 +188,8 @@ public: static LLScriptEditorWSServer::ptr_t getServer(); static LLScriptEditorWSServer::ptr_t ensureServerRunning(); + static std::string buildScriptSubscriptionId(const LLUUID& object_id, + const LLUUID& item_id); static std::string buildVSCodeURI(const LLUUID& object_id = LLUUID::null, const LLUUID& script_id = LLUUID::null); static bool launchVSCode(const LLUUID& object_id = LLUUID::null, @@ -199,7 +210,9 @@ public: LLHandle<LLPanel> findEditorForScript(const std::string& script_id) const; - void forwardChatToIDE(const LLChat& chat_msg) const; + void forwardChatToIDE( + const LLChat& chat_msg, + LLPublishedObjectMgr::RuntimeEventAggregator::Channel channel) const; std::set<std::string> getActiveScripts() const; @@ -268,7 +281,6 @@ protected: void scheduleLinksetFlush(const LLUUID& root_id, F32 delay); void cancelLinksetFlushTimer(const LLUUID& root_id); void flushLinksetUpdate(const LLUUID& root_id); - static LLSD errorResponse(const std::string& message); /// Wraps `fn` in a MethodHandler with a weak-ptr guard on this server, /// so the handler safely no-ops after server shutdown. `fn` is called @@ -291,18 +303,18 @@ protected: } private: + void sendRuntimeEvent( + const LLPublishedObjectMgr::RuntimeChatEvent& event) const; + struct EditorSubscription { - EditorSubscription(const LLUUID &object_id, const LLUUID &item_id, std::string_view script_name, LLHandle<LLPanel> editor_handle): - mObjectID(object_id), - mItemID(item_id), - mScriptName(script_name), + EditorSubscription(const ItemRef& item_ref, LLHandle<LLPanel> editor_handle): + mItemRef(item_ref), mEditorHandle(editor_handle) - {} + { + } U32 mConnectionID{ 0 }; - LLUUID mObjectID; - LLUUID mItemID; - std::string mScriptName; + ItemRef mItemRef; LLScriptEditorWSConnection::wptr_t mConnection; LLHandle<LLPanel> mEditorHandle; }; @@ -312,10 +324,6 @@ private: void unsubscribeConnection(U32 connection_id); subscriptions_t mSubscriptions; - // Per-connection subscription count. Invariant: for c != 0, - // mConnectionSubscriptionCounts[c] == count of entries in mSubscriptions - // whose mConnectionID == c. Maintained transactionally at every site that - // mutates SubscriptionInfo::mConnectionID. std::unordered_map<U32, S32> mConnectionSubscriptionCounts; std::map<U32, LLScriptEditorWSConnection::wptr_t> mActiveConnections; @@ -341,9 +349,4 @@ private: boost::signals2::connection mLanguageChangeSignal; LLUUID mLastSyntaxId; - - LLTimer mCleanupTimer; - static constexpr F32 CLEANUP_INTERVAL = 60.0f; // seconds - static constexpr F32 CONNECTION_TIMEOUT = 300.0f; // 5 minutes - }; |
