summaryrefslogtreecommitdiff
path: root/indra/newview/llscripteditorws.cpp
diff options
context:
space:
mode:
authorJonathan "Geenz" Goodman <geenz@lindenlab.com>2026-08-25 13:21:03 -0400
committerGitHub <noreply@github.com>2026-08-25 13:21:03 -0400
commitf6f5cd0f888dd8ec53821aa4f26e307b51465da0 (patch)
treeaddb875c9bc2b217d39d7d6fd4e4e0a5ea333745 /indra/newview/llscripteditorws.cpp
parent82caf452cdaa59357a25d0844afe0012419beed2 (diff)
parent2e9c387fee881022aec13d5c85f901539683c18f (diff)
Merge pull request #3537 from secondlife/project/lua_editor
Alpha Viewer: Lua editor
Diffstat (limited to 'indra/newview/llscripteditorws.cpp')
-rw-r--r--indra/newview/llscripteditorws.cpp2713
1 files changed, 2713 insertions, 0 deletions
diff --git a/indra/newview/llscripteditorws.cpp b/indra/newview/llscripteditorws.cpp
new file mode 100644
index 0000000000..f886be819f
--- /dev/null
+++ b/indra/newview/llscripteditorws.cpp
@@ -0,0 +1,2713 @@
+/**
+ * @file llscripteditorws.cpp
+ * @brief JSON-RPC 2.0 WebSocket server implementation for external script editor integration
+ *
+ * For a full description of the JSON-RPC protocol and all supported methods,
+ * see doc/external-editor-json-rpc.md in the repository root.
+ *
+ * $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 "llagent.h"
+#include "llagentcamera.h"
+#include "llappviewer.h"
+#include "llchat.h"
+#include "lldate.h"
+#include "llerror.h"
+#include "lleventcoro.h"
+#include "lleventfilter.h"
+#include "llevents.h"
+#include "llfilesystem.h"
+#include "llfloaterperms.h"
+#include "llfloaterreg.h"
+#include "llinventorytype.h"
+#include "llinventorydefines.h"
+#include "llnotecard.h"
+#include "llnotificationsutil.h"
+#include "llpreviewnotecard.h"
+#include "llpreviewscript.h"
+#include "llprocess.h"
+#include "llregex.h"
+#include "llmd5.h"
+#include "llsdjson.h"
+#include "llselectmgr.h"
+#include "lltrans.h"
+#include "lluuid.h"
+#include "llversioninfo.h"
+#include "llviewerassetstorage.h"
+#include "llviewerassettype.h"
+#include "llviewerassetupload.h"
+#include "llviewercontrol.h"
+#include "llviewerinventory.h"
+#include "llviewerobject.h"
+#include "llviewerobjectlist.h"
+#include "llviewerregion.h"
+#include "llviewermenu.h"
+#include "llviewertexteditor.h"
+#include "llvoinventorylistener.h"
+#include "roles_constants.h"
+
+#include <array>
+
+namespace
+{
+ // Per-operation timeouts (seconds) for coroutine-based async RPC handlers.
+ constexpr F32 ASSET_FETCH_TIMEOUT = 30.0f;
+ constexpr F32 SCRIPT_UPLOAD_TIMEOUT = 60.0f;
+ constexpr F32 NOTECARD_UPLOAD_TIMEOUT = 30.0f;
+ constexpr F32 ITEM_CREATE_TIMEOUT = 30.0f;
+
+ // Linkset flush coalescing delays (seconds).
+ constexpr F32 LINKSET_ADD_FLUSH_DELAY = 5.0f;
+ constexpr F32 LINKSET_REMOVE_FLUSH_DELAY = 0.2f;
+
+ static const boost::regex LUAU_LOCATION_PATTERN(
+ R"(^([^:]*):([0-9]+):\s*(.*)$)");
+
+ static const boost::regex LSL_LOCATION_PATTERN(
+ R"(\((\d+), (\d+)\) : ([^:]+) : (.+))");
+
+ // Creates a uniquely-named LLEventMailDrop under "<prefix>.<uuid>", passes
+ // its name to kickoff (which arranges for one post to that pump), then
+ // suspends the current coroutine up to imeout seconds for the result.
+ // Throws RequestTimeoutError(timeout_msg) if the deadline elapses.
+ template <typename Kickoff>
+ LLSD await_async_result(const std::string& pump_prefix,
+ F32 timeout,
+ const std::string& timeout_msg,
+ Kickoff&& kickoff)
+ {
+ LLEventMailDrop pump(pump_prefix + "." + LLUUID::generateNewID().asString(), true);
+ std::string pump_name = pump.getName();
+ std::forward<Kickoff>(kickoff)(pump_name);
+ LLSD result = llcoro::suspendUntilEventOnWithTimeout(
+ pump, timeout, LLSD().with("timeout", true));
+ if (result.has("timeout"))
+ {
+ throw LLJSONRPCConnection::RequestTimeoutError(timeout_msg);
+ }
+ return result;
+ }
+
+ // Builds the (success, failure) callback pair used by LLResourceUploadInfo-
+ // derived uploads. Both outcomes post a single LLSD to pump_name:
+ // - success: the server's response LLSD with item_id/task_id added.
+ // - failure: { "failed": true, "reason": <reason> }.
+ auto make_asset_upload_callbacks(const std::string& pump_name)
+ {
+ auto on_success = [pump_name](LLUUID item_id, LLUUID task_id, LLUUID /*new_asset_id*/, LLSD response)
+ {
+ response["item_id"] = item_id;
+ response["task_id"] = task_id;
+ LLEventPumps::instance().post(pump_name, response);
+ };
+ auto on_failure = [pump_name](LLUUID /*item_id*/, LLUUID /*task_id*/, LLSD /*response*/, std::string reason)
+ {
+ LLSD failure;
+ failure["failed"] = true;
+ failure["reason"] = reason;
+ LLEventPumps::instance().post(pump_name, failure);
+ return false;
+ };
+ return std::make_pair(std::move(on_success), std::move(on_failure));
+ }
+
+ // Returns [root, *root->getChildren()] in stable order. Root must be non-null.
+ std::vector<LLViewerObject*> collect_linkset(LLViewerObject* root)
+ {
+ std::vector<LLViewerObject*> prims;
+ const auto& children = root->getChildren();
+ prims.reserve(1 + children.size());
+ prims.push_back(root);
+ for (LLViewerObject* child : children)
+ {
+ prims.push_back(child);
+ }
+ return prims;
+ }
+
+ // Returns the value of NV pair key on obj as a string, or empty if
+ // obj / pair / string is null or empty. NUL-safe.
+ std::string nv_string(LLViewerObject* obj, const char* key)
+ {
+ if (!obj)
+ {
+ return std::string();
+ }
+ LLNameValue* nv = obj->getNVPair(key);
+ if (!nv)
+ {
+ return std::string();
+ }
+ const char* s = nv->getString();
+ if (!s || s[0] == '\0')
+ {
+ return std::string();
+ }
+ return std::string(s);
+ }
+
+}
+
+//========================================================================
+LLScriptEditorWSServer::LLScriptEditorWSServer(const std::string& name, U16 port, bool local_only):
+ LLJSONRPCServer(name, port, local_only),
+ mPublishedObjectManager(
+ this,
+ [this](const LLPublishedObjectMgr::RuntimeChatEvent& event)
+ {
+ sendRuntimeEvent(event);
+ })
+{
+ LL_INFOS("ScriptEditorWS") << "Created JSON-RPC script editor server: " << name
+ << " on port " << port << LL_ENDL;
+
+ registerCommand({ "viewer.teleport", "Teleport agent to an in-world object" },
+ [](U32, const LLSD& p) -> LLSD
+ {
+ LLUUID object_id = p["object_id"].asUUID();
+ if (object_id.isNull())
+ throw LLJSONRPCConnection::InvalidParams("object_id is required");
+
+ LLViewerObject* object = gObjectList.findObject(object_id);
+ if (!object)
+ throw LLJSONRPCConnection::InvalidParams("object_id not found");
+
+ LLVector3d global_pos = object->getPositionGlobal();
+ gAgent.teleportViaLocation(global_pos);
+
+ LLSD response;
+ response["success"] = true;
+ return response;
+ });
+
+ registerCommand({ "viewer.camera.focus", "Zoom camera to an in-world object (same behavior as context menu Zoom In)" },
+ [](U32, const LLSD& p) -> LLSD
+ {
+ LLUUID object_id = p["object_id"].asUUID();
+ if (object_id.isNull())
+ throw LLJSONRPCConnection::InvalidParams("object_id is required");
+
+ if (!handle_zoom_to_object(object_id))
+ {
+ throw LLJSONRPCConnection::InternalError(
+ "Object not found or not reachable");
+ }
+
+ LLSD response;
+ response["success"] = true;
+ return response;
+ });
+
+ registerCommand({ "viewer.object.save_back_to_contents", "Save an in-world object back to source object contents" },
+ [this](U32 connection_id, const LLSD& p) -> LLSD
+ {
+ return this->handleSaveBackToObjectContents(connection_id, p);
+ });
+}
+
+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));
+}
+
+bool LLScriptEditorWSServer::isEnabled()
+{
+ static LLCachedControl<bool> OPT_ENABLESCRIPTEDITORWS(gSavedSettings, "ExternalWebsocketSyncEnable", false);
+ return OPT_ENABLESCRIPTEDITORWS;
+}
+
+bool LLScriptEditorWSServer::isTightIntegration()
+{
+ static LLCachedControl<bool> OPT_TIGHTINTEGRATION(gSavedSettings, "ExternalWebsocketSyncTightIntegration", false);
+ return OPT_TIGHTINTEGRATION;
+}
+
+LLScriptEditorWSServer::ptr_t LLScriptEditorWSServer::ensureServerRunning()
+{
+ if (!LLScriptEditorWSServer::isEnabled())
+ {
+ LL_DEBUGS("ScriptEditorWS") << "WebSocket server is disabled by ExternalWebsocketSyncEnable" << LL_ENDL;
+ return nullptr;
+ }
+
+ LLWebsocketMgr& wsmgr = LLWebsocketMgr::instance();
+ ptr_t server = std::static_pointer_cast<LLScriptEditorWSServer>(
+ wsmgr.findServerByName(DEFAULT_SERVER_NAME));
+
+ if (!server)
+ {
+ U16 port = static_cast<U16>(gSavedSettings.getS32("ExternalWebsocketSyncPort"));
+ bool local_only = gSavedSettings.getBOOL("ExternalWebsocketSyncLocal");
+ server = std::make_shared<LLScriptEditorWSServer>(DEFAULT_SERVER_NAME, port, local_only);
+ wsmgr.addServer(server);
+ }
+
+ if (!server->isRunning())
+ {
+ U16 port = static_cast<U16>(gSavedSettings.getS32("ExternalWebsocketSyncPort"));
+ LLSD args;
+ args["PORT"] = static_cast<S32>(port);
+
+ if (!wsmgr.startServer(DEFAULT_SERVER_NAME))
+ {
+ LL_WARNS("ScriptEditorWS") << "Failed to start script editor websocket server" << LL_ENDL;
+ LLNotificationsUtil::add("ExternalEditorServerFailed", args);
+ return nullptr;
+ }
+
+ LLNotificationsUtil::add("ExternalEditorServerStarted", args);
+ }
+
+ return server;
+}
+
+std::string LLScriptEditorWSServer::buildScriptSubscriptionId(const LLUUID& object_id,
+ const LLUUID& item_id)
+{
+ std::string script_id = object_id.asString() + "_" + item_id.asString();
+
+ std::array<char, MD5HEX_STR_SIZE> script_id_hash_str = {};
+ LLMD5 script_id_hash((const U8*)script_id.c_str());
+ script_id_hash.hex_digest(script_id_hash_str.data());
+
+ return std::string(script_id_hash_str.data());
+}
+
+std::string LLScriptEditorWSServer::buildVSCodeURI(const LLUUID& object_id,
+ const LLUUID& script_id)
+{
+ std::ostringstream uri;
+ uri << "vscode://lindenlab.sl-vscode-plugin/connect";
+
+ U16 port = static_cast<U16>(gSavedSettings.getS32("ExternalWebsocketSyncPort"));
+ uri << "?port=" << port;
+
+ if (object_id.notNull())
+ {
+ uri << "&object=" << object_id.asString();
+ }
+
+ if (script_id.notNull())
+ {
+ uri << "&script=" << script_id.asString();
+ }
+
+ return uri.str();
+}
+
+bool LLScriptEditorWSServer::launchVSCode(const LLUUID& object_id,
+ const LLUUID& script_id)
+{
+ ptr_t server = ensureServerRunning();
+ if (!server)
+ {
+ LL_WARNS("ScriptEditorWS") << "Cannot launch VS Code: WebSocket server failed to start" << LL_ENDL;
+ return false;
+ }
+
+ std::string uri = buildVSCodeURI(object_id, script_id);
+
+ LLProcess::Params params;
+#if LL_WINDOWS
+ // On Windows, VS Code's 'code' is a batch file (.cmd) which APR cannot
+ // launch directly. Invoke it through cmd.exe instead.
+ // The URI may contain '&' which cmd.exe treats as a command separator,
+ // so the entire argument list is passed as a single quoted string.
+ params.executable = "cmd.exe";
+ params.args.add("/c");
+ params.args.add("code --open-url \"" + uri + "\"");
+#else
+ params.executable = "code";
+ params.args.add("--open-url");
+ params.args.add(uri);
+#endif
+ params.autokill = false;
+
+ LLProcessPtr process = LLProcess::create(params);
+ if (!process)
+ {
+ LL_WARNS("ScriptEditorWS") << "Failed to launch VS Code. "
+ << "Ensure the 'code' command is available on your PATH." << LL_ENDL;
+ return false;
+ }
+
+ LL_INFOS("ScriptEditorWS") << "Launched VS Code with URI: " << uri << LL_ENDL;
+ return true;
+}
+
+
+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()
+{
+ LLSyntaxDefCache& syntax_id_mgr = LLSyntaxDefCache::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();
+
+ // Connections are already closed -- clean up all internal state silently.
+ // Do not attempt to send notifications; the sockets are gone.
+
+ mPublishedObjectManager.clearAllStateWithListenerCleanup();
+
+ mSubscriptions.clear();
+ mActiveConnections.clear();
+
+ LL_INFOS("ScriptEditorWS") << "Script editor WebSocket server stopped, all state cleaned up" << LL_ENDL;
+
+ LLNotificationsUtil::add("ExternalEditorServerStopped");
+}
+
+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())
+ {
+ return false;
+ }
+
+ auto it = mSubscriptions.find(script_id);
+ if (it == mSubscriptions.end())
+ {
+ // New subscription
+ ItemRef item_ref;
+ item_ref.mPrimID = object_id;
+ item_ref.mItemID = item_id;
+ item_ref.mScriptName = script_name;
+ mSubscriptions.emplace(script_id,
+ EditorSubscription(item_ref, editor_handle));
+ }
+ else
+ {
+ // Refresh existing subscription with the new editor handle
+ it->second.mEditorHandle = editor_handle;
+ }
+ return true;
+}
+
+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);
+
+ if (connection_id != 0)
+ {
+ auto cit = mConnectionSubscriptionCounts.find(connection_id);
+ if (cit != mConnectionSubscriptionCounts.end())
+ {
+ if (--cit->second <= 0)
+ {
+ mConnectionSubscriptionCounts.erase(cit);
+ }
+ }
+ }
+
+ }
+}
+
+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();
+ }
+ }
+ // All subs for this connection now have mConnectionID == 0.
+ mConnectionSubscriptionCounts.erase(connection_id);
+}
+
+LLScriptEditorWSServer::SubscriptionError 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 SubscriptionError::INVALID_EDITOR;
+ }
+
+ auto con_it = mActiveConnections.find(connection_id);
+ if (con_it == mActiveConnections.end())
+ {
+ return SubscriptionError::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 SubscriptionError::ALREADY_SUBSCRIBED;
+ }
+
+ // If this entry was previously bound to a different (dead) connection,
+ // it would have been cleared by unsubscribeConnection, so mConnectionID
+ // is always 0 here.
+ it->second.mConnectionID = connection_id;
+ it->second.mConnection = con_it->second;
+ ++mConnectionSubscriptionCounts[connection_id];
+ return SubscriptionError::SUCCESS;
+ }
+ return SubscriptionError::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;
+ U32 connection_id = script_connection->getConnectionID();
+
+ // Sync methods (run on the WebSocket I/O thread; must not touch
+ // main-thread-only viewer state).
+ script_connection->registerMethod("language.syntax.id",
+ bindHandler([](LLScriptEditorWSServer& s, auto&, auto&, auto&)
+ {
+ return s.handleLanguageIdRequest();
+ }));
+
+ script_connection->registerMethod("language.syntax",
+ bindHandler([](LLScriptEditorWSServer& s, auto&, auto&, const LLSD& params)
+ {
+ return s.handleSyntaxRequest(params);
+ }));
+
+ script_connection->registerMethod("language.syntax.cache",
+ bindHandler([](LLScriptEditorWSServer& s, auto&, auto&, auto&)
+ {
+ return s.handleSyntaxCacheRequest();
+ }));
+
+ script_connection->registerMethod("language.syntax.get",
+ bindHandler([](LLScriptEditorWSServer& s, auto&, auto&, const LLSD& params)
+ {
+ return s.handleSyntaxCacheFileRequest(params);
+ }));
+
+ script_connection->registerAsyncMethod("script.subscribe",
+ bindHandler([connection_id](LLScriptEditorWSServer& s, auto&, auto&, const LLSD& params)
+ {
+ return s.handleScriptSubscribe(connection_id, params);
+ }));
+
+ script_connection->registerMethod("script.list",
+ bindHandler([](LLScriptEditorWSServer& s, auto&, auto&, auto&)
+ {
+ return s.handleFileWatcherFileListRequest();
+ }));
+
+ script_connection->registerAsyncMethod("object.unpublish",
+ bindHandler([connection_id](LLScriptEditorWSServer& s, auto&, auto&, const LLSD& params)
+ {
+ return s.handleObjectUnpublish(connection_id, params);
+ }));
+
+ // Async methods (dispatched to the main thread inside a coroutine).
+ script_connection->registerAsyncMethod("script.unsubscribe",
+ bindHandler([connection_id](LLScriptEditorWSServer& s, auto&, auto&, const LLSD& params)
+ {
+ return s.handleScriptUnsubscribe(connection_id, params);
+ }));
+
+ script_connection->registerAsyncMethod("object.request",
+ bindHandler([connection_id](LLScriptEditorWSServer& s, auto&, auto&, const LLSD& params)
+ {
+ return s.handleObjectRequest(connection_id, params);
+ }));
+
+ script_connection->registerAsyncMethod("object.content.get",
+ bindHandler([](LLScriptEditorWSServer& s, const std::string& method, const LLSD& id, const LLSD& params)
+ {
+ return s.handleObjectContentGet(method, id, params);
+ }));
+
+ script_connection->registerAsyncMethod("object.content.save",
+ bindHandler([](LLScriptEditorWSServer& s, const std::string& method, const LLSD& id, const LLSD& params)
+ {
+ return s.handleObjectContentSave(method, id, params);
+ }));
+
+ script_connection->registerAsyncMethod("object.item.delete",
+ bindHandler([connection_id](LLScriptEditorWSServer& s, auto&, auto&, const LLSD& params)
+ {
+ return s.handleObjectItemDelete(connection_id, params);
+ }));
+
+ script_connection->registerAsyncMethod("object.item.create",
+ bindHandler([](LLScriptEditorWSServer& s, const std::string& method, const LLSD& id, const LLSD& params)
+ {
+ return s.handleObjectItemCreate(method, id, params);
+ }));
+
+ script_connection->registerAsyncMethod("object.list",
+ bindHandler([](LLScriptEditorWSServer& s, auto&, auto&, auto&)
+ {
+ return s.handleObjectList();
+ }));
+
+ script_connection->registerAsyncMethod("object.script.set_running",
+ bindHandler([connection_id](LLScriptEditorWSServer& s, auto&, auto&, const LLSD& params)
+ {
+ return s.handleObjectScriptSetRunning(connection_id, params);
+ }));
+
+ script_connection->registerAsyncMethod("object.script.reset",
+ bindHandler([connection_id](LLScriptEditorWSServer& s, auto&, auto&, const LLSD& params)
+ {
+ return s.handleObjectScriptReset(connection_id, params);
+ }));
+
+ script_connection->registerAsyncMethod("object.modify",
+ bindHandler([connection_id](LLScriptEditorWSServer& s, auto&, auto&, const LLSD& params)
+ {
+ return s.handleObjectModify(connection_id, params);
+ }));
+
+ script_connection->registerAsyncMethod("object.item.modify",
+ bindHandler([connection_id](LLScriptEditorWSServer& s, auto&, auto&, const LLSD& params)
+ {
+ return s.handleObjectItemModify(connection_id, params);
+ }));
+
+ script_connection->registerAsyncMethod("command.execute",
+ bindHandler([connection_id](LLScriptEditorWSServer& s, auto&, auto&, const LLSD& params)
+ {
+ return s.handleCommandExecute(connection_id, params);
+ }));
+
+ script_connection->registerMethod("command.list",
+ bindHandler([](LLScriptEditorWSServer& s, auto&, auto&, auto&)
+ {
+ return s.handleCommandList();
+ }));
+ }
+}
+
+LLSD LLScriptEditorWSServer::handleObjectList() const
+{
+ LLSD response;
+ response["objects"] = mPublishedObjectManager.buildObjectListLLSD();
+ return response;
+}
+
+LLSD LLScriptEditorWSServer::handleObjectScriptSetRunning(U32 connection_id, const LLSD& params)
+{
+ LLUUID prim_id = params["prim_id"].asUUID();
+ LLUUID item_id = params["item_id"].asUUID();
+ bool running = params["running"].asBoolean();
+
+ if (prim_id.isNull() || item_id.isNull())
+ throw LLJSONRPCConnection::InvalidParams("prim_id and item_id are required");
+
+ LLViewerObject* prim = gObjectList.findObject(prim_id);
+ if (!prim)
+ throw LLJSONRPCConnection::InvalidParams("Prim not found");
+
+ LLViewerObject* root = prim->getRootEdit();
+ if (!root || !isObjectPublished(root->getID()))
+ throw LLJSONRPCConnection::ForbiddenError("Object is not published");
+
+ LLInventoryItem* item = dynamic_cast<LLInventoryItem*>(prim->getInventoryObject(item_id));
+ if (!item)
+ throw LLJSONRPCConnection::InvalidParams("Script not found in prim inventory");
+
+ if (item->getType() != LLAssetType::AT_LSL_TEXT)
+ throw LLJSONRPCConnection::InvalidParams("Item is not a script");
+
+ if (!gAgent.allowOperation(PERM_MODIFY, item->getPermissions(), GP_OBJECT_MANIPULATE))
+ throw LLJSONRPCConnection::ForbiddenError("No modify permission on script");
+
+ // Send SetScriptRunning message to simulator
+ LLMessageSystem* msg = gMessageSystem;
+ msg->newMessageFast(_PREHASH_SetScriptRunning);
+ msg->nextBlockFast(_PREHASH_AgentData);
+ msg->addUUIDFast(_PREHASH_AgentID, gAgent.getID());
+ msg->addUUIDFast(_PREHASH_SessionID, gAgent.getSessionID());
+ msg->nextBlockFast(_PREHASH_Script);
+ msg->addUUIDFast(_PREHASH_ObjectID, prim_id);
+ msg->addUUIDFast(_PREHASH_ItemID, item_id);
+ msg->addBOOLFast(_PREHASH_Running, running);
+ msg->sendReliable(prim->getRegion()->getHost());
+
+ LLSD response;
+ response["success"] = true;
+ return response;
+}
+
+LLSD LLScriptEditorWSServer::handleObjectScriptReset(U32 connection_id, const LLSD& params)
+{
+ LLUUID prim_id = params["prim_id"].asUUID();
+ LLUUID item_id = params["item_id"].asUUID();
+
+ if (prim_id.isNull() || item_id.isNull())
+ throw LLJSONRPCConnection::InvalidParams("prim_id and item_id are required");
+
+ LLViewerObject* prim = gObjectList.findObject(prim_id);
+ if (!prim)
+ throw LLJSONRPCConnection::InvalidParams("Prim not found");
+
+ LLViewerObject* root = prim->getRootEdit();
+ if (!root || !isObjectPublished(root->getID()))
+ throw LLJSONRPCConnection::ForbiddenError("Object is not published");
+
+ LLInventoryItem* item = dynamic_cast<LLInventoryItem*>(prim->getInventoryObject(item_id));
+ if (!item)
+ throw LLJSONRPCConnection::InvalidParams("Script not found in prim inventory");
+
+ if (item->getType() != LLAssetType::AT_LSL_TEXT)
+ throw LLJSONRPCConnection::InvalidParams("Item is not a script");
+
+ if (!gAgent.allowOperation(PERM_MODIFY, item->getPermissions(), GP_OBJECT_MANIPULATE))
+ throw LLJSONRPCConnection::ForbiddenError("No modify permission on script");
+
+ // Send ScriptReset message to simulator
+ LLMessageSystem* msg = gMessageSystem;
+ msg->newMessageFast(_PREHASH_ScriptReset);
+ msg->nextBlockFast(_PREHASH_AgentData);
+ msg->addUUIDFast(_PREHASH_AgentID, gAgent.getID());
+ msg->addUUIDFast(_PREHASH_SessionID, gAgent.getSessionID());
+ msg->nextBlockFast(_PREHASH_Script);
+ msg->addUUIDFast(_PREHASH_ObjectID, prim_id);
+ msg->addUUIDFast(_PREHASH_ItemID, item_id);
+ msg->sendReliable(prim->getRegion()->getHost());
+
+ LLSD response;
+ response["success"] = true;
+ return response;
+}
+
+LLSD LLScriptEditorWSServer::handleObjectModify(U32 connection_id, const LLSD& params)
+{
+ // Step 1: Parameter Validation
+ LLUUID prim_id = params["prim_id"].asUUID();
+ if (prim_id.isNull())
+ throw LLJSONRPCConnection::InvalidParams("prim_id is required");
+
+ bool has_name = params.has("name");
+ bool has_desc = params.has("description");
+ bool has_perms = params.has("permissions") && params["permissions"].has("next_owner");
+
+ if (!has_name && !has_desc && !has_perms)
+ throw LLJSONRPCConnection::InvalidParams(
+ "At least one property (name, description, or permissions) must be specified");
+
+ // Step 2: Find and Validate Object
+ LLViewerObject* prim = gObjectList.findObject(prim_id);
+ if (!prim)
+ throw LLJSONRPCConnection::InvalidParams("Prim not found");
+
+ LLViewerObject* root = prim->getRootEdit();
+ if (!root || !isObjectPublished(root->getID()))
+ throw LLJSONRPCConnection::ForbiddenError("Object is not published");
+
+ if (!prim->permModify())
+ throw LLJSONRPCConnection::ForbiddenError("No modify permission on object");
+
+ // Step 3: Send Property Update Messages
+ LLMessageSystem* msg = gMessageSystem;
+ LLHost host = prim->getRegion()->getHost();
+ U32 local_id = prim->getLocalID();
+
+ if (has_name)
+ {
+ std::string new_name = params["name"].asString();
+ msg->newMessageFast(_PREHASH_ObjectName);
+ msg->nextBlockFast(_PREHASH_AgentData);
+ msg->addUUIDFast(_PREHASH_AgentID, gAgent.getID());
+ msg->addUUIDFast(_PREHASH_SessionID, gAgent.getSessionID());
+ msg->nextBlockFast(_PREHASH_ObjectData);
+ msg->addU32Fast(_PREHASH_LocalID, local_id);
+ msg->addStringFast(_PREHASH_Name, new_name);
+ msg->sendReliable(host);
+ }
+
+ if (has_desc)
+ {
+ std::string new_desc = params["description"].asString();
+ msg->newMessageFast(_PREHASH_ObjectDescription);
+ msg->nextBlockFast(_PREHASH_AgentData);
+ msg->addUUIDFast(_PREHASH_AgentID, gAgent.getID());
+ msg->addUUIDFast(_PREHASH_SessionID, gAgent.getSessionID());
+ msg->nextBlockFast(_PREHASH_ObjectData);
+ msg->addU32Fast(_PREHASH_LocalID, local_id);
+ msg->addStringFast(_PREHASH_Description, new_desc);
+ msg->sendReliable(host);
+ }
+
+ if (has_perms)
+ {
+ U32 next_owner_mask = static_cast<U32>(params["permissions"]["next_owner"].asInteger());
+ msg->newMessageFast(_PREHASH_ObjectPermissions);
+ msg->nextBlockFast(_PREHASH_AgentData);
+ msg->addUUIDFast(_PREHASH_AgentID, gAgent.getID());
+ msg->addUUIDFast(_PREHASH_SessionID, gAgent.getSessionID());
+ msg->nextBlockFast(_PREHASH_HeaderData);
+ msg->addBOOLFast(_PREHASH_Override, false);
+ msg->nextBlockFast(_PREHASH_ObjectData);
+ msg->addU32Fast(_PREHASH_ObjectLocalID, local_id);
+ msg->addU8Fast(_PREHASH_Field, PERM_NEXT_OWNER);
+ msg->addBOOLFast(_PREHASH_Set, true);
+ msg->addU32Fast(_PREHASH_Mask, next_owner_mask);
+ msg->sendReliable(host);
+ }
+
+ // Step 4: Return Success Response
+ LLSD response;
+ response["success"] = true;
+ response["prim_id"] = prim_id.asString();
+ return response;
+}
+
+LLSD LLScriptEditorWSServer::handleObjectItemModify(U32 connection_id, const LLSD& params)
+{
+ // Step 1: Parameter Validation
+ if (!params.has("prim_id") || !params.has("item_id"))
+ throw LLJSONRPCConnection::InvalidParams("prim_id and item_id are required");
+
+ bool has_name = params.has("name");
+ bool has_desc = params.has("description");
+ bool has_perms = params.has("permissions") && params["permissions"].has("next_owner");
+
+ if (!has_name && !has_desc && !has_perms)
+ throw LLJSONRPCConnection::InvalidParams(
+ "At least one property (name, description, or permissions) must be specified");
+
+ // Step 2: Validate Published Item (reuse existing helper)
+ ValidatedItem v = validatePublishedItem(params, PERM_MODIFY);
+
+ LLUUID prim_id = params["prim_id"].asUUID();
+ LLUUID item_id = params["item_id"].asUUID();
+
+ // Step 3: Create Modified Item Copy
+ LLPointer<LLViewerInventoryItem> new_item =
+ new LLViewerInventoryItem(static_cast<LLViewerInventoryItem*>(v.item));
+
+ if (has_name)
+ {
+ new_item->rename(params["name"].asString());
+ }
+
+ if (has_desc)
+ {
+ new_item->setDescription(params["description"].asString());
+ }
+
+ if (has_perms)
+ {
+ LLPermissions perm = new_item->getPermissions();
+ U32 next_owner_mask = static_cast<U32>(params["permissions"]["next_owner"].asInteger());
+ perm.setMaskNext(next_owner_mask);
+ new_item->setPermissions(perm);
+ }
+
+ // Step 4: Send UpdateTaskInventory Message
+ v.prim->updateInventory(new_item, TASK_INVENTORY_ITEM_KEY, false);
+
+ // Step 5: Return Success Response
+ LLSD response;
+ response["success"] = true;
+ response["prim_id"] = prim_id.asString();
+ response["item_id"] = item_id.asString();
+ return response;
+}
+
+void LLScriptEditorWSServer::registerCommand(const WSCommandInfo& info, WSCommandHandler handler)
+{
+ mCommandRegistry.emplace(info.command, std::make_pair(info, std::move(handler)));
+}
+
+bool LLScriptEditorWSConnection::hasFeature(const std::string& feature) const
+{
+ return mFeatures.count(feature) > 0;
+}
+
+LLSD LLScriptEditorWSServer::handleSaveBackToObjectContents(U32 connection_id, const LLSD& params)
+{
+ LLUUID object_id = params["object_id"].asUUID();
+ if (object_id.isNull())
+ {
+ throw LLJSONRPCConnection::InvalidParams("object_id is required");
+ }
+
+ const LLPublishedObjectMgr::PublishedObjectInfo* published_info =
+ mPublishedObjectManager.getPublished(object_id);
+ if (!published_info)
+ {
+ throw LLJSONRPCConnection::InvalidParams(
+ "Object is not published");
+ }
+
+ if (!published_info->mCanSaveBackToContents || published_info->mSourceTaskID.isNull())
+ {
+ throw LLJSONRPCConnection::ForbiddenError(
+ "Save back is not available for this object");
+ }
+
+ LLViewerObject* root = gObjectList.findObject(object_id);
+ if (!root)
+ {
+ throw LLJSONRPCConnection::InvalidParams(
+ "object_id not found");
+ }
+
+ if (!save_object_back_to_contents(root, published_info->mSourceTaskID))
+ {
+ throw LLJSONRPCConnection::InternalError(
+ "Failed to save object back to contents");
+ }
+
+ LL_DEBUGS("ScriptEditorWS") << "Save-back requested via command for object "
+ << object_id << " on connection " << connection_id << LL_ENDL;
+
+ LLSD response;
+ response["success"] = true;
+
+ LLSD result;
+ result["object_id"] = object_id;
+ response["result"] = result;
+ return response;
+}
+
+LLSD LLScriptEditorWSServer::handleCommandExecute(U32 connection_id, const LLSD& params)
+{
+ const std::string command = params["command"].asString();
+ if (command.empty())
+ {
+ throw LLJSONRPCConnection::InvalidParams("command is required");
+ }
+
+ auto it = mCommandRegistry.find(command);
+ if (it == mCommandRegistry.end())
+ {
+ throw LLJSONRPCConnection::InvalidParams(
+ "Unknown command: " + command);
+ }
+
+ return it->second.second(connection_id, params["params"]);
+}
+
+LLSD LLScriptEditorWSServer::handleCommandList()
+{
+ LLSD commands(LLSD::emptyArray());
+ for (const auto& [name, entry] : mCommandRegistry)
+ {
+ LLSD info;
+ info["command"] = entry.first.command;
+ info["description"] = entry.first.description;
+ commands.append(info);
+ }
+ LLSD response;
+ response["commands"] = commands;
+ return response;
+}
+
+void LLScriptEditorWSServer::sendCommandExecute(
+ U32 connection_id, const std::string& command, const LLSD& params)
+{
+ auto it = mActiveConnections.find(connection_id);
+ if (it == mActiveConnections.end())
+ {
+ return;
+ }
+
+ auto connection = it->second.lock();
+ if (!connection || !connection->hasFeature("commands"))
+ {
+ return;
+ }
+
+ LLSD call_params;
+ call_params["command"] = command;
+ call_params["params"] = params;
+
+ connection->call("command.execute", call_params,
+ [command](const LLSD& result, const LLSD& error)
+ {
+ LL_WARNS_IF(!error.isUndefined() || !result["success"].asBoolean(), "WSCommand")
+ << "command.execute failed for " << command << LL_ENDL;
+ });
+}
+
+void LLScriptEditorWSServer::broadcastLanguageChange()
+{
+ LLUUID syntax_id = LLSyntaxDefCache::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())
+ {
+ throw LLJSONRPCConnection::InvalidParams(
+ "No syntax category specified");
+ }
+
+ response["id"] = mLastSyntaxId;
+ if (category == "defs.lua")
+ {
+ response["defs"] = LLSyntaxDefCache::instance().getLuaKeywords();
+ }
+ else if (category == "defs.lsl")
+ {
+ response["defs"] = LLSyntaxDefCache::instance().getLSLKeywords();
+ }
+ else
+ {
+ throw LLJSONRPCConnection::InvalidParams(
+ "Unknown syntax category requested");
+ }
+
+ if (!response["defs"].isDefined())
+ {
+ throw LLJSONRPCConnection::InternalError(
+ "Syntax definitions are unavailable");
+ }
+
+ response["success"] = true;
+ return response;
+}
+
+LLSD LLScriptEditorWSServer::handleSyntaxCacheRequest() const
+{
+ LLSD response;
+ // Add array of cached syntax definition files
+ LLSD syntax_files = LLSD::emptyArray();
+ for (const auto& name : LLSyntaxDefCache::instance().getCacheFileNames())
+ {
+ syntax_files.append(name);
+ }
+ response["files"] = syntax_files;
+ response["success"] = true;
+ return response;
+}
+
+LLSD LLScriptEditorWSServer::handleSyntaxCacheFileRequest(const LLSD& params) const
+{
+ std::string filename = params["filename"].asString();
+ bool as_json = params["as_json"].asBoolean();
+
+ LLSyntaxDefCache& cache = LLSyntaxDefCache::instance();
+ LLSD response;
+
+ if (filename.empty())
+ {
+ throw LLJSONRPCConnection::InvalidParams(
+ "No filename specified");
+ }
+ if (!cache.hasCacheFile(filename))
+ {
+ throw LLJSONRPCConnection::InvalidParams(
+ "Requested syntax cache file not found");
+ }
+ if (as_json)
+ {
+ LLSD file_content = cache.loadCacheFileAsLLSD(filename);
+ if (file_content.isDefined())
+ {
+ response["content"] = file_content;
+ }
+ else
+ {
+ throw LLJSONRPCConnection::InternalError(
+ "Failed to load and format syntax cache file.");
+ }
+ }
+ else
+ {
+ std::string content = cache.loadCacheFile(filename);
+ if (!content.empty())
+ {
+ response["content"] = content;
+ }
+ else
+ {
+ throw LLJSONRPCConnection::InternalError(
+ "Failed to load syntax cache file");
+ }
+ }
+ response["success"] = true;
+ 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 result = updateScriptSubscription(script_id, connection_id);
+
+ response["script_id"] = script_id;
+ response["success"] = (result == SubscriptionError::SUCCESS);
+ response["status"] = static_cast<S32>(result);
+
+ LL_WARNS_IF(result != SubscriptionError::SUCCESS, "ScriptEditorWS")
+ << "Script connect request for script " << script_id << " failed with status " << static_cast<S32>(result) << LL_ENDL;
+ switch (result)
+ {
+ case SubscriptionError::SUCCESS:
+ response["message"] = "OK";
+ break;
+ case SubscriptionError::INVALID_EDITOR:
+ response["message"] = "Invalid editor handle";
+ break;
+ case SubscriptionError::INVALID_SUBSCRIPTION:
+ response["message"] = "No subscription found for script";
+ break;
+ case SubscriptionError::ALREADY_SUBSCRIBED:
+ response["message"] = "Script already subscribed";
+ break;
+ case SubscriptionError::INTERNAL_ERROR:
+ response["message"] = "Internal server error";
+ break;
+ }
+
+ if (result == SubscriptionError::SUCCESS)
+ {
+ auto it = mSubscriptions.find(script_id);
+ if (it != mSubscriptions.end())
+ {
+ LLUUID prim_id = (*it).second.mItemRef.mPrimID;
+ LLUUID root_id = prim_id;
+ LLViewerObject* object = gObjectList.findObject(prim_id);
+ if (object)
+ {
+ LLViewerObject* root = object->getRootEdit();
+ if (root)
+ {
+ root_id = root->getID();
+ }
+ }
+
+ response["object_id"] = prim_id;
+ response["root_id"] = root_id;
+ //response["object_name"] = object ? object->getName() : "Unknown";
+ response["item_id"] = (*it).second.mItemRef.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;
+}
+
+LLSD LLScriptEditorWSServer::handleObjectRequest(U32 connection_id, const LLSD& params)
+{
+ LLUUID object_id = params["object_id"].asUUID();
+ LLSD response;
+
+ if (object_id.isNull())
+ {
+ throw LLJSONRPCConnection::InvalidParams(
+ "No object_id specified");
+ }
+
+ LLViewerObject* object = gObjectList.findObject(object_id);
+ if (!object)
+ {
+ throw LLJSONRPCConnection::InvalidParams(
+ "Object not found");
+ }
+
+ if (!object->permModify())
+ {
+ throw LLJSONRPCConnection::ForbiddenError(
+ "Permission denied");
+ }
+
+ bool accepted = publishObject(object_id);
+ if (!accepted)
+ {
+ throw LLJSONRPCConnection::InternalError(
+ "Failed to initiate publish");
+ }
+
+ response["success"] = true;
+ return response;
+}
+
+// Helper function to validate that the specified prim and
+// item are valid, published, and have the required permissions.
+// Throws JSON-RPC exceptions if validation fails.
+LLScriptEditorWSServer::ValidatedItem LLScriptEditorWSServer::validatePublishedItem(
+ const LLSD& params, U32 permMask) const
+{
+ LLUUID prim_id = params["prim_id"].asUUID();
+ LLUUID item_id = params["item_id"].asUUID();
+
+ if (prim_id.isNull() || item_id.isNull())
+ throw LLJSONRPCConnection::InvalidParams("prim_id and item_id are required");
+
+ LLViewerObject* prim = gObjectList.findObject(prim_id);
+ if (!prim)
+ throw LLJSONRPCConnection::InvalidParams("Prim not found");
+
+ LLViewerObject* root = prim->getRootEdit();
+ if (!root || !isObjectPublished(root->getID()))
+ throw LLJSONRPCConnection::ForbiddenError("Object is not published");
+
+ LLInventoryItem* item = dynamic_cast<LLInventoryItem*>(prim->getInventoryObject(item_id));
+ if (!item)
+ throw LLJSONRPCConnection::InvalidParams("Item not found in prim inventory");
+
+ LLAssetType::EType type = item->getType();
+ if (type != LLAssetType::AT_LSL_TEXT && type != LLAssetType::AT_NOTECARD)
+ throw LLJSONRPCConnection::InvalidParams("Item is not a script or notecard");
+
+ if ((permMask & PERM_COPY) &&
+ !gAgent.allowOperation(PERM_COPY, item->getPermissions(), GP_OBJECT_MANIPULATE))
+ throw LLJSONRPCConnection::ForbiddenError("Insufficient permissions");
+
+ if (permMask & PERM_MODIFY)
+ {
+ // Writes into task inventory require modify permission on both the
+ // item AND the containing prim. A no-mod object can be published
+ // (read-only), but its contents cannot be changed.
+ if (!gAgent.allowOperation(PERM_MODIFY, item->getPermissions(), GP_OBJECT_MANIPULATE))
+ throw LLJSONRPCConnection::ForbiddenError("Insufficient permissions");
+
+ if (!prim->permModify())
+ throw LLJSONRPCConnection::ForbiddenError("No modify permission on object");
+ }
+
+ return { prim, root, item, type };
+}
+
+LLSD LLScriptEditorWSServer::handleObjectContentGet(const std::string& method, const LLSD& id, const LLSD& params)
+{
+ // Permission policy for reading item contents:
+ // - Scripts: require both PERM_COPY and PERM_MODIFY. No-copy or
+ // no-modify scripts cannot have their source exposed.
+ // - Notecards: no permission requirement -- no-mod notecards remain
+ // readable so external editors can view their contents.
+ U32 required_perms = 0;
+ {
+ LLUUID prim_id_peek = params["prim_id"].asUUID();
+ LLUUID item_id_peek = params["item_id"].asUUID();
+ LLViewerObject* prim_peek = gObjectList.findObject(prim_id_peek);
+ if (prim_peek)
+ {
+ if (auto* it = dynamic_cast<LLInventoryItem*>(prim_peek->getInventoryObject(item_id_peek)))
+ {
+ if (it->getType() == LLAssetType::AT_LSL_TEXT)
+ required_perms = PERM_COPY | PERM_MODIFY;
+ }
+ }
+ }
+
+ auto v = validatePublishedItem(params, required_perms);
+
+ LLUUID prim_id = params["prim_id"].asUUID();
+ LLUUID item_id = params["item_id"].asUUID();
+
+ LLSD cb_result = await_async_result(
+ "objectContentGet", ASSET_FETCH_TIMEOUT, "Asset fetch timed out",
+ [&](const std::string& pump_name)
+ {
+ gAssetStorage->getInvItemAsset(
+ v.prim->getRegion()->getHost(),
+ gAgent.getID(),
+ gAgent.getSessionID(),
+ v.item->getPermissions().getOwner(),
+ v.prim->getID(),
+ v.item->getUUID(),
+ v.item->getAssetUUID(),
+ v.type,
+ [pump_name](const LLUUID& asset_uuid, LLAssetType::EType asset_type, void*, S32 status, LLExtStat)
+ {
+ LLSD result;
+ if (status == LL_ERR_NOERR)
+ {
+ result["asset_uuid"] = asset_uuid;
+ result["asset_type"] = static_cast<S32>(asset_type);
+ }
+ else
+ {
+ result["error"] = status;
+ }
+ LLEventPumps::instance().post(pump_name, result);
+ },
+ nullptr,
+ true);
+ });
+
+ if (cb_result.has("error"))
+ {
+ S32 status = cb_result["error"].asInteger();
+ if (status == LL_ERR_ASSET_REQUEST_NOT_IN_DATABASE || status == LL_ERR_FILE_EMPTY)
+ throw LLJSONRPCConnection::InvalidParams("Asset not found");
+ if (status == LL_ERR_INSUFFICIENT_PERMISSIONS)
+ throw LLJSONRPCConnection::ForbiddenError("Insufficient permissions to read asset");
+ throw LLJSONRPCConnection::InternalError("Asset fetch failed: " + std::to_string(status));
+ }
+
+ LLUUID asset_uuid = cb_result["asset_uuid"].asUUID();
+ LLAssetType::EType asset_type = static_cast<LLAssetType::EType>(cb_result["asset_type"].asInteger());
+
+ LLFileSystem file(asset_uuid, asset_type);
+ S32 file_length = file.getSize();
+ if (file_length <= 0)
+ throw LLJSONRPCConnection::InternalError("Asset file empty or not found in cache");
+
+ std::vector<char> buffer(file_length + 1);
+ file.read(reinterpret_cast<U8*>(buffer.data()), file_length);
+ buffer[file_length] = '\0';
+
+ std::string text_content;
+ if (asset_type == LLAssetType::AT_NOTECARD)
+ {
+ // Notecards are stored in an envelope format -- use LLNotecard to extract the text
+ LLNotecard notecard;
+ std::istringstream istr(std::string(buffer.data(), file_length));
+ if (notecard.importStream(istr))
+ {
+ text_content = notecard.getText();
+ }
+ else
+ {
+ throw LLJSONRPCConnection::InternalError("Failed to parse notecard format");
+ }
+ }
+ else
+ {
+ text_content = std::string(buffer.data());
+ }
+
+ LLSD response;
+ response["success"] = true;
+ response["prim_id"] = prim_id;
+ response["item_id"] = item_id;
+ response["content"] = text_content;
+ return response;
+}
+
+LLSD LLScriptEditorWSServer::handleObjectContentSave(const std::string& method, const LLSD& id, const LLSD& params)
+{
+ std::string content = params["content"].asString();
+ if (content.empty())
+ throw LLJSONRPCConnection::InvalidParams("content is required");
+
+ auto v = validatePublishedItem(params, PERM_MODIFY);
+
+ if (v.type == LLAssetType::AT_LSL_TEXT)
+ {
+ return saveScript(v.prim, v.item, content, params);
+ }
+ else
+ {
+ return saveNotecard(v.prim, v.item, content);
+ }
+}
+
+LLSD LLScriptEditorWSServer::saveScript(LLViewerObject* prim, LLInventoryItem* item,
+ const std::string& content, const LLSD& params)
+{
+ // Determine compile target
+ std::string compile_target;
+ if (params.has("vm"))
+ {
+ compile_target = params["vm"].asString();
+ // The client sends "luau" for the Luau VM -- but if the script is LSL
+ // (not native Luau), the internal compile target is "lsl-luau".
+ if (compile_target == "luau" && item->getInventorySubType() != SST_LUA)
+ {
+ compile_target = "lsl-luau";
+ }
+ }
+ else
+ {
+ U8 subtype = item->getInventorySubType();
+ std::string runtime = item->getRuntime();
+ bool is_lua = (subtype == SST_LUA);
+ if (!is_lua && runtime == "luau")
+ compile_target = "lsl-luau";
+ else if (!runtime.empty())
+ compile_target = runtime;
+ else
+ {
+ is_lua = is_lua_script(content);
+ compile_target = is_lua ? "luau" : "mono";
+ }
+ }
+
+ // The task inventory can be refreshed while the upload is in flight,
+ // invalidating the raw item pointer returned by validatePublishedItem().
+ // Keep stable identifiers for use after await_async_result().
+ const LLUUID prim_id = prim->getID();
+ const LLUUID item_id = item->getUUID();
+
+ std::string url = prim->getRegion()->getCapability("UpdateScriptTask");
+ if (url.empty())
+ throw LLJSONRPCConnection::InternalError("UpdateScriptTask capability not available");
+
+ LLSD cb_result = await_async_result(
+ "objectContentSave", SCRIPT_UPLOAD_TIMEOUT, "Script upload/compile timed out",
+ [&, prim_id, item_id](const std::string& pump_name)
+ {
+ auto [on_success, on_failure] = make_asset_upload_callbacks(pump_name);
+ bool is_running = params.has("running") ? params["running"].asBoolean() : false;
+ LLResourceUploadInfo::ptr_t uploadInfo(std::make_shared<LLScriptAssetUpload>(
+ prim_id, item_id,
+ compile_target, is_running, LLUUID::null, content,
+ std::move(on_success), std::move(on_failure)));
+ LLViewerAssetUpload::EnqueueInventoryUpload(url, uploadInfo);
+ });
+
+ if (cb_result.has("failed"))
+ throw LLJSONRPCConnection::InternalError("Upload failed: " + cb_result["reason"].asString());
+
+ LLSD response;
+ response["success"] = true;
+ response["prim_id"] = prim_id;
+ response["item_id"] = item_id;
+ response["compiled"] = cb_result["compiled"];
+ if (!cb_result["compiled"].asBoolean() && cb_result.has("errors"))
+ {
+ response["diagnostics"] = LLSD::emptyArray();
+
+ const bool is_lua =
+ compile_target == "luau" ||
+ compile_target == "lsl-luau";
+
+ for (const auto& error : llsd::inArray(cb_result["errors"]))
+ {
+ boost::smatch match;
+ LLSD diagnostic;
+ diagnostic["level"] = "ERROR";
+
+ if (is_lua &&
+ boost::regex_match(
+ error.asString(),
+ match,
+ LUAU_LOCATION_PATTERN))
+ {
+ diagnostic["row"] = std::stoi(match[2].str());
+ diagnostic["column"] = 0;
+ diagnostic["message"] = match[3].str();
+ }
+ else if (!is_lua &&
+ boost::regex_match(
+ error.asString(),
+ match,
+ LSL_LOCATION_PATTERN))
+ {
+ diagnostic["row"] =
+ std::stoi(match[1].str()) + 1;
+ diagnostic["column"] =
+ std::stoi(match[2].str()) + 1;
+ diagnostic["level"] = match[3].str();
+ diagnostic["message"] = match[4].str();
+ diagnostic["format"] = "lsl";
+ }
+ else
+ {
+ diagnostic["row"] = 0;
+ diagnostic["column"] = 0;
+ diagnostic["message"] = error.asString();
+ }
+
+ response["diagnostics"].append(diagnostic);
+ }
+ }
+
+ // If the script is open in the viewer's editor, update it
+ LLSD floater_key;
+ floater_key["taskid"] = prim_id;
+ floater_key["itemid"] = item_id;
+ LLLiveLSLEditor* editor = LLFloaterReg::findTypedInstance<LLLiveLSLEditor>("preview_scriptedit", floater_key);
+ if (editor)
+ {
+ LLScriptEdCore* sed = editor->getScriptEdCore();
+ if (sed)
+ {
+ sed->setScriptText(LLStringExplicit(content), true);
+ sed->makeEditorPristine();
+ }
+ }
+
+ return response;
+}
+
+LLSD LLScriptEditorWSServer::saveNotecard(LLViewerObject* prim, LLInventoryItem* item,
+ const std::string& content)
+{
+ // The task inventory can be refreshed while the upload is in flight,
+ // invalidating the raw item pointer returned by validatePublishedItem().
+ // Keep stable identifiers for use after await_async_result().
+ const LLUUID prim_id = prim->getID();
+ const LLUUID item_id = item->getUUID();
+
+ std::string url = prim->getRegion()->getCapability("UpdateNotecardTaskInventory");
+ if (url.empty())
+ throw LLJSONRPCConnection::InternalError("UpdateNotecardTaskInventory capability not available");
+
+ // Use LLNotecard to produce the proper notecard format
+ LLNotecard notecard;
+ notecard.setText(content);
+
+ std::ostringstream ostr;
+ notecard.exportStream(ostr);
+
+ LLSD cb_result = await_async_result(
+ "objectContentSaveNotecard", NOTECARD_UPLOAD_TIMEOUT, "Notecard upload timed out",
+ [&, prim_id, item_id](const std::string& pump_name)
+ {
+ auto [on_success, on_failure] = make_asset_upload_callbacks(pump_name);
+ LLResourceUploadInfo::ptr_t uploadInfo(std::make_shared<LLBufferedAssetUploadInfo>(
+ prim_id, item_id,
+ LLAssetType::AT_NOTECARD, ostr.str(),
+ std::move(on_success), std::move(on_failure)));
+ LLViewerAssetUpload::EnqueueInventoryUpload(url, uploadInfo);
+ });
+
+ if (cb_result.has("failed"))
+ throw LLJSONRPCConnection::InternalError("Upload failed: " + cb_result["reason"].asString());
+
+ LLSD response;
+ response["success"] = true;
+ response["prim_id"] = prim_id;
+ response["item_id"] = item_id;
+
+ // If the notecard is open in the viewer's editor, update it
+ LLSD floater_key;
+ floater_key["taskid"] = prim_id;
+ floater_key["itemid"] = item_id;
+ LLPreviewNotecard* nc = LLFloaterReg::findTypedInstance<LLPreviewNotecard>("preview_notecard", floater_key);
+ if (nc)
+ {
+ LLViewerTextEditor* nc_editor = nc->getChild<LLViewerTextEditor>("Notecard Editor");
+ if (nc_editor)
+ {
+ nc_editor->setText(content);
+ nc_editor->makePristine();
+ }
+ }
+
+ return response;
+}
+
+LLSD LLScriptEditorWSServer::handleObjectItemDelete(U32 connection_id, const LLSD& params)
+{
+ auto v = validatePublishedItem(params, PERM_MODIFY);
+
+ const LLUUID prim_id = v.prim->getID();
+ const LLUUID root_id = v.root->getID();
+ const LLUUID item_id = v.item->getUUID();
+
+ // Optimistic local delete then emit immediate update
+ // for published clients and request authoritative server refresh.
+ v.prim->removeInventory(item_id);
+ onPrimInventoryChanged(root_id, prim_id);
+
+ if (!mPublishedObjectManager.hasInventoryRequestStart(prim_id))
+ {
+ v.prim->dirtyInventory();
+ mPublishedObjectManager.setInventoryRequestStart(
+ prim_id,
+ LLTimer::getTotalSeconds().value());
+ v.prim->requestInventory();
+ }
+
+ LLSD response;
+ response["success"] = true;
+ response["prim_id"] = prim_id;
+ response["item_id"] = item_id;
+ return response;
+}
+
+LLSD LLScriptEditorWSServer::handleObjectUnpublish(U32 connection_id, const LLSD& params)
+{
+ LLUUID object_id = params["object_id"].asUUID();
+ if (object_id.isNull())
+ {
+ throw LLJSONRPCConnection::InvalidParams("object_id is required");
+ }
+
+ if (!mPublishedObjectManager.hasPublished(object_id))
+ {
+ throw LLJSONRPCConnection::InvalidParams("Object is not published");
+ }
+
+ unpublishObject(object_id, "manual");
+
+ LLSD response;
+ response["success"] = true;
+ response["object_id"] = object_id;
+ return response;
+}
+
+LLSD LLScriptEditorWSServer::handleObjectItemCreate(const std::string& method, const LLSD& id, const LLSD& params)
+{
+ std::string type = params["type"].asString();
+ if (type != "script" && type != "notecard")
+ {
+ throw LLJSONRPCConnection::InvalidParams("Unsupported item type: " + type);
+ }
+
+ LLUUID prim_id = params["prim_id"].asUUID();
+ if (prim_id.isNull())
+ {
+ throw LLJSONRPCConnection::InvalidParams("prim_id is required");
+ }
+
+ LLViewerObject* prim = gObjectList.findObject(prim_id);
+ if (!prim)
+ {
+ throw LLJSONRPCConnection::InvalidParams("Prim not found");
+ }
+
+ LLViewerObject* root = prim->getRootEdit();
+ if (!root || !isObjectPublished(root->getID()))
+ {
+ throw LLJSONRPCConnection::ForbiddenError("Object is not published");
+ }
+
+ std::string name = params["name"].asString();
+ if (name.empty())
+ {
+ throw LLJSONRPCConnection::InvalidParams("name is required");
+ }
+
+ bool has_cap = prim->getRegion() && !prim->getRegion()->getCapability("CreateTaskInventoryItem").empty();
+
+ if (type == "notecard" && !has_cap)
+ {
+ throw LLJSONRPCConnection::ForbiddenError("Notecard creation requires CreateTaskInventoryItem capability");
+ }
+
+ // Resolve type-specific fields
+ LLAssetType::EType asset_type;
+ LLInventoryType::EType inv_type;
+ U8 sub_type = 0;
+ const char* perm_key;
+ LLSD cap_params;
+
+ if (type == "script")
+ {
+ std::string vm = params["vm"].asString();
+ if (vm == "luau")
+ {
+ sub_type = SST_LUA;
+ }
+ else if (vm == "mono" || vm == "lsl2")
+ {
+ sub_type = SST_LSL;
+ }
+ else
+ {
+ throw LLJSONRPCConnection::InvalidParams("vm must be 'luau', 'mono', or 'lsl2'");
+ }
+
+ asset_type = LLAssetType::AT_LSL_TEXT;
+ inv_type = LLInventoryType::IT_LSL;
+ perm_key = "Scripts";
+ cap_params["enabled"] = true;
+ cap_params["vm"] = vm;
+ }
+ else
+ {
+ asset_type = LLAssetType::AT_NOTECARD;
+ inv_type = LLInventoryType::IT_NOTECARD;
+ perm_key = "Notecards";
+ if (params.has("text"))
+ {
+ cap_params["text"] = params["text"].asString();
+ }
+ }
+
+ LLPermissions perms;
+ perms.init(gAgent.getID(), gAgent.getID(), LLUUID::null, LLUUID::null);
+ perms.initMasks(
+ PERM_ALL,
+ PERM_ALL,
+ LLFloaterPerms::getEveryonePerms(perm_key),
+ LLFloaterPerms::getGroupPerms(perm_key),
+ PERM_MOVE | LLFloaterPerms::getNextOwnerPerms(perm_key));
+
+ std::string desc;
+ LLViewerAssetType::generateDescriptionFor(asset_type, desc);
+
+ // Snapshot existing item IDs before creation
+ std::set<LLUUID> existing_items;
+ {
+ LLInventoryObject::object_list_t inv;
+ prim->getInventoryContents(inv);
+ for (auto& obj : inv)
+ {
+ existing_items.insert(obj->getUUID());
+ }
+ }
+
+ // Set up event pump to wait for inventory change
+ LLEventMailDrop result_pump("objectItemCreate." + LLUUID::generateNewID().asString(), true);
+
+ // Reject if another item.create is already in flight for this prim; the
+ // map keys by prim, so two concurrent creates would clobber one another.
+ if (!mPublishedObjectManager.reservePendingItemCreate(prim_id, result_pump.getName()))
+ {
+ throw LLJSONRPCConnection::InvalidRequest(
+ "An item.create is already in flight for this prim");
+ }
+
+ // RAII: guarantee the pending entry is cleared on every exit path (throw
+ // or normal return), so no exception between here and the erase-on-post
+ // in onPrimInventoryChanged can leave a stale entry behind. Uses a
+ // shared_ptr custom deleter as a lightweight scope guard.
+ std::shared_ptr<void> pending_guard(nullptr, [this, prim_id](void*)
+ {
+ mPublishedObjectManager.clearPendingItemCreate(prim_id);
+ });
+
+ if (has_cap)
+ {
+ prim->createInventoryItem(asset_type, inv_type, sub_type, name, desc, perms, cap_params,
+ [pump_name = result_pump.getName()](bool success, const LLSD& response)
+ {
+ LLEventPumps::instance().obtain(pump_name).post(response);
+ });
+ }
+ else
+ {
+ // Fallback: legacy RezScript UDP (scripts only -- notecards already rejected above)
+ LLPointer<LLViewerInventoryItem> new_item =
+ new LLViewerInventoryItem(
+ LLUUID::null, LLUUID::null, perms, LLUUID::null,
+ asset_type, inv_type, name, desc, LLSaleInfo::DEFAULT,
+ LLInventoryItemFlags::II_FLAGS_SUBTYPE_MASK & sub_type,
+ time_corrected());
+ prim->saveScript(new_item, true, true, LLUUID::null);
+ }
+
+ // Wait for inventory change callback
+ LLSD event = llcoro::suspendUntilEventOnWithTimeout(result_pump, ITEM_CREATE_TIMEOUT, LLSD().with("timeout", true));
+
+ if (event.has("timeout"))
+ {
+ throw LLJSONRPCConnection::RequestTimeoutError("Timed out waiting for item creation");
+ }
+
+ prim = gObjectList.findObject(prim_id);
+ if (!prim)
+ {
+ throw LLJSONRPCConnection::InternalError("Prim no longer exists");
+ }
+
+ LLSD response;
+
+ // If cap returned item_id directly, use it
+ if (event.has("success") && event["success"].asBoolean() &&
+ event.has("item_id") && event["item_id"].asUUID().notNull())
+ {
+ response["item_id"] = event["item_id"];
+ response["name"] = event["name"];
+ response["description"] = desc;
+ response["type"] = type;
+ response["prim_id"] = prim_id;
+
+ if (type == "script")
+ {
+ response["subtype"] = static_cast<S32>(sub_type);
+ }
+
+ LLSD perm_entry;
+ perm_entry["owner"] = static_cast<S32>(perms.getMaskOwner());
+ perm_entry["next_owner"] = static_cast<S32>(perms.getMaskNextOwner());
+ response["permissions"] = perm_entry;
+ response["creator_id"] = gAgent.getID();
+ }
+ else
+ {
+ // Fallback: search inventory (for UDP path or if cap didn't return item_id)
+ LLInventoryObject::object_list_t inv;
+ prim->getInventoryContents(inv);
+ for (auto& obj : inv)
+ {
+ if (existing_items.find(obj->getUUID()) == existing_items.end())
+ {
+ LLInventoryItem* created = dynamic_cast<LLInventoryItem*>(obj.get());
+ if (created && created->getType() == asset_type)
+ {
+ response["item_id"] = created->getUUID();
+ response["name"] = created->getName();
+ response["description"] = created->getDescription();
+ response["type"] = type;
+
+ if (type == "script")
+ {
+ response["subtype"] = static_cast<S32>(created->getInventorySubType());
+ const std::string& runtime = created->getRuntime();
+ if (!runtime.empty())
+ {
+ response["vm"] = runtime;
+ }
+ }
+
+ const LLPermissions& item_perms = created->getPermissions();
+ LLSD perm_entry;
+ perm_entry["owner"] = static_cast<S32>(item_perms.getMaskOwner());
+ perm_entry["next_owner"] = static_cast<S32>(item_perms.getMaskNextOwner());
+ response["permissions"] = perm_entry;
+ response["creator_id"] = item_perms.getCreator();
+ response["prim_id"] = prim_id;
+ break;
+ }
+ }
+ }
+ }
+
+ if (!response.has("item_id"))
+ {
+ throw LLJSONRPCConnection::InternalError("Item was not found in updated inventory");
+ }
+
+ return response;
+}
+
+
+void LLScriptEditorWSServer::notifyScript(const std::string& script_id, const std::string &method, const LLSD& message) const
+{
+ LL_PROFILE_ZONE_SCOPED_CATEGORY_SCRIPTDEV;
+ 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)
+{
+ LL_PROFILE_ZONE_SCOPED_CATEGORY_SCRIPTDEV;
+ 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
+{
+ LL_PROFILE_ZONE_SCOPED_CATEGORY_SCRIPTDEV;
+ 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["diagnostics"] = LLSD::emptyArray();
+
+ if (is_lua)
+ { // lua errors: ":line: message", line is 1-based
+ 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, LUAU_LOCATION_PATTERN))
+ {
+ S32 line_number = std::stoi(match[2].str());
+ std::string message = match[3].str();
+
+ err_entry["row"] = line_number;
+ err_entry["message"] = message;
+ }
+ else
+ {
+ err_entry["row"] = 0;
+ err_entry["message"] = err.asString();
+ }
+ params["diagnostics"].append(err_entry);
+ }
+ }
+ else
+ { // lsl errors: "(line, column) : SEVERITY : message", line and column are 0-based
+ for (const auto& err : llsd::inArray(results["errors"]))
+ {
+ boost::smatch match;
+ LLSD err_entry;
+
+ if (boost::regex_match(err.asString(), match, LSL_LOCATION_PATTERN))
+ {
+ 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["diagnostics"].append(err_entry);
+ }
+ }
+ }
+
+ notifyScript(script_id, "script.compiled", params);
+}
+
+void LLScriptEditorWSServer::forwardChatToIDE(
+ const LLChat& chat_msg,
+ LLPublishedObjectMgr::RuntimeEventAggregator::Channel channel) const
+{
+ LL_PROFILE_ZONE_SCOPED_CATEGORY_SCRIPTDEV;
+
+ mPublishedObjectManager.ingestRuntimeChat(chat_msg, channel);
+}
+
+void LLScriptEditorWSServer::sendRuntimeEvent(
+ const LLPublishedObjectMgr::RuntimeChatEvent& event) const
+{
+
+ std::string script_id;
+ if (event.mItemID.notNull())
+ {
+ script_id = buildScriptSubscriptionId(event.mPrimID, event.mItemID);
+ }
+
+ if (!isObjectPublished(event.mRootID) &&
+ (script_id.empty() || mSubscriptions.find(script_id) == mSubscriptions.end()))
+ {
+ return;
+ }
+
+ LLSD message;
+ message["object_id"] = event.mRootID;
+ message["prim_id"] = event.mPrimID;
+ message["item_id"] = event.mItemID;
+ message["object_name"] = event.mObjectName;
+ message["message"] = event.mMessage;
+
+ LLSD item;
+ item["root_id"] = event.mRootID;
+ item["prim_id"] = event.mPrimID;
+ item["item_id"] = event.mItemID;
+ item["name"] = event.mScriptName;
+ item["language"] =
+ event.mVM == LLPublishedObjectMgr::RuntimeEventAggregator::VM::LUAU
+ ? "luau"
+ : "lsl";
+ message["item"] = item;
+
+ switch (event.mChannel)
+ {
+ case LLPublishedObjectMgr::RuntimeEventAggregator::Channel::DEBUG:
+ message["channel"] = "debug";
+ break;
+ case LLPublishedObjectMgr::RuntimeEventAggregator::Channel::OWNER_SAY:
+ message["channel"] = "owner_say";
+ break;
+ }
+
+ if (event.mIsError)
+ {
+ message["error"] = event.mError;
+ message["line"] = event.mLine;
+ message["column"] = event.mColumn;
+ message["stack"] = LLSD::emptyArray();
+ for (const auto& line : event.mStack)
+ {
+ message["stack"].append(line);
+ }
+ }
+
+ notifyAll(event.mIsError ? "runtime.error" : "runtime.debug", message);
+}
+
+void LLScriptEditorWSServer::notifyConnection(U32 connection_id, const std::string& method, const LLSD& params) const
+{
+ LL_PROFILE_ZONE_SCOPED_CATEGORY_SCRIPTDEV;
+ auto it = mActiveConnections.find(connection_id);
+ if (it != mActiveConnections.end())
+ {
+ auto connection = it->second.lock();
+ if (connection)
+ {
+ connection->notify(method, params);
+ }
+ }
+}
+
+void LLScriptEditorWSServer::notifyAll(const std::string& method, const LLSD& params) const
+{
+ LL_PROFILE_ZONE_SCOPED_CATEGORY_SCRIPTDEV;
+ // Serialize once, deliver many: build the JSON-RPC envelope and its wire
+ // string a single time, then hand the bytes to each connection.
+ LLSD envelope = LLJSONRPCConnection::makeEnvelope(
+ LLSD(), method, params, LLSD(), LLSD());
+ std::string payload = boost::json::serialize(LlsdToJson(envelope));
+
+
+ for (const auto& pair : mActiveConnections)
+ {
+ auto connection = pair.second.lock();
+ if (connection)
+ {
+ connection->sendMessage(payload);
+ }
+ }
+}
+
+
+// static
+std::string LLScriptEditorWSServer::getPrimName(LLViewerObject* obj)
+{
+ std::string name = nv_string(obj, "Name");
+ if (!name.empty())
+ {
+ return name;
+ }
+
+ if (!obj)
+ {
+ return std::string();
+ }
+
+ LLSelectNode* node = LLSelectMgr::instance().getSelection()->findNode(obj);
+ if (node && !node->mName.empty())
+ {
+ return node->mName;
+ }
+
+ // Never emit an empty prim/object name to downstream tooling.
+ return obj->getID().asString();
+}
+
+bool LLScriptEditorWSServer::publishObject(const LLUUID& object_id)
+{
+ LL_PROFILE_ZONE_SCOPED_CATEGORY_SCRIPTDEV;
+ LLViewerObject* root = gObjectList.findObject(object_id);
+ if (!root)
+ {
+ LL_WARNS("ScriptEditorWS") << "publishObject: object not found: " << object_id << LL_ENDL;
+ return false;
+ }
+
+ if (!root->permModify())
+ {
+ LL_WARNS("ScriptEditorWS") << "publishObject: no modify permission on object: " << object_id << LL_ENDL;
+ return false;
+ }
+
+ // If already published, unpublish first to replace cleanly
+ if (isObjectPublished(object_id))
+ {
+ unpublishObject(object_id, "republish");
+ }
+
+ // Collect root + all children
+ std::vector<LLViewerObject*> prims = collect_linkset(root);
+
+ // Request object properties for each prim in the linkset (root + children),
+ // matching the hover path so name/description metadata is refreshed.
+ for (LLViewerObject* prim : prims)
+ {
+ LLSelectMgr::instance().requestObjectPropertiesFamily(prim);
+ }
+
+ // Set up a PendingPublish to coordinate inventory loading across all prims.
+ // We register a listener and call requestInventory() on every prim.
+ // If inventory is already loaded, requestInventory() fires the callback
+ // synchronously via doInventoryCallback(), so all_ready will naturally
+ // become true before this function returns in the common case.
+ mPublishedObjectManager.beginPendingPublish(object_id, prims);
+
+ // Request inventory for each prim. If already loaded, onPrimInventoryReady()
+ // will be called immediately (possibly building and sending the publish
+ // before this loop even finishes).
+ for (LLViewerObject* prim : prims)
+ {
+ if (!mPublishedObjectManager.hasPendingPublish(object_id))
+ {
+ break; // publish completed synchronously during a previous iteration
+ }
+ mPublishedObjectManager.setInventoryRequestStart(prim->getID(), LLTimer::getTotalSeconds().value());
+ prim->requestInventory();
+ }
+
+ return true;
+}
+
+bool LLScriptEditorWSServer::isObjectPublished(const LLUUID& object_id) const
+{
+ return mPublishedObjectManager.hasPublished(object_id);
+}
+
+void LLScriptEditorWSServer::onPrimInventoryReady(const LLUUID& object_id, const LLUUID& prim_id)
+{
+ LL_PROFILE_ZONE_SCOPED_CATEGORY_SCRIPTDEV;
+ if (mPublishedObjectManager.handlePrimInventoryReadyEvent(object_id, prim_id))
+ {
+ LL_DEBUGS("ScriptEditorWS") << "All prim inventories ready for object " << object_id << LL_ENDL;
+ buildAndSendPublish(object_id);
+ }
+}
+
+void LLScriptEditorWSServer::buildAndSendPublish(const LLUUID& object_id)
+{
+ LL_PROFILE_ZONE_SCOPED_CATEGORY_SCRIPTDEV;
+ if (!mPublishedObjectManager.hasPendingPublish(object_id))
+ {
+ LL_WARNS("ScriptEditorWS") << "buildAndSendPublish: no pending publish for " << object_id << LL_ENDL;
+ return;
+ }
+
+ LLViewerObject* root = gObjectList.findObject(object_id);
+ if (!root)
+ {
+ LL_WARNS("ScriptEditorWS") << "buildAndSendPublish: root object gone: " << object_id << LL_ENDL;
+ mPublishedObjectManager.cancelPendingPublish(object_id);
+ return;
+ }
+
+ LLSD pub = mPublishedObjectManager.buildPublishedObjectLLSD(root);
+
+ // Store in the published registry
+ LLPublishedObjectMgr::PublishedObjectInfo info;
+ info.mObjectID = root->getID();
+ info.mOwnerID = root->mOwnerID;
+ info.mObjectName = pub["object_name"].asString();
+ info.mObjectDescription = pub["object_description"].asString();
+ if (root->getRegion())
+ {
+ info.mRegionName = root->getRegion()->getName();
+ }
+ LLSelectNode* root_select_node = LLSelectMgr::instance().getSelection()->findNode(root);
+ if (root_select_node
+ && root_select_node->mValid
+ && !root_select_node->mFromTaskID.isNull()
+ && !root->isAttachment())
+ {
+ info.mCanSaveBackToContents = true;
+ info.mSourceTaskID = root_select_node->mFromTaskID;
+ }
+ else
+ {
+ info.mCanSaveBackToContents = false;
+ info.mSourceTaskID.setNull();
+ }
+
+ S32 link_num = 1;
+ std::vector<LLViewerObject*> prims = collect_linkset(root);
+ for (LLViewerObject* prim : prims)
+ {
+ LLPublishedObjectMgr::PublishedPrimInfo prim_info;
+ prim_info.mPrimID = prim->getID();
+ prim_info.mPrimName = getPrimName(prim); // Use helper with selection fallback
+ prim_info.mLinkNumber = link_num++;
+ prim_info.mInventorySerial = static_cast<S16>(prim->getInventorySerial());
+ info.mPrims.push_back(prim_info);
+ }
+
+ LLPublishedObjectMgr::PublishedObjectInfo& published_info = mPublishedObjectManager.finalizePendingPublish(object_id, std::move(info));
+
+ // Align outgoing publish payload with any property responses that arrived
+ // while inventory-gated publish was still pending.
+ pub["object_name"] = published_info.mObjectName;
+ pub["can_save_back"] = published_info.mCanSaveBackToContents;
+ pub["object_description"] = published_info.mObjectDescription;
+ if (pub.has("linked_objects"))
+ {
+ LLSD& linked_objects = pub["linked_objects"];
+ for (S32 i = 0; i < linked_objects.size(); ++i)
+ {
+ const LLUUID link_id = linked_objects[i]["link_id"].asUUID();
+ auto prim_it = std::find_if(
+ published_info.mPrims.begin(),
+ published_info.mPrims.end(),
+ [&](const LLPublishedObjectMgr::PublishedPrimInfo& p)
+ {
+ return p.mPrimID == link_id;
+ });
+ if (prim_it != published_info.mPrims.end())
+ {
+ linked_objects[i]["link_name"] = prim_it->mPrimName;
+ linked_objects[i]["link_description"] = prim_it->mPrimDescription;
+ }
+ }
+ }
+
+ // Send notification
+ LLSD message;
+ message["object"] = pub;
+ notifyAll("object.publish", message);
+
+ LL_INFOS("ScriptEditorWS") << "Published object " << object_id
+ << " (" << pub["object_name"].asString() << ") with "
+ << (prims.size() - 1) << " linked prim(s)" << LL_ENDL;
+
+ // Re-request object properties now that the object is published so
+ // onObjectPropertyChanged can emit object.update for root and linked prims.
+ for (LLViewerObject* prim : prims)
+ {
+ LLSelectMgr::instance().requestObjectPropertiesFamily(prim);
+ }
+}
+
+void LLScriptEditorWSServer::onLinksetChildAdded(const LLUUID& root_id, LLViewerObject* child)
+{
+ LL_PROFILE_ZONE_SCOPED_CATEGORY_SCRIPTDEV;
+ if (!child)
+ {
+ return;
+ }
+
+ if (!mPublishedObjectManager.reconcileLinksetChildAdded(
+ root_id,
+ child,
+ LLTimer::getTotalSeconds().value()))
+ {
+ return;
+ }
+
+ // Request inventory (async; fires onPrimInventoryChanged when ready).
+ child->requestInventory();
+
+ // Start safety-timeout timer (no-op if one is already pending for this root)
+ scheduleLinksetFlush(root_id, LINKSET_ADD_FLUSH_DELAY);
+}
+
+void LLScriptEditorWSServer::onLinksetChildRemoved(const LLUUID& root_id, const LLUUID& child_id)
+{
+ LL_PROFILE_ZONE_SCOPED_CATEGORY_SCRIPTDEV;
+ if (!mPublishedObjectManager.reconcileLinksetChildRemoved(root_id, child_id))
+ {
+ return;
+ }
+
+ // Schedule coalesced flush - multiple simultaneous removes share one timer
+ scheduleLinksetFlush(root_id, LINKSET_REMOVE_FLUSH_DELAY);
+}
+
+void LLScriptEditorWSServer::scheduleLinksetFlush(const LLUUID& root_id, F32 delay)
+{
+ // No-op if a timer is already pending for this root_id
+ if (mPublishedObjectManager.hasActiveLinksetFlushTimer(root_id))
+ {
+ return;
+ }
+
+ wptr_t weak = std::static_pointer_cast<LLScriptEditorWSServer>(shared_from_this());
+ LLEventTimer* t = LLEventTimer::run_after(delay, [weak, root_id]()
+ {
+ if (auto self = weak.lock())
+ {
+ self->mPublishedObjectManager.clearLinksetFlushTimer(root_id);
+ self->mPublishedObjectManager.clearPendingNewChildren(root_id); // clear any remaining pending children (timeout path)
+ self->flushLinksetUpdate(root_id);
+ }
+ });
+ mPublishedObjectManager.setLinksetFlushTimer(root_id, t->getWeak());
+}
+
+void LLScriptEditorWSServer::cancelLinksetFlushTimer(const LLUUID& root_id)
+{
+ mPublishedObjectManager.cancelLinksetFlushTimer(root_id);
+}
+
+void LLScriptEditorWSServer::flushLinksetUpdate(const LLUUID& root_id)
+{
+ LL_PROFILE_ZONE_SCOPED_CATEGORY_SCRIPTDEV;
+ LLSD update;
+ if (!mPublishedObjectManager.buildLinksetUpdateLLSD(root_id, update))
+ {
+ return;
+ }
+ notifyAll("object.update", update);
+
+ const LLSD linked_objects = update["linked_objects"];
+ LL_INFOS("ScriptEditorWS") << "Linkset update for " << root_id
+ << ": " << linked_objects.size() << " child(ren)" << LL_ENDL;
+}
+
+void LLScriptEditorWSServer::onPrimInventoryChanged(const LLUUID& object_id, const LLUUID& prim_id)
+{
+ LL_PROFILE_ZONE_SCOPED_CATEGORY_SCRIPTDEV;
+ if (!mPublishedObjectManager.hasPublished(object_id))
+ {
+ return;
+ }
+
+ LLViewerObject* prim = gObjectList.findObject(prim_id);
+ if (!prim)
+ {
+ return;
+ }
+
+ auto inv_result = mPublishedObjectManager.handlePrimInventoryChangedEvent(
+ object_id, prim_id, prim, LLTimer::getTotalSeconds().value());
+
+ if (inv_result.mTimingConsumed)
+ {
+ LL_DEBUGS("ScriptEditorWS") << "[Phase0] inventory refresh object_id=" << object_id
+ << " prim_id=" << prim_id
+ << " elapsed_sec=" << inv_result.mTimingElapsedSec << LL_ENDL;
+ }
+
+ if (inv_result.mKind == LLPublishedObjectMgr::InventoryChangeKind::CHILD_READY_WAIT)
+ {
+ return;
+ }
+ if (inv_result.mKind == LLPublishedObjectMgr::InventoryChangeKind::CHILD_READY_FLUSH_NOW)
+ {
+ cancelLinksetFlushTimer(object_id);
+ flushLinksetUpdate(object_id);
+ return;
+ }
+ if (inv_result.mKind == LLPublishedObjectMgr::InventoryChangeKind::ROOT_INVENTORY_UPDATE ||
+ inv_result.mKind == LLPublishedObjectMgr::InventoryChangeKind::CHILD_INVENTORY_UPDATE)
+ {
+ notifyAll("object.update", inv_result.mUpdate);
+ if (inv_result.mHasPendingItemCreate)
+ {
+ LLEventPumps::instance().post(
+ inv_result.mPendingItemCreatePump,
+ LLSD().with("prim_id", prim_id));
+ }
+
+ LL_DEBUGS("ScriptEditorWS") << "Sent object.update for prim " << prim_id
+ << " in object " << object_id << LL_ENDL;
+ }
+}
+
+void LLScriptEditorWSServer::onObjectPropertyChanged(
+ const LLUUID& prim_id, const std::string& name, const std::string& desc, S16 inventory_serial)
+{
+ LL_PROFILE_ZONE_SCOPED_CATEGORY_SCRIPTDEV;
+ LLViewerObject* prim = gObjectList.findObject(prim_id);
+ if (!prim)
+ {
+ return;
+ }
+
+ LLUUID root_id = prim->getRootEdit()->getID();
+
+ mPublishedObjectManager.recordPendingPropertyChange(root_id, prim_id, name, desc);
+
+ bool should_refresh_inventory = mPublishedObjectManager.markPrimInventorySerialAndDetectChange(
+ root_id,
+ prim_id,
+ inventory_serial);
+
+ LLSD update;
+ if (mPublishedObjectManager.applyPropertyChange(root_id, prim_id, name, desc, update))
+ {
+ notifyAll("object.update", update);
+ }
+
+ if (should_refresh_inventory && !mPublishedObjectManager.hasInventoryRequestStart(prim_id))
+ {
+ prim->dirtyInventory();
+ mPublishedObjectManager.setInventoryRequestStart(prim_id, LLTimer::getTotalSeconds().value());
+ prim->requestInventory();
+ }
+}
+
+void LLScriptEditorWSServer::unpublishObject(const LLUUID& object_id, const std::string& reason)
+{
+ LL_PROFILE_ZONE_SCOPED_CATEGORY_SCRIPTDEV;
+ if (!mPublishedObjectManager.cleanupObjectStateForUnpublish(object_id))
+ {
+ return;
+ }
+
+ LLSD message;
+ message["object_id"] = object_id;
+ if (!reason.empty())
+ {
+ message["reason"] = reason;
+ }
+ notifyAll("object.unpublish", message);
+
+ LL_DEBUGS("ScriptEditorWS") << "Unpublished object " << object_id
+ << " reason: " << reason << LL_ENDL;
+}
+
+
+//========================================================================
+std::atomic<U32> LLScriptEditorWSConnection::sNextConnectionID{1};
+
+std::shared_ptr<LLScriptEditorWSServer> LLScriptEditorWSConnection::getServer() const
+{
+ return std::static_pointer_cast<LLScriptEditorWSServer>(mOwningServer.lock());
+}
+
+void LLScriptEditorWSConnection::onOpen()
+{
+ LL_PROFILE_ZONE_SCOPED_CATEGORY_SCRIPTDEV;
+ // 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"] = LLSyntaxDefCache::instance().getSyntaxID();
+
+ // Features object
+ LLSD features;
+ features["live_sync"] = true;
+ features["compilation"] = true;
+ features["syntax_cache"] = true;
+ features["commands"] = true;
+ features["unified_diagnostics"] = true;
+ handshake["features"] = features;
+
+ wptr_t that = weak_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()
+{
+ LL_PROFILE_ZONE_SCOPED_CATEGORY_SCRIPTDEV;
+ // 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(DisconnectReason reason, const std::string& message)
+{
+ LL_INFOS("ScriptEditorWS") << "Sending disconnect to client: " << message << LL_ENDL;
+ LLSD params;
+ params["reason"] = static_cast<S32>(reason);
+ params["message"] = message;
+ notify("session.disconnect", params);
+ closeConnection(1000, message);
+}
+
+void LLScriptEditorWSConnection::handleHandshakeResponse(const LLSD& result)
+{
+ LL_PROFILE_ZONE_SCOPED_CATEGORY_SCRIPTDEV;
+ LL_INFOS("ScriptEditorWS") << "Processing handshake response from client" << LL_ENDL;
+
+ mClientName = result["client_name"].asString();
+ mClientVersion = result["client_version"].asString();
+ mProtocolVersion = result["protocol_version"].asString();
+
+ // Validate challenge response (if a challenge was issued).
+ const bool challenge_issued = mChallenge.notNull();
+ bool valid_response = true;
+ if (challenge_issued)
+ {
+ valid_response = result.has("challenge_response") &&
+ (result["challenge_response"].asUUID() == mChallenge);
+ mChallenge.setNull();
+ }
+
+ // Always clean up the temporary challenge file if one was created,
+ // regardless of validation outcome.
+ if (!mChallengeFile.empty())
+ {
+ LLFile::remove(mChallengeFile);
+ mChallengeFile.clear();
+ }
+
+ if (challenge_issued && !valid_response)
+ {
+ LL_WARNS("ScriptEditorWS") << "Invalid or missing challenge response from client" << LL_ENDL;
+ sendDisconnect(DisconnectReason::PROTOCOL_ERROR, "Invalid challenge response");
+ return;
+ }
+
+ if (mProtocolVersion != "1.0")
+ {
+ LL_WARNS("ScriptEditorWS") << "Protocol version mismatch. Expected: 1.0, Got: "
+ << mProtocolVersion << LL_ENDL;
+ }
+
+ mScriptName = result["script_name"].asString();
+ mScriptLanguage = result["script_language"].asString();
+
+ 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);
+ }
+ }
+
+ 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_" + mChallenge.asString() + ".tmp";
+
+ llofstream file(mChallengeFile.c_str());
+ if (!file.is_open())
+ {
+ LL_WARNS("ScriptEditorWS") << "Unable to open challenge file: " << mChallengeFile << LL_ENDL;
+ mChallenge.setNull();
+ mChallengeFile.clear();
+ return std::string();
+ }
+
+ file << mChallenge;
+ file.close();
+
+ return mChallengeFile;
+}
+