From a6a2f98070cd55fcaadafc237bd0ffba9d466655 Mon Sep 17 00:00:00 2001 From: Rider Linden Date: Wed, 22 Jul 2026 17:41:55 -0700 Subject: Publish object inventories to external editor. (#5834) * Adding tight integration flag for vscode and open code through a URL. * [WIP] Publish objects and their contents from the viewer into VS code. * [WIP] Still very much a work in progress, supports most of the core operations publish, get, write, delete, and create. Still quite a few bugs to work out. * [WIP] Object publishing checkpoint. * [checkpoint] Script tight integration with vscode, object publishing. * Number of fixed issue. * A few redundancy and performance fixes. * Some cosmetics. * I like "Explore" better than "Publish" * Object renaming, luau inventory icon, runstate, restart. * Some clean up around permissions and possible nullptr deref. * Code review feedback. --- indra/newview/llscripteditorws.cpp | 2170 +++++++++++++++++++++++++++++++++--- 1 file changed, 1993 insertions(+), 177 deletions(-) (limited to 'indra/newview/llscripteditorws.cpp') diff --git a/indra/newview/llscripteditorws.cpp b/indra/newview/llscripteditorws.cpp index 3ca9be44bc..e0bf9b0b61 100644 --- a/indra/newview/llscripteditorws.cpp +++ b/indra/newview/llscripteditorws.cpp @@ -29,18 +29,178 @@ #include "llviewerprecompiledheaders.h" #include "llscripteditorws.h" -#include "llpreviewscript.h" + +#include "llagent.h" #include "llappviewer.h" -#include "lltrans.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 "llpreviewnotecard.h" +#include "llpreviewscript.h" +#include "llprocess.h" +#include "llregex.h" +#include "llsdjson.h" +#include "llselectmgr.h" +#include "lltrans.h" #include "lluuid.h" #include "llversioninfo.h" -#include "llagent.h" -#include "llregex.h" +#include "llviewerassetstorage.h" +#include "llviewerassettype.h" +#include "llviewerassetupload.h" +#include "llviewercontrol.h" +#include "llviewerinventory.h" #include "llviewerobject.h" #include "llviewerobjectlist.h" -#include "llchat.h" +#include "llviewerregion.h" +#include "llviewertexteditor.h" +#include "llvoinventorylistener.h" +#include "roles_constants.h" + +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; + + // Creates a uniquely-named LLEventMailDrop under ".", 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 + 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)(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": }. + 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 collect_linkset(LLViewerObject* root) + { + std::vector 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); + } +} + +LLCachedControl LLScriptEditorWSServer::sEnableScriptEditorWS(gSavedSettings, "ExternalWebsocketSyncEnable", false); +LLCachedControl LLScriptEditorWSServer::sTightIntegration(gSavedSettings, "ExternalEditorTightIntegration", false); + + +class LLPublishedPrimListener : public LLVOInventoryListener +{ +public: + LLPublishedPrimListener(LLScriptEditorWSServer* server, const LLUUID& object_id, const LLUUID& prim_id, + LLViewerObject* object) + : mServer(server) + , mObjectID(object_id) + , mPrimID(prim_id) + { + registerVOInventoryListener(object, nullptr); + } + + ~LLPublishedPrimListener() override = default; + + void inventoryChanged(LLViewerObject* object, + LLInventoryObject::object_list_t* inventory, + S32 serial_num, void* user_data) override + { + if (mServer) + { + if (mServer->isObjectPublished(mObjectID)) + { + mServer->onPrimInventoryChanged(mObjectID, mPrimID); + } + else + { + mServer->onPrimInventoryReady(mObjectID, mPrimID); + } + } + } + + const LLUUID& getObjectID() const { return mObjectID; } + const LLUUID& getPrimID() const { return mPrimID; } + +private: + LLScriptEditorWSServer* mServer; // non-owning; server always outlives listeners + LLUUID mObjectID; // root object this prim belongs to + LLUUID mPrimID; // this specific prim +}; //======================================================================== LLScriptEditorWSServer::LLScriptEditorWSServer(const std::string& name, U16 port, bool local_only) @@ -61,6 +221,100 @@ LLScriptEditorWSServer::ptr_t LLScriptEditorWSServer::getServer() wsmgr.findServerByName(LLScriptEditorWSServer::DEFAULT_SERVER_NAME)); } +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( + wsmgr.findServerByName(DEFAULT_SERVER_NAME)); + + if (!server) + { + U16 port = static_cast(gSavedSettings.getS32("ExternalWebsocketSyncPort")); + bool local_only = gSavedSettings.getBOOL("ExternalWebsocketSyncLocal"); + server = std::make_shared(DEFAULT_SERVER_NAME, port, local_only); + wsmgr.addServer(server); + } + + if (!server->isRunning()) + { + if (!wsmgr.startServer(DEFAULT_SERVER_NAME)) + { + LL_WARNS("ScriptEditorWS") << "Failed to start script editor websocket server" << LL_ENDL; + return nullptr; + } + } + + return server; +} + +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(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) @@ -95,6 +349,26 @@ 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. + + for (auto& [id, pending] : mPendingPublishes) + { + pending.mListeners.clear(); + } + mPendingPublishes.clear(); + + for (auto& [id, info] : mPublishedObjects) + { + info.mListeners.clear(); + } + mPublishedObjects.clear(); + + mSubscriptions.clear(); + mActiveConnections.clear(); + + LL_INFOS("ScriptEditorWS") << "Script editor WebSocket server stopped, all state cleaned up" << LL_ENDL; } void LLScriptEditorWSServer::onConnectionOpened(const LLWebsocketMgr::WSConnection::ptr_t& connection) @@ -130,22 +404,24 @@ void LLScriptEditorWSServer::onConnectionClosed(const LLWebsocketMgr::WSConnecti bool LLScriptEditorWSServer::subscribeScriptEditor(const LLUUID& object_id, const LLUUID& item_id, std::string_view script_name, const LLHandle& editor_handle, const std::string& script_id) { - if (!editor_handle.isDead()) + 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; + } + + auto it = mSubscriptions.find(script_id); + if (it == mSubscriptions.end()) + { + // New subscription + mSubscriptions.emplace(script_id, + EditorSubscription(object_id, item_id, script_name, editor_handle)); } - return false; + else + { + // Refresh existing subscription with the new editor handle + it->second.mEditorHandle = editor_handle; + } + return true; } void LLScriptEditorWSServer::unsubscribeEditor(const std::string &script_id) @@ -156,14 +432,32 @@ void LLScriptEditorWSServer::unsubscribeEditor(const std::string &script_id) 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) + + // Maintain per-connection count; erase entry when it hits zero. + bool last_for_connection = false; + if (connection_id != 0) + { + auto cit = mConnectionSubscriptionCounts.find(connection_id); + if (cit != mConnectionSubscriptionCounts.end()) + { + if (--cit->second <= 0) + { + mConnectionSubscriptionCounts.erase(cit); + last_for_connection = true; + } + } + else + { + // No counter entry means no other subs referenced this connection. + last_for_connection = true; + } + } + + if (connection && last_for_connection) { // 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"); + connection->sendDisconnect(LLScriptEditorWSConnection::DisconnectReason::EDITOR_CLOSED, "Editor closed"); } } @@ -181,9 +475,11 @@ void LLScriptEditorWSServer::unsubscribeConnection(U32 connection_id) it->second.mConnection.reset(); } } + // All subs for this connection now have mConnectionID == 0. + mConnectionSubscriptionCounts.erase(connection_id); } -LLScriptEditorWSServer::SubscriptionError_t LLScriptEditorWSServer::updateScriptSubscription(const std::string &script_id, U32 connection_id) +LLScriptEditorWSServer::SubscriptionError LLScriptEditorWSServer::updateScriptSubscription(const std::string &script_id, U32 connection_id) { auto it = mSubscriptions.find(script_id); if (it != mSubscriptions.end()) @@ -191,13 +487,13 @@ LLScriptEditorWSServer::SubscriptionError_t LLScriptEditorWSServer::updateScript if (it->second.mEditorHandle.isDead()) { unsubscribeEditor(script_id); - return SUBSCRIPTION_INVALID_EDITOR; + return SubscriptionError::INVALID_EDITOR; } auto con_it = mActiveConnections.find(connection_id); if (con_it == mActiveConnections.end()) { - return SUBSCRIPTION_INTERNAL_ERROR; + return SubscriptionError::INTERNAL_ERROR; } if ((it->second.mConnectionID != 0) && !it->second.mConnection.expired() @@ -207,14 +503,18 @@ LLScriptEditorWSServer::SubscriptionError_t LLScriptEditorWSServer::updateScript << ", 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; + 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; - return SUBSCRIPTION_SUCCESS; + ++mConnectionSubscriptionCounts[connection_id]; + return SubscriptionError::SUCCESS; } - return SUBSCRIPTION_INVALID_SUBSCRIPTION; + return SubscriptionError::INVALID_SUBSCRIPTION; } @@ -251,76 +551,411 @@ void LLScriptEditorWSServer::setupConnectionMethods(LLJSONRPCConnection::ptr_t c if (script_connection) { LL_DEBUGS("ScriptEditorWS") << "Setting up script editor connection methods" << LL_ENDL; - wptr_t that(std::static_pointer_cast(shared_from_this())); - 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", - [that](const std::string&, const LLSD&, const LLSD&) -> LLSD + bindHandler([](LLScriptEditorWSServer& s, auto&, auto&, auto&) { - auto server = that.lock(); - if (server) - { - return server->handleLanguageIdRequest(); - } - return LLSD(); - }); + return s.handleLanguageIdRequest(); + })); + script_connection->registerMethod("language.syntax", - [that](const std::string&, const LLSD&, const LLSD& params) + bindHandler([](LLScriptEditorWSServer& s, auto&, auto&, const LLSD& params) { - auto server = that.lock(); - if (server) - { - return server->handleSyntaxRequest(params); - } - return LLSD(); - }); + return s.handleSyntaxRequest(params); + })); + script_connection->registerMethod("language.syntax.cache", - [that](const std::string&, const LLSD&, const LLSD& params) + bindHandler([](LLScriptEditorWSServer& s, auto&, auto&, auto&) { - auto server = that.lock(); - if (server) - { - return server->handleSyntaxCacheRequest(); - } - return LLSD(); - }); + return s.handleSyntaxCacheRequest(); + })); + script_connection->registerMethod("language.syntax.get", - [that](const std::string&, const LLSD&, const LLSD& params) + bindHandler([](LLScriptEditorWSServer& s, auto&, auto&, const LLSD& params) { - auto server = that.lock(); - if (server) - { - return server->handleSyntaxCacheFileRequest(params); - } - return LLSD(); - }); + return s.handleSyntaxCacheFileRequest(params); + })); + script_connection->registerMethod("script.subscribe", - [that, connection_id](const std::string&, const LLSD&, const LLSD& params) -> LLSD + bindHandler([connection_id](LLScriptEditorWSServer& s, auto&, auto&, const LLSD& params) { - 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(); - }); + return s.handleScriptSubscribe(connection_id, params); + })); + script_connection->registerMethod("script.list", - [that](const std::string&, const LLSD&, const LLSD& params) -> LLSD + bindHandler([](LLScriptEditorWSServer& s, auto&, auto&, auto&) { - auto server = that.lock(); - if (server) - { - return server->handleFileWatcherFileListRequest(); - } - return LLSD(); - }); - // script_connection->registerMethod("language.syntax", ) + return s.handleFileWatcherFileListRequest(); + })); + + script_connection->registerMethod("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); + })); + } +} + +LLSD LLScriptEditorWSServer::handleObjectList() const +{ + LLSD objects = LLSD::emptyArray(); + for (const auto& [object_id, info] : mPublishedObjects) + { + LLViewerObject* root = gObjectList.findObject(object_id); + if (!root) + { + LL_DEBUGS("ScriptEditorWS") << "object.list: skipping " << object_id + << " (no longer in scene)" << LL_ENDL; + continue; + } + + // Use cached names from PublishedObjectInfo, but fetch live inventory + LLSD pub; + pub["object_id"] = info.mObjectID; + pub["object_name"] = info.mObjectName; + pub["object_description"] = info.mObjectDescription; + pub["owner_id"] = info.mOwnerID; + if (!info.mRegionName.empty()) + { + pub["region"] = info.mRegionName; + } + pub["inventory"] = buildPrimInventoryLLSD(root); + + LLSD linked_objects = LLSD::emptyArray(); + for (const auto& prim_info : info.mPrims) + { + if (prim_info.mLinkNumber == 1) continue; // skip root + + LLViewerObject* child = gObjectList.findObject(prim_info.mPrimID); + if (!child) continue; + + LLSD link; + link["link_id"] = prim_info.mPrimID; + link["link_number"] = prim_info.mLinkNumber; + link["link_name"] = prim_info.mPrimName; // Cached name + link["inventory"] = buildPrimInventoryLLSD(child); + linked_objects.append(link); + } + if (linked_objects.size() > 0) + { + pub["linked_objects"] = linked_objects; + } + + objects.append(pub); + } + + LLSD response; + response["objects"] = objects; + 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(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(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(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 new_item = + new LLViewerInventoryItem(static_cast(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(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::broadcastLanguageChange() @@ -452,34 +1087,34 @@ LLSD LLScriptEditorWSServer::handleScriptSubscribe(U32 connection_id, const LLSD std::string script_name = params["script_name"].asString(); std::string language = params["script_language"].asString(); - SubscriptionError_t result = updateScriptSubscription(script_id, connection_id); + SubscriptionError result = updateScriptSubscription(script_id, connection_id); response["script_id"] = script_id; - response["success"] = (result == SUBSCRIPTION_SUCCESS); - response["status"] = result; + response["success"] = (result == SubscriptionError::SUCCESS); + response["status"] = static_cast(result); - LL_WARNS_IF(result != SUBSCRIPTION_SUCCESS, "ScriptEditorWS") - << "Script connect request for script " << script_id << " failed with status " << result << LL_ENDL; + LL_WARNS_IF(result != SubscriptionError::SUCCESS, "ScriptEditorWS") + << "Script connect request for script " << script_id << " failed with status " << static_cast(result) << LL_ENDL; switch (result) { - case SUBSCRIPTION_SUCCESS: + case SubscriptionError::SUCCESS: response["message"] = "OK"; break; - case SUBSCRIPTION_INVALID_EDITOR: + case SubscriptionError::INVALID_EDITOR: response["message"] = "Invalid editor handle"; break; - case SUBSCRIPTION_INVALID_SUBSCRIPTION: + case SubscriptionError::INVALID_SUBSCRIPTION: response["message"] = "No subscription found for script"; break; - case SUBSCRIPTION_ALREADY_SUBSCRIBED: + case SubscriptionError::ALREADY_SUBSCRIBED: response["message"] = "Script already subscribed"; break; - case SUBSCRIPTION_INTERNAL_ERROR: + case SubscriptionError::INTERNAL_ERROR: response["message"] = "Internal server error"; break; } - if (result == SUBSCRIPTION_SUCCESS) + if (result == SubscriptionError::SUCCESS) { auto it = mSubscriptions.find(script_id); if (it != mSubscriptions.end()) @@ -525,14 +1160,611 @@ LLSD LLScriptEditorWSServer::handleFileWatcherFileListRequest() const return response; } -void LLScriptEditorWSServer::notifyScript(const std::string& script_id, const std::string &method, const LLSD& message) const +LLSD LLScriptEditorWSServer::handleObjectRequest(U32 connection_id, const LLSD& params) { - auto it = mSubscriptions.find(script_id); - if (it != mSubscriptions.end()) + LLUUID object_id = params["object_id"].asUUID(); + LLSD response; + + if (object_id.isNull()) { - auto connection = it->second.mConnection.lock(); - if (connection) - { + response["success"] = false; + response["message"] = "No object_id specified"; + return response; + } + + LLViewerObject* object = gObjectList.findObject(object_id); + if (!object) + { + response["success"] = false; + response["message"] = "Object not found"; + return response; + } + + if (!object->permModify()) + { + response["success"] = false; + response["message"] = "Permission denied"; + return response; + } + + bool accepted = publishObject(object_id); + response["success"] = accepted; + if (!accepted) + { + response["message"] = "Failed to initiate publish"; + } + return response; +} + +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(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(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(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(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 buffer(file_length + 1); + file.read(reinterpret_cast(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"; + } + } + + 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", + [&](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( + prim->getID(), item->getUUID(), + 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->getID(); + response["item_id"] = item->getUUID(); + response["compiled"] = cb_result["compiled"]; + if (!cb_result["compiled"].asBoolean() && cb_result.has("errors")) + { + response["errors"] = cb_result["errors"]; + } + + // If the script is open in the viewer's editor, update it + LLSD floater_key; + floater_key["taskid"] = prim->getID(); + floater_key["itemid"] = item->getUUID(); + LLLiveLSLEditor* editor = LLFloaterReg::findTypedInstance("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) +{ + 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", + [&](const std::string& pump_name) + { + auto [on_success, on_failure] = make_asset_upload_callbacks(pump_name); + LLResourceUploadInfo::ptr_t uploadInfo(std::make_shared( + prim->getID(), item->getUUID(), + 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->getID(); + response["item_id"] = item->getUUID(); + + // If the notecard is open in the viewer's editor, update it + LLSD floater_key; + floater_key["taskid"] = prim->getID(); + floater_key["itemid"] = item->getUUID(); + LLPreviewNotecard* nc = LLFloaterReg::findTypedInstance("preview_notecard", floater_key); + if (nc) + { + LLViewerTextEditor* nc_editor = nc->getChild("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); + + v.prim->removeInventory(v.item->getUUID()); + + LLSD response; + response["success"] = true; + response["prim_id"] = params["prim_id"].asUUID(); + response["item_id"] = params["item_id"].asUUID(); + 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"); + + auto it = mPublishedObjects.find(object_id); + if (it == mPublishedObjects.end()) + 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 existing_items; + { + LLInventoryObject::object_list_t inv; + prim->getInventoryContents(inv); + for (auto& obj : inv) + { + existing_items.insert(obj->getUUID()); + } + } + + // 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 (mPendingItemCreates.find(prim_id) != mPendingItemCreates.end()) + { + throw LLJSONRPCConnection::InvalidRequest( + "An item.create is already in flight for this prim"); + } + + // Set up event pump to wait for inventory change + LLEventMailDrop result_pump("objectItemCreate." + LLUUID::generateNewID().asString(), true); + mPendingItemCreates[prim_id] = result_pump.getName(); + + // 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 pending_guard(nullptr, [this, prim_id](void*) + { + mPendingItemCreates.erase(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 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(sub_type); + } + + LLSD perm_entry; + perm_entry["owner"] = static_cast(perms.getMaskOwner()); + perm_entry["next_owner"] = static_cast(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(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(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(item_perms.getMaskOwner()); + perm_entry["next_owner"] = static_cast(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); } } @@ -541,6 +1773,7 @@ void LLScriptEditorWSServer::notifyScript(const std::string& script_id, const st void LLScriptEditorWSServer::sendUnsubscribeScriptEditor(const std::string& script_id) { + LL_PROFILE_ZONE_SCOPED_CATEGORY_SCRIPTDEV; LLSD params; params["script_id"] = script_id; @@ -549,6 +1782,7 @@ void LLScriptEditorWSServer::sendUnsubscribeScriptEditor(const std::string& scri void LLScriptEditorWSServer::sendCompileResults(const std::string &script_id, const LLSD &results) const { + LL_PROFILE_ZONE_SCOPED_CATEGORY_SCRIPTDEV; LLHandle editor_handle = findEditorForScript(script_id); if (editor_handle.isDead()) { @@ -638,6 +1872,7 @@ void LLScriptEditorWSServer::sendCompileResults(const std::string &script_id, co void LLScriptEditorWSServer::forwardChatToIDE(const LLChat& chat_msg) const { + LL_PROFILE_ZONE_SCOPED_CATEGORY_SCRIPTDEV; auto it = std::find_if(mSubscriptions.begin(), mSubscriptions.end(), [&chat_msg](const auto& pair) { return (pair.second.mObjectID == chat_msg.mFromID); }); @@ -656,7 +1891,12 @@ void LLScriptEditorWSServer::forwardChatToIDE(const LLChat& chat_msg) const std::vector lines = LLStringUtil::getTokens(chat_msg.mText, "\n"); // If this is a runtime error, the first line will look like: " [script: