diff options
| -rw-r--r-- | indra/llcorehttp/llwebsocketmgr.cpp | 287 | ||||
| -rw-r--r-- | indra/llcorehttp/llwebsocketmgr.h | 241 |
2 files changed, 452 insertions, 76 deletions
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 <websocketpp/client.hpp> #include <websocketpp/server.hpp> +#include <thread> +#include <atomic> +#include <chrono> + //------------------------------------------------------------------------ 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 <map> #include <vector> #include <functional> +#include <thread> +#include <atomic> #include <websocketpp/common/connection_hdl.hpp> -#if 0 -// Forward declarations -namespace websocketpp { - namespace config { - struct asio_client; - struct asio; - } - template <typename config> - class client; - template <typename config> - 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<void(EventType event, const LLUUID& connection_id, const LLSD& data)>; - using MessageCallback = std::function<void(const LLUUID& connection_id, const std::string& message, MessageType type)>; - - /// 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<WSConnection>; + + /** + * @brief Constructor for WSConnection + * @param server Shared pointer to the parent WSServer + * @param handle WebSocket connection handle from websocketpp + */ WSConnection(const std::shared_ptr<WSServer> &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<WSServer> 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<MyConnection>(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<WSServer> { friend struct Server_impl; @@ -154,7 +254,7 @@ public: using ptr_t = std::shared_ptr<WSServer>; 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<connection_h, WSConnection::ptr_t, std::owner_less<connection_h> >; @@ -183,6 +295,11 @@ public: std::unique_ptr<Server_impl> mImpl; connection_map_t mConnections; LLMutex mConnectionMutex; + + // Threading support + std::thread mServerThread; ///< Thread running the ASIO event loop + std::atomic<bool> mShouldStop{ false }; ///< Thread-safe stop flag + mutable LLMutex mThreadMutex; ///< Mutex for thread synchronization }; // Server and Connection Management |
