diff options
| author | Erik Kundiman <erik@megapahit.org> | 2026-09-01 18:35:23 +0800 |
|---|---|---|
| committer | Erik Kundiman <erik@megapahit.org> | 2026-09-04 21:58:53 +0800 |
| commit | 00bb3bb6f07660bd914d29a1389342bb3060d254 (patch) | |
| tree | 431f574922494d2201718f13085f17bc6bd7fb42 /indra/llcorehttp | |
| parent | a8636720129952c10cf49ab80da21bf8b340b96c (diff) | |
| parent | 4ef9f8f14b35ed388c294d53767a0dca0e890924 (diff) | |
Merge remote-tracking branch 'secondlife/release/26.4' into 26.4
Diffstat (limited to 'indra/llcorehttp')
| -rw-r--r-- | indra/llcorehttp/CMakeLists.txt | 22 | ||||
| -rw-r--r-- | indra/llcorehttp/_httpservice.cpp | 50 | ||||
| -rw-r--r-- | indra/llcorehttp/_httpservice.h | 2 | ||||
| -rw-r--r-- | indra/llcorehttp/_mutex.h | 11 | ||||
| -rw-r--r-- | indra/llcorehttp/_refcounted.h | 10 | ||||
| -rw-r--r-- | indra/llcorehttp/_thread.h | 103 | ||||
| -rw-r--r-- | indra/llcorehttp/httpcommon.cpp | 4 | ||||
| -rw-r--r-- | indra/llcorehttp/httpcommon.h | 6 | ||||
| -rw-r--r-- | indra/llcorehttp/httpstats.cpp | 3 | ||||
| -rw-r--r-- | indra/llcorehttp/httpstats.h | 12 | ||||
| -rw-r--r--[-rwxr-xr-x] | indra/llcorehttp/llhttpconstants.cpp | 0 | ||||
| -rw-r--r--[-rwxr-xr-x] | indra/llcorehttp/llhttpconstants.h | 2 | ||||
| -rw-r--r-- | indra/llcorehttp/lljsonrpcws.cpp | 815 | ||||
| -rw-r--r-- | indra/llcorehttp/lljsonrpcws.h | 511 | ||||
| -rw-r--r-- | indra/llcorehttp/llwebsocketmgr.cpp | 710 | ||||
| -rw-r--r-- | indra/llcorehttp/llwebsocketmgr.h | 310 | ||||
| -rw-r--r--[-rwxr-xr-x] | indra/llcorehttp/tests/llcorehttp_test.cpp | 1 | ||||
| -rw-r--r-- | indra/llcorehttp/tests/test_httprequest.hpp | 15 | ||||
| -rw-r--r-- | indra/llcorehttp/tests/test_jsonrpcws.hpp | 146 |
19 files changed, 2609 insertions, 124 deletions
diff --git a/indra/llcorehttp/CMakeLists.txt b/indra/llcorehttp/CMakeLists.txt index ab3a86b37e..2cf889bb9d 100644 --- a/indra/llcorehttp/CMakeLists.txt +++ b/indra/llcorehttp/CMakeLists.txt @@ -13,6 +13,7 @@ include(LLAddBuildTest) endif () include(LLCommon) include(bugsplat) +include(websocketpp) set(llcorehttp_SOURCE_FILES bufferarray.cpp @@ -24,6 +25,8 @@ set(llcorehttp_SOURCE_FILES httprequest.cpp httpresponse.cpp httpstats.cpp + llwebsocketmgr.cpp + lljsonrpcws.cpp _httplibcurl.cpp _httpopcancel.cpp _httpoperation.cpp @@ -52,6 +55,8 @@ set(llcorehttp_HEADER_FILES httprequest.h httpresponse.h httpstats.h + llwebsocketmgr.h + lljsonrpcws.h _httpinternal.h _httplibcurl.h _httpopcancel.h @@ -82,10 +87,12 @@ list(APPEND llcorehttp_SOURCE_FILES ${llcorehttp_HEADER_FILES}) add_library (llcorehttp ${llcorehttp_SOURCE_FILES}) target_link_libraries( llcorehttp + llmath llcommon ll::libcurl ll::openssl ll::nghttp2 + ll::websocketpp ) target_include_directories( llcorehttp INTERFACE ${CMAKE_CURRENT_SOURCE_DIR}) # llmessage depends on llcorehttp, yet llcorehttp also depends on llmessage (at least for includes). @@ -95,6 +102,10 @@ target_include_directories( llcorehttp PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/../ll include(LibraryInstall) +if (USE_PRECOMPILED_HEADERS) + target_precompile_headers(llcorehttp REUSE_FROM llprecompiled) +endif () + # tests set(LLCOREHTTP_TESTS ON CACHE BOOL "Build and run llcorehttp integration tests specifically") @@ -111,6 +122,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}) @@ -134,7 +146,7 @@ if (LL_TESTS AND LLCOREHTTP_TESTS) ${PYTHON_EXECUTABLE} "${CMAKE_CURRENT_SOURCE_DIR}/tests/test_llcorehttp_peer.py" ) - + # # Example Programs # @@ -154,6 +166,7 @@ if (LL_TESTS AND LLCOREHTTP_TESTS) set_target_properties(http_texture_load PROPERTIES RUNTIME_OUTPUT_DIRECTORY "${EXE_STAGING_DIR}" + FOLDER "Tests" ) if (WINDOWS) @@ -166,6 +179,13 @@ if (LL_TESTS AND LLCOREHTTP_TESTS) ) endif (WINDOWS) + if(DARWIN) + set_target_properties(http_texture_load + PROPERTIES + OSX_ARCHITECTURES ${LL_MACOS_TEST_ARCHITECTURE} + ) + endif() + target_link_libraries(http_texture_load ${example_libs}) endif (LL_TESTS AND LLCOREHTTP_TESTS) diff --git a/indra/llcorehttp/_httpservice.cpp b/indra/llcorehttp/_httpservice.cpp index a8660decc3..e14a533479 100644 --- a/indra/llcorehttp/_httpservice.cpp +++ b/indra/llcorehttp/_httpservice.cpp @@ -70,7 +70,6 @@ volatile HttpService::EState HttpService::sState(NOT_INITIALIZED); HttpService::HttpService() : mRequestQueue(NULL), mExitRequested(0U), - mThread(NULL), mPolicy(NULL), mTransport(NULL), mLastPolicy(0) @@ -89,23 +88,30 @@ HttpService::~HttpService() { if (mRequestQueue->stopQueue()) { - // Give mRequestQueue a chance to finish - ms_sleep(10); + // Now wait a bit for the thread to exit + S32 counter = 0; + const S32 MAX_WAIT = 600; + while (counter < MAX_WAIT) + { + if (STOPPED == sState) + { + break; + } + // Sleep for a tenth of a second + ms_sleep(100); + std::this_thread::yield(); + counter++; + } } } - if (mThread) + if (mThread && RUNNING == sState) { - if (! mThread->timedJoin(250)) - { - // Failed to join, expect problems ahead so do a hard termination. - LL_WARNS(LOG_CORE) << "Destroying HttpService with running thread. Expect problems." << LL_NEWLINE - << "State: " << S32(sState) - << " Last policy: " << U32(mLastPolicy) - << LL_ENDL; + // Failed to shutdown, expect problems ahead so do a hard termination. + LL_WARNS(LOG_CORE) << "Destroying HttpService with running thread. Expect problems." << LL_NEWLINE << "State: " << S32(sState) + << " Last policy: " << U32(mLastPolicy) << LL_ENDL; - mThread->cancel(); - } + mThread->cancel(); } } @@ -121,11 +127,7 @@ HttpService::~HttpService() delete mPolicy; mPolicy = NULL; - if (mThread) - { - mThread->release(); - mThread = NULL; - } + mThread.reset(); } @@ -204,14 +206,15 @@ void HttpService::startThread() if (mThread) { - mThread->release(); + mThread.reset(); } // Push current policy definitions, enable policy & transport components mPolicy->start(); mTransport->start(mLastPolicy + 1); - mThread = new LLCoreInt::HttpThread(boost::bind(&HttpService::threadRun, this, _1)); + mThread = std::make_unique<LLCoreInt::HttpThread>(boost::bind(&HttpService::threadRun, this, _1)); + mThread->detach(); // Detach thread to let it clean its self up sState = RUNNING; } @@ -282,12 +285,9 @@ void HttpService::shutdown() // requested to stop. void HttpService::threadRun(LLCoreInt::HttpThread * thread) { + set_thread_name("HttpService"); LL_PROFILER_SET_THREAD_NAME("HttpService"); - boost::this_thread::disable_interruption di; - - LLThread::registerThreadID(); - ELoopSpeed loop(REQUEST_SLEEP); while (! mExitRequested) { @@ -314,7 +314,7 @@ void HttpService::threadRun(LLCoreInt::HttpThread * thread) { LOG_UNHANDLED_EXCEPTION(""); } - catch (std::bad_alloc&) + catch (const std::bad_alloc&) { LLMemory::logMemoryInfo(true); diff --git a/indra/llcorehttp/_httpservice.h b/indra/llcorehttp/_httpservice.h index 7202b496fc..4540fb9383 100644 --- a/indra/llcorehttp/_httpservice.h +++ b/indra/llcorehttp/_httpservice.h @@ -218,7 +218,7 @@ protected: static volatile EState sState; HttpRequestQueue * mRequestQueue; // Refcounted LLAtomicU32 mExitRequested; - LLCoreInt::HttpThread * mThread; + std::unique_ptr<LLCoreInt::HttpThread> mThread; // === working-thread-only data === HttpPolicy * mPolicy; // Simple pointer, has ownership diff --git a/indra/llcorehttp/_mutex.h b/indra/llcorehttp/_mutex.h index 5e0164eb51..43782d3a37 100644 --- a/indra/llcorehttp/_mutex.h +++ b/indra/llcorehttp/_mutex.h @@ -27,9 +27,8 @@ #ifndef LLCOREINT_MUTEX_H_ #define LLCOREINT_MUTEX_H_ - -#include <boost/thread.hpp> - +#include <condition_variable> +#include <mutex> namespace LLCoreInt { @@ -37,17 +36,17 @@ namespace LLCoreInt // MUTEX TYPES // unique mutex type -typedef boost::mutex HttpMutex; +typedef std::mutex HttpMutex; // CONDITION VARIABLES // standard condition variable -typedef boost::condition_variable HttpConditionVariable; +typedef std::condition_variable HttpConditionVariable; // LOCKS AND FENCES // scoped unique lock -typedef boost::unique_lock<HttpMutex> HttpScopedLock; +typedef std::unique_lock<HttpMutex> HttpScopedLock; } diff --git a/indra/llcorehttp/_refcounted.h b/indra/llcorehttp/_refcounted.h index 63684b5e2c..bc90f8274d 100644 --- a/indra/llcorehttp/_refcounted.h +++ b/indra/llcorehttp/_refcounted.h @@ -30,7 +30,6 @@ #include "linden_common.h" -#include "fix_macros.h" #include <boost/intrusive_ptr.hpp> #include "llatomic.h" @@ -40,12 +39,11 @@ namespace LLCoreInt class RefCounted { -private: - RefCounted() = delete; // may not be default constructed - RefCounted(const RefCounted&) = delete; - RefCounted& operator=(const RefCounted&) = delete; - public: + RefCounted() = delete; // Not defined - may not be default constructed + RefCounted(const RefCounted&) = delete; // Not defined - may not be copy constructed + void operator=(const RefCounted&) = delete; // Not defined + explicit RefCounted(bool const implicit) : mRefCount(implicit) {} diff --git a/indra/llcorehttp/_thread.h b/indra/llcorehttp/_thread.h index 93efbbedbc..66b9689c5a 100644 --- a/indra/llcorehttp/_thread.h +++ b/indra/llcorehttp/_thread.h @@ -29,94 +29,75 @@ #include "linden_common.h" -#include <boost/thread.hpp> -#include <boost/function.hpp> -#include <boost/date_time/posix_time/posix_time_types.hpp> +#include <functional> +#include <thread> -#include "apr.h" // thread-related functions #include "_refcounted.h" namespace LLCoreInt { -class HttpThread : public RefCounted +class HttpThread { -private: - // May not be default constructed or copied - HttpThread() = delete; - HttpThread(const HttpThread&) = delete; - void operator=(const HttpThread &) = delete; - - void at_exit() - { - // the thread function has exited so we need to release our reference - // to ourself so that we will be automagically cleaned up. - release(); - } +public: + HttpThread() = delete; // Not defined + HttpThread(const HttpThread&) = delete; // Not defined + void operator=(const HttpThread &) = delete; // Not defined +private: void run() - { // THREAD CONTEXT - - // Take out additional reference for the at_exit handler - addRef(); - boost::this_thread::at_thread_exit(boost::bind(&HttpThread::at_exit, this)); - - // run the thread function - mThreadFunc(this); - - } // THREAD CONTEXT - -protected: - virtual ~HttpThread() - { - delete mThread; - } + { // THREAD CONTEXT + // run the thread function + mThreadFunc(this); + } // THREAD CONTEXT public: /// Constructs a thread object for concurrent execution but does /// not start running. Caller receives on refcount on the thread /// instance. If the thread is started, another will be taken /// out for the exit handler. - explicit HttpThread(boost::function<void (HttpThread *)> threadFunc) - : RefCounted(true), // implicit reference - mThreadFunc(threadFunc) - { - // this creates a boost thread that will call HttpThread::run on this instance - // and pass it the threadfunc callable... - boost::function<void()> f = boost::bind(&HttpThread::run, this); + explicit HttpThread(std::function<void (HttpThread *)> threadFunc) + : mThreadFunc(threadFunc) + { + // this creates a std thread that will call HttpThread::run on this instance + // and pass it the threadfunc callable... + std::function<void()> f = std::bind(&HttpThread::run, this); - mThread = new boost::thread(f); - } + mThread = std::make_unique<std::thread>(f); + mNativeHandle = mThread->native_handle(); + } - inline void join() - { - mThread->join(); - } + ~HttpThread() = default; - inline bool timedJoin(S32 millis) - { - return mThread->timed_join(boost::posix_time::milliseconds(millis)); - } + inline void join() + { + mThread->join(); + } inline bool joinable() const - { - return mThread->joinable(); - } + { + return mThread->joinable(); + } + + inline void detach() + { + mThread->detach(); + } // A very hostile method to force a thread to quit inline void cancel() - { - boost::thread::native_handle_type thread(mThread->native_handle()); -#if LL_WINDOWS - TerminateThread(thread, 0); + { +#if LL_WINDOWS + TerminateThread(mNativeHandle, 0); #else - pthread_cancel(thread); + pthread_cancel(mNativeHandle); #endif - } + } private: - boost::function<void(HttpThread *)> mThreadFunc; - boost::thread * mThread; + std::function<void(HttpThread *)> mThreadFunc; + std::unique_ptr<std::thread> mThread; + std::thread::native_handle_type mNativeHandle; }; // end class HttpThread } // end namespace LLCoreInt diff --git a/indra/llcorehttp/httpcommon.cpp b/indra/llcorehttp/httpcommon.cpp index 2b09653d8e..1037855017 100644 --- a/indra/llcorehttp/httpcommon.cpp +++ b/indra/llcorehttp/httpcommon.cpp @@ -36,10 +36,6 @@ namespace LLCore { -HttpStatus::type_enum_t EXT_CURL_EASY; -HttpStatus::type_enum_t EXT_CURL_MULTI; -HttpStatus::type_enum_t LLCORE; - HttpStatus::operator U32() const { // Effectively, concatenate mType (high) with mStatus (low). diff --git a/indra/llcorehttp/httpcommon.h b/indra/llcorehttp/httpcommon.h index 1c003a0966..001fd08f93 100644 --- a/indra/llcorehttp/httpcommon.h +++ b/indra/llcorehttp/httpcommon.h @@ -340,9 +340,9 @@ struct HttpStatus return *this; } - static const type_enum_t EXT_CURL_EASY = 0; ///< mStatus is an error from a curl_easy_*() call - static const type_enum_t EXT_CURL_MULTI = 1; ///< mStatus is an error from a curl_multi_*() call - static const type_enum_t LLCORE = 2; ///< mStatus is an HE_* error code + static constexpr type_enum_t EXT_CURL_EASY = 0; ///< mStatus is an error from a curl_easy_*() call + static constexpr type_enum_t EXT_CURL_MULTI = 1; ///< mStatus is an error from a curl_multi_*() call + static constexpr type_enum_t LLCORE = 2; ///< mStatus is an HE_* error code ///< 100-999 directly represent HTTP status codes /// Test for successful status in the code regardless /// of error source (internal, libcurl). 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/llhttpconstants.cpp b/indra/llcorehttp/llhttpconstants.cpp index 7a671543d9..7a671543d9 100755..100644 --- a/indra/llcorehttp/llhttpconstants.cpp +++ b/indra/llcorehttp/llhttpconstants.cpp diff --git a/indra/llcorehttp/llhttpconstants.h b/indra/llcorehttp/llhttpconstants.h index 71c1dfa173..d05d0f1af1 100755..100644 --- a/indra/llcorehttp/llhttpconstants.h +++ b/indra/llcorehttp/llhttpconstants.h @@ -100,6 +100,8 @@ extern const std::string HTTP_VERB_POST; extern const std::string HTTP_VERB_DELETE; extern const std::string HTTP_VERB_MOVE; extern const std::string HTTP_VERB_OPTIONS; +extern const std::string HTTP_VERB_PATCH; +extern const std::string HTTP_VERB_COPY; enum EHTTPMethod { diff --git a/indra/llcorehttp/lljsonrpcws.cpp b/indra/llcorehttp/lljsonrpcws.cpp new file mode 100644 index 0000000000..d6c5b0eb97 --- /dev/null +++ b/indra/llcorehttp/lljsonrpcws.cpp @@ -0,0 +1,815 @@ +/** + * @file lljsonrpcws.cpp + * @brief JSON-RPC 2.0 WebSocket server and connection implementation + * + * $LicenseInfo:firstyear=2025&license=viewerlgpl$ + * Second Life Viewer Source Code + * Copyright (C) 2025, Linden Research, Inc. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; + * version 2.1 of the License only. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * + * Linden Research, Inc., 945 Battery Street, San Francisco, CA 94111 USA + * $/LicenseInfo$ + */ + +#include "linden_common.h" + +#include "lljsonrpcws.h" +#include "llerror.h" +#include "llsdjson.h" +#include "lldate.h" +#include "llcoros.h" +#include "llmainthreadtask.h" +#include "lleventtimer.h" +#include "lltimer.h" + +#include <boost/json.hpp> + +//======================================================================== +// LLJSONRPCConnection Implementation +//======================================================================== + +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 " + << pending.size() << " pending requests" << LL_ENDL; + + for (auto& [id, callback] : pending) + { + if (callback) + { + LLSD error; + error["code"] = RPCError::CONNECTION_CLOSED; + error["message"] = "Connection closed"; + callback(LLSD(), error); + } + } +} + +void LLJSONRPCConnection::onMessage(const std::string& message) +{ + LL_PROFILE_ZONE_SCOPED_CATEGORY_WEBSOCKET; + LL_DEBUGS("JSONRPC") << "Received JSON-RPC message: " << message << LL_ENDL; + + try + { + // Parse JSON message + boost::system::error_code ec; + boost::json::value json_value = boost::json::parse(message, ec); + + if (ec.failed()) + { + LL_WARNS("JSONRPC") << "Failed to parse JSON: " << ec.message() << LL_ENDL; + sendError(LLSD(), ParseError(ec.message())); + return; + } + + // Convert to LLSD + LLSD message_obj = LlsdFromJson(json_value); + + // Handle batch vs single message + if (message_obj.isArray()) + { + // 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) + { + LL_WARNS("JSONRPC") << "Exception processing JSON-RPC message: " << e.what() << LL_ENDL; + sendError(LLSD(), InternalError(e.what())); + } +} + +void LLJSONRPCConnection::processMessage(const LLSD& message_obj) +{ + LL_PROFILE_ZONE_SCOPED_CATEGORY_WEBSOCKET; + try + { + // Determine if this is a request, notification, or response + if (message_obj.has("method")) + { + // This is a request or notification + if (validateMessage(message_obj, true)) + { + processRequest(message_obj); + } + } + else if (message_obj.has("result") || message_obj.has("error")) + { + // This is a response + if (validateMessage(message_obj, false)) + { + processResponse(message_obj); + } + } + else + { + LL_WARNS("JSONRPC") << "Message must contain 'method' or 'result'/'error'" << LL_ENDL; + } + } + catch (const RPCError& e) + { + LLSD id = message_obj.has("id") ? message_obj["id"] : LLSD(); + sendError(id, e); + } +} + +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(); + bool is_notification = !request.has("id"); + + LL_DEBUGS("JSONRPC") << "Processing " << (is_notification ? "notification" : "request") + << " for method: " << method << LL_ENDL; + + // 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) + { + sendError(id, MethodNotFound(method)); + } + return; + } + + try + { + LLSD result = handler(method, id, params); + + if (!is_notification) + { + sendResponse(id, result); + } + } + catch (const RPCError& e) + { + if (!is_notification) + { + sendError(id, e); + } + else + { + LL_WARNS("JSONRPC") << "Error in notification handler for " << method + << ": " << e.what() << LL_ENDL; + } + } + catch (const std::exception& e) + { + if (!is_notification) + { + sendError(id, InternalError(e.what())); + } + else + { + LL_WARNS("JSONRPC") << "Exception in notification handler for " << method + << ": " << e.what() << LL_ENDL; + } + } +} + +void LLJSONRPCConnection::processResponse(const LLSD& response) +{ + LL_PROFILE_ZONE_SCOPED_CATEGORY_WEBSOCKET; + if (!response.has("id")) + { + LL_WARNS("JSONRPC") << "Response missing id field" << LL_ENDL; + return; + } + + std::string id = response["id"].asString(); + ResponseCallback callback; + { + 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); + } + + if (callback) + { + LLSD result = response.has("result") ? response["result"] : LLSD(); + LLSD error = response.has("error") ? response["error"] : LLSD(); + + callback(result, error); + } +} + +bool LLJSONRPCConnection::validateMessage(const LLSD& message, bool is_request) +{ + // Check JSON-RPC version + if (!message.has("jsonrpc") || message["jsonrpc"].asString() != "2.0") + { + LL_WARNS("JSONRPC") << "Missing or invalid jsonrpc version" << LL_ENDL; + return false; + } + + if (is_request) + { + // Request/notification validation + if (!message.has("method")) + { + LL_WARNS("JSONRPC") << "Missing method field" << LL_ENDL; + return false; + } + + if (!message["method"].isString()) + { + LL_WARNS("JSONRPC") << "Method must be a string" << LL_ENDL; + return false; + } + + // Params are optional but must be array or object if present + if (message.has("params")) + { + if (!message["params"].isArray() && !message["params"].isMap()) + { + LL_WARNS("JSONRPC") << "Params must be array or object" << LL_ENDL; + return false; + } + } + } + else + { + // Response validation + if (!message.has("id")) + { + LL_WARNS("JSONRPC") << "Response missing id field" << LL_ENDL; + return false; + } + + // Must have either result or error, but not both + bool has_result = message.has("result"); + bool has_error = message.has("error"); + + if (!has_result && !has_error) + { + LL_WARNS("JSONRPC") << "Response must have result or error" << LL_ENDL; + return false; + } + + if (has_result && has_error) + { + LL_WARNS("JSONRPC") << "Response cannot have both result and error" << LL_ENDL; + return false; + } + + // Error must be an object with code and message + if (has_error) + { + LLSD error = message["error"]; + 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); + } + } +} + +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. + // 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); + return LLSD(llformat("rpc_%llu", id)); +} + +void LLJSONRPCConnection::registerMethod(const std::string& method, MethodHandler 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) +{ + { + LLMutexLock lock(&mMutex); + mMethodHandlers.erase(method); + mAsyncMethodHandlers.erase(method); + } + LL_DEBUGS("JSONRPC") << "Unregistered method: " << method << LL_ENDL; +} + +LLSD LLJSONRPCConnection::makeEnvelope(const LLSD& id, + const std::string& method, + const LLSD& params, + const LLSD& result, + const LLSD& error) +{ + 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()) + { + 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(); + LLSD request = makeEnvelope(id, method, params, LLSD(), LLSD()); + const std::string id_str = id.asString(); + + // 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) + { + 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 + { + LLMutexLock lock(&mMutex); + mPendingRequests.erase(id_str); + } + LL_WARNS("JSONRPC") << "Failed to send request" << LL_ENDL; + return LLSD(); + } + + LL_DEBUGS("JSONRPC") << "Sent request: " << method << " with id: " << id.asString() << LL_ENDL; + return id; +} + +bool LLJSONRPCConnection::notify(const std::string& method, const LLSD& params) +{ + LL_PROFILE_ZONE_SCOPED_CATEGORY_WEBSOCKET; + LLSD notification = makeEnvelope(LLSD(), method, params, LLSD(), LLSD()); + + if (!sendMessage(LlsdToJson(notification))) + { + LL_WARNS("JSONRPC") << "Failed to send notification" << LL_ENDL; + return false; + } + + LL_DEBUGS("JSONRPC") << "Sent notification: " << method << LL_ENDL; + return true; +} + +bool LLJSONRPCConnection::sendResponse(const LLSD& id, const LLSD& result) +{ + LL_PROFILE_ZONE_SCOPED_CATEGORY_WEBSOCKET; + LLSD response = makeEnvelope(id, std::string(), LLSD(), result, LLSD()); + + if (!sendMessage(LlsdToJson(response))) + { + LL_WARNS("JSONRPC") << "Failed to send response for id: " << id.asString() << LL_ENDL; + return false; + } + LL_DEBUGS("JSONRPC") << "Sent response for id: " << id.asString() << LL_ENDL; + return true; +} + +bool LLJSONRPCConnection::sendError(const LLSD& id, const RPCError& error) +{ + LL_PROFILE_ZONE_SCOPED_CATEGORY_WEBSOCKET; + LLSD error_obj; + error_obj["code"] = error.getCode(); + error_obj["message"] = error.what(); + + if (!error.getData().isUndefined()) + { + error_obj["data"] = error.getData(); + } + + // 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))) + { + LL_WARNS("JSONRPC") << "Failed to send error response" << LL_ENDL; + return false; + } + LL_DEBUGS("JSONRPC") << "Sent error response: " << error.what() << LL_ENDL; + return true; +} + +bool LLJSONRPCConnection::sendBatch(const LLSD& batch, ResponseCallback callback) +{ + if (!batch.isArray() || batch.size() == 0) + { + LL_WARNS("JSONRPC") << "Batch must be non-empty array" << LL_ENDL; + return false; + } + + // For batch requests with callbacks, we need to track multiple responses + // This is complex as we need to correlate all responses before calling callback + // For now, we'll send the batch but won't support batch response callbacks + if (callback) + { + LL_WARNS("JSONRPC") << "Batch response callbacks not yet implemented" << LL_ENDL; + } + + if (!sendMessage(LlsdToJson(batch))) + { + LL_WARNS("JSONRPC") << "Failed to send batch" << LL_ENDL; + return false; + } + + LL_DEBUGS("JSONRPC") << "Sent batch with " << batch.size() << " messages" << LL_ENDL; + return true; +} + +//======================================================================== +// LLJSONRPCServer Implementation +//======================================================================== + +LLJSONRPCServer::LLJSONRPCServer(const std::string& name, U16 port, bool local_only) + : LLWebsocketMgr::WSServer(name, port, local_only), mServerName(name) +{ + LL_INFOS("JSONRPC") << "Created JSON-RPC server: " << name + << " on port " << port << LL_ENDL; + + // Register standard JSON-RPC methods + registerGlobalMethod("system.listMethods", [this](const std::string& method, const LLSD& id, const LLSD& params) -> LLSD { + LL_DEBUGS("JSONRPC") << "System method " << method << " called" << LL_ENDL; + return getMethodList(); + }); + + registerGlobalMethod("system.getStats", [this](const std::string& method, const LLSD& id, const LLSD& params) -> LLSD { + LL_DEBUGS("JSONRPC") << "System method " << method << " called" << LL_ENDL; + return getServerStats(); + }); + + registerGlobalMethod("system.ping", [](const std::string& method, const LLSD& id, const LLSD& params) -> LLSD { + LL_DEBUGS("JSONRPC") << "System method " << method << " called" << LL_ENDL; + LLSD result; + result["pong"] = LLDate::now().asString(); + result["params"] = params; + return result; + }); +} + +LLWebsocketMgr::WSConnection::ptr_t LLJSONRPCServer::connectionFactory(LLWebsocketMgr::WSServer::ptr_t server, + LLWebsocketMgr::connection_h handle) +{ + auto connection = std::make_shared<LLJSONRPCConnection>(server, handle); + setupConnectionMethods(connection); + return connection; +} + +void LLJSONRPCServer::onConnectionOpened(const LLWebsocketMgr::WSConnection::ptr_t& connection) +{ + LL_INFOS("JSONRPC") << "JSON-RPC client connected, total connections: " + << getConnectionCount() << LL_ENDL; +} + +void LLJSONRPCServer::onConnectionClosed(const LLWebsocketMgr::WSConnection::ptr_t& connection) +{ + LL_INFOS("JSONRPC") << "JSON-RPC client disconnected, total connections: " + << getConnectionCount() << LL_ENDL; +} + +void LLJSONRPCServer::setupConnectionMethods(LLJSONRPCConnection::ptr_t connection) +{ + LLMutexLock lock(&mGlobalMethodsMutex); + + // Register all global methods on the new connection + for (const auto& [method, handler] : mGlobalMethods) + { + connection->registerMethod(method, handler); + } + + // 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) +{ + { + LLMutexLock lock(&mGlobalMethodsMutex); + mGlobalMethods[method] = handler; + } + + // Apply to all existing connections - we need to iterate through connections + // Since mConnections is private, we need to use broadcastMessage or find another approach + // For now, we'll only apply to new connections + + LL_INFOS("JSONRPC") << "Registered global method: " << method << LL_ENDL; +} + +void LLJSONRPCServer::unregisterGlobalMethod(const std::string& method) +{ + { + LLMutexLock lock(&mGlobalMethodsMutex); + mGlobalMethods.erase(method); + } + + // For existing connections, we would need access to them + // This is a limitation of the current design - methods added after connection + // establishment won't be retroactively applied + + LL_INFOS("JSONRPC") << "Unregistered global method: " << method << LL_ENDL; +} + +LLSD LLJSONRPCServer::getMethodList() const +{ + LLMutexLock lock(&mGlobalMethodsMutex); + + LLSD methods = LLSD::emptyArray(); + for (const auto& [method, handler] : mGlobalMethods) + { + methods.append(method); + } + + return methods; +} + +void LLJSONRPCServer::broadcastNotification(const std::string& method, const LLSD& params) +{ + 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 = LLJSONRPCConnection::makeEnvelope(LLSD(), method, params, LLSD(), LLSD()); + + // Use the base class broadcast functionality + broadcastMessage(boost::json::serialize(LlsdToJson(notification))); + + // Cache the count: getConnectionCount() walks a locked container in the base. + size_t count = getConnectionCount(); + mTotalNotificationsSent += count; + LL_DEBUGS("JSONRPC") << "Broadcast notification: " << method + << " to " << count << " clients" << LL_ENDL; +} + +LLSD LLJSONRPCServer::getServerStats() const +{ + LLSD stats; + stats["server_name"] = mServerName; + stats["connection_count"] = static_cast<S32>(getConnectionCount()); + stats["is_running"] = isRunning(); + + { + LLMutexLock lock(&mGlobalMethodsMutex); + stats["global_method_count"] = static_cast<S32>(mGlobalMethods.size()); + } + + stats["total_requests_handled"] = static_cast<LLSD::Integer>(mTotalRequestsHandled.load()); + stats["total_notifications_sent"] = static_cast<LLSD::Integer>(mTotalNotificationsSent.load()); + stats["uptime"] = LLDate::now().asString(); + + return stats; +} diff --git a/indra/llcorehttp/lljsonrpcws.h b/indra/llcorehttp/lljsonrpcws.h new file mode 100644 index 0000000000..7cb26b1fb2 --- /dev/null +++ b/indra/llcorehttp/lljsonrpcws.h @@ -0,0 +1,511 @@ +/** + * @file lljsonrpcws.h + * @brief JSON-RPC 2.0 WebSocket server and connection implementation + * + * $LicenseInfo:firstyear=2025&license=viewerlgpl$ + * Second Life Viewer Source Code + * Copyright (C) 2025, Linden Research, Inc. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; + * version 2.1 of the License only. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * + * Linden Research, Inc., 945 Battery Street, San Francisco, CA 94111 USA + * $/LicenseInfo$ + */ + +#pragma once + +#include "llwebsocketmgr.h" +#include "llsd.h" +#include "lluuid.h" + +#include <functional> +#include <unordered_map> +#include <memory> +#include <queue> + +class LLEventTimer; + +/** + * @class LLJSONRPCConnection + * @brief JSON-RPC 2.0 WebSocket connection implementation + * + * This class implements the JSON-RPC 2.0 protocol over WebSocket connections. + * It handles request/response patterns, notifications, method registration, + * and error handling according to the JSON-RPC 2.0 specification. + * + * ## JSON-RPC 2.0 Protocol Features + * + * - **Requests**: Method calls that expect a response + * - **Notifications**: Method calls that do not expect a response + * - **Batch Operations**: Multiple requests/notifications in a single message + * - **Error Handling**: Standardized error codes and messages + * - **ID Correlation**: Request/response correlation using unique identifiers + * + * ## Method Handler Registration + * + * Methods use an enhanced handler signature that provides method name and request ID context: + * + * @code + * connection->registerMethod("echo", [](const std::string& method, const LLSD& id, const LLSD& params) -> LLSD { + * LL_INFOS("JSONRPC") << "Method " << method << " called with ID " << id.asString() << LL_ENDL; + * return params; // Echo back the parameters + * }); + * + * connection->registerMethod("add", [](const std::string& method, const LLSD& id, const LLSD& params) -> LLSD { + * if (params.isArray() && params.size() >= 2) { + * LL_INFOS("JSONRPC") << "Adding numbers via " << method << LL_ENDL; + * return params[0].asReal() + params[1].asReal(); + * } + * throw LLJSONRPCConnection::InvalidParams("Expected array with 2 numbers"); + * }); + * @endcode + * + * The enhanced signature enables: + * - Method context awareness for shared handlers + * - Request correlation and distributed tracing + * - Distinction between notifications (id undefined) and requests + * - Enhanced logging and error reporting with context + * + * ## Making RPC Calls + * + * @code + * // Asynchronous request with callback + * LLSD params; + * params.append(5); + * params.append(3); + * connection->call("add", params, [](const LLSD& result, const LLSD& error) { + * if (error.isUndefined()) { + * LL_INFOS() << "Result: " << result.asReal() << LL_ENDL; + * } else { + * LL_WARNS() << "Error: " << error["message"].asString() << LL_ENDL; + * } + * }); + * + * // Fire-and-forget notification + * connection->notify("log", LLSD("Server started")); + * @endcode + */ +class LLJSONRPCConnection : public LLWebsocketMgr::WSConnection +{ +public: + using ptr_t = std::shared_ptr<LLJSONRPCConnection>; + + /// Method handler function signature + /// @param method The method name that was called + /// @param id The request ID (undefined for notifications) + /// @param params The parameters passed to the method + /// @return The result to return to the caller + /// @throw RPCError-derived exceptions for error responses + using MethodHandler = std::function<LLSD(const std::string& method, const LLSD& id, const LLSD& params)>; + + /// Response callback function signature + /// @param result The result from a successful call (undefined if error occurred) + /// @param error The error object if call failed (undefined if successful) + using ResponseCallback = std::function<void(const LLSD& result, const LLSD& error)>; + + /** + * @brief JSON-RPC error base class + */ + class RPCError : public std::runtime_error + { + public: + // JSON-RPC 2.0 Standard Error Codes + static constexpr S32 PARSE_ERROR = -32700; ///< Invalid JSON was received by the server + static constexpr S32 INVALID_REQUEST = -32600; ///< The JSON sent is not a valid Request object + static constexpr S32 METHOD_NOT_FOUND = -32601; ///< The method does not exist / is not available + static constexpr S32 INVALID_PARAMS = -32602; ///< Invalid method parameter(s) + static constexpr S32 INTERNAL_ERROR = -32603; ///< Internal JSON-RPC error + + // Server Error Range (-32000 to -32099) + static constexpr S32 SERVER_ERROR_MIN = -32099; ///< Server error range minimum + static constexpr S32 SERVER_ERROR_MAX = -32000; ///< Server error range maximum + + // Common server-specific errors + static constexpr S32 CONNECTION_CLOSED = -32000; ///< Connection closed unexpectedly + static constexpr S32 REQUEST_TIMEOUT = -32001; ///< Request timed out + static constexpr S32 UNAUTHORIZED = -32002; ///< Authentication required + static constexpr S32 FORBIDDEN = -32003; ///< Access denied + static constexpr S32 RATE_LIMITED = -32004; ///< Too many requests + static constexpr S32 SERVICE_UNAVAILABLE = -32005; ///< Service temporarily unavailable + static constexpr S32 MESSAGE_TOO_LARGE = -32006; ///< Message exceeds maximum size + static constexpr S32 INVALID_SESSION = -32007; ///< Session expired or invalid + + RPCError(S32 code, const std::string& message, const LLSD& data = LLSD()) + : std::runtime_error(message), mCode(code), mData(data) {} + + S32 getCode() const { return mCode; } + const LLSD& getData() const { return mData; } + + protected: + S32 mCode; + LLSD mData; + }; + + /// Standard JSON-RPC error classes using named constants + class ParseError : public RPCError { + public: + ParseError(const std::string& details = "") + : RPCError(PARSE_ERROR, "Parse error" + (details.empty() ? "" : ": " + details)) {} + }; + + class InvalidRequest : public RPCError { + public: + InvalidRequest(const std::string& details = "") + : RPCError(INVALID_REQUEST, "Invalid Request" + (details.empty() ? "" : ": " + details)) {} + }; + + class MethodNotFound : public RPCError { + public: + MethodNotFound(const std::string& method = "") + : RPCError(METHOD_NOT_FOUND, "Method not found" + (method.empty() ? "" : ": " + method)) {} + }; + + class InvalidParams : public RPCError { + public: + InvalidParams(const std::string& details = "") + : RPCError(INVALID_PARAMS, "Invalid params" + (details.empty() ? "" : ": " + details)) {} + }; + + class InternalError : public RPCError { + public: + InternalError(const std::string& details = "") + : RPCError(INTERNAL_ERROR, "Internal error" + (details.empty() ? "" : ": " + details)) {} + }; + + /// Server-specific errors (in the -32000 to -32099 range) + class ConnectionClosedError : public RPCError { + public: + ConnectionClosedError(const std::string& details = "Connection closed") + : RPCError(CONNECTION_CLOSED, details) {} + }; + + class RequestTimeoutError : public RPCError { + public: + RequestTimeoutError(const std::string& details = "Request timed out") + : RPCError(REQUEST_TIMEOUT, details) {} + }; + + class UnauthorizedError : public RPCError { + public: + UnauthorizedError(const std::string& details = "Authentication required") + : RPCError(UNAUTHORIZED, details) {} + }; + + class ForbiddenError : public RPCError { + public: + ForbiddenError(const std::string& details = "Access denied") + : RPCError(FORBIDDEN, details) {} + }; + + class RateLimitedError : public RPCError { + public: + RateLimitedError(const std::string& details = "Too many requests") + : RPCError(RATE_LIMITED, details) {} + }; + + class ServiceUnavailableError : public RPCError { + public: + ServiceUnavailableError(const std::string& details = "Service temporarily unavailable") + : RPCError(SERVICE_UNAVAILABLE, details) {} + }; + + class InvalidSessionError : public RPCError { + public: + InvalidSessionError(const std::string& details = "Session expired or invalid") + : RPCError(INVALID_SESSION, details) {} + }; + + + LLJSONRPCConnection(const LLWebsocketMgr::WSServer::ptr_t server, + const LLWebsocketMgr::connection_h& handle) + : LLWebsocketMgr::WSConnection(server, handle) {} + + ~LLJSONRPCConnection() override = default; + + // WebSocket connection lifecycle + void onOpen() override; + void onClose() override; + void onMessage(const std::string& message) override; + + /** + * @brief Register a method handler + * @param method The method name to register + * @param handler The function to call when this method is invoked + * + * @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 + */ + void unregisterMethod(const std::string& method); + + /** + * @brief Make an asynchronous JSON-RPC call + * @param method The method name to call + * @param params The parameters to pass + * @param callback Callback for the response (optional) + * @return The request ID for correlation + */ + LLSD call(const std::string& method, const LLSD& params = LLSD(), + ResponseCallback callback = nullptr); + + /** + * @brief Send a JSON-RPC notification (no response expected) + * @param method The method name + * @param params The parameters to pass + */ + bool notify(const std::string& method, const LLSD& params = LLSD()); + + /** + * @brief Send a successful response to a request + * @param id The request ID from the original request + * @param result The result to return + */ + bool sendResponse(const LLSD& id, const LLSD& result); + + /** + * @brief Send an error response to a request + * @param id The request ID from the original request (can be null) + * @param error The RPCError to send + */ + bool sendError(const LLSD& id, const RPCError& error); + + /** + * @brief Send a batch of requests/notifications + * @param batch Array of request/notification objects + * @param callback Callback for batch response (optional) + */ + bool sendBatch(const LLSD& batch, ResponseCallback callback = nullptr); + +protected: + /** + * @brief Process a single JSON-RPC message + * @param message_obj The parsed JSON message + */ + void processMessage(const LLSD& message_obj); + + /** + * @brief Process a JSON-RPC request + * @param request The request object + */ + void processRequest(const LLSD& request); + + /** + * @brief Process a JSON-RPC response + * @param response The response object + */ + void processResponse(const LLSD& response); + + /** + * @brief Validate a JSON-RPC message structure + * @param message The message to validate + * @param is_request True if validating a request, false for response + */ + bool validateMessage(const LLSD& message, bool is_request = true); + + /** + * @brief Generate the next unique request ID + * @return A server-unique request ID + * + * Generates a server-wide unique identifier using an atomic counter. + * This ensures request IDs are unique across all connections within the + * server instance, providing efficient ID generation with guaranteed uniqueness. + * + * IDs follow the format "rpc_{counter}" where counter is a monotonically + * increasing 64-bit value starting from 1. This approach provides: + * - Guaranteed uniqueness within server scope + * - High performance (atomic increment operation) + * - Predictable, sequential ordering for debugging + * - Thread-safe generation across multiple connections + */ + LLSD generateId(); + +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(); + +public: + void testInjectPendingRequest(const std::string& id, F64 deadline, ResponseCallback callback); + void testSweepTimeouts(); + size_t testPendingRequestCount() const; +}; + +/** + * @class LLJSONRPCServer + * @brief JSON-RPC 2.0 WebSocket server implementation + * + * This server extends the basic WebSocket server to provide JSON-RPC 2.0 + * protocol support. It manages JSON-RPC connections and provides server-wide + * method registration and broadcasting capabilities. + * + * ## Server-Wide Method Registration + * + * Methods can be registered at the server level and will be available + * on all connections: + * + * @code + * auto server = std::make_shared<LLJSONRPCServer>("rpc_server", 8080); + * + * server->registerGlobalMethod("getServerInfo", [](const std::string& method, const LLSD& id, const LLSD& params) -> LLSD { + * LL_INFOS("JSONRPC") << "Server info requested via " << method << LL_ENDL; + * LLSD info; + * info["name"] = "My RPC Server"; + * info["version"] = "1.0.0"; + * info["uptime"] = LLDate::now().secondsSinceEpoch(); + * return info; + * }); + * + * server->registerGlobalMethod("listMethods", [server](const std::string& method, const LLSD& id, const LLSD& params) -> LLSD { + * return server->getMethodList(); + * }); + * @endcode + * + * ## Broadcasting and Multi-client Operations + * + * @code + * // Broadcast notification to all connected clients + * server->broadcastNotification("serverAlert", LLSD("Server will restart in 5 minutes")); + * @endcode + */ +class LLJSONRPCServer : public LLWebsocketMgr::WSServer +{ +public: + using ptr_t = std::shared_ptr<LLJSONRPCServer>; + using MethodHandler = LLJSONRPCConnection::MethodHandler; + using ResponseCallback = LLJSONRPCConnection::ResponseCallback; + + LLJSONRPCServer(const std::string& name, U16 port, bool local_only = true); + ~LLJSONRPCServer() override = default; + + // Server lifecycle callbacks + void onConnectionOpened(const LLWebsocketMgr::WSConnection::ptr_t& connection) override; + void onConnectionClosed(const LLWebsocketMgr::WSConnection::ptr_t& connection) override; + + /** + * @brief Register a global method available on all connections + * @param method The method name to register + * @param handler The function to call when this method is invoked + */ + void registerGlobalMethod(const std::string& method, MethodHandler handler); + + /** + * @brief Unregister a global method + * @param method The method name to unregister + */ + void unregisterGlobalMethod(const std::string& method); + + /** + * @brief Get list of registered global methods + * @return Array of method names + */ + LLSD getMethodList() const; + + /** + * @brief Broadcast a notification to all connected clients + * @param method The method name + * @param params The parameters to pass + */ + void broadcastNotification(const std::string& method, const LLSD& params = LLSD()); + + /** + * @brief Get server statistics + * @return Statistics object with connection count, method count, etc. + */ + LLSD getServerStats() const; + +protected: + LLWebsocketMgr::WSConnection::ptr_t connectionFactory(LLWebsocketMgr::WSServer::ptr_t server, + LLWebsocketMgr::connection_h handle) override; + + /** + * @brief Apply global method handlers to a new connection + * @param connection The connection to configure + */ + virtual void setupConnectionMethods(LLJSONRPCConnection::ptr_t connection); + +private: + std::unordered_map<std::string, MethodHandler> mGlobalMethods; + mutable LLMutex mGlobalMethodsMutex; + + std::string mServerName; // Store server name for stats + std::atomic<U64> mTotalRequestsHandled{0}; + std::atomic<U64> mTotalNotificationsSent{0}; +}; diff --git a/indra/llcorehttp/llwebsocketmgr.cpp b/indra/llcorehttp/llwebsocketmgr.cpp new file mode 100644 index 0000000000..be1ab54efd --- /dev/null +++ b/indra/llcorehttp/llwebsocketmgr.cpp @@ -0,0 +1,710 @@ +/** + * @file llwebsocketmgr.cpp + * @brief WebSocket manager singleton implementation + * + * $LicenseInfo:firstyear=2025&license=viewerlgpl$ + * Second Life Viewer Source Code + * Copyright (C) 2025, Linden Research, Inc. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; + * version 2.1 of the License only. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * + * Linden Research, Inc., 945 Battery Street, San Francisco, CA 94111 USA + * $/LicenseInfo$ + */ + +#include "linden_common.h" + +#include "llwebsocketmgr.h" +#include "llerror.h" +#include "llsdserialize.h" +#include "llhost.h" +#include "llsdjson.h" + +#include <websocketpp/config/boost_config.hpp> +#include <websocketpp/config/asio_no_tls.hpp> +#include <websocketpp/client.hpp> +#include <websocketpp/server.hpp> + +#include <thread> +#include <atomic> +#include <chrono> + +//------------------------------------------------------------------------ +namespace +{ + using Server_t = websocketpp::server<websocketpp::config::asio>; + using Client_t = websocketpp::client<websocketpp::config::asio>; + using Connection_t = websocketpp::connection<websocketpp::config::asio>; +} + +//------------------------------------------------------------------------ + +//------------------------------------------------------------------------ +// LLWebsocketMgr Implementation +// + +void LLWebsocketMgr::initSingleton() +{ } + +void LLWebsocketMgr::cleanupSingleton() +{ + stopAllServers(); +} + +void LLWebsocketMgr::update() +{ + LL_PROFILE_ZONE_SCOPED_CATEGORY_WEBSOCKET; + std::vector<WSServer::ptr_t> stops; + + for (auto &[name, server] : mServers) + { + if (server && server->isRunning()) + { + if (!server->update()) + { + stops.push_back(server); + } + } + } + + for (const auto& server : stops) + { + if (server) + { + LL_DEBUGS("WebSocket") << "Stopping server: " << server->mServerName << LL_ENDL; + removeServer(server->mServerName); + } + } +} + + +LLWebsocketMgr::WSServer::ptr_t LLWebsocketMgr::findServerByName(const std::string &name) const +{ + auto it = mServers.find(std::string(name)); + if (it != mServers.end()) + { + return it->second; + } + return nullptr; +} + +bool LLWebsocketMgr::addServer(const LLWebsocketMgr::WSServer::ptr_t& server) +{ + if (!server) + { + LL_WARNS("WebSocket") << "Attempted to add a null server" << LL_ENDL; + return false; + } + + auto it = mServers.find(server->mServerName); + if (it != mServers.end()) + { + LL_WARNS("WebSocket") << "Server with name " << server->mServerName << " already exists" << LL_ENDL; + return false; + } + mServers[server->mServerName] = server; + LL_INFOS("WebSocket") << "Added WebSocket server: " << server->mServerName << LL_ENDL; + return true; +} + +bool LLWebsocketMgr::removeServer(const std::string& name) +{ + auto it = mServers.find(name); + if (it == mServers.end()) + { + LL_WARNS("WebSocket") << "No server found with name " << name << " to remove" << LL_ENDL; + return false; + } + if (it->second && it->second->isRunning()) + it->second->stop(); + mServers.erase(it); + LL_INFOS("WebSocket") << "Removed WebSocket server: " << name << LL_ENDL; + return true; +} + + +bool LLWebsocketMgr::startServer(const std::string &name) const +{ + LLWebsocketMgr::WSServer::ptr_t server = findServerByName(name); + if (!server) + { + LL_WARNS("WebSocket") << "No server found with name " << name << " to start" << LL_ENDL; + return false; + } + if (server->isRunning()) + { + LL_WARNS("WebSocket") << "Server " << name << " is already running" << LL_ENDL; + return false; + } + return server->start(); +} + +void LLWebsocketMgr::stopServer(const std::string& name) const +{ + LLWebsocketMgr::WSServer::ptr_t server = findServerByName(name); + if (!server) + { + LL_WARNS("WebSocket") << "No server found with name " << name << " to stop" << LL_ENDL; + return; + } + if (!server->isRunning()) + { + LL_WARNS("WebSocket") << "Server " << name << " is not running" << LL_ENDL; + return; + } + server->stop(); +} + +void LLWebsocketMgr::stopAllServers() +{ + for (auto &[name, server] : mServers) + { + if (server && server->isRunning()) + { + LL_INFOS("WebSocket") << "Stopping server: " << name << LL_ENDL; + server->stop(); + } + } + + mServers.clear(); +} + +//------------------------------------------------------------------------ +struct Server_impl +{ + Server_impl(LLWebsocketMgr::WSServer *owner, U16 port, bool local_only) : + mOwner(owner), + mPort(port), + mLocalOnly(local_only) + { + + mServer.set_open_handler([this](websocketpp::connection_hdl hdl) { this->onOpen(hdl); }); + mServer.set_close_handler([this](websocketpp::connection_hdl hdl) { this->onClose(hdl); }); + mServer.set_message_handler([this](websocketpp::connection_hdl hdl, Server_t::message_ptr msg) { this->onMessage(hdl, msg); }); + } + + ~Server_impl() = default; + + /** + * @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 + */ + void init() + { + mServer.init_asio(); + try { + if (mLocalOnly) + { + std::stringstream port_str; + port_str << mPort; + mServer.listen("127.0.0.1", port_str.str()); + } + else + { + mServer.listen(mPort); + } + } + catch (const websocketpp::exception& e) + { + LL_WARNS("WebSocket") << "WebSocket server listen exception: " << e.what() << LL_ENDL; + } + catch (const std::exception& e) + { + LL_WARNS("WebSocket") << "WebSocket server listen std::exception: " << e.what() << LL_ENDL; + } + catch (...) + { + LL_WARNS("WebSocket") << "WebSocket server listen unknown exception" << LL_ENDL; + } + } + + /** + * @brief Start the websocket server and begin accepting connections + * @return true if server started successfully, false on error + */ + bool start() + { + //if (!mServer.stopped()) + //{ + // LL_WARNS("WebSocket") << "WebSocket server is already running" << LL_ENDL; + // return false; + //} + + try + { + LL_INFOS("WebSocket") << "Starting WebSocket server on port " << mPort + << (mLocalOnly ? " (localhost only)" : " (all interfaces)") << LL_ENDL; + mServer.start_accept(); + + // Run controlled event loop with periodic stop flag checking + 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); + + // 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 std::exception: " << e.what() << LL_ENDL; + return false; + } + catch (...) + { + LL_WARNS("WebSocket") << "WebSocket server unknown exception" << LL_ENDL; + return false; + } + } + + void stop() + { + if (mServer.stopped()) + { + return; + } + try + { + mServer.stop_listening(); + mServer.stop(); + } + catch (const std::exception&) + { + LL_WARNS("WebSocket") << "Error stopping WebSocket server" << LL_ENDL; + } + } + + /** + * @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. + */ + 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); + } + + /** + * @brief Handle connection closure event + * @param hdl WebSocket connection handle from websocketpp + */ + 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); + } + + /** + * @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. + */ + 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) + { + LL_WARNS("WebSocket") << "Received message for unknown connection" << LL_ENDL; + return; + } + + // TODO: check the FIN bit and handle fragmented messages if needed + // TODO: check terminal and close codes and handle connection closure if needed + mOwner->handleMessage(hdl, msg->get_payload()); + } + + //------------------------------------------- + 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) +}; + +//------------------------------------------------------------------------ +LLWebsocketMgr::WSServer::WSServer(std::string_view name, U16 port, bool local_only): + mServerName(name), + mImpl(std::make_unique<Server_impl>(this, port, local_only)) +{ + mImpl->init(); + + // Initialize the server with the given name and host + LL_INFOS("WebSocket") << "Creating WebSocket server: " << name << + " listening " << (mImpl->mLocalOnly ? "locally" : "ON ALL INTERFACES") << + " on port " << mImpl->mPort << LL_ENDL; +} + +LLWebsocketMgr::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) +{ + return std::make_shared<LLWebsocketMgr::WSConnection>(server, handle); +} + +bool LLWebsocketMgr::WSServer::start() +{ + LL_ERRS_IF(!mImpl, "WebSocket") << "WebSocket server " << mServerName << " implementation is null !" << LL_ENDL; + + 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; + }); + + onStarted(); + 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; + + { + LLMutexLock lock(&mThreadMutex); + + // Check if already stopped + if (!isRunning()) + { + return; + } + + LL_INFOS("WebSocket") << "Stopping WebSocket server: " << mServerName << LL_ENDL; + + 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 + + if (mServerThread.joinable()) + { + mServerThread.join(); + LL_INFOS("WebSocket") << "WebSocket server thread joined for: " << mServerName << LL_ENDL; + } + onStopped(); +} + +bool LLWebsocketMgr::WSServer::isRunning() const +{ + LL_ERRS_IF(!mImpl, "WebSocket") << "WebSocket server " << mServerName << " implementation is null !" << LL_ENDL; + + // 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) +{ + 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) + { + sendMessageTo(handle, 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); + if (ec) + { + LL_WARNS("WebSocket") << mServerName << " failed to send message: " << ec.message() << LL_ENDL; + return false; + } + 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); + auto it = mConnections.find(handle); + if (it != mConnections.end()) + { + return it->second; + } + return nullptr; +} + +LLWebsocketMgr::connection_state_t LLWebsocketMgr::WSServer::getConnectionState(const connection_h& handle) const +{ + websocketpp::lib::error_code ec; + auto con = mImpl->mServer.get_con_from_hdl(handle, ec); + if (ec) + { + LL_WARNS("WebSocket") << mServerName << " failed to get connection state: " << ec.message() << LL_ENDL; + websocketpp::session::state::value state = websocketpp::session::state::closed; + return connection_closed; + } + return static_cast<connection_state_t>(con->get_state()); +} + + +void LLWebsocketMgr::WSServer::handleOpenConnection(const connection_h& handle) +{ + LL_PROFILE_ZONE_SCOPED_CATEGORY_WEBSOCKET; + WSConnection::ptr_t connection; + size_t size(0); + { + LLMutexLock lock(&mConnectionMutex); + auto it = mConnections.find(handle); + if (it == mConnections.end()) + { + connection = connectionFactory(shared_from_this(), handle); + if (!connection) + { + LL_WARNS("WebSocket") << "Failed to create connection for websocket server " << mServerName << LL_ENDL; + return; + } + mConnections[handle] = connection; + } + else + { + connection = it->second; + } + + if (!connection) + { + LL_WARNS("WebSocket") << mServerName << " failed to create connection object" << LL_ENDL; + return; + } + // Removed redundant assignment to mConnections[handle] + size = mConnections.size(); + } + + onConnectionOpened(connection); // TODO: consider letting the server reject the connection here + connection->onOpen(); + LL_INFOS("WebSocket") << mServerName << " opened new connection, total connections: " << size << LL_ENDL; +} + +void LLWebsocketMgr::WSServer::handleCloseConnection(const connection_h& handle) +{ + LL_PROFILE_ZONE_SCOPED_CATEGORY_WEBSOCKET; + size_t size(0); + WSConnection::ptr_t connection; + { + LLMutexLock lock(&mConnectionMutex); + auto it = mConnections.find(handle); + if (it != mConnections.end()) + { + connection = it->second; + mConnections.erase(it); + } + size = mConnections.size(); + } + if (connection) + { + connection->onClose(); + onConnectionClosed(connection); + LL_INFOS("WebSocket") << mServerName << " closed connection, total connections: " << size << LL_ENDL; + } + else + { + LL_WARNS("WebSocket") << mServerName << " attempted to close unknown connection" << LL_ENDL; + } +} + +void LLWebsocketMgr::WSServer::handleMessage(const connection_h& handle, const std::string& message) +{ + LL_PROFILE_ZONE_SCOPED_CATEGORY_WEBSOCKET; + WSConnection::ptr_t connection = getConnection(handle); + if (connection) + { + connection->onMessage(message); + } + else + { + LL_WARNS("WebSocket") << mServerName << " received message for unknown connection" << LL_ENDL; + } +} + +//------------------------------------------------------------------------ +bool LLWebsocketMgr::WSConnection::sendMessage(const std::string& message) const +{ + if (mOwningServer.expired()) + { + LL_WARNS("WebSocket") << "Attempted to send message on connection with null server reference" << LL_ENDL; + return false; + } + return mOwningServer.lock()->sendMessageTo(mConnectionHandle, message); +} + +bool LLWebsocketMgr::WSConnection::sendMessage(const boost::json::value& json) const +{ + std::string message = boost::json::serialize(json); + return sendMessage(message); +} + +bool LLWebsocketMgr::WSConnection::sendMessage(const LLSD& data) const +{ + return sendMessage(LlsdToJson(data)); +} + +void LLWebsocketMgr::WSConnection::closeConnection(U16 code, const std::string& reason) +{ + if (mOwningServer.expired()) + { + 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 (!mOwningServer.lock()->closeConnection(mConnectionHandle, code, reason)) + { + LL_WARNS("WebSocket") << "Failed to close connection through server" << LL_ENDL; + } +} + +bool LLWebsocketMgr::WSConnection::isConnected() const +{ + if (mOwningServer.expired()) + { + return false; + } + + LLWebsocketMgr::WSServer::ptr_t server = mOwningServer.lock(); + if (!server) + { + return false; + } + return server->getConnectionState(mConnectionHandle) == connection_open; +} + +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 new file mode 100644 index 0000000000..2c335307e3 --- /dev/null +++ b/indra/llcorehttp/llwebsocketmgr.h @@ -0,0 +1,310 @@ +/** + * @file llwebsocketmgr.h + * @brief WebSocket manager singleton for managing WebSocket servers and connections + * + * $LicenseInfo:firstyear=2025&license=viewerlgpl$ + * Second Life Viewer Source Code + * Copyright (C) 2025, Linden Research, Inc. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; + * version 2.1 of the License only. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * + * Linden Research, Inc., 945 Battery Street, San Francisco, CA 94111 USA + * $/LicenseInfo$ + */ + +#pragma once + +#include "llsingleton.h" +#include "llsd.h" +#include "lluuid.h" +#include "llhost.h" +#include "llmutex.h" + +#include <memory> +#include <map> +#include <vector> +#include <functional> +#include <thread> +#include <atomic> + +#include <boost/json.hpp> + +#include <websocketpp/common/connection_hdl.hpp> + +struct Server_impl; + +/** + * @class LLWebsocketMgr + * @brief Singleton manager for WebSocket connections and servers + * + * This class provides a high-level interface for managing WebSocket connections + * and servers using websocketpp library. It handles both client and server + * connections, provides thread-safe operations, and integrates with the + * existing Linden Lab infrastructure. + */ +class LLWebsocketMgr: public LLSingleton<LLWebsocketMgr> +{ + LLSINGLETON(LLWebsocketMgr) = default; + virtual ~LLWebsocketMgr() = default; + LOG_CLASS(LLWebsocketMgr); + +public: + using connection_h = websocketpp::connection_hdl; + class WSServer; + + enum connection_state_t + { // must map to websocketpp::session::state + connection_connecting = 0, + connection_open = 1, + connection_closing = 2, + connection_closed = 3 + }; + + class WSConnection + { + friend class LLWebsocketMgr; + + 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), + mOwningServer(server) + {} + + virtual ~WSConnection() = default; + + /** + * 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() {} + + /** + * 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. + */ + virtual void onMessage(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. + */ + bool sendMessage(const std::string& message) const; + bool sendMessage(const boost::json::value& json) const; + bool sendMessage(const LLSD& data) const; + + /** + * @brief Close the WebSocket connection gracefully + * @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 + * + * @note After calling this method, no further messages should be sent + * @note The onClose() callback will be invoked when the close handshake completes + */ + void closeConnection(U16 code = 1000, const std::string& reason = std::string()); + + bool isConnected() const; + + protected: + /// 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 + }; + + /** + * @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; + friend class WSConnection; + friend class LLWebsocketMgr; + + public: + using ptr_t = std::shared_ptr<WSServer>; + + WSServer(std::string_view name, U16 port, bool local_only = true); + virtual ~WSServer(); + + virtual void onStarted() {} + virtual void onStopped() {} + + virtual void onConnectionOpened(const WSConnection::ptr_t& connection) { } + virtual void onConnectionClosed(const WSConnection::ptr_t& connection) { } + + bool isRunning() const; + size_t getConnectionCount() const + { + LLMutexLock lock(&mConnectionMutex); + return mConnections.size(); + } + + void broadcastMessage(const std::string& message); + virtual bool update() { return true; } + + connection_state_t getConnectionState(const connection_h& handle) const; + + protected: + virtual WSConnection::ptr_t connectionFactory(WSServer::ptr_t server, connection_h handle); + + bool start(); + void stop(); + + 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()); + 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> >; + + WSConnection::ptr_t getConnection(const connection_h& handle); + + void handleOpenConnection(const connection_h& handle); + void handleCloseConnection(const connection_h& handle); + void handleMessage(const connection_h& handle, const std::string& message); + + std::string mServerName; + std::unique_ptr<Server_impl> mImpl; + connection_map_t mConnections; + mutable 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 + WSServer::ptr_t findServerByName(const std::string &name) const; + + bool addServer(const WSServer::ptr_t& server); + bool removeServer(const std::string &name); + + bool startServer(const std::string &name) const; + void stopServer(const std::string &name) const; + + void update(); + +protected: + void initSingleton() override; + void cleanupSingleton() override; + +private: + using server_map_t = std::map<std::string, WSServer::ptr_t>; + + void stopAllServers(); + + server_map_t mServers; +}; diff --git a/indra/llcorehttp/tests/llcorehttp_test.cpp b/indra/llcorehttp/tests/llcorehttp_test.cpp index c7c50e6166..65d7fe93a9 100755..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_httprequest.hpp b/indra/llcorehttp/tests/test_httprequest.hpp index 77ed8df066..68f8c4f71f 100644 --- a/indra/llcorehttp/tests/test_httprequest.hpp +++ b/indra/llcorehttp/tests/test_httprequest.hpp @@ -2744,13 +2744,6 @@ void HttpRequestTestObjectType::test<22>() set_test_name("BUG-2295"); -#if LL_WINDOWS && ADDRESS_SIZE == 64 - // teamcity win64 builds freeze on this test, if you figure out the cause, please fix it - if (getenv("TEAMCITY_PROJECT_NAME")) - { - skip("BUG-2295 - partial load on W64 causes freeze"); - } -#endif // Handler can be stack-allocated *if* there are no dangling // references to it after completion of this method. // Create before memory record as the string copy will bump numbers. @@ -2924,14 +2917,6 @@ void HttpRequestTestObjectType::test<23>() set_test_name("HttpRequest GET 503s with 'Retry-After'"); -#if LL_WINDOWS && ADDRESS_SIZE == 64 - // teamcity win64 builds freeze on this test, if you figure out the cause, please fix it - if (getenv("TEAMCITY_PROJECT_NAME")) - { - skip("llcorehttp 503-with-retry test hangs on Windows 64"); - } -#endif - // This tests mainly that the code doesn't fall over if // various well- and mis-formed Retry-After headers are // sent along with the response. Direct inspection of 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 |
