diff options
| author | Rider Linden <rider@lindenlab.com> | 2025-09-17 13:14:23 -0700 |
|---|---|---|
| committer | Rider Linden <rider@lindenlab.com> | 2025-10-07 09:19:37 -0700 |
| commit | 684af8426b36f9ad4c2324ae0c5b4dc62f840079 (patch) | |
| tree | add9042ebd25157bb248fb36628cb69494af1c17 /indra/newview/llscripteditorws.cpp | |
| parent | f31c194b8674df295edf74693a2f843ca314ca92 (diff) | |
initial Lua types files, and switch websocket server to use a single connection for all scripts.
Diffstat (limited to 'indra/newview/llscripteditorws.cpp')
| -rw-r--r-- | indra/newview/llscripteditorws.cpp | 423 |
1 files changed, 271 insertions, 152 deletions
diff --git a/indra/newview/llscripteditorws.cpp b/indra/newview/llscripteditorws.cpp index 9863130aea..cb67880455 100644 --- a/indra/newview/llscripteditorws.cpp +++ b/indra/newview/llscripteditorws.cpp @@ -24,24 +24,6 @@ * $/LicenseInfo$ */ -/** - * This implementation provides JSON-RPC 2.0 WebSocket communication between - * the Second Life viewer and external script editors. It uses the standard - * JSON-RPC 2.0 protocol without pre-defined script-specific methods, - * allowing for flexible integration approaches. - * - * ## JSON-RPC Integration - * - * The connection provides a clean JSON-RPC 2.0 interface that can be - * extended with script-specific functionality as needed: - * - * ### Server-to-Client (Viewer to Editor): - * - `session.handshake`: Welcome message on connection - * - `session.disconnect`: Notify editor of disconnection - * - * ### Notifications (no response expected): - */ - #include "llviewerprecompiledheaders.h" #include "llscripteditorws.h" #include "llpreviewscript.h" @@ -51,6 +33,7 @@ #include "llerror.h" #include "lluuid.h" #include "llversioninfo.h" +#include "llagent.h" //======================================================================== LLScriptEditorWSServer::LLScriptEditorWSServer(const std::string& name, U16 port, bool local_only) @@ -64,7 +47,7 @@ LLWebsocketMgr::WSConnection::ptr_t LLScriptEditorWSServer::connectionFactory(LL LLWebsocketMgr::connection_h handle) { auto connection = std::make_shared<LLScriptEditorWSConnection>(server, handle); - mActiveConnections.insert(connection); + mActiveConnections[connection->getConnectionID()] = connection; // Call setupConnectionMethods to register any global methods setupConnectionMethods(connection); @@ -72,6 +55,29 @@ LLWebsocketMgr::WSConnection::ptr_t LLScriptEditorWSServer::connectionFactory(LL return connection; } +void LLScriptEditorWSServer::onStarted() +{ + LLSyntaxIdLSL& syntax_id_mgr = LLSyntaxIdLSL::instance(); + wptr_t that(std::static_pointer_cast<LLScriptEditorWSServer>(shared_from_this())); + + mLastSyntaxId = syntax_id_mgr.getSyntaxID(); + mLanguageChangeSignal = syntax_id_mgr.addSyntaxIDCallback( + [that]() + { + auto server = that.lock(); + if (server && server->isRunning()) + { + server->broadcastLangugeChange(); + } + }); +} + +void LLScriptEditorWSServer::onStopped() +{ + mLanguageChangeSignal.disconnect(); + mLastSyntaxId.setNull(); +} + void LLScriptEditorWSServer::onConnectionOpened(const LLWebsocketMgr::WSConnection::ptr_t& connection) { // Call parent class to handle JSON-RPC setup and standard methods @@ -92,35 +98,102 @@ void LLScriptEditorWSServer::onConnectionClosed(const LLWebsocketMgr::WSConnecti auto script_connection = std::dynamic_pointer_cast<LLScriptEditorWSConnection>(connection); if (script_connection) { - mActiveConnections.erase(script_connection); + U32 connection_id = script_connection->getConnectionID(); + unsubscribeConnection(connection_id); + mActiveConnections.erase(connection_id); - LL_INFOS("ScriptEditorWS") << "Removed connection from active connections. Total: " + LL_DEBUGS("ScriptEditorWS") << "Removed connection from active connections. Total: " << mActiveConnections.size() << LL_ENDL; // TODO: When connections reach 0, stop the server aftera a timeout. } } -bool LLScriptEditorWSServer::associateEditor(const LLHandle<LLPanel>& editor_handle, const std::string& script_id) +bool LLScriptEditorWSServer::subscribeScriptEditor(const LLHandle<LLPanel>& editor_handle, const std::string &script_id) { if (!editor_handle.isDead()) { - mScriptEditors[script_id] = editor_handle; + auto it = mSubscriptions.find(script_id); + if (it == mSubscriptions.end()) + { // Don't readd if already subscribed + mSubscriptions.emplace(script_id, EditorSubscription{ editor_handle, LLScriptEditorWSConnection::wptr_t() }); + return false; + } + else + { // Update existing subscription with new editor handle + it->second.mEditorHandle = editor_handle; + } return true; } return false; } -void LLScriptEditorWSServer::dissociateEditor(const std::string& script_id) +void LLScriptEditorWSServer::unsubscribeEditor(const std::string &script_id) +{ + auto it = mSubscriptions.find(script_id); + if (it != mSubscriptions.end()) + { + mSubscriptions.erase(it); + } +} + +void LLScriptEditorWSServer::unsubscribeConnection(U32 connection_id) { - mScriptEditors.erase(script_id); + for (auto it = mSubscriptions.begin(); it != mSubscriptions.end(); ) + { + if (it->second.mConnectionID == connection_id) + { + LL_DEBUGS("ScriptEditorWS") << "Unsubscribing script " << it->first + << " from connection ID " << connection_id << LL_ENDL; + it = mSubscriptions.erase(it); + } + else + { + ++it; + } + } } +LLScriptEditorWSServer::SubscriptionError_t LLScriptEditorWSServer::updateScriptSubscription(const std::string &script_id, U32 connection_id) +{ + auto it = mSubscriptions.find(script_id); + if (it != mSubscriptions.end()) + { + if (it->second.mEditorHandle.isDead()) + { + unsubscribeEditor(script_id); + return SUBSCRIPTION_INVALID_EDITOR; + } + + auto con_it = mActiveConnections.find(connection_id); + if (con_it == mActiveConnections.end()) + { + return SUBSCRIPTION_INTERNAL_ERROR; + } + + if ((it->second.mConnectionID != 0) && !it->second.mConnection.expired() + && it->second.mConnection.lock()->isConnected()) + { + LL_WARNS("ScriptEditorWS") << "Script " << script_id << " is already subscribed on connection ID " << it->second.mConnectionID + << ", cannot subscribe again on connection ID " << connection_id << LL_ENDL; + // In the future we may want to support multiple connections per script. + // That would imply it was open in multiple editors. + return SUBSCRIPTION_ALREADY_SUBSCRIBED; + } + + it->second.mConnectionID = connection_id; + it->second.mConnection = con_it->second; + return SUBSCRIPTION_SUCCESS; + } + return SUBSCRIPTION_INVALID_SUBSCRIPTION; +} + + LLHandle<LLPanel> LLScriptEditorWSServer::findEditorForScript(const std::string& script_id) const { - auto it = mScriptEditors.find(script_id); - if (it != mScriptEditors.end()) + auto it = mSubscriptions.find(script_id); + if (it != mSubscriptions.end()) { - return it->second; + return it->second.mEditorHandle; } return LLHandle<LLPanel>(); } @@ -135,9 +208,9 @@ std::shared_ptr<LLScriptEditorWSConnection> LLScriptEditorWSServer::findConnecti std::set<std::string> LLScriptEditorWSServer::getActiveScripts() const { std::set<std::string> active_scripts; - for (const auto& [script_id, editor_handle] : mScriptEditors) + for (const auto& [script_id, subinfo] : mSubscriptions) { - if (!editor_handle.isDead()) + if (!subinfo.mEditorHandle.isDead()) { active_scripts.insert(script_id); } @@ -145,69 +218,182 @@ std::set<std::string> LLScriptEditorWSServer::getActiveScripts() const return active_scripts; } -void LLScriptEditorWSServer::broadcastScriptUpdate(const std::string& script_id, const std::string& content, const LLSD& metadata) +void LLScriptEditorWSServer::setupConnectionMethods(LLJSONRPCConnection::ptr_t connection) { - LL_DEBUGS("ScriptEditorWS") << "Broadcasting script update for script: " << script_id << LL_ENDL; + // Call parent class to register global JSON-RPC methods + LLJSONRPCServer::setupConnectionMethods(connection); + + // Cast to our specific connection type to access script editor functionality + auto script_connection = std::dynamic_pointer_cast<LLScriptEditorWSConnection>(connection); + if (script_connection) + { + LL_DEBUGS("ScriptEditorWS") << "Setting up script editor connection methods" << LL_ENDL; + wptr_t that(std::static_pointer_cast<LLScriptEditorWSServer>(shared_from_this())); + + U32 connection_id = script_connection->getConnectionID(); + + script_connection->registerMethod("language.syntax.id", + [that](const std::string&, const LLSD&, const LLSD&) -> LLSD + { + auto server = that.lock(); + if (server) + { + return server->handleLanguageIdRequest(); + } + return LLSD(); + }); + script_connection->registerMethod("language.syntax", + [that](const std::string&, const LLSD&, const LLSD& params) + { + auto server = that.lock(); + if (server) + { + return server->handleSyntaxRequest(params); + } + return LLSD(); + }); + script_connection->registerMethod("script.subscribe", + [that, connection_id](const std::string&, const LLSD&, const LLSD& params) -> LLSD + { + auto server = that.lock(); + if (server) + { + return server->handleScriptSubscribe(connection_id, params); + } + return LLSD(); + }); + script_connection->registerMethod("script.unsubscribe", [](const std::string&, const LLSD&, const LLSD& params) -> LLSD + { // this is a notification, no response expected + return LLSD(); + }); + // script_connection->registerMethod("language.syntax", ) + } +} - LLSD params; - params["script_id"] = script_id; - params["content"] = content; - params["timestamp"] = LLDate::now().asString(); +void LLScriptEditorWSServer::broadcastLangugeChange() +{ + LLUUID syntax_id = LLSyntaxIdLSL::instance().getSyntaxID(); - if (!metadata.isUndefined()) + if (syntax_id != mLastSyntaxId) { - params["metadata"] = metadata; + mLastSyntaxId = syntax_id; + LLSD params; + params["id"] = syntax_id; + + if (isRunning()) + { + broadcastNotification("language.syntax.change", params); + } } +} + +LLSD LLScriptEditorWSServer::handleLanguageIdRequest() const +{ + LLSD response; - // Send to all connected editors as a notification - broadcastNotification("script.update", params); + response["id"] = mLastSyntaxId; + return response; } -void LLScriptEditorWSServer::broadcastCompilationResult(const std::string& script_id, bool success, const LLSD& errors) +LLSD LLScriptEditorWSServer::handleSyntaxRequest(const LLSD& params) const { - LL_DEBUGS("ScriptEditorWS") << "Broadcasting compilation result for script: " << script_id - << " (success: " << success << ")" << LL_ENDL; + LLSD response(LLSD::emptyMap()); + std::string category = params["kind"].asString(); - LLSD params; - params["script_id"] = script_id; - params["success"] = success; - params["timestamp"] = LLDate::now().asString(); + response["id"] = mLastSyntaxId; - if (!errors.isUndefined() && errors.isArray()) + if (category == "types.luau") { - params["errors"] = errors; + response["types"] = LLSyntaxLua::instance().getTypesXML(); } + else + { + LLSD syntax = LLSyntaxIdLSL::instance().getKeywordsXML(); - // Send to all connected editors as a notification - broadcastNotification("compilation.result", params); + // TODO: support language definitions and additional modules. + + if (syntax.has(category)) + { + response[category] = syntax[category]; + } + } + return response; } -void LLScriptEditorWSServer::setupConnectionMethods(LLJSONRPCConnection::ptr_t connection) +LLSD LLScriptEditorWSServer::handleScriptSubscribe(U32 connection_id, const LLSD& params) { - // Call parent class to register global JSON-RPC methods - LLJSONRPCServer::setupConnectionMethods(connection); + LLSD response(LLSD::emptyMap()); - // Cast to our specific connection type to access script editor functionality - auto script_connection = std::dynamic_pointer_cast<LLScriptEditorWSConnection>(connection); - if (script_connection) + std::string script_id = params["script_id"].asString(); + std::string script_name = params["script_name"].asString(); + std::string language = params["script_language"].asString(); + + SubscriptionError_t result = updateScriptSubscription(script_id, connection_id); + + response["script_id"] = script_id; + response["success"] = (result == SUBSCRIPTION_SUCCESS); + response["status"] = result; + + LL_WARNS_IF(result != SUBSCRIPTION_SUCCESS, "ScriptEditorWS") + << "Script connect request for script " << script_id << " failed with status " << result << LL_ENDL; + switch (result) { - LL_INFOS("ScriptEditorWS") << "Setting up script editor connection methods" << LL_ENDL; + case SUBSCRIPTION_SUCCESS: + response["message"] = "OK"; + break; + case SUBSCRIPTION_INVALID_EDITOR: + response["message"] = "Invalid editor handle"; + break; + case SUBSCRIPTION_INVALID_SUBSCRIPTION: + response["message"] = "No subscription found for script"; + break; + case SUBSCRIPTION_ALREADY_SUBSCRIBED: + response["message"] = "Script already subscribed"; + break; + case SUBSCRIPTION_INTERNAL_ERROR: + response["message"] = "Internal server error"; + break; + } - // Here derived classes could add script-specific method registrations - // For now, the base LLScriptEditorWSConnection doesn't register any specific methods - // but this provides a hook for future customization + if (result == SUBSCRIPTION_SUCCESS) + { + //TODO: Build an info block for the subscribed script. + //buildScriptSubscriptionInfo(result); + } - // Example of how custom methods could be registered: - // script_connection->registerMethod("script.custom", handler); + return response; +} + +LLSD LLScriptEditorWSServer::handleScriptUnsubscribe(U32 connection_id, const LLSD& params) +{ + std::string script_id = params["script_id"].asString(); + + auto it = mSubscriptions.find(script_id); + if (it != mSubscriptions.end() && (it->second.mConnectionID == connection_id)) + { + unsubscribeEditor(script_id); } + return LLSD(); } -//======================================================================== -LLScriptEdContainer* LLScriptEditorWSConnection::getEditor() const +void LLScriptEditorWSServer::sendUnsubscribeScriptEditor(const std::string& script_id) { - return mEditorPanel.isDead() ? nullptr : dynamic_cast<LLScriptEdContainer*>(mEditorPanel.get()); + auto it = mSubscriptions.find(script_id); + if (it != mSubscriptions.end()) + { + auto connection = it->second.mConnection.lock(); + if (connection) + { + LLSD params; + params["script_id"] = script_id; + connection->notify("script.unsubscribe", params); + } + } } +//======================================================================== +U32 LLScriptEditorWSConnection::sNextConnectionID = 1; + std::shared_ptr<LLScriptEditorWSServer> LLScriptEditorWSConnection::getServer() const { return std::static_pointer_cast<LLScriptEditorWSServer>(mOwningServer.lock()); @@ -220,20 +406,17 @@ void LLScriptEditorWSConnection::onOpen() LL_INFOS("ScriptEditorWS") << "Script editor JSON-RPC connection opened" << LL_ENDL; - // Generate unique editor session ID - mEditorId = LLUUID::generateNewID().asString(); - mEditorReady = false; - - LL_INFOS("ScriptEditorWS") << "Initialized editor session: " << mEditorId << LL_ENDL; - - // Build hello data according to the protocol specification + // Build hello data LLSD handshake; handshake["server_version"] = "1.0.0"; handshake["protocol_version"] = "1.0"; handshake["viewer_name"] = LLVersionInfo::instance().getChannel(); handshake["viewer_version"] = LLVersionInfo::instance().getVersion(); - // Supported languages array + handshake["agent_id"] = gAgent.getID(); + + // handshake["challenge"] = ... TODO: simple challenge, write to a file and have the client echo it back? + LLSD languages = LLSD::emptyArray(); languages.append("lsl"); languages.append("luau"); @@ -243,14 +426,19 @@ void LLScriptEditorWSConnection::onOpen() LLSD features; features["live_sync"] = true; features["compilation"] = true; - features["syntax_highlight"] = true; handshake["features"] = features; - // Send editor.handshake method call to the client and handle response - call("session.handshake", handshake, [this](const LLSD& result, const LLSD& error) { + wptr_t that = shared_from_this(); + + // Send session.handshake method call and the response + call("session.handshake", handshake, [that](const LLSD& result, const LLSD& error) { if (error.isUndefined()) { - handleHandshakeResponse(result); + auto self = that.lock(); + if (self) + { + self->handleHandshakeResponse(result); + } } else { @@ -266,17 +454,7 @@ void LLScriptEditorWSConnection::onClose() { // Call parent class to clean up JSON-RPC infrastructure LLJSONRPCConnection::onClose(); - - LL_INFOS("ScriptEditorWS") << "Script editor JSON-RPC connection closed for session: " - << mEditorId << LL_ENDL; - - cleanupConnection(); - - // Clean up editor-specific state - mEditorId.clear(); - mEditorCapabilities.clear(); - mScriptId.clear(); - mEditorReady = false; + mOwningServer.reset(); // Clean up handshake response data mClientName.clear(); @@ -297,6 +475,8 @@ void LLScriptEditorWSConnection::handleHandshakeResponse(const LLSD& result) mClientVersion = result["client_version"].asString(); mProtocolVersion = result["protocol_version"].asString(); + // TODO: Validate challenge_response if implemented + // Validate protocol compatibility if (mProtocolVersion != "1.0") { @@ -307,7 +487,6 @@ void LLScriptEditorWSConnection::handleHandshakeResponse(const LLSD& result) // Store script information if provided mScriptName = result["script_name"].asString(); mScriptLanguage = result["script_language"].asString(); - mScriptId = result["script_id"].asString(); // Store supported languages for (const auto& lang : llsd::inArray( result["languages"])) @@ -326,67 +505,7 @@ void LLScriptEditorWSConnection::handleHandshakeResponse(const LLSD& result) } } - connectToEditor(mScriptId); - // Mark editor as ready - mEditorReady = true; - - LL_INFOS("ScriptEditorWS") << "Handshake completed successfully for session: " << mEditorId << LL_ENDL; -} - -bool LLScriptEditorWSConnection::connectToEditor(const std::string& script_id) -{ - LLScriptEditorWSServer::ptr_t server = std::dynamic_pointer_cast<LLScriptEditorWSServer>(mOwningServer.lock()); - if (!server) - { - LL_WARNS("ScriptEditorWS") << "Cannot connect to editor - server reference lost" << LL_ENDL; - return false; - } - - mEditorPanel = server->findEditorForScript(script_id); - - LLScriptEdContainer* editor_core = getEditor(); - if (!editor_core) - { - LL_INFOS("ScriptEditorWS") << "Could not find editor: " << script_id << LL_ENDL; - // TODO: Disconnect the client if no editor found - return false; - } - - return true; -} - -void LLScriptEditorWSConnection::cleanupConnection() -{ - LL_INFOS("ScriptEditorWS") << "Cleaning up connection for editor session: " << mEditorId << LL_ENDL; - - LLScriptEditorWSServer::ptr_t server = getServer(); - if (server) - { - server->dissociateEditor(mScriptId); - } - - LLScriptEdContainer* editor_core = getEditor(); - - if (editor_core) - { - editor_core->cleanupWebSocket(); - - // Notify the editor panel of disconnection - //editor_core->onExternalEditorDisconnected(); - } - - mEditorPanel = LLHandle<LLPanel>(); -} - - -void LLScriptEditorWSConnection::sendDisconnect(S32 reason, const std::string& message) -{ - LL_INFOS("ScriptEditorWS") << "Sending disconnect message to editor (reason: " - << reason << ", message: " << message << ")" << LL_ENDL; - - LLSD params; - params["reason"] = reason; - params["message"] = message; + notify("session.ok"); - notify("session.disconnect", params); + LL_INFOS("ScriptEditorWS") << "Handshake completed successfully." << LL_ENDL; } |
