diff options
Diffstat (limited to 'indra/newview/llscripteditorws.h')
| -rw-r--r-- | indra/newview/llscripteditorws.h | 352 |
1 files changed, 352 insertions, 0 deletions
diff --git a/indra/newview/llscripteditorws.h b/indra/newview/llscripteditorws.h new file mode 100644 index 0000000000..1dc218853b --- /dev/null +++ b/indra/newview/llscripteditorws.h @@ -0,0 +1,352 @@ +/** + * @file llscripteditorws.h + * @brief WebSocket server and connection classes for external script editor integration + * + * $LicenseInfo:firstyear=2025&license=viewerlgpl$ + * Second Life Viewer Source Code + * Copyright (C) 2025, Linden Research, Inc. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; + * version 2.1 of the License only. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * + * Linden Research, Inc., 945 Battery Street, San Francisco, CA 94111 USA + * $/LicenseInfo$ + */ + +#pragma once + +#include "lljsonrpcws.h" +#include "llpublishedobjectmgr.h" +#include "llsd.h" +#include "lluuid.h" +#include "llhandle.h" +#include "lltimer.h" +#include "lleventtimer.h" + +#include <memory> +#include <string> +#include <vector> +#include <map> +#include <set> +#include <atomic> +#include <functional> + +// Forward declarations +class LLLiveLSLEditor; +class LLScriptEdContainer; +class LLScriptEditorWSServer; +class LLChat; +class LLPanel; +class LLViewerObject; +class LLInventoryItem; + +class LLScriptEditorWSConnection : public LLJSONRPCConnection, public std::enable_shared_from_this<LLScriptEditorWSConnection> +{ +public: + using ptr_t = std::shared_ptr<LLScriptEditorWSConnection>; + using wptr_t = std::weak_ptr<LLScriptEditorWSConnection>; + + enum class DisconnectReason : S32 + { + NORMAL = 0, + EDITOR_CLOSED = 1, + PROTOCOL_ERROR = 2, + TIMEOUT = 3, + INTERNAL_ERROR = 4 + }; + + LLScriptEditorWSConnection(const LLWebsocketMgr::WSServer::ptr_t server, const LLWebsocketMgr::connection_h& handle) : + LLJSONRPCConnection(server, handle) + { + // Reserve id 0 as the "unassigned" sentinel used by EditorSubscription; + // on wrap, skip past it. + U32 id; + do + { + id = sNextConnectionID.fetch_add(1, std::memory_order_relaxed); + } + while (id == 0); + mConnectionID = id; + } + + ~LLScriptEditorWSConnection() override = default; + + U32 getConnectionID() const { return mConnectionID; } + + // Connection lifecycle overrides + void onOpen() override; + void onClose() override; + + void sendDisconnect(DisconnectReason reason = DisconnectReason::NORMAL, const std::string& message = "Goodbye"); + bool hasFeature(const std::string& feature) const; + +private: + using string_set_t = std::set<std::string>; + /** + * @brief Handle the handshake response from the client + * @param result The response data from the client containing client information + */ + void handleHandshakeResponse(const LLSD& result); + std::string generateChallenge(); + + LLScriptEdContainer* getEditor() const; + std::shared_ptr<LLScriptEditorWSServer> getServer() const; + + U32 mConnectionID{ 0 }; ///< Unique identifier for this connection + + // Client handshake response data + std::string mClientName; ///< Name of the external editor client + std::string mClientVersion; ///< Version of the external editor client + std::string mProtocolVersion; ///< JSON-RPC protocol version supported by client + std::string mScriptName; ///< Name of the script being edited + std::string mScriptLanguage; ///< Programming language of the script (lsl, luau, etc.) + string_set_t mLanguages; ///< Set of supported scripting languages + string_set_t mFeatures; ///< Active client features (live_sync, compilation, etc.) + LLUUID mChallenge; + std::string mChallengeFile; ///< Temporary file used for challenge-response verification + + static std::atomic<U32> sNextConnectionID; +}; + +/** + * @class LLScriptEditorWSServer + * @brief JSON-RPC 2.0 WebSocket server for external script editor integration + * + * This server extends the JSON-RPC server to provide specialized functionality + * for external script editor integration. It manages WebSocket connections from + * external script editors and provides a structured JSON-RPC 2.0 interface + * between the Second Life viewer's script editing functionality and external + * development tools. + * + * ## Architecture + * + * The server acts as a JSON-RPC communication hub between: + * - LLLiveLSLEditor instances (in-world script editing) + * - External script editors (VS Code, Atom, Sublime Text, etc.) + * - Script compilation and save services + * + * ## Usage + * + * @code + * // Create and start the JSON-RPC server + * auto server = std::make_shared<LLScriptEditorWSServer>("script_editor_server", 9020); + * LLWebsocketMgr::getInstance()->addServer(server); + * LLWebsocketMgr::getInstance()->startServer("script_editor_server"); + * + * // Associate with an LSL editor + * server->associateEditor(editor_handle, script_id); + * @endcode + * + * ## Security Considerations + * + * - Server binds to localhost only by default for security + * - JSON-RPC 2.0 structured protocol with validation + * - Rate limiting handled by base JSON-RPC server + * - Error handling with standardized JSON-RPC error codes + */ +class LLScriptEditorWSServer : public LLJSONRPCServer +{ +public: + struct ItemRef + { + LLUUID mRootID; + LLUUID mPrimID; + LLUUID mItemID; + std::string mScriptName; + }; + + static constexpr U32 ALL_CONNECTIONS = 0xFFFFFFFF; + enum class SubscriptionError + { + SUCCESS = 0, + INVALID_EDITOR, + INVALID_SUBSCRIPTION, + ALREADY_SUBSCRIBED, + INTERNAL_ERROR + }; + + static constexpr const char* DEFAULT_SERVER_NAME = "script_editor_server"; + static constexpr U16 DEFAULT_SERVER_PORT = 9020; + + using ptr_t = std::shared_ptr<LLScriptEditorWSServer>; + using wptr_t = std::weak_ptr<LLScriptEditorWSServer>; + + LLScriptEditorWSServer(const std::string& name, U16 port, bool local_only = true); + + ~LLScriptEditorWSServer() override = default; + + static LLScriptEditorWSServer::ptr_t getServer(); + static LLScriptEditorWSServer::ptr_t ensureServerRunning(); + static std::string buildScriptSubscriptionId(const LLUUID& object_id, + const LLUUID& item_id); + static std::string buildVSCodeURI(const LLUUID& object_id = LLUUID::null, + const LLUUID& script_id = LLUUID::null); + static bool launchVSCode(const LLUUID& object_id = LLUUID::null, + const LLUUID& script_id = LLUUID::null); + + void onStarted() override; + void onStopped() override; + void onConnectionOpened(const LLWebsocketMgr::WSConnection::ptr_t& connection) override; + void onConnectionClosed(const LLWebsocketMgr::WSConnection::ptr_t& connection) override; + + bool subscribeScriptEditor(const LLUUID& object_id, const LLUUID& item_id, std::string_view script_name, + const LLHandle<LLPanel>& editor_handle, const std::string &script_id); + void unsubscribeEditor(const std::string &script_id); + + void notifyScript(const std::string& script_id, const std::string& method, const LLSD& message) const; + void sendUnsubscribeScriptEditor(const std::string& script_id); + void sendCompileResults(const std::string& script_id, const LLSD& results) const; + + LLHandle<LLPanel> findEditorForScript(const std::string& script_id) const; + + void forwardChatToIDE( + const LLChat& chat_msg, + LLPublishedObjectMgr::RuntimeEventAggregator::Channel channel) const; + + std::set<std::string> getActiveScripts() const; + + // --- Object Content Publishing --- + bool publishObject(const LLUUID& object_id); + void unpublishObject(const LLUUID& object_id, const std::string& reason = ""); + bool isObjectPublished(const LLUUID& object_id) const; + + // *TODO*: These should be moved to LLPublishedObjectMgr at some point. + void onPrimInventoryReady(const LLUUID& object_id, const LLUUID& prim_id); + void onPrimInventoryChanged(const LLUUID& object_id, const LLUUID& prim_id); + void onObjectPropertyChanged(const LLUUID& prim_id, const std::string& name, const std::string& desc, S16 inventory_serial = -1); + void onLinksetChildAdded(const LLUUID& root_id, LLViewerObject* child); + void onLinksetChildRemoved(const LLUUID& root_id, const LLUUID& child_id); + + static bool isEnabled(); + static bool isTightIntegration(); + +protected: + LLWebsocketMgr::WSConnection::ptr_t connectionFactory(LLWebsocketMgr::WSServer::ptr_t server, + LLWebsocketMgr::connection_h handle) override; + + void setupConnectionMethods(LLJSONRPCConnection::ptr_t connection) override; + + void broadcastLanguageChange(); + + LLSD handleLanguageIdRequest() const; + LLSD handleSyntaxRequest(const LLSD ¶ms) const; + LLSD handleSyntaxCacheRequest() const; + LLSD handleSyntaxCacheFileRequest(const LLSD& params) const; + LLSD handleScriptSubscribe(U32 connection_id, const LLSD& params); + LLSD handleScriptUnsubscribe(U32 connection_id, const LLSD& params); + LLSD handleFileWatcherFileListRequest() const; + LLSD handleObjectRequest(U32 connection_id, const LLSD& params); + LLSD handleObjectContentGet(const std::string& method, const LLSD& id, const LLSD& params); + LLSD handleObjectContentSave(const std::string& method, const LLSD& id, const LLSD& params); + LLSD saveScript(LLViewerObject* prim, LLInventoryItem* item, const std::string& content, const LLSD& params); + LLSD saveNotecard(LLViewerObject* prim, LLInventoryItem* item, const std::string& content); + LLSD handleObjectItemDelete(U32 connection_id, const LLSD& params); + LLSD handleObjectItemCreate(const std::string& method, const LLSD& id, const LLSD& params); + LLSD handleObjectUnpublish(U32 connection_id, const LLSD& params); + LLSD handleObjectList() const; + LLSD handleObjectScriptSetRunning(U32 connection_id, const LLSD& params); + LLSD handleObjectScriptReset(U32 connection_id, const LLSD& params); + LLSD handleObjectModify(U32 connection_id, const LLSD& params); + LLSD handleObjectItemModify(U32 connection_id, const LLSD& params); + LLSD handleCommandExecute(U32 connection_id, const LLSD& params); + LLSD handleSaveBackToObjectContents(U32 connection_id, const LLSD& params); + LLSD handleCommandList(); + void sendCommandExecute(U32 connection_id, const std::string& command, const LLSD& params); + + struct ValidatedItem + { + LLViewerObject* prim{ nullptr }; + LLViewerObject* root{ nullptr }; + LLInventoryItem* item{ nullptr }; + LLAssetType::EType type{ LLAssetType::AT_NONE }; + }; + ValidatedItem validatePublishedItem(const LLSD& params, U32 permMask) const; + + // --- Object Content Publishing (helpers) --- + static std::string getPrimName(LLViewerObject* obj); + void notifyConnection(U32 connection_id, const std::string& method, const LLSD& params) const; + void notifyAll(const std::string& method, const LLSD& params) const; + void buildAndSendPublish(const LLUUID& object_id); + void scheduleLinksetFlush(const LLUUID& root_id, F32 delay); + void cancelLinksetFlushTimer(const LLUUID& root_id); + void flushLinksetUpdate(const LLUUID& root_id); + + /// Wraps `fn` in a MethodHandler with a weak-ptr guard on this server, + /// so the handler safely no-ops after server shutdown. `fn` is called + /// with (LLScriptEditorWSServer&, method, id, params) and returns LLSD. + template <typename Fn> + LLJSONRPCConnection::MethodHandler bindHandler(Fn fn) + { + std::weak_ptr<LLWebsocketMgr::WSServer> weak_base = weak_from_this(); + return [weak_base, fn = std::move(fn)] + (const std::string& method, const LLSD& id, const LLSD& params) -> LLSD + { + auto base = weak_base.lock(); + if (!base) + { + return LLSD(); + } + auto server = std::static_pointer_cast<LLScriptEditorWSServer>(base); + return fn(*server, method, id, params); + }; + } + +private: + void sendRuntimeEvent( + const LLPublishedObjectMgr::RuntimeChatEvent& event) const; + + struct EditorSubscription + { + EditorSubscription(const ItemRef& item_ref, LLHandle<LLPanel> editor_handle): + mItemRef(item_ref), + mEditorHandle(editor_handle) + { + } + U32 mConnectionID{ 0 }; + ItemRef mItemRef; + LLScriptEditorWSConnection::wptr_t mConnection; + LLHandle<LLPanel> mEditorHandle; + }; + using subscriptions_t = std::unordered_map<std::string, EditorSubscription>; + + SubscriptionError updateScriptSubscription(const std::string &script_id, U32 connection_id); + void unsubscribeConnection(U32 connection_id); + + subscriptions_t mSubscriptions; + std::unordered_map<U32, S32> mConnectionSubscriptionCounts; + std::map<U32, LLScriptEditorWSConnection::wptr_t> mActiveConnections; + + mutable LLPublishedObjectMgr mPublishedObjectManager; + + struct WSCommandInfo + { + std::string command; + std::string description; + }; + enum WSCommandError + { + UnknownCommand = 1, + InvalidParams = 2, + NotPermitted = 3, + ExecutionError = 4, + }; + using WSCommandHandler = std::function<LLSD(U32 connection_id, const LLSD& params)>; + std::unordered_map<std::string, std::pair<WSCommandInfo, WSCommandHandler>> mCommandRegistry; + + void registerCommand(const WSCommandInfo& info, WSCommandHandler handler); + + boost::signals2::connection mLanguageChangeSignal; + LLUUID mLastSyntaxId; + +}; |
