From dcf6d8c5024c831d54f0f5f140551e65748ed513 Mon Sep 17 00:00:00 2001 From: Rider Linden Date: Fri, 22 Aug 2025 16:16:23 -0700 Subject: Initial simple websockets server with managing singleton. --- indra/llcorehttp/llwebsocketmgr.cpp | 411 ++++++++++++++++++++++++++++++++++++ 1 file changed, 411 insertions(+) create mode 100644 indra/llcorehttp/llwebsocketmgr.cpp (limited to 'indra/llcorehttp/llwebsocketmgr.cpp') diff --git a/indra/llcorehttp/llwebsocketmgr.cpp b/indra/llcorehttp/llwebsocketmgr.cpp new file mode 100644 index 0000000000..0c61e0f68d --- /dev/null +++ b/indra/llcorehttp/llwebsocketmgr.cpp @@ -0,0 +1,411 @@ +/** + * @file llwebsocketmgr.cpp + * @brief WebSocket manager singleton 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 "llwebsocketmgr.h" +#include "llerror.h" +#include "llsdserialize.h" +#include "llhost.h" + +#include +#include +#include +#include + +//------------------------------------------------------------------------ +namespace +{ + using Server_t = websocketpp::server; + using Client_t = websocketpp::client; + using Connection_t = websocketpp::connection; +} + +//------------------------------------------------------------------------ + +//------------------------------------------------------------------------ +// LLWebsocketMgr Implementation +// + +void LLWebsocketMgr::initSingleton() +{ } + +void LLWebsocketMgr::cleanupSingleton() +{ + stopAllServers(); +} + +LLWebsocketMgr::WSServer::ptr_t LLWebsocketMgr::findServerByName(const std::string &name) const +{ + auto it = mServers.find(std::string(name)); + if (it != mServers.end()) + { + return it->second; + } + return nullptr; +} + +bool LLWebsocketMgr::addServer(const LLWebsocketMgr::WSServer::ptr_t& server) +{ + if (!server) + { + LL_WARNS("WebSocket") << "Attempted to add a null server" << LL_ENDL; + return false; + } + + auto it = mServers.find(server->mServerName); + if (it != mServers.end()) + { + LL_WARNS("WebSocket") << "Server with name " << server->mServerName << " already exists" << LL_ENDL; + return false; + } + mServers[server->mServerName] = server; + LL_INFOS("WebSocket") << "Added WebSocket server: " << server->mServerName << LL_ENDL; + return true; +} + +bool LLWebsocketMgr::removeServer(const std::string& name) +{ + auto it = mServers.find(name); + if (it == mServers.end()) + { + LL_WARNS("WebSocket") << "No server found with name " << name << " to remove" << LL_ENDL; + return false; + } + if (it->second && it->second->isRunning()) + it->second->stop(); + mServers.erase(it); + LL_INFOS("WebSocket") << "Removed WebSocket server: " << name << LL_ENDL; + return true; +} + + +bool LLWebsocketMgr::startServer(const std::string &name) const +{ + LLWebsocketMgr::WSServer::ptr_t server = findServerByName(name); + if (!server) + { + LL_WARNS("WebSocket") << "No server found with name " << name << " to start" << LL_ENDL; + return false; + } + if (server->isRunning()) + { + LL_WARNS("WebSocket") << "Server " << name << " is already running" << LL_ENDL; + return false; + } + return server->start(); +} + +void LLWebsocketMgr::stopServer(const std::string& name) const +{ + LLWebsocketMgr::WSServer::ptr_t server = findServerByName(name); + if (!server) + { + LL_WARNS("WebSocket") << "No server found with name " << name << " to stop" << LL_ENDL; + return; + } + if (!server->isRunning()) + { + LL_WARNS("WebSocket") << "Server " << name << " is not running" << LL_ENDL; + return; + } + server->stop(); +} + +void LLWebsocketMgr::stopAllServers() +{ + for (auto &[name, server] : mServers) + { + if (server && server->isRunning()) + { + LL_INFOS("WebSocket") << "Stopping server: " << name << LL_ENDL; + server->stop(); + } + } + + mServers.clear(); +} + + +//------------------------------------------------------------------------ +struct Server_impl +{ + Server_impl(LLWebsocketMgr::WSServer *owner, U16 port, bool local_only) : + mOwner(owner), + 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); }); + } + + ~Server_impl() = default; + + void init() + { + mServer.init_asio(); + if (mLocalOnly) + { + std::stringstream port_str; + port_str << mPort; + mServer.listen("127.0.0.1", port_str.str()); + } + else + { + mServer.listen(mPort); + } + } + + bool start() + { + if (!mServer.stopped()) + { + LL_WARNS("WebSocket") << "WebSocket server is already running" << LL_ENDL; + return false; + } + try + { + mServer.start_accept(); + mServer.run(); + } + catch (const std::exception& e) + { + LL_WARNS("WebSocket") << "WebSocket server encountered an error: " << e.what() << LL_ENDL; + return false; + } + return true; + } + + void stop() + { + if (mServer.stopped()) + { + return; + } + try + { + mServer.stop_listening(); + mServer.stop(); + } + catch (const std::exception&) + { + LL_WARNS("WebSocket") << "Error stopping WebSocket server" << LL_ENDL; + } + } + + void onOpen(websocketpp::connection_hdl hdl) const + { + assert(mOwner); // mOwner should never be null. If it is, something is very wrong! + + mOwner->handleOpenConnection(hdl); + } + + void onClose(websocketpp::connection_hdl hdl) const + { + assert(mOwner); // mOwner should never be null + mOwner->handleCloseConnection(hdl); + } + + void onMessage(websocketpp::connection_hdl hdl, Server_t::message_ptr msg) const + { + assert(mOwner); // mOwner should never be null + LLWebsocketMgr::WSConnection::ptr_t connection = mOwner->getConnection(hdl); + if (!connection) + { + LL_WARNS("WebSocket") << "Received message for unknown connection" << LL_ENDL; + return; + } + + // TODO: check the FIN bit and handle fragmented messages if needed + // TODO: check terminal and close codes and handle connection closure if needed + // TODO: handle binary messages if needed + mOwner->handleMessage(hdl, msg->get_payload()); + } + + //------------------------------------------- + Server_t mServer; + LLWebsocketMgr::WSServer* mOwner{ nullptr }; // Back-reference to the owning server, note that this pointer can never be null + U16 mPort{ 0 }; + bool mLocalOnly{ true }; +}; + +//------------------------------------------------------------------------ +LLWebsocketMgr::WSServer::WSServer(std::string_view name, U16 port, bool local_only): + mServerName(name), + mImpl(std::make_unique(this, port, local_only)) +{ + mImpl->init(); + + // Initialize the server with the given name and host + LL_INFOS("WebSocket") << "Creating WebSocket server: " << name << + " listening " << (mImpl->mLocalOnly ? "locally" : "ON ALL INTERFACES") << + " on port " << mImpl->mPort << LL_ENDL; +} + +LLWebsocketMgr::WSConnection::ptr_t LLWebsocketMgr::WSServer::connectionFactory(LLWebsocketMgr::WSServer::ptr_t server, + LLWebsocketMgr::connection_h handle) +{ + return std::make_shared(server, handle); +} + +bool LLWebsocketMgr::WSServer::start() +{ + LL_ERRS_IF(!mImpl, "WebSocket") << "WebSocket server " << mServerName << " implementation is null !" << LL_ENDL; + return mImpl->start(); +} + +void LLWebsocketMgr::WSServer::stop() +{ + LL_ERRS_IF(!mImpl, "WebSocket") << "WebSocket server " << mServerName << " implementation is null !" << LL_ENDL; + mImpl->stop(); +} + +bool LLWebsocketMgr::WSServer::isRunning() const +{ + LL_ERRS_IF(!mImpl, "WebSocket") << "WebSocket server " << mServerName << " implementation is null !" << LL_ENDL; + return !mImpl->mServer.stopped(); +} + +void LLWebsocketMgr::WSServer::broadcastMessage(const std::string& message) +{ + LL_ERRS_IF(!mImpl, "WebSocket") << "WebSocket server " << mServerName << " implementation is null !" << LL_ENDL; + LLMutexLock lock(&mConnectionMutex); + for (const auto& [handle, conn] : mConnections) + { + sendMessageTo(handle, message); + } +} + +bool LLWebsocketMgr::WSServer::sendMessageTo(const connection_h& handle, const std::string& message) +{ + LL_ERRS_IF(!mImpl, "WebSocket") << "WebSocket server " << mServerName << " implementation is null !" << LL_ENDL; + websocketpp::lib::error_code ec; + mImpl->mServer.send(handle, message, websocketpp::frame::opcode::text, ec); + if (ec) + { + LL_WARNS("WebSocket") << mServerName << " failed to send message: " << ec.message() << LL_ENDL; + return false; + } + return true; +} + +LLWebsocketMgr::WSConnection::ptr_t LLWebsocketMgr::WSServer::getConnection(const connection_h& handle) +{ + LLMutexLock lock(&mConnectionMutex); + auto it = mConnections.find(handle); + if (it != mConnections.end()) + { + return it->second; + } + return nullptr; +} + +void LLWebsocketMgr::WSServer::handleOpenConnection(const connection_h& handle) +{ + WSConnection::ptr_t connection; + size_t size(0); + { + LLMutexLock lock(&mConnectionMutex); + auto it = mConnections.find(handle); + if (it == mConnections.end()) + { + connection = connectionFactory(shared_from_this(), handle); + if (!connection) + { + LL_WARNS("WebSocket") << "Failed to create connection for websocket server " << mServerName << LL_ENDL; + return; + } + mConnections[handle] = connection; + } + else + { + connection = it->second; + } + + if (!connection) + { + LL_WARNS("WebSocket") << mServerName << " failed to create connection object" << LL_ENDL; + return; + } + mConnections[handle] = connection; + size = mConnections.size(); + } + + onConnectionOpened(connection); // TODO: consider letting the server reject the connection here + connection->onOpen(); + LL_INFOS("WebSocket") << mServerName << " opened new connection, total connections: " << size << LL_ENDL; +} + +void LLWebsocketMgr::WSServer::handleCloseConnection(const connection_h& handle) +{ + size_t size(0); + WSConnection::ptr_t connection; + { + LLMutexLock lock(&mConnectionMutex); + auto it = mConnections.find(handle); + if (it != mConnections.end()) + { + connection = it->second; + mConnections.erase(it); + } + size = mConnections.size(); + } + if (connection) + { + connection->onClose(); + onConnectionClosed(connection); + LL_INFOS("WebSocket") << mServerName << " closed connection, total connections: " << size << LL_ENDL; + } + else + { + LL_WARNS("WebSocket") << mServerName << " attempted to close unknown connection" << LL_ENDL; + } +} + +void LLWebsocketMgr::WSServer::handleMessage(const connection_h& handle, const std::string& message) +{ + WSConnection::ptr_t connection = getConnection(handle); + if (connection) + { + connection->onMessage(message); + } + else + { + LL_WARNS("WebSocket") << mServerName << " received message for unknown connection" << LL_ENDL; + } +} + +//------------------------------------------------------------------------ +bool LLWebsocketMgr::WSConnection::sendMessage(const std::string& message) +{ + if (!mServer) + { + LL_WARNS("WebSocket") << "Attempted to send message on connection with null server reference" << LL_ENDL; + return false; + } + return mServer->sendMessageTo(mConnectionHandle, message); +} -- cgit v1.3 From e27b363a9a315a5fce53d0d036095ab33e37eee3 Mon Sep 17 00:00:00 2001 From: Rider Linden Date: Fri, 22 Aug 2025 18:32:45 -0700 Subject: Finish blocking run and adding documentation. --- indra/llcorehttp/llwebsocketmgr.cpp | 287 ++++++++++++++++++++++++++++++++++-- indra/llcorehttp/llwebsocketmgr.h | 241 ++++++++++++++++++++++-------- 2 files changed, 452 insertions(+), 76 deletions(-) (limited to 'indra/llcorehttp/llwebsocketmgr.cpp') diff --git a/indra/llcorehttp/llwebsocketmgr.cpp b/indra/llcorehttp/llwebsocketmgr.cpp index 0c61e0f68d..9fc9027be3 100644 --- a/indra/llcorehttp/llwebsocketmgr.cpp +++ b/indra/llcorehttp/llwebsocketmgr.cpp @@ -36,6 +36,10 @@ #include #include +#include +#include +#include + //------------------------------------------------------------------------ namespace { @@ -149,10 +153,39 @@ void LLWebsocketMgr::stopAllServers() mServers.clear(); } - //------------------------------------------------------------------------ +/** + * @struct Server_impl + * @brief Internal implementation wrapper for websocketpp server functionality + * + * This structure serves as a PIMPL (Pointer to Implementation) wrapper around + * the websocketpp::server template, providing a clean interface between the + * high-level WSServer class and the low-level websocketpp library. It handles + * all direct websocketpp interactions including server lifecycle management, + * event handling, and connection management. + * + * The Server_impl follows Linden Lab conventions while maintaining compatibility + * with the websocketpp library. It provides thread-safe operations where possible + * and integrates with the existing logging infrastructure. + * + * @note This class uses the websocketpp::config::asio configuration which provides + * ASIO-based networking without TLS/SSL support. + */ struct Server_impl { + /** + * @brief Constructor - Initializes the websocketpp server with configuration + * @param owner Pointer to the owning WSServer instance (must not be null) + * @param port The port number to bind the server to (1-65535) + * @param local_only If true, binds only to localhost; if false, binds to all interfaces + * + * Sets up the websocketpp server instance and registers lambda-based event handlers + * for connection open, close, and message events. The handlers delegate back to the + * owning WSServer instance for processing, maintaining the abstraction layer. + * + * @warning The owner pointer must remain valid for the lifetime of this object + * @pre port must be a valid port number (typically > 1024 for non-privileged access) + */ Server_impl(LLWebsocketMgr::WSServer *owner, U16 port, bool local_only) : mOwner(owner), mPort(port), @@ -165,6 +198,19 @@ struct Server_impl ~Server_impl() = default; + /** + * @brief Initialize the websocketpp server and configure listening + * + * Performs the initial setup of the websocketpp server by calling init_asio() + * to initialize the ASIO networking layer, then configures the server to listen + * on the specified port. The binding behavior depends on the mLocalOnly flag: + * - If mLocalOnly is true: binds to "127.0.0.1" (localhost only) + * - If mLocalOnly is false: binds to all available network interfaces + * + * @note This method must be called before attempting to start the server + * @pre The server must not already be initialized + * @post The server is ready to accept connections when start() is called + */ void init() { mServer.init_asio(); @@ -180,6 +226,20 @@ struct Server_impl } } + /** + * @brief Start the websocket server and begin accepting connections + * @return true if server started successfully, false on error + * + * Runs a controlled event loop that periodically checks the stop flag for clean shutdown. + * Instead of calling run() once and blocking indefinitely, this implementation uses + * run_for() with a timeout to process events in chunks, checking mOwner->mShouldStop between + * iterations to allow for responsive termination. + * + * @note This method blocks the calling thread until the server stops + * @pre init() must have been called successfully + * @post On success, the server is actively accepting connections + * @warning Any exceptions during startup are caught and logged as warnings + */ bool start() { if (!mServer.stopped()) @@ -187,19 +247,61 @@ struct Server_impl LL_WARNS("WebSocket") << "WebSocket server is already running" << LL_ENDL; return false; } + try { mServer.start_accept(); - mServer.run(); + + // Run controlled event loop with periodic stop flag checking + while (!mOwner->mShouldStop && !mServer.stopped()) + { + // Process events for up to 100ms, then check the stop flag + std::chrono::milliseconds timeout(100); + std::size_t handlers_run = mServer.get_io_service().run_for(timeout); + + // If no handlers were run and the server isn't stopped, + // reset the io_service for the next iteration + if (handlers_run == 0 && !mServer.stopped() && !mOwner->mShouldStop) + { + mServer.get_io_service().restart(); + } + } + + LL_INFOS("WebSocket") << "WebSocket server event loop exited cleanly" << LL_ENDL; + return true; + } + catch (const websocketpp::exception& e) + { + LL_WARNS("WebSocket") << "WebSocket server exception: " << e.what() << LL_ENDL; + return false; } catch (const std::exception& e) { - LL_WARNS("WebSocket") << "WebSocket server encountered an error: " << e.what() << LL_ENDL; + LL_WARNS("WebSocket") << "WebSocket server std::exception: " << e.what() << LL_ENDL; + return false; + } + catch (...) + { + LL_WARNS("WebSocket") << "WebSocket server unknown exception" << LL_ENDL; return false; } - return true; } + /** + * @brief Stop the websocket server and cease accepting new connections + * + * Gracefully shuts down the server by first stopping the listener to prevent + * new connections, then stopping the ASIO event loop. Existing connections + * may remain active briefly during the shutdown process. + * + * The method performs a safe shutdown by checking if the server is already + * stopped before attempting shutdown operations. Any exceptions during shutdown + * are caught and logged but do not propagate. + * + * @note This method is non-blocking and safe to call multiple times + * @post The server will no longer accept new connections + * @post Existing connections will be cleanly terminated + */ void stop() { if (mServer.stopped()) @@ -217,22 +319,65 @@ struct Server_impl } } + /** + * @brief Handle new connection establishment event + * @param hdl WebSocket connection handle from websocketpp + * + * Called automatically by the websocketpp library when a new client connection + * is successfully established. This method serves as a bridge between the + * low-level websocketpp callback and the high-level WSServer interface. + * + * @note This is an internal callback method called by websocketpp + * @pre mOwner must be valid (assertion will fail if null) + * @post The owning WSServer will be notified of the new connection + */ void onOpen(websocketpp::connection_hdl hdl) const { - assert(mOwner); // mOwner should never be null. If it is, something is very wrong! + LL_ERRS_IF(!mOwner, "WebSocket") << "mOwner should never be null. If it is, something is very wrong!" << LL_ENDL; mOwner->handleOpenConnection(hdl); } + /** + * @brief Handle connection closure event + * @param hdl WebSocket connection handle from websocketpp + * + * Called automatically by the websocketpp library when a client connection + * is closed, either by the client, server, or due to a network error. + * This method delegates the event to the owning WSServer for processing. + * + * @note This is an internal callback method called by websocketpp + * @pre mOwner must be valid (assertion will fail if null) + * @post The owning WSServer will be notified of the connection closure + */ void onClose(websocketpp::connection_hdl hdl) const { - assert(mOwner); // mOwner should never be null + LL_ERRS_IF(!mOwner, "WebSocket") << "mOwner should never be null" << LL_ENDL; mOwner->handleCloseConnection(hdl); } + /** + * @brief Handle incoming message from client + * @param hdl WebSocket connection handle identifying the sender + * @param msg Shared pointer to the message object containing payload and metadata + * + * Called automatically by the websocketpp library when a complete message is + * received from a client. This method validates the connection exists, extracts + * the message payload, and forwards it to the appropriate connection handler. + * + * Currently handles text messages only. Binary message support and message + * fragmentation handling are noted as TODO items for future implementation. + * + * @note This is an internal callback method called by websocketpp + * @pre mOwner must be valid (assertion will fail if null) + * @post If connection exists, the message is forwarded for processing + * @todo Add support for binary messages + * @todo Implement fragmented message handling + * @todo Process connection close codes for graceful closure + */ void onMessage(websocketpp::connection_hdl hdl, Server_t::message_ptr msg) const { - assert(mOwner); // mOwner should never be null + LL_ERRS_IF(!mOwner, "WebSocket") << "mOwner should never be null" << LL_ENDL; LLWebsocketMgr::WSConnection::ptr_t connection = mOwner->getConnection(hdl); if (!connection) { @@ -247,10 +392,10 @@ struct Server_impl } //------------------------------------------- - Server_t mServer; - LLWebsocketMgr::WSServer* mOwner{ nullptr }; // Back-reference to the owning server, note that this pointer can never be null - U16 mPort{ 0 }; - bool mLocalOnly{ true }; + Server_t mServer; ///< The underlying websocketpp server instance + LLWebsocketMgr::WSServer* mOwner{ nullptr }; ///< Back-reference to the owning WSServer instance (guaranteed non-null) + U16 mPort{ 0 }; ///< TCP port number the server listens on + bool mLocalOnly{ true }; ///< Whether to bind to localhost only (true) or all interfaces (false) }; //------------------------------------------------------------------------ @@ -266,6 +411,12 @@ LLWebsocketMgr::WSServer::WSServer(std::string_view name, U16 port, bool local_o " on port " << mImpl->mPort << LL_ENDL; } +LLWebsocketMgr::WSServer::~WSServer() +{ + // Ensure the server is stopped before destruction + stop(); +} + LLWebsocketMgr::WSConnection::ptr_t LLWebsocketMgr::WSServer::connectionFactory(LLWebsocketMgr::WSServer::ptr_t server, LLWebsocketMgr::connection_h handle) { @@ -275,19 +426,75 @@ LLWebsocketMgr::WSConnection::ptr_t LLWebsocketMgr::WSServer::connectionFactory( bool LLWebsocketMgr::WSServer::start() { LL_ERRS_IF(!mImpl, "WebSocket") << "WebSocket server " << mServerName << " implementation is null !" << LL_ENDL; - return mImpl->start(); + + LLMutexLock lock(&mThreadMutex); + + // Check if already running + if (isRunning()) + { + LL_WARNS("WebSocket") << "Server " << mServerName << " is already running" << LL_ENDL; + return false; + } + + // Reset the stop flag + mShouldStop = false; + + // Start the server thread + mServerThread = std::thread([this]() { + LL_INFOS("WebSocket") << "WebSocket server thread starting for: " << mServerName << LL_ENDL; + + // Run the controlled server loop that checks the stop flag + // Server_impl accesses mShouldStop through the mOwner pointer + bool success = mImpl->start(); + + if (!success) + { + LL_WARNS("WebSocket") << "WebSocket server thread failed to start for: " << mServerName << LL_ENDL; + } + + LL_INFOS("WebSocket") << "WebSocket server thread exiting for: " << mServerName << LL_ENDL; + }); + + LL_INFOS("WebSocket") << "Started WebSocket server thread: " << mServerName << LL_ENDL; + return true; } void LLWebsocketMgr::WSServer::stop() { LL_ERRS_IF(!mImpl, "WebSocket") << "WebSocket server " << mServerName << " implementation is null !" << LL_ENDL; - mImpl->stop(); + + { + LLMutexLock lock(&mThreadMutex); + + // Check if already stopped + if (!isRunning()) + { + return; + } + + LL_INFOS("WebSocket") << "Stopping WebSocket server: " << mServerName << LL_ENDL; + + // Signal the thread to stop + mShouldStop = true; + + // Stop the websocket server (this will cause the controlled run loop to exit) + mImpl->stop(); + } // Release the lock here + + // Wait for the thread to finish (outside the lock to avoid deadlock) + if (mServerThread.joinable()) + { + mServerThread.join(); + LL_INFOS("WebSocket") << "WebSocket server thread joined for: " << mServerName << LL_ENDL; + } } bool LLWebsocketMgr::WSServer::isRunning() const { LL_ERRS_IF(!mImpl, "WebSocket") << "WebSocket server " << mServerName << " implementation is null !" << LL_ENDL; - return !mImpl->mServer.stopped(); + + // Check both the thread state, websocket server state, and the stop flag + return mServerThread.joinable() && !mImpl->mServer.stopped() && !mShouldStop; } void LLWebsocketMgr::WSServer::broadcastMessage(const std::string& message) @@ -313,6 +520,41 @@ bool LLWebsocketMgr::WSServer::sendMessageTo(const connection_h& handle, const s return true; } +bool LLWebsocketMgr::WSServer::closeConnection(const connection_h& handle, U16 code, const std::string& reason) +{ + LL_ERRS_IF(!mImpl, "WebSocket") << "WebSocket server " << mServerName << " implementation is null !" << LL_ENDL; + + try + { + websocketpp::lib::error_code ec; + mImpl->mServer.close(handle, code, reason, ec); + if (ec) + { + LL_WARNS("WebSocket") << mServerName << " failed to close connection: " << ec.message() << LL_ENDL; + return false; + } + + LL_INFOS("WebSocket") << mServerName << " initiated close for connection with code " + << code << " and reason: " << reason << LL_ENDL; + return true; + } + catch (const websocketpp::exception& e) + { + LL_WARNS("WebSocket") << mServerName << " exception closing connection: " << e.what() << LL_ENDL; + return false; + } + catch (const std::exception& e) + { + LL_WARNS("WebSocket") << mServerName << " std::exception closing connection: " << e.what() << LL_ENDL; + return false; + } + catch (...) + { + LL_WARNS("WebSocket") << mServerName << " unknown exception closing connection" << LL_ENDL; + return false; + } +} + LLWebsocketMgr::WSConnection::ptr_t LLWebsocketMgr::WSServer::getConnection(const connection_h& handle) { LLMutexLock lock(&mConnectionMutex); @@ -409,3 +651,20 @@ bool LLWebsocketMgr::WSConnection::sendMessage(const std::string& message) } return mServer->sendMessageTo(mConnectionHandle, message); } + +void LLWebsocketMgr::WSConnection::closeConnection(U16 code, const std::string& reason) +{ + if (!mServer) + { + LL_WARNS("WebSocket") << "Attempted to close connection with null server reference" << LL_ENDL; + return; + } + + LL_INFOS("WebSocket") << "WSConnection closing connection with code " << code + << " and reason: " << (reason.empty() ? "(no reason)" : reason) << LL_ENDL; + + if (!mServer->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 8cf0562262..5634acde01 100644 --- a/indra/llcorehttp/llwebsocketmgr.h +++ b/indra/llcorehttp/llwebsocketmgr.h @@ -36,69 +36,11 @@ #include #include #include +#include +#include #include -#if 0 -// Forward declarations -namespace websocketpp { - namespace config { - struct asio_client; - struct asio; - } - template - class client; - template - class server; - class connection_hdl; -} - -namespace LLCore { - namespace WebSocket { - - /// WebSocket connection state enumeration - enum class ConnectionState - { - DISCONNECTED, - CONNECTING, - CONNECTED, - DISCONNECTING, - FAILED - }; - - /// WebSocket message types - enum class MessageType - { - TEXT, - BINARY - }; - - /// WebSocket event types for callbacks - enum class EventType - { - OPEN, - CLOSE, - MESSAGE, - ERROR - }; - - /// Forward declarations for internal classes - class WSConnection; - class WSServer; - - /// Callback function types - using EventCallback = std::function; - using MessageCallback = std::function; - - /// WebSocket connection handle type - using ConnectionHandle = LLUUID; - using ServerHandle = LLUUID; - - } // namespace WebSocket -} // namespace LLCore -#endif - - struct Server_impl; /** @@ -126,6 +68,12 @@ public: public: using ptr_t = std::shared_ptr; + + /** + * @brief Constructor for WSConnection + * @param server Shared pointer to the parent WSServer + * @param handle WebSocket connection handle from websocketpp + */ WSConnection(const std::shared_ptr &server, const connection_h& handle): mConnectionHandle(handle), mServer(server) @@ -133,17 +81,169 @@ public: virtual ~WSConnection() = default; + /** + * @brief Called when the connection is opened + * + * Override this method in derived classes to handle connection establishment. + * This is called after the WebSocket handshake is complete and the connection + * is ready to send/receive messages. + */ virtual void onOpen() {} + + /** + * @brief Called when the connection is closed + * + * Override this method in derived classes to handle connection closure. + * This is called when the connection has been terminated, either normally + * or due to an error condition. + */ virtual void onClose() {} + + /** + * @brief Called when a message is received + * @param message The received message as a string + * + * Override this method in derived classes to handle incoming messages. + * Currently only text messages are supported. + * + * @code + * class MyConnection : public LLWebsocketMgr::WSConnection + * { + * public: + * void onMessage(const std::string& message) override + * { + * // Parse and handle the message + * if (message == "ping") { + * sendMessage("pong"); + * } + * // Process JSON messages + * try { + * LLSD data = LLSDSerialize::fromJSON(message); + * handleStructuredMessage(data); + * } catch (...) { + * LL_WARNS("MyConnection") << "Invalid JSON received" << LL_ENDL; + * } + * } + * }; + * @endcode + */ virtual void onMessage(const std::string& message) {} - bool sendMessage(const std::string& message); + /** + * @brief Send a message to the connected client + * @param message The message string to send + * @return true if the message was queued successfully, false on error + * + * Sends a text message to the remote endpoint. The message is queued + * asynchronously and may not be sent immediately. + * + * @code + * // Send a simple text message + * connection->sendMessage("Hello, client!"); + * + * // Send JSON data + * LLSD response; + * response["status"] = "ok"; + * response["data"] = "some data"; + * connection->sendMessage(LLSDSerialize::toJSON(response)); + * @endcode + */ + bool sendMessage(const std::string& message); + + /** + * @brief Close the WebSocket connection gracefully + * @param code Optional close code (default: normal closure) + * @param reason Optional reason string (default: empty) + * + * Initiates a graceful WebSocket close handshake. The connection will + * send a close frame with the specified code and reason, then wait for + * the remote endpoint to respond with its own close frame before + * actually closing the underlying TCP connection. + * + * Common close codes: + * - 1000: Normal closure (default) + * - 1001: Going away (server shutting down, page navigating away) + * - 1002: Protocol error + * - 1003: Unsupported data type + * - 1008: Policy violation + * - 1009: Message too big + * + * @code + * // Normal closure + * connection->closeConnection(); + * + * // Close with specific reason + * connection->closeConnection(1000, "Session ended"); + * + * // Close due to policy violation + * connection->closeConnection(1008, "Authentication failed"); + * @endcode + * + * @note After calling this method, no further messages should be sent + * @note The onClose() callback will be invoked when the close handshake completes + */ + void closeConnection(U16 code = 1000, const std::string& reason = std::string()); private: connection_h mConnectionHandle; std::shared_ptr mServer; // Back-reference to the server this connection belongs to }; + /** + * @class WSServer + * @brief Base class for WebSocket servers with customizable connection handling + * + * WSServer provides a high-level abstraction over websocketpp servers, handling + * threading, connection management, and event dispatching. Derive from this class + * to create custom WebSocket servers with application-specific logic. + * + * ## Basic Usage + * + * @code + * class MyServer : public LLWebsocketMgr::WSServer + * { + * public: + * MyServer(const std::string& name, U16 port) + * : WSServer(name, port, false) // Listen on all interfaces + * {} + * + * void onConnectionOpened(const WSConnection::ptr_t& connection) override + * { + * LL_INFOS("MyServer") << "New client connected" << LL_ENDL; + * // Send welcome message + * connection->sendMessage("Welcome to the server!"); + * } + * + * void onConnectionClosed(const WSConnection::ptr_t& connection) override + * { + * LL_INFOS("MyServer") << "Client disconnected" << LL_ENDL; + * } + * + * protected: + * // Use custom connection class + * WSConnection::ptr_t connectionFactory(WSServer::ptr_t server, connection_h handle) override + * { + * return std::make_shared(server, handle); + * } + * }; + * @endcode + * + * ## Connection Management + * + * The server automatically manages connection lifetimes and provides several ways + * to interact with connections: + * + * - `broadcastMessage()` - Send message to all connected clients + * - `sendMessageTo()` - Send message to specific connection + * - `closeConnection()` - Close specific connection with code/reason + * - `getConnection()` - Get connection object by handle + * + * ## Thread Safety + * + * All public methods are thread-safe and can be called from any thread. The server + * runs its own background thread for handling WebSocket events, while connection + * callbacks are also executed on this background thread. + */ class WSServer: public std::enable_shared_from_this { friend struct Server_impl; @@ -154,7 +254,7 @@ public: using ptr_t = std::shared_ptr; WSServer(std::string_view name, U16 port, bool local_only = true); - virtual ~WSServer() = default; + virtual ~WSServer(); virtual void onConnectionOpened(const WSConnection::ptr_t& connection) { } virtual void onConnectionClosed(const WSConnection::ptr_t& connection) { } @@ -170,6 +270,18 @@ public: bool sendMessageTo(const connection_h& handle, const std::string& message); + /** + * @brief Close a specific connection gracefully + * @param handle The connection handle to close + * @param code Close code (default: normal closure) + * @param reason Close reason string (default: empty) + * @return true if close was initiated successfully, false on error + * + * Internal method used by WSConnection to close individual connections. + * This method is thread-safe and can be called from any thread. + */ + bool closeConnection(const connection_h& handle, U16 code = 1000, const std::string& reason = std::string()); + private: using connection_map_t = std::map >; @@ -183,6 +295,11 @@ public: std::unique_ptr mImpl; connection_map_t mConnections; LLMutex mConnectionMutex; + + // Threading support + std::thread mServerThread; ///< Thread running the ASIO event loop + std::atomic mShouldStop{ false }; ///< Thread-safe stop flag + mutable LLMutex mThreadMutex; ///< Mutex for thread synchronization }; // Server and Connection Management -- cgit v1.3 From de65dbfcaf4a4f9d3083f014caa1fe2294952e7b Mon Sep 17 00:00:00 2001 From: Rider Linden Date: Fri, 22 Aug 2025 22:22:09 -0700 Subject: Update indra/llcorehttp/llwebsocketmgr.cpp Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- indra/llcorehttp/llwebsocketmgr.cpp | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) (limited to 'indra/llcorehttp/llwebsocketmgr.cpp') diff --git a/indra/llcorehttp/llwebsocketmgr.cpp b/indra/llcorehttp/llwebsocketmgr.cpp index 9fc9027be3..0427bdab54 100644 --- a/indra/llcorehttp/llwebsocketmgr.cpp +++ b/indra/llcorehttp/llwebsocketmgr.cpp @@ -592,8 +592,7 @@ void LLWebsocketMgr::WSServer::handleOpenConnection(const connection_h& handle) { LL_WARNS("WebSocket") << mServerName << " failed to create connection object" << LL_ENDL; return; - } - mConnections[handle] = connection; + // Removed redundant assignment to mConnections[handle] size = mConnections.size(); } -- cgit v1.3 From 7b174ab2a7c54d3e25a56530a2179848db2d46c0 Mon Sep 17 00:00:00 2001 From: Rider Linden Date: Sun, 24 Aug 2025 21:16:36 -0700 Subject: First pass at websocket server for editing. --- indra/llcorehttp/llwebsocketmgr.cpp | 27 +- indra/llcorehttp/llwebsocketmgr.h | 13 +- indra/newview/CMakeLists.txt | 2 + indra/newview/llpreviewscript.cpp | 74 +++-- indra/newview/llpreviewscript.h | 3 +- indra/newview/llscripteditorws.cpp | 608 ++++++++++++++++++++++++++++++++++++ indra/newview/llscripteditorws.h | 185 +++++++++++ 7 files changed, 877 insertions(+), 35 deletions(-) create mode 100644 indra/newview/llscripteditorws.cpp create mode 100644 indra/newview/llscripteditorws.h (limited to 'indra/llcorehttp/llwebsocketmgr.cpp') diff --git a/indra/llcorehttp/llwebsocketmgr.cpp b/indra/llcorehttp/llwebsocketmgr.cpp index 0427bdab54..3e4262b49b 100644 --- a/indra/llcorehttp/llwebsocketmgr.cpp +++ b/indra/llcorehttp/llwebsocketmgr.cpp @@ -30,6 +30,7 @@ #include "llerror.h" #include "llsdserialize.h" #include "llhost.h" +#include "llsdjson.h" #include #include @@ -242,14 +243,16 @@ struct Server_impl */ bool start() { - if (!mServer.stopped()) - { - LL_WARNS("WebSocket") << "WebSocket server is already running" << LL_ENDL; - return false; - } + //if (!mServer.stopped()) + //{ + // LL_WARNS("WebSocket") << "WebSocket server is already running" << LL_ENDL; + // return false; + //} try { + LL_INFOS("WebSocket") << "Starting WebSocket server on port " << mPort + << (mLocalOnly ? " (localhost only)" : " (all interfaces)") << LL_ENDL; mServer.start_accept(); // Run controlled event loop with periodic stop flag checking @@ -592,6 +595,7 @@ void LLWebsocketMgr::WSServer::handleOpenConnection(const connection_h& handle) { LL_WARNS("WebSocket") << mServerName << " failed to create connection object" << LL_ENDL; return; + } // Removed redundant assignment to mConnections[handle] size = mConnections.size(); } @@ -641,7 +645,7 @@ void LLWebsocketMgr::WSServer::handleMessage(const connection_h& handle, const s } //------------------------------------------------------------------------ -bool LLWebsocketMgr::WSConnection::sendMessage(const std::string& message) +bool LLWebsocketMgr::WSConnection::sendMessage(const std::string& message) const { if (!mServer) { @@ -651,6 +655,17 @@ bool LLWebsocketMgr::WSConnection::sendMessage(const std::string& message) return mServer->sendMessageTo(mConnectionHandle, message); } +bool LLWebsocketMgr::WSConnection::sendMessage(const boost::json::value& json) const +{ + std::string message = boost::json::serialize(json); + return sendMessage(message); +} + +bool LLWebsocketMgr::WSConnection::sendMessage(const LLSD& data) const +{ + return sendMessage(LlsdToJson(data)); +} + void LLWebsocketMgr::WSConnection::closeConnection(U16 code, const std::string& reason) { if (!mServer) diff --git a/indra/llcorehttp/llwebsocketmgr.h b/indra/llcorehttp/llwebsocketmgr.h index 5634acde01..08f6f22dbc 100644 --- a/indra/llcorehttp/llwebsocketmgr.h +++ b/indra/llcorehttp/llwebsocketmgr.h @@ -39,6 +39,8 @@ #include #include +#include + #include struct Server_impl; @@ -148,7 +150,9 @@ public: * connection->sendMessage(LLSDSerialize::toJSON(response)); * @endcode */ - bool sendMessage(const std::string& message); + bool sendMessage(const std::string& message) const; + bool sendMessage(const boost::json::value& json) const; + bool sendMessage(const LLSD& data) const; /** * @brief Close the WebSocket connection gracefully @@ -260,6 +264,11 @@ public: virtual void onConnectionClosed(const WSConnection::ptr_t& connection) { } bool isRunning() const; + size_t getConnectionCount() const + { + LLMutexLock lock(&mConnectionMutex); + return mConnections.size(); + } void broadcastMessage(const std::string& message); protected: @@ -294,7 +303,7 @@ public: std::string mServerName; std::unique_ptr mImpl; connection_map_t mConnections; - LLMutex mConnectionMutex; + mutable LLMutex mConnectionMutex; // Threading support std::thread mServerThread; ///< Thread running the ASIO event loop diff --git a/indra/newview/CMakeLists.txt b/indra/newview/CMakeLists.txt index c727d5ae57..d1527ef578 100644 --- a/indra/newview/CMakeLists.txt +++ b/indra/newview/CMakeLists.txt @@ -551,6 +551,7 @@ set(viewer_SOURCE_FILES llsceneview.cpp llscreenchannel.cpp llscripteditor.cpp + llscripteditorws.cpp llscriptfloater.cpp llscrollingpanelparam.cpp llscrollingpanelparambase.cpp @@ -1216,6 +1217,7 @@ set(viewer_HEADER_FILES llsceneview.h llscreenchannel.h llscripteditor.h + llscripteditorws.h llscriptfloater.h llscriptruntimeperms.h llscrollingpanelparam.h diff --git a/indra/newview/llpreviewscript.cpp b/indra/newview/llpreviewscript.cpp index 69f5812853..9b2a149212 100644 --- a/indra/newview/llpreviewscript.cpp +++ b/indra/newview/llpreviewscript.cpp @@ -65,22 +65,12 @@ #include "llviewerobject.h" #include "llviewerobjectlist.h" #include "llviewerregion.h" -#include "llkeyboard.h" -#include "llscrollcontainer.h" -#include "llcheckboxctrl.h" #include "llscripteditor.h" -#include "llselectmgr.h" -#include "lltooldraganddrop.h" -#include "llscrolllistctrl.h" #include "lltextbox.h" -#include "llslider.h" -#include "lldir.h" -#include "llcombobox.h" #include "llviewerstats.h" #include "llviewerwindow.h" #include "lluictrlfactory.h" #include "llmediactrl.h" -#include "lluictrlfactory.h" #include "lltrans.h" #include "llviewercontrol.h" #include "llappviewer.h" @@ -91,6 +81,8 @@ #include "lltoggleablemenu.h" #include "llmenubutton.h" #include "llinventoryfunctions.h" +#include "llwebsocketmgr.h" +#include "llscripteditorws.h" #include const std::string HELP_LSL_PORTAL_TOPIC = "LSL_Portal"; @@ -1129,11 +1121,13 @@ void LLScriptEdCore::openInExternalEditor() // Generate a suitable filename std::string script_name = mScriptName; - std::string forbidden_chars = "<>:\"\\/|?*"; - for (std::string::iterator c = forbidden_chars.begin(); c != forbidden_chars.end(); c++) - { - script_name.erase(std::remove(script_name.begin(), script_name.end(), *c), script_name.end()); - } + + static const std::set forbidden_chars{ '<', '>', ':', '"', '\\', '/', '|', '?', '*' }; + script_name.erase( + std::remove_if(script_name.begin(), script_name.end(), [](char c) { + return forbidden_chars.contains(c); + }), script_name.end()); + std::string filename = mContainer->getTmpFileName(script_name); // Save the script to a temporary file. @@ -1151,6 +1145,28 @@ void LLScriptEdCore::openInExternalEditor() 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(wsmgr.findServerByName(server_name)); + + if (!server) + { + server = std::make_shared(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); + } + // Open it in external editor. { LLExternalEditor ed; @@ -1560,18 +1576,9 @@ LLScriptEdContainer::~LLScriptEdContainer() mLiveLogFile = nullptr; } -std::string LLScriptEdContainer::getTmpFileName(const std::string& script_name) +std::string LLScriptEdContainer::getTmpFileName(const std::string& script_name) const { - // Take script inventory item id (within the object inventory) - // to consideration so that it's possible to edit multiple scripts - // in the same object inventory simultaneously (STORM-781). - std::string script_id = mObjectUUID.asString() + "_" + mItemUUID.asString(); - - // Use MD5 sum to make the file name shorter and not exceed maximum path length. - char script_id_hash_str[33]; /* Flawfinder: ignore */ - LLMD5 script_id_hash((const U8 *)script_id.c_str()); - script_id_hash.hex_digest(script_id_hash_str); - + std::string script_id_hash_str(getUniqueHash()); std::string script_extension = mScriptEd->mEditor->getIsLuauLanguage() ? ".luau" : ".lsl"; if (script_name.empty()) @@ -1584,6 +1591,21 @@ std::string LLScriptEdContainer::getTmpFileName(const std::string& script_name) } } +std::string LLScriptEdContainer::getUniqueHash() const +{ + // Take script inventory item id (within the object inventory) + // to consideration so that it's possible to edit multiple scripts + // in the same object inventory simultaneously (STORM-781). + std::string script_id = mObjectUUID.asString() + "_" + mItemUUID.asString(); + + // Use MD5 sum to make the file name shorter and not exceed maximum path length. + char script_id_hash_str[33]; /* Flawfinder: ignore */ + LLMD5 script_id_hash((const U8*)script_id.c_str()); + script_id_hash.hex_digest(script_id_hash_str); + + return std::string(script_id_hash_str); +} + std::string LLScriptEdContainer::getErrorLogFileName(const std::string& script_path) { if (script_path.empty()) diff --git a/indra/newview/llpreviewscript.h b/indra/newview/llpreviewscript.h index 1b60774deb..14ec54d42b 100644 --- a/indra/newview/llpreviewscript.h +++ b/indra/newview/llpreviewscript.h @@ -219,7 +219,8 @@ public: bool handleKeyHere(KEY key, MASK mask); protected: - std::string getTmpFileName(const std::string& script_name); + std::string getTmpFileName(const std::string& script_name) const; + std::string getUniqueHash() const; std::string getErrorLogFileName(const std::string& script_path); bool onExternalChange(const std::string& filename); virtual void saveIfNeeded(bool sync = true) = 0; diff --git a/indra/newview/llscripteditorws.cpp b/indra/newview/llscripteditorws.cpp new file mode 100644 index 0000000000..503624d15a --- /dev/null +++ b/indra/newview/llscripteditorws.cpp @@ -0,0 +1,608 @@ +/** + * @file llscripteditorws.cpp + * + * $LicenseInfo:firstyear=2002&license=viewerlgpl$ + * Second Life Viewer Source Code + * Copyright (C) 2010, 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$ + */ + +/* + * ============================================================================ + * 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 + * + * - `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 + * + * ## IMPLEMENTATION NOTES + * + * - 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 + * + * ============================================================================ + */ + + +#include "llviewerprecompiledheaders.h" +#include "llscripteditorws.h" +#include "llpreviewscript.h" +#include "llappviewer.h" +#include "lltrans.h" +#include "lldate.h" +#include "llerror.h" +#include "lluuid.h" +#include "llsdjson.h" +#include + +//------------------------------------------------------------------------ + +LLScriptEditorWSServer::LLScriptEditorWSServer(const std::string_view name, U16 port, bool local_only): + LLWebsocketMgr::WSServer(name, port, local_only) +{ +} + +LLWebsocketMgr::WSConnection::ptr_t LLScriptEditorWSServer::connectionFactory(WSServer::ptr_t server, LLWebsocketMgr::connection_h handle) +{ + return std::make_shared(server, handle); +} + +//------------------------------------------------------------------------ +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(); + + // Session information + data["session_id"] = LLUUID::generateNewID().asString(); + data["timestamp"] = LLDate::now().asString(); + + // 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) +{ + LL_INFOS("ScriptEditorWS") << "Script editor client disconnected" << LL_ENDL; + + // Remove from active connections + auto script_connection = std::dynamic_pointer_cast(connection); + if (script_connection) + { + mActiveConnections.erase(script_connection); + + // Remove from any script associations + LL_INFOS("ScriptEditorWS") << "Removed connection from active connections. Total: " + << mActiveConnections.size() << LL_ENDL; + } +} + +//------------------------------------------------------------------------ +bool LLScriptEditorWSServer::associateEditor(const LLHandle& editor_handle, const std::string& script_id) +{ + if (!editor_handle.isDead()) + { + mScriptEditors[script_id] = editor_handle; + return true; + } + return false; +} + +void LLScriptEditorWSServer::dissociateEditor(const std::string& script_id) +{ + mScriptEditors.erase(script_id); +} + +LLHandle LLScriptEditorWSServer::findEditorForScript(const std::string& script_id) const +{ + auto it = mScriptEditors.find(script_id); + if (it != mScriptEditors.end()) + { + return it->second; + } + return LLHandle(); +} + +//======================================================================== +void LLScriptEditorWSConnection::onOpen() +{ + +} + +void LLScriptEditorWSConnection::onClose() +{ +} + +void LLScriptEditorWSConnection::onMessage(const std::string& message) +{ + LL_DEBUGS("ScriptEditorWS") << "Received message: " << message << LL_ENDL; + + // Convert JSON string to LLSD + LLSD parsed_message; + try + { + boost::system::error_code ec; + boost::json::value json_value = boost::json::parse(message, ec); + + if (ec.failed()) + { + 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; + } + + // Convert boost::json::value to LLSD + parsed_message = LlsdFromJson(json_value); + + 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; + + // 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; + } + + // Validate that we have a proper message structure + if (!parsed_message.has("command")) + { + LL_WARNS("ScriptEditorWS") << "Received message without 'type' field" << LL_ENDL; + + 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; + } + + std::string message_type = parsed_message["command"].asString(); + LL_INFOS("ScriptEditorWS") << "Processing message of type: " << message_type << LL_ENDL; + + // Route message to appropriate handler based on type + if (message_type == "connect") + { + processConnectMessage(parsed_message); + } + 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); + } +} + +void LLScriptEditorWSConnection::processConnectMessage(const LLSD& message) +{ + LL_INFOS("ScriptEditorWS") << "Processing connect message from client" << LL_ENDL; + + // Extract connection information from the message + LLSD data; + if (message.has("data")) + { + data = message["data"]; + } + + // Parse client information + std::string client_name; + std::string client_version; + std::string protocol_version; + LLSD client_capabilities; + LLSD supported_languages; + LLSD client_features; + + // Extract client identification + if (data.has("client_name")) + { + client_name = data["client_name"].asString(); + LL_INFOS("ScriptEditorWS") << "Client name: " << client_name << LL_ENDL; + } + + if (data.has("client_version")) + { + client_version = data["client_version"].asString(); + LL_INFOS("ScriptEditorWS") << "Client version: " << client_version << LL_ENDL; + } + + if (data.has("protocol_version")) + { + protocol_version = data["protocol_version"].asString(); + LL_INFOS("ScriptEditorWS") << "Protocol version: " << protocol_version << LL_ENDL; + } + + // 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; + } + + // Extract supported languages + if (data.has("supported_languages")) + { + supported_languages = data["supported_languages"]; + LL_INFOS("ScriptEditorWS") << "Supported languages count: " << supported_languages.size() << LL_ENDL; + } + + // Extract client features + if (data.has("features")) + { + client_features = data["features"]; + LL_INFOS("ScriptEditorWS") << "Client features available" << LL_ENDL; + } + + // 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; + } + + // 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") + { + protocol_compatible = false; + LL_WARNS("ScriptEditorWS") << "Unsupported protocol version: " << protocol_version + << ", expected: 1.0" << LL_ENDL; + } + } + + // 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) + { + if (it->asString() == "script_editing") + { + has_script_editing = true; + break; + } + } + } + + // Build response message + LLSD response; + response["command"] = "connect_ack"; + + LLSD& response_data = response["data"]; + + 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(); + + // Send back our supported capabilities that match the client's + LLSD mutual_capabilities = LLSD::emptyArray(); + + // Check which capabilities we both support + if (client_capabilities.isArray()) + { + // Our server capabilities (from onConnectionOpened) + std::set server_caps = { + "script_editing", "script_synchronization", "compilation", + "metadata", "syntax_highlighting", "error_reporting" + }; + + 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); + } + } + } + + response_data["mutual_capabilities"] = mutual_capabilities; + + // Send supported languages intersection + LLSD mutual_languages = LLSD::emptyArray(); + if (supported_languages.isArray()) + { + std::set server_languages = {"lsl", "luau"}; + + 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"); + } + + response_data["supported_languages"] = mutual_languages; + + // Connection limits and constraints + response_data["max_script_size"] = 65536; + response_data["heartbeat_interval"] = 30; + + LL_INFOS("ScriptEditorWS") << "Successfully connected client: " << client_name + << " v" << client_version + << " with " << mutual_capabilities.size() << " mutual capabilities" << LL_ENDL; + } + else + { + // Connection failed + response_data["status"] = "rejected"; + + if (!protocol_compatible) + { + response_data["error"] = "incompatible_protocol"; + response_data["message"] = "Unsupported protocol version: " + protocol_version; + } + else if (!has_script_editing) + { + response_data["error"] = "missing_capabilities"; + response_data["message"] = "Client must support 'script_editing' capability"; + } + else + { + response_data["error"] = "connection_failed"; + response_data["message"] = "Connection failed for unknown reason"; + } + + LL_WARNS("ScriptEditorWS") << "Rejected connection from client: " << client_name + << " - " << response_data["message"].asString() << LL_ENDL; + } + + // Send the response back to the client + if (sendMessage(response)) + { + LL_INFOS("ScriptEditorWS") << "Sent connect acknowledgment to client" << LL_ENDL; + } + else + { + LL_WARNS("ScriptEditorWS") << "Failed to send connect acknowledgment to client" << LL_ENDL; + } + + // If connection was successful, we could also send any initial state or configuration + if (response_data["status"].asString() == "connected") + { + // TODO: Send initial script list, active editors, or other relevant state + // Example: sendScriptList(), sendActiveEditors(), etc. + } +} diff --git a/indra/newview/llscripteditorws.h b/indra/newview/llscripteditorws.h new file mode 100644 index 0000000000..73efb1a65f --- /dev/null +++ b/indra/newview/llscripteditorws.h @@ -0,0 +1,185 @@ +/** + * @file llscripteditorws.h + * @brief WebSocket server and connection classes for external script editor integration + * + * $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 "llhandle.h" +#include "lltimer.h" + +#include +#include +#include +#include + +// Forward declarations +class LLLiveLSLEditor; +class LLScriptEdCore; + +/** + * @class LLScriptEditorWSConnection + * @brief 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. + * + * ## 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: + * + * #### 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 + * + * #### 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 + */ +class LLScriptEditorWSConnection : public LLWebsocketMgr::WSConnection +{ +public: + + LLScriptEditorWSConnection(const LLWebsocketMgr::WSServer::ptr_t server, + const LLWebsocketMgr::connection_h& handle): + LLWebsocketMgr::WSConnection(server, handle), + mMessageSequence(0) + { } + + ~LLScriptEditorWSConnection() override = default; + + + // Connection lifecycle overrides + void onOpen() override; + void onClose() override; + void onMessage(const std::string& message) override; + +private: + /** + * @brief Handle connect/connection messages from editor + * @param message Parsed LLSD message + */ + void processConnectMessage(const LLSD& message); + + 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 +}; + +/** + * @class LLScriptEditorWSServer + * @brief 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. + * + * ## Architecture + * + * The server acts as a communication hub between: + * - LLLiveLSLEditor instances (in-world script editing) + * - External script editors (VS Code, Atom, Sublime Text, etc.) + * - Script compilation and save services + * + * ## Usage + * + * @code + * // Create and start the server + * auto server = std::make_shared("script_editor_server", 8080); + * LLWebsocketMgr::getInstance()->addServer(server); + * LLWebsocketMgr::getInstance()->startServer("script_editor_server"); + * + * // Associate with an LSL editor + * server->associateEditor(editor_handle, script_id); + * @endcode + * + * ## 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 + */ +class LLScriptEditorWSServer : public LLWebsocketMgr::WSServer +{ +public: + static constexpr char const* DEFAULT_SERVER_NAME = "script_editor_server"; + static constexpr U16 DEFAULT_SERVER_PORT = 9020; + + using ptr_t = std::shared_ptr; + + LLScriptEditorWSServer(const std::string_view name, U16 port, bool local_only = true); + + virtual ~LLScriptEditorWSServer() = default; + + // Server lifecycle callbacks + void onConnectionOpened(const LLWebsocketMgr::WSConnection::ptr_t& connection) override; + void onConnectionClosed(const LLWebsocketMgr::WSConnection::ptr_t& connection) override; + + bool associateEditor(const LLHandle& editor_handle, const std::string& script_id); + void dissociateEditor(const std::string& script_id); + + LLHandle findEditorForScript(const std::string& script_id) const; + std::shared_ptr findConnectionForScript(const std::string& script_id); + + /** + * @brief Get list of active script editing sessions + * @return Set of script IDs currently being edited + */ + std::set getActiveScripts() const; + +protected: + LLWebsocketMgr::WSConnection::ptr_t connectionFactory(WSServer::ptr_t server, LLWebsocketMgr::connection_h handle) override; + +private: + using map_id_to_editor_t = std::unordered_map >; + + map_id_to_editor_t mScriptEditors; + + std::set> mActiveConnections; + + /** + * @brief Connection timeout management + */ + LLTimer mCleanupTimer; + static constexpr F32 CLEANUP_INTERVAL = 60.0f; // seconds + static constexpr F32 CONNECTION_TIMEOUT = 300.0f; // 5 minutes +}; -- cgit v1.3 From f31c194b8674df295edf74693a2f843ca314ca92 Mon Sep 17 00:00:00 2001 From: Rider Linden Date: Tue, 26 Aug 2025 21:29:41 -0700 Subject: Expand the websocket implementation a bit. Added JSONRPC specialization. --- indra/llcorehttp/CMakeLists.txt | 2 + indra/llcorehttp/lljsonrpcws.cpp | 613 +++++++++++++++++++++++++++++++ indra/llcorehttp/lljsonrpcws.h | 461 ++++++++++++++++++++++++ indra/llcorehttp/llwebsocketmgr.cpp | 35 +- indra/llcorehttp/llwebsocketmgr.h | 10 +- indra/newview/llappviewer.cpp | 6 + indra/newview/llpreviewscript.cpp | 71 ++-- indra/newview/llpreviewscript.h | 17 +- indra/newview/llscripteditorws.cpp | 696 +++++++++++++----------------------- indra/newview/llscripteditorws.h | 156 +++++--- 10 files changed, 1523 insertions(+), 544 deletions(-) create mode 100644 indra/llcorehttp/lljsonrpcws.cpp create mode 100644 indra/llcorehttp/lljsonrpcws.h (limited to 'indra/llcorehttp/llwebsocketmgr.cpp') 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 + +//======================================================================== +// 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 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(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 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(getConnectionCount()); + stats["is_running"] = isRunning(); + + { + LLMutexLock lock(&mGlobalMethodsMutex); + stats["global_method_count"] = static_cast(mGlobalMethods.size()); + } + + stats["total_requests_handled"] = static_cast(mTotalRequestsHandled.load()); + stats["total_notifications_sent"] = static_cast(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 +#include +#include + +/** + * @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; + + /// 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; + + /// 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; + + /** + * @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 mMethodHandlers; + std::unordered_map 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("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; + using MethodHandler = LLJSONRPCConnection::MethodHandler; + using ResponseCallback = LLJSONRPCConnection::ResponseCallback; + using BatchResponseCallback = std::function; + + 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 mGlobalMethods; + mutable LLMutex mGlobalMethodsMutex; + + std::string mServerName; // Store server name for stats + std::atomic mTotalRequestsHandled{0}; + std::atomic 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 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 &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 mServer; // Back-reference to the server this connection belongs to + std::weak_ptr 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(wsmgr.findServerByName(server_name)); - - if (!server) - { - server = std::make_shared(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(wsmgr.findServerByName(server_name)); + + if (!server) + { + server = std::make_shared(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& 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& 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 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 - -//------------------------------------------------------------------------ +#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(server, handle); + auto connection = std::make_shared(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"; + // Call parent class to handle JSON-RPC setup and standard methods + LLJSONRPCServer::onConnectionOpened(connection); - // Viewer information - data["viewer_name"] = LLTrans::getString("APP_NAME"); - data["viewer_version"] = LLAppViewer::instance()->getSecondLifeTitle(); + LL_INFOS("ScriptEditorWS") << "New script editor client connected via JSON-RPC" << LL_ENDL; - // Session information - data["session_id"] = LLUUID::generateNewID().asString(); - data["timestamp"] = LLDate::now().asString(); - - // 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& editor_handle, const std::string& script_id) { if (!editor_handle.isDead()) @@ -305,304 +125,268 @@ LLHandle LLScriptEditorWSServer::findEditorForScript(const std::string& return LLHandle(); } -//======================================================================== -void LLScriptEditorWSConnection::onOpen() -{ - -} - -void LLScriptEditorWSConnection::onClose() +std::shared_ptr 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 LLScriptEditorWSServer::getActiveScripts() const { - LL_DEBUGS("ScriptEditorWS") << "Received message: " << message << LL_ENDL; - - // Convert JSON string to LLSD - LLSD parsed_message; - try + std::set 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); } - - // Convert boost::json::value to LLSD - parsed_message = LlsdFromJson(json_value); - - 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; - - // 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; } + return active_scripts; +} - // Validate that we have a proper message structure - if (!parsed_message.has("command")) - { - LL_WARNS("ScriptEditorWS") << "Received message without 'type' field" << LL_ENDL; - - 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::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; - 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["content"] = content; + params["timestamp"] = LLDate::now().asString(); - // Route message to appropriate handler based on type - if (message_type == "connect") + if (!metadata.isUndefined()) { - processConnectMessage(parsed_message); + params["metadata"] = metadata; } - 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("script.update", params); } -void LLScriptEditorWSConnection::processConnectMessage(const LLSD& message) +void LLScriptEditorWSServer::broadcastCompilationResult(const std::string& script_id, bool success, const LLSD& errors) { - LL_INFOS("ScriptEditorWS") << "Processing connect message from client" << LL_ENDL; + LL_DEBUGS("ScriptEditorWS") << "Broadcasting compilation result for script: " << script_id + << " (success: " << success << ")" << LL_ENDL; - // Extract connection information from the message - LLSD data; - if (message.has("data")) - { - data = message["data"]; - } + LLSD params; + params["script_id"] = script_id; + params["success"] = success; + params["timestamp"] = LLDate::now().asString(); - // Parse client information - std::string client_name; - std::string client_version; - std::string protocol_version; - LLSD client_capabilities; - LLSD supported_languages; - LLSD client_features; - - // Extract client identification - if (data.has("client_name")) + if (!errors.isUndefined() && errors.isArray()) { - client_name = data["client_name"].asString(); - LL_INFOS("ScriptEditorWS") << "Client name: " << client_name << LL_ENDL; + params["errors"] = errors; } - if (data.has("client_version")) - { - client_version = data["client_version"].asString(); - LL_INFOS("ScriptEditorWS") << "Client version: " << client_version << LL_ENDL; - } - - if (data.has("protocol_version")) - { - protocol_version = data["protocol_version"].asString(); - LL_INFOS("ScriptEditorWS") << "Protocol version: " << protocol_version << LL_ENDL; - } + // Send to all connected editors as a notification + broadcastNotification("compilation.result", params); +} - // 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 LLScriptEditorWSServer::setupConnectionMethods(LLJSONRPCConnection::ptr_t connection) +{ + // Call parent class to register global JSON-RPC methods + LLJSONRPCServer::setupConnectionMethods(connection); - // Extract supported languages - if (data.has("supported_languages")) + // Cast to our specific connection type to access script editor functionality + auto script_connection = std::dynamic_pointer_cast(connection); + if (script_connection) { - supported_languages = data["supported_languages"]; - LL_INFOS("ScriptEditorWS") << "Supported languages count: " << supported_languages.size() << LL_ENDL; - } + LL_INFOS("ScriptEditorWS") << "Setting up script editor connection methods" << LL_ENDL; - // Extract client features - if (data.has("features")) - { - client_features = data["features"]; - LL_INFOS("ScriptEditorWS") << "Client features available" << LL_ENDL; - } + // 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 - // 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; + // Example of how custom methods could be registered: + // script_connection->registerMethod("script.custom", handler); } +} - // 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") - { - protocol_compatible = false; - LL_WARNS("ScriptEditorWS") << "Unsupported protocol version: " << protocol_version - << ", expected: 1.0" << LL_ENDL; - } - } +//======================================================================== +LLScriptEdContainer* LLScriptEditorWSConnection::getEditor() const +{ + return mEditorPanel.isDead() ? nullptr : dynamic_cast(mEditorPanel.get()); +} - // 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) - { - if (it->asString() == "script_editing") - { - has_script_editing = true; - break; - } - } - } +std::shared_ptr LLScriptEditorWSConnection::getServer() const +{ + return std::static_pointer_cast(mOwningServer.lock()); +} - // Build response message - LLSD response; - response["command"] = "connect_ack"; +void LLScriptEditorWSConnection::onOpen() +{ + // Call parent class to set up JSON-RPC infrastructure + LLJSONRPCConnection::onOpen(); - LLSD& response_data = response["data"]; + LL_INFOS("ScriptEditorWS") << "Script editor JSON-RPC connection opened" << 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(); + // Generate unique editor session ID + mEditorId = LLUUID::generateNewID().asString(); + mEditorReady = false; - // Send back our supported capabilities that match the client's - LLSD mutual_capabilities = LLSD::emptyArray(); + LL_INFOS("ScriptEditorWS") << "Initialized editor session: " << mEditorId << LL_ENDL; - // Check which capabilities we both support - if (client_capabilities.isArray()) - { - // Our server capabilities (from onConnectionOpened) - std::set server_caps = { - "script_editing", "script_synchronization", "compilation", - "metadata", "syntax_highlighting", "error_reporting" - }; - - 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); - } - } - } + // 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(); - response_data["mutual_capabilities"] = mutual_capabilities; + // Supported languages array + LLSD languages = LLSD::emptyArray(); + languages.append("lsl"); + languages.append("luau"); + handshake["supported_languages"] = languages; - // Send supported languages intersection - LLSD mutual_languages = LLSD::emptyArray(); - if (supported_languages.isArray()) + // 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()) { - std::set server_languages = {"lsl", "luau"}; - - 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); - } - } + handleHandshakeResponse(result); } else { - // Default to all our supported languages if client didn't specify - mutual_languages.append("lsl"); - mutual_languages.append("luau"); + LL_WARNS("ScriptEditorWS") << "Handshake failed: " + << error["message"].asString() << LL_ENDL; } + }); - response_data["supported_languages"] = mutual_languages; + LL_INFOS("ScriptEditorWS") << "Sent handshake call to new editor client" << LL_ENDL; +} - // Connection limits and constraints - response_data["max_script_size"] = 65536; - response_data["heartbeat_interval"] = 30; +void LLScriptEditorWSConnection::onClose() +{ + // Call parent class to clean up JSON-RPC infrastructure + LLJSONRPCConnection::onClose(); + + LL_INFOS("ScriptEditorWS") << "Script editor JSON-RPC connection closed for session: " + << mEditorId << LL_ENDL; + + cleanupConnection(); + + // Clean up editor-specific state + mEditorId.clear(); + mEditorCapabilities.clear(); + mScriptId.clear(); + mEditorReady = false; + + // Clean up handshake response data + mClientName.clear(); + mClientVersion.clear(); + mProtocolVersion.clear(); + mScriptName.clear(); + mScriptLanguage.clear(); + mLanguages.clear(); + mFeatures.clear(); +} - LL_INFOS("ScriptEditorWS") << "Successfully connected client: " << client_name - << " v" << client_version - << " with " << mutual_capabilities.size() << " mutual capabilities" << LL_ENDL; - } - else +void LLScriptEditorWSConnection::handleHandshakeResponse(const LLSD& result) +{ + LL_INFOS("ScriptEditorWS") << "Processing handshake response from client" << LL_ENDL; + + // Extract and validate client information + mClientName = result["client_name"].asString(); + mClientVersion = result["client_version"].asString(); + mProtocolVersion = result["protocol_version"].asString(); + + // Validate protocol compatibility + if (mProtocolVersion != "1.0") { - // Connection failed - response_data["status"] = "rejected"; + LL_WARNS("ScriptEditorWS") << "Protocol version mismatch. Expected: 1.0, Got: " + << mProtocolVersion << LL_ENDL; + } - if (!protocol_compatible) - { - response_data["error"] = "incompatible_protocol"; - response_data["message"] = "Unsupported protocol version: " + protocol_version; - } - else if (!has_script_editing) + // Store script information if provided + mScriptName = result["script_name"].asString(); + mScriptLanguage = result["script_language"].asString(); + mScriptId = result["script_id"].asString(); + + // Store supported languages + for (const auto& lang : llsd::inArray( result["languages"])) + { + if (lang.isString()) { - response_data["error"] = "missing_capabilities"; - response_data["message"] = "Client must support 'script_editing' capability"; + mLanguages.insert(lang.asString()); } - 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(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(); +} + + +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 - * - * 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. - * - * ## 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: - * - * #### 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 - * - * #### 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 + * @brief JSON-RPC WebSocket connection specialized for external script editor communication + * + * 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. + * + * ## Usage + * + * @code + * // Create server and let base JSON-RPC handle method registration + * auto server = std::make_shared("script_editor_server", 9020); + * + * // 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 { 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; /** - * @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 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 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("script_editor_server", 8080); + * // Create and start the JSON-RPC server + * auto server = std::make_shared("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(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 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 >; + using map_id_to_editor_t = std::unordered_map>; map_id_to_editor_t mScriptEditors; - std::set> mActiveConnections; /** -- cgit v1.3 From 684af8426b36f9ad4c2324ae0c5b4dc62f840079 Mon Sep 17 00:00:00 2001 From: Rider Linden Date: Wed, 17 Sep 2025 13:14:23 -0700 Subject: initial Lua types files, and switch websocket server to use a single connection for all scripts. --- indra/llcommon/llstl.h | 204 +++++++++++++++++ indra/llcorehttp/llwebsocketmgr.cpp | 31 +++ indra/llcorehttp/llwebsocketmgr.h | 62 ++---- indra/newview/CMakeLists.txt | 2 + indra/newview/llpreviewscript.cpp | 48 ++-- indra/newview/llpreviewscript.h | 11 +- indra/newview/llscripteditorws.cpp | 423 +++++++++++++++++++++++------------- indra/newview/llscripteditorws.h | 141 ++++++------ indra/newview/llsyntaxid.cpp | 17 +- indra/newview/llsyntaxid.h | 22 +- 10 files changed, 650 insertions(+), 311 deletions(-) (limited to 'indra/llcorehttp/llwebsocketmgr.cpp') diff --git a/indra/llcommon/llstl.h b/indra/llcommon/llstl.h index 7d41c42ba7..0088eeec67 100644 --- a/indra/llcommon/llstl.h +++ b/indra/llcommon/llstl.h @@ -35,6 +35,7 @@ #include #include #include +#include #ifdef LL_LINUX // For strcmp @@ -709,5 +710,208 @@ struct ll_template_cast_impl \ } \ } +//----------------------------------------------- +namespace LL +{ + /** + * @brief A range adapter that provides filtered iteration over a container. + * + * filter_range creates a filtered view of an iterator range using a predicate function. + * Only elements that satisfy the predicate will be accessible when iterating through + * the range. This is useful for processing subsets of containers without copying data. + * + * The class uses boost::filter_iterator internally to provide the filtering functionality. + * + * @tparam Predicate A callable object (function, functor, lambda) that takes an element + * from the iterator range and returns true if the element should be + * included in the filtered range. + * @tparam Iterator The iterator type for the underlying container/range. + * + * Example usage: + * @code + * std::vector numbers = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10}; + * + * // Create a predicate to filter even numbers + * auto is_even = [](int n) { return n % 2 == 0; }; + * + * // Create filtered range using make_filter helper + * auto even_range = LL::make_filter(is_even, numbers.begin(), numbers.end()); + * + * // Iterate through only even numbers + * for (auto value : even_range) { + * std::cout << value << " "; // Prints: 2 4 6 8 10 + * } + * + * // Or manually construct the filter_range + * LL::filter_range::iterator> + * manual_range(is_even, numbers.begin(), numbers.end()); + * @endcode + * + * @note This class provides a lightweight view over the original data. + * No copying of elements occurs, making it efficient for large containers. + * @note The predicate is applied during iteration, so complex predicates may + * impact performance for frequently-accessed ranges. + * @note The underlying container must remain valid for the lifetime of the filter_range. + * + * @see make_filter() for a convenient factory function + * @see boost::filter_iterator for the underlying implementation details + */ + template + class filter_range + { + public: + /// The filtered iterator type - combines predicate with base iterator + using filter_iter = boost::filter_iterator; + + /// Value type of the filtered elements + using value_type = typename std::iterator_traits::value_type; + + /// Iterator type for range-based for loops and STL algorithms + using iterator = filter_iter; + using const_iterator = filter_iter; + + /** + * @brief Constructs a filter_range with the given predicate and iterator range. + * + * @param pred The predicate function/functor to filter elements. + * Must be callable with signature: bool(const value_type&) + * @param begin Iterator to the beginning of the range to filter + * @param end Iterator to the end of the range to filter + * + * @pre begin and end must form a valid iterator range + * @pre pred must be a valid callable that can be invoked with elements from [begin, end) + */ + filter_range(Predicate pred, Iterator begin, Iterator end) + : begin_(pred, begin, end), end_(pred, end, end) {} + + /** + * @brief Returns an iterator to the first element that satisfies the predicate. + * + * @return filter_iter Iterator pointing to the first filtered element, + * or equal to end() if no elements satisfy the predicate. + */ + filter_iter begin() const { return begin_; } + + /** + * @brief Returns an iterator representing the end of the filtered range. + * + * @return filter_iter Past-the-end iterator for the filtered range. + */ + filter_iter end() const { return end_; } + + /** + * @brief Checks if the filtered range is empty. + * + * @return true if no elements in the range satisfy the predicate, false otherwise. + * + * @note This operation has O(1) complexity as it only compares iterators. + */ + bool empty() const { return begin_ == end_; } + + private: + filter_iter begin_; ///< Iterator to first element satisfying predicate + filter_iter end_; ///< Past-the-end iterator for the filtered range + }; + + /** + * @brief Factory function to create a filter_range with automatic template deduction. + * + * This convenience function eliminates the need to explicitly specify template parameters + * when creating a filter_range. The template parameters are automatically deduced from + * the function arguments. + * + * @tparam Predicate Automatically deduced predicate type + * @tparam Iterator Automatically deduced iterator type + * + * @param pred Predicate function/functor for filtering elements + * @param begin Iterator to the beginning of the range + * @param end Iterator to the end of the range + * + * @return filter_range A filter_range object configured with + * the provided predicate and range + * + * Example usage: + * @code + * std::vector words = {"hello", "world", "test", "example"}; + * + * // Filter strings longer than 4 characters + * auto long_words = LL::make_filter( + * [](const std::string& s) { return s.length() > 4; }, + * words.begin(), + * words.end() + * ); + * + * // Use with range-based for loop + * for (const auto& word : long_words) { + * std::cout << word << std::endl; // Prints: hello, world, example + * } + * + * // Use with STL algorithms + * auto count = std::distance(long_words.begin(), long_words.end()); + * std::cout << "Found " << count << " long words." << std::endl; + * @endcode + * + * @note This function is preferred over direct construction of filter_range + * for most use cases due to automatic template parameter deduction. + */ + template + filter_range make_filter(Predicate pred, Iterator begin, Iterator end) + { + return filter_range(pred, begin, end); + } + + /** + * @brief Create a filter_range over an entire container with automatic template deduction. + * + * This convenience function creates a filtered view over an entire container without + * requiring explicit begin() and end() calls. It automatically handles both const and + * non-const containers, preserving constness in the resulting iterator types. + * + * @tparam Predicate Automatically deduced predicate type + * @tparam Container Automatically deduced container type (const or non-const) + * + * @param pred Predicate function/functor for filtering elements + * @param container The container to filter (can be const or non-const) + * + * @return filter_range with appropriate iterator type for the container + * + * Example usage: + * @code + * // Non-const container + * std::vector numbers = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10}; + * auto evens = LL::make_filter([](int n) { return n % 2 == 0; }, numbers); + * + * // Const container + * const std::list words = {"cat", "elephant", "dog", "hippopotamus"}; + * auto long_words = LL::make_filter([](const std::string& s) { return s.size() > 3; }, words); + * + * // Works with any container that supports begin()/end() + * std::set values = {1.1, 2.2, 3.3, 4.4, 5.5}; + * auto large_values = LL::make_filter([](double d) { return d > 3.0; }, values); + * + * // Use with range-based for loops + * for (const auto& word : long_words) { + * std::cout << word << " "; // Prints: elephant hippopotamus + * } + * + * // Chain with STL algorithms + * auto even_count = std::distance(evens.begin(), evens.end()); + * std::cout << "Found " << even_count << " even numbers." << std::endl; + * @endcode + * + * @note This overload automatically calls begin() and end() on the container, + * making it more convenient than the iterator-based version. + * @note The container must remain valid for the lifetime of the returned filter_range. + * @note Constness of the container is preserved in the iterator type. + */ + template + filter_range()))> + make_filter(Predicate pred, Container&& container) + { + return filter_range()))>( + pred, std::begin(container), std::end(container)); + } + +} // namespace LL #endif // LL_LLSTL_H diff --git a/indra/llcorehttp/llwebsocketmgr.cpp b/indra/llcorehttp/llwebsocketmgr.cpp index d44d5d877d..09eb728500 100644 --- a/indra/llcorehttp/llwebsocketmgr.cpp +++ b/indra/llcorehttp/llwebsocketmgr.cpp @@ -485,6 +485,7 @@ bool LLWebsocketMgr::WSServer::start() LL_INFOS("WebSocket") << "WebSocket server thread exiting for: " << mServerName << LL_ENDL; }); + onStarted(); LL_INFOS("WebSocket") << "Started WebSocket server thread: " << mServerName << LL_ENDL; return true; } @@ -517,6 +518,7 @@ void LLWebsocketMgr::WSServer::stop() mServerThread.join(); LL_INFOS("WebSocket") << "WebSocket server thread joined for: " << mServerName << LL_ENDL; } + onStopped(); } bool LLWebsocketMgr::WSServer::isRunning() const @@ -596,6 +598,20 @@ LLWebsocketMgr::WSConnection::ptr_t LLWebsocketMgr::WSServer::getConnection(cons return nullptr; } +LLWebsocketMgr::connection_state_t LLWebsocketMgr::WSServer::getConnectionState(const connection_h& handle) const +{ + websocketpp::lib::error_code ec; + auto con = mImpl->mServer.get_con_from_hdl(handle, ec); + if (ec) + { + LL_WARNS("WebSocket") << mServerName << " failed to get connection state: " << ec.message() << LL_ENDL; + websocketpp::session::state::value state = websocketpp::session::state::closed; + return connection_closed; + } + return static_cast(con->get_state()); +} + + void LLWebsocketMgr::WSServer::handleOpenConnection(const connection_h& handle) { WSConnection::ptr_t connection; @@ -709,3 +725,18 @@ void LLWebsocketMgr::WSConnection::closeConnection(U16 code, const std::string& LL_WARNS("WebSocket") << "Failed to close connection through server" << LL_ENDL; } } + +bool LLWebsocketMgr::WSConnection::isConnected() const +{ + if (mOwningServer.expired()) + { + return false; + } + + LLWebsocketMgr::WSServer::ptr_t server = mOwningServer.lock(); + if (!server) + { + return false; + } + return server->getConnectionState(mConnectionHandle) == connection_open; +} diff --git a/indra/llcorehttp/llwebsocketmgr.h b/indra/llcorehttp/llwebsocketmgr.h index 570c8ad6cd..4165b3cecc 100644 --- a/indra/llcorehttp/llwebsocketmgr.h +++ b/indra/llcorehttp/llwebsocketmgr.h @@ -64,6 +64,14 @@ public: using connection_h = websocketpp::connection_hdl; class WSServer; + enum connection_state_t + { // must map to websocketpp::session::state + connection_connecting = 0, + connection_open = 1, + connection_closing = 2, + connection_closed = 3 + }; + class WSConnection { friend class LLWebsocketMgr; @@ -84,8 +92,6 @@ public: virtual ~WSConnection() = default; /** - * @brief Called when the connection is opened - * * Override this method in derived classes to handle connection establishment. * This is called after the WebSocket handshake is complete and the connection * is ready to send/receive messages. @@ -93,8 +99,6 @@ public: virtual void onOpen() {} /** - * @brief Called when the connection is closed - * * Override this method in derived classes to handle connection closure. * This is called when the connection has been terminated, either normally * or due to an error condition. @@ -107,27 +111,6 @@ public: * * Override this method in derived classes to handle incoming messages. * Currently only text messages are supported. - * - * @code - * class MyConnection : public LLWebsocketMgr::WSConnection - * { - * public: - * void onMessage(const std::string& message) override - * { - * // Parse and handle the message - * if (message == "ping") { - * sendMessage("pong"); - * } - * // Process JSON messages - * try { - * LLSD data = LLSDSerialize::fromJSON(message); - * handleStructuredMessage(data); - * } catch (...) { - * LL_WARNS("MyConnection") << "Invalid JSON received" << LL_ENDL; - * } - * } - * }; - * @endcode */ virtual void onMessage(const std::string& message) {} @@ -138,17 +121,6 @@ public: * * Sends a text message to the remote endpoint. The message is queued * asynchronously and may not be sent immediately. - * - * @code - * // Send a simple text message - * connection->sendMessage("Hello, client!"); - * - * // Send JSON data - * LLSD response; - * response["status"] = "ok"; - * response["data"] = "some data"; - * connection->sendMessage(LLSDSerialize::toJSON(response)); - * @endcode */ bool sendMessage(const std::string& message) const; bool sendMessage(const boost::json::value& json) const; @@ -172,22 +144,13 @@ public: * - 1008: Policy violation * - 1009: Message too big * - * @code - * // Normal closure - * connection->closeConnection(); - * - * // Close with specific reason - * connection->closeConnection(1000, "Session ended"); - * - * // Close due to policy violation - * connection->closeConnection(1008, "Authentication failed"); - * @endcode - * * @note After calling this method, no further messages should be sent * @note The onClose() callback will be invoked when the close handshake completes */ void closeConnection(U16 code = 1000, const std::string& reason = std::string()); + bool isConnected() const; + protected: connection_h mConnectionHandle; std::weak_ptr mOwningServer; // Back-reference to the server this connection belongs to @@ -260,6 +223,9 @@ public: WSServer(std::string_view name, U16 port, bool local_only = true); virtual ~WSServer(); + virtual void onStarted() {} + virtual void onStopped() {} + virtual void onConnectionOpened(const WSConnection::ptr_t& connection) { } virtual void onConnectionClosed(const WSConnection::ptr_t& connection) { } @@ -273,6 +239,8 @@ public: void broadcastMessage(const std::string& message); virtual bool update() { return true; } + connection_state_t getConnectionState(const connection_h& handle) const; + protected: virtual WSConnection::ptr_t connectionFactory(WSServer::ptr_t server, connection_h handle); diff --git a/indra/newview/CMakeLists.txt b/indra/newview/CMakeLists.txt index d1527ef578..6c7b4fef81 100644 --- a/indra/newview/CMakeLists.txt +++ b/indra/newview/CMakeLists.txt @@ -1663,6 +1663,7 @@ set(viewer_APPSETTINGS_FILES app_settings/ignorable_dialogs.xml app_settings/key_bindings.xml app_settings/keywords_lsl_default.xml + app_settings/keywords_lua_default.xml app_settings/logcontrol.xml app_settings/settings.xml app_settings/settings_crash_behavior.xml @@ -1671,6 +1672,7 @@ set(viewer_APPSETTINGS_FILES app_settings/std_bump.ini app_settings/toolbars.xml app_settings/trees.xml + app_settings/types_lua_default.llsd app_settings/viewerart.xml app_settings/message.xml ${CMAKE_SOURCE_DIR}/../scripts/messages/message_template.msg diff --git a/indra/newview/llpreviewscript.cpp b/indra/newview/llpreviewscript.cpp index 3ab2e747ea..f17b2aeed1 100644 --- a/indra/newview/llpreviewscript.cpp +++ b/indra/newview/llpreviewscript.cpp @@ -1553,6 +1553,11 @@ LLScriptEdContainer::~LLScriptEdContainer() delete mLiveLogFile; mLiveLogFile = nullptr; + + if (!mWebSocketServer.expired()) + { + unsubscribeScript(); + } } std::string LLScriptEdContainer::getTmpFileName(const std::string& script_name) const @@ -1675,44 +1680,45 @@ void LLScriptEdContainer::startWebsocketServer() U16 server_port(LLScriptEditorWSServer::DEFAULT_SERVER_PORT); bool server_localhost(true); + // Attempt to find an existing server LLWebsocketMgr& wsmgr = LLWebsocketMgr::instance(); LLScriptEditorWSServer::ptr_t server = std::static_pointer_cast(wsmgr.findServerByName(server_name)); if (!server) - { + { // We couldn't find one, so create it server = std::make_shared(server_name, server_port, server_localhost); wsmgr.addServer(server); - wsmgr.startServer(server_name); + } + + bool is_running = server->isRunning(); + if (!is_running) + { // Server isn't running, so start it + is_running = wsmgr.startServer(server_name); + } + + if (!is_running && !server->isRunning()) + { // Failed to start the server + LL_WARNS() << "Failed to start script editor websocket server" << LL_ENDL; + return; } std::string script_id_hash_str(getUniqueHash()); - server->associateEditor(getHandle(), script_id_hash_str); + server->subscribeScriptEditor(getHandle(), script_id_hash_str); + mWebSocketServer = server; } } -void LLScriptEdContainer::attachToWebSocket(const std::shared_ptr& connection) +void LLScriptEdContainer::unsubscribeScript() { - mWebSocket = connection; -} - -void LLScriptEdContainer::detachFromWebSocket(bool send_disconnect) -{ - if (mWebSocket) + auto server = mWebSocketServer.lock(); + if (server) { - if (send_disconnect) - { - // TODO: - mWebSocket->sendDisconnect(LLScriptEditorWSConnection::REASON_EDITOR_CLOSED); - mWebSocket->closeConnection(); - } - mWebSocket.reset(); + std::string script_id_hash_str(getUniqueHash()); + server->sendUnsubscribeScriptEditor(script_id_hash_str); + server->unsubscribeEditor(script_id_hash_str); } } -void LLScriptEdContainer::cleanupWebSocket() -{ - mWebSocket.reset(); -} /// --------------------------------------------------------------------------- /// LLPreviewLSL diff --git a/indra/newview/llpreviewscript.h b/indra/newview/llpreviewscript.h index 72d52a115e..8d53c8899c 100644 --- a/indra/newview/llpreviewscript.h +++ b/indra/newview/llpreviewscript.h @@ -54,7 +54,7 @@ class LLScriptEdContainer; class LLFloaterGotoLine; class LLFloaterExperienceProfile; class LLScriptMovedObserver; -class LLScriptEditorWSConnection; +class LLScriptEditorWSServer; class LLLiveLSLFile : public LLLiveFile { @@ -105,6 +105,7 @@ public: void initMenu(); void processKeywords(); void processKeywords(bool luau_language); + LLKeywords& getKeywords() { return mEditor->getKeywords(); } void draw() override; bool postBuild() override; @@ -221,9 +222,9 @@ public: bool handleKeyHere(KEY key, MASK mask); void startWebsocketServer(); - void attachToWebSocket(const std::shared_ptr& connection); - void detachFromWebSocket(bool send_disconnect); - void cleanupWebSocket(); + void unsubscribeScript(); + + LLScriptEdCore* getScriptEdCore() const { return mScriptEd; } protected: std::string getTmpFileName(const std::string& script_name) const; @@ -238,7 +239,7 @@ protected: LLLiveLSLFile* mLiveFile = nullptr; LLLiveLSLFile* mLiveLogFile = nullptr; - std::shared_ptr mWebSocket; + std::weak_ptr mWebSocketServer; }; // Used to view and edit an LSL script from your inventory. diff --git a/indra/newview/llscripteditorws.cpp b/indra/newview/llscripteditorws.cpp index 9863130aea..cb67880455 100644 --- a/indra/newview/llscripteditorws.cpp +++ b/indra/newview/llscripteditorws.cpp @@ -24,24 +24,6 @@ * $/LicenseInfo$ */ -/** - * 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. - * - * ## JSON-RPC Integration - * - * The connection provides a clean JSON-RPC 2.0 interface that can be - * extended with script-specific functionality as needed: - * - * ### 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" @@ -51,6 +33,7 @@ #include "llerror.h" #include "lluuid.h" #include "llversioninfo.h" +#include "llagent.h" //======================================================================== LLScriptEditorWSServer::LLScriptEditorWSServer(const std::string& name, U16 port, bool local_only) @@ -64,7 +47,7 @@ LLWebsocketMgr::WSConnection::ptr_t LLScriptEditorWSServer::connectionFactory(LL LLWebsocketMgr::connection_h handle) { auto connection = std::make_shared(server, handle); - mActiveConnections.insert(connection); + mActiveConnections[connection->getConnectionID()] = connection; // Call setupConnectionMethods to register any global methods setupConnectionMethods(connection); @@ -72,6 +55,29 @@ LLWebsocketMgr::WSConnection::ptr_t LLScriptEditorWSServer::connectionFactory(LL return connection; } +void LLScriptEditorWSServer::onStarted() +{ + LLSyntaxIdLSL& syntax_id_mgr = LLSyntaxIdLSL::instance(); + wptr_t that(std::static_pointer_cast(shared_from_this())); + + mLastSyntaxId = syntax_id_mgr.getSyntaxID(); + mLanguageChangeSignal = syntax_id_mgr.addSyntaxIDCallback( + [that]() + { + auto server = that.lock(); + if (server && server->isRunning()) + { + server->broadcastLangugeChange(); + } + }); +} + +void LLScriptEditorWSServer::onStopped() +{ + mLanguageChangeSignal.disconnect(); + mLastSyntaxId.setNull(); +} + void LLScriptEditorWSServer::onConnectionOpened(const LLWebsocketMgr::WSConnection::ptr_t& connection) { // Call parent class to handle JSON-RPC setup and standard methods @@ -92,35 +98,102 @@ void LLScriptEditorWSServer::onConnectionClosed(const LLWebsocketMgr::WSConnecti auto script_connection = std::dynamic_pointer_cast(connection); if (script_connection) { - mActiveConnections.erase(script_connection); + U32 connection_id = script_connection->getConnectionID(); + unsubscribeConnection(connection_id); + mActiveConnections.erase(connection_id); - LL_INFOS("ScriptEditorWS") << "Removed connection from active connections. Total: " + LL_DEBUGS("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& editor_handle, const std::string& script_id) +bool LLScriptEditorWSServer::subscribeScriptEditor(const LLHandle& editor_handle, const std::string &script_id) { if (!editor_handle.isDead()) { - mScriptEditors[script_id] = editor_handle; + auto it = mSubscriptions.find(script_id); + if (it == mSubscriptions.end()) + { // Don't readd if already subscribed + mSubscriptions.emplace(script_id, EditorSubscription{ editor_handle, LLScriptEditorWSConnection::wptr_t() }); + return false; + } + else + { // Update existing subscription with new editor handle + it->second.mEditorHandle = editor_handle; + } return true; } return false; } -void LLScriptEditorWSServer::dissociateEditor(const std::string& script_id) +void LLScriptEditorWSServer::unsubscribeEditor(const std::string &script_id) +{ + auto it = mSubscriptions.find(script_id); + if (it != mSubscriptions.end()) + { + mSubscriptions.erase(it); + } +} + +void LLScriptEditorWSServer::unsubscribeConnection(U32 connection_id) +{ + for (auto it = mSubscriptions.begin(); it != mSubscriptions.end(); ) + { + if (it->second.mConnectionID == connection_id) + { + LL_DEBUGS("ScriptEditorWS") << "Unsubscribing script " << it->first + << " from connection ID " << connection_id << LL_ENDL; + it = mSubscriptions.erase(it); + } + else + { + ++it; + } + } +} + +LLScriptEditorWSServer::SubscriptionError_t LLScriptEditorWSServer::updateScriptSubscription(const std::string &script_id, U32 connection_id) { - mScriptEditors.erase(script_id); + auto it = mSubscriptions.find(script_id); + if (it != mSubscriptions.end()) + { + if (it->second.mEditorHandle.isDead()) + { + unsubscribeEditor(script_id); + return SUBSCRIPTION_INVALID_EDITOR; + } + + auto con_it = mActiveConnections.find(connection_id); + if (con_it == mActiveConnections.end()) + { + return SUBSCRIPTION_INTERNAL_ERROR; + } + + if ((it->second.mConnectionID != 0) && !it->second.mConnection.expired() + && it->second.mConnection.lock()->isConnected()) + { + LL_WARNS("ScriptEditorWS") << "Script " << script_id << " is already subscribed on connection ID " << it->second.mConnectionID + << ", cannot subscribe again on connection ID " << connection_id << LL_ENDL; + // In the future we may want to support multiple connections per script. + // That would imply it was open in multiple editors. + return SUBSCRIPTION_ALREADY_SUBSCRIBED; + } + + it->second.mConnectionID = connection_id; + it->second.mConnection = con_it->second; + return SUBSCRIPTION_SUCCESS; + } + return SUBSCRIPTION_INVALID_SUBSCRIPTION; } + LLHandle LLScriptEditorWSServer::findEditorForScript(const std::string& script_id) const { - auto it = mScriptEditors.find(script_id); - if (it != mScriptEditors.end()) + auto it = mSubscriptions.find(script_id); + if (it != mSubscriptions.end()) { - return it->second; + return it->second.mEditorHandle; } return LLHandle(); } @@ -135,9 +208,9 @@ std::shared_ptr LLScriptEditorWSServer::findConnecti std::set LLScriptEditorWSServer::getActiveScripts() const { std::set active_scripts; - for (const auto& [script_id, editor_handle] : mScriptEditors) + for (const auto& [script_id, subinfo] : mSubscriptions) { - if (!editor_handle.isDead()) + if (!subinfo.mEditorHandle.isDead()) { active_scripts.insert(script_id); } @@ -145,69 +218,182 @@ std::set LLScriptEditorWSServer::getActiveScripts() const return active_scripts; } -void LLScriptEditorWSServer::broadcastScriptUpdate(const std::string& script_id, const std::string& content, const LLSD& metadata) +void LLScriptEditorWSServer::setupConnectionMethods(LLJSONRPCConnection::ptr_t connection) { - LL_DEBUGS("ScriptEditorWS") << "Broadcasting script update for script: " << script_id << LL_ENDL; + // Call parent class to register global JSON-RPC methods + LLJSONRPCServer::setupConnectionMethods(connection); - LLSD params; - params["script_id"] = script_id; - params["content"] = content; - params["timestamp"] = LLDate::now().asString(); + // Cast to our specific connection type to access script editor functionality + auto script_connection = std::dynamic_pointer_cast(connection); + if (script_connection) + { + LL_DEBUGS("ScriptEditorWS") << "Setting up script editor connection methods" << LL_ENDL; + wptr_t that(std::static_pointer_cast(shared_from_this())); + + U32 connection_id = script_connection->getConnectionID(); + + script_connection->registerMethod("language.syntax.id", + [that](const std::string&, const LLSD&, const LLSD&) -> LLSD + { + auto server = that.lock(); + if (server) + { + return server->handleLanguageIdRequest(); + } + return LLSD(); + }); + script_connection->registerMethod("language.syntax", + [that](const std::string&, const LLSD&, const LLSD& params) + { + auto server = that.lock(); + if (server) + { + return server->handleSyntaxRequest(params); + } + return LLSD(); + }); + script_connection->registerMethod("script.subscribe", + [that, connection_id](const std::string&, const LLSD&, const LLSD& params) -> LLSD + { + auto server = that.lock(); + if (server) + { + return server->handleScriptSubscribe(connection_id, params); + } + return LLSD(); + }); + script_connection->registerMethod("script.unsubscribe", [](const std::string&, const LLSD&, const LLSD& params) -> LLSD + { // this is a notification, no response expected + return LLSD(); + }); + // script_connection->registerMethod("language.syntax", ) + } +} + +void LLScriptEditorWSServer::broadcastLangugeChange() +{ + LLUUID syntax_id = LLSyntaxIdLSL::instance().getSyntaxID(); - if (!metadata.isUndefined()) + if (syntax_id != mLastSyntaxId) { - params["metadata"] = metadata; + mLastSyntaxId = syntax_id; + LLSD params; + params["id"] = syntax_id; + + if (isRunning()) + { + broadcastNotification("language.syntax.change", params); + } } +} - // Send to all connected editors as a notification - broadcastNotification("script.update", params); +LLSD LLScriptEditorWSServer::handleLanguageIdRequest() const +{ + LLSD response; + + response["id"] = mLastSyntaxId; + return response; } -void LLScriptEditorWSServer::broadcastCompilationResult(const std::string& script_id, bool success, const LLSD& errors) +LLSD LLScriptEditorWSServer::handleSyntaxRequest(const LLSD& params) const { - LL_DEBUGS("ScriptEditorWS") << "Broadcasting compilation result for script: " << script_id - << " (success: " << success << ")" << LL_ENDL; + LLSD response(LLSD::emptyMap()); + std::string category = params["kind"].asString(); - LLSD params; - params["script_id"] = script_id; - params["success"] = success; - params["timestamp"] = LLDate::now().asString(); + response["id"] = mLastSyntaxId; - if (!errors.isUndefined() && errors.isArray()) + if (category == "types.luau") { - params["errors"] = errors; + response["types"] = LLSyntaxLua::instance().getTypesXML(); } + else + { + LLSD syntax = LLSyntaxIdLSL::instance().getKeywordsXML(); + + // TODO: support language definitions and additional modules. - // Send to all connected editors as a notification - broadcastNotification("compilation.result", params); + if (syntax.has(category)) + { + response[category] = syntax[category]; + } + } + return response; } -void LLScriptEditorWSServer::setupConnectionMethods(LLJSONRPCConnection::ptr_t connection) +LLSD LLScriptEditorWSServer::handleScriptSubscribe(U32 connection_id, const LLSD& params) { - // Call parent class to register global JSON-RPC methods - LLJSONRPCServer::setupConnectionMethods(connection); + LLSD response(LLSD::emptyMap()); - // Cast to our specific connection type to access script editor functionality - auto script_connection = std::dynamic_pointer_cast(connection); - if (script_connection) + std::string script_id = params["script_id"].asString(); + std::string script_name = params["script_name"].asString(); + std::string language = params["script_language"].asString(); + + SubscriptionError_t result = updateScriptSubscription(script_id, connection_id); + + response["script_id"] = script_id; + response["success"] = (result == SUBSCRIPTION_SUCCESS); + response["status"] = result; + + LL_WARNS_IF(result != SUBSCRIPTION_SUCCESS, "ScriptEditorWS") + << "Script connect request for script " << script_id << " failed with status " << result << LL_ENDL; + switch (result) { - LL_INFOS("ScriptEditorWS") << "Setting up script editor connection methods" << LL_ENDL; + case SUBSCRIPTION_SUCCESS: + response["message"] = "OK"; + break; + case SUBSCRIPTION_INVALID_EDITOR: + response["message"] = "Invalid editor handle"; + break; + case SUBSCRIPTION_INVALID_SUBSCRIPTION: + response["message"] = "No subscription found for script"; + break; + case SUBSCRIPTION_ALREADY_SUBSCRIBED: + response["message"] = "Script already subscribed"; + break; + case SUBSCRIPTION_INTERNAL_ERROR: + response["message"] = "Internal server error"; + break; + } + + if (result == SUBSCRIPTION_SUCCESS) + { + //TODO: Build an info block for the subscribed script. + //buildScriptSubscriptionInfo(result); + } - // 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 + return response; +} + +LLSD LLScriptEditorWSServer::handleScriptUnsubscribe(U32 connection_id, const LLSD& params) +{ + std::string script_id = params["script_id"].asString(); - // Example of how custom methods could be registered: - // script_connection->registerMethod("script.custom", handler); + auto it = mSubscriptions.find(script_id); + if (it != mSubscriptions.end() && (it->second.mConnectionID == connection_id)) + { + unsubscribeEditor(script_id); } + return LLSD(); } -//======================================================================== -LLScriptEdContainer* LLScriptEditorWSConnection::getEditor() const +void LLScriptEditorWSServer::sendUnsubscribeScriptEditor(const std::string& script_id) { - return mEditorPanel.isDead() ? nullptr : dynamic_cast(mEditorPanel.get()); + auto it = mSubscriptions.find(script_id); + if (it != mSubscriptions.end()) + { + auto connection = it->second.mConnection.lock(); + if (connection) + { + LLSD params; + params["script_id"] = script_id; + connection->notify("script.unsubscribe", params); + } + } } +//======================================================================== +U32 LLScriptEditorWSConnection::sNextConnectionID = 1; + std::shared_ptr LLScriptEditorWSConnection::getServer() const { return std::static_pointer_cast(mOwningServer.lock()); @@ -220,20 +406,17 @@ void LLScriptEditorWSConnection::onOpen() LL_INFOS("ScriptEditorWS") << "Script editor JSON-RPC connection opened" << LL_ENDL; - // Generate unique editor session ID - mEditorId = LLUUID::generateNewID().asString(); - mEditorReady = false; - - LL_INFOS("ScriptEditorWS") << "Initialized editor session: " << mEditorId << LL_ENDL; - - // Build hello data according to the protocol specification + // Build hello data 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 + handshake["agent_id"] = gAgent.getID(); + + // handshake["challenge"] = ... TODO: simple challenge, write to a file and have the client echo it back? + LLSD languages = LLSD::emptyArray(); languages.append("lsl"); languages.append("luau"); @@ -243,14 +426,19 @@ void LLScriptEditorWSConnection::onOpen() 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) { + wptr_t that = shared_from_this(); + + // Send session.handshake method call and the response + call("session.handshake", handshake, [that](const LLSD& result, const LLSD& error) { if (error.isUndefined()) { - handleHandshakeResponse(result); + auto self = that.lock(); + if (self) + { + self->handleHandshakeResponse(result); + } } else { @@ -266,17 +454,7 @@ void LLScriptEditorWSConnection::onClose() { // Call parent class to clean up JSON-RPC infrastructure LLJSONRPCConnection::onClose(); - - LL_INFOS("ScriptEditorWS") << "Script editor JSON-RPC connection closed for session: " - << mEditorId << LL_ENDL; - - cleanupConnection(); - - // Clean up editor-specific state - mEditorId.clear(); - mEditorCapabilities.clear(); - mScriptId.clear(); - mEditorReady = false; + mOwningServer.reset(); // Clean up handshake response data mClientName.clear(); @@ -297,6 +475,8 @@ void LLScriptEditorWSConnection::handleHandshakeResponse(const LLSD& result) mClientVersion = result["client_version"].asString(); mProtocolVersion = result["protocol_version"].asString(); + // TODO: Validate challenge_response if implemented + // Validate protocol compatibility if (mProtocolVersion != "1.0") { @@ -307,7 +487,6 @@ void LLScriptEditorWSConnection::handleHandshakeResponse(const LLSD& result) // Store script information if provided mScriptName = result["script_name"].asString(); mScriptLanguage = result["script_language"].asString(); - mScriptId = result["script_id"].asString(); // Store supported languages for (const auto& lang : llsd::inArray( result["languages"])) @@ -326,67 +505,7 @@ void LLScriptEditorWSConnection::handleHandshakeResponse(const LLSD& result) } } - connectToEditor(mScriptId); - // Mark editor as ready - mEditorReady = true; - - LL_INFOS("ScriptEditorWS") << "Handshake completed successfully for session: " << mEditorId << LL_ENDL; -} - -bool LLScriptEditorWSConnection::connectToEditor(const std::string& script_id) -{ - LLScriptEditorWSServer::ptr_t server = std::dynamic_pointer_cast(mOwningServer.lock()); - if (!server) - { - LL_WARNS("ScriptEditorWS") << "Cannot connect to editor - server reference lost" << LL_ENDL; - return false; - } - - mEditorPanel = server->findEditorForScript(script_id); - - LLScriptEdContainer* editor_core = getEditor(); - if (!editor_core) - { - LL_INFOS("ScriptEditorWS") << "Could not find editor: " << script_id << LL_ENDL; - // TODO: Disconnect the client if no editor found - return false; - } - - return true; -} - -void LLScriptEditorWSConnection::cleanupConnection() -{ - LL_INFOS("ScriptEditorWS") << "Cleaning up connection for editor session: " << mEditorId << LL_ENDL; - - LLScriptEditorWSServer::ptr_t server = getServer(); - if (server) - { - server->dissociateEditor(mScriptId); - } - - LLScriptEdContainer* editor_core = getEditor(); - - if (editor_core) - { - editor_core->cleanupWebSocket(); - - // Notify the editor panel of disconnection - //editor_core->onExternalEditorDisconnected(); - } - - mEditorPanel = LLHandle(); -} - - -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.ok"); - notify("session.disconnect", params); + LL_INFOS("ScriptEditorWS") << "Handshake completed successfully." << LL_ENDL; } diff --git a/indra/newview/llscripteditorws.h b/indra/newview/llscripteditorws.h index 78c675a1f4..f6e40fad6e 100644 --- a/indra/newview/llscripteditorws.h +++ b/indra/newview/llscripteditorws.h @@ -42,44 +42,30 @@ class LLLiveLSLEditor; class LLScriptEdContainer; class LLScriptEditorWSServer; -/** - * @class LLScriptEditorWSConnection - * @brief JSON-RPC WebSocket connection specialized for external script editor communication - * - * 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. - * - * ## Usage - * - * @code - * // Create server and let base JSON-RPC handle method registration - * auto server = std::make_shared("script_editor_server", 9020); - * - * // Register custom methods as needed - * connection->registerMethod("custom.method", handler); - * @endcode - */ -class LLScriptEditorWSConnection : public LLJSONRPCConnection, - public std::enable_shared_from_this +class LLScriptEditorWSConnection : public LLJSONRPCConnection, public std::enable_shared_from_this { public: + using ptr_t = std::shared_ptr; + using wptr_t = std::weak_ptr; + enum DisconnectReason { - REASON_NORMAL = 0, - REASON_EDITOR_CLOSED = 1, + REASON_NORMAL = 0, + REASON_EDITOR_CLOSED = 1, REASON_PROTOCOL_ERROR = 2, - REASON_TIMEOUT = 3, + REASON_TIMEOUT = 3, REASON_INTERNAL_ERROR = 4 }; - LLScriptEditorWSConnection(const LLWebsocketMgr::WSServer::ptr_t server, - const LLWebsocketMgr::connection_h& handle) - : LLJSONRPCConnection(server, handle) - { } + LLScriptEditorWSConnection(const LLWebsocketMgr::WSServer::ptr_t server, const LLWebsocketMgr::connection_h& handle) : + LLJSONRPCConnection(server, handle) + { + mConnectionID = sNextConnectionID++; + } ~LLScriptEditorWSConnection() override = default; + U32 getConnectionID() const { return mConnectionID; } // Connection lifecycle overrides void onOpen() override; @@ -100,26 +86,21 @@ private: */ void handleHandshakeResponse(const LLSD& result); - bool connectToEditor(const std::string& script_id); - void cleanupConnection(); - - LLScriptEdContainer* getEditor() const; + LLScriptEdContainer* getEditor() const; std::shared_ptr getServer() const; - std::string mEditorId; ///< Unique identifier for this editor session - LLSD mEditorCapabilities; ///< Editor capabilities metadata - std::string mScriptId; ///< Unique identifier for the script being edited - bool mEditorReady; ///< Whether editor has completed initialization - LLHandle mEditorPanel; ///< Handle to the associated LSL editor panel + U32 mConnectionID{ 0 }; ///< Unique identifier for this connection // 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.) + 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.) + + static U32 sNextConnectionID; }; /** @@ -161,21 +142,34 @@ private: class LLScriptEditorWSServer : public LLJSONRPCServer { public: + enum SubscriptionError_t + { + SUBSCRIPTION_SUCCESS = 0, + SUBSCRIPTION_INVALID_EDITOR, + SUBSCRIPTION_INVALID_SUBSCRIPTION, + SUBSCRIPTION_ALREADY_SUBSCRIBED, + SUBSCRIPTION_INTERNAL_ERROR + }; + static constexpr char const* DEFAULT_SERVER_NAME = "script_editor_server"; static constexpr U16 DEFAULT_SERVER_PORT = 9020; using ptr_t = std::shared_ptr; + using wptr_t = std::weak_ptr; LLScriptEditorWSServer(const std::string& name, U16 port, bool local_only = true); virtual ~LLScriptEditorWSServer() = default; - // Server lifecycle callbacks + void onStarted() override; + void onStopped() override; void onConnectionOpened(const LLWebsocketMgr::WSConnection::ptr_t& connection) override; void onConnectionClosed(const LLWebsocketMgr::WSConnection::ptr_t& connection) override; - bool associateEditor(const LLHandle& editor_handle, const std::string& script_id); - void dissociateEditor(const std::string& script_id); + bool subscribeScriptEditor(const LLHandle& editor_handle, const std::string &script_id); + void unsubscribeEditor(const std::string &script_id); + + void sendUnsubscribeScriptEditor(const std::string& script_id); LLHandle findEditorForScript(const std::string& script_id) const; std::shared_ptr findConnectionForScript(const std::string& script_id); @@ -186,45 +180,38 @@ public: */ std::set 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(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; + void setupConnectionMethods(LLJSONRPCConnection::ptr_t connection) override; + + void broadcastLangugeChange(); + + LLSD handleLanguageIdRequest() const; + LLSD handleSyntaxRequest(const LLSD ¶ms) const; + LLSD handleScriptSubscribe(U32 connection_id, const LLSD& params); + LLSD handleScriptUnsubscribe(U32 connection_id, const LLSD& params); private: - using map_id_to_editor_t = std::unordered_map>; + struct EditorSubscription + { + LLHandle mEditorHandle; + LLScriptEditorWSConnection::wptr_t mConnection; + U32 mConnectionID{ 0 }; + }; + using subscriptions_t = std::unordered_map; + + SubscriptionError_t updateScriptSubscription(const std::string &script_id, U32 connection_id); + void unsubscribeConnection(U32 connection_id); + + subscriptions_t mSubscriptions; + std::map mActiveConnections; + + boost::signals2::connection mLanguageChangeSignal; + LLUUID mLastSyntaxId; - map_id_to_editor_t mScriptEditors; - std::set> mActiveConnections; - /** - * @brief Connection timeout management - */ LLTimer mCleanupTimer; static constexpr F32 CLEANUP_INTERVAL = 60.0f; // seconds static constexpr F32 CONNECTION_TIMEOUT = 300.0f; // 5 minutes diff --git a/indra/newview/llsyntaxid.cpp b/indra/newview/llsyntaxid.cpp index 1f8766eea2..a891bf945e 100644 --- a/indra/newview/llsyntaxid.cpp +++ b/indra/newview/llsyntaxid.cpp @@ -343,7 +343,7 @@ void LLSyntaxLua::initialize() if (mInitialized) return; loadDefaultKeywordsIntoLLSD(); - + loadLuaTypesIntoLLSD(); mInitialized = true; } @@ -361,3 +361,18 @@ void LLSyntaxLua::loadDefaultKeywordsIntoLLSD() } } } + +void LLSyntaxLua::loadLuaTypesIntoLLSD() +{ + std::string fullFileSpec = gDirUtilp->getExpandedFilename(LL_PATH_APP_SETTINGS, "types_lua_default.llsd"); + llifstream file(fullFileSpec.c_str()); + + if (file.good()) + { + LLSD content; + if (LLSDSerialize::fromXML(content, file) != LLSDParser::PARSE_FAILURE) + { + mTypesXml = content; + } + } +} diff --git a/indra/newview/llsyntaxid.h b/indra/newview/llsyntaxid.h index c24cea1776..51103e3396 100644 --- a/indra/newview/llsyntaxid.h +++ b/indra/newview/llsyntaxid.h @@ -41,11 +41,20 @@ class LLSyntaxIdLSL : public LLSingleton LLSINGLETON(LLSyntaxIdLSL); friend class fetchKeywordsFileResponder; +public: + using syntax_id_changed_signal_t = boost::signals2::signal; + using syntax_id_changed_h = boost::signals2::connection; + + void initialize(); + bool keywordFetchInProgress(); + LLSD getKeywordsXML() const { return mKeywordsXml; }; + LLUUID getSyntaxID() const { return mSyntaxId; } + syntax_id_changed_h addSyntaxIDCallback(const syntax_id_changed_signal_t::slot_type& cb); + private: std::set mInflightFetches; - typedef boost::signals2::signal syntax_id_changed_signal_t; syntax_id_changed_signal_t mSyntaxIDChangedSignal; - boost::signals2::connection mRegionChangedCallback; + syntax_id_changed_h mRegionChangedCallback; bool syntaxIdChanged(); bool isSupportedVersion(const LLSD& content); @@ -67,11 +76,6 @@ private: LLSD mKeywordsXml; bool mInitialized; -public: - void initialize(); - bool keywordFetchInProgress(); - LLSD getKeywordsXML() const { return mKeywordsXml; }; - boost::signals2::connection addSyntaxIDCallback(const syntax_id_changed_signal_t::slot_type& cb); }; @@ -82,11 +86,13 @@ class LLSyntaxLua : public LLSingleton public: void initialize(); LLSD getKeywordsXML() const { return mKeywordsXml; } + LLSD getTypesXML() const { return mTypesXml; } private: void loadDefaultKeywordsIntoLLSD(); - + void loadLuaTypesIntoLLSD(); LLSD mKeywordsXml; + LLSD mTypesXml; bool mInitialized; }; -- cgit v1.3 From 672bdc8915bc5f7f305b6b51751e2c0578ef6032 Mon Sep 17 00:00:00 2001 From: Rider Linden Date: Mon, 29 Sep 2025 16:03:44 -0700 Subject: Some cleanup on the JSONRPC class. --- indra/llcorehttp/lljsonrpcws.cpp | 83 +++++++++++++++++++++--------------- indra/llcorehttp/lljsonrpcws.h | 12 +++--- indra/llcorehttp/llwebsocketmgr.cpp | 84 +------------------------------------ 3 files changed, 56 insertions(+), 123 deletions(-) (limited to 'indra/llcorehttp/llwebsocketmgr.cpp') diff --git a/indra/llcorehttp/lljsonrpcws.cpp b/indra/llcorehttp/lljsonrpcws.cpp index 5de595c595..93e38a8397 100644 --- a/indra/llcorehttp/lljsonrpcws.cpp +++ b/indra/llcorehttp/lljsonrpcws.cpp @@ -118,18 +118,22 @@ void LLJSONRPCConnection::processMessage(const LLSD& message_obj) if (message_obj.has("method")) { // This is a request or notification - validateMessage(message_obj, true); - processRequest(message_obj); + if (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); + if (validateMessage(message_obj, false)) + { + processResponse(message_obj); + } } else { - throw InvalidRequest("Message must contain 'method' or 'result'/'error'"); + LL_WARNS("JSONRPC") << "Message must contain 'method' or 'result'/'error'" << LL_ENDL; } } catch (const RPCError& e) @@ -165,7 +169,6 @@ void LLJSONRPCConnection::processRequest(const LLSD& request) // 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); @@ -225,12 +228,13 @@ void LLJSONRPCConnection::processResponse(const LLSD& response) } } -void LLJSONRPCConnection::validateMessage(const LLSD& message, bool is_request) +bool 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"); + LL_WARNS("JSONRPC") << "Missing or invalid jsonrpc version" << LL_ENDL; + return false; } if (is_request) @@ -238,12 +242,14 @@ void LLJSONRPCConnection::validateMessage(const LLSD& message, bool is_request) // Request/notification validation if (!message.has("method")) { - throw InvalidRequest("Missing method field"); + LL_WARNS("JSONRPC") << "Missing method field" << LL_ENDL; + return false; } if (!message["method"].isString()) { - throw InvalidRequest("Method must be a string"); + LL_WARNS("JSONRPC") << "Method must be a string" << LL_ENDL; + return false; } // Params are optional but must be array or object if present @@ -251,7 +257,8 @@ void LLJSONRPCConnection::validateMessage(const LLSD& message, bool is_request) { if (!message["params"].isArray() && !message["params"].isMap()) { - throw InvalidParams("Params must be array or object"); + LL_WARNS("JSONRPC") << "Params must be array or object" << LL_ENDL; + return false; } } } @@ -260,7 +267,8 @@ void LLJSONRPCConnection::validateMessage(const LLSD& message, bool is_request) // Response validation if (!message.has("id")) { - throw InvalidRequest("Response missing id field"); + LL_WARNS("JSONRPC") << "Response missing id field" << LL_ENDL; + return false; } // Must have either result or error, but not both @@ -269,12 +277,14 @@ void LLJSONRPCConnection::validateMessage(const LLSD& message, bool is_request) if (!has_result && !has_error) { - throw InvalidRequest("Response must have result or error"); + LL_WARNS("JSONRPC") << "Response must have result or error" << LL_ENDL; + return false; } if (has_result && has_error) { - throw InvalidRequest("Response cannot have both result and error"); + LL_WARNS("JSONRPC") << "Response cannot have both result and error" << LL_ENDL; + return false; } // Error must be an object with code and message @@ -283,14 +293,15 @@ void LLJSONRPCConnection::validateMessage(const LLSD& message, bool is_request) LLSD error = message["error"]; if (!error.isMap()) { - throw InvalidRequest("Error must be an object"); + LL_WARNS("JSONRPC") << "Error must be an object" << LL_ENDL; } if (!error.has("code") || !error.has("message")) { - throw InvalidRequest("Error must have code and message"); + LL_WARNS("JSONRPC") << "Error must have code and message" << LL_ENDL; } } } + return true; } LLSD LLJSONRPCConnection::generateId() @@ -307,13 +318,13 @@ LLSD LLJSONRPCConnection::generateId() void LLJSONRPCConnection::registerMethod(const std::string& method, MethodHandler handler) { mMethodHandlers[method] = handler; - LL_INFOS("JSONRPC") << "Registered method: " << method << LL_ENDL; + LL_DEBUGS("JSONRPC") << "Registered method: " << method << LL_ENDL; } void LLJSONRPCConnection::unregisterMethod(const std::string& method) { mMethodHandlers.erase(method); - LL_INFOS("JSONRPC") << "Unregistered method: " << method << LL_ENDL; + LL_DEBUGS("JSONRPC") << "Unregistered method: " << method << LL_ENDL; } LLSD LLJSONRPCConnection::call(const std::string& method, const LLSD& params, ResponseCallback callback) @@ -344,14 +355,15 @@ LLSD LLJSONRPCConnection::call(const std::string& method, const LLSD& params, Re { mPendingRequests.erase(id.asString()); } - throw InternalError("Failed to send request"); + LL_WARNS("JSONRPC") << "Failed to send request" << LL_ENDL; + return LLSD(); } LL_DEBUGS("JSONRPC") << "Sent request: " << method << " with id: " << id.asString() << LL_ENDL; return id; } -void LLJSONRPCConnection::notify(const std::string& method, const LLSD& params) +bool LLJSONRPCConnection::notify(const std::string& method, const LLSD& params) { LLSD notification; notification["jsonrpc"] = "2.0"; @@ -366,13 +378,15 @@ void LLJSONRPCConnection::notify(const std::string& method, const LLSD& params) if (!sendMessage(LlsdToJson(notification))) { - throw InternalError("Failed to send notification"); + LL_WARNS("JSONRPC") << "Failed to send notification" << LL_ENDL; + return false; } LL_DEBUGS("JSONRPC") << "Sent notification: " << method << LL_ENDL; + return true; } -void LLJSONRPCConnection::sendResponse(const LLSD& id, const LLSD& result) +bool LLJSONRPCConnection::sendResponse(const LLSD& id, const LLSD& result) { LLSD response; response["jsonrpc"] = "2.0"; @@ -382,14 +396,13 @@ void LLJSONRPCConnection::sendResponse(const LLSD& id, const LLSD& result) if (!sendMessage(LlsdToJson(response))) { LL_WARNS("JSONRPC") << "Failed to send response for id: " << id.asString() << LL_ENDL; + return false; } - else - { - LL_DEBUGS("JSONRPC") << "Sent response for id: " << id.asString() << LL_ENDL; - } + LL_DEBUGS("JSONRPC") << "Sent response for id: " << id.asString() << LL_ENDL; + return true; } -void LLJSONRPCConnection::sendError(const LLSD& id, const RPCError& error) +bool LLJSONRPCConnection::sendError(const LLSD& id, const RPCError& error) { LLSD response; response["jsonrpc"] = "2.0"; @@ -409,18 +422,18 @@ void LLJSONRPCConnection::sendError(const LLSD& id, const RPCError& error) if (!sendMessage(LlsdToJson(response))) { LL_WARNS("JSONRPC") << "Failed to send error response" << LL_ENDL; + return false; } - else - { - LL_DEBUGS("JSONRPC") << "Sent error response: " << error.what() << LL_ENDL; - } + LL_DEBUGS("JSONRPC") << "Sent error response: " << error.what() << LL_ENDL; + return true; } -void LLJSONRPCConnection::sendBatch(const LLSD& batch, ResponseCallback callback) +bool LLJSONRPCConnection::sendBatch(const LLSD& batch, ResponseCallback callback) { if (!batch.isArray() || batch.size() == 0) { - throw InvalidRequest("Batch must be non-empty array"); + LL_WARNS("JSONRPC") << "Batch must be non-empty array" << LL_ENDL; + return false; } // For batch requests with callbacks, we need to track multiple responses @@ -433,10 +446,12 @@ void LLJSONRPCConnection::sendBatch(const LLSD& batch, ResponseCallback callback if (!sendMessage(LlsdToJson(batch))) { - throw InternalError("Failed to send batch"); + LL_WARNS("JSONRPC") << "Failed to send batch" << LL_ENDL; + return false; } LL_DEBUGS("JSONRPC") << "Sent batch with " << batch.size() << " messages" << LL_ENDL; + return true; } //======================================================================== diff --git a/indra/llcorehttp/lljsonrpcws.h b/indra/llcorehttp/lljsonrpcws.h index 54578c0e93..bd9939aa33 100644 --- a/indra/llcorehttp/lljsonrpcws.h +++ b/indra/llcorehttp/lljsonrpcws.h @@ -230,6 +230,7 @@ public: : RPCError(INVALID_SESSION, details) {} }; + LLJSONRPCConnection(const LLWebsocketMgr::WSServer::ptr_t server, const LLWebsocketMgr::connection_h& handle) : LLWebsocketMgr::WSConnection(server, handle) {} @@ -269,28 +270,28 @@ public: * @param method The method name * @param params The parameters to pass */ - void notify(const std::string& method, const LLSD& params = LLSD()); + bool 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); + bool 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); + bool 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); + bool sendBatch(const LLSD& batch, ResponseCallback callback = nullptr); protected: /** @@ -315,9 +316,8 @@ protected: * @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); + bool validateMessage(const LLSD& message, bool is_request = true); /** * @brief Generate the next unique request ID diff --git a/indra/llcorehttp/llwebsocketmgr.cpp b/indra/llcorehttp/llwebsocketmgr.cpp index 09eb728500..addd108b71 100644 --- a/indra/llcorehttp/llwebsocketmgr.cpp +++ b/indra/llcorehttp/llwebsocketmgr.cpp @@ -181,38 +181,8 @@ void LLWebsocketMgr::stopAllServers() } //------------------------------------------------------------------------ -/** - * @struct Server_impl - * @brief Internal implementation wrapper for websocketpp server functionality - * - * This structure serves as a PIMPL (Pointer to Implementation) wrapper around - * the websocketpp::server template, providing a clean interface between the - * high-level WSServer class and the low-level websocketpp library. It handles - * all direct websocketpp interactions including server lifecycle management, - * event handling, and connection management. - * - * The Server_impl follows Linden Lab conventions while maintaining compatibility - * with the websocketpp library. It provides thread-safe operations where possible - * and integrates with the existing logging infrastructure. - * - * @note This class uses the websocketpp::config::asio configuration which provides - * ASIO-based networking without TLS/SSL support. - */ struct Server_impl { - /** - * @brief Constructor - Initializes the websocketpp server with configuration - * @param owner Pointer to the owning WSServer instance (must not be null) - * @param port The port number to bind the server to (1-65535) - * @param local_only If true, binds only to localhost; if false, binds to all interfaces - * - * Sets up the websocketpp server instance and registers lambda-based event handlers - * for connection open, close, and message events. The handlers delegate back to the - * owning WSServer instance for processing, maintaining the abstraction layer. - * - * @warning The owner pointer must remain valid for the lifetime of this object - * @pre port must be a valid port number (typically > 1024 for non-privileged access) - */ Server_impl(LLWebsocketMgr::WSServer *owner, U16 port, bool local_only) : mOwner(owner), mPort(port), @@ -234,10 +204,6 @@ struct Server_impl * on the specified port. The binding behavior depends on the mLocalOnly flag: * - If mLocalOnly is true: binds to "127.0.0.1" (localhost only) * - If mLocalOnly is false: binds to all available network interfaces - * - * @note This method must be called before attempting to start the server - * @pre The server must not already be initialized - * @post The server is ready to accept connections when start() is called */ void init() { @@ -257,16 +223,6 @@ struct Server_impl /** * @brief Start the websocket server and begin accepting connections * @return true if server started successfully, false on error - * - * Runs a controlled event loop that periodically checks the stop flag for clean shutdown. - * Instead of calling run() once and blocking indefinitely, this implementation uses - * run_for() with a timeout to process events in chunks, checking mOwner->mShouldStop between - * iterations to allow for responsive termination. - * - * @note This method blocks the calling thread until the server stops - * @pre init() must have been called successfully - * @post On success, the server is actively accepting connections - * @warning Any exceptions during startup are caught and logged as warnings */ bool start() { @@ -317,21 +273,6 @@ struct Server_impl } } - /** - * @brief Stop the websocket server and cease accepting new connections - * - * Gracefully shuts down the server by first stopping the listener to prevent - * new connections, then stopping the ASIO event loop. Existing connections - * may remain active briefly during the shutdown process. - * - * The method performs a safe shutdown by checking if the server is already - * stopped before attempting shutdown operations. Any exceptions during shutdown - * are caught and logged but do not propagate. - * - * @note This method is non-blocking and safe to call multiple times - * @post The server will no longer accept new connections - * @post Existing connections will be cleanly terminated - */ void stop() { if (mServer.stopped()) @@ -356,10 +297,6 @@ struct Server_impl * Called automatically by the websocketpp library when a new client connection * is successfully established. This method serves as a bridge between the * low-level websocketpp callback and the high-level WSServer interface. - * - * @note This is an internal callback method called by websocketpp - * @pre mOwner must be valid (assertion will fail if null) - * @post The owning WSServer will be notified of the new connection */ void onOpen(websocketpp::connection_hdl hdl) const { @@ -371,14 +308,6 @@ struct Server_impl /** * @brief Handle connection closure event * @param hdl WebSocket connection handle from websocketpp - * - * Called automatically by the websocketpp library when a client connection - * is closed, either by the client, server, or due to a network error. - * This method delegates the event to the owning WSServer for processing. - * - * @note This is an internal callback method called by websocketpp - * @pre mOwner must be valid (assertion will fail if null) - * @post The owning WSServer will be notified of the connection closure */ void onClose(websocketpp::connection_hdl hdl) const { @@ -395,15 +324,7 @@ struct Server_impl * received from a client. This method validates the connection exists, extracts * the message payload, and forwards it to the appropriate connection handler. * - * Currently handles text messages only. Binary message support and message - * fragmentation handling are noted as TODO items for future implementation. - * - * @note This is an internal callback method called by websocketpp - * @pre mOwner must be valid (assertion will fail if null) - * @post If connection exists, the message is forwarded for processing - * @todo Add support for binary messages - * @todo Implement fragmented message handling - * @todo Process connection close codes for graceful closure + * Currently handles text messages only. */ void onMessage(websocketpp::connection_hdl hdl, Server_t::message_ptr msg) const { @@ -417,7 +338,6 @@ struct Server_impl // TODO: check the FIN bit and handle fragmented messages if needed // TODO: check terminal and close codes and handle connection closure if needed - // TODO: handle binary messages if needed mOwner->handleMessage(hdl, msg->get_payload()); } @@ -505,14 +425,12 @@ void LLWebsocketMgr::WSServer::stop() LL_INFOS("WebSocket") << "Stopping WebSocket server: " << mServerName << LL_ENDL; - // Signal the thread to stop mShouldStop = true; // Stop the websocket server (this will cause the controlled run loop to exit) mImpl->stop(); } // Release the lock here - // Wait for the thread to finish (outside the lock to avoid deadlock) if (mServerThread.joinable()) { mServerThread.join(); -- cgit v1.3 From 939118207ede734fec67dc864aad766b63679552 Mon Sep 17 00:00:00 2001 From: WolfGangS Date: Fri, 21 Nov 2025 17:02:01 +0000 Subject: Minimal fix for ctd --- indra/llcorehttp/llwebsocketmgr.cpp | 26 ++++++++++++++++++++------ 1 file changed, 20 insertions(+), 6 deletions(-) (limited to 'indra/llcorehttp/llwebsocketmgr.cpp') diff --git a/indra/llcorehttp/llwebsocketmgr.cpp b/indra/llcorehttp/llwebsocketmgr.cpp index addd108b71..ea87d1e07e 100644 --- a/indra/llcorehttp/llwebsocketmgr.cpp +++ b/indra/llcorehttp/llwebsocketmgr.cpp @@ -208,15 +208,29 @@ struct Server_impl void init() { mServer.init_asio(); - if (mLocalOnly) + try { + if (mLocalOnly) + { + std::stringstream port_str; + port_str << mPort; + mServer.listen("127.0.0.1", port_str.str()); + } + else + { + mServer.listen(mPort); + } + } + catch (const websocketpp::exception& e) { - std::stringstream port_str; - port_str << mPort; - mServer.listen("127.0.0.1", port_str.str()); + LL_WARNS("WebSocket") << "WebSocket server listen exception: " << e.what() << LL_ENDL; } - else + catch (const std::exception& e) + { + LL_WARNS("WebSocket") << "WebSocket server listen std::exception: " << e.what() << LL_ENDL; + } + catch (...) { - mServer.listen(mPort); + LL_WARNS("WebSocket") << "WebSocket server listen unknown exception" << LL_ENDL; } } -- cgit v1.3