diff options
| -rw-r--r-- | doc/external-editor-json-rpc.md | 146 | ||||
| -rw-r--r-- | indra/llcorehttp/lljsonrpcws.cpp | 111 | ||||
| -rw-r--r-- | indra/llcorehttp/lljsonrpcws.h | 13 | ||||
| -rw-r--r-- | indra/newview/llscripteditorws.cpp | 27 | ||||
| -rw-r--r-- | indra/newview/llscripteditorws.h | 4 |
5 files changed, 265 insertions, 36 deletions
diff --git a/doc/external-editor-json-rpc.md b/doc/external-editor-json-rpc.md index a09bde2b22..570388ac7c 100644 --- a/doc/external-editor-json-rpc.md +++ b/doc/external-editor-json-rpc.md @@ -17,7 +17,10 @@ This document describes all the message interfaces defined for WebSocket communi - [SessionHandshakeResponse](#sessionhandshakeresponse) - [Session OK](#session-ok) - [SessionDisconnect](#sessiondisconnect) - - [SessionPing](#sessionping) + - [SystemPing](#systemping) + - [SystemVersion](#systemversion) + - [SystemStatus](#systemstatus) + - [SystemListMethods](#systemlistmethods) - [Language and Syntax Interfaces](#language-and-syntax-interfaces) - [SyntaxChange](#syntaxchange) - [Language Syntax ID Request](#language-syntax-id-request) @@ -165,8 +168,14 @@ WebSocket connects → session.handshake → session.ok | `session.handshake` (response) | Extension → Viewer | Response | `SessionHandshakeResponse` | | `session.ok` | Viewer → Extension | Notification | _(no interface)_ | | `session.disconnect` | Bidirectional | Notification | `SessionDisconnect` | -| `session.ping` | Bidirectional | Call | `SessionPing` | -| `session.ping` (response) | Bidirectional | Response | `SessionPingResponse` | +| `system.ping` | Bidirectional | Call | `SystemPing` | +| `system.ping` (response) | Bidirectional | Response | `SystemPingResponse` | +| `system.getVersion` | Bidirectional | Call | _(no parameters)_ | +| `system.getVersion` (response) | Bidirectional | Response | `SystemVersionResponse` | +| `system.status` | Bidirectional | Call | _(no parameters)_ | +| `system.status` (response) | Bidirectional | Response | `SystemStatusResponse` | +| `system.listMethods` | Bidirectional | Call | _(no parameters)_ | +| `system.listMethods` (response) | Bidirectional | Response | `SystemListMethodsResponse`| | `script.subscribe` | Extension → Viewer | Call | `ScriptSubscribe` | | `script.subscribe` (response) | Viewer → Extension | Response | `ScriptSubscribeResponse` | | `script.unsubscribe` | Viewer → Extension | Notification | `ScriptUnsubscribe` | @@ -373,18 +382,19 @@ interface SessionDisconnect { - `4`: Internal server error - `message`: Human-readable description of the disconnect reason -### SessionPing +### SystemPing -**JSON-RPC Method:** `session.ping` (call, bidirectional) +**JSON-RPC Method:** `system.ping` (call, bidirectional) -Heartbeat call used to verify the connection is alive and measure latency. Either side can initiate a ping; the recipient responds with the original timestamp plus its own server time. +Ping call used to verify that the connection is alive and measure latency. Either side can +initiate a ping. -In practice the extension initiates and the viewer only answers — the viewer never sends -`session.ping` itself. The extension pings every 30 seconds and tears the connection down after -two consecutive failures. +The generic JSON-RPC server responds with the simple result `"pong"`. The editor server extends +that response with the original timestamp and its current server time. The extension uses the +extended response for its periodic connection-health check. ```typescript -interface SessionPing { +interface SystemPing { timestamp: number; } ``` @@ -396,7 +406,8 @@ interface SessionPing { **Response:** ```typescript -interface SessionPingResponse { +interface SystemPingResponse { + pong: string; timestamp: number; server_time: number; } @@ -404,15 +415,19 @@ interface SessionPingResponse { **Response Fields:** -- `timestamp`: The original timestamp from the request. Echoed back only when the request supplied one. +- `pong`: Acknowledgement that the ping was received. +- `timestamp`: The original timestamp from the request, echoed by the editor server. - `server_time`: Unix timestamp in milliseconds when the response was generated +The extension sends a `system.ping` request every 30 seconds and tears the connection down +after two consecutive failures. + **Example Request:** ```json { "jsonrpc": "2.0", - "method": "session.ping", + "method": "system.ping", "id": 42, "params": { "timestamp": 1721145600000 @@ -420,6 +435,50 @@ interface SessionPingResponse { } ``` +### SystemVersion + +**JSON-RPC Method:** `system.getVersion` (call, bidirectional) + +Requests the identity and version of the peer. The response uses the same field names in both +directions: + +```typescript +interface SystemVersionResponse { + client_name: string; + client_version: string; +} +``` + +The viewer returns its viewer channel as `client_name` and its full viewer version as +`client_version`. The extension returns its package name as `client_name` and its package version +as `client_version`. + +**Example viewer response:** + +```json +{ + "jsonrpc": "2.0", + "id": 43, + "result": { + "client_name": "Second Life", + "client_version": "7.1.0.123456" + } +} +``` + +**Example extension response:** + +```json +{ + "jsonrpc": "2.0", + "id": 43, + "result": { + "client_name": "sl-vscode-plugin", + "client_version": "1.0.6" + } +} +``` + **Example Response:** ```json @@ -427,12 +486,73 @@ interface SessionPingResponse { "jsonrpc": "2.0", "id": 42, "result": { + "pong": "pong", "timestamp": 1721145600000, "server_time": 1721145600015 } } ``` +### SystemStatus + +**JSON-RPC Method:** `system.status` (call, bidirectional) + +Requests the current status of the peer. The default response is: + +```typescript +interface SystemStatusResponse { + status: "OK"; +} +``` + +Both the viewer and the extension currently return `status: "OK"`. The response may be extended +with additional status information in the future. + +**Example response:** + +```json +{ + "jsonrpc": "2.0", + "id": 44, + "result": { + "status": "OK" + } +} +``` + +### SystemListMethods + +**JSON-RPC Method:** `system.listMethods` (call, bidirectional) + +Requests the names of the methods available on the receiving peer. The response contains an +alphabetically ordered list of unique method names. Sync versus async dispatch is an internal +implementation detail and is not exposed by this interface. + +```typescript +type SystemListMethodsResponse = string[]; +``` + +The viewer returns all methods registered on the connection, including methods registered by the +base JSON-RPC server and the editor server. The extension returns its built-in system methods +together with dynamically registered handlers. + +**Example response:** + +```json +{ + "jsonrpc": "2.0", + "id": 45, + "result": [ + "command.execute", + "command.list", + "system.getVersion", + "system.listMethods", + "system.ping", + "system.status" + ] +} +``` + ## Language and Syntax Interfaces ### SyntaxChange diff --git a/indra/llcorehttp/lljsonrpcws.cpp b/indra/llcorehttp/lljsonrpcws.cpp index df76f6a2bf..5b704e6322 100644 --- a/indra/llcorehttp/lljsonrpcws.cpp +++ b/indra/llcorehttp/lljsonrpcws.cpp @@ -516,6 +516,24 @@ void LLJSONRPCConnection::unregisterMethod(const std::string& method) LL_DEBUGS("JSONRPC") << "Unregistered method: " << method << LL_ENDL; } +std::set<std::string> LLJSONRPCConnection::getMethods() const +{ + LLMutexLock lock(&mMutex); + std::set<std::string> methods; + + for (const auto& [method, handler] : mMethodHandlers) + { + methods.insert(method); + } + + for (const auto& [method, handler] : mAsyncMethodHandlers) + { + methods.insert(method); + } + + return methods; +} + LLSD LLJSONRPCConnection::makeEnvelope(const LLSD& id, const std::string& method, const LLSD& params, @@ -670,23 +688,11 @@ LLJSONRPCServer::LLJSONRPCServer(const std::string& name, U16 port, bool local_o << " on port " << port << LL_ENDL; // Register standard JSON-RPC methods - registerGlobalMethod("system.listMethods", [this](const std::string& method, const LLSD& id, const LLSD& params) -> LLSD { - LL_DEBUGS("JSONRPC") << "System method " << method << " called" << LL_ENDL; - return getMethodList(); - }); - registerGlobalMethod("system.getStats", [this](const std::string& method, const LLSD& id, const LLSD& params) -> LLSD { LL_DEBUGS("JSONRPC") << "System method " << method << " called" << LL_ENDL; return getServerStats(); }); - registerGlobalMethod("system.ping", [](const std::string& method, const LLSD& id, const LLSD& params) -> LLSD { - LL_DEBUGS("JSONRPC") << "System method " << method << " called" << LL_ENDL; - LLSD result; - result["pong"] = LLDate::now().asString(); - result["params"] = params; - return result; - }); } LLWebsocketMgr::WSConnection::ptr_t LLJSONRPCServer::connectionFactory(LLWebsocketMgr::WSServer::ptr_t server, @@ -719,21 +725,80 @@ void LLJSONRPCServer::setupConnectionMethods(LLJSONRPCConnection::ptr_t connecti connection->registerMethod(method, handler); } - // Register session.ping handler for connection health monitoring - connection->registerMethod("session.ping", - [](const std::string&, const LLSD&, const LLSD& params) -> LLSD + std::weak_ptr<LLJSONRPCConnection> weak_connection = connection; + connection->registerMethod( + "system.listMethods", + [weak_connection]( + const std::string&, + const LLSD&, + const LLSD&) -> LLSD { - LLSD result; - // Echo back the original timestamp - if (params.has("timestamp")) + LLSD methods(LLSD::emptyArray()); + auto connection = weak_connection.lock(); + if (!connection) + { + return methods; + } + + for (const std::string& method : connection->getMethods()) { - result["timestamp"] = params["timestamp"]; + methods.append(method); } - // Add server's current time in milliseconds - result["server_time"] = static_cast<LLSD::Integer>( - LLDate::now().secondsSinceEpoch() * 1000.0); - return result; + + return methods; + }); + + connection->registerMethod( + "system.ping", + [this, weak_connection]( + const std::string& method, + const LLSD& id, + const LLSD& params) -> LLSD + { + LL_DEBUGS("JSONRPC") << "System method " << method + << " called" << LL_ENDL; + return handlePing(weak_connection.lock(), params); + }); + + connection->registerMethod( + "system.getVersion", + [this, weak_connection]( + const std::string& method, + const LLSD& id, + const LLSD& params) -> LLSD + { + LL_DEBUGS("JSONRPC") << "System method " << method + << " called" << LL_ENDL; + return handleGetVersion(weak_connection.lock(), params); }); + + connection->registerMethod( + "system.status", + [this, weak_connection]( + const std::string& method, + const LLSD& id, + const LLSD& params) -> LLSD + { + LL_DEBUGS("JSONRPC") << "System method " << method + << " called" << LL_ENDL; + return handleStatus(weak_connection.lock(), params); + }); +} + +LLSD LLJSONRPCServer::handlePing( + const LLJSONRPCConnection::ptr_t& connection, + const LLSD& params) const +{ + return LLSD("pong"); +} + +LLSD LLJSONRPCServer::handleStatus( + const LLJSONRPCConnection::ptr_t& connection, + const LLSD& params) const +{ + LLSD result; + result["status"] = "OK"; + return result; } void LLJSONRPCServer::registerGlobalMethod(const std::string& method, MethodHandler handler) diff --git a/indra/llcorehttp/lljsonrpcws.h b/indra/llcorehttp/lljsonrpcws.h index 7cb26b1fb2..aa4a96be4d 100644 --- a/indra/llcorehttp/lljsonrpcws.h +++ b/indra/llcorehttp/lljsonrpcws.h @@ -275,6 +275,12 @@ public: void unregisterMethod(const std::string& method); /** + * @brief Get the names of all registered methods + * @return Ordered set containing sync and async method names + */ + virtual std::set<std::string> getMethods() const; + + /** * @brief Make an asynchronous JSON-RPC call * @param method The method name to call * @param params The parameters to pass @@ -495,6 +501,13 @@ protected: LLWebsocketMgr::WSConnection::ptr_t connectionFactory(LLWebsocketMgr::WSServer::ptr_t server, LLWebsocketMgr::connection_h handle) override; + virtual LLSD handlePing(const LLJSONRPCConnection::ptr_t& connection, + const LLSD& params) const; + virtual LLSD handleGetVersion(const LLJSONRPCConnection::ptr_t& connection, + const LLSD& params) const = 0; + virtual LLSD handleStatus(const LLJSONRPCConnection::ptr_t& connection, + const LLSD& params) const; + /** * @brief Apply global method handlers to a new connection * @param connection The connection to configure diff --git a/indra/newview/llscripteditorws.cpp b/indra/newview/llscripteditorws.cpp index 729f335db4..41efa7e016 100644 --- a/indra/newview/llscripteditorws.cpp +++ b/indra/newview/llscripteditorws.cpp @@ -1251,6 +1251,33 @@ void LLScriptEditorWSServer::broadcastLanguageChange() } } +LLSD LLScriptEditorWSServer::handlePing( + const LLJSONRPCConnection::ptr_t& connection, + const LLSD& params) const +{ + LLSD result; + result["pong"] = "pong"; + + if (params.has("timestamp")) + { + result["timestamp"] = params["timestamp"]; + } + + result["server_time"] = static_cast<LLSD::Integer>( + LLDate::now().secondsSinceEpoch() * 1000.0); + return result; +} + +LLSD LLScriptEditorWSServer::handleGetVersion( + const LLJSONRPCConnection::ptr_t& connection, + const LLSD& params) const +{ + LLSD result; + result["client_name"] = LLVersionInfo::instance().getChannel(); + result["client_version"] = LLVersionInfo::instance().getVersion(); + return result; +} + LLSD LLScriptEditorWSServer::handleLanguageIdRequest() const { LLSD response; diff --git a/indra/newview/llscripteditorws.h b/indra/newview/llscripteditorws.h index e569d4ff7d..6eb8385210 100644 --- a/indra/newview/llscripteditorws.h +++ b/indra/newview/llscripteditorws.h @@ -236,6 +236,10 @@ protected: LLWebsocketMgr::connection_h handle) override; void setupConnectionMethods(LLJSONRPCConnection::ptr_t connection) override; + LLSD handlePing(const LLJSONRPCConnection::ptr_t& connection, + const LLSD& params) const override; + LLSD handleGetVersion(const LLJSONRPCConnection::ptr_t& connection, + const LLSD& params) const override; void broadcastLanguageChange(); |
