diff options
| author | Andrey Lihatskiy <alihatskiy@productengine.com> | 2026-04-01 19:50:33 +0300 |
|---|---|---|
| committer | Andrey Lihatskiy <alihatskiy@productengine.com> | 2026-04-01 19:50:33 +0300 |
| commit | f5908eddc683dd705d334c88377139e39bbfb54f (patch) | |
| tree | 75c0ff026fe13c0a507a4527b697aa212736998d /indra/newview/llscripteditorws.cpp | |
| parent | d045f9aa32789a35628aced166145c182751acbf (diff) | |
| parent | f209affdfdf271098843ba400d7a75b00f827233 (diff) | |
Merge branch 'project/lua_editor' into marchcat/devlinux-slua
# Conflicts:
# indra/llcommon/llstl.h
# indra/newview/app_settings/settings.xml
Diffstat (limited to 'indra/newview/llscripteditorws.cpp')
| -rw-r--r-- | indra/newview/llscripteditorws.cpp | 856 |
1 files changed, 856 insertions, 0 deletions
diff --git a/indra/newview/llscripteditorws.cpp b/indra/newview/llscripteditorws.cpp new file mode 100644 index 0000000000..30a1ca53f6 --- /dev/null +++ b/indra/newview/llscripteditorws.cpp @@ -0,0 +1,856 @@ +/** + * @file llscripteditorws.cpp + * @brief JSON-RPC 2.0 WebSocket server implementation for external script editor integration + * + * $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 "llviewerprecompiledheaders.h" +#include "llscripteditorws.h" +#include "llpreviewscript.h" +#include "llappviewer.h" +#include "lltrans.h" +#include "lldate.h" +#include "llerror.h" +#include "lluuid.h" +#include "llversioninfo.h" +#include "llagent.h" +#include "llregex.h" +#include "llviewerobject.h" +#include "llviewerobjectlist.h" +#include "llchat.h" + +//======================================================================== +LLScriptEditorWSServer::LLScriptEditorWSServer(const std::string& name, U16 port, bool local_only) + : LLJSONRPCServer(name, port, local_only) +{ + LL_INFOS("ScriptEditorWS") << "Created JSON-RPC script editor server: " << name + << " on port " << port << LL_ENDL; +} + +LLScriptEditorWSServer::ptr_t LLScriptEditorWSServer::getServer() +{ + if (!LLWebsocketMgr::instanceExists()) + { + return nullptr; + } + LLWebsocketMgr& wsmgr = LLWebsocketMgr::instance(); + return std::static_pointer_cast<LLScriptEditorWSServer>( + wsmgr.findServerByName(LLScriptEditorWSServer::DEFAULT_SERVER_NAME)); +} + + +LLWebsocketMgr::WSConnection::ptr_t LLScriptEditorWSServer::connectionFactory(LLWebsocketMgr::WSServer::ptr_t server, + LLWebsocketMgr::connection_h handle) +{ + auto connection = std::make_shared<LLScriptEditorWSConnection>(server, handle); + mActiveConnections[connection->getConnectionID()] = connection; + + // Call setupConnectionMethods to register any global methods + setupConnectionMethods(connection); + + 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->broadcastLanguageChange(); + } + }); +} + +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 + LLJSONRPCServer::onConnectionOpened(connection); + + LL_INFOS("ScriptEditorWS") << "New script editor client connected via JSON-RPC" << LL_ENDL; + +} + +void LLScriptEditorWSServer::onConnectionClosed(const LLWebsocketMgr::WSConnection::ptr_t& connection) +{ + // Call parent class to handle JSON-RPC cleanup + LLJSONRPCServer::onConnectionClosed(connection); + + LL_INFOS("ScriptEditorWS") << "Script editor client disconnected" << LL_ENDL; + + // Remove from active connections + auto script_connection = std::dynamic_pointer_cast<LLScriptEditorWSConnection>(connection); + if (script_connection) + { + U32 connection_id = script_connection->getConnectionID(); + unsubscribeConnection(connection_id); + mActiveConnections.erase(connection_id); + + LL_DEBUGS("ScriptEditorWS") << "Removed connection from active connections. Total: " + << mActiveConnections.size() << LL_ENDL; + // TODO: When connections reach 0, stop the server after a timeout. + } +} + +bool LLScriptEditorWSServer::subscribeScriptEditor(const LLUUID& object_id, const LLUUID& item_id, std::string_view script_name, + const LLHandle<LLPanel>& editor_handle, const std::string& script_id) +{ + if (!editor_handle.isDead()) + { + auto it = mSubscriptions.find(script_id); + if (it == mSubscriptions.end()) + { // Don't re-add if already subscribed + mSubscriptions.emplace(script_id, + LLScriptEditorWSServer::EditorSubscription(object_id, item_id, script_name, editor_handle)); + return false; + } + else + { // Update existing subscription with new editor handle + it->second.mEditorHandle = editor_handle; + } + return true; + } + return false; +} + +void LLScriptEditorWSServer::unsubscribeEditor(const std::string &script_id) +{ + auto it = mSubscriptions.find(script_id); + if (it != mSubscriptions.end()) + { + S32 connection_id = it->second.mConnectionID; + auto connection = it->second.mConnection.lock(); + mSubscriptions.erase(it); + ptrdiff_t count = std::count_if(mSubscriptions.begin(), mSubscriptions.end(), [connection_id](const auto& pair) { + return pair.second.mConnectionID == connection_id; + }); + if (connection && !count) + { // We have removed the last subscription, close the connection + LL_DEBUGS("ScriptEditorWS") << "Closing connection ID " << connection_id << + " as last subscription was removed" << LL_ENDL; + connection->sendDisconnect(LLScriptEditorWSConnection::REASON_EDITOR_CLOSED, "Editor closed"); + } + + } +} + +void LLScriptEditorWSServer::unsubscribeConnection(U32 connection_id) +{ + for (auto it = mSubscriptions.begin(); it != mSubscriptions.end(); ++it) + { + if (it->second.mConnectionID == connection_id) + { + LL_DEBUGS("ScriptEditorWS") << "Unsubscribing script " << it->first + << " from connection ID " << connection_id << LL_ENDL; + it->second.mConnectionID = 0; + it->second.mConnection.reset(); + } + } +} + +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 = mSubscriptions.find(script_id); + if (it != mSubscriptions.end()) + { + return it->second.mEditorHandle; + } + return LLHandle<LLPanel>(); +} + +std::set<std::string> LLScriptEditorWSServer::getActiveScripts() const +{ + std::set<std::string> active_scripts; + for (const auto& [script_id, subinfo] : mSubscriptions) + { + if (!subinfo.mEditorHandle.isDead()) + { + active_scripts.insert(script_id); + } + } + return active_scripts; +} + +void LLScriptEditorWSServer::setupConnectionMethods(LLJSONRPCConnection::ptr_t connection) +{ + // 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("script.list", + [that](const std::string&, const LLSD&, const LLSD& params) -> LLSD + { + auto server = that.lock(); + if (server) + { + return server->handleFileWatcherFileListRequest(); + } + return LLSD(); + }); + // script_connection->registerMethod("language.syntax", ) + } +} + +void LLScriptEditorWSServer::broadcastLanguageChange() +{ + LLUUID syntax_id = LLSyntaxIdLSL::instance().getSyntaxID(); + + if (syntax_id != mLastSyntaxId) + { + mLastSyntaxId = syntax_id; + LLSD params; + params["id"] = syntax_id; + + if (isRunning()) + { + broadcastNotification("language.syntax.change", params); + } + } +} + +LLSD LLScriptEditorWSServer::handleLanguageIdRequest() const +{ + LLSD response; + + response["id"] = mLastSyntaxId; + return response; +} + +LLSD LLScriptEditorWSServer::handleSyntaxRequest(const LLSD& params) const +{ + LLSD response(LLSD::emptyMap()); + std::string category = params["kind"].asString(); + + if (category.empty()) + { + response["error"] = "No syntax category specified"; + response["success"] = false; + return response; + } + + response["id"] = mLastSyntaxId; + if (category == "defs.lua") + { + response["defs"] = LLSyntaxLua::instance().getTypesXML(); + response["success"] = response["defs"].isDefined(); + } + else if (category == "defs.lsl") + { + response["defs"] = LLSyntaxIdLSL::instance().getKeywordsXML(); + response["success"] = response["defs"].isDefined(); + } + else + { + response["error"] = "Unknown syntax category requested"; + response["success"] = false; + } + return response; +} + +LLSD LLScriptEditorWSServer::handleScriptSubscribe(U32 connection_id, const LLSD& params) +{ + LLSD response(LLSD::emptyMap()); + + 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) + { + 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; + } + + if (result == SUBSCRIPTION_SUCCESS) + { + auto it = mSubscriptions.find(script_id); + if (it != mSubscriptions.end()) + { + LLViewerObject* object = gObjectList.findObject((*it).second.mObjectID); + response["object_id"] = (*it).second.mObjectID; + //response["object_name"] = object ? object->getName() : "Unknown"; + response["item_id"] = (*it).second.mItemID; + } + } + + 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(); +} + +LLSD LLScriptEditorWSServer::handleFileWatcherFileListRequest() const +{ + LLSD response; + + response["temp_dir"] = LLFile::tmpdir(); + + // Add array of script_id's from active scripts + LLSD script_ids_array = LLSD::emptyArray(); + for (const auto& [script_id, subinfo] : mSubscriptions) + { + script_ids_array.append(script_id); + } + response["script_ids"] = script_ids_array; + + response["success"] = true; + + return response; +} + +void LLScriptEditorWSServer::notifyScript(const std::string& script_id, const std::string &method, const LLSD& message) const +{ + auto it = mSubscriptions.find(script_id); + if (it != mSubscriptions.end()) + { + auto connection = it->second.mConnection.lock(); + if (connection) + { + connection->notify(method, message); + } + } +} + + +void LLScriptEditorWSServer::sendUnsubscribeScriptEditor(const std::string& script_id) +{ + LLSD params; + params["script_id"] = script_id; + + notifyScript(script_id, "script.unsubscribe", params); +} + +void LLScriptEditorWSServer::sendCompileResults(const std::string &script_id, const LLSD &results) const +{ + LLHandle<LLPanel> editor_handle = findEditorForScript(script_id); + if (editor_handle.isDead()) + { + return; + } + LLScriptEdContainer* editor = dynamic_cast<LLScriptEdContainer*>(editor_handle.get()); + if (!editor) + { + return; + } + LLScriptEdCore* core = editor->getScriptEdCore(); + bool is_lua = core && (core->isLuauLanguage()); + + LLSD params; + params["script_id"] = script_id; + params["success"] = results["compiled"].asBoolean(); + params["running"] = results["is_running"].asBoolean(); + if (results.has("errors")) + { + params["errors"] = LLSD::emptyArray(); + + if (is_lua) + { // lua errors: ":line: message", line is 1-based + const static boost::regex lua_err_regex(R"(^[^:]*:(\d+): (.+)$)"); + + for (const auto& err : llsd::inArray(results["errors"])) + { + boost::smatch match; + LLSD err_entry; + + err_entry["column"] = 0; // TODO: Lua compiler does not provide column info + err_entry["level"] = "ERROR"; + + if (boost::regex_match(err.asString(), match, lua_err_regex)) + { + S32 line_number = std::stoi(match[1].str()); + std::string message = match[2].str(); + + err_entry["row"] = line_number; + err_entry["message"] = message; + } + else + { + err_entry["row"] = 0; + err_entry["message"] = err.asString(); + } + params["errors"].append(err_entry); + } + } + else + { // lsl errors: "(line, column) : SEVERITY : message", line and column are 0-based + static const boost::regex lsl_err_regex(R"(\((\d+), (\d+)\) : ([^:]+) : (.+))"); + + for (const auto& err : llsd::inArray(results["errors"])) + { + boost::smatch match; + LLSD err_entry; + + if (boost::regex_match(err.asString(), match, lsl_err_regex)) + { + S32 line_number = std::stoi(match[1].str()); + S32 col_number = std::stoi(match[2].str()); + std::string severity = match[3].str(); + std::string message = match[4].str(); + + err_entry["row"] = line_number + 1; + err_entry["column"] = col_number + 1; + err_entry["level"] = severity; + err_entry["message"] = message; + err_entry["format"] = "lsl"; + } + else + { + err_entry["row"] = 0; + err_entry["column"] = 0; + err_entry["level"] = "ERROR"; + err_entry["message"] = err.asString(); + err_entry["format"] = "lsl"; + } + params["errors"].append(err_entry); + } + } + } + + notifyScript(script_id, "script.compiled", params); +} + +void LLScriptEditorWSServer::forwardChatToIDE(const LLChat& chat_msg) const +{ + auto it = std::find_if(mSubscriptions.begin(), mSubscriptions.end(), + [&chat_msg](const auto& pair) { return (pair.second.mObjectID == chat_msg.mFromID); }); + + if (it == mSubscriptions.end()) + { // Not a script we are tracking + return; + } + + bool is_error = false; + std::string error_message; + std::string object_name; + std::string script_name; + S32 line_number = 0; + // We have at least one script from this object, we will forward the message to the IDE + // but first we need to see if it is a runtime error + std::vector<std::string> lines = LLStringUtil::getTokens(chat_msg.mText, "\n"); + // If this is a runtime error, the first line will look like: "<Object Name> [script:<Script Name>] Script run-time error" + static const std::string runtime_error_marker = "Script run-time error"; + if (!lines.empty() && std::equal(runtime_error_marker.rbegin(), runtime_error_marker.rend(), lines.front().rbegin())) + { + is_error = true; + std::string first_line = lines.front(); + + // Extract the object and script name from the first line + static const boost::regex RUNTIME_ERR_REGEX_FLEX(R"(^(.+?)\s+\[script:([^\]]+)\]\s+Script run-time error)"); + boost::smatch m; + + S32 remove_count = 0; + if (boost::regex_match(first_line, m, RUNTIME_ERR_REGEX_FLEX)) + { + object_name = m[1].str(); + script_name = m[2].str(); + remove_count++; + } + + // TODO: Build an actual error message to forward to the external editor + // Explaination: + // Well! Heck! + // As it turns out, the complete error message arrives as either two or three + // separate chat messages from the server. + // 2 if the script is LSL or if it is Lua but not owned by the editing agent + // 3 if the script is Lua and owned by the editing agent. + // + // Message 1: <Object Name> [script:<Script Name>] Script run-time error + // Message 2: <runtime error> + // Message 3: <script>:<line>: <actual error message>\n + // <call stack> + // + // These need to be compositited into a single error message to send to the IDE. + // + //if (lines.size() > 1) + //{ // The second line is the actual error message + // error_message = lines[1]; + // remove_count++; + // if ((error_message == "runtime error") && (lines.size() > 2)) + // { // If the error message is just "runtime error", the next line might actually be the real message: + // // "lua_script:7: attempt to perform arithmetic (sub) on nil" + // static const boost::regex LUA_ERROR_REGEX(R"(^(.+?):(\d+):\s*(.+)$)"); + // + // if (boost::regex_match(first_line, m, RUNTIME_ERR_REGEX_FLEX)) + // { + // line_number = std::stoi(m[2].str()); + // error_message = m[3].str(); + // remove_count++; + // } + // } + // else + // { + // error_message = "Unknown script runtime error"; + // } + //} + //else + //{ + // error_message = "Unknown script runtime error"; + //} + if (lines.size() > remove_count) + { // The rest of the lines may contain a stack trace + lines.erase(lines.begin(), lines.begin() + remove_count); + } + else + { + lines.clear(); + } + + // We should also check that the script name matches one of our subscriptions + if (!script_name.empty() && (it->second.mScriptName != script_name)) + { // right object, wrong script + auto sit = std::find_if(mSubscriptions.begin(), mSubscriptions.end(), + [&chat_msg, &script_name](const auto& pair) + { + return (pair.second.mScriptName == script_name) && (pair.second.mObjectID == chat_msg.mFromID); + }); + if (sit != mSubscriptions.end()) + { // We have a better match + it = sit; + } + } + } + std::string script_id = it->first; + LLSD message; + message["script_id"] = script_id; + message["object_id"] = chat_msg.mFromID; + message["object_name"] = chat_msg.mFromName; + message["message"] = chat_msg.mText; + + if (is_error) + { + message["error"] = error_message; + message["line"] = line_number; + if (!lines.empty()) + { + message["stack"] = LLSD::emptyArray(); + for (const auto& line : lines) + { + message["stack"].append(line); + } + } + } + + if (!it->second.mConnection.expired()) + { + it->second.mConnection.lock()->notify(is_error ? "runtime.error" : "runtime.debug", message); + } +} + +//======================================================================== +U32 LLScriptEditorWSConnection::sNextConnectionID = 1; + +std::shared_ptr<LLScriptEditorWSServer> LLScriptEditorWSConnection::getServer() const +{ + return std::static_pointer_cast<LLScriptEditorWSServer>(mOwningServer.lock()); +} + +void LLScriptEditorWSConnection::onOpen() +{ + // Call parent class to set up JSON-RPC infrastructure + LLJSONRPCConnection::onOpen(); + + LL_INFOS("ScriptEditorWS") << "Script editor JSON-RPC connection opened" << LL_ENDL; + + // 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(); + + handshake["agent_id"] = gAgent.getID(); + handshake["agent_name"] = gAgentUsername; + + std::string challenge_file = generateChallenge(); + if (!challenge_file.empty()) + { + handshake["challenge"] = challenge_file; + } + + LLSD languages = LLSD::emptyArray(); + languages.append("lsl"); + languages.append("luau"); + handshake["languages"] = languages; + handshake["syntax_id"] = LLSyntaxIdLSL::instance().getSyntaxID(); + + // Features object + LLSD features; + features["live_sync"] = true; + features["compilation"] = true; + handshake["features"] = features; + + 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()) + { + auto self = that.lock(); + if (self) + { + self->handleHandshakeResponse(result); + } + } + else + { + LL_WARNS("ScriptEditorWS") << "Handshake failed: " + << error["message"].asString() << LL_ENDL; + } + }); + + LL_INFOS("ScriptEditorWS") << "Sent handshake call to new editor client" << LL_ENDL; +} + +void LLScriptEditorWSConnection::onClose() +{ + // Call parent class to clean up JSON-RPC infrastructure + LLJSONRPCConnection::onClose(); + mOwningServer.reset(); + + // Clean up handshake response data + mClientName.clear(); + mClientVersion.clear(); + mProtocolVersion.clear(); + mScriptName.clear(); + mScriptLanguage.clear(); + mLanguages.clear(); + mFeatures.clear(); +} + +void LLScriptEditorWSConnection::sendDisconnect(S32 reason, const std::string& message) +{ + LL_INFOS("ScriptEditorWS") << "Sending disconnect to client: " << message << LL_ENDL; + LLSD params; + params["reason"] = reason; + params["message"] = message; + notify("session.disconnect", params); + closeConnection(1000, message); +} + +void LLScriptEditorWSConnection::handleHandshakeResponse(const LLSD& result) +{ + LL_INFOS("ScriptEditorWS") << "Processing handshake response from client" << LL_ENDL; + + // Extract and validate client information + mClientName = result["client_name"].asString(); + mClientVersion = result["client_version"].asString(); + mProtocolVersion = result["protocol_version"].asString(); + + if (mChallenge.notNull()) + { + // Validate challenge response + bool valid_response = (result.has("challenge_response") && + (result["challenge_response"].asUUID() == mChallenge)); + + LLFile::remove(mChallengeFile); + mChallengeFile.clear(); + mChallenge.setNull(); + if (!valid_response) + { + LL_WARNS("ScriptEditorWS") << "Invalid or missing challenge response from client" << LL_ENDL; + sendDisconnect(REASON_PROTOCOL_ERROR, "Invalid challenge response"); + return; + } + } + LLUUID challenge_response = result["challenge_response"].asUUID(); + + // Validate protocol compatibility + if (mProtocolVersion != "1.0") + { + LL_WARNS("ScriptEditorWS") << "Protocol version mismatch. Expected: 1.0, Got: " + << mProtocolVersion << LL_ENDL; + } + + // Store script information if provided + mScriptName = result["script_name"].asString(); + mScriptLanguage = result["script_language"].asString(); + + // Store supported languages + for (const auto& lang : llsd::inArray( result["languages"])) + { + if (lang.isString()) + { + mLanguages.insert(lang.asString()); + } + } + + for (const auto& [feature, enabled] : llsd::inMap(result["features"])) + { + if (enabled.asBoolean()) + { + mFeatures.insert(feature); + } + } + + if (mChallenge.notNull()) + { + // Remove temporary challenge file + LLFile::remove(mChallengeFile); + mChallenge.setNull(); + mChallengeFile.clear(); + } + + notify("session.ok"); + + LL_INFOS("ScriptEditorWS") << "Handshake completed successfully." << LL_ENDL; +} + +std::string LLScriptEditorWSConnection::generateChallenge() +{ + mChallenge.generate(); + + mChallengeFile = std::string(LLFile::tmpdir()) + "sl_script_challenge.tmp"; + + llofstream file(mChallengeFile.c_str()); + if (!file.is_open()) + { + LL_WARNS() << "Unable to open challenge file: " << mChallengeFile << LL_ENDL; + mChallenge.setNull(); + mChallengeFile.clear(); + return std::string(); + } + + file << mChallenge; + file.close(); + + return mChallengeFile; +} |
