summaryrefslogtreecommitdiff
path: root/indra/llcorehttp
diff options
context:
space:
mode:
authorRider Linden <rider@lindenlab.com>2026-07-22 17:41:55 -0700
committerGitHub <noreply@github.com>2026-07-22 17:41:55 -0700
commita6a2f98070cd55fcaadafc237bd0ffba9d466655 (patch)
tree82f5444312786242e062de65ce6891213b032051 /indra/llcorehttp
parent8a22869dfad731f8cd9f4164a7a2b57cd70af51c (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.cpp389
-rw-r--r--indra/llcorehttp/lljsonrpcws.h95
-rw-r--r--indra/llcorehttp/llwebsocketmgr.cpp36
-rw-r--r--indra/llcorehttp/llwebsocketmgr.h5
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> >;