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
547 changes: 493 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 @@ -14210,6 +14210,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
112 changes: 97 additions & 15 deletions indra/newview/llpanelcontents.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@

// linden library includes
#include "llerror.h"
#include "llcombobox.h"
#include "llfiltereditor.h"
#include "llfloaterreg.h"
#include "llfontgl.h"
Expand All @@ -55,6 +56,7 @@
#include "lltrans.h"
#include "llviewerassettype.h"
#include "llviewerinventory.h"
#include "llviewercontrol.h"
#include "llviewerobject.h"
#include "llviewerregion.h"
#include "llviewerwindow.h"
Expand Down Expand Up @@ -82,9 +84,12 @@ bool LLPanelContents::postBuild()
{
setMouseOpaque(false);

childSetAction("button new script",&LLPanelContents::onClickNewScript, this);
getChild<LLUICtrl>("button new script")->setCommitCallback(boost::bind(&LLPanelContents::onNewScriptFlyoutCommit, this, _1));
childSetAction("button permissions",&LLPanelContents::onClickPermissions, this);

mPublishButton = getChild<LLButton>("button publish");
mPublishButton->setClickedCallback([this](LLUICtrl*, const LLSD&) { onClickPublish(); });

mFilterEditor = getChild<LLFilterEditor>("contents_filter");
mFilterEditor->setCommitCallback([&](LLUICtrl*, const LLSD&) { onFilterEdit(); });

Expand Down Expand Up @@ -114,6 +119,8 @@ void LLPanelContents::getState(LLViewerObject *objectp )
if( !objectp )
{
getChildView("button new script")->setEnabled(false);
mPublishButton->setEnabled(false);
mPublishButton->setToggleState(false);
return;
}

Expand All @@ -133,8 +140,35 @@ void LLPanelContents::getState(LLViewerObject *objectp )
((LLSelectMgr::getInstance()->getSelection()->getRootObjectCount() == 1)
|| (LLSelectMgr::getInstance()->getSelection()->getObjectCount() == 1)));

// Enable the Lua script option only when the region supports it.
bool lua_region = false;
LLViewerRegion* region = objectp->getRegion();
if (region && region->simulatorFeaturesReceived())
{
LLSD simulatorFeatures;
region->getSimulatorFeatures(simulatorFeatures);
lua_region = simulatorFeatures["LuaScriptsEnabled"].asBoolean();
}
getChild<LLComboBox>("button new script")->setEnabledByValue("lua", lua_region);

getChildView("button permissions")->setEnabled(!objectp->isPermanentEnforced());
mPanelInventoryObject->setEnabled(!objectp->isPermanentEnforced());

// Publish button - enabled only when WS server is configured, and a single editable root object is selected.
bool ws_enabled = gSavedSettings.getBOOL("ExternalWebsocketSyncEnable");
bool single_root = (LLSelectMgr::getInstance()->getSelection()->getRootObjectCount() == 1);
mPublishButton->setEnabled(ws_enabled && editable && all_volume && single_root);

// Sync toggle state to reflect whether the object is currently published.
if (ws_enabled)
{
auto server = LLScriptEditorWSServer::getServer();
mPublishButton->setToggleState(server && server->isObjectPublished(objectp->getID()));
}
else
{
mPublishButton->setToggleState(false);
}
}

void LLPanelContents::onFilterEdit()
Expand Down Expand Up @@ -221,8 +255,7 @@ void LLPanelContents::clearContents()
// Static functions
//

// static
void LLPanelContents::onClickNewScript(void *userdata)
void LLPanelContents::onNewScriptFlyoutCommit(LLUICtrl* ctrl)
{
const bool children_ok = true;
LLViewerObject* object = LLSelectMgr::getInstance()->getSelection()->getFirstRootObject(children_ok);
Expand All @@ -241,20 +274,34 @@ void LLPanelContents::onClickNewScript(void *userdata)
std::string desc;
LLViewerAssetType::generateDescriptionFor(LLAssetType::AT_LSL_TEXT, desc);

U8 script_language = SST_LSL;
LLUUID template_id;

LLViewerRegion* region = object->getRegion();
if (region && region->simulatorFeaturesReceived())
U8 script_language;
const std::string value = ctrl->getValue().asString();
if (value == "lsl")
{
LLSD simulatorFeatures;
region->getSimulatorFeatures(simulatorFeatures);
if (simulatorFeatures["LuaScriptsEnabled"].asBoolean())
script_language = SST_LSL;
}
else if (value == "lua")
{
script_language = SST_LUA;
}
else
{
// Action button clicked without a selection — auto-detect from region.
script_language = SST_LSL;
LLViewerRegion* region = object->getRegion();
if (region && region->simulatorFeaturesReceived())
{
script_language = SST_LUA;
LLSD simulatorFeatures;
region->getSimulatorFeatures(simulatorFeatures);
if (simulatorFeatures["LuaScriptsEnabled"].asBoolean())
{
script_language = SST_LUA;
}
}
}
// *TODO* Get a template ID and script_language based on user preferences. Template ID is the inventory item UUID of a script

LLUUID template_id;
// *TODO* Get a template ID based on user preferences. Template ID is the inventory item UUID of a script
// in the user's inventory that is used as a template for new scripts.

LLPointer<LLViewerInventoryItem> new_item =
Expand All @@ -272,8 +319,6 @@ void LLPanelContents::onClickNewScript(void *userdata)
time_corrected());
object->saveScript(new_item, true, true, template_id);

std::string name = new_item->getName();

// *NOTE: In order to resolve SL-22177, we needed to create
// the script first, and then you have to click it in
// inventory to edit it.
Expand All @@ -289,3 +334,40 @@ void LLPanelContents::onClickPermissions(void *userdata)
LLPanelContents* self = (LLPanelContents*)userdata;
gFloaterView->getParentFloater(self)->addDependentFloater(LLFloaterReg::showInstance("bulk_perms"));
}

void LLPanelContents::onClickPublish()
{
const bool children_ok = true;
LLViewerObject* object = LLSelectMgr::getInstance()->getSelection()->getFirstRootObject(children_ok);
if (!object)
{
LL_WARNS() << "No root object selected for publish/unpublish" << LL_ENDL;
return;
}

auto server = LLScriptEditorWSServer::ensureServerRunning();
if (!server)
{
LL_WARNS() << "Cannot publish/unpublish: WebSocket server failed to start" << LL_ENDL;
return;
}

const LLUUID object_id = object->getID();
if (server->getConnectionCount())
{ // if we already have at least one connection, then we can toggle the publish state of the object
if (server->isObjectPublished(object_id))
{
server->unpublishObject(object_id, "user");
}
else
{
server->publishObject(object_id);
}
}
else
{ // if we don't have any connections, we need to build the url and launch vscode
// Launch VSCode
LLScriptEditorWSServer::launchVSCode(object_id);

}
}
5 changes: 4 additions & 1 deletion indra/newview/llpanelcontents.h
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
#include "lluuid.h"
#include "llviewerobject.h"
#include "llvoinventorylistener.h"
#include "llscripteditorws.h"
#include "v3math.h"

class LLButton;
Expand All @@ -52,8 +53,9 @@ class LLPanelContents : public LLPanel
void clearContents();


static void onClickNewScript(void*);
void onNewScriptFlyoutCommit(LLUICtrl* ctrl);
static void onClickPermissions(void*);
void onClickPublish();

// Key suffix for "tentative" fields
static const char* TENTATIVE_SUFFIX;
Expand All @@ -76,6 +78,7 @@ class LLPanelContents : public LLPanel
class LLFilterEditor* mFilterEditor;
LLSaveFolderState mSavedFolderState;
LLPanelObjectInventory* mPanelInventoryObject;
LLButton* mPublishButton { nullptr };
};

#endif // LL_LLPANELCONTENTS_H
Loading
Loading