summaryrefslogtreecommitdiff
path: root/indra/newview/llscripteditorws.cpp
diff options
context:
space:
mode:
authorJonathan "Geenz" Goodman <geenz@lindenlab.com>2026-09-22 17:35:03 -0400
committerGitHub <noreply@github.com>2026-09-22 17:35:03 -0400
commitf3fba78d9185a2385cc6c5226b7a87fddff826dd (patch)
tree4d04ad5876c69ec0b3203dec69c711161fa7f934 /indra/newview/llscripteditorws.cpp
parentcc5d3dd3cd653971fff16cf2bc1d380edd5ea7f2 (diff)
parent125dfba5f94191fc99d1c511d190d6a9f2d9d2f1 (diff)
Merge pull request #6357 from secondlife/pepper/chat-merge-26.5
Merge `release/26.5` into `project/chat-modernization`
Diffstat (limited to 'indra/newview/llscripteditorws.cpp')
-rw-r--r--indra/newview/llscripteditorws.cpp654
1 files changed, 433 insertions, 221 deletions
diff --git a/indra/newview/llscripteditorws.cpp b/indra/newview/llscripteditorws.cpp
index 0487ebd06d..93b1bd5eb0 100644
--- a/indra/newview/llscripteditorws.cpp
+++ b/indra/newview/llscripteditorws.cpp
@@ -34,6 +34,7 @@
#include "llagentcamera.h"
#include "llappviewer.h"
#include "llchat.h"
+#include "llcompilequeue.h"
#include "lldate.h"
#include "llerror.h"
#include "lleventcoro.h"
@@ -50,6 +51,7 @@
#include "llpreviewscript.h"
#include "llprocess.h"
#include "llregex.h"
+#include "llmd5.h"
#include "llsdjson.h"
#include "llselectmgr.h"
#include "lltrans.h"
@@ -68,6 +70,8 @@
#include "llvoinventorylistener.h"
#include "roles_constants.h"
+#include <array>
+
namespace
{
// Per-operation timeouts (seconds) for coroutine-based async RPC handlers.
@@ -80,6 +84,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.
@@ -160,17 +170,31 @@ namespace
return std::string(s);
}
+ LLSD object_id_command_params()
+ {
+ LLSD params(LLSD::emptyMap());
+ params["object_id"]["type"] = "string";
+ params["object_id"]["required"] = true;
+ return params;
+ }
+
}
//========================================================================
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;
- registerCommand({ "viewer.teleport", "Teleport agent to an in-world object" },
+ registerCommand({ "viewer.teleport", "Teleport agent to an in-world object",
+ object_id_command_params() },
[](U32, const LLSD& p) -> LLSD
{
LLUUID object_id = p["object_id"].asUUID();
@@ -189,7 +213,9 @@ LLScriptEditorWSServer::LLScriptEditorWSServer(const std::string& name, U16 port
return response;
});
- registerCommand({ "viewer.camera.focus", "Zoom camera to an in-world object (same behavior as context menu Zoom In)" },
+ registerCommand({ "viewer.camera.focus",
+ "Zoom camera to an in-world object (same behavior as context menu Zoom In)",
+ object_id_command_params() },
[](U32, const LLSD& p) -> LLSD
{
LLUUID object_id = p["object_id"].asUUID();
@@ -198,11 +224,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;
@@ -210,11 +233,36 @@ LLScriptEditorWSServer::LLScriptEditorWSServer(const std::string& name, U16 port
return response;
});
- registerCommand({ "viewer.object.save_back_to_contents", "Save an in-world object back to source object contents" },
+ registerCommand({ "viewer.object.save_back_to_contents",
+ "Save an in-world object back to source object contents",
+ object_id_command_params() },
[this](U32 connection_id, const LLSD& p) -> LLSD
{
return this->handleSaveBackToObjectContents(connection_id, p);
});
+
+ registerCommand({ "viewer.script.reset_all",
+ "Reset all scripts in an in-world object",
+ object_id_command_params() },
+ [this](U32 connection_id, const LLSD& p) -> LLSD
+ {
+ return this->handleObjectScriptResetAll(connection_id, p);
+ });
+
+ registerCommand({ "viewer.script.recompile_all",
+ "Recompile all scripts in an in-world object",
+ LLSDMap("object_id",
+ LLSDMap("type", "string")
+ ("required", true))
+ ("target",
+ LLSDMap("type", "string")
+ ("required", true)
+ ("description",
+ "Compilation target: luau, lsl2, mono, or auto")) },
+ [this](U32 connection_id, const LLSD& p) -> LLSD
+ {
+ return this->handleObjectScriptRecompileAll(connection_id, p);
+ });
}
LLScriptEditorWSServer::ptr_t LLScriptEditorWSServer::getServer()
@@ -252,6 +300,13 @@ LLScriptEditorWSServer::ptr_t LLScriptEditorWSServer::ensureServerRunning()
ptr_t server = std::static_pointer_cast<LLScriptEditorWSServer>(
wsmgr.findServerByName(DEFAULT_SERVER_NAME));
+ if (server && !server->isRunning())
+ {
+ // thread stopped/was joined
+ wsmgr.removeServer(DEFAULT_SERVER_NAME);
+ server.reset();
+ }
+
if (!server)
{
U16 port = static_cast<U16>(gSavedSettings.getS32("ExternalWebsocketSyncPort"));
@@ -279,6 +334,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 +498,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 +522,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 +530,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 +653,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 +665,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);
@@ -789,6 +845,146 @@ LLSD LLScriptEditorWSServer::handleObjectScriptReset(U32 connection_id, const LL
return response;
}
+LLSD LLScriptEditorWSServer::handleObjectScriptResetAll(U32 connection_id, const LLSD& params)
+{
+ LLUUID prim_id = params["object_id"].asUUID();
+ if (prim_id.isNull())
+ {
+ throw LLJSONRPCConnection::InvalidParams("object_id is required");
+ }
+
+ LLViewerObject* prim = gObjectList.findObject(prim_id);
+ if (!prim)
+ {
+ throw LLJSONRPCConnection::InvalidParams("Object not found");
+ }
+
+ LLViewerObject* root = prim->getRootEdit();
+ if (!root || !isObjectPublished(root->getID()))
+ {
+ throw LLJSONRPCConnection::ForbiddenError("Object is not published");
+ }
+
+ if (!prim->flagScripted())
+ {
+ throw LLJSONRPCConnection::InvalidParams(
+ "Prim contains no scripts");
+ }
+
+ if (!prim->permModify())
+ {
+ throw LLJSONRPCConnection::ForbiddenError(
+ "No modify permission on prim");
+ }
+
+ LLUUID queue_id;
+ queue_id.generate();
+
+ LLFloaterScriptQueue* queue =
+ LLFloaterReg::getTypedInstance<LLFloaterScriptQueue>(
+ "reset_queue", LLSD(queue_id));
+ if (!queue)
+ {
+ throw LLJSONRPCConnection::InternalError(
+ "Unable to open reset queue");
+ }
+
+ queue->addObject(prim->getID(), prim->getID().asString());
+ if (!queue->start())
+ {
+ queue->closeFloater();
+ throw LLJSONRPCConnection::InternalError(
+ "Unable to start reset queue");
+ }
+
+ queue->setTitle(LLTrans::getString("ResetQueueTitle"));
+
+ LLSD response;
+ response["success"] = true;
+ response["object_id"] = prim->getID();
+ response["queued"] = true;
+ return response;
+}
+
+LLSD LLScriptEditorWSServer::handleObjectScriptRecompileAll(
+ U32 connection_id, const LLSD& params)
+{
+ LLUUID object_id = params["object_id"].asUUID();
+ if (object_id.isNull())
+ {
+ throw LLJSONRPCConnection::InvalidParams("object_id is required");
+ }
+
+ std::string target = params["target"].asString();
+ if (target != "luau" &&
+ target != "lsl2" &&
+ target != "mono" &&
+ target != "auto")
+ {
+ throw LLJSONRPCConnection::InvalidParams(
+ "target must be 'luau', 'lsl2', 'mono', or 'auto'");
+ }
+
+ LLViewerObject* object = gObjectList.findObject(object_id);
+ if (!object)
+ {
+ throw LLJSONRPCConnection::InvalidParams("Object not found");
+ }
+
+ LLViewerObject* root = object->getRootEdit();
+ if (!root || root->getID() != object_id || !isObjectPublished(root->getID()))
+ {
+ throw LLJSONRPCConnection::ForbiddenError("Object is not published");
+ }
+
+ if (!root->flagScripted())
+ {
+ throw LLJSONRPCConnection::InvalidParams(
+ "Object contains no scripts");
+ }
+
+ if (!root->permModify())
+ {
+ throw LLJSONRPCConnection::ForbiddenError(
+ "No modify permission on object");
+ }
+
+ if (target == "luau")
+ {
+ target = "auto-luau";
+ }
+
+ LLUUID queue_id;
+ queue_id.generate();
+
+ LLFloaterCompileQueue* queue =
+ LLFloaterReg::getTypedInstance<LLFloaterCompileQueue>(
+ "compile_queue", LLSD(queue_id));
+ if (!queue)
+ {
+ throw LLJSONRPCConnection::InternalError(
+ "Unable to open compile queue");
+ }
+
+ queue->setCompileTarget(target);
+ queue->addObject(root->getID(), root->getID().asString());
+ if (!queue->start())
+ {
+ queue->closeFloater();
+ throw LLJSONRPCConnection::InternalError(
+ "Unable to start compile queue");
+ }
+
+ queue->setTitle(LLTrans::getString("CompileQueueTitle"));
+
+ LLSD response;
+ response["success"] = true;
+ response["object_id"] = root->getID();
+ response["target"] = (target == "auto-luau") ? "luau" : target;
+ response["queued"] = true;
+ return response;
+}
+
LLSD LLScriptEditorWSServer::handleObjectModify(U32 connection_id, const LLSD& params)
{
// Step 1: Parameter Validation
@@ -946,39 +1142,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 +1188,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"]);
@@ -1022,6 +1203,10 @@ LLSD LLScriptEditorWSServer::handleCommandList()
LLSD info;
info["command"] = entry.first.command;
info["description"] = entry.first.description;
+ if (!entry.first.params.isUndefined())
+ {
+ info["params"] = entry.first.params;
+ }
commands.append(info);
}
LLSD response;
@@ -1073,6 +1258,33 @@ void LLScriptEditorWSServer::broadcastLanguageChange()
}
}
+LLSD LLScriptEditorWSServer::handlePing(
+ const LLJSONRPCConnection::ptr_t& connection,
+ const LLSD& params) const
+{
+ LLSD result;
+ result["pong"] = "pong";
+
+ if (params.has("timestamp"))
+ {
+ result["timestamp"] = params["timestamp"];
+ }
+
+ result["server_time"] = static_cast<LLSD::Integer>(
+ LLDate::now().secondsSinceEpoch() * 1000.0);
+ return result;
+}
+
+LLSD LLScriptEditorWSServer::handleGetVersion(
+ const LLJSONRPCConnection::ptr_t& connection,
+ const LLSD& params) const
+{
+ LLSD result;
+ result["client_name"] = LLVersionInfo::instance().getChannel();
+ result["client_version"] = LLVersionInfo::instance().getVersion();
+ return result;
+}
+
LLSD LLScriptEditorWSServer::handleLanguageIdRequest() const
{
LLSD response;
@@ -1088,27 +1300,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 +1353,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 +1380,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 +1431,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 +1491,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;
}
@@ -1446,6 +1671,7 @@ LLSD LLScriptEditorWSServer::handleObjectContentGet(const std::string& method, c
response["prim_id"] = prim_id;
response["item_id"] = item_id;
response["content"] = text_content;
+ response["encoding"] = "utf-8";
return response;
}
@@ -1513,7 +1739,13 @@ LLSD LLScriptEditorWSServer::saveScript(LLViewerObject* prim, LLInventoryItem* i
[&, prim_id, item_id](const std::string& pump_name)
{
auto [on_success, on_failure] = make_asset_upload_callbacks(pump_name);
- bool is_running = params.has("running") ? params["running"].asBoolean() : false;
+ const LLViewerInventoryItem* viewer_item =
+ dynamic_cast<const LLViewerInventoryItem*>(item);
+ bool is_running = viewer_item ? viewer_item->getIsRunning() : false;
+ if (params.has("running"))
+ {
+ is_running = params["running"].asBoolean();
+ }
LLResourceUploadInfo::ptr_t uploadInfo(std::make_shared<LLScriptAssetUpload>(
prim_id, item_id,
compile_target, is_running, LLUUID::null, content,
@@ -1531,7 +1763,56 @@ 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";
+ S32 line_number = 0;
+ S32 col_number = 0;
+
+ if (is_lua &&
+ boost::regex_match(
+ error.asString(),
+ match,
+ LUAU_LOCATION_PATTERN) &&
+ LLStringUtil::convertToS32(match[2].str(), line_number))
+ {
+ diagnostic["row"] = line_number;
+ diagnostic["column"] = 0;
+ diagnostic["message"] = match[3].str();
+ }
+ else if (!is_lua &&
+ boost::regex_match(
+ error.asString(),
+ match,
+ LSL_LOCATION_PATTERN) &&
+ LLStringUtil::convertToS32(match[1].str(), line_number) &&
+ LLStringUtil::convertToS32(match[2].str(), col_number) &&
+ line_number < S32_MAX &&
+ col_number < S32_MAX)
+ {
+ diagnostic["row"] = line_number + 1;
+ diagnostic["column"] = col_number + 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 +2215,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 +2227,11 @@ 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))
+ S32 line_number = 0;
+ if (boost::regex_match(err.asString(), match, LUAU_LOCATION_PATTERN) &&
+ LLStringUtil::convertToS32(match[2].str(), line_number))
{
- S32 line_number = std::stoi(match[1].str());
- std::string message = match[2].str();
+ std::string message = match[3].str();
err_entry["row"] = line_number;
err_entry["message"] = message;
@@ -1961,22 +2241,24 @@ 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))
+ S32 line_number = 0;
+ S32 col_number = 0;
+ if (boost::regex_match(err.asString(), match, LSL_LOCATION_PATTERN) &&
+ LLStringUtil::convertToS32(match[1].str(), line_number) &&
+ LLStringUtil::convertToS32(match[2].str(), col_number) &&
+ line_number < S32_MAX &&
+ col_number < S32_MAX)
{
- S32 line_number = std::stoi(match[1].str());
- S32 col_number = std::stoi(match[2].str());
std::string severity = match[3].str();
std::string message = match[4].str();
@@ -1994,7 +2276,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 +2284,76 @@ 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))
+ LLSD message;
+ if (!script_id.empty())
{
- 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++;
- }
+ message["script_id"] = script_id;
+ }
+ 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 +2392,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 +2810,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();