Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
525 changes: 471 additions & 54 deletions doc/external-editor-json-rpc.md

Large diffs are not rendered by default.

48 changes: 47 additions & 1 deletion indra/llcorehttp/lljsonrpcws.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@
#include "llerror.h"
#include "llsdjson.h"
#include "lldate.h"
#include "llcoros.h"
#include "llmainthreadtask.h"

#include <boost/json.hpp>

Expand Down Expand Up @@ -153,7 +155,44 @@ void LLJSONRPCConnection::processRequest(const LLSD& request)
LL_DEBUGS("JSONRPC") << "Processing " << (is_notification ? "notification" : "request")
<< " for method: " << method << LL_ENDL;

// Find method handler
// Check async handlers first — launched as a coroutine, response sent by the lambda
auto async_it = mAsyncMethodHandlers.find(method);
if (async_it != mAsyncMethodHandlers.end())
{
if (is_notification)
{
LL_WARNS("JSONRPC") << "Async method " << method
<< " called as notification; ignoring" << LL_ENDL;
return;
}
ptr_t conn = std::static_pointer_cast<LLJSONRPCConnection>(getSelfPtr());
MethodHandler handler = async_it->second;
LLMainThreadTask::dispatch(
[handler, method, id, params, conn]()
{
LLCoros::instance().launch(
"JSONRPC::" + method,
[handler, method, id, params, conn]()
{
try
{
LLSD result = handler(method, id, params);
conn->sendResponse(id, result);
}
catch (const RPCError& e)
{
conn->sendError(id, e);
}
catch (const std::exception& e)
{
Comment on lines +179 to +187
conn->sendError(id, InternalError(e.what()));
}
});
});
return;
}

// Find sync method handler
auto it = mMethodHandlers.find(method);
if (it == mMethodHandlers.end())
{
Expand Down Expand Up @@ -321,9 +360,16 @@ void LLJSONRPCConnection::registerMethod(const std::string& method, MethodHandle
LL_DEBUGS("JSONRPC") << "Registered method: " << method << LL_ENDL;
}

void LLJSONRPCConnection::registerAsyncMethod(const std::string& method, MethodHandler handler)
{
mAsyncMethodHandlers[method] = handler;
LL_DEBUGS("JSONRPC") << "Registered async method: " << method << LL_ENDL;
}

void LLJSONRPCConnection::unregisterMethod(const std::string& method)
{
mMethodHandlers.erase(method);
mAsyncMethodHandlers.erase(method);
LL_DEBUGS("JSONRPC") << "Unregistered method: " << method << LL_ENDL;
}

Expand Down
14 changes: 14 additions & 0 deletions indra/llcorehttp/lljsonrpcws.h
Original file line number Diff line number Diff line change
Expand Up @@ -249,6 +249,19 @@ class LLJSONRPCConnection : public LLWebsocketMgr::WSConnection
*/
void registerMethod(const std::string& method, MethodHandler handler);

/**
* @brief Register an async method handler, executed in a coroutine
*
* Unlike registerMethod(), the handler runs inside an LLCoros coroutine
* and may use llcoro::suspendUntilEventOn* to wait for async results.
* The handler returns its result normally; the framework sends the
* JSON-RPC response automatically when the coroutine returns.
*
* @param method The method name to register
* @param handler The coroutine-safe function to call
*/
void registerAsyncMethod(const std::string& method, MethodHandler handler);

/**
* @brief Unregister a method handler
* @param method The method name to unregister
Expand Down Expand Up @@ -338,6 +351,7 @@ class LLJSONRPCConnection : public LLWebsocketMgr::WSConnection

private:
std::unordered_map<std::string, MethodHandler> mMethodHandlers;
std::unordered_map<std::string, MethodHandler> mAsyncMethodHandlers;
std::unordered_map<std::string, ResponseCallback> mPendingRequests;
};

Expand Down
26 changes: 26 additions & 0 deletions indra/llcorehttp/llwebsocketmgr.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -441,6 +441,9 @@ void LLWebsocketMgr::WSServer::stop()

mShouldStop = true;

// Send close frames to all connected clients before stopping the ASIO loop
closeAllConnections(1001, "Server shutting down");

// Stop the websocket server (this will cause the controlled run loop to exit)
mImpl->stop();
} // Release the lock here
Expand Down Expand Up @@ -672,3 +675,26 @@ bool LLWebsocketMgr::WSConnection::isConnected() const
}
return server->getConnectionState(mConnectionHandle) == connection_open;
}

LLWebsocketMgr::WSConnection::ptr_t LLWebsocketMgr::WSConnection::getSelfPtr()
{
auto server = mOwningServer.lock();
if (!server) return nullptr;
return server->getConnection(mConnectionHandle);
}

void LLWebsocketMgr::WSServer::closeAllConnections(U16 code, const std::string& reason)
{
std::vector<connection_h> handles;
{
LLMutexLock lock(&mConnectionMutex);
for (const auto& [handle, conn] : mConnections)
{
handles.push_back(handle);
}
}
for (const auto& handle : handles)
{
closeConnection(handle, code, reason);
}
}
5 changes: 5 additions & 0 deletions indra/llcorehttp/llwebsocketmgr.h
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,10 @@ class LLWebsocketMgr: public LLSingleton<LLWebsocketMgr>
bool isConnected() const;

protected:
/// Returns a shared_ptr to this connection, retrieved from the owning server.
/// Valid only while the connection is open and registered with the server.
ptr_t getSelfPtr();

connection_h mConnectionHandle;
std::weak_ptr<WSServer> mOwningServer; // Back-reference to the server this connection belongs to
};
Expand Down Expand Up @@ -260,6 +264,7 @@ class LLWebsocketMgr: public LLSingleton<LLWebsocketMgr>
* This method is thread-safe and can be called from any thread.
*/
bool closeConnection(const connection_h& handle, U16 code = 1000, const std::string& reason = std::string());
void closeAllConnections(U16 code = 1001, const std::string& reason = "Server shutting down");

private:
using connection_map_t = std::map<connection_h, WSConnection::ptr_t, std::owner_less<connection_h> >;
Expand Down
11 changes: 11 additions & 0 deletions indra/newview/app_settings/settings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -14155,6 +14155,17 @@
<key>Value</key>
<integer>1</integer>
</map>
<key>ExternalEditorTightIntegration</key>
<map>
<key>Comment</key>
<string>When true, Edit in External Editor launches VS Code via the code CLI with a vscode:// URI instead of using the configured external editor.</string>
<key>Persist</key>
<integer>1</integer>
<key>Type</key>
<string>Boolean</string>
<key>Value</key>
<integer>0</integer>
</map>
<key>ExternalWebsocketForwardDebug</key>
<map>
<key>Comment</key>
Expand Down
69 changes: 37 additions & 32 deletions indra/newview/llpreviewscript.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1152,10 +1152,38 @@ void LLScriptEdCore::openInExternalEditor()
mContainer->mLiveFile = new LLLiveLSLFile(filename, boost::bind(&LLScriptEdContainer::onExternalChange, mContainer, _1));
mContainer->mLiveFile->addToEventTimer();
}
mContainer->startWebsocketServer();
if (gSavedSettings.getBOOL("ExternalEditorTightIntegration"))
{
// VS Code tight integration path
auto server = LLScriptEditorWSServer::ensureServerRunning();
if (server)
{
std::string script_id_hash_str(mContainer->getUniqueHash());
server->subscribeScriptEditor(mContainer->mObjectUUID, mContainer->mItemUUID,
mScriptName, mContainer->getHandle(), script_id_hash_str);
mContainer->mWebSocketServer = server;

LLViewerObject* object = gObjectList.findObject(mContainer->mObjectUUID);
LLViewerObject* root_object = object ? object->getRootEdit() : nullptr;
LLUUID root_id = root_object ? root_object->getID() : mContainer->mObjectUUID;

// Open it in external editor.
if (!LLScriptEditorWSServer::launchVSCode(root_id))
{
LLNotificationsUtil::add("GenericAlert",
LLSD().with("MESSAGE", LLTrans::getString("VSCodeLaunchFailed")));
}
}
else
{
LLNotificationsUtil::add("GenericAlert",
LLSD().with("MESSAGE", LLTrans::getString("ExternalEditorFailedToStart")));
}
}
else
{
// Legacy external editor path
mContainer->startWebsocketServer();

LLExternalEditor ed;
LLExternalEditor::EErrorCode status;
std::string msg;
Expand Down Expand Up @@ -1680,38 +1708,15 @@ bool LLScriptEdContainer::handleKeyHere(KEY key, MASK mask)

void LLScriptEdContainer::startWebsocketServer()
{
if (gSavedSettings.getBOOL("ExternalWebsocketSyncEnable"))
auto server = LLScriptEditorWSServer::ensureServerRunning();
if (!server)
{
// Attempt to find an existing server
LLWebsocketMgr& wsmgr = LLWebsocketMgr::instance();
LLScriptEditorWSServer::ptr_t server =
std::static_pointer_cast<LLScriptEditorWSServer>(
wsmgr.findServerByName(LLScriptEditorWSServer::DEFAULT_SERVER_NAME));

if (!server)
{ // We couldn't find one, so create it
U16 server_port = static_cast<U16>(gSavedSettings.getS32("ExternalWebsocketSyncPort"));
bool server_localhost = gSavedSettings.getBOOL("ExternalWebsocketSyncLocal");
server = std::make_shared<LLScriptEditorWSServer>(LLScriptEditorWSServer::DEFAULT_SERVER_NAME, server_port, server_localhost);
wsmgr.addServer(server);
}

bool is_running = server->isRunning();
if (!is_running)
{ // Server isn't running, so start it
is_running = wsmgr.startServer(LLScriptEditorWSServer::DEFAULT_SERVER_NAME);
}

if (!is_running && !server->isRunning())
{ // Failed to start the server
LL_WARNS() << "Failed to start script editor websocket server" << LL_ENDL;
return;
}

std::string script_id_hash_str(getUniqueHash());
server->subscribeScriptEditor(mObjectUUID, mItemUUID, mScriptEd->mScriptName, getHandle(), script_id_hash_str);
mWebSocketServer = server;
return;
}

std::string script_id_hash_str(getUniqueHash());
server->subscribeScriptEditor(mObjectUUID, mItemUUID, mScriptEd->mScriptName, getHandle(), script_id_hash_str);
mWebSocketServer = server;
}

void LLScriptEdContainer::unsubscribeScript()
Expand Down
Loading
Loading