diff options
| author | Rider Linden <rider@lindenlab.com> | 2026-07-22 17:41:55 -0700 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2026-07-22 17:41:55 -0700 |
| commit | a6a2f98070cd55fcaadafc237bd0ffba9d466655 (patch) | |
| tree | 82f5444312786242e062de65ce6891213b032051 /indra/llcorehttp | |
| parent | 8a22869dfad731f8cd9f4164a7a2b57cd70af51c (diff) | |
Publish object inventories to external editor. (#5834)
* Adding tight integration flag for vscode and open code through a URL.
* [WIP] Publish objects and their contents from the viewer into VS code.
* [WIP] Still very much a work in progress, supports most of the core operations publish, get, write, delete, and create. Still quite a few bugs to work out.
* [WIP] Object publishing checkpoint.
* [checkpoint] Script tight integration with vscode, object publishing.
* Number of fixed issue.
* A few redundancy and performance fixes.
* Some cosmetics.
* I like "Explore" better than "Publish"
* Object renaming, luau inventory icon, runstate, restart.
* Some clean up around permissions and possible nullptr deref.
* Code review feedback.
Diffstat (limited to 'indra/llcorehttp')
| -rw-r--r-- | indra/llcorehttp/lljsonrpcws.cpp | 389 | ||||
| -rw-r--r-- | indra/llcorehttp/lljsonrpcws.h | 95 | ||||
| -rw-r--r-- | indra/llcorehttp/llwebsocketmgr.cpp | 36 | ||||
| -rw-r--r-- | indra/llcorehttp/llwebsocketmgr.h | 5 |
4 files changed, 390 insertions, 135 deletions
diff --git a/indra/llcorehttp/lljsonrpcws.cpp b/indra/llcorehttp/lljsonrpcws.cpp index 93e38a8397..3a0d3d1f26 100644 --- a/indra/llcorehttp/lljsonrpcws.cpp +++ b/indra/llcorehttp/lljsonrpcws.cpp @@ -30,6 +30,10 @@ #include "llerror.h" #include "llsdjson.h" #include "lldate.h" +#include "llcoros.h" +#include "llmainthreadtask.h" +#include "lleventtimer.h" +#include "lltimer.h" #include <boost/json.hpp> @@ -40,29 +44,57 @@ void LLJSONRPCConnection::onOpen() { LL_INFOS("JSONRPC") << "JSON-RPC connection opened" << LL_ENDL; + + // Start the recurring timeout sweep timer on the main thread. The timer + // is canceled in onClose() before the connection can be destroyed, so + // capturing `this` is safe. Keep a weak_ptr so we can safely test + // whether the timer instance still exists at cancellation time. + LLEventTimer* timer = LLEventTimer::run_every(TIMEOUT_SWEEP_INTERVAL, + [this]() { sweepTimeouts(); }); + mTimeoutTimer = timer->getWeak(); } void LLJSONRPCConnection::onClose() { + // Cancel the sweep timer if it is still alive. LLEventTimer's instance + // tracker keeps a shared_ptr with a no-op deleter, so raw `delete` is + // the documented cancellation idiom (see lleventtimer.h). + if (auto timer = mTimeoutTimer.lock()) + { + delete timer.get(); + } + mTimeoutTimer.reset(); + + // Move the pending-request map out under the lock so we can invoke the + // callbacks without holding it (callbacks may themselves call into this + // connection). + std::unordered_map<std::string, ResponseCallback> pending; + { + LLMutexLock lock(&mMutex); + pending.swap(mPendingRequests); + // Deadlines correspond to entries in mPendingRequests; drop them. + std::priority_queue<PendingDeadline> empty; + mPendingDeadlines.swap(empty); + } + LL_INFOS("JSONRPC") << "JSON-RPC connection closed, clearing " - << mPendingRequests.size() << " pending requests" << LL_ENDL; + << pending.size() << " pending requests" << LL_ENDL; - // Cancel all pending requests - for (auto& [id, callback] : mPendingRequests) + for (auto& [id, callback] : pending) { if (callback) { LLSD error; - error["code"] = RPCError::CONNECTION_CLOSED; // Use named constant instead of magic number + error["code"] = RPCError::CONNECTION_CLOSED; error["message"] = "Connection closed"; callback(LLSD(), error); } } - mPendingRequests.clear(); } void LLJSONRPCConnection::onMessage(const std::string& message) { + LL_PROFILE_ZONE_SCOPED_CATEGORY_WEBSOCKET; LL_DEBUGS("JSONRPC") << "Received JSON-RPC message: " << message << LL_ENDL; try @@ -84,24 +116,18 @@ void LLJSONRPCConnection::onMessage(const std::string& message) // 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); + // JSON-RPC 2.0 batch requests are intentionally not supported. + // No known client (including the sl-vscode-plugin) sends batches, + // and a spec-compliant implementation would require accumulating + // responses across sync + async handlers before shipping a single + // array frame. If a real use case appears, implement per + // JSON-RPC 2.0 §6. + sendError(LLSD(), InvalidRequest("Batch requests are not supported")); + return; } + + // Single message + processMessage(message_obj); } catch (const std::exception& e) { @@ -112,6 +138,7 @@ void LLJSONRPCConnection::onMessage(const std::string& message) void LLJSONRPCConnection::processMessage(const LLSD& message_obj) { + LL_PROFILE_ZONE_SCOPED_CATEGORY_WEBSOCKET; try { // Determine if this is a request, notification, or response @@ -145,6 +172,7 @@ void LLJSONRPCConnection::processMessage(const LLSD& message_obj) void LLJSONRPCConnection::processRequest(const LLSD& request) { + LL_PROFILE_ZONE_SCOPED_CATEGORY_WEBSOCKET; std::string method = request["method"].asString(); LLSD params = request.has("params") ? request["params"] : LLSD(); LLSD id = request.has("id") ? request["id"] : LLSD(); @@ -153,9 +181,93 @@ void LLJSONRPCConnection::processRequest(const LLSD& request) 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()) + // Resolve the handler under the mutex, then invoke it unlocked. + MethodHandler handler; + bool is_async = false; + { + LLMutexLock lock(&mMutex); + auto async_it = mAsyncMethodHandlers.find(method); + if (async_it != mAsyncMethodHandlers.end()) + { + handler = async_it->second; + is_async = true; + } + else + { + auto sync_it = mMethodHandlers.find(method); + if (sync_it != mMethodHandlers.end()) + { + handler = sync_it->second; + } + } + } + + if (is_async) + { + // Async handler — launched as a coroutine, response sent by the lambda. + if (is_notification) + { + LL_WARNS("JSONRPC") << "Async method " << method + << " called as notification; ignoring" << LL_ENDL; + return; + } + ptr_t conn = std::static_pointer_cast<LLJSONRPCConnection>(getSelfPtr()); + if (!conn) + { + LL_WARNS("JSONRPC") << "Connection expired before async method " << method + << " could be launched" << LL_ENDL; + return; + } + LLMainThreadTask::dispatch( + [handler, method, id, params, conn]() + { + LLCoros::instance().launch( + "JSONRPC::" + method, + [handler, method, id, params, conn]() + { + try + { + LLSD result = handler(method, id, params); + if (conn->isConnected()) + { + conn->sendResponse(id, result); + } + else + { + LL_WARNS("JSONRPC") << "Connection closed before async method " + << method << " could send response" << LL_ENDL; + } + } + catch (const RPCError& e) + { + if (conn->isConnected()) + { + conn->sendError(id, e); + } + else + { + LL_WARNS("JSONRPC") << "Connection closed before async method " + << method << " could send error" << LL_ENDL; + } + } + catch (const std::exception& e) + { + if (conn->isConnected()) + { + conn->sendError(id, InternalError(e.what())); + } + else + { + LL_WARNS("JSONRPC") << "Connection closed before async method " + << method << " could send error" << LL_ENDL; + } + } + }); + }); + return; + } + + if (!handler) { if (!is_notification) { @@ -166,8 +278,7 @@ void LLJSONRPCConnection::processRequest(const LLSD& request) try { - // Call the method handler with method name, ID, and parameters - LLSD result = it->second(method, id, params); + LLSD result = handler(method, id, params); if (!is_notification) { @@ -202,6 +313,7 @@ void LLJSONRPCConnection::processRequest(const LLSD& request) void LLJSONRPCConnection::processResponse(const LLSD& response) { + LL_PROFILE_ZONE_SCOPED_CATEGORY_WEBSOCKET; if (!response.has("id")) { LL_WARNS("JSONRPC") << "Response missing id field" << LL_ENDL; @@ -209,20 +321,23 @@ void LLJSONRPCConnection::processResponse(const LLSD& response) } std::string id = response["id"].asString(); - auto it = mPendingRequests.find(id); - if (it == mPendingRequests.end()) + ResponseCallback callback; { - LL_WARNS("JSONRPC") << "Received response for unknown request id: " << id << LL_ENDL; - return; + LLMutexLock lock(&mMutex); + auto it = mPendingRequests.find(id); + if (it == mPendingRequests.end()) + { + LL_WARNS("JSONRPC") << "Received response for unknown request id: " << id << LL_ENDL; + return; + } + callback = std::move(it->second); + mPendingRequests.erase(it); } - 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(); + LLSD error = response.has("error") ? response["error"] : LLSD(); callback(result, error); } @@ -294,21 +409,60 @@ bool LLJSONRPCConnection::validateMessage(const LLSD& message, bool is_request) if (!error.isMap()) { LL_WARNS("JSONRPC") << "Error must be an object" << LL_ENDL; + return false; } if (!error.has("code") || !error.has("message")) { LL_WARNS("JSONRPC") << "Error must have code and message" << LL_ENDL; + return false; } } } return true; } +void LLJSONRPCConnection::sweepTimeouts() +{ + LL_PROFILE_ZONE_SCOPED_CATEGORY_WEBSOCKET; + // Pop expired deadlines and collect their callbacks. Tombstones (entries + // whose request already completed) are silently discarded. + std::vector<std::pair<std::string, ResponseCallback>> expired; + const F64 now = LLTimer::getTotalSeconds(); + { + LLMutexLock lock(&mMutex); + while (!mPendingDeadlines.empty() && mPendingDeadlines.top().mDeadline <= now) + { + std::string id = mPendingDeadlines.top().mId; + mPendingDeadlines.pop(); + auto it = mPendingRequests.find(id); + if (it != mPendingRequests.end()) + { + expired.emplace_back(std::move(id), std::move(it->second)); + mPendingRequests.erase(it); + } + } + } + + for (auto& [id, callback] : expired) + { + LL_WARNS("JSONRPC") << "Request " << id << " timed out after " + << REQUEST_TIMEOUT_SECONDS << " seconds" << LL_ENDL; + if (callback) + { + LLSD error; + error["code"] = RPCError::REQUEST_TIMEOUT; + error["message"] = "Request timed out"; + callback(LLSD(), error); + } + } +} + LLSD LLJSONRPCConnection::generateId() { - // Server-wide atomic counter for efficient unique ID generation - // Start from 1000 to avoid conflicts with any manual test IDs - static std::atomic<U64> sRequestIdCounter{1000}; + // Server-wide atomic counter for efficient unique ID generation. + // Start above zero to avoid conflicts with any manual test IDs. + static constexpr U64 REQUEST_ID_START = 1000; + static std::atomic<U64> sRequestIdCounter{REQUEST_ID_START}; // Generate server-unique sequential ID U64 id = sRequestIdCounter.fetch_add(1); @@ -317,43 +471,87 @@ LLSD LLJSONRPCConnection::generateId() void LLJSONRPCConnection::registerMethod(const std::string& method, MethodHandler handler) { - mMethodHandlers[method] = handler; + { + LLMutexLock lock(&mMutex); + mMethodHandlers[method] = std::move(handler); + } LL_DEBUGS("JSONRPC") << "Registered method: " << method << LL_ENDL; } +void LLJSONRPCConnection::registerAsyncMethod(const std::string& method, MethodHandler handler) +{ + { + LLMutexLock lock(&mMutex); + mAsyncMethodHandlers[method] = std::move(handler); + } + LL_DEBUGS("JSONRPC") << "Registered async method: " << method << LL_ENDL; +} + void LLJSONRPCConnection::unregisterMethod(const std::string& method) { - mMethodHandlers.erase(method); + { + LLMutexLock lock(&mMutex); + mMethodHandlers.erase(method); + mAsyncMethodHandlers.erase(method); + } LL_DEBUGS("JSONRPC") << "Unregistered method: " << method << LL_ENDL; } -LLSD LLJSONRPCConnection::call(const std::string& method, const LLSD& params, ResponseCallback callback) +LLSD LLJSONRPCConnection::makeEnvelope(const LLSD& id, + const std::string& method, + const LLSD& params, + const LLSD& result, + const LLSD& error) { - LLSD request; - request["jsonrpc"] = "2.0"; - request["method"] = method; - - if (!params.isUndefined()) + LLSD env; + env["jsonrpc"] = "2.0"; + // Notifications (requests without an id) are the only case that omits id. + if (!(id.isUndefined() && !method.empty())) + { + env["id"] = id; + } + if (!method.empty()) + { + env["method"] = method; + } + if (params.isDefined()) + { + env["params"] = params; + } + if (result.isDefined()) { - request["params"] = params; + env["result"] = result; } + if (error.isDefined()) + { + env["error"] = error; + } + return env; +} +LLSD LLJSONRPCConnection::call(const std::string& method, const LLSD& params, ResponseCallback callback) +{ + LL_PROFILE_ZONE_SCOPED_CATEGORY_WEBSOCKET; LLSD id = generateId(); - request["id"] = id; + LLSD request = makeEnvelope(id, method, params, LLSD(), LLSD()); + const std::string id_str = id.asString(); - // Store callback if provided + // Store callback if provided. Fire-and-forget calls (no callback) are + // not tracked for timeouts since there is nobody to deliver the error to. if (callback) { - mPendingRequests[id.asString()] = callback; + LLMutexLock lock(&mMutex); + mPendingRequests[id_str] = std::move(callback); + mPendingDeadlines.push({ LLTimer::getTotalSeconds() + REQUEST_TIMEOUT_SECONDS, id_str }); } // Send the request if (!sendMessage(LlsdToJson(request))) { // Remove from pending if send failed - if (callback) { - mPendingRequests.erase(id.asString()); + LLMutexLock lock(&mMutex); + mPendingRequests.erase(id_str); } LL_WARNS("JSONRPC") << "Failed to send request" << LL_ENDL; return LLSD(); @@ -365,16 +563,8 @@ LLSD LLJSONRPCConnection::call(const std::string& method, const LLSD& params, Re bool 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 + LL_PROFILE_ZONE_SCOPED_CATEGORY_WEBSOCKET; + LLSD notification = makeEnvelope(LLSD(), method, params, LLSD(), LLSD()); if (!sendMessage(LlsdToJson(notification))) { @@ -388,10 +578,8 @@ bool LLJSONRPCConnection::notify(const std::string& method, const LLSD& params) bool LLJSONRPCConnection::sendResponse(const LLSD& id, const LLSD& result) { - LLSD response; - response["jsonrpc"] = "2.0"; - response["result"] = result; - response["id"] = id; + LL_PROFILE_ZONE_SCOPED_CATEGORY_WEBSOCKET; + LLSD response = makeEnvelope(id, std::string(), LLSD(), result, LLSD()); if (!sendMessage(LlsdToJson(response))) { @@ -404,9 +592,7 @@ bool LLJSONRPCConnection::sendResponse(const LLSD& id, const LLSD& result) bool LLJSONRPCConnection::sendError(const LLSD& id, const RPCError& error) { - LLSD response; - response["jsonrpc"] = "2.0"; - + LL_PROFILE_ZONE_SCOPED_CATEGORY_WEBSOCKET; LLSD error_obj; error_obj["code"] = error.getCode(); error_obj["message"] = error.what(); @@ -416,8 +602,8 @@ bool LLJSONRPCConnection::sendError(const LLSD& id, const RPCError& error) error_obj["data"] = error.getData(); } - response["error"] = error_obj; - response["id"] = id.isUndefined() ? LLSD() : id; // null for parse errors + // Responses always include id; an undefined id serializes as null (used for parse errors). + LLSD response = makeEnvelope(id, std::string(), LLSD(), LLSD(), error_obj); if (!sendMessage(LlsdToJson(response))) { @@ -513,6 +699,22 @@ void LLJSONRPCServer::setupConnectionMethods(LLJSONRPCConnection::ptr_t connecti { connection->registerMethod(method, handler); } + + // Register session.ping handler for connection health monitoring + connection->registerMethod("session.ping", + [](const std::string&, const LLSD&, const LLSD& params) -> LLSD + { + LLSD result; + // Echo back the original timestamp + if (params.has("timestamp")) + { + result["timestamp"] = params["timestamp"]; + } + // Add server's current time in milliseconds + result["server_time"] = static_cast<LLSD::Integer>( + LLDate::now().secondsSinceEpoch() * 1000.0); + return result; + }); } void LLJSONRPCServer::registerGlobalMethod(const std::string& method, MethodHandler handler) @@ -558,54 +760,21 @@ LLSD LLJSONRPCServer::getMethodList() const void LLJSONRPCServer::broadcastNotification(const std::string& method, const LLSD& params) { + LL_PROFILE_ZONE_SCOPED_CATEGORY_WEBSOCKET; // 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; - } + LLSD notification = LLJSONRPCConnection::makeEnvelope(LLSD(), method, params, LLSD(), LLSD()); // Use the base class broadcast functionality broadcastMessage(boost::json::serialize(LlsdToJson(notification))); - mTotalNotificationsSent += getConnectionCount(); + // Cache the count: getConnectionCount() walks a locked container in the base. + size_t count = getConnectionCount(); + mTotalNotificationsSent += count; LL_DEBUGS("JSONRPC") << "Broadcast notification: " << method - << " to " << getConnectionCount() << " clients" << LL_ENDL; -} - -void LLJSONRPCServer::broadcastCall(const std::string& method, const LLSD& params, - BatchResponseCallback callback) -{ - if (callback) - { - LL_WARNS("JSONRPC") << "Broadcast call response callbacks not yet implemented" << LL_ENDL; - } - - // Create the request message with a server-unique ID - LLSD request; - request["jsonrpc"] = "2.0"; - request["method"] = method; - - // Use the same ID generation as connections for consistency - static std::atomic<U64> sBroadcastIdCounter{10000000}; // Start at 10M to clearly distinguish from regular requests - U64 id = sBroadcastIdCounter.fetch_add(1); - request["id"] = LLSD(llformat("broadcast_%llu", id)); - - if (!params.isUndefined()) - { - request["params"] = params; - } - - // Use the base class broadcast functionality - broadcastMessage(boost::json::serialize(LlsdToJson(request))); - - LL_DEBUGS("JSONRPC") << "Broadcast call: " << method - << " to " << getConnectionCount() << " clients" << LL_ENDL; + << " to " << count << " clients" << LL_ENDL; } LLSD LLJSONRPCServer::getServerStats() const diff --git a/indra/llcorehttp/lljsonrpcws.h b/indra/llcorehttp/lljsonrpcws.h index bd9939aa33..cd71473a47 100644 --- a/indra/llcorehttp/lljsonrpcws.h +++ b/indra/llcorehttp/lljsonrpcws.h @@ -33,6 +33,9 @@ #include <functional> #include <unordered_map> #include <memory> +#include <queue> + +class LLEventTimer; /** * @class LLJSONRPCConnection @@ -218,12 +221,6 @@ public: : 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") @@ -235,7 +232,7 @@ public: const LLWebsocketMgr::connection_h& handle) : LLWebsocketMgr::WSConnection(server, handle) {} - virtual ~LLJSONRPCConnection() = default; + ~LLJSONRPCConnection() override = default; // WebSocket connection lifecycle void onOpen() override; @@ -246,10 +243,32 @@ public: * @brief Register a method handler * @param method The method name to register * @param handler The function to call when this method is invoked + * + * @warning Sync handlers execute on the WebSocket I/O thread. They must + * only touch state that is either internal to this connection + * (protected by the connection's mutex) or otherwise thread-safe. + * Do NOT read or write viewer main-thread-only state (e.g., + * gAgent, gObjectList, LLSelectMgr, LLFloaterReg, gSavedSettings, + * LLInventoryModel, or any LLViewerObject) from a sync handler; + * register with registerAsyncMethod() instead, which dispatches + * to the main thread inside a coroutine. */ void registerMethod(const std::string& method, MethodHandler handler); /** + * @brief Register an async method handler, executed in a coroutine + * + * Unlike registerMethod(), the handler runs inside an LLCoros coroutine + * and may use llcoro::suspendUntilEventOn* to wait for async results. + * The handler returns its result normally; the framework sends the + * JSON-RPC response automatically when the coroutine returns. + * + * @param method The method name to register + * @param handler The coroutine-safe function to call + */ + void registerAsyncMethod(const std::string& method, MethodHandler handler); + + /** * @brief Unregister a method handler * @param method The method name to unregister */ @@ -336,9 +355,52 @@ protected: */ LLSD generateId(); +public: + /** + * @brief Build a JSON-RPC 2.0 envelope. + * + * Stamps "jsonrpc" = "2.0" and includes only the fields that are set: + * - @a method is included when non-empty. + * - @a params, @a result, @a error are included when defined. + * - @a id is included unless it is undefined and @a method is non-empty + * (i.e. notifications omit id; responses keep id, serializing an + * undefined id as JSON null per the JSON-RPC spec). + */ + static LLSD makeEnvelope(const LLSD& id, + const std::string& method, + const LLSD& params, + const LLSD& result, + const LLSD& error); + private: + // Guards the three maps below. Handlers/callbacks are copied out from + // under the lock and then invoked without it held, to avoid re-entrancy + // and to keep the critical section short. + mutable LLMutex mMutex; std::unordered_map<std::string, MethodHandler> mMethodHandlers; + std::unordered_map<std::string, MethodHandler> mAsyncMethodHandlers; std::unordered_map<std::string, ResponseCallback> mPendingRequests; + + // Per-request timeout tracking. mPendingDeadlines is a min-heap of + // (deadline, request_id) ordered by deadline; entries whose request has + // already been answered become tombstones (skipped when they reach the + // top). A single recurring timer per connection sweeps the heap. + struct PendingDeadline + { + F64 mDeadline; // absolute time in seconds (LLTimer::getTotalSeconds) + std::string mId; + // std::priority_queue is a max-heap; invert to get min-heap by deadline. + bool operator<(const PendingDeadline& rhs) const { return mDeadline > rhs.mDeadline; } + }; + std::priority_queue<PendingDeadline> mPendingDeadlines; + std::weak_ptr<LLEventTimer> mTimeoutTimer; + + static constexpr F64 REQUEST_TIMEOUT_SECONDS = 120.0; + static constexpr F32 TIMEOUT_SWEEP_INTERVAL = 1.0f; + + /// Invoked by the sweep timer; fires the timeout callback for any + /// request whose deadline has passed. Safe to call from the main thread. + void sweepTimeouts(); }; /** @@ -376,13 +438,6 @@ private: * @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 @@ -391,10 +446,9 @@ public: using ptr_t = std::shared_ptr<LLJSONRPCServer>; using MethodHandler = LLJSONRPCConnection::MethodHandler; using ResponseCallback = LLJSONRPCConnection::ResponseCallback; - using BatchResponseCallback = std::function<void(const LLSD& responses)>; LLJSONRPCServer(const std::string& name, U16 port, bool local_only = true); - virtual ~LLJSONRPCServer() = default; + ~LLJSONRPCServer() override = default; // Server lifecycle callbacks void onConnectionOpened(const LLWebsocketMgr::WSConnection::ptr_t& connection) override; @@ -427,15 +481,6 @@ public: 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. */ diff --git a/indra/llcorehttp/llwebsocketmgr.cpp b/indra/llcorehttp/llwebsocketmgr.cpp index ea87d1e07e..be1ab54efd 100644 --- a/indra/llcorehttp/llwebsocketmgr.cpp +++ b/indra/llcorehttp/llwebsocketmgr.cpp @@ -65,6 +65,7 @@ void LLWebsocketMgr::cleanupSingleton() void LLWebsocketMgr::update() { + LL_PROFILE_ZONE_SCOPED_CATEGORY_WEBSOCKET; std::vector<WSServer::ptr_t> stops; for (auto &[name, server] : mServers) @@ -255,6 +256,7 @@ struct Server_impl // Run controlled event loop with periodic stop flag checking while (!mOwner->mShouldStop && !mServer.stopped()) { + LL_PROFILE_ZONE_NAMED_CATEGORY_WEBSOCKET("ws server run_for"); // 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); @@ -314,6 +316,7 @@ struct Server_impl */ void onOpen(websocketpp::connection_hdl hdl) const { + LL_PROFILE_ZONE_SCOPED_CATEGORY_WEBSOCKET; LL_ERRS_IF(!mOwner, "WebSocket") << "mOwner should never be null. If it is, something is very wrong!" << LL_ENDL; mOwner->handleOpenConnection(hdl); @@ -325,6 +328,7 @@ struct Server_impl */ void onClose(websocketpp::connection_hdl hdl) const { + LL_PROFILE_ZONE_SCOPED_CATEGORY_WEBSOCKET; LL_ERRS_IF(!mOwner, "WebSocket") << "mOwner should never be null" << LL_ENDL; mOwner->handleCloseConnection(hdl); } @@ -342,6 +346,7 @@ struct Server_impl */ void onMessage(websocketpp::connection_hdl hdl, Server_t::message_ptr msg) const { + LL_PROFILE_ZONE_SCOPED_CATEGORY_WEBSOCKET; LL_ERRS_IF(!mOwner, "WebSocket") << "mOwner should never be null" << LL_ENDL; LLWebsocketMgr::WSConnection::ptr_t connection = mOwner->getConnection(hdl); if (!connection) @@ -441,6 +446,9 @@ void LLWebsocketMgr::WSServer::stop() mShouldStop = true; + // Send close frames to all connected clients before stopping the ASIO loop + closeAllConnections(1001, "Server shutting down"); + // Stop the websocket server (this will cause the controlled run loop to exit) mImpl->stop(); } // Release the lock here @@ -463,6 +471,7 @@ bool LLWebsocketMgr::WSServer::isRunning() const void LLWebsocketMgr::WSServer::broadcastMessage(const std::string& message) { + LL_PROFILE_ZONE_SCOPED_CATEGORY_WEBSOCKET; LL_ERRS_IF(!mImpl, "WebSocket") << "WebSocket server " << mServerName << " implementation is null !" << LL_ENDL; LLMutexLock lock(&mConnectionMutex); for (const auto& [handle, conn] : mConnections) @@ -473,6 +482,7 @@ void LLWebsocketMgr::WSServer::broadcastMessage(const std::string& message) bool LLWebsocketMgr::WSServer::sendMessageTo(const connection_h& handle, const std::string& message) { + LL_PROFILE_ZONE_SCOPED_CATEGORY_WEBSOCKET; 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); @@ -546,6 +556,7 @@ LLWebsocketMgr::connection_state_t LLWebsocketMgr::WSServer::getConnectionState( void LLWebsocketMgr::WSServer::handleOpenConnection(const connection_h& handle) { + LL_PROFILE_ZONE_SCOPED_CATEGORY_WEBSOCKET; WSConnection::ptr_t connection; size_t size(0); { @@ -582,6 +593,7 @@ void LLWebsocketMgr::WSServer::handleOpenConnection(const connection_h& handle) void LLWebsocketMgr::WSServer::handleCloseConnection(const connection_h& handle) { + LL_PROFILE_ZONE_SCOPED_CATEGORY_WEBSOCKET; size_t size(0); WSConnection::ptr_t connection; { @@ -608,6 +620,7 @@ void LLWebsocketMgr::WSServer::handleCloseConnection(const connection_h& handle) void LLWebsocketMgr::WSServer::handleMessage(const connection_h& handle, const std::string& message) { + LL_PROFILE_ZONE_SCOPED_CATEGORY_WEBSOCKET; WSConnection::ptr_t connection = getConnection(handle); if (connection) { @@ -672,3 +685,26 @@ bool LLWebsocketMgr::WSConnection::isConnected() const } return server->getConnectionState(mConnectionHandle) == connection_open; } + +LLWebsocketMgr::WSConnection::ptr_t LLWebsocketMgr::WSConnection::getSelfPtr() +{ + auto server = mOwningServer.lock(); + if (!server) return nullptr; + return server->getConnection(mConnectionHandle); +} + +void LLWebsocketMgr::WSServer::closeAllConnections(U16 code, const std::string& reason) +{ + std::vector<connection_h> handles; + { + LLMutexLock lock(&mConnectionMutex); + for (const auto& [handle, conn] : mConnections) + { + handles.push_back(handle); + } + } + for (const auto& handle : handles) + { + closeConnection(handle, code, reason); + } +} diff --git a/indra/llcorehttp/llwebsocketmgr.h b/indra/llcorehttp/llwebsocketmgr.h index 4165b3cecc..2c335307e3 100644 --- a/indra/llcorehttp/llwebsocketmgr.h +++ b/indra/llcorehttp/llwebsocketmgr.h @@ -152,6 +152,10 @@ public: bool isConnected() const; protected: + /// Returns a shared_ptr to this connection, retrieved from the owning server. + /// Valid only while the connection is open and registered with the server. + ptr_t getSelfPtr(); + connection_h mConnectionHandle; std::weak_ptr<WSServer> mOwningServer; // Back-reference to the server this connection belongs to }; @@ -260,6 +264,7 @@ public: * 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()); + void closeAllConnections(U16 code = 1001, const std::string& reason = "Server shutting down"); private: using connection_map_t = std::map<connection_h, WSConnection::ptr_t, std::owner_less<connection_h> >; |
