summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorRider Linden <rider@lindenlab.com>2025-08-26 21:29:41 -0700
committerRider Linden <rider@lindenlab.com>2025-10-07 09:19:36 -0700
commitf31c194b8674df295edf74693a2f843ca314ca92 (patch)
tree936a587328d122e0bd12f811a2c310d23ec872b8
parent7b174ab2a7c54d3e25a56530a2179848db2d46c0 (diff)
Expand the websocket implementation a bit. Added JSONRPC specialization.
-rw-r--r--indra/llcorehttp/CMakeLists.txt2
-rw-r--r--indra/llcorehttp/lljsonrpcws.cpp613
-rw-r--r--indra/llcorehttp/lljsonrpcws.h461
-rw-r--r--indra/llcorehttp/llwebsocketmgr.cpp35
-rw-r--r--indra/llcorehttp/llwebsocketmgr.h10
-rw-r--r--indra/newview/llappviewer.cpp6
-rw-r--r--indra/newview/llpreviewscript.cpp71
-rw-r--r--indra/newview/llpreviewscript.h17
-rw-r--r--indra/newview/llscripteditorws.cpp692
-rw-r--r--indra/newview/llscripteditorws.h148
10 files changed, 1517 insertions, 538 deletions
diff --git a/indra/llcorehttp/CMakeLists.txt b/indra/llcorehttp/CMakeLists.txt
index d8d5c577d5..fa37d23126 100644
--- a/indra/llcorehttp/CMakeLists.txt
+++ b/indra/llcorehttp/CMakeLists.txt
@@ -25,6 +25,7 @@ set(llcorehttp_SOURCE_FILES
httpresponse.cpp
httpstats.cpp
llwebsocketmgr.cpp
+ lljsonrpcws.cpp
_httplibcurl.cpp
_httpopcancel.cpp
_httpoperation.cpp
@@ -54,6 +55,7 @@ set(llcorehttp_HEADER_FILES
httpresponse.h
httpstats.h
llwebsocketmgr.h
+ lljsonrpcws.h
_httpinternal.h
_httplibcurl.h
_httpopcancel.h
diff --git a/indra/llcorehttp/lljsonrpcws.cpp b/indra/llcorehttp/lljsonrpcws.cpp
new file mode 100644
index 0000000000..5de595c595
--- /dev/null
+++ b/indra/llcorehttp/lljsonrpcws.cpp
@@ -0,0 +1,613 @@
+/**
+ * @file lljsonrpcws.cpp
+ * @brief JSON-RPC 2.0 WebSocket server and connection implementation
+ *
+ * $LicenseInfo:firstyear=2025&license=viewerlgpl$
+ * Second Life Viewer Source Code
+ * Copyright (C) 2025, Linden Research, Inc.
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation;
+ * version 2.1 of the License only.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ *
+ * Linden Research, Inc., 945 Battery Street, San Francisco, CA 94111 USA
+ * $/LicenseInfo$
+ */
+
+#include "linden_common.h"
+
+#include "lljsonrpcws.h"
+#include "llerror.h"
+#include "llsdjson.h"
+#include "lldate.h"
+
+#include <boost/json.hpp>
+
+//========================================================================
+// LLJSONRPCConnection Implementation
+//========================================================================
+
+void LLJSONRPCConnection::onOpen()
+{
+ LL_INFOS("JSONRPC") << "JSON-RPC connection opened" << LL_ENDL;
+}
+
+void LLJSONRPCConnection::onClose()
+{
+ LL_INFOS("JSONRPC") << "JSON-RPC connection closed, clearing "
+ << mPendingRequests.size() << " pending requests" << LL_ENDL;
+
+ // Cancel all pending requests
+ for (auto& [id, callback] : mPendingRequests)
+ {
+ if (callback)
+ {
+ LLSD error;
+ error["code"] = RPCError::CONNECTION_CLOSED; // Use named constant instead of magic number
+ error["message"] = "Connection closed";
+ callback(LLSD(), error);
+ }
+ }
+ mPendingRequests.clear();
+}
+
+void LLJSONRPCConnection::onMessage(const std::string& message)
+{
+ LL_DEBUGS("JSONRPC") << "Received JSON-RPC message: " << message << LL_ENDL;
+
+ try
+ {
+ // Parse JSON message
+ boost::system::error_code ec;
+ boost::json::value json_value = boost::json::parse(message, ec);
+
+ if (ec.failed())
+ {
+ LL_WARNS("JSONRPC") << "Failed to parse JSON: " << ec.message() << LL_ENDL;
+ sendError(LLSD(), ParseError(ec.message()));
+ return;
+ }
+
+ // Convert to LLSD
+ LLSD message_obj = LlsdFromJson(json_value);
+
+ // Handle batch vs single message
+ if (message_obj.isArray())
+ {
+ // Batch request
+ if (message_obj.size() == 0)
+ {
+ sendError(LLSD(), InvalidRequest("Empty batch"));
+ return;
+ }
+
+ // Process each message in the batch
+ for (S32 i = 0; i < message_obj.size(); ++i)
+ {
+ processMessage(message_obj[i]);
+ }
+ }
+ else
+ {
+ // Single message
+ processMessage(message_obj);
+ }
+ }
+ catch (const std::exception& e)
+ {
+ LL_WARNS("JSONRPC") << "Exception processing JSON-RPC message: " << e.what() << LL_ENDL;
+ sendError(LLSD(), InternalError(e.what()));
+ }
+}
+
+void LLJSONRPCConnection::processMessage(const LLSD& message_obj)
+{
+ try
+ {
+ // Determine if this is a request, notification, or response
+ if (message_obj.has("method"))
+ {
+ // This is a request or notification
+ validateMessage(message_obj, true);
+ processRequest(message_obj);
+ }
+ else if (message_obj.has("result") || message_obj.has("error"))
+ {
+ // This is a response
+ validateMessage(message_obj, false);
+ processResponse(message_obj);
+ }
+ else
+ {
+ throw InvalidRequest("Message must contain 'method' or 'result'/'error'");
+ }
+ }
+ catch (const RPCError& e)
+ {
+ LLSD id = message_obj.has("id") ? message_obj["id"] : LLSD();
+ sendError(id, e);
+ }
+}
+
+void LLJSONRPCConnection::processRequest(const LLSD& request)
+{
+ std::string method = request["method"].asString();
+ LLSD params = request.has("params") ? request["params"] : LLSD();
+ LLSD id = request.has("id") ? request["id"] : LLSD();
+ bool is_notification = !request.has("id");
+
+ LL_DEBUGS("JSONRPC") << "Processing " << (is_notification ? "notification" : "request")
+ << " for method: " << method << LL_ENDL;
+
+ // Find method handler
+ auto it = mMethodHandlers.find(method);
+ if (it == mMethodHandlers.end())
+ {
+ if (!is_notification)
+ {
+ sendError(id, MethodNotFound(method));
+ }
+ return;
+ }
+
+ try
+ {
+ // Call the method handler with method name, ID, and parameters
+ LLSD result = it->second(method, id, params);
+
+ // Send response (only for requests, not notifications)
+ if (!is_notification)
+ {
+ sendResponse(id, result);
+ }
+ }
+ catch (const RPCError& e)
+ {
+ if (!is_notification)
+ {
+ sendError(id, e);
+ }
+ else
+ {
+ LL_WARNS("JSONRPC") << "Error in notification handler for " << method
+ << ": " << e.what() << LL_ENDL;
+ }
+ }
+ catch (const std::exception& e)
+ {
+ if (!is_notification)
+ {
+ sendError(id, InternalError(e.what()));
+ }
+ else
+ {
+ LL_WARNS("JSONRPC") << "Exception in notification handler for " << method
+ << ": " << e.what() << LL_ENDL;
+ }
+ }
+}
+
+void LLJSONRPCConnection::processResponse(const LLSD& response)
+{
+ if (!response.has("id"))
+ {
+ LL_WARNS("JSONRPC") << "Response missing id field" << LL_ENDL;
+ return;
+ }
+
+ std::string id = response["id"].asString();
+ auto it = mPendingRequests.find(id);
+ if (it == mPendingRequests.end())
+ {
+ LL_WARNS("JSONRPC") << "Received response for unknown request id: " << id << LL_ENDL;
+ return;
+ }
+
+ ResponseCallback callback = it->second;
+ mPendingRequests.erase(it);
+
+ if (callback)
+ {
+ LLSD result = response.has("result") ? response["result"] : LLSD();
+ LLSD error = response.has("error") ? response["error"] : LLSD();
+
+ callback(result, error);
+ }
+}
+
+void LLJSONRPCConnection::validateMessage(const LLSD& message, bool is_request)
+{
+ // Check JSON-RPC version
+ if (!message.has("jsonrpc") || message["jsonrpc"].asString() != "2.0")
+ {
+ throw InvalidRequest("Missing or invalid jsonrpc version");
+ }
+
+ if (is_request)
+ {
+ // Request/notification validation
+ if (!message.has("method"))
+ {
+ throw InvalidRequest("Missing method field");
+ }
+
+ if (!message["method"].isString())
+ {
+ throw InvalidRequest("Method must be a string");
+ }
+
+ // Params are optional but must be array or object if present
+ if (message.has("params"))
+ {
+ if (!message["params"].isArray() && !message["params"].isMap())
+ {
+ throw InvalidParams("Params must be array or object");
+ }
+ }
+ }
+ else
+ {
+ // Response validation
+ if (!message.has("id"))
+ {
+ throw InvalidRequest("Response missing id field");
+ }
+
+ // Must have either result or error, but not both
+ bool has_result = message.has("result");
+ bool has_error = message.has("error");
+
+ if (!has_result && !has_error)
+ {
+ throw InvalidRequest("Response must have result or error");
+ }
+
+ if (has_result && has_error)
+ {
+ throw InvalidRequest("Response cannot have both result and error");
+ }
+
+ // Error must be an object with code and message
+ if (has_error)
+ {
+ LLSD error = message["error"];
+ if (!error.isMap())
+ {
+ throw InvalidRequest("Error must be an object");
+ }
+ if (!error.has("code") || !error.has("message"))
+ {
+ throw InvalidRequest("Error must have code and message");
+ }
+ }
+ }
+}
+
+LLSD LLJSONRPCConnection::generateId()
+{
+ // Server-wide atomic counter for efficient unique ID generation
+ // Start from 1000 to avoid conflicts with any manual test IDs
+ static std::atomic<U64> sRequestIdCounter{1000};
+
+ // Generate server-unique sequential ID
+ U64 id = sRequestIdCounter.fetch_add(1);
+ return LLSD(llformat("rpc_%llu", id));
+}
+
+void LLJSONRPCConnection::registerMethod(const std::string& method, MethodHandler handler)
+{
+ mMethodHandlers[method] = handler;
+ LL_INFOS("JSONRPC") << "Registered method: " << method << LL_ENDL;
+}
+
+void LLJSONRPCConnection::unregisterMethod(const std::string& method)
+{
+ mMethodHandlers.erase(method);
+ LL_INFOS("JSONRPC") << "Unregistered method: " << method << LL_ENDL;
+}
+
+LLSD LLJSONRPCConnection::call(const std::string& method, const LLSD& params, ResponseCallback callback)
+{
+ LLSD request;
+ request["jsonrpc"] = "2.0";
+ request["method"] = method;
+
+ if (!params.isUndefined())
+ {
+ request["params"] = params;
+ }
+
+ LLSD id = generateId();
+ request["id"] = id;
+
+ // Store callback if provided
+ if (callback)
+ {
+ mPendingRequests[id.asString()] = callback;
+ }
+
+ // Send the request
+ if (!sendMessage(LlsdToJson(request)))
+ {
+ // Remove from pending if send failed
+ if (callback)
+ {
+ mPendingRequests.erase(id.asString());
+ }
+ throw InternalError("Failed to send request");
+ }
+
+ LL_DEBUGS("JSONRPC") << "Sent request: " << method << " with id: " << id.asString() << LL_ENDL;
+ return id;
+}
+
+void LLJSONRPCConnection::notify(const std::string& method, const LLSD& params)
+{
+ LLSD notification;
+ notification["jsonrpc"] = "2.0";
+ notification["method"] = method;
+
+ if (!params.isUndefined())
+ {
+ notification["params"] = params;
+ }
+
+ // Notifications don't have an id
+
+ if (!sendMessage(LlsdToJson(notification)))
+ {
+ throw InternalError("Failed to send notification");
+ }
+
+ LL_DEBUGS("JSONRPC") << "Sent notification: " << method << LL_ENDL;
+}
+
+void LLJSONRPCConnection::sendResponse(const LLSD& id, const LLSD& result)
+{
+ LLSD response;
+ response["jsonrpc"] = "2.0";
+ response["result"] = result;
+ response["id"] = id;
+
+ if (!sendMessage(LlsdToJson(response)))
+ {
+ LL_WARNS("JSONRPC") << "Failed to send response for id: " << id.asString() << LL_ENDL;
+ }
+ else
+ {
+ LL_DEBUGS("JSONRPC") << "Sent response for id: " << id.asString() << LL_ENDL;
+ }
+}
+
+void LLJSONRPCConnection::sendError(const LLSD& id, const RPCError& error)
+{
+ LLSD response;
+ response["jsonrpc"] = "2.0";
+
+ LLSD error_obj;
+ error_obj["code"] = error.getCode();
+ error_obj["message"] = error.what();
+
+ if (!error.getData().isUndefined())
+ {
+ error_obj["data"] = error.getData();
+ }
+
+ response["error"] = error_obj;
+ response["id"] = id.isUndefined() ? LLSD() : id; // null for parse errors
+
+ if (!sendMessage(LlsdToJson(response)))
+ {
+ LL_WARNS("JSONRPC") << "Failed to send error response" << LL_ENDL;
+ }
+ else
+ {
+ LL_DEBUGS("JSONRPC") << "Sent error response: " << error.what() << LL_ENDL;
+ }
+}
+
+void LLJSONRPCConnection::sendBatch(const LLSD& batch, ResponseCallback callback)
+{
+ if (!batch.isArray() || batch.size() == 0)
+ {
+ throw InvalidRequest("Batch must be non-empty array");
+ }
+
+ // For batch requests with callbacks, we need to track multiple responses
+ // This is complex as we need to correlate all responses before calling callback
+ // For now, we'll send the batch but won't support batch response callbacks
+ if (callback)
+ {
+ LL_WARNS("JSONRPC") << "Batch response callbacks not yet implemented" << LL_ENDL;
+ }
+
+ if (!sendMessage(LlsdToJson(batch)))
+ {
+ throw InternalError("Failed to send batch");
+ }
+
+ LL_DEBUGS("JSONRPC") << "Sent batch with " << batch.size() << " messages" << LL_ENDL;
+}
+
+//========================================================================
+// LLJSONRPCServer Implementation
+//========================================================================
+
+LLJSONRPCServer::LLJSONRPCServer(const std::string& name, U16 port, bool local_only)
+ : LLWebsocketMgr::WSServer(name, port, local_only), mServerName(name)
+{
+ LL_INFOS("JSONRPC") << "Created JSON-RPC server: " << name
+ << " on port " << port << LL_ENDL;
+
+ // Register standard JSON-RPC methods
+ registerGlobalMethod("system.listMethods", [this](const std::string& method, const LLSD& id, const LLSD& params) -> LLSD {
+ LL_DEBUGS("JSONRPC") << "System method " << method << " called" << LL_ENDL;
+ return getMethodList();
+ });
+
+ registerGlobalMethod("system.getStats", [this](const std::string& method, const LLSD& id, const LLSD& params) -> LLSD {
+ LL_DEBUGS("JSONRPC") << "System method " << method << " called" << LL_ENDL;
+ return getServerStats();
+ });
+
+ registerGlobalMethod("system.ping", [](const std::string& method, const LLSD& id, const LLSD& params) -> LLSD {
+ LL_DEBUGS("JSONRPC") << "System method " << method << " called" << LL_ENDL;
+ LLSD result;
+ result["pong"] = LLDate::now().asString();
+ result["params"] = params;
+ return result;
+ });
+}
+
+LLWebsocketMgr::WSConnection::ptr_t LLJSONRPCServer::connectionFactory(LLWebsocketMgr::WSServer::ptr_t server,
+ LLWebsocketMgr::connection_h handle)
+{
+ auto connection = std::make_shared<LLJSONRPCConnection>(server, handle);
+ setupConnectionMethods(connection);
+ return connection;
+}
+
+void LLJSONRPCServer::onConnectionOpened(const LLWebsocketMgr::WSConnection::ptr_t& connection)
+{
+ LL_INFOS("JSONRPC") << "JSON-RPC client connected, total connections: "
+ << getConnectionCount() << LL_ENDL;
+}
+
+void LLJSONRPCServer::onConnectionClosed(const LLWebsocketMgr::WSConnection::ptr_t& connection)
+{
+ LL_INFOS("JSONRPC") << "JSON-RPC client disconnected, total connections: "
+ << getConnectionCount() << LL_ENDL;
+}
+
+void LLJSONRPCServer::setupConnectionMethods(LLJSONRPCConnection::ptr_t connection)
+{
+ LLMutexLock lock(&mGlobalMethodsMutex);
+
+ // Register all global methods on the new connection
+ for (const auto& [method, handler] : mGlobalMethods)
+ {
+ connection->registerMethod(method, handler);
+ }
+}
+
+void LLJSONRPCServer::registerGlobalMethod(const std::string& method, MethodHandler handler)
+{
+ {
+ LLMutexLock lock(&mGlobalMethodsMutex);
+ mGlobalMethods[method] = handler;
+ }
+
+ // Apply to all existing connections - we need to iterate through connections
+ // Since mConnections is private, we need to use broadcastMessage or find another approach
+ // For now, we'll only apply to new connections
+
+ LL_INFOS("JSONRPC") << "Registered global method: " << method << LL_ENDL;
+}
+
+void LLJSONRPCServer::unregisterGlobalMethod(const std::string& method)
+{
+ {
+ LLMutexLock lock(&mGlobalMethodsMutex);
+ mGlobalMethods.erase(method);
+ }
+
+ // For existing connections, we would need access to them
+ // This is a limitation of the current design - methods added after connection
+ // establishment won't be retroactively applied
+
+ LL_INFOS("JSONRPC") << "Unregistered global method: " << method << LL_ENDL;
+}
+
+LLSD LLJSONRPCServer::getMethodList() const
+{
+ LLMutexLock lock(&mGlobalMethodsMutex);
+
+ LLSD methods = LLSD::emptyArray();
+ for (const auto& [method, handler] : mGlobalMethods)
+ {
+ methods.append(method);
+ }
+
+ return methods;
+}
+
+void LLJSONRPCServer::broadcastNotification(const std::string& method, const LLSD& params)
+{
+ // Use custom broadcast logic since we need to call notify() on each JSON-RPC connection
+ // We can't use the base broadcastMessage() because we need structured JSON-RPC messages
+
+ // Create the notification message
+ LLSD notification;
+ notification["jsonrpc"] = "2.0";
+ notification["method"] = method;
+ if (!params.isUndefined())
+ {
+ notification["params"] = params;
+ }
+
+ // Use the base class broadcast functionality
+ broadcastMessage(boost::json::serialize(LlsdToJson(notification)));
+
+ mTotalNotificationsSent += getConnectionCount();
+ LL_DEBUGS("JSONRPC") << "Broadcast notification: " << method
+ << " to " << getConnectionCount() << " clients" << LL_ENDL;
+}
+
+void LLJSONRPCServer::broadcastCall(const std::string& method, const LLSD& params,
+ BatchResponseCallback callback)
+{
+ if (callback)
+ {
+ LL_WARNS("JSONRPC") << "Broadcast call response callbacks not yet implemented" << LL_ENDL;
+ }
+
+ // Create the request message with a server-unique ID
+ LLSD request;
+ request["jsonrpc"] = "2.0";
+ request["method"] = method;
+
+ // Use the same ID generation as connections for consistency
+ static std::atomic<U64> sBroadcastIdCounter{10000000}; // Start at 10M to clearly distinguish from regular requests
+ U64 id = sBroadcastIdCounter.fetch_add(1);
+ request["id"] = LLSD(llformat("broadcast_%llu", id));
+
+ if (!params.isUndefined())
+ {
+ request["params"] = params;
+ }
+
+ // Use the base class broadcast functionality
+ broadcastMessage(boost::json::serialize(LlsdToJson(request)));
+
+ LL_DEBUGS("JSONRPC") << "Broadcast call: " << method
+ << " to " << getConnectionCount() << " clients" << LL_ENDL;
+}
+
+LLSD LLJSONRPCServer::getServerStats() const
+{
+ LLSD stats;
+ stats["server_name"] = mServerName;
+ stats["connection_count"] = static_cast<S32>(getConnectionCount());
+ stats["is_running"] = isRunning();
+
+ {
+ LLMutexLock lock(&mGlobalMethodsMutex);
+ stats["global_method_count"] = static_cast<S32>(mGlobalMethods.size());
+ }
+
+ stats["total_requests_handled"] = static_cast<LLSD::Integer>(mTotalRequestsHandled.load());
+ stats["total_notifications_sent"] = static_cast<LLSD::Integer>(mTotalNotificationsSent.load());
+ stats["uptime"] = LLDate::now().asString();
+
+ return stats;
+}
diff --git a/indra/llcorehttp/lljsonrpcws.h b/indra/llcorehttp/lljsonrpcws.h
new file mode 100644
index 0000000000..54578c0e93
--- /dev/null
+++ b/indra/llcorehttp/lljsonrpcws.h
@@ -0,0 +1,461 @@
+/**
+ * @file lljsonrpcws.h
+ * @brief JSON-RPC 2.0 WebSocket server and connection implementation
+ *
+ * $LicenseInfo:firstyear=2025&license=viewerlgpl$
+ * Second Life Viewer Source Code
+ * Copyright (C) 2025, Linden Research, Inc.
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation;
+ * version 2.1 of the License only.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ *
+ * Linden Research, Inc., 945 Battery Street, San Francisco, CA 94111 USA
+ * $/LicenseInfo$
+ */
+
+#pragma once
+
+#include "llwebsocketmgr.h"
+#include "llsd.h"
+#include "lluuid.h"
+
+#include <functional>
+#include <unordered_map>
+#include <memory>
+
+/**
+ * @class LLJSONRPCConnection
+ * @brief JSON-RPC 2.0 WebSocket connection implementation
+ *
+ * This class implements the JSON-RPC 2.0 protocol over WebSocket connections.
+ * It handles request/response patterns, notifications, method registration,
+ * and error handling according to the JSON-RPC 2.0 specification.
+ *
+ * ## JSON-RPC 2.0 Protocol Features
+ *
+ * - **Requests**: Method calls that expect a response
+ * - **Notifications**: Method calls that do not expect a response
+ * - **Batch Operations**: Multiple requests/notifications in a single message
+ * - **Error Handling**: Standardized error codes and messages
+ * - **ID Correlation**: Request/response correlation using unique identifiers
+ *
+ * ## Method Handler Registration
+ *
+ * Methods use an enhanced handler signature that provides method name and request ID context:
+ *
+ * @code
+ * connection->registerMethod("echo", [](const std::string& method, const LLSD& id, const LLSD& params) -> LLSD {
+ * LL_INFOS("JSONRPC") << "Method " << method << " called with ID " << id.asString() << LL_ENDL;
+ * return params; // Echo back the parameters
+ * });
+ *
+ * connection->registerMethod("add", [](const std::string& method, const LLSD& id, const LLSD& params) -> LLSD {
+ * if (params.isArray() && params.size() >= 2) {
+ * LL_INFOS("JSONRPC") << "Adding numbers via " << method << LL_ENDL;
+ * return params[0].asReal() + params[1].asReal();
+ * }
+ * throw LLJSONRPCConnection::InvalidParams("Expected array with 2 numbers");
+ * });
+ * @endcode
+ *
+ * The enhanced signature enables:
+ * - Method context awareness for shared handlers
+ * - Request correlation and distributed tracing
+ * - Distinction between notifications (id undefined) and requests
+ * - Enhanced logging and error reporting with context
+ *
+ * ## Making RPC Calls
+ *
+ * @code
+ * // Asynchronous request with callback
+ * LLSD params;
+ * params.append(5);
+ * params.append(3);
+ * connection->call("add", params, [](const LLSD& result, const LLSD& error) {
+ * if (error.isUndefined()) {
+ * LL_INFOS() << "Result: " << result.asReal() << LL_ENDL;
+ * } else {
+ * LL_WARNS() << "Error: " << error["message"].asString() << LL_ENDL;
+ * }
+ * });
+ *
+ * // Fire-and-forget notification
+ * connection->notify("log", LLSD("Server started"));
+ * @endcode
+ */
+class LLJSONRPCConnection : public LLWebsocketMgr::WSConnection
+{
+public:
+ using ptr_t = std::shared_ptr<LLJSONRPCConnection>;
+
+ /// Method handler function signature
+ /// @param method The method name that was called
+ /// @param id The request ID (undefined for notifications)
+ /// @param params The parameters passed to the method
+ /// @return The result to return to the caller
+ /// @throw RPCError-derived exceptions for error responses
+ using MethodHandler = std::function<LLSD(const std::string& method, const LLSD& id, const LLSD& params)>;
+
+ /// Response callback function signature
+ /// @param result The result from a successful call (undefined if error occurred)
+ /// @param error The error object if call failed (undefined if successful)
+ using ResponseCallback = std::function<void(const LLSD& result, const LLSD& error)>;
+
+ /**
+ * @brief JSON-RPC error base class
+ */
+ class RPCError : public std::runtime_error
+ {
+ public:
+ // JSON-RPC 2.0 Standard Error Codes
+ static constexpr S32 PARSE_ERROR = -32700; ///< Invalid JSON was received by the server
+ static constexpr S32 INVALID_REQUEST = -32600; ///< The JSON sent is not a valid Request object
+ static constexpr S32 METHOD_NOT_FOUND = -32601; ///< The method does not exist / is not available
+ static constexpr S32 INVALID_PARAMS = -32602; ///< Invalid method parameter(s)
+ static constexpr S32 INTERNAL_ERROR = -32603; ///< Internal JSON-RPC error
+
+ // Server Error Range (-32000 to -32099)
+ static constexpr S32 SERVER_ERROR_MIN = -32099; ///< Server error range minimum
+ static constexpr S32 SERVER_ERROR_MAX = -32000; ///< Server error range maximum
+
+ // Common server-specific errors
+ static constexpr S32 CONNECTION_CLOSED = -32000; ///< Connection closed unexpectedly
+ static constexpr S32 REQUEST_TIMEOUT = -32001; ///< Request timed out
+ static constexpr S32 UNAUTHORIZED = -32002; ///< Authentication required
+ static constexpr S32 FORBIDDEN = -32003; ///< Access denied
+ static constexpr S32 RATE_LIMITED = -32004; ///< Too many requests
+ static constexpr S32 SERVICE_UNAVAILABLE = -32005; ///< Service temporarily unavailable
+ static constexpr S32 MESSAGE_TOO_LARGE = -32006; ///< Message exceeds maximum size
+ static constexpr S32 INVALID_SESSION = -32007; ///< Session expired or invalid
+
+ RPCError(S32 code, const std::string& message, const LLSD& data = LLSD())
+ : std::runtime_error(message), mCode(code), mData(data) {}
+
+ S32 getCode() const { return mCode; }
+ const LLSD& getData() const { return mData; }
+
+ protected:
+ S32 mCode;
+ LLSD mData;
+ };
+
+ /// Standard JSON-RPC error classes using named constants
+ class ParseError : public RPCError {
+ public:
+ ParseError(const std::string& details = "")
+ : RPCError(PARSE_ERROR, "Parse error" + (details.empty() ? "" : ": " + details)) {}
+ };
+
+ class InvalidRequest : public RPCError {
+ public:
+ InvalidRequest(const std::string& details = "")
+ : RPCError(INVALID_REQUEST, "Invalid Request" + (details.empty() ? "" : ": " + details)) {}
+ };
+
+ class MethodNotFound : public RPCError {
+ public:
+ MethodNotFound(const std::string& method = "")
+ : RPCError(METHOD_NOT_FOUND, "Method not found" + (method.empty() ? "" : ": " + method)) {}
+ };
+
+ class InvalidParams : public RPCError {
+ public:
+ InvalidParams(const std::string& details = "")
+ : RPCError(INVALID_PARAMS, "Invalid params" + (details.empty() ? "" : ": " + details)) {}
+ };
+
+ class InternalError : public RPCError {
+ public:
+ InternalError(const std::string& details = "")
+ : RPCError(INTERNAL_ERROR, "Internal error" + (details.empty() ? "" : ": " + details)) {}
+ };
+
+ /// Server-specific errors (in the -32000 to -32099 range)
+ class ConnectionClosedError : public RPCError {
+ public:
+ ConnectionClosedError(const std::string& details = "Connection closed")
+ : RPCError(CONNECTION_CLOSED, details) {}
+ };
+
+ class RequestTimeoutError : public RPCError {
+ public:
+ RequestTimeoutError(const std::string& details = "Request timed out")
+ : RPCError(REQUEST_TIMEOUT, details) {}
+ };
+
+ class UnauthorizedError : public RPCError {
+ public:
+ UnauthorizedError(const std::string& details = "Authentication required")
+ : RPCError(UNAUTHORIZED, details) {}
+ };
+
+ class ForbiddenError : public RPCError {
+ public:
+ ForbiddenError(const std::string& details = "Access denied")
+ : RPCError(FORBIDDEN, details) {}
+ };
+
+ class RateLimitedError : public RPCError {
+ public:
+ RateLimitedError(const std::string& details = "Too many requests")
+ : RPCError(RATE_LIMITED, details) {}
+ };
+
+ class ServiceUnavailableError : public RPCError {
+ public:
+ ServiceUnavailableError(const std::string& details = "Service temporarily unavailable")
+ : RPCError(SERVICE_UNAVAILABLE, details) {}
+ };
+
+ class MessageTooLargeError : public RPCError {
+ public:
+ MessageTooLargeError(const std::string& details = "Message exceeds maximum size")
+ : RPCError(MESSAGE_TOO_LARGE, details) {}
+ };
+
+ class InvalidSessionError : public RPCError {
+ public:
+ InvalidSessionError(const std::string& details = "Session expired or invalid")
+ : RPCError(INVALID_SESSION, details) {}
+ };
+
+ LLJSONRPCConnection(const LLWebsocketMgr::WSServer::ptr_t server,
+ const LLWebsocketMgr::connection_h& handle)
+ : LLWebsocketMgr::WSConnection(server, handle) {}
+
+ virtual ~LLJSONRPCConnection() = default;
+
+ // WebSocket connection lifecycle
+ void onOpen() override;
+ void onClose() override;
+ void onMessage(const std::string& message) override;
+
+ /**
+ * @brief Register a method handler
+ * @param method The method name to register
+ * @param handler The function to call when this method is invoked
+ */
+ void registerMethod(const std::string& method, MethodHandler handler);
+
+ /**
+ * @brief Unregister a method handler
+ * @param method The method name to unregister
+ */
+ void unregisterMethod(const std::string& method);
+
+ /**
+ * @brief Make an asynchronous JSON-RPC call
+ * @param method The method name to call
+ * @param params The parameters to pass
+ * @param callback Callback for the response (optional)
+ * @return The request ID for correlation
+ */
+ LLSD call(const std::string& method, const LLSD& params = LLSD(),
+ ResponseCallback callback = nullptr);
+
+ /**
+ * @brief Send a JSON-RPC notification (no response expected)
+ * @param method The method name
+ * @param params The parameters to pass
+ */
+ void notify(const std::string& method, const LLSD& params = LLSD());
+
+ /**
+ * @brief Send a successful response to a request
+ * @param id The request ID from the original request
+ * @param result The result to return
+ */
+ void sendResponse(const LLSD& id, const LLSD& result);
+
+ /**
+ * @brief Send an error response to a request
+ * @param id The request ID from the original request (can be null)
+ * @param error The RPCError to send
+ */
+ void sendError(const LLSD& id, const RPCError& error);
+
+ /**
+ * @brief Send a batch of requests/notifications
+ * @param batch Array of request/notification objects
+ * @param callback Callback for batch response (optional)
+ */
+ void sendBatch(const LLSD& batch, ResponseCallback callback = nullptr);
+
+protected:
+ /**
+ * @brief Process a single JSON-RPC message
+ * @param message_obj The parsed JSON message
+ */
+ void processMessage(const LLSD& message_obj);
+
+ /**
+ * @brief Process a JSON-RPC request
+ * @param request The request object
+ */
+ void processRequest(const LLSD& request);
+
+ /**
+ * @brief Process a JSON-RPC response
+ * @param response The response object
+ */
+ void processResponse(const LLSD& response);
+
+ /**
+ * @brief Validate a JSON-RPC message structure
+ * @param message The message to validate
+ * @param is_request True if validating a request, false for response
+ * @throw InvalidRequest if validation fails
+ */
+ void validateMessage(const LLSD& message, bool is_request = true);
+
+ /**
+ * @brief Generate the next unique request ID
+ * @return A server-unique request ID
+ *
+ * Generates a server-wide unique identifier using an atomic counter.
+ * This ensures request IDs are unique across all connections within the
+ * server instance, providing efficient ID generation with guaranteed uniqueness.
+ *
+ * IDs follow the format "rpc_{counter}" where counter is a monotonically
+ * increasing 64-bit value starting from 1. This approach provides:
+ * - Guaranteed uniqueness within server scope
+ * - High performance (atomic increment operation)
+ * - Predictable, sequential ordering for debugging
+ * - Thread-safe generation across multiple connections
+ */
+ LLSD generateId();
+
+private:
+ std::unordered_map<std::string, MethodHandler> mMethodHandlers;
+ std::unordered_map<std::string, ResponseCallback> mPendingRequests;
+};
+
+/**
+ * @class LLJSONRPCServer
+ * @brief JSON-RPC 2.0 WebSocket server implementation
+ *
+ * This server extends the basic WebSocket server to provide JSON-RPC 2.0
+ * protocol support. It manages JSON-RPC connections and provides server-wide
+ * method registration and broadcasting capabilities.
+ *
+ * ## Server-Wide Method Registration
+ *
+ * Methods can be registered at the server level and will be available
+ * on all connections:
+ *
+ * @code
+ * auto server = std::make_shared<LLJSONRPCServer>("rpc_server", 8080);
+ *
+ * server->registerGlobalMethod("getServerInfo", [](const std::string& method, const LLSD& id, const LLSD& params) -> LLSD {
+ * LL_INFOS("JSONRPC") << "Server info requested via " << method << LL_ENDL;
+ * LLSD info;
+ * info["name"] = "My RPC Server";
+ * info["version"] = "1.0.0";
+ * info["uptime"] = LLDate::now().secondsSinceEpoch();
+ * return info;
+ * });
+ *
+ * server->registerGlobalMethod("listMethods", [server](const std::string& method, const LLSD& id, const LLSD& params) -> LLSD {
+ * return server->getMethodList();
+ * });
+ * @endcode
+ *
+ * ## Broadcasting and Multi-client Operations
+ *
+ * @code
+ * // Broadcast notification to all connected clients
+ * server->broadcastNotification("serverAlert", LLSD("Server will restart in 5 minutes"));
+ *
+ * // Call a method on all clients and collect responses
+ * server->broadcastCall("getClientStatus", LLSD(), [](const LLSD& responses) {
+ * for (const auto& response : llsd::inArray(responses)) {
+ * LL_INFOS() << "Client status: " << response << LL_ENDL;
+ * }
+ * });
+ * @endcode
+ */
+class LLJSONRPCServer : public LLWebsocketMgr::WSServer
+{
+public:
+ using ptr_t = std::shared_ptr<LLJSONRPCServer>;
+ using MethodHandler = LLJSONRPCConnection::MethodHandler;
+ using ResponseCallback = LLJSONRPCConnection::ResponseCallback;
+ using BatchResponseCallback = std::function<void(const LLSD& responses)>;
+
+ LLJSONRPCServer(const std::string& name, U16 port, bool local_only = true);
+ virtual ~LLJSONRPCServer() = default;
+
+ // Server lifecycle callbacks
+ void onConnectionOpened(const LLWebsocketMgr::WSConnection::ptr_t& connection) override;
+ void onConnectionClosed(const LLWebsocketMgr::WSConnection::ptr_t& connection) override;
+
+ /**
+ * @brief Register a global method available on all connections
+ * @param method The method name to register
+ * @param handler The function to call when this method is invoked
+ */
+ void registerGlobalMethod(const std::string& method, MethodHandler handler);
+
+ /**
+ * @brief Unregister a global method
+ * @param method The method name to unregister
+ */
+ void unregisterGlobalMethod(const std::string& method);
+
+ /**
+ * @brief Get list of registered global methods
+ * @return Array of method names
+ */
+ LLSD getMethodList() const;
+
+ /**
+ * @brief Broadcast a notification to all connected clients
+ * @param method The method name
+ * @param params The parameters to pass
+ */
+ void broadcastNotification(const std::string& method, const LLSD& params = LLSD());
+
+ /**
+ * @brief Call a method on all connected clients
+ * @param method The method name
+ * @param params The parameters to pass
+ * @param callback Callback to receive aggregated responses
+ */
+ void broadcastCall(const std::string& method, const LLSD& params = LLSD(),
+ BatchResponseCallback callback = nullptr);
+
+ /**
+ * @brief Get server statistics
+ * @return Statistics object with connection count, method count, etc.
+ */
+ LLSD getServerStats() const;
+
+protected:
+ LLWebsocketMgr::WSConnection::ptr_t connectionFactory(LLWebsocketMgr::WSServer::ptr_t server,
+ LLWebsocketMgr::connection_h handle) override;
+
+ /**
+ * @brief Apply global method handlers to a new connection
+ * @param connection The connection to configure
+ */
+ virtual void setupConnectionMethods(LLJSONRPCConnection::ptr_t connection);
+
+private:
+ std::unordered_map<std::string, MethodHandler> mGlobalMethods;
+ mutable LLMutex mGlobalMethodsMutex;
+
+ std::string mServerName; // Store server name for stats
+ std::atomic<U64> mTotalRequestsHandled{0};
+ std::atomic<U64> mTotalNotificationsSent{0};
+};
diff --git a/indra/llcorehttp/llwebsocketmgr.cpp b/indra/llcorehttp/llwebsocketmgr.cpp
index 3e4262b49b..d44d5d877d 100644
--- a/indra/llcorehttp/llwebsocketmgr.cpp
+++ b/indra/llcorehttp/llwebsocketmgr.cpp
@@ -63,6 +63,32 @@ void LLWebsocketMgr::cleanupSingleton()
stopAllServers();
}
+void LLWebsocketMgr::update()
+{
+ std::vector<WSServer::ptr_t> stops;
+
+ for (auto &[name, server] : mServers)
+ {
+ if (server && server->isRunning())
+ {
+ if (!server->update())
+ {
+ stops.push_back(server);
+ }
+ }
+ }
+
+ for (const auto& server : stops)
+ {
+ if (server)
+ {
+ LL_DEBUGS("WebSocket") << "Stopping server: " << server->mServerName << LL_ENDL;
+ removeServer(server->mServerName);
+ }
+ }
+}
+
+
LLWebsocketMgr::WSServer::ptr_t LLWebsocketMgr::findServerByName(const std::string &name) const
{
auto it = mServers.find(std::string(name));
@@ -192,6 +218,7 @@ struct Server_impl
mPort(port),
mLocalOnly(local_only)
{
+
mServer.set_open_handler([this](websocketpp::connection_hdl hdl) { this->onOpen(hdl); });
mServer.set_close_handler([this](websocketpp::connection_hdl hdl) { this->onClose(hdl); });
mServer.set_message_handler([this](websocketpp::connection_hdl hdl, Server_t::message_ptr msg) { this->onMessage(hdl, msg); });
@@ -647,12 +674,12 @@ void LLWebsocketMgr::WSServer::handleMessage(const connection_h& handle, const s
//------------------------------------------------------------------------
bool LLWebsocketMgr::WSConnection::sendMessage(const std::string& message) const
{
- if (!mServer)
+ if (mOwningServer.expired())
{
LL_WARNS("WebSocket") << "Attempted to send message on connection with null server reference" << LL_ENDL;
return false;
}
- return mServer->sendMessageTo(mConnectionHandle, message);
+ return mOwningServer.lock()->sendMessageTo(mConnectionHandle, message);
}
bool LLWebsocketMgr::WSConnection::sendMessage(const boost::json::value& json) const
@@ -668,7 +695,7 @@ bool LLWebsocketMgr::WSConnection::sendMessage(const LLSD& data) const
void LLWebsocketMgr::WSConnection::closeConnection(U16 code, const std::string& reason)
{
- if (!mServer)
+ if (mOwningServer.expired())
{
LL_WARNS("WebSocket") << "Attempted to close connection with null server reference" << LL_ENDL;
return;
@@ -677,7 +704,7 @@ void LLWebsocketMgr::WSConnection::closeConnection(U16 code, const std::string&
LL_INFOS("WebSocket") << "WSConnection closing connection with code " << code
<< " and reason: " << (reason.empty() ? "(no reason)" : reason) << LL_ENDL;
- if (!mServer->closeConnection(mConnectionHandle, code, reason))
+ if (!mOwningServer.lock()->closeConnection(mConnectionHandle, code, reason))
{
LL_WARNS("WebSocket") << "Failed to close connection through server" << LL_ENDL;
}
diff --git a/indra/llcorehttp/llwebsocketmgr.h b/indra/llcorehttp/llwebsocketmgr.h
index 08f6f22dbc..570c8ad6cd 100644
--- a/indra/llcorehttp/llwebsocketmgr.h
+++ b/indra/llcorehttp/llwebsocketmgr.h
@@ -78,7 +78,7 @@ public:
*/
WSConnection(const std::shared_ptr<WSServer> &server, const connection_h& handle):
mConnectionHandle(handle),
- mServer(server)
+ mOwningServer(server)
{}
virtual ~WSConnection() = default;
@@ -188,9 +188,9 @@ public:
*/
void closeConnection(U16 code = 1000, const std::string& reason = std::string());
- private:
+ protected:
connection_h mConnectionHandle;
- std::shared_ptr<WSServer> mServer; // Back-reference to the server this connection belongs to
+ std::weak_ptr<WSServer> mOwningServer; // Back-reference to the server this connection belongs to
};
/**
@@ -271,6 +271,8 @@ public:
}
void broadcastMessage(const std::string& message);
+ virtual bool update() { return true; }
+
protected:
virtual WSConnection::ptr_t connectionFactory(WSServer::ptr_t server, connection_h handle);
@@ -320,6 +322,8 @@ public:
bool startServer(const std::string &name) const;
void stopServer(const std::string &name) const;
+ void update();
+
protected:
void initSingleton() override;
void cleanupSingleton() override;
diff --git a/indra/newview/llappviewer.cpp b/indra/newview/llappviewer.cpp
index 63d364eaa8..6540517ef2 100644
--- a/indra/newview/llappviewer.cpp
+++ b/indra/newview/llappviewer.cpp
@@ -109,6 +109,7 @@
#include "lllocalbitmaps.h"
#include "llperfstats.h"
#include "llgltfmateriallist.h"
+#include "llwebsocketmgr.h"
// Linden library includes
#include "llavatarnamecache.h"
@@ -4754,6 +4755,11 @@ void LLAppViewer::idle()
LLMortician::updateClass();
LLFilePickerThread::clearDead(); //calls LLFilePickerThread::notify()
LLDirPickerThread::clearDead();
+
+ if (LLWebsocketMgr::instanceExists())
+ {
+ LLWebsocketMgr::instance().update();
+ }
F32 dt_raw = idle_timer.getElapsedTimeAndResetF32();
LLGLTFMaterialList::flushUpdates();
diff --git a/indra/newview/llpreviewscript.cpp b/indra/newview/llpreviewscript.cpp
index 9b2a149212..3ab2e747ea 100644
--- a/indra/newview/llpreviewscript.cpp
+++ b/indra/newview/llpreviewscript.cpp
@@ -1144,28 +1144,7 @@ void LLScriptEdCore::openInExternalEditor()
// Start watching file changes.
mContainer->mLiveFile = new LLLiveLSLFile(filename, boost::bind(&LLScriptEdContainer::onExternalChange, mContainer, _1));
mContainer->mLiveFile->addToEventTimer();
-
- // if the user has enabled websockets, create the server to talk to the external editor
- {
- // TODO: Get the name, port, and locality from settings
- std::string server_name(LLScriptEditorWSServer::DEFAULT_SERVER_NAME);
- U16 server_port(LLScriptEditorWSServer::DEFAULT_SERVER_PORT);
- bool server_localhost(true);
-
- LLWebsocketMgr& wsmgr = LLWebsocketMgr::instance();
- LLScriptEditorWSServer::ptr_t server =
- std::static_pointer_cast<LLScriptEditorWSServer>(wsmgr.findServerByName(server_name));
-
- if (!server)
- {
- server = std::make_shared<LLScriptEditorWSServer>(server_name, server_port, server_localhost);
- wsmgr.addServer(server);
- wsmgr.startServer(server_name);
- }
-
- std::string script_id_hash_str(mContainer->getUniqueHash());
- server->associateEditor(getHandle(), script_id_hash_str);
- }
+ mContainer->startWebsocketServer();
// Open it in external editor.
{
@@ -1687,6 +1666,54 @@ bool LLScriptEdContainer::handleKeyHere(KEY key, MASK mask)
return true;
}
+void LLScriptEdContainer::startWebsocketServer()
+{
+ // if the user has enabled websockets, create the server to talk to the external editor
+ {
+ // TODO: Get the name, port, and locality from settings
+ std::string server_name(LLScriptEditorWSServer::DEFAULT_SERVER_NAME);
+ U16 server_port(LLScriptEditorWSServer::DEFAULT_SERVER_PORT);
+ bool server_localhost(true);
+
+ LLWebsocketMgr& wsmgr = LLWebsocketMgr::instance();
+ LLScriptEditorWSServer::ptr_t server = std::static_pointer_cast<LLScriptEditorWSServer>(wsmgr.findServerByName(server_name));
+
+ if (!server)
+ {
+ server = std::make_shared<LLScriptEditorWSServer>(server_name, server_port, server_localhost);
+ wsmgr.addServer(server);
+ wsmgr.startServer(server_name);
+ }
+
+ std::string script_id_hash_str(getUniqueHash());
+ server->associateEditor(getHandle(), script_id_hash_str);
+ }
+}
+
+void LLScriptEdContainer::attachToWebSocket(const std::shared_ptr<LLScriptEditorWSConnection>& connection)
+{
+ mWebSocket = connection;
+}
+
+void LLScriptEdContainer::detachFromWebSocket(bool send_disconnect)
+{
+ if (mWebSocket)
+ {
+ if (send_disconnect)
+ {
+ // TODO:
+ mWebSocket->sendDisconnect(LLScriptEditorWSConnection::REASON_EDITOR_CLOSED);
+ mWebSocket->closeConnection();
+ }
+ mWebSocket.reset();
+ }
+}
+
+void LLScriptEdContainer::cleanupWebSocket()
+{
+ mWebSocket.reset();
+}
+
/// ---------------------------------------------------------------------------
/// LLPreviewLSL
/// ---------------------------------------------------------------------------
diff --git a/indra/newview/llpreviewscript.h b/indra/newview/llpreviewscript.h
index 14ec54d42b..72d52a115e 100644
--- a/indra/newview/llpreviewscript.h
+++ b/indra/newview/llpreviewscript.h
@@ -54,6 +54,7 @@ class LLScriptEdContainer;
class LLFloaterGotoLine;
class LLFloaterExperienceProfile;
class LLScriptMovedObserver;
+class LLScriptEditorWSConnection;
class LLLiveLSLFile : public LLLiveFile
{
@@ -99,14 +100,14 @@ protected:
bool live,
S32 bottom_pad = 0); // pad below bottom row of buttons
public:
- ~LLScriptEdCore();
+ ~LLScriptEdCore() override;
void initMenu();
void processKeywords();
void processKeywords(bool luau_language);
- virtual void draw();
- /*virtual*/ bool postBuild();
+ void draw() override;
+ bool postBuild() override;
bool canClose();
void setEnableEditing(bool enable);
bool canLoadOrSaveToFile( void* userdata );
@@ -159,6 +160,7 @@ public:
void enableSave(bool b) { mEnableSave = b; }
bool hasChanged() const;
+
private:
void onBtnDynamicHelp();
void onBtnUndoChanges();
@@ -204,7 +206,7 @@ private:
LLScriptEdContainer* mContainer; // parent view
-public:
+ public:
boost::signals2::connection mSyntaxIDConnection;
};
@@ -218,6 +220,11 @@ public:
bool handleKeyHere(KEY key, MASK mask);
+ void startWebsocketServer();
+ void attachToWebSocket(const std::shared_ptr<LLScriptEditorWSConnection>& connection);
+ void detachFromWebSocket(bool send_disconnect);
+ void cleanupWebSocket();
+
protected:
std::string getTmpFileName(const std::string& script_name) const;
std::string getUniqueHash() const;
@@ -230,6 +237,8 @@ protected:
LLScriptEdCore* mScriptEd;
LLLiveLSLFile* mLiveFile = nullptr;
LLLiveLSLFile* mLiveLogFile = nullptr;
+
+ std::shared_ptr<LLScriptEditorWSConnection> mWebSocket;
};
// Used to view and edit an LSL script from your inventory.
diff --git a/indra/newview/llscripteditorws.cpp b/indra/newview/llscripteditorws.cpp
index 503624d15a..9863130aea 100644
--- a/indra/newview/llscripteditorws.cpp
+++ b/indra/newview/llscripteditorws.cpp
@@ -1,9 +1,10 @@
/**
* @file llscripteditorws.cpp
+ * @brief JSON-RPC 2.0 WebSocket server implementation for external script editor integration
*
- * $LicenseInfo:firstyear=2002&license=viewerlgpl$
+ * $LicenseInfo:firstyear=2025&license=viewerlgpl$
* Second Life Viewer Source Code
- * Copyright (C) 2010, Linden Research, Inc.
+ * Copyright (C) 2025, Linden Research, Inc.
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
@@ -23,164 +24,24 @@
* $/LicenseInfo$
*/
-/*
- * ============================================================================
- * WEBSOCKET MESSAGE FORMAT SPECIFICATION
- * ============================================================================
- *
- * This WebSocket implementation uses a structured JSON message format for
- * communication between the Second Life viewer and external script editors.
- * All messages are encoded as UTF-8 JSON strings.
- *
- * ## BASE MESSAGE STRUCTURE
- *
- * Every WebSocket message follows this mandatory structure:
- *
- * ```json
- * {
- * "command": "message_type_identifier", // REQUIRED: Command/message type
- * "data": { // OPTIONAL: Command-specific payload
- * // ... command-specific fields ...
- * },
- * "timestamp": "2025-01-27T15:30:00Z", // OPTIONAL: ISO8601 timestamp
- * "session_id": "uuid-string", // OPTIONAL: Session identifier
- * "message_id": "unique-id" // OPTIONAL: For request/response correlation
- * }
- * ```
- *
- * ## MESSAGE TYPES
- *
- * ### 1. SERVER-TO-CLIENT MESSAGES
- *
- * #### capabilities
- * Sent immediately when client connects to announce server capabilities.
- * ```json
- * {
- * "command": "capabilities",
- * "data": {
- * "server_name": "Second Life Script Editor WebSocket Server",
- * "server_version": "1.0.0",
- * "protocol_version": "1.0",
- * "viewer_name": "Second Life",
- * "viewer_version": "Second Life Release 6.4.26",
- * "session_id": "generated-uuid",
- * "timestamp": "2025-01-27T15:30:00Z",
- * "capabilities": ["script_editing", "compilation", "metadata", ...],
- * "supported_languages": ["lsl", "luau"],
- * "features": {
- * "live_compilation": true,
- * "debugging": false,
- * "breakpoints": false
- * }
- * }
- * }
- * ```
- *
- * #### error
- * Sent when message processing fails or protocol violations occur.
- * ```json
- * {
- * "command": "error",
- * "data": {
- * "error_code": "PARSE_ERROR|INVALID_MESSAGE|UNKNOWN_MESSAGE_TYPE",
- * "error_message": "Human-readable error description"
- * }
- * }
- * ```
- *
- * ### 2. CLIENT-TO-SERVER MESSAGES
- *
- * #### connect
- * Client handshake message to establish connection and negotiate capabilities.
- * ```json
- * {
- * "command": "connect",
- * "data": {
- * "client_name": "VS Code LSL Extension",
- * "client_version": "1.2.3",
- * "protocol_version": "1.0",
- * "capabilities": ["script_editing", "syntax_highlighting"],
- * "supported_languages": ["lsl", "luau"],
- * "features": {
- * "auto_completion": true,
- * "live_preview": false
- * },
- * "session_id": "optional-client-provided-uuid"
- * }
- * }
- * ```
- *
- * ### 3. BIDIRECTIONAL MESSAGES
- *
- * #### connect_ack
- * Server response to client connect message.
- * ```json
- * {
- * "command": "connect_ack",
- * "data": {
- * "status": "connected|rejected",
- * "message": "Connection established successfully",
- * "session_id": "established-session-uuid",
- * "timestamp": "2025-01-27T15:30:00Z",
- * "mutual_capabilities": ["script_editing", "compilation"],
- * "supported_languages": ["lsl", "luau"],
- * "max_script_size": 65536,
- * "heartbeat_interval": 30,
- * // For rejected connections:
- * "error": "incompatible_protocol|missing_capabilities|connection_failed"
- * }
- * }
- * ```
- *
- * ## FUTURE MESSAGE TYPES (Planned)
- *
- * The following message types are referenced in the protocol design but not
- * yet implemented:
- *
- * - `script_updated`: Editor notifies viewer of script content changes
- * - `save_request`: Editor requests script save to SL servers
- * - `compile_request`: Editor requests script compilation
- * - `script_content`: Viewer sends full script content to editor
- * - `compile_result`: Viewer sends compilation results to editor
- * - `save_result`: Viewer sends save operation results to editor
- * - `metadata`: Script and object metadata exchange
- * - `ping`/`pong`: Connection health check messages
- * - `editor_ready`: Editor initialization complete notification
- *
- * ## PROTOCOL RULES
- *
- * 1. **Encoding**: All messages must be valid UTF-8 JSON
- * 2. **Required Fields**: Every message MUST have a "command" field
- * 3. **Case Sensitivity**: All field names are case-sensitive
- * 4. **Protocol Version**: Currently only "1.0" is supported
- * 5. **Error Handling**: Invalid messages trigger "error" responses
- * 6. **Connection Flow**: Client must send "connect" before other commands
- * 7. **Session Management**: session_id tracks individual editor sessions
- * 8. **Capability Negotiation**: Features limited to mutual capabilities
- *
- * ## ERROR CODES
+/**
+ * This implementation provides JSON-RPC 2.0 WebSocket communication between
+ * the Second Life viewer and external script editors. It uses the standard
+ * JSON-RPC 2.0 protocol without pre-defined script-specific methods,
+ * allowing for flexible integration approaches.
*
- * - `PARSE_ERROR`: Invalid JSON syntax
- * - `PARSE_EXCEPTION`: JSON parsing threw exception
- * - `INVALID_MESSAGE`: Missing required "command" field
- * - `UNKNOWN_MESSAGE_TYPE`: Unrecognized command type
- * - `INCOMPATIBLE_PROTOCOL`: Unsupported protocol version
- * - `MISSING_CAPABILITIES`: Required capabilities not supported
- * - `CONNECTION_FAILED`: General connection establishment failure
+ * ## JSON-RPC Integration
*
- * ## IMPLEMENTATION NOTES
+ * The connection provides a clean JSON-RPC 2.0 interface that can be
+ * extended with script-specific functionality as needed:
*
- * - Messages are parsed using boost::json and converted to LLSD internally
- * - All timestamps use ISO 8601 format in UTC timezone
- * - UUIDs are generated using LLUUID::generateNewID() for session tracking
- * - Maximum script size is currently limited to 65536 bytes
- * - WebSocket server binds to localhost only for security by default
- * - Protocol designed for extensibility with additional message types
+ * ### Server-to-Client (Viewer to Editor):
+ * - `session.handshake`: Welcome message on connection
+ * - `session.disconnect`: Notify editor of disconnection
*
- * ============================================================================
+ * ### Notifications (no response expected):
*/
-
#include "llviewerprecompiledheaders.h"
#include "llscripteditorws.h"
#include "llpreviewscript.h"
@@ -189,82 +50,42 @@
#include "lldate.h"
#include "llerror.h"
#include "lluuid.h"
-#include "llsdjson.h"
-#include <boost/json.hpp>
-
-//------------------------------------------------------------------------
+#include "llversioninfo.h"
-LLScriptEditorWSServer::LLScriptEditorWSServer(const std::string_view name, U16 port, bool local_only):
- LLWebsocketMgr::WSServer(name, port, local_only)
+//========================================================================
+LLScriptEditorWSServer::LLScriptEditorWSServer(const std::string& name, U16 port, bool local_only)
+ : LLJSONRPCServer(name, port, local_only)
{
+ LL_INFOS("ScriptEditorWS") << "Created JSON-RPC script editor server: " << name
+ << " on port " << port << LL_ENDL;
}
-LLWebsocketMgr::WSConnection::ptr_t LLScriptEditorWSServer::connectionFactory(WSServer::ptr_t server, LLWebsocketMgr::connection_h handle)
+LLWebsocketMgr::WSConnection::ptr_t LLScriptEditorWSServer::connectionFactory(LLWebsocketMgr::WSServer::ptr_t server,
+ LLWebsocketMgr::connection_h handle)
{
- return std::make_shared<LLScriptEditorWSConnection>(server, handle);
+ auto connection = std::make_shared<LLScriptEditorWSConnection>(server, handle);
+ mActiveConnections.insert(connection);
+
+ // Call setupConnectionMethods to register any global methods
+ setupConnectionMethods(connection);
+
+ return connection;
}
-//------------------------------------------------------------------------
void LLScriptEditorWSServer::onConnectionOpened(const LLWebsocketMgr::WSConnection::ptr_t& connection)
{
- LL_INFOS("ScriptEditorWS") << "New script editor client connected" << LL_ENDL;
-
- // Build capabilities message to send to the newly connected external editor
- LLSD capabilities_message;
- capabilities_message["command"] = "capabilities";
-
- LLSD& data = capabilities_message["data"];
-
- // Server identification and version information
- data["server_name"] = "Second Life Script Editor WebSocket Server";
- data["server_version"] = "1.0.0";
- data["protocol_version"] = "1.0";
-
- // Viewer information
- data["viewer_name"] = LLTrans::getString("APP_NAME");
- data["viewer_version"] = LLAppViewer::instance()->getSecondLifeTitle();
+ // Call parent class to handle JSON-RPC setup and standard methods
+ LLJSONRPCServer::onConnectionOpened(connection);
- // Session information
- data["session_id"] = LLUUID::generateNewID().asString();
- data["timestamp"] = LLDate::now().asString();
+ LL_INFOS("ScriptEditorWS") << "New script editor client connected via JSON-RPC" << LL_ENDL;
- // Server capabilities - what the viewer/server supports
- LLSD capabilities = LLSD::emptyArray();
- capabilities.append("script_editing"); // Basic script content editing
- capabilities.append("script_synchronization"); // Real-time sync between viewer and editor
- capabilities.append("compilation"); // Compile results with errors/warnings
- capabilities.append("metadata"); // Script and object metadata
- capabilities.append("syntax_highlighting"); // LSL/Luau syntax information
- capabilities.append("error_reporting"); // Detailed error reporting
-
- data["capabilities"] = capabilities;
-
- // Language support information
- LLSD languages = LLSD::emptyArray();
- languages.append("lsl"); // Linden Scripting Language
- languages.append("luau"); // Luau scripting language
- data["supported_languages"] = languages;
-
- // Feature flags
- LLSD features;
- features["live_compilation"] = true;
- features["debugging"] = false; // Not implemented yet
- features["breakpoints"] = false; // Not implemented yet
- data["features"] = features;
-
- // Send the capabilities message to the newly connected client
- if (connection->sendMessage(capabilities_message))
- {
- LL_INFOS("ScriptEditorWS") << "Sent capabilities message to new client" << LL_ENDL;
- }
- else
- {
- LL_WARNS("ScriptEditorWS") << "Failed to send capabilities message to new client" << LL_ENDL;
- }
}
void LLScriptEditorWSServer::onConnectionClosed(const LLWebsocketMgr::WSConnection::ptr_t& connection)
{
+ // Call parent class to handle JSON-RPC cleanup
+ LLJSONRPCServer::onConnectionClosed(connection);
+
LL_INFOS("ScriptEditorWS") << "Script editor client disconnected" << LL_ENDL;
// Remove from active connections
@@ -273,13 +94,12 @@ void LLScriptEditorWSServer::onConnectionClosed(const LLWebsocketMgr::WSConnecti
{
mActiveConnections.erase(script_connection);
- // Remove from any script associations
LL_INFOS("ScriptEditorWS") << "Removed connection from active connections. Total: "
<< mActiveConnections.size() << LL_ENDL;
+ // TODO: When connections reach 0, stop the server aftera a timeout.
}
}
-//------------------------------------------------------------------------
bool LLScriptEditorWSServer::associateEditor(const LLHandle<LLPanel>& editor_handle, const std::string& script_id)
{
if (!editor_handle.isDead())
@@ -305,304 +125,268 @@ LLHandle<LLPanel> LLScriptEditorWSServer::findEditorForScript(const std::string&
return LLHandle<LLPanel>();
}
-//========================================================================
-void LLScriptEditorWSConnection::onOpen()
-{
-
-}
-
-void LLScriptEditorWSConnection::onClose()
+std::shared_ptr<LLScriptEditorWSConnection> LLScriptEditorWSServer::findConnectionForScript(const std::string& script_id)
{
+ // TODO: Implement logic to find connection handling a specific script
+ // This would require tracking which connection is responsible for which script
+ return nullptr;
}
-void LLScriptEditorWSConnection::onMessage(const std::string& message)
+std::set<std::string> LLScriptEditorWSServer::getActiveScripts() const
{
- LL_DEBUGS("ScriptEditorWS") << "Received message: " << message << LL_ENDL;
-
- // Convert JSON string to LLSD
- LLSD parsed_message;
- try
+ std::set<std::string> active_scripts;
+ for (const auto& [script_id, editor_handle] : mScriptEditors)
{
- boost::system::error_code ec;
- boost::json::value json_value = boost::json::parse(message, ec);
-
- if (ec.failed())
+ if (!editor_handle.isDead())
{
- LL_WARNS("ScriptEditorWS") << "Failed to parse JSON message: " << ec.message() << LL_ENDL;
-
- // Send error response back to client
- LLSD error_response;
- error_response["command"] = "error";
- error_response["data"]["error_code"] = "PARSE_ERROR";
- error_response["data"]["error_message"] = "Invalid JSON format: " + std::string(ec.message());
- sendMessage(error_response);
- return;
+ active_scripts.insert(script_id);
}
+ }
+ return active_scripts;
+}
- // Convert boost::json::value to LLSD
- parsed_message = LlsdFromJson(json_value);
+void LLScriptEditorWSServer::broadcastScriptUpdate(const std::string& script_id, const std::string& content, const LLSD& metadata)
+{
+ LL_DEBUGS("ScriptEditorWS") << "Broadcasting script update for script: " << script_id << LL_ENDL;
- LL_DEBUGS("ScriptEditorWS") << "Parsed LLSD message type: " << parsed_message.type()
- << ", has 'type' field: " << parsed_message.has("command") << LL_ENDL;
- }
- catch (const std::exception& e)
- {
- LL_WARNS("ScriptEditorWS") << "Exception parsing JSON message: " << e.what() << LL_ENDL;
+ LLSD params;
+ params["script_id"] = script_id;
+ params["content"] = content;
+ params["timestamp"] = LLDate::now().asString();
- // Send error response back to client
- LLSD error_response;
- error_response["command"] = "error";
- error_response["data"]["error_code"] = "PARSE_EXCEPTION";
- error_response["data"]["error_message"] = "JSON parsing exception: " + std::string(e.what());
- sendMessage(error_response);
- return;
+ if (!metadata.isUndefined())
+ {
+ params["metadata"] = metadata;
}
- // Validate that we have a proper message structure
- if (!parsed_message.has("command"))
- {
- LL_WARNS("ScriptEditorWS") << "Received message without 'type' field" << LL_ENDL;
+ // Send to all connected editors as a notification
+ broadcastNotification("script.update", params);
+}
- LLSD error_response;
- error_response["command"] = "error";
- error_response["data"]["error_code"] = "INVALID_MESSAGE";
- error_response["data"]["error_message"] = "Message must have a 'type' field";
- sendMessage(error_response);
- return;
- }
+void LLScriptEditorWSServer::broadcastCompilationResult(const std::string& script_id, bool success, const LLSD& errors)
+{
+ LL_DEBUGS("ScriptEditorWS") << "Broadcasting compilation result for script: " << script_id
+ << " (success: " << success << ")" << LL_ENDL;
- std::string message_type = parsed_message["command"].asString();
- LL_INFOS("ScriptEditorWS") << "Processing message of type: " << message_type << LL_ENDL;
+ LLSD params;
+ params["script_id"] = script_id;
+ params["success"] = success;
+ params["timestamp"] = LLDate::now().asString();
- // Route message to appropriate handler based on type
- if (message_type == "connect")
+ if (!errors.isUndefined() && errors.isArray())
{
- processConnectMessage(parsed_message);
+ params["errors"] = errors;
}
- else
- {
- LL_WARNS("ScriptEditorWS") << "Received unknown message type: " << message_type << LL_ENDL;
- LLSD error_response;
- error_response["command"] = "error";
- error_response["data"]["error_code"] = "UNKNOWN_MESSAGE_TYPE";
- error_response["data"]["error_message"] = "Unknown message type: " + message_type;
- sendMessage(error_response);
- }
+ // Send to all connected editors as a notification
+ broadcastNotification("compilation.result", params);
}
-void LLScriptEditorWSConnection::processConnectMessage(const LLSD& message)
+void LLScriptEditorWSServer::setupConnectionMethods(LLJSONRPCConnection::ptr_t connection)
{
- LL_INFOS("ScriptEditorWS") << "Processing connect message from client" << LL_ENDL;
+ // Call parent class to register global JSON-RPC methods
+ LLJSONRPCServer::setupConnectionMethods(connection);
- // Extract connection information from the message
- LLSD data;
- if (message.has("data"))
+ // Cast to our specific connection type to access script editor functionality
+ auto script_connection = std::dynamic_pointer_cast<LLScriptEditorWSConnection>(connection);
+ if (script_connection)
{
- data = message["data"];
- }
+ LL_INFOS("ScriptEditorWS") << "Setting up script editor connection methods" << LL_ENDL;
- // Parse client information
- std::string client_name;
- std::string client_version;
- std::string protocol_version;
- LLSD client_capabilities;
- LLSD supported_languages;
- LLSD client_features;
+ // Here derived classes could add script-specific method registrations
+ // For now, the base LLScriptEditorWSConnection doesn't register any specific methods
+ // but this provides a hook for future customization
- // Extract client identification
- if (data.has("client_name"))
- {
- client_name = data["client_name"].asString();
- LL_INFOS("ScriptEditorWS") << "Client name: " << client_name << LL_ENDL;
+ // Example of how custom methods could be registered:
+ // script_connection->registerMethod("script.custom", handler);
}
+}
- if (data.has("client_version"))
- {
- client_version = data["client_version"].asString();
- LL_INFOS("ScriptEditorWS") << "Client version: " << client_version << LL_ENDL;
- }
+//========================================================================
+LLScriptEdContainer* LLScriptEditorWSConnection::getEditor() const
+{
+ return mEditorPanel.isDead() ? nullptr : dynamic_cast<LLScriptEdContainer*>(mEditorPanel.get());
+}
- if (data.has("protocol_version"))
- {
- protocol_version = data["protocol_version"].asString();
- LL_INFOS("ScriptEditorWS") << "Protocol version: " << protocol_version << LL_ENDL;
- }
+std::shared_ptr<LLScriptEditorWSServer> LLScriptEditorWSConnection::getServer() const
+{
+ return std::static_pointer_cast<LLScriptEditorWSServer>(mOwningServer.lock());
+}
- // Extract client capabilities
- if (data.has("capabilities"))
- {
- client_capabilities = data["capabilities"];
- mEditorCapabilities = client_capabilities; // Store for later use
- LL_INFOS("ScriptEditorWS") << "Client capabilities count: " << client_capabilities.size() << LL_ENDL;
- }
+void LLScriptEditorWSConnection::onOpen()
+{
+ // Call parent class to set up JSON-RPC infrastructure
+ LLJSONRPCConnection::onOpen();
- // Extract supported languages
- if (data.has("supported_languages"))
- {
- supported_languages = data["supported_languages"];
- LL_INFOS("ScriptEditorWS") << "Supported languages count: " << supported_languages.size() << LL_ENDL;
- }
+ LL_INFOS("ScriptEditorWS") << "Script editor JSON-RPC connection opened" << LL_ENDL;
- // Extract client features
- if (data.has("features"))
- {
- client_features = data["features"];
- LL_INFOS("ScriptEditorWS") << "Client features available" << LL_ENDL;
- }
+ // Generate unique editor session ID
+ mEditorId = LLUUID::generateNewID().asString();
+ mEditorReady = false;
- // Generate or extract editor session ID
- if (data.has("session_id"))
- {
- mEditorId = data["session_id"].asString();
- LL_INFOS("ScriptEditorWS") << "Using client session ID: " << mEditorId << LL_ENDL;
- }
- else
- {
- // Generate a new session ID if client didn't provide one
- mEditorId = LLUUID::generateNewID().asString();
- LL_INFOS("ScriptEditorWS") << "Generated session ID: " << mEditorId << LL_ENDL;
- }
+ LL_INFOS("ScriptEditorWS") << "Initialized editor session: " << mEditorId << LL_ENDL;
- // Validate protocol compatibility
- bool protocol_compatible = true;
- if (!protocol_version.empty())
- {
- // For now, we only support protocol version "1.0"
- if (protocol_version != "1.0")
+ // Build hello data according to the protocol specification
+ LLSD handshake;
+ handshake["server_version"] = "1.0.0";
+ handshake["protocol_version"] = "1.0";
+ handshake["viewer_name"] = LLVersionInfo::instance().getChannel();
+ handshake["viewer_version"] = LLVersionInfo::instance().getVersion();
+
+ // Supported languages array
+ LLSD languages = LLSD::emptyArray();
+ languages.append("lsl");
+ languages.append("luau");
+ handshake["supported_languages"] = languages;
+
+ // Features object
+ LLSD features;
+ features["live_sync"] = true;
+ features["compilation"] = true;
+ features["syntax_highlight"] = true;
+ handshake["features"] = features;
+
+ // Send editor.handshake method call to the client and handle response
+ call("session.handshake", handshake, [this](const LLSD& result, const LLSD& error) {
+ if (error.isUndefined())
{
- protocol_compatible = false;
- LL_WARNS("ScriptEditorWS") << "Unsupported protocol version: " << protocol_version
- << ", expected: 1.0" << LL_ENDL;
+ handleHandshakeResponse(result);
}
- }
-
- // Determine if we have feature compatibility
- bool has_script_editing = false;
- if (client_capabilities.isArray())
- {
- for (LLSD::array_const_iterator it = client_capabilities.beginArray();
- it != client_capabilities.endArray(); ++it)
+ else
{
- if (it->asString() == "script_editing")
- {
- has_script_editing = true;
- break;
- }
+ LL_WARNS("ScriptEditorWS") << "Handshake failed: "
+ << error["message"].asString() << LL_ENDL;
}
- }
-
- // Build response message
- LLSD response;
- response["command"] = "connect_ack";
+ });
- LLSD& response_data = response["data"];
+ LL_INFOS("ScriptEditorWS") << "Sent handshake call to new editor client" << LL_ENDL;
+}
- if (protocol_compatible && has_script_editing)
- {
- // Successful connection
- response_data["status"] = "connected";
- response_data["message"] = "Connection established successfully";
- response_data["session_id"] = mEditorId;
- response_data["timestamp"] = LLDate::now().asString();
+void LLScriptEditorWSConnection::onClose()
+{
+ // Call parent class to clean up JSON-RPC infrastructure
+ LLJSONRPCConnection::onClose();
- // Send back our supported capabilities that match the client's
- LLSD mutual_capabilities = LLSD::emptyArray();
+ LL_INFOS("ScriptEditorWS") << "Script editor JSON-RPC connection closed for session: "
+ << mEditorId << LL_ENDL;
- // Check which capabilities we both support
- if (client_capabilities.isArray())
- {
- // Our server capabilities (from onConnectionOpened)
- std::set<std::string> server_caps = {
- "script_editing", "script_synchronization", "compilation",
- "metadata", "syntax_highlighting", "error_reporting"
- };
+ cleanupConnection();
- for (LLSD::array_const_iterator it = client_capabilities.beginArray();
- it != client_capabilities.endArray(); ++it)
- {
- std::string cap = it->asString();
- if (server_caps.count(cap) > 0)
- {
- mutual_capabilities.append(cap);
- }
- }
- }
+ // Clean up editor-specific state
+ mEditorId.clear();
+ mEditorCapabilities.clear();
+ mScriptId.clear();
+ mEditorReady = false;
- response_data["mutual_capabilities"] = mutual_capabilities;
+ // Clean up handshake response data
+ mClientName.clear();
+ mClientVersion.clear();
+ mProtocolVersion.clear();
+ mScriptName.clear();
+ mScriptLanguage.clear();
+ mLanguages.clear();
+ mFeatures.clear();
+}
- // Send supported languages intersection
- LLSD mutual_languages = LLSD::emptyArray();
- if (supported_languages.isArray())
- {
- std::set<std::string> server_languages = {"lsl", "luau"};
+void LLScriptEditorWSConnection::handleHandshakeResponse(const LLSD& result)
+{
+ LL_INFOS("ScriptEditorWS") << "Processing handshake response from client" << LL_ENDL;
- for (LLSD::array_const_iterator it = supported_languages.beginArray();
- it != supported_languages.endArray(); ++it)
- {
- std::string lang = it->asString();
- if (server_languages.count(lang) > 0)
- {
- mutual_languages.append(lang);
- }
- }
- }
- else
- {
- // Default to all our supported languages if client didn't specify
- mutual_languages.append("lsl");
- mutual_languages.append("luau");
- }
+ // Extract and validate client information
+ mClientName = result["client_name"].asString();
+ mClientVersion = result["client_version"].asString();
+ mProtocolVersion = result["protocol_version"].asString();
- response_data["supported_languages"] = mutual_languages;
+ // Validate protocol compatibility
+ if (mProtocolVersion != "1.0")
+ {
+ LL_WARNS("ScriptEditorWS") << "Protocol version mismatch. Expected: 1.0, Got: "
+ << mProtocolVersion << LL_ENDL;
+ }
- // Connection limits and constraints
- response_data["max_script_size"] = 65536;
- response_data["heartbeat_interval"] = 30;
+ // Store script information if provided
+ mScriptName = result["script_name"].asString();
+ mScriptLanguage = result["script_language"].asString();
+ mScriptId = result["script_id"].asString();
- LL_INFOS("ScriptEditorWS") << "Successfully connected client: " << client_name
- << " v" << client_version
- << " with " << mutual_capabilities.size() << " mutual capabilities" << LL_ENDL;
- }
- else
+ // Store supported languages
+ for (const auto& lang : llsd::inArray( result["languages"]))
{
- // Connection failed
- response_data["status"] = "rejected";
-
- if (!protocol_compatible)
+ if (lang.isString())
{
- response_data["error"] = "incompatible_protocol";
- response_data["message"] = "Unsupported protocol version: " + protocol_version;
+ mLanguages.insert(lang.asString());
}
- else if (!has_script_editing)
- {
- response_data["error"] = "missing_capabilities";
- response_data["message"] = "Client must support 'script_editing' capability";
- }
- else
+ }
+
+ for (const auto& [feature, enabled] : llsd::inMap(result["features"]))
+ {
+ if (enabled.asBoolean())
{
- response_data["error"] = "connection_failed";
- response_data["message"] = "Connection failed for unknown reason";
+ mFeatures.insert(feature);
}
+ }
+
+ connectToEditor(mScriptId);
+ // Mark editor as ready
+ mEditorReady = true;
+
+ LL_INFOS("ScriptEditorWS") << "Handshake completed successfully for session: " << mEditorId << LL_ENDL;
+}
- LL_WARNS("ScriptEditorWS") << "Rejected connection from client: " << client_name
- << " - " << response_data["message"].asString() << LL_ENDL;
+bool LLScriptEditorWSConnection::connectToEditor(const std::string& script_id)
+{
+ LLScriptEditorWSServer::ptr_t server = std::dynamic_pointer_cast<LLScriptEditorWSServer>(mOwningServer.lock());
+ if (!server)
+ {
+ LL_WARNS("ScriptEditorWS") << "Cannot connect to editor - server reference lost" << LL_ENDL;
+ return false;
}
- // Send the response back to the client
- if (sendMessage(response))
+ mEditorPanel = server->findEditorForScript(script_id);
+
+ LLScriptEdContainer* editor_core = getEditor();
+ if (!editor_core)
{
- LL_INFOS("ScriptEditorWS") << "Sent connect acknowledgment to client" << LL_ENDL;
+ LL_INFOS("ScriptEditorWS") << "Could not find editor: " << script_id << LL_ENDL;
+ // TODO: Disconnect the client if no editor found
+ return false;
}
- else
+
+ return true;
+}
+
+void LLScriptEditorWSConnection::cleanupConnection()
+{
+ LL_INFOS("ScriptEditorWS") << "Cleaning up connection for editor session: " << mEditorId << LL_ENDL;
+
+ LLScriptEditorWSServer::ptr_t server = getServer();
+ if (server)
{
- LL_WARNS("ScriptEditorWS") << "Failed to send connect acknowledgment to client" << LL_ENDL;
+ server->dissociateEditor(mScriptId);
}
- // If connection was successful, we could also send any initial state or configuration
- if (response_data["status"].asString() == "connected")
+ LLScriptEdContainer* editor_core = getEditor();
+
+ if (editor_core)
{
- // TODO: Send initial script list, active editors, or other relevant state
- // Example: sendScriptList(), sendActiveEditors(), etc.
+ editor_core->cleanupWebSocket();
+
+ // Notify the editor panel of disconnection
+ //editor_core->onExternalEditorDisconnected();
}
+
+ mEditorPanel = LLHandle<LLPanel>();
+}
+
+
+void LLScriptEditorWSConnection::sendDisconnect(S32 reason, const std::string& message)
+{
+ LL_INFOS("ScriptEditorWS") << "Sending disconnect message to editor (reason: "
+ << reason << ", message: " << message << ")" << LL_ENDL;
+
+ LLSD params;
+ params["reason"] = reason;
+ params["message"] = message;
+
+ notify("session.disconnect", params);
}
diff --git a/indra/newview/llscripteditorws.h b/indra/newview/llscripteditorws.h
index 73efb1a65f..78c675a1f4 100644
--- a/indra/newview/llscripteditorws.h
+++ b/indra/newview/llscripteditorws.h
@@ -26,7 +26,7 @@
#pragma once
-#include "llwebsocketmgr.h"
+#include "lljsonrpcws.h"
#include "llsd.h"
#include "lluuid.h"
#include "llhandle.h"
@@ -39,48 +39,43 @@
// Forward declarations
class LLLiveLSLEditor;
-class LLScriptEdCore;
+class LLScriptEdContainer;
+class LLScriptEditorWSServer;
/**
* @class LLScriptEditorWSConnection
- * @brief WebSocket connection specialized for external script editor communication
+ * @brief JSON-RPC WebSocket connection specialized for external script editor communication
*
- * This class handles WebSocket communication between the Second Life viewer
- * and external script editors. It manages script content synchronization,
- * compilation status updates, and editor metadata exchange.
+ * This class handles JSON-RPC 2.0 communication between the Second Life viewer
+ * and external script editors. It provides a clean base for implementing
+ * script editor integration using the standard JSON-RPC 2.0 protocol.
*
- * ## Message Protocol
- *
- * The connection uses JSON messages with the following structure:
- * - `type`: Message type identifier
- * - `data`: Message payload (varies by type)
- * - `timestamp`: Message timestamp for ordering
- * - `id`: Optional message ID for request/response correlation
- *
- * ### Supported Message Types:
+ * ## Usage
*
- * #### From Editor to Viewer:
- * - `script_updated`: Script content has been modified
- * - `save_request`: Request to save script to SL servers
- * - `compile_request`: Request to compile script
- * - `editor_ready`: Editor initialization complete
- * - `ping`: Connection health check
+ * @code
+ * // Create server and let base JSON-RPC handle method registration
+ * auto server = std::make_shared<LLScriptEditorWSServer>("script_editor_server", 9020);
*
- * #### From Viewer to Editor:
- * - `script_content`: Full script content
- * - `compile_result`: Compilation success/failure with errors
- * - `save_result`: Save operation result
- * - `metadata`: Script and object metadata
- * - `pong`: Response to ping
+ * // Register custom methods as needed
+ * connection->registerMethod("custom.method", handler);
+ * @endcode
*/
-class LLScriptEditorWSConnection : public LLWebsocketMgr::WSConnection
+class LLScriptEditorWSConnection : public LLJSONRPCConnection,
+ public std::enable_shared_from_this<LLScriptEditorWSConnection>
{
public:
+ enum DisconnectReason
+ {
+ REASON_NORMAL = 0,
+ REASON_EDITOR_CLOSED = 1,
+ REASON_PROTOCOL_ERROR = 2,
+ REASON_TIMEOUT = 3,
+ REASON_INTERNAL_ERROR = 4
+ };
LLScriptEditorWSConnection(const LLWebsocketMgr::WSServer::ptr_t server,
- const LLWebsocketMgr::connection_h& handle):
- LLWebsocketMgr::WSConnection(server, handle),
- mMessageSequence(0)
+ const LLWebsocketMgr::connection_h& handle)
+ : LLJSONRPCConnection(server, handle)
{ }
~LLScriptEditorWSConnection() override = default;
@@ -89,32 +84,57 @@ public:
// Connection lifecycle overrides
void onOpen() override;
void onClose() override;
- void onMessage(const std::string& message) override;
+
+ /**
+ * @brief Send session disconnect message to the external editor
+ * @param reason Numeric reason code for the disconnect (default 0 for normal closure)
+ * @param message Human-readable disconnect message (default "Goodbye")
+ */
+ void sendDisconnect(S32 reason = 0, const std::string& message = "Goodbye");
private:
+ using string_set_t = std::set<std::string>;
/**
- * @brief Handle connect/connection messages from editor
- * @param message Parsed LLSD message
+ * @brief Handle the handshake response from the client
+ * @param result The response data from the client containing client information
*/
- void processConnectMessage(const LLSD& message);
+ void handleHandshakeResponse(const LLSD& result);
+
+ bool connectToEditor(const std::string& script_id);
+ void cleanupConnection();
+
+ LLScriptEdContainer* getEditor() const;
+ std::shared_ptr<LLScriptEditorWSServer> getServer() const;
std::string mEditorId; ///< Unique identifier for this editor session
LLSD mEditorCapabilities; ///< Editor capabilities metadata
- U32 mMessageSequence; ///< Message sequence counter
std::string mScriptId; ///< Unique identifier for the script being edited
+ bool mEditorReady; ///< Whether editor has completed initialization
+ LLHandle<LLPanel> mEditorPanel; ///< Handle to the associated LSL editor panel
+
+ // Client handshake response data
+ std::string mClientName; ///< Name of the external editor client
+ std::string mClientVersion; ///< Version of the external editor client
+ std::string mProtocolVersion; ///< JSON-RPC protocol version supported by client
+ std::string mScriptName; ///< Name of the script being edited
+ std::string mScriptLanguage; ///< Programming language of the script (lsl, luau, etc.)
+ string_set_t mLanguages; ///< Set of supported scripting languages
+ string_set_t mFeatures; ///< Active client features (live_sync, compilation, etc.)
};
/**
* @class LLScriptEditorWSServer
- * @brief WebSocket server for external script editor integration
+ * @brief JSON-RPC 2.0 WebSocket server for external script editor integration
*
- * This server manages WebSocket connections from external script editors,
- * providing a bridge between the Second Life viewer's script editing
- * functionality and external development tools.
+ * This server extends the JSON-RPC server to provide specialized functionality
+ * for external script editor integration. It manages WebSocket connections from
+ * external script editors and provides a structured JSON-RPC 2.0 interface
+ * between the Second Life viewer's script editing functionality and external
+ * development tools.
*
* ## Architecture
*
- * The server acts as a communication hub between:
+ * The server acts as a JSON-RPC communication hub between:
* - LLLiveLSLEditor instances (in-world script editing)
* - External script editors (VS Code, Atom, Sublime Text, etc.)
* - Script compilation and save services
@@ -122,8 +142,8 @@ private:
* ## Usage
*
* @code
- * // Create and start the server
- * auto server = std::make_shared<LLScriptEditorWSServer>("script_editor_server", 8080);
+ * // Create and start the JSON-RPC server
+ * auto server = std::make_shared<LLScriptEditorWSServer>("script_editor_server", 9020);
* LLWebsocketMgr::getInstance()->addServer(server);
* LLWebsocketMgr::getInstance()->startServer("script_editor_server");
*
@@ -134,11 +154,11 @@ private:
* ## Security Considerations
*
* - Server binds to localhost only by default for security
- * - Editor authentication via connection handshake
- * - Script content encryption for sensitive projects
- * - Rate limiting to prevent abuse
+ * - JSON-RPC 2.0 structured protocol with validation
+ * - Rate limiting handled by base JSON-RPC server
+ * - Error handling with standardized JSON-RPC error codes
*/
-class LLScriptEditorWSServer : public LLWebsocketMgr::WSServer
+class LLScriptEditorWSServer : public LLJSONRPCServer
{
public:
static constexpr char const* DEFAULT_SERVER_NAME = "script_editor_server";
@@ -146,7 +166,7 @@ public:
using ptr_t = std::shared_ptr<LLScriptEditorWSServer>;
- LLScriptEditorWSServer(const std::string_view name, U16 port, bool local_only = true);
+ LLScriptEditorWSServer(const std::string& name, U16 port, bool local_only = true);
virtual ~LLScriptEditorWSServer() = default;
@@ -166,14 +186,40 @@ public:
*/
std::set<std::string> getActiveScripts() const;
+ /**
+ * @brief Send script content to all connected editors for a specific script
+ * @param script_id The script identifier
+ * @param content The script content
+ * @param metadata Optional metadata about the script
+ */
+ void broadcastScriptUpdate(const std::string& script_id, const std::string& content, const LLSD& metadata = LLSD());
+
+ /**
+ * @brief Send compilation results to all connected editors for a specific script
+ * @param script_id The script identifier
+ * @param success Whether compilation succeeded
+ * @param errors Array of compilation errors/warnings
+ */
+ void broadcastCompilationResult(const std::string& script_id, bool success, const LLSD& errors = LLSD());
+
protected:
- LLWebsocketMgr::WSConnection::ptr_t connectionFactory(WSServer::ptr_t server, LLWebsocketMgr::connection_h handle) override;
+ LLWebsocketMgr::WSConnection::ptr_t connectionFactory(LLWebsocketMgr::WSServer::ptr_t server,
+ LLWebsocketMgr::connection_h handle) override;
+
+ /**
+ * @brief Apply global method handlers to a new connection
+ * @param connection The connection to configure
+ *
+ * Override this method to customize which methods are registered on
+ * new connections. The base implementation registers all global methods,
+ * but derived classes can add additional script-specific methods.
+ */
+ virtual void setupConnectionMethods(LLJSONRPCConnection::ptr_t connection) override;
private:
- using map_id_to_editor_t = std::unordered_map<std::string, LLHandle<LLPanel> >;
+ using map_id_to_editor_t = std::unordered_map<std::string, LLHandle<LLPanel>>;
map_id_to_editor_t mScriptEditors;
-
std::set<std::shared_ptr<LLScriptEditorWSConnection>> mActiveConnections;
/**