summaryrefslogtreecommitdiff
path: root/indra/llcorehttp
diff options
context:
space:
mode:
Diffstat (limited to 'indra/llcorehttp')
-rw-r--r--indra/llcorehttp/CMakeLists.txt1
-rw-r--r--indra/llcorehttp/httpstats.cpp3
-rw-r--r--indra/llcorehttp/httpstats.h12
-rw-r--r--indra/llcorehttp/lljsonrpcws.cpp142
-rw-r--r--indra/llcorehttp/lljsonrpcws.h18
-rw-r--r--indra/llcorehttp/tests/llcorehttp_test.cpp1
-rw-r--r--indra/llcorehttp/tests/test_jsonrpcws.hpp146
7 files changed, 292 insertions, 31 deletions
diff --git a/indra/llcorehttp/CMakeLists.txt b/indra/llcorehttp/CMakeLists.txt
index fbc25a7cd5..c2c4078ff0 100644
--- a/indra/llcorehttp/CMakeLists.txt
+++ b/indra/llcorehttp/CMakeLists.txt
@@ -119,6 +119,7 @@ if (LL_TESTS AND LLCOREHTTP_TESTS)
tests/test_httpheaders.hpp
tests/test_bufferarray.hpp
tests/test_bufferstream.hpp
+ tests/test_jsonrpcws.hpp
)
list(APPEND llcorehttp_TEST_SOURCE_FILES ${llcorehttp_TEST_HEADER_FILES})
diff --git a/indra/llcorehttp/httpstats.cpp b/indra/llcorehttp/httpstats.cpp
index dc2ac35c71..f64752e91e 100644
--- a/indra/llcorehttp/httpstats.cpp
+++ b/indra/llcorehttp/httpstats.cpp
@@ -41,6 +41,7 @@ HTTPStats::~HTTPStats()
void HTTPStats::resetStats()
{
+ std::lock_guard<std::mutex> lock(mStatsMutex);
mResutCodes.clear();
mDataDown.reset();
mDataUp.reset();
@@ -50,6 +51,7 @@ void HTTPStats::resetStats()
void HTTPStats::recordResultCode(S32 code)
{
+ std::lock_guard<std::mutex> lock(mStatsMutex);
std::map<S32, S32>::iterator it;
it = mResutCodes.find(code);
@@ -86,6 +88,7 @@ namespace
void HTTPStats::dumpStats()
{
+ std::lock_guard<std::mutex> lock(mStatsMutex);
std::stringstream out;
out << "HTTP DATA SUMMARY" << std::endl;
diff --git a/indra/llcorehttp/httpstats.h b/indra/llcorehttp/httpstats.h
index 5c0f26d34e..08a537cda7 100644
--- a/indra/llcorehttp/httpstats.h
+++ b/indra/llcorehttp/httpstats.h
@@ -47,17 +47,23 @@ namespace LLCore
void recordDataDown(size_t bytes)
{
+ std::lock_guard<std::mutex> lock(mStatsMutex);
mDataDown.push((F32)bytes);
}
void recordDataUp(size_t bytes)
{
+ std::lock_guard<std::mutex> lock(mStatsMutex);
mDataUp.push((F32)bytes);
}
- void recordHTTPRequest() { ++mRequests; }
+ void recordHTTPRequest()
+ {
+ std::lock_guard<std::mutex> lock(mStatsMutex);
+ ++mRequests;
+ }
- void recordResultCode(S32 code);
+ void recordResultCode(S32 code); // http thread
void dumpStats();
private:
@@ -67,6 +73,8 @@ namespace LLCore
S32 mRequests;
std::map<S32, S32> mResutCodes;
+
+ std::mutex mStatsMutex;
};
diff --git a/indra/llcorehttp/lljsonrpcws.cpp b/indra/llcorehttp/lljsonrpcws.cpp
index 3a0d3d1f26..5b704e6322 100644
--- a/indra/llcorehttp/lljsonrpcws.cpp
+++ b/indra/llcorehttp/lljsonrpcws.cpp
@@ -207,16 +207,17 @@ void LLJSONRPCConnection::processRequest(const LLSD& request)
// 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;
+ LL_WARNS("JSONRPC") << "Method " << method
+ << " called as notification; rejecting request" << LL_ENDL;
+ throw InvalidRequest("Method " + method + " cannot be called as a notification");
}
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;
+ LL_WARNS("JSONRPC") << "Connection expired before method " << method
+ << " could be launched; failing request" << LL_ENDL;
+ throw InternalError("Connection expired before method " + method
+ + " could be launched");
}
LLMainThreadTask::dispatch(
[handler, method, id, params, conn]()
@@ -457,6 +458,24 @@ void LLJSONRPCConnection::sweepTimeouts()
}
}
+void LLJSONRPCConnection::testInjectPendingRequest(const std::string& id, F64 deadline, ResponseCallback callback)
+{
+ LLMutexLock lock(&mMutex);
+ mPendingRequests[id] = std::move(callback);
+ mPendingDeadlines.push({ deadline, id });
+}
+
+void LLJSONRPCConnection::testSweepTimeouts()
+{
+ sweepTimeouts();
+}
+
+size_t LLJSONRPCConnection::testPendingRequestCount() const
+{
+ LLMutexLock lock(&mMutex);
+ return mPendingRequests.size();
+}
+
LLSD LLJSONRPCConnection::generateId()
{
// Server-wide atomic counter for efficient unique ID generation.
@@ -497,6 +516,24 @@ void LLJSONRPCConnection::unregisterMethod(const std::string& method)
LL_DEBUGS("JSONRPC") << "Unregistered method: " << method << LL_ENDL;
}
+std::set<std::string> LLJSONRPCConnection::getMethods() const
+{
+ LLMutexLock lock(&mMutex);
+ std::set<std::string> methods;
+
+ for (const auto& [method, handler] : mMethodHandlers)
+ {
+ methods.insert(method);
+ }
+
+ for (const auto& [method, handler] : mAsyncMethodHandlers)
+ {
+ methods.insert(method);
+ }
+
+ return methods;
+}
+
LLSD LLJSONRPCConnection::makeEnvelope(const LLSD& id,
const std::string& method,
const LLSD& params,
@@ -651,23 +688,11 @@ LLJSONRPCServer::LLJSONRPCServer(const std::string& name, U16 port, bool local_o
<< " 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,
@@ -700,21 +725,80 @@ 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
+ std::weak_ptr<LLJSONRPCConnection> weak_connection = connection;
+ connection->registerMethod(
+ "system.listMethods",
+ [weak_connection](
+ const std::string&,
+ const LLSD&,
+ const LLSD&) -> LLSD
{
- LLSD result;
- // Echo back the original timestamp
- if (params.has("timestamp"))
+ LLSD methods(LLSD::emptyArray());
+ auto connection = weak_connection.lock();
+ if (!connection)
+ {
+ return methods;
+ }
+
+ for (const std::string& method : connection->getMethods())
{
- result["timestamp"] = params["timestamp"];
+ methods.append(method);
}
- // Add server's current time in milliseconds
- result["server_time"] = static_cast<LLSD::Integer>(
- LLDate::now().secondsSinceEpoch() * 1000.0);
- return result;
+
+ return methods;
+ });
+
+ connection->registerMethod(
+ "system.ping",
+ [this, weak_connection](
+ const std::string& method,
+ const LLSD& id,
+ const LLSD& params) -> LLSD
+ {
+ LL_DEBUGS("JSONRPC") << "System method " << method
+ << " called" << LL_ENDL;
+ return handlePing(weak_connection.lock(), params);
});
+
+ connection->registerMethod(
+ "system.getVersion",
+ [this, weak_connection](
+ const std::string& method,
+ const LLSD& id,
+ const LLSD& params) -> LLSD
+ {
+ LL_DEBUGS("JSONRPC") << "System method " << method
+ << " called" << LL_ENDL;
+ return handleGetVersion(weak_connection.lock(), params);
+ });
+
+ connection->registerMethod(
+ "system.status",
+ [this, weak_connection](
+ const std::string& method,
+ const LLSD& id,
+ const LLSD& params) -> LLSD
+ {
+ LL_DEBUGS("JSONRPC") << "System method " << method
+ << " called" << LL_ENDL;
+ return handleStatus(weak_connection.lock(), params);
+ });
+}
+
+LLSD LLJSONRPCServer::handlePing(
+ const LLJSONRPCConnection::ptr_t& connection,
+ const LLSD& params) const
+{
+ return LLSD("pong");
+}
+
+LLSD LLJSONRPCServer::handleStatus(
+ const LLJSONRPCConnection::ptr_t& connection,
+ const LLSD& params) const
+{
+ LLSD result;
+ result["status"] = "OK";
+ return result;
}
void LLJSONRPCServer::registerGlobalMethod(const std::string& method, MethodHandler handler)
diff --git a/indra/llcorehttp/lljsonrpcws.h b/indra/llcorehttp/lljsonrpcws.h
index cd71473a47..aa4a96be4d 100644
--- a/indra/llcorehttp/lljsonrpcws.h
+++ b/indra/llcorehttp/lljsonrpcws.h
@@ -275,6 +275,12 @@ public:
void unregisterMethod(const std::string& method);
/**
+ * @brief Get the names of all registered methods
+ * @return Ordered set containing sync and async method names
+ */
+ virtual std::set<std::string> getMethods() const;
+
+ /**
* @brief Make an asynchronous JSON-RPC call
* @param method The method name to call
* @param params The parameters to pass
@@ -401,6 +407,11 @@ private:
/// 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();
+
+public:
+ void testInjectPendingRequest(const std::string& id, F64 deadline, ResponseCallback callback);
+ void testSweepTimeouts();
+ size_t testPendingRequestCount() const;
};
/**
@@ -490,6 +501,13 @@ protected:
LLWebsocketMgr::WSConnection::ptr_t connectionFactory(LLWebsocketMgr::WSServer::ptr_t server,
LLWebsocketMgr::connection_h handle) override;
+ virtual LLSD handlePing(const LLJSONRPCConnection::ptr_t& connection,
+ const LLSD& params) const;
+ virtual LLSD handleGetVersion(const LLJSONRPCConnection::ptr_t& connection,
+ const LLSD& params) const = 0;
+ virtual LLSD handleStatus(const LLJSONRPCConnection::ptr_t& connection,
+ const LLSD& params) const;
+
/**
* @brief Apply global method handlers to a new connection
* @param connection The connection to configure
diff --git a/indra/llcorehttp/tests/llcorehttp_test.cpp b/indra/llcorehttp/tests/llcorehttp_test.cpp
index c7c50e6166..65d7fe93a9 100644
--- a/indra/llcorehttp/tests/llcorehttp_test.cpp
+++ b/indra/llcorehttp/tests/llcorehttp_test.cpp
@@ -44,6 +44,7 @@
#include "test_httprequest.hpp"
#include "test_httpheaders.hpp"
#include "test_httprequestqueue.hpp"
+#include "test_jsonrpcws.hpp"
#include "_httpservice.h"
#include "llproxy.h"
diff --git a/indra/llcorehttp/tests/test_jsonrpcws.hpp b/indra/llcorehttp/tests/test_jsonrpcws.hpp
new file mode 100644
index 0000000000..6cf7932aa0
--- /dev/null
+++ b/indra/llcorehttp/tests/test_jsonrpcws.hpp
@@ -0,0 +1,146 @@
+/**
+ * @file test_jsonrpcws.hpp
+ * @brief unit tests for LLJSONRPCConnection helpers and dispatch behavior
+ */
+
+#ifndef TEST_LLCORE_JSONRPCWS_H_
+#define TEST_LLCORE_JSONRPCWS_H_
+
+#include "lljsonrpcws.h"
+#include "lltimer.h"
+
+namespace
+{
+class TestJSONRPCConnection : public LLJSONRPCConnection
+{
+public:
+ TestJSONRPCConnection()
+ : LLJSONRPCConnection(LLWebsocketMgr::WSServer::ptr_t(), LLWebsocketMgr::connection_h())
+ {
+ }
+
+ using LLJSONRPCConnection::processMessage;
+ using LLJSONRPCConnection::validateMessage;
+ using LLJSONRPCConnection::testInjectPendingRequest;
+ using LLJSONRPCConnection::testPendingRequestCount;
+ using LLJSONRPCConnection::testSweepTimeouts;
+};
+}
+
+namespace tut
+{
+ struct JSONRPCWSTestData
+ {
+ };
+
+ typedef test_group<JSONRPCWSTestData> JSONRPCWSTestGroupType;
+ typedef JSONRPCWSTestGroupType::object JSONRPCWSTestObjectType;
+ JSONRPCWSTestGroupType JSONRPCWSTestGroup("LLJSONRPCConnection Tests");
+
+ template<> template<>
+ void JSONRPCWSTestObjectType::test<1>()
+ {
+ set_test_name("makeEnvelope notification omits id");
+
+ LLSD params;
+ params["value"] = 42;
+ LLSD env = LLJSONRPCConnection::makeEnvelope(LLSD(), "runtime.debug", params, LLSD(), LLSD());
+
+ ensure("jsonrpc field should exist", env.has("jsonrpc"));
+ ensure_equals("jsonrpc version", env["jsonrpc"].asString(), "2.0");
+ ensure("notification should omit id", !env.has("id"));
+ ensure_equals("method", env["method"].asString(), "runtime.debug");
+ ensure_equals("param round trip", env["params"]["value"].asInteger(), 42);
+ }
+
+ template<> template<>
+ void JSONRPCWSTestObjectType::test<2>()
+ {
+ set_test_name("makeEnvelope response keeps id slot");
+
+ LLSD env = LLJSONRPCConnection::makeEnvelope(LLSD(), std::string(), LLSD(), LLSD("ok"), LLSD());
+
+ ensure("response should include id", env.has("id"));
+ ensure("response should not include method", !env.has("method"));
+ ensure_equals("result", env["result"].asString(), "ok");
+ }
+
+ template<> template<>
+ void JSONRPCWSTestObjectType::test<3>()
+ {
+ set_test_name("validateMessage accepts valid request and rejects invalid params");
+
+ TestJSONRPCConnection conn;
+
+ LLSD valid;
+ valid["jsonrpc"] = "2.0";
+ valid["method"] = "session.ping";
+ LLSD params = LLSD::emptyMap();
+ params["timestamp"] = 123;
+ valid["params"] = params;
+ ensure("valid request should pass", conn.validateMessage(valid, true));
+
+ LLSD invalid = valid;
+ invalid["params"] = "not-an-array-or-object";
+ ensure("invalid params type should fail", !conn.validateMessage(invalid, true));
+ }
+
+ template<> template<>
+ void JSONRPCWSTestObjectType::test<4>()
+ {
+ set_test_name("processMessage dispatches notification handler");
+
+ TestJSONRPCConnection conn;
+
+ bool called = false;
+ LLSD seen_id;
+ LLSD seen_params;
+ conn.registerMethod("runtime.debug",
+ [&](const std::string& method, const LLSD& id, const LLSD& params) -> LLSD
+ {
+ called = true;
+ ensure_equals("method propagated", method, "runtime.debug");
+ seen_id = id;
+ seen_params = params;
+ return LLSD();
+ });
+
+ LLSD params;
+ params["message"] = "hello";
+ LLSD notification = LLJSONRPCConnection::makeEnvelope(LLSD(), "runtime.debug", params, LLSD(), LLSD());
+ conn.processMessage(notification);
+
+ ensure("handler should be called", called);
+ ensure("notification id should be undefined", seen_id.isUndefined());
+ ensure_equals("payload should be forwarded", seen_params["message"].asString(), "hello");
+ }
+
+ template<> template<>
+ void JSONRPCWSTestObjectType::test<5>()
+ {
+ set_test_name("sweepTimeouts expires overdue callbacks");
+
+ TestJSONRPCConnection conn;
+
+ bool callback_called = false;
+ conn.testInjectPendingRequest(
+ "req_1",
+ LLTimer::getTotalSeconds() - 1.0,
+ [&](const LLSD& result, const LLSD& error)
+ {
+ callback_called = true;
+ ensure("timed out result should be undefined", result.isUndefined());
+ ensure_equals(
+ "timeout code",
+ error["code"].asInteger(),
+ LLJSONRPCConnection::RPCError::REQUEST_TIMEOUT);
+ });
+
+ ensure_equals("pending request should be tracked", conn.testPendingRequestCount(), (size_t)1);
+ conn.testSweepTimeouts();
+ ensure("timeout callback should be called", callback_called);
+ ensure_equals("pending request should be removed", conn.testPendingRequestCount(), (size_t)0);
+ }
+}
+
+#endif \ No newline at end of file