summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorRider Linden <rider@lindenlab.com>2026-08-19 13:53:16 -0700
committerGitHub <noreply@github.com>2026-08-19 13:53:16 -0700
commitf26e024aee3957193a3bd7ffdd614c004c243eaf (patch)
tree30c12980f211cb46ce4ea21c09f9b0bdfec3e230
parent305bebbdb3b16b10b6011b5b3b5d63ee7c7334be (diff)
parentece8b732d963afe257b4ba7396c9deb340dbf47c (diff)
Merge pull request #6156 from secondlife/rider/runtime_msging
Rider/runtime msging
-rw-r--r--doc/external-editor-json-rpc.md38
-rw-r--r--indra/llcorehttp/CMakeLists.txt1
-rw-r--r--indra/llcorehttp/lljsonrpcws.cpp18
-rw-r--r--indra/llcorehttp/lljsonrpcws.h5
-rw-r--r--indra/llcorehttp/tests/llcorehttp_test.cpp1
-rw-r--r--indra/llcorehttp/tests/test_jsonrpcws.hpp146
-rw-r--r--indra/newview/llfloaterimnearbychathandler.cpp36
-rw-r--r--indra/newview/llpreviewscript.cpp10
-rw-r--r--indra/newview/llpublishedobjectmgr.cpp381
-rw-r--r--indra/newview/llpublishedobjectmgr.h127
-rw-r--r--indra/newview/llscripteditorws.cpp408
-rw-r--r--indra/newview/llscripteditorws.h41
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
-
};