summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJonathan "Geenz" Goodman <geenz@lindenlab.com>2026-10-13 14:36:53 -0400
committerGitHub <noreply@github.com>2026-10-13 14:36:53 -0400
commitfac07196d86e899686c4f1ce88776b11a906cd58 (patch)
treef3a3a530b80b1b17e71c7b6d7fa8b981b52c61f9
parent083db782e29434ab17963fcac76b0ec3cd93b9d0 (diff)
parent181fd93f799e15a6ff924362c4a05be8db3639af (diff)
Merge pull request #6438 from secondlife/geenz/main-26.5
Main -> 26.5
-rw-r--r--doc/external-editor-json-rpc.md33
-rw-r--r--indra/cmake/BuildPackagesInfo.cmake3
-rw-r--r--indra/llcommon/workqueue.cpp6
-rw-r--r--indra/llcommon/workqueue.h10
-rw-r--r--indra/llcorehttp/CMakeLists.txt5
-rw-r--r--indra/llcorehttp/lljsonrpcws.cpp75
-rw-r--r--indra/llcorehttp/lljsonrpcws.h43
-rw-r--r--indra/llcorehttp/llwebsocketmgr.cpp74
-rw-r--r--indra/llcorehttp/llwebsocketmgr.h20
-rw-r--r--indra/llcorehttp/tests/jsonrpcws_test.cpp553
-rw-r--r--indra/llcorehttp/tests/llcorehttp_test.cpp1
-rw-r--r--indra/llcorehttp/tests/test_jsonrpcws.hpp57
-rw-r--r--indra/llcorehttp/tests/test_websocketmgr.hpp157
-rw-r--r--indra/llcorehttp/tests/testwsclient.h170
-rw-r--r--indra/llcorehttp/tests/wsserver_test.cpp438
-rw-r--r--indra/llfilesystem/lldiriterator.cpp2
-rw-r--r--indra/llfilesystem/lldiskcache.cpp16
-rw-r--r--indra/llmath/llvolume.cpp159
-rw-r--r--indra/llmath/llvolume.h9
-rw-r--r--indra/llui/llfolderviewitem.cpp17
-rw-r--r--indra/llwebrtc/llwebrtc.cpp23
-rw-r--r--indra/llwebrtc/llwebrtc_impl.h5
-rw-r--r--indra/newview/CMakeLists.txt5
-rw-r--r--indra/newview/app_settings/settings.xml46
-rw-r--r--indra/newview/llagent.cpp12
-rw-r--r--indra/newview/llappviewer.cpp41
-rw-r--r--indra/newview/llappviewer.h13
-rw-r--r--indra/newview/llappviewerlinux.cpp182
-rw-r--r--indra/newview/llappviewerlinux.h10
-rw-r--r--indra/newview/llappviewermacosx-objc.h1
-rw-r--r--indra/newview/llappviewermacosx-objc.mm66
-rw-r--r--indra/newview/llappviewermacosx.cpp7
-rw-r--r--indra/newview/llappviewermacosx.h1
-rw-r--r--indra/newview/llappviewerwin32.cpp50
-rw-r--r--indra/newview/llappviewerwin32.h1
-rw-r--r--indra/newview/llbuycurrencyhtml.cpp101
-rw-r--r--indra/newview/llbuycurrencyhtml.h19
-rw-r--r--indra/newview/llfloateraddpaymentmethod.cpp84
-rw-r--r--indra/newview/llfloateraddpaymentmethod.h52
-rw-r--r--indra/newview/llfloaterbuycurrency.cpp39
-rw-r--r--indra/newview/llfloaterbuycurrency.h16
-rw-r--r--indra/newview/llinventorypanel.cpp5
-rw-r--r--indra/newview/llpanelcontents.cpp37
-rw-r--r--indra/newview/llpanelcontents.h4
-rw-r--r--indra/newview/llpanelprofilepicks.cpp15
-rw-r--r--indra/newview/llpreviewscript.cpp2
-rw-r--r--indra/newview/llpublishedobjectmgr.cpp166
-rw-r--r--indra/newview/llpublishedobjectmgr.h15
-rw-r--r--indra/newview/llscripteditorws.cpp339
-rw-r--r--indra/newview/llscripteditorws.h39
-rw-r--r--indra/newview/llselectmgr.cpp12
-rw-r--r--indra/newview/llviewerfloaterreg.cpp3
-rw-r--r--indra/newview/llviewerwindow.cpp30
-rw-r--r--indra/newview/llvoicewebrtc.cpp51
-rw-r--r--indra/newview/llvoicewebrtc.h7
-rw-r--r--indra/newview/luau.txt22
-rw-r--r--indra/newview/skins/default/xui/da/panel_preferences_setup.xml8
-rw-r--r--indra/newview/skins/default/xui/de/panel_preferences_setup.xml8
-rw-r--r--indra/newview/skins/default/xui/en/floater_add_payment_method.xml142
-rw-r--r--indra/newview/skins/default/xui/en/menu_viewer.xml16
-rw-r--r--indra/newview/skins/default/xui/en/panel_inbox_inventory.xml3
-rw-r--r--indra/newview/skins/default/xui/en/panel_preferences_setup.xml37
-rw-r--r--indra/newview/skins/default/xui/es/panel_preferences_setup.xml8
-rw-r--r--indra/newview/skins/default/xui/fr/panel_preferences_setup.xml8
-rw-r--r--indra/newview/skins/default/xui/it/panel_preferences_setup.xml8
-rw-r--r--indra/newview/skins/default/xui/ja/floater_add_payment_method.xml25
-rw-r--r--indra/newview/skins/default/xui/ja/panel_preferences_setup.xml8
-rw-r--r--indra/newview/skins/default/xui/pl/floater_add_payment_method.xml19
-rw-r--r--indra/newview/skins/default/xui/pl/panel_preferences_setup.xml8
-rw-r--r--indra/newview/skins/default/xui/pt/panel_preferences_setup.xml8
-rw-r--r--indra/newview/skins/default/xui/pt/settings_comments.xml2
-rw-r--r--indra/newview/skins/default/xui/ru/panel_preferences_setup.xml8
-rw-r--r--indra/newview/skins/default/xui/tr/panel_preferences_setup.xml8
-rw-r--r--indra/newview/skins/default/xui/zh/panel_preferences_setup.xml8
-rw-r--r--indra/newview/tests/llscripteditorws_test.cpp130
-rwxr-xr-xindra/newview/viewer_manifest.py3
-rwxr-xr-xscripts/packages-formatter.py4
77 files changed, 2537 insertions, 1301 deletions
diff --git a/doc/external-editor-json-rpc.md b/doc/external-editor-json-rpc.md
index 0c32c043fe..80cc135522 100644
--- a/doc/external-editor-json-rpc.md
+++ b/doc/external-editor-json-rpc.md
@@ -65,9 +65,14 @@ This document describes all the message interfaces defined for WebSocket communi
1. **Connection Establishment:**
+ - The viewer refuses a WebSocket upgrade request that carries an `Origin` header (HTTP 403):
+ browsers send one with every socket a page opens, and a program connecting need not
- Viewer sends `session.handshake` call with `SessionHandshake` data
- - Extension responds with `SessionHandshakeResponse`
+ - Extension responds with `SessionHandshakeResponse`, answering the challenge
- Viewer confirms with `session.ok` notification
+ - Until `session.ok`, every request the extension makes is refused with `-32002`
+ (Authentication required) and the viewer sends it no notifications. A wrong challenge
+ response, an error in place of one, or no response within 30 seconds closes the connection.
2. **Language Information Exchange:**
@@ -291,7 +296,7 @@ interface SessionHandshake {
viewer_version: string;
agent_id: string;
agent_name: string;
- challenge?: string;
+ challenge: string;
languages: string[];
syntax_id: string;
features: { [feature: string]: boolean };
@@ -306,7 +311,7 @@ interface SessionHandshake {
- `viewer_version`: Version string of the viewer
- `agent_id`: Unique identifier for the user/agent
- `agent_name`: Human-readable name of the agent
-- `challenge` (optional): Path to a temporary file on the local filesystem containing a UUID. The client must read this file and return the UUID as `challenge_response` to authenticate the connection.
+- `challenge`: Path to a temporary file on the local filesystem containing a UUID, and nothing else. The client must read this file and return its contents as `challenge_response` to authenticate the connection. The file is named at random, readable only by the user running the viewer, and deleted once the handshake is answered or abandoned, so only a client running as that user can answer.
- `languages`: Array of supported scripting languages (e.g., `["lsl", "luau"]`)
- `syntax_id`: Current active syntax identifier as a UUID string
- `features`: Dictionary of feature flags indicating viewer capabilities. Known flags:
@@ -327,7 +332,7 @@ interface SessionHandshakeResponse {
client_name: string;
client_version: "1.0";
protocol_version: string;
- challenge_response?: string;
+ challenge_response: string;
languages: string[];
features: { [feature: string]: boolean };
script_name?: string;
@@ -340,7 +345,7 @@ interface SessionHandshakeResponse {
- `client_name`: Name of the client (VS Code extension)
- `client_version`: Fixed version "1.0" of the client
- `protocol_version`: Protocol version the client supports
-- `challenge_response` (optional): The UUID read from the temporary file identified by the `challenge` field in the handshake. Must be provided if `challenge` was present, otherwise the connection will be closed.
+- `challenge_response`: The UUID read from the temporary file identified by the `challenge` field in the handshake. Without it, or with any other value, the connection is closed.
- `languages`: Array of languages supported by the client
- `features`: Dictionary of features supported by the client. Known flags:
- `live_sync`: Client supports live script synchronisation
@@ -1084,7 +1089,7 @@ interface LinkedObject {
interface ObjectPermissions {
owner: number;
- next_owner: number;
+ next_owner?: number; // Absent until the viewer receives ObjectProperties for the prim
}
/** Root of a linkset, as published to the extension */
@@ -1235,19 +1240,30 @@ Sent when an explored object's inventory, properties, or linkset membership chan
| ------- | ------- |
| Root prim inventory changed | `object_id`, `inventory` (complete replacement array) |
| Child prim inventory changed | `object_id`, `changes.linked_objects.modified[]` with `link_id` and `inventory` (complete replacement array for that child) |
-| Root prim name/description changed | `object_id`, `object_name` and/or `object_description` |
-| Child prim name/description changed | `object_id`, `changes.linked_objects.modified[]` with `link_id` and `link_name` and/or `link_description` |
+| Root prim properties changed | `object_id`, plus any changed subset of `object_name`, `object_description`, `owner_id`, `permissions`, `can_save_back` |
+| Child prim properties changed | `object_id`, `changes.linked_objects.modified[]` with `link_id` plus any changed subset of `link_name`, `link_description`, `permissions` |
| Linkset membership changed | `object_id`, `linked_objects` (complete replacement array covering every child prim) |
`inventory` and `linked_objects` are always complete replacements of the prior state, never
increments. Linkset membership changes are coalesced behind a short flush delay, so several
rapid link or unlink operations may arrive as a single update.
+**Deferred permission fields.** `object.publish` is gated on inventory loading, which can finish
+before the `ObjectProperties` reply arrives. Until it does the viewer derives `permissions.owner`
+from the object's update flags, which describe the current agent's rights, and omits `next_owner`.
+`owner_id` may be absent or the null UUID, and `can_save_back` may be `false` because it depends on
+selection state. Once properties arrive the viewer sends an `object.update` carrying the full
+`owner`/`next_owner` block and the real `owner_id`, so clients must merge these fields on update
+rather than assuming the publish payload is final.
+
```typescript
interface ObjectUpdateMessage {
object_id: string;
object_name?: string;
object_description?: string;
+ owner_id?: string;
+ permissions?: ObjectPermissions;
+ can_save_back?: boolean;
// Full replacement
inventory?: ObjectInventoryItem[];
linked_objects?: LinkedObject[];
@@ -1264,6 +1280,7 @@ interface LinkedObjectChanges {
link_id: string;
link_name?: string;
link_description?: string;
+ permissions?: ObjectPermissions;
inventory?: ObjectInventoryItem[]; // Complete replacement array, not a delta
}[];
}
diff --git a/indra/cmake/BuildPackagesInfo.cmake b/indra/cmake/BuildPackagesInfo.cmake
index 659ba9d579..9a615ad3cd 100644
--- a/indra/cmake/BuildPackagesInfo.cmake
+++ b/indra/cmake/BuildPackagesInfo.cmake
@@ -12,9 +12,10 @@ add_custom_command(OUTPUT packages-info.txt
COMMENT "Generating packages-info.txt for the about box"
MAIN_DEPENDENCY ${CMAKE_SOURCE_DIR}/../autobuild.xml
DEPENDS ${CMAKE_SOURCE_DIR}/../scripts/packages-formatter.py
+ ${CMAKE_SOURCE_DIR}/newview/luau.txt
${CMAKE_SOURCE_DIR}/../autobuild.xml
COMMAND ${PYTHON_EXECUTABLE}
- ${CMAKE_SOURCE_DIR}/cmake/run_build_test.py -DAUTOBUILD_ADDRSIZE=${ADDRESS_SIZE} -DAUTOBUILD=${AUTOBUILD_EXECUTABLE}
+ ${CMAKE_SOURCE_DIR}/cmake/run_build_test.py -DAUTOBUILD_ADDRSIZE=${ADDRESS_SIZE} -DAUTOBUILD=${AUTOBUILD_EXECUTABLE} -DPYTHONIOENCODING=utf-8
${PYTHON_EXECUTABLE}
${CMAKE_SOURCE_DIR}/../scripts/packages-formatter.py "${VIEWER_CHANNEL}" "${VIEWER_SHORT_VERSION}.${VIEWER_VERSION_REVISION}" "${AUTOBUILD_INSTALL_DIR}" > packages-info.txt
)
diff --git a/indra/llcommon/workqueue.cpp b/indra/llcommon/workqueue.cpp
index fbd92d089e..4f19ab2cc2 100644
--- a/indra/llcommon/workqueue.cpp
+++ b/indra/llcommon/workqueue.cpp
@@ -319,6 +319,12 @@ bool LL::WorkQueue::tryPost(const Work& callable)
}
}
+bool LL::WorkQueue::postToMainLoop(const Work& work)
+{
+ auto queue = getInstance("mainloop");
+ return queue && queue->tryPost(work);
+}
+
LL::WorkQueue::Work LL::WorkQueue::pop_()
{
return mQueue.pop();
diff --git a/indra/llcommon/workqueue.h b/indra/llcommon/workqueue.h
index 69f3286c1b..322baa36e6 100644
--- a/indra/llcommon/workqueue.h
+++ b/indra/llcommon/workqueue.h
@@ -265,6 +265,16 @@ namespace LL
*/
bool tryPost(const Work&) override;
+ /**
+ * @brief Post work to the well-known main-loop queue without blocking
+ * @return true if posted, false if the main loop is gone or its
+ * queue is closed/full
+ *
+ * For callers on a secondary thread that must not block waiting for
+ * the main thread (e.g. a websocket I/O thread).
+ */
+ static bool postToMainLoop(const Work& work);
+
private:
using Queue = LLThreadSafeQueue<Work>;
Queue mQueue;
diff --git a/indra/llcorehttp/CMakeLists.txt b/indra/llcorehttp/CMakeLists.txt
index c2c4078ff0..bdc2582eef 100644
--- a/indra/llcorehttp/CMakeLists.txt
+++ b/indra/llcorehttp/CMakeLists.txt
@@ -120,6 +120,7 @@ if (LL_TESTS AND LLCOREHTTP_TESTS)
tests/test_bufferarray.hpp
tests/test_bufferstream.hpp
tests/test_jsonrpcws.hpp
+ tests/test_websocketmgr.hpp
)
list(APPEND llcorehttp_TEST_SOURCE_FILES ${llcorehttp_TEST_HEADER_FILES})
@@ -144,6 +145,10 @@ if (LL_TESTS AND LLCOREHTTP_TESTS)
"${CMAKE_CURRENT_SOURCE_DIR}/tests/test_llcorehttp_peer.py"
)
+ LL_ADD_INTEGRATION_TEST(wsserver "" "${test_libs}")
+
+ LL_ADD_INTEGRATION_TEST(jsonrpcws "" "${test_libs}")
+
#
# Example Programs
#
diff --git a/indra/llcorehttp/lljsonrpcws.cpp b/indra/llcorehttp/lljsonrpcws.cpp
index 5b704e6322..f04738d805 100644
--- a/indra/llcorehttp/lljsonrpcws.cpp
+++ b/indra/llcorehttp/lljsonrpcws.cpp
@@ -31,9 +31,8 @@
#include "llsdjson.h"
#include "lldate.h"
#include "llcoros.h"
-#include "llmainthreadtask.h"
-#include "lleventtimer.h"
#include "lltimer.h"
+#include "workqueue.h"
#include <boost/json.hpp>
@@ -44,27 +43,10 @@
void LLJSONRPCConnection::onOpen()
{
LL_INFOS("JSONRPC") << "JSON-RPC connection opened" << LL_ENDL;
-
- // Start the recurring timeout sweep timer on the main thread. The timer
- // is canceled in onClose() before the connection can be destroyed, so
- // capturing `this` is safe. Keep a weak_ptr so we can safely test
- // whether the timer instance still exists at cancellation time.
- LLEventTimer* timer = LLEventTimer::run_every(TIMEOUT_SWEEP_INTERVAL,
- [this]() { sweepTimeouts(); });
- mTimeoutTimer = timer->getWeak();
}
void LLJSONRPCConnection::onClose()
{
- // Cancel the sweep timer if it is still alive. LLEventTimer's instance
- // tracker keeps a shared_ptr with a no-op deleter, so raw `delete` is
- // the documented cancellation idiom (see lleventtimer.h).
- if (auto timer = mTimeoutTimer.lock())
- {
- delete timer.get();
- }
- mTimeoutTimer.reset();
-
// Move the pending-request map out under the lock so we can invoke the
// callbacks without holding it (callbacks may themselves call into this
// connection).
@@ -132,7 +114,7 @@ void LLJSONRPCConnection::onMessage(const std::string& message)
catch (const std::exception& e)
{
LL_WARNS("JSONRPC") << "Exception processing JSON-RPC message: " << e.what() << LL_ENDL;
- sendError(LLSD(), InternalError(e.what()));
+ sendError(LLSD(), InternalError());
}
}
@@ -181,6 +163,18 @@ void LLJSONRPCConnection::processRequest(const LLSD& request)
LL_DEBUGS("JSONRPC") << "Processing " << (is_notification ? "notification" : "request")
<< " for method: " << method << LL_ENDL;
+ // Nothing for a peer that has not proven itself, and nothing said of
+ // which methods there are.
+ if (!mAuthenticated)
+ {
+ LL_DEBUGS("JSONRPC") << "Refused " << method << " from a peer not authenticated" << LL_ENDL;
+ if (!is_notification)
+ {
+ sendError(id, UnauthorizedError());
+ }
+ return;
+ }
+
// Resolve the handler under the mutex, then invoke it unlocked.
MethodHandler handler;
bool is_async = false;
@@ -219,7 +213,7 @@ void LLJSONRPCConnection::processRequest(const LLSD& request)
throw InternalError("Connection expired before method " + method
+ " could be launched");
}
- LLMainThreadTask::dispatch(
+ bool posted = LL::WorkQueue::postToMainLoop(
[handler, method, id, params, conn]()
{
LLCoros::instance().launch(
@@ -255,7 +249,9 @@ void LLJSONRPCConnection::processRequest(const LLSD& request)
{
if (conn->isConnected())
{
- conn->sendError(id, InternalError(e.what()));
+ LL_WARNS("JSONRPC") << "Exception in async method " << method
+ << ": " << e.what() << LL_ENDL;
+ conn->sendError(id, InternalError());
}
else
{
@@ -265,6 +261,11 @@ void LLJSONRPCConnection::processRequest(const LLSD& request)
}
});
});
+ if (!posted)
+ {
+ LL_WARNS("JSONRPC") << "Main loop unavailable; failing async method " << method << LL_ENDL;
+ throw InternalError("Viewer is shutting down; cannot service " + method);
+ }
return;
}
@@ -302,7 +303,9 @@ void LLJSONRPCConnection::processRequest(const LLSD& request)
{
if (!is_notification)
{
- sendError(id, InternalError(e.what()));
+ LL_WARNS("JSONRPC") << "Exception in handler for " << method
+ << ": " << e.what() << LL_ENDL;
+ sendError(id, InternalError());
}
else
{
@@ -446,8 +449,7 @@ void LLJSONRPCConnection::sweepTimeouts()
for (auto& [id, callback] : expired)
{
- LL_WARNS("JSONRPC") << "Request " << id << " timed out after "
- << REQUEST_TIMEOUT_SECONDS << " seconds" << LL_ENDL;
+ LL_WARNS("JSONRPC") << "Request " << id << " timed out" << LL_ENDL;
if (callback)
{
LLSD error;
@@ -566,7 +568,7 @@ LLSD LLJSONRPCConnection::makeEnvelope(const LLSD& id,
return env;
}
-LLSD LLJSONRPCConnection::call(const std::string& method, const LLSD& params, ResponseCallback callback)
+LLSD LLJSONRPCConnection::call(const std::string& method, const LLSD& params, ResponseCallback callback, F64 timeout)
{
LL_PROFILE_ZONE_SCOPED_CATEGORY_WEBSOCKET;
LLSD id = generateId();
@@ -579,7 +581,7 @@ LLSD LLJSONRPCConnection::call(const std::string& method, const LLSD& params, Re
{
LLMutexLock lock(&mMutex);
mPendingRequests[id_str] = std::move(callback);
- mPendingDeadlines.push({ LLTimer::getTotalSeconds() + REQUEST_TIMEOUT_SECONDS, id_str });
+ mPendingDeadlines.push({ LLTimer::getTotalSeconds() + timeout, id_str });
}
// Send the request
@@ -715,6 +717,25 @@ void LLJSONRPCServer::onConnectionClosed(const LLWebsocketMgr::WSConnection::ptr
<< getConnectionCount() << LL_ENDL;
}
+bool LLJSONRPCServer::update()
+{
+ // Main thread, once per frame; throttled to TIMEOUT_SWEEP_INTERVAL.
+ if (mSweepTimer.getElapsedTimeF32() < LLJSONRPCConnection::TIMEOUT_SWEEP_INTERVAL)
+ {
+ return true;
+ }
+ mSweepTimer.reset();
+
+ for (const auto& conn : getConnections())
+ {
+ if (auto rpc = std::dynamic_pointer_cast<LLJSONRPCConnection>(conn))
+ {
+ rpc->sweepTimeouts();
+ }
+ }
+ return true;
+}
+
void LLJSONRPCServer::setupConnectionMethods(LLJSONRPCConnection::ptr_t connection)
{
LLMutexLock lock(&mGlobalMethodsMutex);
diff --git a/indra/llcorehttp/lljsonrpcws.h b/indra/llcorehttp/lljsonrpcws.h
index aa4a96be4d..98c858b022 100644
--- a/indra/llcorehttp/lljsonrpcws.h
+++ b/indra/llcorehttp/lljsonrpcws.h
@@ -29,14 +29,13 @@
#include "llwebsocketmgr.h"
#include "llsd.h"
#include "lluuid.h"
+#include "lltimer.h"
#include <functional>
#include <unordered_map>
#include <memory>
#include <queue>
-class LLEventTimer;
-
/**
* @class LLJSONRPCConnection
* @brief JSON-RPC 2.0 WebSocket connection implementation
@@ -285,10 +284,23 @@ public:
* @param method The method name to call
* @param params The parameters to pass
* @param callback Callback for the response (optional)
- * @return The request ID for correlation
+ * @param timeout Seconds before the callback is told the call timed out
+ * @return The request ID for correlation, undefined where it could not
+ * be sent (the callback is then never called)
*/
LLSD call(const std::string& method, const LLSD& params = LLSD(),
- ResponseCallback callback = nullptr);
+ ResponseCallback callback = nullptr, F64 timeout = REQUEST_TIMEOUT_SECONDS);
+
+ /**
+ * @brief Whether the peer's requests are dispatched
+ *
+ * A connection starts out unauthenticated and refuses every request as
+ * unauthorized, whether or not the method exists, until it is told the
+ * peer has proven itself -- e.g. by answering a challenge. Answers to
+ * its own calls go through either way: that is how the peer proves
+ * itself.
+ */
+ bool isAuthenticated() const { return mAuthenticated.load(); }
/**
* @brief Send a JSON-RPC notification (no response expected)
@@ -361,6 +373,8 @@ protected:
*/
LLSD generateId();
+ void setAuthenticated(bool authenticated) { mAuthenticated = authenticated; }
+
public:
/**
* @brief Build a JSON-RPC 2.0 envelope.
@@ -390,7 +404,8 @@ private:
// Per-request timeout tracking. mPendingDeadlines is a min-heap of
// (deadline, request_id) ordered by deadline; entries whose request has
// already been answered become tombstones (skipped when they reach the
- // top). A single recurring timer per connection sweeps the heap.
+ // top). The owning server calls sweepTimeouts() once per frame (see
+ // LLJSONRPCServer::update()) to fire any expired entries.
struct PendingDeadline
{
F64 mDeadline; // absolute time in seconds (LLTimer::getTotalSeconds)
@@ -399,16 +414,20 @@ private:
bool operator<(const PendingDeadline& rhs) const { return mDeadline > rhs.mDeadline; }
};
std::priority_queue<PendingDeadline> mPendingDeadlines;
- std::weak_ptr<LLEventTimer> mTimeoutTimer;
+
+ std::atomic<bool> mAuthenticated{ false };
static constexpr F64 REQUEST_TIMEOUT_SECONDS = 120.0;
- static constexpr F32 TIMEOUT_SWEEP_INTERVAL = 1.0f;
- /// Invoked by the sweep timer; fires the timeout callback for any
- /// request whose deadline has passed. Safe to call from the main thread.
+public:
+ /// Sweep interval used by LLJSONRPCServer::update() to throttle sweepTimeouts().
+ static constexpr F32 TIMEOUT_SWEEP_INTERVAL = 1.0f;
+
+ /// Invoked once per frame by the owning server (see LLJSONRPCServer::update());
+ /// fires the timeout callback for any request whose deadline has passed.
+ /// Main-thread only.
void sweepTimeouts();
-public:
void testInjectPendingRequest(const std::string& id, F64 deadline, ResponseCallback callback);
void testSweepTimeouts();
size_t testPendingRequestCount() const;
@@ -497,12 +516,15 @@ public:
*/
LLSD getServerStats() const;
+ bool update() override;
+
protected:
LLWebsocketMgr::WSConnection::ptr_t connectionFactory(LLWebsocketMgr::WSServer::ptr_t server,
LLWebsocketMgr::connection_h handle) override;
virtual LLSD handlePing(const LLJSONRPCConnection::ptr_t& connection,
const LLSD& params) const;
+
virtual LLSD handleGetVersion(const LLJSONRPCConnection::ptr_t& connection,
const LLSD& params) const = 0;
virtual LLSD handleStatus(const LLJSONRPCConnection::ptr_t& connection,
@@ -521,4 +543,5 @@ private:
std::string mServerName; // Store server name for stats
std::atomic<U64> mTotalRequestsHandled{0};
std::atomic<U64> mTotalNotificationsSent{0};
+ LLTimer mSweepTimer;
};
diff --git a/indra/llcorehttp/llwebsocketmgr.cpp b/indra/llcorehttp/llwebsocketmgr.cpp
index be1ab54efd..ef07295eed 100644
--- a/indra/llcorehttp/llwebsocketmgr.cpp
+++ b/indra/llcorehttp/llwebsocketmgr.cpp
@@ -31,6 +31,7 @@
#include "llsdserialize.h"
#include "llhost.h"
#include "llsdjson.h"
+#include "lltimer.h"
#include <websocketpp/config/boost_config.hpp>
#include <websocketpp/config/asio_no_tls.hpp>
@@ -47,6 +48,10 @@ namespace
using Server_t = websocketpp::server<websocketpp::config::asio>;
using Client_t = websocketpp::client<websocketpp::config::asio>;
using Connection_t = websocketpp::connection<websocketpp::config::asio>;
+
+ // How long stop() waits for the I/O thread to exit before logging a
+ // warning. It still joins after this -- this is diagnostic, not a timeout.
+ constexpr F32 SERVER_STOP_WARN_SECONDS = 5.0f;
}
//------------------------------------------------------------------------
@@ -190,6 +195,7 @@ struct Server_impl
mLocalOnly(local_only)
{
+ mServer.set_validate_handler([this](websocketpp::connection_hdl hdl) { return this->onValidate(hdl); });
mServer.set_open_handler([this](websocketpp::connection_hdl hdl) { this->onOpen(hdl); });
mServer.set_close_handler([this](websocketpp::connection_hdl hdl) { this->onClose(hdl); });
mServer.set_message_handler([this](websocketpp::connection_hdl hdl, Server_t::message_ptr msg) { this->onMessage(hdl, msg); });
@@ -307,6 +313,34 @@ struct Server_impl
}
/**
+ * @brief Decide whether an upgrade request is let in
+ * @param hdl WebSocket connection handle from websocketpp
+ * @return false to answer the request 403 and never open it
+ *
+ * Called by websocketpp once the request has been read, before it is
+ * answered: the owner says which origins it takes.
+ */
+ bool onValidate(websocketpp::connection_hdl hdl)
+ {
+ LL_PROFILE_ZONE_SCOPED_CATEGORY_WEBSOCKET;
+ websocketpp::lib::error_code ec;
+ Server_t::connection_ptr con = mServer.get_con_from_hdl(hdl, ec);
+ if (ec || !con)
+ {
+ return false;
+ }
+ const std::string& origin = con->get_origin();
+ if (mOwner->acceptOrigin(origin))
+ {
+ return true;
+ }
+ // Once for each origin: a page can ask as often as it likes.
+ LL_WARNS_ONCE("WebSocket") << mOwner->mServerName << " refused a connection from origin " << origin << LL_ENDL;
+ con->set_status(websocketpp::http::status_code::forbidden);
+ return false;
+ }
+
+ /**
* @brief Handle new connection establishment event
* @param hdl WebSocket connection handle from websocketpp
*
@@ -407,9 +441,11 @@ bool LLWebsocketMgr::WSServer::start()
// Reset the stop flag
mShouldStop = false;
+ mRunning = true;
// Start the server thread
mServerThread = std::thread([this]() {
+ LL_PROFILER_SET_THREAD_NAME(mServerName.c_str());
LL_INFOS("WebSocket") << "WebSocket server thread starting for: " << mServerName << LL_ENDL;
// Run the controlled server loop that checks the stop flag
@@ -422,6 +458,7 @@ bool LLWebsocketMgr::WSServer::start()
}
LL_INFOS("WebSocket") << "WebSocket server thread exiting for: " << mServerName << LL_ENDL;
+ mRunning = false;
});
onStarted();
@@ -455,6 +492,22 @@ void LLWebsocketMgr::WSServer::stop()
if (mServerThread.joinable())
{
+ // The I/O thread should exit promptly once mImpl->stop() returns. If
+ // it doesn't, something on that thread is blocked -- most likely a
+ // regression reintroducing a cross-thread wait. Warn instead of
+ // hanging silently; still join (detaching would use-after-free mImpl).
+ LLTimer wait_timer;
+ while (mRunning && wait_timer.getElapsedTimeF32() < SERVER_STOP_WARN_SECONDS)
+ {
+ ms_sleep(10);
+ }
+ if (mRunning)
+ {
+ LL_WARNS("WebSocket") << mServerName << ": I/O thread did not exit within "
+ << SERVER_STOP_WARN_SECONDS << "s; joining anyway. This indicates a "
+ << "thread-boundary regression." << LL_ENDL;
+ }
+
mServerThread.join();
LL_INFOS("WebSocket") << "WebSocket server thread joined for: " << mServerName << LL_ENDL;
}
@@ -463,10 +516,11 @@ void LLWebsocketMgr::WSServer::stop()
bool LLWebsocketMgr::WSServer::isRunning() const
{
- LL_ERRS_IF(!mImpl, "WebSocket") << "WebSocket server " << mServerName << " implementation is null !" << LL_ENDL;
-
- // Check both the thread state, websocket server state, and the stop flag
- return mServerThread.joinable() && !mImpl->mServer.stopped() && !mShouldStop;
+ // mRunning is set true just before the I/O thread launches and false as
+ // the last thing that thread does before exiting -- lock-free, and safe
+ // to call while start()/stop() hold mThreadMutex on this thread (unlike
+ // a mutex-based check, which would self-deadlock there).
+ return mRunning && !mShouldStop;
}
void LLWebsocketMgr::WSServer::broadcastMessage(const std::string& message)
@@ -540,6 +594,18 @@ LLWebsocketMgr::WSConnection::ptr_t LLWebsocketMgr::WSServer::getConnection(cons
return nullptr;
}
+std::vector<LLWebsocketMgr::WSConnection::ptr_t> LLWebsocketMgr::WSServer::getConnections() const
+{
+ LLMutexLock lock(&mConnectionMutex);
+ std::vector<WSConnection::ptr_t> result;
+ result.reserve(mConnections.size());
+ for (const auto& [handle, conn] : mConnections)
+ {
+ result.push_back(conn);
+ }
+ return result;
+}
+
LLWebsocketMgr::connection_state_t LLWebsocketMgr::WSServer::getConnectionState(const connection_h& handle) const
{
websocketpp::lib::error_code ec;
diff --git a/indra/llcorehttp/llwebsocketmgr.h b/indra/llcorehttp/llwebsocketmgr.h
index 2c335307e3..19ceb5feed 100644
--- a/indra/llcorehttp/llwebsocketmgr.h
+++ b/indra/llcorehttp/llwebsocketmgr.h
@@ -122,7 +122,7 @@ public:
* Sends a text message to the remote endpoint. The message is queued
* asynchronously and may not be sent immediately.
*/
- bool sendMessage(const std::string& message) const;
+ virtual bool sendMessage(const std::string& message) const;
bool sendMessage(const boost::json::value& json) const;
bool sendMessage(const LLSD& data) const;
@@ -243,9 +243,26 @@ public:
void broadcastMessage(const std::string& message);
virtual bool update() { return true; }
+ /**
+ * @brief Whether an upgrade request from this origin is let in
+ * @param origin The request's Origin header, empty where it had none
+ *
+ * A browser sends the page's origin with every WebSocket it opens,
+ * and nothing stops a page -- or media on a prim, in the viewer's
+ * own browser -- from opening one to 127.0.0.1. A program, such as
+ * an editor's extension, sends none. By default only those are let
+ * in; a request refused is answered 403 and never opens. Called on
+ * the server's thread.
+ */
+ virtual bool acceptOrigin(const std::string& origin) const { return origin.empty(); }
+
connection_state_t getConnectionState(const connection_h& handle) const;
protected:
+ /// Snapshot of live connections. Safe to iterate after the lock is
+ /// released -- these are real shared_ptrs, not weak references.
+ std::vector<WSConnection::ptr_t> getConnections() const;
+
virtual WSConnection::ptr_t connectionFactory(WSServer::ptr_t server, connection_h handle);
bool start();
@@ -283,6 +300,7 @@ public:
// Threading support
std::thread mServerThread; ///< Thread running the ASIO event loop
std::atomic<bool> mShouldStop{ false }; ///< Thread-safe stop flag
+ std::atomic<bool> mRunning{ false }; ///< True from just before the I/O thread launches until it exits
mutable LLMutex mThreadMutex; ///< Mutex for thread synchronization
};
diff --git a/indra/llcorehttp/tests/jsonrpcws_test.cpp b/indra/llcorehttp/tests/jsonrpcws_test.cpp
new file mode 100644
index 0000000000..c07cb6f4fd
--- /dev/null
+++ b/indra/llcorehttp/tests/jsonrpcws_test.cpp
@@ -0,0 +1,553 @@
+/**
+ * @file jsonrpcws_test.cpp
+ * @brief Security-focused unit tests for LLJSONRPCConnection / LLJSONRPCServer
+ *
+ * $LicenseInfo:firstyear=2026&license=viewerlgpl$
+ * Second Life Viewer Source Code
+ * Copyright (C) 2026, 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 "linden_common.h"
+
+#include "lljsonrpcws.h"
+#include "llsdjson.h"
+
+#include "../test/lltut.h"
+
+#include <boost/json.hpp>
+#include <vector>
+
+namespace
+{
+ // Exposes the protected test-only members LLJSONRPCConnection provides
+ // for exactly this purpose, on a connection with no real transport.
+ class TestJSONRPCConnection : public LLJSONRPCConnection
+ {
+ public:
+ TestJSONRPCConnection()
+ : LLJSONRPCConnection(LLWebsocketMgr::WSServer::ptr_t(), LLWebsocketMgr::connection_h())
+ {
+ }
+
+ using LLJSONRPCConnection::processMessage;
+ using LLJSONRPCConnection::validateMessage;
+ using LLJSONRPCConnection::testInjectPendingRequest;
+ using LLJSONRPCConnection::testPendingRequestCount;
+ using LLJSONRPCConnection::testSweepTimeouts;
+ using LLJSONRPCConnection::setAuthenticated;
+ };
+
+ // Minimal concrete server, used only to register the built-in
+ // system.* methods on a connection via setupConnectionMethods() --
+ // LLJSONRPCServer itself is abstract (handleGetVersion is pure virtual).
+ // Never started, so port 0 is fine.
+ class TestJSONRPCServer : public LLJSONRPCServer
+ {
+ public:
+ TestJSONRPCServer() : LLJSONRPCServer("test_jsonrpcws_server", 0, true) {}
+
+ using LLJSONRPCServer::setupConnectionMethods;
+
+ LLSD handleGetVersion(const LLJSONRPCConnection::ptr_t&, const LLSD&) const override
+ {
+ return LLSD();
+ }
+ };
+
+ // Captures what would be sent to the peer, since there is no real
+ // transport to observe it on.
+ class CapturingJSONRPCConnection : public TestJSONRPCConnection
+ {
+ public:
+ mutable std::vector<std::string> mSent;
+
+ bool sendMessage(const std::string& message) const override
+ {
+ mSent.push_back(message);
+ return true;
+ }
+ };
+}
+
+namespace tut
+{
+ struct jsonrpcws_data
+ {
+ };
+
+ typedef test_group<jsonrpcws_data> jsonrpcws_test;
+ typedef jsonrpcws_test::object jsonrpcws_object;
+ jsonrpcws_test jsonrpcws_testcase("LLJSONRPCConnection");
+
+ template<> template<>
+ void jsonrpcws_object::test<1>()
+ {
+ set_test_name("an unauthenticated connection dispatches neither requests nor notifications");
+
+ TestJSONRPCConnection conn;
+ conn.setAuthenticated(false);
+
+ int calls = 0;
+ conn.registerMethod("object.list",
+ [&](const std::string&, const LLSD&, const LLSD&) -> LLSD
+ {
+ ++calls;
+ return LLSD();
+ });
+
+ const LLSD request = LLJSONRPCConnection::makeEnvelope(LLSD("req_1"), "object.list", LLSD::emptyMap(), LLSD(), LLSD());
+ conn.processMessage(request);
+
+ const LLSD notification = LLJSONRPCConnection::makeEnvelope(LLSD(), "object.list", LLSD::emptyMap(), LLSD(), LLSD());
+ conn.processMessage(notification);
+
+ ensure_equals("neither a request nor a notification reaches the handler", calls, 0);
+ }
+
+ template<> template<>
+ void jsonrpcws_object::test<2>()
+ {
+ set_test_name("once authenticated, requests and notifications dispatch normally");
+
+ TestJSONRPCConnection conn;
+ conn.setAuthenticated(true);
+ ensure("authenticated after the peer proves itself", conn.isAuthenticated());
+
+ int calls = 0;
+ conn.registerMethod("object.list",
+ [&](const std::string&, const LLSD&, const LLSD&) -> LLSD
+ {
+ ++calls;
+ return LLSD();
+ });
+
+ const LLSD request = LLJSONRPCConnection::makeEnvelope(LLSD("req_1"), "object.list", LLSD::emptyMap(), LLSD(), LLSD());
+ conn.processMessage(request);
+ ensure_equals("the request reaches the handler", calls, 1);
+
+ const LLSD notification = LLJSONRPCConnection::makeEnvelope(LLSD(), "object.list", LLSD::emptyMap(), LLSD(), LLSD());
+ conn.processMessage(notification);
+ ensure_equals("the notification also reaches the handler", calls, 2);
+ }
+
+ template<> template<>
+ void jsonrpcws_object::test<3>()
+ {
+ set_test_name("an unauthenticated connection still receives the answers to its own calls");
+
+ TestJSONRPCConnection conn;
+ ensure("unauthenticated by default", !conn.isAuthenticated());
+
+ LLSD answer;
+ conn.testInjectPendingRequest(
+ "rpc_1",
+ LLTimer::getTotalSeconds() + 60.0,
+ [&](const LLSD& result, const LLSD& error)
+ {
+ ensure("no error", error.isUndefined());
+ answer = result;
+ });
+
+ LLSD result;
+ result["challenge_response"] = "secret";
+ conn.processMessage(LLJSONRPCConnection::makeEnvelope(LLSD("rpc_1"), std::string(), LLSD(), result, LLSD()));
+ ensure_equals("the answer reached the caller", answer["challenge_response"].asString(), std::string("secret"));
+ ensure("and it is answered", conn.testPendingRequestCount() == 0);
+ }
+
+ template<> template<>
+ void jsonrpcws_object::test<4>()
+ {
+ set_test_name("an unauthenticated request is answered -32002; a notification gets nothing back");
+
+ CapturingJSONRPCConnection conn;
+ ensure("unauthenticated by default", !conn.isAuthenticated());
+
+ const LLSD request = LLJSONRPCConnection::makeEnvelope(LLSD("req_1"), "object.list", LLSD::emptyMap(), LLSD(), LLSD());
+ conn.processMessage(request);
+ ensure_equals("exactly one message sent for the request", conn.mSent.size(), (size_t)1);
+
+ boost::system::error_code ec;
+ const LLSD sent = LlsdFromJson(boost::json::parse(conn.mSent[0], ec));
+ ensure("parsed without error", !ec);
+ ensure_equals("unauthorized error code", sent["error"]["code"].asInteger(), LLJSONRPCConnection::RPCError::UNAUTHORIZED);
+
+ const LLSD notification = LLJSONRPCConnection::makeEnvelope(LLSD(), "object.list", LLSD::emptyMap(), LLSD(), LLSD());
+ conn.processMessage(notification);
+ ensure_equals("nothing sent for a notification", conn.mSent.size(), (size_t)1);
+ }
+
+ template<> template<>
+ void jsonrpcws_object::test<5>()
+ {
+ set_test_name("malformed JSON is answered with a parse error, and the connection survives");
+
+ CapturingJSONRPCConnection conn;
+ conn.setAuthenticated(true);
+
+ conn.onMessage("{not valid json");
+ ensure_equals("a parse error is sent", conn.mSent.size(), (size_t)1);
+
+ boost::system::error_code ec;
+ const LLSD sent = LlsdFromJson(boost::json::parse(conn.mSent[0], ec));
+ ensure("parsed without error", !ec);
+ ensure_equals("parse error code", sent["error"]["code"].asInteger(), LLJSONRPCConnection::RPCError::PARSE_ERROR);
+
+ // The connection itself is unaffected: valid follow-up traffic still works.
+ int calls = 0;
+ conn.registerMethod("object.list",
+ [&](const std::string&, const LLSD&, const LLSD&) -> LLSD
+ {
+ ++calls;
+ return LLSD();
+ });
+ conn.onMessage(boost::json::serialize(LlsdToJson(LLJSONRPCConnection::makeEnvelope(LLSD("req_1"), "object.list", LLSD::emptyMap(), LLSD(), LLSD()))));
+ ensure_equals("a later valid request still dispatches", calls, 1);
+ }
+
+ template<> template<>
+ void jsonrpcws_object::test<6>()
+ {
+ set_test_name("a batch request is rejected outright; batch support is intentionally disabled");
+
+ CapturingJSONRPCConnection conn;
+ conn.setAuthenticated(true);
+
+ const LLSD one = LLJSONRPCConnection::makeEnvelope(LLSD("req_1"), "object.list", LLSD::emptyMap(), LLSD(), LLSD());
+ LLSD batch = LLSD::emptyArray();
+ batch.append(one);
+ batch.append(one);
+
+ conn.onMessage(boost::json::serialize(LlsdToJson(batch)));
+ ensure_equals("a single error is sent for the whole batch", conn.mSent.size(), (size_t)1);
+
+ boost::system::error_code ec;
+ const LLSD sent = LlsdFromJson(boost::json::parse(conn.mSent[0], ec));
+ ensure("parsed without error", !ec);
+ ensure_equals("invalid request error code", sent["error"]["code"].asInteger(), LLJSONRPCConnection::RPCError::INVALID_REQUEST);
+ }
+
+ template<> template<>
+ void jsonrpcws_object::test<7>()
+ {
+ set_test_name("a missing or wrong jsonrpc version is rejected by validateMessage");
+
+ TestJSONRPCConnection conn;
+
+ LLSD missing_version;
+ missing_version["method"] = "object.list";
+ ensure("missing version fails validation", !conn.validateMessage(missing_version, true));
+
+ LLSD wrong_version = missing_version;
+ wrong_version["jsonrpc"] = "1.0";
+ ensure("wrong version fails validation", !conn.validateMessage(wrong_version, true));
+
+ LLSD correct_version = missing_version;
+ correct_version["jsonrpc"] = "2.0";
+ ensure("correct version passes validation", conn.validateMessage(correct_version, true));
+ }
+
+ template<> template<>
+ void jsonrpcws_object::test<8>()
+ {
+ set_test_name("params of the wrong type are rejected; the handler is never invoked");
+
+ TestJSONRPCConnection conn;
+ conn.setAuthenticated(true);
+
+ LLSD bad_params;
+ bad_params["jsonrpc"] = "2.0";
+ bad_params["method"] = "object.list";
+ bad_params["params"] = "not-an-array-or-object";
+ ensure("string params fail validation", !conn.validateMessage(bad_params, true));
+
+ int calls = 0;
+ conn.registerMethod("object.list",
+ [&](const std::string&, const LLSD&, const LLSD&) -> LLSD
+ {
+ ++calls;
+ return LLSD();
+ });
+
+ conn.processMessage(bad_params);
+ ensure_equals("the handler is never invoked", calls, 0);
+
+ LLSD good_params = bad_params;
+ good_params["params"] = LLSD::emptyMap();
+ conn.processMessage(good_params);
+ ensure_equals("valid params do dispatch", calls, 1);
+ }
+
+ template<> template<>
+ void jsonrpcws_object::test<9>()
+ {
+ set_test_name("a response for an unknown id is silently ignored");
+
+ TestJSONRPCConnection conn;
+
+ bool callback_called = false;
+ conn.testInjectPendingRequest(
+ "req_1",
+ LLTimer::getTotalSeconds() + 60.0,
+ [&](const LLSD&, const LLSD&)
+ {
+ callback_called = true;
+ });
+
+ LLSD result;
+ result["value"] = "ok";
+ conn.processMessage(LLJSONRPCConnection::makeEnvelope(LLSD("unknown_id"), std::string(), LLSD(), result, LLSD()));
+
+ ensure("no callback fired for an unrelated id", !callback_called);
+ ensure_equals("the real pending request is still tracked", conn.testPendingRequestCount(), (size_t)1);
+
+ conn.processMessage(LLJSONRPCConnection::makeEnvelope(LLSD("req_1"), std::string(), LLSD(), result, LLSD()));
+ ensure("the callback fires for its own id", callback_called);
+ ensure_equals("and it is removed once answered", conn.testPendingRequestCount(), (size_t)0);
+ }
+
+ template<> template<>
+ void jsonrpcws_object::test<10>()
+ {
+ set_test_name("a response with both result and error, or with neither, is rejected as invalid");
+
+ TestJSONRPCConnection conn;
+
+ LLSD both;
+ both["jsonrpc"] = "2.0";
+ both["id"] = "req_1";
+ both["result"] = "ok";
+ both["error"]["code"] = -1;
+ both["error"]["message"] = "bad";
+ ensure("result and error together fail validation", !conn.validateMessage(both, false));
+
+ LLSD neither;
+ neither["jsonrpc"] = "2.0";
+ neither["id"] = "req_1";
+ ensure("neither result nor error fails validation", !conn.validateMessage(neither, false));
+
+ LLSD result_only;
+ result_only["jsonrpc"] = "2.0";
+ result_only["id"] = "req_1";
+ result_only["result"] = "ok";
+ ensure("result alone passes validation", conn.validateMessage(result_only, false));
+
+ bool callback_called = false;
+ conn.testInjectPendingRequest(
+ "req_1",
+ LLTimer::getTotalSeconds() + 60.0,
+ [&](const LLSD&, const LLSD&)
+ {
+ callback_called = true;
+ });
+
+ conn.processMessage(both);
+ ensure("an invalid response never reaches the callback", !callback_called);
+ ensure_equals("the pending request is still tracked", conn.testPendingRequestCount(), (size_t)1);
+ }
+
+ template<> template<>
+ void jsonrpcws_object::test<11>()
+ {
+ set_test_name("an unauthenticated peer is told unauthorized, never that the method doesn't exist");
+
+ CapturingJSONRPCConnection conn;
+
+ const LLSD request = LLJSONRPCConnection::makeEnvelope(LLSD("req_1"), "no.such.method", LLSD::emptyMap(), LLSD(), LLSD());
+ conn.processMessage(request);
+ ensure_equals("one message sent", conn.mSent.size(), (size_t)1);
+
+ boost::system::error_code ec;
+ LLSD sent = LlsdFromJson(boost::json::parse(conn.mSent[0], ec));
+ ensure("parsed without error", !ec);
+ ensure_equals("unauthorized, not method-not-found", sent["error"]["code"].asInteger(), LLJSONRPCConnection::RPCError::UNAUTHORIZED);
+
+ conn.setAuthenticated(true);
+ conn.processMessage(request);
+ ensure_equals("a second message sent once authenticated", conn.mSent.size(), (size_t)2);
+
+ sent = LlsdFromJson(boost::json::parse(conn.mSent[1], ec));
+ ensure("parsed without error", !ec);
+ ensure_equals("now it is method-not-found", sent["error"]["code"].asInteger(), LLJSONRPCConnection::RPCError::METHOD_NOT_FOUND);
+ }
+
+ template<> template<>
+ void jsonrpcws_object::test<12>()
+ {
+ set_test_name("unregisterMethod removes both sync and async handlers");
+
+ TestJSONRPCConnection conn;
+
+ conn.registerMethod("sync.method",
+ [](const std::string&, const LLSD&, const LLSD&) -> LLSD
+ {
+ return LLSD();
+ });
+ conn.registerAsyncMethod("async.method",
+ [](const std::string&, const LLSD&, const LLSD&) -> LLSD
+ {
+ return LLSD();
+ });
+
+ ensure_equals("sync method registered", conn.getMethods().count("sync.method"), (size_t)1);
+ ensure_equals("async method registered", conn.getMethods().count("async.method"), (size_t)1);
+
+ conn.unregisterMethod("sync.method");
+ conn.unregisterMethod("async.method");
+
+ ensure_equals("sync method gone", conn.getMethods().count("sync.method"), (size_t)0);
+ ensure_equals("async method gone", conn.getMethods().count("async.method"), (size_t)0);
+ }
+
+ template<> template<>
+ void jsonrpcws_object::test<13>()
+ {
+ set_test_name("a handler throwing a plain std::exception is converted to a generic internal error");
+
+ CapturingJSONRPCConnection conn;
+ conn.setAuthenticated(true);
+
+ conn.registerMethod("boom",
+ [](const std::string&, const LLSD&, const LLSD&) -> LLSD
+ {
+ throw std::runtime_error("boom detail");
+ });
+
+ conn.processMessage(LLJSONRPCConnection::makeEnvelope(LLSD("req_1"), "boom", LLSD::emptyMap(), LLSD(), LLSD()));
+ ensure_equals("one message sent", conn.mSent.size(), (size_t)1);
+
+ boost::system::error_code ec;
+ const LLSD sent = LlsdFromJson(boost::json::parse(conn.mSent[0], ec));
+ ensure("parsed without error", !ec);
+ ensure_equals("internal error code", sent["error"]["code"].asInteger(), LLJSONRPCConnection::RPCError::INTERNAL_ERROR);
+
+ // The exception's own message must never reach the client -- it can
+ // carry paths, connection strings, or other internal detail.
+ ensure("the exception's own message is not forwarded to the client",
+ sent["error"]["message"].asString().find("boom detail") == std::string::npos);
+ }
+
+ template<> template<>
+ void jsonrpcws_object::test<14>()
+ {
+ set_test_name("call() with no way to send returns undefined and leaves nothing pending");
+
+ TestJSONRPCConnection conn;
+
+ bool callback_called = false;
+ const LLSD id = conn.call("object.list", LLSD::emptyMap(),
+ [&](const LLSD&, const LLSD&)
+ {
+ callback_called = true;
+ });
+
+ ensure("call() reports it could not send", id.isUndefined());
+ ensure_equals("nothing left pending", conn.testPendingRequestCount(), (size_t)0);
+ ensure("the callback is never invoked for an unsent call", !callback_called);
+ }
+
+ template<> template<>
+ void jsonrpcws_object::test<15>()
+ {
+ set_test_name("the built-in system.* methods are gated by authentication like any other method");
+
+ TestJSONRPCServer server;
+ auto conn = std::make_shared<CapturingJSONRPCConnection>();
+ server.setupConnectionMethods(conn);
+
+ ensure_equals("system.listMethods is registered", conn->getMethods().count("system.listMethods"), (size_t)1);
+ ensure_equals("system.getStats is registered", conn->getMethods().count("system.getStats"), (size_t)1);
+
+ conn->processMessage(LLJSONRPCConnection::makeEnvelope(LLSD("req_1"), "system.listMethods", LLSD::emptyMap(), LLSD(), LLSD()));
+ ensure_equals("one message sent", conn->mSent.size(), (size_t)1);
+
+ boost::system::error_code ec;
+ const LLSD sent = LlsdFromJson(boost::json::parse(conn->mSent[0], ec));
+ ensure("parsed without error", !ec);
+ ensure_equals("unauthenticated, even a built-in method is refused", sent["error"]["code"].asInteger(), LLJSONRPCConnection::RPCError::UNAUTHORIZED);
+ }
+
+ template<> template<>
+ void jsonrpcws_object::test<16>()
+ {
+ set_test_name("makeEnvelope includes id and method by the documented rules");
+
+ // Notification: id undefined, method non-empty -> id omitted.
+ const LLSD notification = LLJSONRPCConnection::makeEnvelope(LLSD(), "object.list", LLSD::emptyMap(), LLSD(), LLSD());
+ ensure("notification omits id", !notification.has("id"));
+ ensure_equals("notification keeps method", notification["method"].asString(), std::string("object.list"));
+
+ // Request: id defined, method non-empty -> both kept.
+ const LLSD request = LLJSONRPCConnection::makeEnvelope(LLSD("req_1"), "object.list", LLSD::emptyMap(), LLSD(), LLSD());
+ ensure("request keeps id", request.has("id"));
+ ensure_equals("request keeps method", request["method"].asString(), std::string("object.list"));
+
+ // Success response: id defined, method empty -> id kept, method omitted.
+ const LLSD response = LLJSONRPCConnection::makeEnvelope(LLSD("req_1"), std::string(), LLSD(), LLSD("ok"), LLSD());
+ ensure("response keeps id", response.has("id"));
+ ensure("response omits method", !response.has("method"));
+ ensure_equals("response keeps result", response["result"].asString(), std::string("ok"));
+
+ // Error response with an undefined id (e.g. a parse error): method
+ // is empty, so id is kept even though undefined -- serializes as null.
+ LLSD error_obj;
+ error_obj["code"] = -32700;
+ error_obj["message"] = "Parse error";
+ const LLSD error_response = LLJSONRPCConnection::makeEnvelope(LLSD(), std::string(), LLSD(), LLSD(), error_obj);
+ ensure("error response keeps id even when undefined", error_response.has("id"));
+ ensure("that id is null", error_response["id"].isUndefined());
+ ensure("error response omits method", !error_response.has("method"));
+ }
+
+ template<> template<>
+ void jsonrpcws_object::test<17>()
+ {
+ set_test_name("onClose fails every pending request with CONNECTION_CLOSED, exactly once");
+
+ TestJSONRPCConnection conn;
+
+ int callback_count_a = 0;
+ int callback_count_b = 0;
+ conn.testInjectPendingRequest(
+ "req_a",
+ LLTimer::getTotalSeconds() + 60.0,
+ [&](const LLSD& result, const LLSD& error)
+ {
+ ++callback_count_a;
+ ensure("no result on connection close", result.isUndefined());
+ ensure_equals("connection closed error code", error["code"].asInteger(), LLJSONRPCConnection::RPCError::CONNECTION_CLOSED);
+ });
+ conn.testInjectPendingRequest(
+ "req_b",
+ LLTimer::getTotalSeconds() + 60.0,
+ [&](const LLSD&, const LLSD& error)
+ {
+ ++callback_count_b;
+ ensure_equals("connection closed error code", error["code"].asInteger(), LLJSONRPCConnection::RPCError::CONNECTION_CLOSED);
+ });
+ ensure_equals("two requests pending", conn.testPendingRequestCount(), (size_t)2);
+
+ conn.onClose();
+
+ ensure_equals("first callback fires exactly once", callback_count_a, 1);
+ ensure_equals("second callback fires exactly once", callback_count_b, 1);
+ ensure_equals("nothing left pending", conn.testPendingRequestCount(), (size_t)0);
+ }
+}
diff --git a/indra/llcorehttp/tests/llcorehttp_test.cpp b/indra/llcorehttp/tests/llcorehttp_test.cpp
index 65d7fe93a9..cc3c241a96 100644
--- a/indra/llcorehttp/tests/llcorehttp_test.cpp
+++ b/indra/llcorehttp/tests/llcorehttp_test.cpp
@@ -45,6 +45,7 @@
#include "test_httpheaders.hpp"
#include "test_httprequestqueue.hpp"
#include "test_jsonrpcws.hpp"
+#include "test_websocketmgr.hpp"
#include "_httpservice.h"
#include "llproxy.h"
diff --git a/indra/llcorehttp/tests/test_jsonrpcws.hpp b/indra/llcorehttp/tests/test_jsonrpcws.hpp
index 6cf7932aa0..a40d0117b8 100644
--- a/indra/llcorehttp/tests/test_jsonrpcws.hpp
+++ b/indra/llcorehttp/tests/test_jsonrpcws.hpp
@@ -24,6 +24,7 @@ public:
using LLJSONRPCConnection::testInjectPendingRequest;
using LLJSONRPCConnection::testPendingRequestCount;
using LLJSONRPCConnection::testSweepTimeouts;
+ using LLJSONRPCConnection::setAuthenticated;
};
}
@@ -91,6 +92,7 @@ namespace tut
set_test_name("processMessage dispatches notification handler");
TestJSONRPCConnection conn;
+ conn.setAuthenticated(true);
bool called = false;
LLSD seen_id;
@@ -121,6 +123,7 @@ namespace tut
set_test_name("sweepTimeouts expires overdue callbacks");
TestJSONRPCConnection conn;
+ conn.setAuthenticated(true);
bool callback_called = false;
conn.testInjectPendingRequest(
@@ -141,6 +144,60 @@ namespace tut
ensure("timeout callback should be called", callback_called);
ensure_equals("pending request should be removed", conn.testPendingRequestCount(), (size_t)0);
}
+
+ template<> template<>
+ void JSONRPCWSTestObjectType::test<6>()
+ {
+ set_test_name("a connection not yet authenticated dispatches nothing, and everything once it is");
+
+ TestJSONRPCConnection conn;
+ ensure("unauthenticated unless told otherwise", !conn.isAuthenticated());
+
+ int calls = 0;
+ conn.registerMethod("object.list",
+ [&](const std::string&, const LLSD&, const LLSD&) -> LLSD
+ {
+ ++calls;
+ return LLSD();
+ });
+
+ conn.setAuthenticated(false);
+ ensure("not authenticated", !conn.isAuthenticated());
+ const LLSD request = LLJSONRPCConnection::makeEnvelope(LLSD("req_1"), "object.list", LLSD::emptyMap(), LLSD(), LLSD());
+ conn.processMessage(request);
+ const LLSD notification = LLJSONRPCConnection::makeEnvelope(LLSD(), "object.list", LLSD::emptyMap(), LLSD(), LLSD());
+ conn.processMessage(notification);
+ ensure_equals("neither a request nor a notification reaches the handler", calls, 0);
+
+ conn.setAuthenticated(true);
+ conn.processMessage(request);
+ ensure_equals("dispatched once authenticated", calls, 1);
+ }
+
+ template<> template<>
+ void JSONRPCWSTestObjectType::test<7>()
+ {
+ set_test_name("a connection not yet authenticated still takes the answers to its own calls");
+
+ TestJSONRPCConnection conn;
+ conn.setAuthenticated(false);
+
+ LLSD answer;
+ conn.testInjectPendingRequest(
+ "rpc_1",
+ LLTimer::getTotalSeconds() + 60.0,
+ [&](const LLSD& result, const LLSD& error)
+ {
+ ensure("no error", error.isUndefined());
+ answer = result;
+ });
+
+ LLSD result;
+ result["challenge_response"] = "secret";
+ conn.processMessage(LLJSONRPCConnection::makeEnvelope(LLSD("rpc_1"), std::string(), LLSD(), result, LLSD()));
+ ensure_equals("the answer reached the caller", answer["challenge_response"].asString(), std::string("secret"));
+ ensure("and it is answered", conn.testPendingRequestCount() == 0);
+ }
}
#endif \ No newline at end of file
diff --git a/indra/llcorehttp/tests/test_websocketmgr.hpp b/indra/llcorehttp/tests/test_websocketmgr.hpp
new file mode 100644
index 0000000000..38b3d79cbf
--- /dev/null
+++ b/indra/llcorehttp/tests/test_websocketmgr.hpp
@@ -0,0 +1,157 @@
+/**
+ * @file test_websocketmgr.hpp
+ * @brief A WebSocket server lets programs in and turns pages away.
+ *
+ * $LicenseInfo:firstyear=2026&license=viewerlgpl$
+ * Second Life Viewer Source Code
+ * Copyright (C) 2026, 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$
+ */
+
+#ifndef TEST_LLCORE_WEBSOCKETMGR_H_
+#define TEST_LLCORE_WEBSOCKETMGR_H_
+
+#include "llwebsocketmgr.h"
+
+#include <boost/asio/io_context.hpp>
+#include <boost/asio/ip/tcp.hpp>
+#include <boost/asio/read_until.hpp>
+#include <boost/asio/streambuf.hpp>
+#include <boost/asio/write.hpp>
+
+#include <chrono>
+#include <istream>
+#include <random>
+
+namespace
+{
+ // A port nothing on this machine is listening on, as far as can be
+ // told; zero where none turned up.
+ U16 unusedPort()
+ {
+ std::random_device random;
+ for (int tries = 0; tries < 20; ++tries)
+ {
+ const U16 port = static_cast<U16>(40000 + random() % 20000);
+ boost::asio::io_context io;
+ boost::asio::ip::tcp::acceptor acceptor(io);
+ boost::system::error_code ec;
+ acceptor.open(boost::asio::ip::tcp::v4(), ec);
+ if (!ec)
+ {
+ acceptor.bind(boost::asio::ip::tcp::endpoint(boost::asio::ip::make_address("127.0.0.1"), port), ec);
+ }
+ if (!ec)
+ {
+ return port;
+ }
+ }
+ return 0;
+ }
+
+ // The HTTP status a WebSocket upgrade request from this origin -- none,
+ // where empty -- is answered with: 101 where it would open, -1 where
+ // nothing answered at all. The request is what a browser or a program
+ // sends, written out, so that what is tested is what the server says.
+ int upgradeFrom(U16 port, const std::string& origin)
+ {
+ boost::asio::io_context io;
+ boost::asio::ip::tcp::socket socket(io);
+ boost::system::error_code ec;
+ socket.connect(boost::asio::ip::tcp::endpoint(boost::asio::ip::make_address("127.0.0.1"), port), ec);
+ if (ec)
+ {
+ return -1;
+ }
+
+ std::string request = "GET / HTTP/1.1\r\n"
+ "Host: 127.0.0.1:" + std::to_string(port) + "\r\n"
+ "Upgrade: websocket\r\n"
+ "Connection: Upgrade\r\n"
+ "Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==\r\n"
+ "Sec-WebSocket-Version: 13\r\n";
+ if (!origin.empty())
+ {
+ request += "Origin: " + origin + "\r\n";
+ }
+ request += "\r\n";
+ boost::asio::write(socket, boost::asio::buffer(request), ec);
+ if (ec)
+ {
+ return -1;
+ }
+
+ // The status line, or nothing within the time.
+ boost::asio::streambuf response;
+ bool answered = false;
+ boost::asio::async_read_until(socket, response, "\r\n",
+ [&](const boost::system::error_code& error, std::size_t) { answered = !error; });
+ io.run_for(std::chrono::seconds(10));
+ socket.close(ec);
+ if (!answered)
+ {
+ return -1;
+ }
+ std::istream line(&response);
+ std::string version;
+ int status = -1;
+ line >> version >> status;
+ return status;
+ }
+}
+
+namespace tut
+{
+ struct WebsocketMgrTestData
+ {
+ };
+
+ typedef test_group<WebsocketMgrTestData> WebsocketMgrTestGroupType;
+ typedef WebsocketMgrTestGroupType::object WebsocketMgrTestObjectType;
+ WebsocketMgrTestGroupType WebsocketMgrTestGroup("LLWebsocketMgr Tests");
+
+ template<> template<>
+ void WebsocketMgrTestObjectType::test<1>()
+ {
+ set_test_name("a program, which sends no origin, is let in; a page, which always does, is refused");
+
+ const U16 port = unusedPort();
+ if (!port)
+ {
+ skip("no port free to listen on");
+ }
+ const std::string name = "origin_test";
+ LLWebsocketMgr& manager = LLWebsocketMgr::instance();
+ ensure("added", manager.addServer(std::make_shared<LLWebsocketMgr::WSServer>(name, port, true)));
+ ensure("started", manager.startServer(name));
+
+ const int program = upgradeFrom(port, std::string());
+ const int page = upgradeFrom(port, "https://example.com");
+ const int opaque = upgradeFrom(port, "null");
+ const int local = upgradeFrom(port, "http://127.0.0.1:8080");
+ manager.removeServer(name);
+
+ ensure_equals("no origin: in", program, 101);
+ ensure_equals("a web page: refused", page, 403);
+ ensure_equals("a page with an opaque origin, a sandboxed frame or a file: refused", opaque, 403);
+ ensure_equals("a page served from this machine: refused", local, 403);
+ }
+}
+
+#endif
diff --git a/indra/llcorehttp/tests/testwsclient.h b/indra/llcorehttp/tests/testwsclient.h
new file mode 100644
index 0000000000..587934f2b6
--- /dev/null
+++ b/indra/llcorehttp/tests/testwsclient.h
@@ -0,0 +1,170 @@
+/**
+ * @file testwsclient.h
+ * @brief Minimal WebSocket client for driving a server under test
+ *
+ * $LicenseInfo:firstyear=2026&license=viewerlgpl$
+ * Second Life Viewer Source Code
+ * Copyright (C) 2026, 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$
+ */
+
+#ifndef LL_TESTWSCLIENT_H
+#define LL_TESTWSCLIENT_H
+
+// MSVC does not report __cplusplus >= 201103L without /Zc:__cplusplus, so
+// websocketpp's own C++11 detection would otherwise fall back to
+// boost::random_device, which nothing here links.
+#define _WEBSOCKETPP_CPP11_RANDOM_DEVICE_
+
+#include <websocketpp/config/asio_no_tls_client.hpp>
+#include <websocketpp/client.hpp>
+
+#include <chrono>
+#include <condition_variable>
+#include <mutex>
+#include <string>
+#include <thread>
+#include <vector>
+
+// A minimal WebSocket client for driving a server under test: connects,
+// optionally with an Origin header, and can send and collect messages.
+class TestWSClient
+{
+public:
+ using client_t = websocketpp::client<websocketpp::config::asio_client>;
+
+ TestWSClient()
+ {
+ mClient.clear_access_channels(websocketpp::log::alevel::all);
+ mClient.clear_error_channels(websocketpp::log::elevel::all);
+ mClient.init_asio();
+
+ mClient.set_open_handler([this](websocketpp::connection_hdl hdl)
+ {
+ std::lock_guard<std::mutex> lock(mMutex);
+ mHandle = hdl;
+ mOpened = true;
+ mCondVar.notify_all();
+ });
+ mClient.set_fail_handler([this](websocketpp::connection_hdl)
+ {
+ std::lock_guard<std::mutex> lock(mMutex);
+ mFailed = true;
+ mCondVar.notify_all();
+ });
+ mClient.set_close_handler([this](websocketpp::connection_hdl)
+ {
+ std::lock_guard<std::mutex> lock(mMutex);
+ mClosed = true;
+ mCondVar.notify_all();
+ });
+ mClient.set_message_handler([this](websocketpp::connection_hdl, client_t::message_ptr msg)
+ {
+ std::lock_guard<std::mutex> lock(mMutex);
+ mReceived.push_back(msg->get_payload());
+ mCondVar.notify_all();
+ });
+ }
+
+ ~TestWSClient()
+ {
+ close();
+ mClient.stop();
+ if (mThread.joinable())
+ {
+ mThread.join();
+ }
+ }
+
+ // Initiates a close and waits for the ack, so teardown looks like a
+ // normal closure instead of a dropped socket. Safe to call more than
+ // once, and safe if the connection never opened.
+ void close(std::chrono::seconds timeout = std::chrono::seconds(2))
+ {
+ std::unique_lock<std::mutex> lock(mMutex);
+ if (!mOpened || mClosed)
+ {
+ return;
+ }
+ websocketpp::lib::error_code ec;
+ mClient.close(mHandle, websocketpp::close::status::normal, "test done", ec);
+ mCondVar.wait_for(lock, timeout, [this]() { return mClosed; });
+ }
+
+ // Connects to 127.0.0.1:port, optionally with an Origin header, and
+ // waits up to timeout for the handshake to open or fail.
+ // force_origin_header sends "Origin:" with an empty value instead of
+ // omitting the header entirely, for origin.empty().
+ bool connect(U16 port, const std::string& origin, bool force_origin_header = false,
+ std::chrono::seconds timeout = std::chrono::seconds(5))
+ {
+ websocketpp::lib::error_code ec;
+ client_t::connection_ptr con = mClient.get_connection(
+ "ws://127.0.0.1:" + std::to_string(port) + "/", ec);
+ if (ec)
+ {
+ return false;
+ }
+ if (!origin.empty() || force_origin_header)
+ {
+ con->append_header("Origin", origin);
+ }
+ mClient.connect(con);
+ mThread = std::thread([this]() { mClient.run(); });
+
+ std::unique_lock<std::mutex> lock(mMutex);
+ return mCondVar.wait_for(lock, timeout, [this]() { return mOpened || mFailed; }) && mOpened;
+ }
+
+ bool send(const std::string& message)
+ {
+ websocketpp::lib::error_code ec;
+ mClient.send(mHandle, message, websocketpp::frame::opcode::text, ec);
+ return !ec;
+ }
+
+ // Waits up to timeout for at least one message to arrive, then
+ // removes and returns it; empty where none arrived in time.
+ std::string waitForMessage(std::chrono::seconds timeout = std::chrono::seconds(5))
+ {
+ std::unique_lock<std::mutex> lock(mMutex);
+ if (!mCondVar.wait_for(lock, timeout, [this]() { return !mReceived.empty(); }))
+ {
+ return std::string();
+ }
+ std::string message = mReceived.front();
+ mReceived.erase(mReceived.begin());
+ return message;
+ }
+
+ bool isOpen() const { return mOpened && !mClosed; }
+
+private:
+ client_t mClient;
+ websocketpp::connection_hdl mHandle;
+ std::thread mThread;
+ std::mutex mMutex;
+ std::condition_variable mCondVar;
+ bool mOpened{ false };
+ bool mFailed{ false };
+ bool mClosed{ false };
+ std::vector<std::string> mReceived;
+};
+
+#endif // LL_TESTWSCLIENT_H
diff --git a/indra/llcorehttp/tests/wsserver_test.cpp b/indra/llcorehttp/tests/wsserver_test.cpp
new file mode 100644
index 0000000000..62fa66a9cd
--- /dev/null
+++ b/indra/llcorehttp/tests/wsserver_test.cpp
@@ -0,0 +1,438 @@
+/**
+ * @file wsserver_test.cpp
+ * @brief Security-focused unit tests for LLWebsocketMgr::WSServer
+ *
+ * $LicenseInfo:firstyear=2026&license=viewerlgpl$
+ * Second Life Viewer Source Code
+ * Copyright (C) 2026, 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 "linden_common.h"
+
+#include "llwebsocketmgr.h"
+
+#include "testwsclient.h"
+#include "../test/lltut.h"
+
+#include <boost/asio/io_context.hpp>
+#include <boost/asio/ip/tcp.hpp>
+#include <boost/asio/ip/udp.hpp>
+#include <boost/asio/read_until.hpp>
+#include <boost/asio/streambuf.hpp>
+#include <boost/asio/write.hpp>
+
+#include <atomic>
+#include <chrono>
+#include <istream>
+#include <random>
+#include <thread>
+
+namespace
+{
+ // A port nothing on this machine is listening on, as far as can be
+ // told; zero where none turned up.
+ U16 unusedPort()
+ {
+ std::random_device random;
+ for (int tries = 0; tries < 20; ++tries)
+ {
+ const U16 port = static_cast<U16>(40000 + random() % 20000);
+ boost::asio::io_context io;
+ boost::asio::ip::tcp::acceptor acceptor(io);
+ boost::system::error_code ec;
+ acceptor.open(boost::asio::ip::tcp::v4(), ec);
+ if (!ec)
+ {
+ acceptor.bind(boost::asio::ip::tcp::endpoint(boost::asio::ip::make_address("127.0.0.1"), port), ec);
+ }
+ if (!ec)
+ {
+ return port;
+ }
+ }
+ return 0;
+ }
+
+ // The HTTP status a WebSocket upgrade request from this origin -- none,
+ // where empty -- is answered with: 101 where it would open, -1 where
+ // nothing answered at all.
+ // force_origin_header sends "Origin:" with an empty value instead of
+ // omitting the header entirely, for origin.empty().
+ int upgradeFrom(U16 port, const std::string& origin, bool force_origin_header = false)
+ {
+ boost::asio::io_context io;
+ boost::asio::ip::tcp::socket socket(io);
+ boost::system::error_code ec;
+ socket.connect(boost::asio::ip::tcp::endpoint(boost::asio::ip::make_address("127.0.0.1"), port), ec);
+ if (ec)
+ {
+ return -1;
+ }
+
+ std::string request = "GET / HTTP/1.1\r\n"
+ "Host: 127.0.0.1:" + std::to_string(port) + "\r\n"
+ "Upgrade: websocket\r\n"
+ "Connection: Upgrade\r\n"
+ "Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==\r\n"
+ "Sec-WebSocket-Version: 13\r\n";
+ if (!origin.empty() || force_origin_header)
+ {
+ request += "Origin: " + origin + "\r\n";
+ }
+ request += "\r\n";
+ boost::asio::write(socket, boost::asio::buffer(request), ec);
+ if (ec)
+ {
+ return -1;
+ }
+
+ boost::asio::streambuf response;
+ bool answered = false;
+ boost::asio::async_read_until(socket, response, "\r\n",
+ [&](const boost::system::error_code& error, std::size_t) { answered = !error; });
+ io.run_for(std::chrono::seconds(10));
+ socket.close(ec);
+ if (!answered)
+ {
+ return -1;
+ }
+ std::istream line(&response);
+ std::string version;
+ int status = -1;
+ line >> version >> status;
+ return status;
+ }
+
+ // Polls a predicate until it is true or the deadline passes. A client
+ // seeing its own handshake complete says nothing about whether the
+ // server's own thread has finished its side of onOpen bookkeeping yet.
+ template <typename Predicate>
+ bool waitUntil(Predicate predicate, std::chrono::milliseconds timeout = std::chrono::milliseconds(2000))
+ {
+ const auto deadline = std::chrono::steady_clock::now() + timeout;
+ while (!predicate())
+ {
+ if (std::chrono::steady_clock::now() >= deadline)
+ {
+ return false;
+ }
+ std::this_thread::sleep_for(std::chrono::milliseconds(10));
+ }
+ return true;
+ }
+
+ // The machine's own non-loopback IPv4 address, as seen when routing
+ // toward the internet (no packet is actually sent); empty where none
+ // could be found, such as a loopback-only sandbox.
+ std::string localNonLoopbackAddress()
+ {
+ boost::asio::io_context io;
+ boost::asio::ip::udp::socket socket(io);
+ boost::system::error_code ec;
+ socket.connect(boost::asio::ip::udp::endpoint(boost::asio::ip::make_address("8.8.8.8"), 53), ec);
+ if (ec)
+ {
+ return std::string();
+ }
+ return socket.local_endpoint(ec).address().to_string();
+ }
+
+ // Whether a TCP connection to host:port succeeds within a short time;
+ // refused or silently dropped are both treated as "no".
+ bool canConnect(const std::string& host, U16 port)
+ {
+ boost::asio::io_context io;
+ boost::asio::ip::tcp::socket socket(io);
+ boost::system::error_code result = boost::asio::error::would_block;
+ socket.async_connect(
+ boost::asio::ip::tcp::endpoint(boost::asio::ip::make_address(host), port),
+ [&](const boost::system::error_code& ec) { result = ec; });
+ io.run_for(std::chrono::seconds(3));
+ boost::system::error_code close_ec;
+ socket.close(close_ec);
+ return !result;
+ }
+
+ // Records what a server actually did, so tests can assert on lifecycle
+ // callbacks the base class leaves as no-ops.
+ class TestWSServer : public LLWebsocketMgr::WSServer
+ {
+ public:
+ using LLWebsocketMgr::WSServer::WSServer;
+
+ std::atomic<int> mOpenedCount{ 0 };
+ std::atomic<int> mClosedCount{ 0 };
+
+ // Empty where the base policy (reject any Origin) should apply;
+ // otherwise the one origin this test server lets in.
+ std::string mAllowedOrigin;
+
+ void onConnectionOpened(const LLWebsocketMgr::WSConnection::ptr_t&) override
+ {
+ ++mOpenedCount;
+ }
+
+ void onConnectionClosed(const LLWebsocketMgr::WSConnection::ptr_t&) override
+ {
+ ++mClosedCount;
+ }
+
+ bool acceptOrigin(const std::string& origin) const override
+ {
+ if (!mAllowedOrigin.empty() && origin == mAllowedOrigin)
+ {
+ return true;
+ }
+ return WSServer::acceptOrigin(origin);
+ }
+ };
+}
+
+namespace tut
+{
+ struct wsserver_data
+ {
+ U16 mPort;
+ std::string mName;
+
+ wsserver_data() :
+ mPort(unusedPort()),
+ mName("wsserver_test")
+ {
+ }
+
+ ~wsserver_data()
+ {
+ LLWebsocketMgr::instance().removeServer(mName);
+ }
+ };
+
+ typedef test_group<wsserver_data> wsserver_test;
+ typedef wsserver_test::object wsserver_object;
+ wsserver_test wsserver_testcase("LLWebsocketMgr::WSServer");
+
+ template<> template<>
+ void wsserver_object::test<1>()
+ {
+ set_test_name("a program, which sends no Origin header, is let in");
+
+ if (!mPort)
+ {
+ skip("no port free to listen on");
+ }
+
+ LLWebsocketMgr& manager = LLWebsocketMgr::instance();
+ auto server = std::make_shared<LLWebsocketMgr::WSServer>(mName, mPort, true);
+ ensure("added", manager.addServer(server));
+ ensure("started", manager.startServer(mName));
+
+ ensure_equals("no origin: connection opens", upgradeFrom(mPort, std::string()), 101);
+ ensure("server clears the abruptly closed raw connection", waitUntil([&]() { return server->getConnectionCount() == 0; }));
+
+ TestWSClient client;
+ ensure("no origin: client connects over the network", client.connect(mPort, std::string()));
+ ensure("server sees the open connection", waitUntil([&]() { return server->getConnectionCount() == 1; }));
+
+ client.close();
+ manager.removeServer(mName);
+ }
+
+ template<> template<>
+ void wsserver_object::test<2>()
+ {
+ set_test_name("a page, which sends an Origin header, is refused");
+
+ if (!mPort)
+ {
+ skip("no port free to listen on");
+ }
+
+ LLWebsocketMgr& manager = LLWebsocketMgr::instance();
+ auto server = std::make_shared<LLWebsocketMgr::WSServer>(mName, mPort, true);
+ ensure("added", manager.addServer(server));
+ ensure("started", manager.startServer(mName));
+
+ ensure_equals("a web page: refused", upgradeFrom(mPort, "https://example.com"), 403);
+
+ TestWSClient client;
+ ensure("a web page: client never opens over the network", !client.connect(mPort, "https://example.com"));
+ ensure_equals("server never sees a connection", server->getConnectionCount(), (size_t)0);
+
+ manager.removeServer(mName);
+ }
+
+ template<> template<>
+ void wsserver_object::test<3>()
+ {
+ set_test_name("a page with an opaque origin (a sandboxed frame or a file) is refused");
+
+ if (!mPort)
+ {
+ skip("no port free to listen on");
+ }
+
+ LLWebsocketMgr& manager = LLWebsocketMgr::instance();
+ auto server = std::make_shared<LLWebsocketMgr::WSServer>(mName, mPort, true);
+ ensure("added", manager.addServer(server));
+ ensure("started", manager.startServer(mName));
+
+ ensure_equals("an opaque origin: refused", upgradeFrom(mPort, "null"), 403);
+
+ TestWSClient client;
+ ensure("an opaque origin: client never opens over the network", !client.connect(mPort, "null"));
+ ensure_equals("server never sees a connection", server->getConnectionCount(), (size_t)0);
+
+ manager.removeServer(mName);
+ }
+
+ template<> template<>
+ void wsserver_object::test<4>()
+ {
+ set_test_name("a page served from this machine is refused");
+
+ if (!mPort)
+ {
+ skip("no port free to listen on");
+ }
+
+ LLWebsocketMgr& manager = LLWebsocketMgr::instance();
+ auto server = std::make_shared<LLWebsocketMgr::WSServer>(mName, mPort, true);
+ ensure("added", manager.addServer(server));
+ ensure("started", manager.startServer(mName));
+
+ ensure_equals("a page on this machine: refused", upgradeFrom(mPort, "http://127.0.0.1:8080"), 403);
+
+ TestWSClient client;
+ ensure("a page on this machine: client never opens over the network", !client.connect(mPort, "http://127.0.0.1:8080"));
+ ensure_equals("server never sees a connection", server->getConnectionCount(), (size_t)0);
+
+ manager.removeServer(mName);
+ }
+
+ template<> template<>
+ void wsserver_object::test<5>()
+ {
+ set_test_name("an Origin header present but empty is treated the same as none");
+
+ if (!mPort)
+ {
+ skip("no port free to listen on");
+ }
+
+ LLWebsocketMgr& manager = LLWebsocketMgr::instance();
+ auto server = std::make_shared<LLWebsocketMgr::WSServer>(mName, mPort, true);
+ ensure("added", manager.addServer(server));
+ ensure("started", manager.startServer(mName));
+
+ ensure_equals("empty origin: connection opens", upgradeFrom(mPort, std::string(), true), 101);
+ ensure("server clears the abruptly closed raw connection", waitUntil([&]() { return server->getConnectionCount() == 0; }));
+
+ TestWSClient client;
+ ensure("empty origin: client connects over the network", client.connect(mPort, std::string(), true));
+ ensure("server sees the open connection", waitUntil([&]() { return server->getConnectionCount() == 1; }));
+
+ client.close();
+ manager.removeServer(mName);
+ }
+
+ template<> template<>
+ void wsserver_object::test<7>()
+ {
+ set_test_name("local_only binds to loopback only; the machine's own LAN address is unreachable");
+
+ if (!mPort)
+ {
+ skip("no port free to listen on");
+ }
+
+ const std::string lan_address = localNonLoopbackAddress();
+ if (lan_address.empty())
+ {
+ skip("no non-loopback network interface to test against");
+ }
+
+ LLWebsocketMgr& manager = LLWebsocketMgr::instance();
+ auto server = std::make_shared<LLWebsocketMgr::WSServer>(mName, mPort, true);
+ ensure("added", manager.addServer(server));
+ ensure("started", manager.startServer(mName));
+
+ ensure("loopback: reachable", canConnect("127.0.0.1", mPort));
+ ensure("LAN address: unreachable", !canConnect(lan_address, mPort));
+
+ manager.removeServer(mName);
+ }
+
+ template<> template<>
+ void wsserver_object::test<8>()
+ {
+ set_test_name("a refused connection never reaches onConnectionOpened, and an accepted one does");
+
+ if (!mPort)
+ {
+ skip("no port free to listen on");
+ }
+
+ LLWebsocketMgr& manager = LLWebsocketMgr::instance();
+ auto server = std::make_shared<TestWSServer>(mName, mPort, true);
+ ensure("added", manager.addServer(server));
+ ensure("started", manager.startServer(mName));
+
+ ensure_equals("a web page: refused", upgradeFrom(mPort, "https://example.com"), 403);
+ ensure_equals("refused: onConnectionOpened never fires", server->mOpenedCount.load(), 0);
+
+ TestWSClient client;
+ ensure("no origin: client connects over the network", client.connect(mPort, std::string()));
+ ensure("accepted: onConnectionOpened fires once", waitUntil([&]() { return server->mOpenedCount.load() == 1; }));
+
+ client.close();
+ manager.removeServer(mName);
+ }
+
+ template<> template<>
+ void wsserver_object::test<9>()
+ {
+ set_test_name("multiple simultaneous connections are each tracked independently");
+
+ if (!mPort)
+ {
+ skip("no port free to listen on");
+ }
+
+ LLWebsocketMgr& manager = LLWebsocketMgr::instance();
+ auto server = std::make_shared<TestWSServer>(mName, mPort, true);
+ ensure("added", manager.addServer(server));
+ ensure("started", manager.startServer(mName));
+
+ TestWSClient client_a;
+ TestWSClient client_b;
+ TestWSClient client_c;
+ ensure("first client connects", client_a.connect(mPort, std::string()));
+ ensure("second client connects", client_b.connect(mPort, std::string()));
+ ensure("third client connects", client_c.connect(mPort, std::string()));
+
+ ensure("server counts all three connections", waitUntil([&]() { return server->getConnectionCount() == 3; }));
+ ensure("onConnectionOpened fires once per connection", waitUntil([&]() { return server->mOpenedCount.load() == 3; }));
+
+ client_a.close();
+ client_b.close();
+ client_c.close();
+ manager.removeServer(mName);
+ }
+}
diff --git a/indra/llfilesystem/lldiriterator.cpp b/indra/llfilesystem/lldiriterator.cpp
index ca00d25fb7..ec6d1726f0 100644
--- a/indra/llfilesystem/lldiriterator.cpp
+++ b/indra/llfilesystem/lldiriterator.cpp
@@ -69,7 +69,7 @@ LLDirIterator::Impl::Impl(const std::string &dirname, const std::string &mask)
if (!is_dir)
{
- LL_WARNS() << "Invalid path: \"" << dir_path.string() << "\"" << LL_ENDL;
+ LL_WARNS() << "Invalid path: \"" << dirname << "\"" << LL_ENDL;
return;
}
diff --git a/indra/llfilesystem/lldiskcache.cpp b/indra/llfilesystem/lldiskcache.cpp
index e7b43e15e2..5af801a1df 100644
--- a/indra/llfilesystem/lldiskcache.cpp
+++ b/indra/llfilesystem/lldiskcache.cpp
@@ -34,6 +34,7 @@
#include "llapp.h"
#include "llassettype.h"
#include "lldir.h"
+#include "llstring.h"
#include <chrono>
#include <filesystem>
@@ -116,14 +117,14 @@ void LLDiskCache::purge()
}
if (std::filesystem::is_regular_file(*iter, ec) && !ec)
{
- if ((*iter).path().string().find(CACHE_FILENAME_PREFIX) != std::string::npos)
+ if (fsyspath((*iter).path()).string().find(CACHE_FILENAME_PREFIX) != std::string::npos)
{
uintmax_t file_size = std::filesystem::file_size(*iter, ec);
if (ec)
{
continue;
}
- const std::string file_path = (*iter).path().string();
+ const std::string file_path = fsyspath((*iter).path()).string();
const std::filesystem::file_time_type file_time = std::filesystem::last_write_time(*iter, ec);
if (ec)
{
@@ -165,7 +166,7 @@ void LLDiskCache::purge()
}
if (should_remove)
{
- std::filesystem::remove(entry.second.second, ec);
+ std::filesystem::remove(fsyspath(entry.second.second), ec);
if (ec)
{
LL_WARNS() << "Failed to delete cache file " << entry.second.second << ": " << ec.message() << LL_ENDL;
@@ -245,7 +246,7 @@ void LLDiskCache::clearCache()
{
if (std::filesystem::is_regular_file(*iter, ec) && !ec)
{
- if ((*iter).path().string().find(CACHE_FILENAME_PREFIX) != std::string::npos)
+ if (fsyspath((*iter).path()).string().find(CACHE_FILENAME_PREFIX) != std::string::npos)
{
std::filesystem::remove(*iter, ec);
if (ec)
@@ -274,8 +275,9 @@ void LLDiskCache::removeOldVFSFiles()
{
if (std::filesystem::is_regular_file(*iter, ec) && !ec)
{
- if (((*iter).path().string().find(CACHE_FORMAT) != std::string::npos) ||
- ((*iter).path().string().find(DB_FORMAT) != std::string::npos))
+ const std::string file_path = fsyspath((*iter).path()).string();
+ if ((file_path.find(CACHE_FORMAT) != std::string::npos) ||
+ (file_path.find(DB_FORMAT) != std::string::npos))
{
std::filesystem::remove(*iter, ec);
if (ec)
@@ -311,7 +313,7 @@ uintmax_t LLDiskCache::dirFileSize(const std::string& dir)
{
if (std::filesystem::is_regular_file(*iter, ec) && !ec)
{
- if ((*iter).path().string().find(CACHE_FILENAME_PREFIX) != std::string::npos)
+ if (fsyspath((*iter).path()).string().find(CACHE_FILENAME_PREFIX) != std::string::npos)
{
uintmax_t file_size = std::filesystem::file_size(*iter, ec);
if (!ec)
diff --git a/indra/llmath/llvolume.cpp b/indra/llmath/llvolume.cpp
index 09a0ce0b7a..33ec074029 100644
--- a/indra/llmath/llvolume.cpp
+++ b/indra/llmath/llvolume.cpp
@@ -1976,8 +1976,6 @@ LLVolume::LLVolume(const LLVolumeParams &params, const F32 detail, const bool ge
mNumHullIndices = 0;
// set defaults
- mSculptValidationCache.fill(LLSculptValidationState::Unvalidated);
-
if (mParams.getPathParams().getCurveType() == LL_PCODE_PATH_FLEXIBLE)
{
mPathp = new LLDynamicPath();
@@ -2005,13 +2003,6 @@ void LLVolume::resizePath(S32 length)
setDirty();
}
-void LLVolume::setDirty()
-{
- mPathp->setDirty();
- mProfilep->setDirty();
- mSculptValidationCache.fill(LLSculptValidationState::Unvalidated);
-}
-
void LLVolume::regen()
{
generate();
@@ -3293,100 +3284,10 @@ S32 sculpt_sides(F32 detail)
}
}
-static bool validate_sculpt_geometry(const LLVolume* volume)
-{
- if (!volume || volume->getNumVolumeFaces() == 0)
- {
- return true;
- }
-
- // Validate each face in the sculpt
- for (S32 face_idx = 0; face_idx < volume->getNumVolumeFaces(); ++face_idx)
- {
- const LLVolumeFace& face = volume->getVolumeFace(face_idx);
-
- // Skip validation for degenerate faces
- if (face.mNumVertices < 3 || face.mNumIndices < 3)
- {
- continue;
- }
-
- // Calculate 'bounding' surface
- LLVector4a extent_size;
- extent_size.setSub(face.mExtents[1], face.mExtents[0]);
-
- F32 width = extent_size[0];
- F32 height = extent_size[1];
- F32 depth = extent_size[2];
-
- F32 bounding_surface = width * height + width * depth + height * depth;
- bounding_surface *= 2;
-
- if (!llfinite(bounding_surface) || bounding_surface <= FLT_EPSILON)
- {
- return false;
- }
-
- // Calculate total surface area of all triangles
- F32 total_triangle_area = 0.0f;
- S32 num_triangles = face.mNumIndices / 3;
-
- for (S32 i = 0; i < num_triangles; ++i)
- {
- S32 idx_base = i * 3;
- U16 idx0 = face.mIndices[idx_base];
- U16 idx1 = face.mIndices[idx_base + 1];
- U16 idx2 = face.mIndices[idx_base + 2];
-
- // Validate indices
- if (idx0 >= face.mNumVertices || idx1 >= face.mNumVertices || idx2 >= face.mNumVertices)
- {
- continue;
- }
-
- const LLVector4a& v0 = face.mPositions[idx0];
- const LLVector4a& v1 = face.mPositions[idx1];
- const LLVector4a& v2 = face.mPositions[idx2];
-
- // Calculate triangle area
- LLVector4a edge1, edge2, cross;
- edge1.setSub(v1, v0);
- edge2.setSub(v2, v0);
- cross.setCross3(edge1, edge2);
-
- F32 triangle_area = cross.getLength3().getF32() * 0.5f;
- total_triangle_area += triangle_area;
- }
-
- if (!llfinite(total_triangle_area))
- {
- return false;
- }
-
- // The idea here is that the total area of a sculpt is normally comparable
- // to the area of a bounding box. But a random overlapping collection of
- // triangles has a significantly larger area than a sculpt normally has.
- F32 area_ratio = total_triangle_area / bounding_surface;
- constexpr F32 MAX_AREA_TO_AREA_RATIO = 16.0f;
- if (area_ratio > MAX_AREA_TO_AREA_RATIO)
- {
- LL_DEBUGS("LLVOLUME") << "Sculpt rejected: excessive triangle area."
- << " Face index: " << face_idx
- << " Total triangle area: " << total_triangle_area << " sq m"
- << " Bounding surface area: " << bounding_surface << " sq m"
- << " Ratio: " << area_ratio
- << " Triangle count: " << num_triangles
- << " Bounds: [" << width << " x " << height << " x " << depth << "]" << LL_ENDL;
- return false;
- }
- }
-
- return true;
-}
// determine the number of vertices in both s and t direction for this sculpt
-static void sculpt_calc_mesh_resolution(U16 width, U16 height, U8 type, F32 detail, S32& s, S32& t)
+void sculpt_calc_mesh_resolution(U16 width, U16 height, U8 type, F32 detail, S32& s, S32& t)
{
// this code has the following properties:
// 1) the aspect ratio of the mesh is as close as possible to the ratio of the map
@@ -3496,62 +3397,10 @@ void LLVolume::sculpt(U16 sculpt_width, U16 sculpt_height, S8 sculpt_components,
mSculptLevel = sculpt_level;
- LLSculptValidationState cached_state = LLSculptValidationState::Valid;
- if (!data_is_empty && (sculpt_level >= 0) && sculpt_level < SCULPT_CACHE_SIZE)
- {
- // validation might be expensive, so we do it only once per lod.
- cached_state = mSculptValidationCache[sculpt_level];
- }
-
- switch (cached_state)
- {
- case LLSculptValidationState::Valid:
- {
- // Delete any existing faces, then regenerate
- mVolumeFaces.clear();
- createVolumeFaces();
- break;
- }
- case LLSculptValidationState::Invalid:
- {
- // Invalid geometry
- // Regenerate mesh with an empty placeholder
- mVolumeFaces.clear();
- sculptGenerateEmptyPlaceholder();
- createVolumeFaces();
- break;
- }
- case LLSculptValidationState::Unvalidated:
- {
- // First time at this LOD
- mVolumeFaces.clear();
- createVolumeFaces();
-
- bool valid_geometry = true;
-
- // Todo: can this be backed into createVolumeFaces?
- // Or calculated without having to call createVolumeFaces first?
- // Todo 2: should the same be done for meshes?
- valid_geometry = validate_sculpt_geometry(this);
- mSculptValidationCache[sculpt_level] = valid_geometry ? LLSculptValidationState::Valid : LLSculptValidationState::Invalid;
-
- if (!valid_geometry)
- {
- LL_WARNS("LLVOLUME") << "Sculpt failed geometry validation - either invalid image or malicious. "
- << "Replacing with placeholder. Image: " << mParams.getSculptID() << LL_ENDL;
-
- // Clear the malicious faces
- mVolumeFaces.clear();
-
- // Regenerate mesh with empty placeholder
- sculptGenerateEmptyPlaceholder();
+ // Delete any existing faces so that they get regenerated
+ mVolumeFaces.clear();
- // Recreate faces with placeholder geometry
- createVolumeFaces();
- }
- break;
- }
- } //switch (cached_state)
+ createVolumeFaces();
}
diff --git a/indra/llmath/llvolume.h b/indra/llmath/llvolume.h
index 86a7bf0e6a..3496928f7b 100644
--- a/indra/llmath/llvolume.h
+++ b/indra/llmath/llvolume.h
@@ -60,8 +60,6 @@ class LLVolumeOctree;
#include "llalignedarray.h"
#include "llrigginginfo.h"
-#include <array>
-
//============================================================================
constexpr S32 MIN_DETAIL_FACES = 6;
@@ -1042,7 +1040,7 @@ public:
const LLVector4a& getMeshPt(const U32 i) const { return mMesh[i]; }
- void setDirty();
+ void setDirty() { mPathp->setDirty(); mProfilep->setDirty(); }
void regen();
void genTangents(S32 face);
@@ -1133,11 +1131,6 @@ public:
bool mIsMeshAssetLoaded;
bool mIsMeshAssetUnavaliable;
- // Cache for sculpt geometry validation results
- static constexpr S32 SCULPT_CACHE_SIZE = 6; // 0 to 5 inclusive
- enum class LLSculptValidationState : S8 { Unvalidated, Valid, Invalid };
- std::array<LLSculptValidationState, SCULPT_CACHE_SIZE> mSculptValidationCache;
-
const LLVolumeParams mParams;
LLPath *mPathp;
LLProfile *mProfilep;
diff --git a/indra/llui/llfolderviewitem.cpp b/indra/llui/llfolderviewitem.cpp
index dea95de955..f63477c325 100644
--- a/indra/llui/llfolderviewitem.cpp
+++ b/indra/llui/llfolderviewitem.cpp
@@ -234,7 +234,11 @@ LLFolderViewItem::LLFolderViewItem(const LLFolderViewItem::Params& p)
// Destroys the object
LLFolderViewItem::~LLFolderViewItem()
{
- mViewModelItem = NULL;
+ if (mViewModelItem)
+ {
+ mViewModelItem->setFolderViewItem(nullptr);
+ mViewModelItem = nullptr;
+ }
gFocusMgr.removeKeyboardFocusWithoutCallback(this);
}
@@ -356,7 +360,12 @@ bool LLFolderViewItem::isPotentiallyVisible(S32 filter_generation)
void LLFolderViewItem::refresh()
{
- LLFolderViewModelItem& vmi = *getViewModelItem();
+ LLFolderViewModelItem* vmi_ptr = getViewModelItem();
+ if (!vmi_ptr)
+ {
+ return;
+ }
+ LLFolderViewModelItem& vmi = *vmi_ptr;
mLabel = utf8str_to_wstring(vmi.getDisplayName());
mLabelFontBuffer.reset();
@@ -387,6 +396,10 @@ void LLFolderViewItem::refresh()
void LLFolderViewItem::refreshSuffix()
{
LLFolderViewModelItem const* vmi = getViewModelItem();
+ if (!vmi)
+ {
+ return;
+ }
// icons are slightly expensive to get, can be optimized
// see LLInventoryIcon::getIcon()
diff --git a/indra/llwebrtc/llwebrtc.cpp b/indra/llwebrtc/llwebrtc.cpp
index 3b3e7ea3ba..87ed21291f 100644
--- a/indra/llwebrtc/llwebrtc.cpp
+++ b/indra/llwebrtc/llwebrtc.cpp
@@ -478,7 +478,7 @@ bool LLWebRTCImpl::terminate()
{
for (auto& connection : connections)
{
- connection->closeOnSignalingThread();
+ connection->closeOnSignalingThread(true);
}
// Destroy the connections here, on the signaling thread, while
// it's still running.
@@ -1146,13 +1146,13 @@ void LLWebRTCPeerConnectionImpl::terminate()
mWebRTCImpl->PostSignalingTask(
[self]()
{
- self->closeOnSignalingThread();
+ self->closeOnSignalingThread(false);
self->mPendingJobs--;
});
}
// Signaling thread only.
-void LLWebRTCPeerConnectionImpl::closeOnSignalingThread()
+void LLWebRTCPeerConnectionImpl::closeOnSignalingThread(bool webrtc_terminate)
{
// Stop issuing stats requests; one may already be in flight, and
// Close() below will flush it.
@@ -1203,13 +1203,16 @@ void LLWebRTCPeerConnectionImpl::closeOnSignalingThread()
observer->OnPeerConnectionClosed();
}
- // Nothing may call back into the viewer past this point. Connections
- // closed while the viewer is still running unset themselves as observers
- // when they're destroyed, but any that are left for llwebrtc::terminate()
- // to close deliberately don't -- they're torn down as soon as it returns,
- // so a late callback would be reaching into freed memory.
- mSignalingObserverList.clear();
- mDataObserverList.clear();
+ if (webrtc_terminate)
+ {
+ // Nothing may call back into the viewer past this point. Connections
+ // closed while the viewer is still running unset themselves as observers
+ // when they're destroyed, but any that are left for llwebrtc::terminate()
+ // to close deliberately don't -- they're torn down as soon as it returns,
+ // so a late callback would be reaching into freed memory.
+ mSignalingObserverList.clear();
+ mDataObserverList.clear();
+ }
}
void LLWebRTCPeerConnectionImpl::setSignalingObserver(LLWebRTCSignalingObserver *observer) { mSignalingObserverList.emplace_back(observer); }
diff --git a/indra/llwebrtc/llwebrtc_impl.h b/indra/llwebrtc/llwebrtc_impl.h
index ae3a6a4903..c980b28d6e 100644
--- a/indra/llwebrtc/llwebrtc_impl.h
+++ b/indra/llwebrtc/llwebrtc_impl.h
@@ -600,10 +600,13 @@ class LLWebRTCPeerConnectionImpl : public LLWebRTCPeerConnectionInterface,
void init(LLWebRTCImpl * webrtc_impl);
// Posts closeOnSignalingThread() and returns immediately.
void terminate();
+
// The actual close. Signaling thread only. Callable directly (via a
// BlockingCall) when the caller needs the connection to be fully closed
// before it continues -- see LLWebRTCImpl::terminate().
- void closeOnSignalingThread();
+ // webrtc_terminate indicates we're shutting down the webrtc library,
+ // if not, we'll be reusing the peer connection for a reconnection
+ void closeOnSignalingThread(bool webrtc_terminate);
virtual void AddRef() const override = 0;
virtual webrtc::RefCountReleaseStatus Release() const override = 0;
diff --git a/indra/newview/CMakeLists.txt b/indra/newview/CMakeLists.txt
index c7a80a7a4b..b91cc546a6 100644
--- a/indra/newview/CMakeLists.txt
+++ b/indra/newview/CMakeLists.txt
@@ -10,6 +10,9 @@ include(Linking)
include(Boost)
include(bugsplat)
include(BuildPackagesInfo)
+# Luau isn't an autobuild package, so stage its license separately.
+file(MAKE_DIRECTORY "${AUTOBUILD_INSTALL_DIR}/LICENSES")
+configure_file("${CMAKE_CURRENT_SOURCE_DIR}/luau.txt" "${AUTOBUILD_INSTALL_DIR}/LICENSES/luau.txt" COPYONLY)
include(BuildVersion)
include(CMakeCopyIfDifferent)
include(CubemapToEquirectangularJS)
@@ -192,7 +195,6 @@ set(viewer_SOURCE_FILES
llfloaterabout.cpp
llfloateravatarwelcomepack.cpp
llfloaterbvhpreview.cpp
- llfloateraddpaymentmethod.cpp
llfloaterauction.cpp
llfloaterautoreplacesettings.cpp
llfloateravatarpicker.cpp
@@ -881,7 +883,6 @@ set(viewer_HEADER_FILES
llfloaterabout.h
llfloateravatarwelcomepack.h
llfloaterbvhpreview.h
- llfloateraddpaymentmethod.h
llfloaterauction.h
llfloaterautoreplacesettings.h
llfloateravatarpicker.h
diff --git a/indra/newview/app_settings/settings.xml b/indra/newview/app_settings/settings.xml
index aa8d051439..3434ab1516 100644
--- a/indra/newview/app_settings/settings.xml
+++ b/indra/newview/app_settings/settings.xml
@@ -1194,6 +1194,39 @@
<key>Value</key>
<string>https://accounts.secondlife.com/packs/floater?lang=[LANGUAGE]&amp;shortfall=[SHORTFALL]</string>
</map>
+ <key>BuyCurrencyAddPaymentURL</key>
+ <map>
+ <key>Comment</key>
+ <string>URL the viewer probes at login; a 200 means the L$ packs floater handles residents without a payment method</string>
+ <key>Persist</key>
+ <integer>1</integer>
+ <key>Type</key>
+ <string>String</string>
+ <key>Value</key>
+ <string>https://accounts.secondlife.com/packs/floater/add-payment-enabled</string>
+ </map>
+ <key>PaymentMethodURL</key>
+ <map>
+ <key>Comment</key>
+ <string>URL for adding a payment method</string>
+ <key>Persist</key>
+ <integer>1</integer>
+ <key>Type</key>
+ <string>String</string>
+ <key>Value</key>
+ <string>https://secondlife.com/my/lindex/buy.php?associate_for_viewer=1</string>
+ </map>
+ <key>PaymentMethodStagingURL</key>
+ <map>
+ <key>Comment</key>
+ <string>URL for adding a payment method on the Damballah</string>
+ <key>Persist</key>
+ <integer>1</integer>
+ <key>Type</key>
+ <string>String</string>
+ <key>Value</key>
+ <string>https://secondlife-staging.com/my/lindex/buy.php?associate_for_viewer=1</string>
+ </map>
<key>EnableButtonFlashing</key>
<map>
<key>Comment</key>
@@ -13414,7 +13447,7 @@
<key>Type</key>
<string>Boolean</string>
<key>Value</key>
- <integer>1</integer>
+ <integer>0</integer>
</map>
<key>PreferredBrowserBehavior</key>
<map>
@@ -14600,17 +14633,6 @@
<key>Value</key>
<integer>180</integer>
</map>
- <key>OSHibernationMode</key>
- <map>
- <key>Comment</key>
- <string>Whether to prevent OS from hibernating. 0 - can hibernate; 1 - can't hibernate, can turn screen off; 2 - can't hibernate, can't turn screen off </string>
- <key>Persist</key>
- <integer>1</integer>
- <key>Type</key>
- <string>S32</string>
- <key>Value</key>
- <integer>0</integer>
- </map>
<key>HeightUnits</key>
<map>
<key>Comment</key>
diff --git a/indra/newview/llagent.cpp b/indra/newview/llagent.cpp
index 2f0ad15d18..178a14f470 100644
--- a/indra/newview/llagent.cpp
+++ b/indra/newview/llagent.cpp
@@ -579,6 +579,9 @@ void LLAgent::setFirstLogin(bool b)
{
setFeatureVersion(UI_FEATURE_VERSION, UI_FEATURE_FLAGS);
}
+
+ // Shared environment on teleport should only be enabled by default for new users;
+ gSavedSettings.setBOOL("SwitchToSharedEnvAfterTeleport", true);
}
}
@@ -1552,8 +1555,6 @@ void LLAgent::setAFK()
setControlFlags(AGENT_CONTROL_AWAY | AGENT_CONTROL_STOP);
gAwayTimer.start();
}
-
- LLAppViewer::instance()->setPermitOSHibernation(true);
}
//-----------------------------------------------------------------------------
@@ -1572,13 +1573,6 @@ void LLAgent::clearAFK()
sendAnimationRequest(ANIM_AGENT_AWAY, ANIM_REQUEST_STOP);
clearControlFlags(AGENT_CONTROL_AWAY);
}
-
- if (isAgentAvatarValid())
- {
- // Only set this if agent is inworld, login screen
- // shouldn't prevent hibernation.
- LLAppViewer::instance()->setPermitOSHibernation(false);
- }
}
//-----------------------------------------------------------------------------
diff --git a/indra/newview/llappviewer.cpp b/indra/newview/llappviewer.cpp
index 2a7e2b61f9..441f43163d 100644
--- a/indra/newview/llappviewer.cpp
+++ b/indra/newview/llappviewer.cpp
@@ -643,12 +643,6 @@ bool LLAppViewer::sendURLToOtherInstance(const std::string& url)
return false;
}
-//virtual
-void LLAppViewer::setOSHibernationMode(eHibernationMode mode)
-{
- // See OS specific files
-}
-
//----------------------------------------------------------------------------
// LLAppViewer definition
@@ -5996,29 +5990,6 @@ void LLAppViewer::outOfMemorySoftQuit()
}
}
-void LLAppViewer::setPermitOSHibernation(bool permit)
-{
- if (permit)
- {
- if (mCurrentHibernationMode != LL_HIBERNATE_MODE_DEFAULT)
- {
- // Will call OS specific code to let OS hibernate when idle
- setOSHibernationMode(LL_HIBERNATE_MODE_DEFAULT);
- mCurrentHibernationMode = LL_HIBERNATE_MODE_DEFAULT;
- }
- }
- else
- {
- static LLCachedControl<S32> os_hibernation_mode(gSavedSettings, "OSHibernationMode", 0);
- eHibernationMode mode = static_cast<eHibernationMode>(os_hibernation_mode());
- if (mode != LL_HIBERNATE_MODE_DEFAULT && mCurrentHibernationMode != mode)
- {
- setOSHibernationMode(mode);
- mCurrentHibernationMode = mode;
- }
- }
-}
-
void LLAppViewer::idleNameCache()
{
// Neither old nor new name cache can function before agent has a region
@@ -6229,9 +6200,6 @@ void LLAppViewer::disconnectViewer()
// Pass the connection state to LLUrlEntryParcel not to attempt
// parcel info requests while disconnected.
LLUrlEntryParcel::setDisconnected(gDisconnected);
-
- // Restore default OS hibernation mode
- setPermitOSHibernation(true);
}
void LLAppViewer::forceErrorLLError()
@@ -6525,15 +6493,6 @@ void LLAppViewer::handleLoginComplete()
// we logged in successfully, so save settings on logout
LL_INFOS() << "Login successful, per account settings will be saved on log out." << LL_ENDL;
mSavePerAccountSettings=true;
-
- // Don't allow hibernation while we're running
- setPermitOSHibernation(false);
- // Track 'hibernation' mode changes
- mOSHibernationModeChangeConnection = gSavedSettings.getControl("OSHibernationMode")->getSignal()->connect([](LLControlVariable* control, const LLSD& new_val, const LLSD& old_val)
- {
- // setPermitOSHibernation will sort itself out based on new mode.
- LLAppViewer::instance()->setPermitOSHibernation(false);
- });
}
//virtual
diff --git a/indra/newview/llappviewer.h b/indra/newview/llappviewer.h
index 837b598ae5..54f94fe98c 100644
--- a/indra/newview/llappviewer.h
+++ b/indra/newview/llappviewer.h
@@ -270,8 +270,6 @@ public:
// Note: mQuitRequested can be aborted by user.
void outOfMemorySoftQuit();
- virtual void setPermitOSHibernation(bool permit);
-
#ifdef LL_DISCORD
static void initDiscordSocial();
static void updateDiscordActivity();
@@ -288,14 +286,6 @@ protected:
virtual bool initSLURLHandler();
virtual bool sendURLToOtherInstance(const std::string& url);
- typedef enum
- {
- LL_HIBERNATE_MODE_DEFAULT = 0, // Use the platform's default behavior.
- LL_HIBERNATE_MODE_PREVENT = 1,
- LL_HIBERNATE_MODE_PREVENT_SCREEN = 2,
- } eHibernationMode;
- virtual void setOSHibernationMode(eHibernationMode mode);
-
virtual bool initParseCommandLine(LLCommandLineParser& clp)
{ return true; } // Allow platforms to specify the command line args.
@@ -403,9 +393,6 @@ private:
LLAppCoreHttp mAppCoreHttp;
bool mIsFirstRun;
-
- eHibernationMode mCurrentHibernationMode = LL_HIBERNATE_MODE_DEFAULT;
- boost::signals2::scoped_connection mOSHibernationModeChangeConnection;
};
// Globals with external linkage. From viewer.h
diff --git a/indra/newview/llappviewerlinux.cpp b/indra/newview/llappviewerlinux.cpp
index 2fd0eebd44..bcc9127d0e 100644
--- a/indra/newview/llappviewerlinux.cpp
+++ b/indra/newview/llappviewerlinux.cpp
@@ -79,10 +79,6 @@ namespace
void (*gOldTerminateHandler)() = NULL;
}
-// Initialize static members
-guint32 LLAppViewerLinux::sPowerInhibitCookie = 0;
-bool LLAppViewerLinux::sPowerInhibitActive = false;
-
void check_vm_bloat()
{
#if LL_LINUX
@@ -174,7 +170,6 @@ finally:
}
#endif // LL_LINUX
}
-
static void exceptionTerminateHandler()
{
// reinstall default terminate() handler in case we re-terminate.
@@ -286,11 +281,6 @@ LLAppViewerLinux::LLAppViewerLinux()
LLAppViewerLinux::~LLAppViewerLinux()
{
- // Clean up any power management inhibition on exit
- if (sPowerInhibitActive)
- {
- uninhibitPowerManagement();
- }
}
bool LLAppViewerLinux::init()
@@ -466,178 +456,6 @@ bool LLAppViewerLinux::sendURLToOtherInstance(const std::string& url)
}
#endif // LL_GLIB
-
-#if LL_GLIB
-
-namespace
-{
- // Session-bus sleep inhibitors, tried in order; both hand back a uint cookie.
- struct PowerInhibitor
- {
- const char* service;
- const char* path;
- const char* iface;
- const char* uninhibit;
- bool gnome_args;
- };
-
- const PowerInhibitor POWER_INHIBITORS[] =
- {
- { "org.freedesktop.PowerManagement", "/org/freedesktop/PowerManagement/Inhibit",
- "org.freedesktop.PowerManagement.Inhibit", "UnInhibit", false },
- { "org.gnome.SessionManager", "/org/gnome/SessionManager",
- "org.gnome.SessionManager", "Uninhibit", true }
- };
-
- const PowerInhibitor* sActiveInhibitor = nullptr;
-
- // Returns the reply, or null on failure. Takes ownership of args.
- GVariant* call_power_manager(const PowerInhibitor& inhibitor, const char* method, GVariant* args,
- const GVariantType* reply_type)
- {
- g_variant_ref_sink(args);
-
- GDBusConnection* bus = g_bus_get_sync(G_BUS_TYPE_SESSION, nullptr, nullptr);
- if (!bus)
- {
- LL_WARNS("OS") << "Getting dbus failed." << LL_ENDL;
- g_variant_unref(args);
- return nullptr;
- }
-
- GError* error = nullptr;
- GVariant* result = g_dbus_connection_call_sync(bus, inhibitor.service, inhibitor.path,
- inhibitor.iface, method, args, reply_type,
- G_DBUS_CALL_FLAGS_NONE, -1, nullptr, &error);
- if (!result)
- {
- LL_DEBUGS("OS") << inhibitor.service << "." << method << " failed: "
- << (error ? error->message : "unknown error") << LL_ENDL;
- }
- if (error)
- {
- g_error_free(error);
- }
-
- g_variant_unref(args);
- g_object_unref(bus);
- return result;
- }
-}
-
-#endif // LL_GLIB
-
-void LLAppViewerLinux::setOSHibernationMode(eHibernationMode mode)
-{
- if (mode == LL_HIBERNATE_MODE_DEFAULT)
- {
- // Allow OS to sleep/hibernate - remove any inhibition
- if (sPowerInhibitActive)
- {
- uninhibitPowerManagement();
- LL_INFOS("OS") << "Permitted OS hibernation/sleep" << LL_ENDL;
- }
- }
- else if (mode == LL_HIBERNATE_MODE_PREVENT)
- {
- // Prevent system sleep, but allow display to turn off
- // Release any existing inhibition first to allow mode switching
- if (sPowerInhibitActive)
- {
- uninhibitPowerManagement();
- }
-
- if (inhibitPowerManagement(false))
- {
- LL_INFOS("OS") << "Prevented OS hibernation/sleep, display sleep allowed" << LL_ENDL;
- }
- else
- {
- LL_WARNS("OS") << "Failed to prevent OS hibernation/sleep" << LL_ENDL;
- }
- }
- else if (mode == LL_HIBERNATE_MODE_PREVENT_SCREEN)
- {
- // Prevent both system and display sleep
- // Release any existing inhibition first to allow mode switching
- if (sPowerInhibitActive)
- {
- uninhibitPowerManagement();
- }
-
- if (inhibitPowerManagement(true))
- {
- LL_INFOS("OS") << "Prevented OS hibernation/sleep and display sleep" << LL_ENDL;
- }
- else
- {
- LL_WARNS("OS") << "Failed to prevent OS hibernation/sleep and display sleep" << LL_ENDL;
- }
- }
-}
-
-// TODO: This is AI Generated!!!, needs review and testing.
-bool LLAppViewerLinux::inhibitPowerManagement(bool inhibit_display)
-{
-#if LL_GLIB
- const char* reason = inhibit_display ?
- "Viewer active - preventing system and display sleep" :
- "Viewer active - preventing system sleep";
-
- for (const PowerInhibitor& inhibitor : POWER_INHIBITORS)
- {
- // Inhibit(app: s, reason: s) -> u, or GNOME's Inhibit(app: s, xid: u, reason: s, flags: u) -> u
- // GNOME flags: 4 = suspend, 8 = idle (display), 12 = both
- GVariant* args = inhibitor.gnome_args ?
- g_variant_new("(susu)", "SecondLifeViewer", (guint32)0, reason,
- (guint32)(inhibit_display ? 12 : 4)) :
- g_variant_new("(ss)", "Second Life Viewer", reason);
-
- GVariant* result = call_power_manager(inhibitor, "Inhibit", args, G_VARIANT_TYPE("(u)"));
- if (!result)
- {
- continue;
- }
-
- guint32 cookie = 0;
- g_variant_get(result, "(u)", &cookie);
- g_variant_unref(result);
-
- sPowerInhibitCookie = cookie;
- sPowerInhibitActive = true;
- sActiveInhibitor = &inhibitor;
- LL_INFOS("OS") << "Successfully inhibited power management using "
- << inhibitor.service << LL_ENDL;
- return true;
- }
-#endif // LL_GLIB
-
- return false;
-}
-
-void LLAppViewerLinux::uninhibitPowerManagement()
-{
-#if LL_GLIB
- if (sPowerInhibitActive && sActiveInhibitor)
- {
- GVariant* result = call_power_manager(*sActiveInhibitor, sActiveInhibitor->uninhibit,
- g_variant_new("(u)", (guint32)sPowerInhibitCookie),
- nullptr);
- if (result)
- {
- g_variant_unref(result);
- LL_INFOS("OS") << "Successfully uninhibited power management using "
- << sActiveInhibitor->service << LL_ENDL;
- }
-
- sActiveInhibitor = nullptr;
- }
-#endif // LL_GLIB
-
- sPowerInhibitCookie = 0;
- sPowerInhibitActive = false;
-}
-
void LLAppViewerLinux::initCrashReporting(bool reportFreeze)
{
}
diff --git a/indra/newview/llappviewerlinux.h b/indra/newview/llappviewerlinux.h
index faac3c0a05..6f4e087726 100644
--- a/indra/newview/llappviewerlinux.h
+++ b/indra/newview/llappviewerlinux.h
@@ -60,16 +60,6 @@ protected:
bool initSLURLHandler() override;
bool sendURLToOtherInstance(const std::string& url) override;
- void setOSHibernationMode(eHibernationMode mode) override;
-
-private:
- // Power management state tracking
- static guint32 sPowerInhibitCookie;
- static bool sPowerInhibitActive;
-
- // Helper methods for power management
- bool inhibitPowerManagement(bool inhibit_display);
- void uninhibitPowerManagement();
};
#endif // LL_LLAPPVIEWERLINUX_H
diff --git a/indra/newview/llappviewermacosx-objc.h b/indra/newview/llappviewermacosx-objc.h
index 13b8056193..bfbb48dadb 100644
--- a/indra/newview/llappviewermacosx-objc.h
+++ b/indra/newview/llappviewermacosx-objc.h
@@ -32,6 +32,5 @@
void force_ns_sxeption();
void register_url_schemes();
-void set_os_hibernation_mode(int mode);
#endif // LL_LLAPPVIEWERMACOSX_OBJC_H
diff --git a/indra/newview/llappviewermacosx-objc.mm b/indra/newview/llappviewermacosx-objc.mm
index ab4ae428b9..75d6b56e3e 100644
--- a/indra/newview/llappviewermacosx-objc.mm
+++ b/indra/newview/llappviewermacosx-objc.mm
@@ -29,7 +29,6 @@
#endif
#import <Cocoa/Cocoa.h>
-#import <IOKit/pwr_mgt/IOPMLib.h>
#include <iostream>
#include "llappviewermacosx-objc.h"
@@ -64,68 +63,3 @@ void register_url_schemes()
}
}
}
-
-// Add these as static variables at file scope
-static IOPMAssertionID gPowerAssertionID = kIOPMNullAssertionID;
-
-void set_os_hibernation_mode(int mode)
-{
- // Release existing assertion
- if (gPowerAssertionID != kIOPMNullAssertionID)
- {
- IOReturn result = IOPMAssertionRelease(gPowerAssertionID);
- if (result == kIOReturnSuccess)
- {
- gPowerAssertionID = kIOPMNullAssertionID;
- NSLog(@"Permitted OS hibernation/sleep");
- }
- else
- {
- NSLog(@"Failed to release power assertion: %d", result);
- }
- }
-
- if (mode == 1)
- {
- // Prevent OS from sleeping/hibernating
- CFStringRef assertionName = CFSTR("Second Life Viewer");
- // kIOPMAssertionTypeNoIdleSleep prevents idle sleep
- IOReturn result = IOPMAssertionCreateWithName(
- kIOPMAssertionTypeNoIdleSleep,
- kIOPMAssertionLevelOn,
- assertionName,
- &gPowerAssertionID
- );
-
- if (result == kIOReturnSuccess)
- {
- NSLog(@"Prevented OS hibernation/sleep, allow display sleep");
- }
- else
- {
- NSLog(@"Failed to create power assertion: %d", result);
- }
- }
- else if (mode == 2)
- {
- // Prevent OS from sleeping/hibernating, prevent screen from going off
- CFStringRef assertionName = CFSTR("Second Life Viewer");
- // kIOPMAssertionTypeNoIdleSleep prevents idle sleep
- // kIOPMAssertionTypeNoDisplaySleep prevents display sleep
- IOReturn result = IOPMAssertionCreateWithName(
- kIOPMAssertionTypeNoDisplaySleep,
- kIOPMAssertionLevelOn,
- assertionName,
- &gPowerAssertionID
- );
-
- if (result == kIOReturnSuccess)
- {
- NSLog(@"Prevented OS hibernation/sleep or screen from turning off");
- }
- else
- {
- NSLog(@"Failed to create power assertion: %d", result);
- }
- }
-}
diff --git a/indra/newview/llappviewermacosx.cpp b/indra/newview/llappviewermacosx.cpp
index c188bb459f..f914a76fc6 100644
--- a/indra/newview/llappviewermacosx.cpp
+++ b/indra/newview/llappviewermacosx.cpp
@@ -126,7 +126,6 @@ bool pumpMainLoop()
void cleanupViewer()
{
- set_os_hibernation_mode(0); // restore default OS hibernation behavior
if(!LLApp::isError())
{
if (gViewerAppPtr)
@@ -429,12 +428,6 @@ bool LLAppViewerMacOSX::initSLURLHandler()
return true;
}
-void LLAppViewerMacOSX::setOSHibernationMode(eHibernationMode mode)
-{
- // pass to objective-c++
- set_os_hibernation_mode((int)mode);
-}
-
std::string LLAppViewerMacOSX::generateSerialNumber()
{
char serial_md5[MD5HEX_STR_SIZE]; // Flawfinder: ignore
diff --git a/indra/newview/llappviewermacosx.h b/indra/newview/llappviewermacosx.h
index 1153bd4191..35fc99dbd9 100644
--- a/indra/newview/llappviewermacosx.h
+++ b/indra/newview/llappviewermacosx.h
@@ -47,7 +47,6 @@ public:
protected:
virtual bool restoreErrorTrap();
virtual bool initSLURLHandler();
- virtual void setOSHibernationMode(eHibernationMode mode);
std::string generateSerialNumber();
virtual bool initParseCommandLine(LLCommandLineParser& clp);
diff --git a/indra/newview/llappviewerwin32.cpp b/indra/newview/llappviewerwin32.cpp
index 2080d0b7bf..f8a728a163 100644
--- a/indra/newview/llappviewerwin32.cpp
+++ b/indra/newview/llappviewerwin32.cpp
@@ -1054,56 +1054,6 @@ bool LLAppViewerWin32::sendURLToOtherInstance(const std::string& url)
return false;
}
-void LLAppViewerWin32::setOSHibernationMode(eHibernationMode mode)
-{
- // ES_CONTINUOUS tells Windows to reset the idle timer
- // and restore normal operation
- // ES_SYSTEM_REQUIRED prevents system sleep/hibernation
- // ES_DISPLAY_REQUIRED prevents display sleep
-
- if (mode == LL_HIBERNATE_MODE_DEFAULT)
- {
- // Allow OS to hibernate - clear the previous execution state flags
- // ES_CONTINUOUS without other flags allows the system to idle normally
- SetThreadExecutionState(ES_CONTINUOUS);
- LL_INFOS("OS") << "Permitted OS hibernation/sleep" << LL_ENDL;
- }
- else if (mode == LL_HIBERNATE_MODE_PREVENT)
- {
- // Prevent OS from hibernating while viewer is running
- // ES_CONTINUOUS | ES_SYSTEM_REQUIRED keeps the system awake
- EXECUTION_STATE result = SetThreadExecutionState(
- ES_CONTINUOUS | ES_SYSTEM_REQUIRED
- );
- if (result == NULL)
- {
- LL_WARNS("OS") << "Failed to prevent OS hibernation, error: " << GetLastError() << LL_ENDL;
- }
- else
- {
- LL_INFOS("OS") << "Prevented OS hibernation, but allowed display sleep" << LL_ENDL;
- }
- }
- else if (mode == LL_HIBERNATE_MODE_PREVENT_SCREEN)
- {
- // Prevent OS from hibernating or turning screen off while viewer is running
- // ES_CONTINUOUS | ES_SYSTEM_REQUIRED keeps the system awake
- // ES_DISPLAY_REQUIRED keeps the display on
- EXECUTION_STATE result = SetThreadExecutionState(
- ES_CONTINUOUS | ES_SYSTEM_REQUIRED | ES_DISPLAY_REQUIRED
- );
-
- if (result == NULL)
- {
- LL_WARNS("OS") << "Failed to prevent OS hibernation and display sleep, error: " << GetLastError() << LL_ENDL;
- }
- else
- {
- LL_INFOS("OS") << "Prevented OS hibernation/sleep" << LL_ENDL;
- }
- }
-}
-
bool LLAppViewerWin32::sendShutdownToOtherInstances(const std::wstring& install_dir)
{
// Velopack installs viewer like this:
diff --git a/indra/newview/llappviewerwin32.h b/indra/newview/llappviewerwin32.h
index 7c3c7a475a..971907694a 100644
--- a/indra/newview/llappviewerwin32.h
+++ b/indra/newview/llappviewerwin32.h
@@ -62,7 +62,6 @@ protected:
bool restoreErrorTrap() override;
bool sendURLToOtherInstance(const std::string& url) override;
- void setOSHibernationMode(eHibernationMode mode) override;
std::string generateSerialNumber();
diff --git a/indra/newview/llbuycurrencyhtml.cpp b/indra/newview/llbuycurrencyhtml.cpp
index 9ad068cf7f..f5bc3b5357 100644
--- a/indra/newview/llbuycurrencyhtml.cpp
+++ b/indra/newview/llbuycurrencyhtml.cpp
@@ -89,6 +89,8 @@ public:
LLBuyCurrencyHTMLHandler gBuyCurrencyHTMLHandler;
bool LLBuyCurrencyHTML::sWebFloaterEnabled = false;
+bool LLBuyCurrencyHTML::sAddPaymentEnabled = false;
+LLFetchAvatarPaymentInfo* LLBuyCurrencyHTML::sPaymentInfoRequest = NULL;
////////////////////////////////////////////////////////////////////////////////
// static
@@ -110,11 +112,37 @@ static void checkFeatureFlag_coro(std::string check_url)
}
// static
+static void checkAddPaymentFlag_coro(std::string check_url)
+{
+ LLCore::HttpRequest::policy_t httpPolicy(LLCore::HttpRequest::DEFAULT_POLICY_ID);
+ LLCoreHttpUtil::HttpCoroutineAdapter::ptr_t
+ httpAdapter = std::make_shared<LLCoreHttpUtil::HttpCoroutineAdapter>("CheckBuyCurrencyAddPaymentURL", httpPolicy);
+ LLCore::HttpRequest::ptr_t httpRequest = std::make_shared<LLCore::HttpRequest>();
+
+ // a redirect to some other page that answers 200 must not read as enabled
+ LLCore::HttpOptions::ptr_t httpOptions = std::make_shared<LLCore::HttpOptions>();
+ httpOptions->setRetries(0);
+ httpOptions->setFollowRedirects(false);
+
+ LLSD result = httpAdapter->getRawAndSuspend(httpRequest, check_url, httpOptions);
+ LLSD httpResults = result[LLCoreHttpUtil::HttpCoroutineAdapter::HTTP_RESULTS];
+ LLCore::HttpStatus status = LLCoreHttpUtil::HttpCoroutineAdapter::getStatusFromLLSD(httpResults);
+
+ LLBuyCurrencyHTML::sAddPaymentEnabled = status.isHttpStatus() && status.getType() == 200;
+ LL_INFOS("LLBuyCurrency") << "Add payment probe returned " << status.toString()
+ << ", in-floater add payment " << (LLBuyCurrencyHTML::sAddPaymentEnabled ? "enabled" : "disabled") << LL_ENDL;
+}
+
+// static
void LLBuyCurrencyHTML::checkFeatureFlag()
{
std::string check_url = LLFloaterBuyCurrencyHTML::buildURL();
LLCoros::instance().launch("checkFeatureFlag_coro",
[check_url]() { checkFeatureFlag_coro(check_url); });
+
+ std::string add_payment_url = gSavedSettings.getString("BuyCurrencyAddPaymentURL");
+ LLCoros::instance().launch("checkAddPaymentFlag_coro",
+ [add_payment_url]() { checkAddPaymentFlag_coro(add_payment_url); });
}
////////////////////////////////////////////////////////////////////////////////
@@ -124,15 +152,7 @@ void LLBuyCurrencyHTML::checkFeatureFlag()
// the case where the amount is not requested.
void LLBuyCurrencyHTML::openCurrencyFloater()
{
- if (gSavedSettings.getBOOL("BuyCurrencyHTML") && sWebFloaterEnabled)
- {
- LLBuyCurrencyHTML::showDialog();
- }
- else
- {
- // legacy version
- LLFloaterBuyCurrency::buyCurrency();
- }
+ routeCurrencyRequest(false, LLStringUtil::null, 0);
}
////////////////////////////////////////////////////////////////////////////////
@@ -142,19 +162,68 @@ void LLBuyCurrencyHTML::openCurrencyFloater()
// the case where the amount and a string to display are requested.
void LLBuyCurrencyHTML::openCurrencyFloater( const std::string& message, S32 sum )
{
- if (gSavedSettings.getBOOL("BuyCurrencyHTML") && sWebFloaterEnabled)
+ routeCurrencyRequest(true, message, sum);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// static
+// Without the in-floater add payment flow, the web floater is opened only for
+// residents with payment info on file; the rest go to the LindeX payment page.
+void LLBuyCurrencyHTML::routeCurrencyRequest( bool has_target, const std::string& message, S32 sum )
+{
+ if (!gSavedSettings.getBOOL("BuyCurrencyHTML") || !sWebFloaterEnabled)
{
- LLBuyCurrencyHTML::showDialog(sum - gStatusBar->getBalance());
- LLFloaterBuyCurrencyHTML* floater = dynamic_cast<LLFloaterBuyCurrencyHTML*>(LLFloaterReg::getInstance("buy_currency_html"));
- if (floater)
+ // legacy version, which runs its own payment info check
+ if (has_target)
{
- floater->setFallbackContext(message, sum);
+ LLFloaterBuyCurrency::buyCurrency(message, sum);
}
+ else
+ {
+ LLFloaterBuyCurrency::buyCurrency();
+ }
+ }
+ else if (sAddPaymentEnabled)
+ {
+ openWebFloater(has_target, message, sum);
}
else
{
- // legacy version
- LLFloaterBuyCurrency::buyCurrency( message, sum );
+ delete sPaymentInfoRequest;
+ sPaymentInfoRequest = new LLFetchAvatarPaymentInfo(
+ [has_target, message, sum](bool has_piof)
+ {
+ delete sPaymentInfoRequest;
+ sPaymentInfoRequest = NULL;
+
+ if (has_piof)
+ {
+ openWebFloater(has_target, message, sum);
+ }
+ else
+ {
+ LL_INFOS("LLBuyCurrency") << "No payment info on file, opening payment method page" << LL_ENDL;
+ LLFloaterBuyCurrency::openPaymentMethodPage();
+ }
+ });
+ }
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// static
+void LLBuyCurrencyHTML::openWebFloater( bool has_target, const std::string& message, S32 sum )
+{
+ if (!has_target)
+ {
+ LLBuyCurrencyHTML::showDialog();
+ return;
+ }
+
+ LLBuyCurrencyHTML::showDialog(sum - gStatusBar->getBalance());
+ LLFloaterBuyCurrencyHTML* floater = dynamic_cast<LLFloaterBuyCurrencyHTML*>(LLFloaterReg::getInstance("buy_currency_html"));
+ if (floater)
+ {
+ floater->setFallbackContext(message, sum);
}
}
diff --git a/indra/newview/llbuycurrencyhtml.h b/indra/newview/llbuycurrencyhtml.h
index 2f78199dcb..c8a98c42a9 100644
--- a/indra/newview/llbuycurrencyhtml.h
+++ b/indra/newview/llbuycurrencyhtml.h
@@ -29,6 +29,8 @@
#include "llsingleton.h"
+class LLFetchAvatarPaymentInfo;
+
class LLBuyCurrencyHTML
{
public:
@@ -45,10 +47,25 @@ class LLBuyCurrencyHTML
// close (and destroy) the currency floater
static void closeDialog();
- // probe BuyCurrencyPacksURL once at login; clears sWebFloaterEnabled if 501
+ // probe BuyCurrencyPacksURL once at login; clears sWebFloaterEnabled if 501.
+ // also launches the BuyCurrencyAddPaymentURL probe that sets sAddPaymentEnabled
static void checkFeatureFlag();
static bool sWebFloaterEnabled;
+
+ // true only when BuyCurrencyAddPaymentURL answered 200, meaning the web floater
+ // can add a payment method; otherwise residents without one are sent to the LindeX
+ static bool sAddPaymentEnabled;
+
+ private:
+ // route to the legacy floater, the web floater, or the LindeX payment page
+ static void routeCurrencyRequest( bool has_target, const std::string& message, S32 sum );
+
+ // open the web floater, with the shortfall and fallback context when there is a target
+ static void openWebFloater( bool has_target, const std::string& message, S32 sum );
+
+ // pending payment info check made before opening the web floater
+ static LLFetchAvatarPaymentInfo* sPaymentInfoRequest;
};
#endif // LL_LLBUYCURRENCYHTML_H
diff --git a/indra/newview/llfloateraddpaymentmethod.cpp b/indra/newview/llfloateraddpaymentmethod.cpp
deleted file mode 100644
index 64bd7b6aa1..0000000000
--- a/indra/newview/llfloateraddpaymentmethod.cpp
+++ /dev/null
@@ -1,84 +0,0 @@
-/**
- * @file llfloateraddpaymentmethod.cpp
- * @brief LLFloaterAddPaymentMethod class implementation
- *
- * $LicenseInfo:firstyear=2020&license=viewerlgpl$
- * Second Life Viewer Source Code
- * Copyright (C) 2020, 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 "llfloateraddpaymentmethod.h"
-#include "llnotificationsutil.h"
-#include "lluictrlfactory.h"
-#include "llviewernetwork.h"
-#include "llweb.h"
-
-
-LLFloaterAddPaymentMethod::LLFloaterAddPaymentMethod(const LLSD& key)
- : LLFloater(key)
-{
-}
-
-LLFloaterAddPaymentMethod::~LLFloaterAddPaymentMethod()
-{
-}
-
-bool LLFloaterAddPaymentMethod::postBuild()
-{
- setCanDrag(false);
- getChild<LLButton>("continue_btn")->setCommitCallback(boost::bind(&LLFloaterAddPaymentMethod::onContinueBtn, this));
- getChild<LLButton>("close_btn")->setCommitCallback(boost::bind(&LLFloaterAddPaymentMethod::onCloseBtn, this));
- return true;
-}
-
-void LLFloaterAddPaymentMethod::onOpen(const LLSD& key)
-{
- centerOnScreen();
-}
-
-void LLFloaterAddPaymentMethod::onContinueBtn()
-{
- closeFloater();
- LLNotificationsUtil::add("AddPaymentMethod", LLSD(), LLSD(),
- [this](const LLSD&notif, const LLSD&resp)
- {
- S32 opt = LLNotificationsUtil::getSelectedOption(notif, resp);
- if (opt == 0)
- {
- const std::string& grid_id = LLGridManager::getInstance()->getGridId();
- const std::string& grid_id_lower = utf8str_tolower(grid_id);
- LLWeb::loadURL(getString(grid_id_lower == "damballah" ? "continue_staging_url" : "continue_url"));
- }
- });
-}
-
-void LLFloaterAddPaymentMethod::onCloseBtn()
-{
- closeFloater();
-}
-
-void LLFloaterAddPaymentMethod::centerOnScreen()
-{
- LLVector2 window_size = LLUI::getInstance()->getWindowSize();
- centerWithin(LLRect(0, 0, ll_round(window_size.mV[VX]), ll_round(window_size.mV[VY])));
-}
-
diff --git a/indra/newview/llfloateraddpaymentmethod.h b/indra/newview/llfloateraddpaymentmethod.h
deleted file mode 100644
index 252caf8f5b..0000000000
--- a/indra/newview/llfloateraddpaymentmethod.h
+++ /dev/null
@@ -1,52 +0,0 @@
-/**
- * @file llfloateraddpaymentmethod.h
- * @brief LLFloaterAddPaymentMethod class definition
- *
- * $LicenseInfo:firstyear=2020&license=viewerlgpl$
- * Second Life Viewer Source Code
- * Copyright (C) 2020, 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$
- */
-
-#ifndef LL_FLOATER_ADDPAYMENTMETHOD_H
-#define LL_FLOATER_ADDPAYMENTMETHOD_H
-
-#include "llfloater.h"
-
-class LLFloaterAddPaymentMethod:
- public LLFloater
-{
- friend class LLFloaterReg;
-public:
- bool postBuild() override;
- void onOpen(const LLSD& key) override;
-
-private:
- LLFloaterAddPaymentMethod(const LLSD& key);
-
- void centerOnScreen();
-
- void onCloseBtn();
- void onContinueBtn();
-
- /*virtual*/ ~LLFloaterAddPaymentMethod();
-
-};
-
-#endif
diff --git a/indra/newview/llfloaterbuycurrency.cpp b/indra/newview/llfloaterbuycurrency.cpp
index 506cff31f4..7533145625 100644
--- a/indra/newview/llfloaterbuycurrency.cpp
+++ b/indra/newview/llfloaterbuycurrency.cpp
@@ -40,6 +40,8 @@
#include "llviewchildren.h"
#include "llviewerwindow.h"
#include "lluictrlfactory.h"
+#include "llviewercontrol.h"
+#include "llviewernetwork.h"
#include "llweb.h"
#include "llwindow.h"
#include "llappviewer.h"
@@ -336,16 +338,39 @@ void LLFloaterBuyCurrency::handleBuyCurrency(bool has_piof, bool has_target, con
}
else
{
- LLFloaterReg::showInstance("add_payment_method");
+ openPaymentMethodPage();
}
}
+// static
+void LLFloaterBuyCurrency::openPaymentMethodPage()
+{
+ const std::string& grid_id = LLGridManager::getInstance()->getGridId();
+ const std::string& grid_id_lower = utf8str_tolower(grid_id);
+ const std::string url = gSavedSettings.getString(grid_id_lower == "damballah" ? "PaymentMethodStagingURL" : "PaymentMethodURL");
+ LLWeb::loadURL(url);
+}
+
LLFetchAvatarPaymentInfo::LLFetchAvatarPaymentInfo(bool has_target, const std::string& name, S32 price)
: mAvatarID(gAgent.getID()),
mHasTarget(has_target),
mPrice(price),
mName(name)
{
+ sendRequest();
+}
+
+LLFetchAvatarPaymentInfo::LLFetchAvatarPaymentInfo(const callback_t& cb)
+: mAvatarID(gAgent.getID()),
+ mHasTarget(false),
+ mPrice(0),
+ mCallback(cb)
+{
+ sendRequest();
+}
+
+void LLFetchAvatarPaymentInfo::sendRequest()
+{
LLAvatarPropertiesProcessor* processor = LLAvatarPropertiesProcessor::getInstance();
// register ourselves as an observer
processor->addObserver(mAvatarID, this);
@@ -364,6 +389,16 @@ void LLFetchAvatarPaymentInfo::processProperties(void* data, EAvatarProcessorTyp
if (data && type == APT_PROPERTIES)
{
LLAvatarData* avatar_data = static_cast<LLAvatarData*>(data);
- LLFloaterBuyCurrency::handleBuyCurrency(LLAvatarPropertiesProcessor::hasPaymentInfoOnFile(avatar_data), mHasTarget, mName, mPrice);
+ bool has_piof = LLAvatarPropertiesProcessor::hasPaymentInfoOnFile(avatar_data);
+ if (mCallback)
+ {
+ // call a local copy, since the callback may delete this observer
+ callback_t cb = mCallback;
+ cb(has_piof);
+ }
+ else
+ {
+ LLFloaterBuyCurrency::handleBuyCurrency(has_piof, mHasTarget, mName, mPrice);
+ }
}
}
diff --git a/indra/newview/llfloaterbuycurrency.h b/indra/newview/llfloaterbuycurrency.h
index 1b04c9c6d2..c6a80de926 100644
--- a/indra/newview/llfloaterbuycurrency.h
+++ b/indra/newview/llfloaterbuycurrency.h
@@ -30,21 +30,34 @@
#include "llavatarpropertiesprocessor.h"
#include "stdtypes.h"
#include "llagent.h"
+
+#include <functional>
+
class LLFloater;
class LLFetchAvatarPaymentInfo : public LLAvatarPropertiesObserver
{
public:
+ typedef std::function<void(bool has_piof)> callback_t;
+
+ // hands the result to LLFloaterBuyCurrency::handleBuyCurrency
LLFetchAvatarPaymentInfo(bool has_target, const std::string& name = std::string(), S32 price = 0);
+
+ // hands the result to cb, which may delete this observer
+ LLFetchAvatarPaymentInfo(const callback_t& cb);
+
~LLFetchAvatarPaymentInfo();
void processProperties(void* data, EAvatarProcessorType type);
private:
+ void sendRequest();
+
LLUUID mAvatarID;
bool mHasTarget;
std::string mName;
S32 mPrice;
+ callback_t mCallback;
};
@@ -62,6 +75,9 @@ public:
a space and the price will be appended
*/
+ // open the LindeX page where a resident adds a payment method
+ static void openPaymentMethodPage();
+
static LLFloater* buildFloater(const LLSD& key);
static LLFetchAvatarPaymentInfo* sPropertiesRequest;
diff --git a/indra/newview/llinventorypanel.cpp b/indra/newview/llinventorypanel.cpp
index 301a0a0cc9..92cf302a8d 100644
--- a/indra/newview/llinventorypanel.cpp
+++ b/indra/newview/llinventorypanel.cpp
@@ -602,8 +602,8 @@ void LLInventoryPanel::itemChanged(const LLUUID& item_id, U32 mask, const LLInve
if (model_item && view_item && viewmodel_item)
{
const LLUUID idp = viewmodel_item->getUUID();
- view_item->destroyView();
removeItemID(idp);
+ view_item->destroyView();
}
LLInventoryObject const* objectp = mInventory->getObject(item_id);
@@ -709,6 +709,7 @@ void LLInventoryPanel::itemChanged(const LLUUID& item_id, U32 mask, const LLInve
LLFolderViewModelItem* old_parent_vmi = old_parent->getViewModelItem();
LLFolderViewModelItemInventory* viewmodel_folder = static_cast<LLFolderViewModelItemInventory*>(old_parent_vmi);
LLFolderViewFolder* new_parent = getFolderByID(model_item->getParentUUID());
+ const bool was_favorite = view_item->isFavorite();
if (old_parent != new_parent // Item has been moved.
&& (new_parent != NULL || !isInRootContent(item_id, view_item)) // item is not or shouldn't be in root content
@@ -748,7 +749,7 @@ void LLInventoryPanel::itemChanged(const LLUUID& item_id, U32 mask, const LLInve
old_parent_vmi->dirtyDescendantsFilter();
}
- if (view_item->isFavorite())
+ if (was_favorite)
{
if (old_parent)
{
diff --git a/indra/newview/llpanelcontents.cpp b/indra/newview/llpanelcontents.cpp
index 43dd1bfe40..d9301d60f8 100644
--- a/indra/newview/llpanelcontents.cpp
+++ b/indra/newview/llpanelcontents.cpp
@@ -111,6 +111,7 @@ LLPanelContents::LLPanelContents()
LLPanelContents::~LLPanelContents()
{
+ mSimulatorFeaturesConnection.disconnect();
// Children all cleaned up by default view destructor.
}
@@ -119,6 +120,7 @@ void LLPanelContents::getState(LLViewerObject *objectp )
{
if( !objectp )
{
+ mSimulatorFeaturesConnection.disconnect();
mLastScriptObjectID.setNull();
getChildView("button new script")->setEnabled(false);
getChildView("button new notecard")->setEnabled(false);
@@ -154,11 +156,18 @@ void LLPanelContents::getState(LLViewerObject *objectp )
region->getSimulatorFeatures(simulatorFeatures);
lua_region = simulatorFeatures["LuaScriptsEnabled"].asBoolean();
}
+ mSimulatorFeaturesConnection.disconnect();
+ if (region && !region->simulatorFeaturesReceived())
+ {
+ mSimulatorFeaturesConnection = region->setSimulatorFeaturesReceivedCallback(
+ boost::bind(&LLPanelContents::onSimulatorFeaturesReceived, this, _1));
+ }
LLComboBox* new_script = getChild<LLComboBox>("button new script");
new_script->setEnabledByValue("lua", lua_region);
if (mLastScriptObjectID != objectp->getID() || mLastLuaRegion != lua_region)
{
- new_script->setValue(lua_region ? "lua" : "lsl");
+ mNewScriptIsLua = lua_region;
+ new_script->setValue(mNewScriptIsLua ? "lua" : "lsl");
}
mLastScriptObjectID = objectp->getID();
mLastLuaRegion = lua_region;
@@ -187,6 +196,11 @@ void LLPanelContents::getState(LLViewerObject *objectp )
}
}
+void LLPanelContents::onSimulatorFeaturesReceived(const LLUUID&)
+{
+ refresh();
+}
+
void LLPanelContents::onFilterEdit()
{
const std::string& filter_substring = mFilterEditor->getText();
@@ -277,31 +291,18 @@ void LLPanelContents::onNewScriptFlyoutCommit(LLUICtrl* ctrl)
LLViewerObject* object = LLSelectMgr::getInstance()->getSelection()->getFirstRootObject(children_ok);
if (!object) return;
- U8 script_language;
const std::string value = ctrl->getValue().asString();
if (value == "lsl")
{
- script_language = SST_LSL;
+ mNewScriptIsLua = false;
}
else if (value == "lua")
{
- script_language = SST_LUA;
- }
- else
- {
- script_language = SST_LSL;
- LLViewerRegion* region = object->getRegion();
- if (region && region->simulatorFeaturesReceived())
- {
- LLSD simulatorFeatures;
- region->getSimulatorFeatures(simulatorFeatures);
- if (simulatorFeatures["LuaScriptsEnabled"].asBoolean())
- {
- script_language = SST_LUA;
- }
- }
+ mNewScriptIsLua = true;
}
+ ctrl->setValue(mNewScriptIsLua ? "lua" : "lsl");
+ const U8 script_language = mNewScriptIsLua ? SST_LUA : SST_LSL;
std::string vm = (script_language == SST_LUA) ? "luau" : "mono";
LLSD params;
diff --git a/indra/newview/llpanelcontents.h b/indra/newview/llpanelcontents.h
index d576b72353..e2909d21fc 100644
--- a/indra/newview/llpanelcontents.h
+++ b/indra/newview/llpanelcontents.h
@@ -35,6 +35,7 @@
#include "llvoinventorylistener.h"
#include "llscripteditorws.h"
#include "v3math.h"
+#include <boost/signals2/connection.hpp>
class LLButton;
class LLPanelObjectInventory;
@@ -79,10 +80,13 @@ public:
protected:
void getState(LLViewerObject *object);
void onFilterEdit();
+ void onSimulatorFeaturesReceived(const LLUUID& region_id);
bool mDirtyFilter { false };
LLUUID mLastScriptObjectID;
bool mLastLuaRegion { false };
+ bool mNewScriptIsLua { false };
+ boost::signals2::connection mSimulatorFeaturesConnection;
public:
class LLFilterEditor* mFilterEditor;
diff --git a/indra/newview/llpanelprofilepicks.cpp b/indra/newview/llpanelprofilepicks.cpp
index 6870d719af..ca1dd1b786 100644
--- a/indra/newview/llpanelprofilepicks.cpp
+++ b/indra/newview/llpanelprofilepicks.cpp
@@ -41,6 +41,7 @@
#include "llpanelavatar.h"
#include "llpanelprofile.h"
#include "llparcel.h"
+#include "llregionhandle.h"
#include "llstartup.h"
#include "lltabcontainer.h"
#include "lltextbox.h"
@@ -915,7 +916,19 @@ void LLPanelProfilePick::processParcelInfo(const LLParcelData& parcel_data)
{
// Region might have moved since the pick was saved; refresh the stored global position
// using the parcel info so map/teleport use the current location.
- setPosGlobal(LLVector3d(parcel_data.global_x, parcel_data.global_y, parcel_data.global_z));
+ LLVector3d parcel_pos(parcel_data.global_x, parcel_data.global_y, parcel_data.global_z);
+ U64 old_region_handle = to_region_handle(getPosGlobal());
+ U64 new_region_handle = to_region_handle(parcel_pos);
+ if (old_region_handle != new_region_handle)
+ {
+ F32 old_origin_x, old_origin_y;
+ from_region_handle(old_region_handle, &old_origin_x, &old_origin_y);
+ LLVector3d local_offset = getPosGlobal() - LLVector3d(old_origin_x, old_origin_y, 0.0);
+
+ F32 new_origin_x, new_origin_y;
+ from_region_handle(new_region_handle, &new_origin_x, &new_origin_y);
+ setPosGlobal(LLVector3d(new_origin_x, new_origin_y, 0.0) + local_offset);
+ }
setPickLocation(createLocationText(LLStringUtil::null, parcel_data.name, parcel_data.sim_name, getPosGlobal()));
diff --git a/indra/newview/llpreviewscript.cpp b/indra/newview/llpreviewscript.cpp
index cee1180fa7..ffb748baae 100644
--- a/indra/newview/llpreviewscript.cpp
+++ b/indra/newview/llpreviewscript.cpp
@@ -2046,6 +2046,7 @@ void LLPreviewLSL::saveIfNeeded(bool sync /*= true*/)
mPendingUploads = 0;
mScriptEd->mErrorList->deleteAllItems();
+ mScriptEd->enableSave(false);
mScriptEd->mEditor->makePristine();
if (sync)
@@ -2082,6 +2083,7 @@ void LLPreviewLSL::saveIfNeeded(bool sync /*= true*/)
void LLPreviewLSL::onCompileTargetChanged()
{
+ mScriptEd->enableSave(true);
bool is_lua = (mScriptEd->mCompileTarget->getValue().asString() == "luau");
mScriptEd->mEditor->setLuauLanguage(is_lua);
mScriptEd->processKeywords(is_lua);
diff --git a/indra/newview/llpublishedobjectmgr.cpp b/indra/newview/llpublishedobjectmgr.cpp
index 85a9542dce..5cbb7ca76d 100644
--- a/indra/newview/llpublishedobjectmgr.cpp
+++ b/indra/newview/llpublishedobjectmgr.cpp
@@ -32,6 +32,7 @@
#include "llchat.h"
#include "llinventorydefines.h"
#include "llregex.h"
+#include "llsdutil.h"
#include "llselectmgr.h"
#include "llviewerinventory.h"
#include "llviewerobject.h"
@@ -66,7 +67,7 @@ namespace
return std::string(s);
}
- std::string get_prim_name(LLViewerObject* obj)
+ std::string get_prim_name(LLViewerObject* obj, S32 link_number = 1)
{
std::string name = nv_string(obj, "Name");
if (!name.empty())
@@ -85,25 +86,17 @@ namespace
return node->mName;
}
- // Never emit an empty prim/object name to downstream tooling.
- return obj->getID().asString();
+ // Real prim names need a full ObjectProperties reply, which requires selection.
+ return (link_number > 1) ? llformat("Link #%d", link_number) : std::string("Object");
}
void add_object_permissions(LLSD& object_data, LLViewerObject* object)
{
- LLPermissions* permissions =
- LLSelectMgr::getInstance()->findObjectPermissions(object);
- if (!permissions)
+ LLSD permissions = LLPublishedObjectMgr::getObjectPermissionsLLSD(object);
+ if (permissions.isDefined())
{
- return;
+ object_data["permissions"] = permissions;
}
-
- LLSD permission_entry;
- permission_entry["owner"] =
- static_cast<S32>(permissions->getMaskOwner());
- permission_entry["next_owner"] =
- static_cast<S32>(permissions->getMaskNextOwner());
- object_data["permissions"] = permission_entry;
}
}
@@ -742,6 +735,59 @@ LLSD LLPublishedObjectMgr::buildPrimInventoryLLSD(LLViewerObject* object) const
return items;
}
+LLSD LLPublishedObjectMgr::makePermissionsLLSD(U32 owner_mask, U32 next_owner_mask)
+{
+ LLSD permissions;
+ permissions["owner"] = static_cast<S32>(owner_mask);
+ permissions["next_owner"] = static_cast<S32>(next_owner_mask);
+ return permissions;
+}
+
+LLSD LLPublishedObjectMgr::getObjectPermissionsLLSD(LLViewerObject* object)
+{
+ if (!object)
+ {
+ return LLSD();
+ }
+
+ LLPermissions* permissions =
+ LLSelectMgr::getInstance()->findObjectPermissions(object);
+ if (permissions)
+ {
+ return makePermissionsLLSD(permissions->getMaskOwner(), permissions->getMaskNextOwner());
+ }
+
+ // Unselected prims carry no LLPermissions, but object flags still hold this agent's rights.
+ // next_owner is unavailable from flags and is omitted until ObjectProperties arrives.
+ U32 owner_mask = 0;
+ if (object->permModify()) { owner_mask |= PERM_MODIFY; }
+ if (object->permCopy()) { owner_mask |= PERM_COPY; }
+ if (object->permTransfer()) { owner_mask |= PERM_TRANSFER; }
+ if (object->permMove()) { owner_mask |= PERM_MOVE; }
+
+ LLSD permission_entry;
+ permission_entry["owner"] = static_cast<S32>(owner_mask);
+ return permission_entry;
+}
+
+bool LLPublishedObjectMgr::computeCanSaveBack(LLViewerObject* root, LLUUID& source_task_id)
+{
+ source_task_id.setNull();
+ if (!root || root->isAttachment())
+ {
+ return false;
+ }
+
+ LLSelectNode* node = LLSelectMgr::instance().getSelection()->findNode(root);
+ if (!node || !node->mValid || node->mFromTaskID.isNull())
+ {
+ return false;
+ }
+
+ source_task_id = node->mFromTaskID;
+ return true;
+}
+
LLSD LLPublishedObjectMgr::buildPublishedObjectLLSD(LLViewerObject* root) const
{
LL_PROFILE_ZONE_SCOPED_CATEGORY_SCRIPTDEV;
@@ -768,12 +814,13 @@ LLSD LLPublishedObjectMgr::buildPublishedObjectLLSD(LLViewerObject* root) const
{
LLSD link;
link["link_id"] = child->getID();
- link["link_number"] = link_number++;
- link["link_name"] = get_prim_name(child);
+ link["link_number"] = link_number;
+ link["link_name"] = get_prim_name(child, link_number);
link["link_description"] = nv_string(child, "Desc");
add_object_permissions(link, child);
link["inventory"] = buildPrimInventoryLLSD(child);
linked_objects.append(link);
+ ++link_number;
}
if (linked_objects.size() > 0)
{
@@ -1083,25 +1130,37 @@ bool LLPublishedObjectMgr::applyPropertyChange(
if (prim_id == root_id)
{
- bool has_name = !name.empty();
- bool name_changed = has_name && (pub_info->mObjectName != name);
- bool desc_changed = (pub_info->mObjectDescription != desc);
- if (!name_changed && !desc_changed)
- {
- return false;
- }
+ bool changed = false;
- if (name_changed)
+ if (!name.empty() && pub_info->mObjectName != name)
{
pub_info->mObjectName = name;
update["object_name"] = name;
+ changed = true;
}
- if (desc_changed)
+ if (pub_info->mObjectDescription != desc)
{
pub_info->mObjectDescription = desc;
update["object_description"] = desc;
+ changed = true;
}
- return true;
+
+ // Properties arrive after publish, so this field is backfilled here.
+ LLViewerObject* root = gObjectList.findObject(root_id);
+ if (root)
+ {
+ LLUUID source_task_id;
+ const bool can_save_back = computeCanSaveBack(root, source_task_id);
+ if (can_save_back != pub_info->mCanSaveBackToContents)
+ {
+ pub_info->mCanSaveBackToContents = can_save_back;
+ pub_info->mSourceTaskID = source_task_id;
+ update["can_save_back"] = can_save_back;
+ changed = true;
+ }
+ }
+
+ return changed;
}
auto prim_it = std::find_if(pub_info->mPrims.begin(), pub_info->mPrims.end(),
@@ -1135,6 +1194,61 @@ bool LLPublishedObjectMgr::applyPropertyChange(
return true;
}
+bool LLPublishedObjectMgr::applyPermissionsChange(
+ const LLUUID& root_id,
+ const LLUUID& prim_id,
+ const LLUUID& owner_id,
+ U32 owner_mask,
+ U32 next_owner_mask,
+ LLSD& update)
+{
+ PublishedObjectInfo* pub_info = getPublished(root_id);
+ if (!pub_info)
+ {
+ return false;
+ }
+
+ const LLSD permissions = makePermissionsLLSD(owner_mask, next_owner_mask);
+
+ update = LLSD();
+ update["object_id"] = root_id;
+
+ if (prim_id == root_id)
+ {
+ bool changed = false;
+ if (!llsd_equals(permissions, pub_info->mPermissions))
+ {
+ pub_info->mPermissions = permissions;
+ update["permissions"] = permissions;
+ changed = true;
+ }
+ if (owner_id.notNull() && pub_info->mOwnerID != owner_id)
+ {
+ pub_info->mOwnerID = owner_id;
+ update["owner_id"] = owner_id;
+ changed = true;
+ }
+ return changed;
+ }
+
+ auto prim_it = std::find_if(pub_info->mPrims.begin(), pub_info->mPrims.end(),
+ [&](const PublishedPrimInfo& p) { return p.mPrimID == prim_id; });
+ if (prim_it == pub_info->mPrims.end() || llsd_equals(permissions, prim_it->mPermissions))
+ {
+ return false;
+ }
+
+ prim_it->mPermissions = permissions;
+
+ LLSD modified_entry;
+ modified_entry["link_id"] = prim_id;
+ modified_entry["permissions"] = permissions;
+ LLSD modified_arr = LLSD::emptyArray();
+ modified_arr.append(modified_entry);
+ update["changes"]["linked_objects"]["modified"] = modified_arr;
+ return true;
+}
+
bool LLPublishedObjectMgr::hasActiveLinksetFlushTimer(const LLUUID& root_id) const
{
auto it = mLinksetFlushTimers.find(root_id);
diff --git a/indra/newview/llpublishedobjectmgr.h b/indra/newview/llpublishedobjectmgr.h
index 51af129dcb..8a6c8b705c 100644
--- a/indra/newview/llpublishedobjectmgr.h
+++ b/indra/newview/llpublishedobjectmgr.h
@@ -55,6 +55,7 @@ public:
S32 mLinkNumber;
std::string mPrimDescription;
S16 mInventorySerial;
+ LLSD mPermissions;
};
struct PublishedObjectInfo
@@ -73,6 +74,7 @@ public:
std::string mRegionName;
bool mCanSaveBackToContents{ false };
LLUUID mSourceTaskID;
+ LLSD mPermissions;
std::vector<PublishedPrimInfo> mPrims;
std::vector<std::unique_ptr<LLPublishedPrimListener>> mListeners;
};
@@ -256,6 +258,12 @@ public:
LLSD buildPrimInventoryLLSD(LLViewerObject* object) const;
LLSD buildPublishedObjectLLSD(LLViewerObject* root) const;
LLSD buildObjectListLLSD() const;
+
+ // Both depend on a valid LLSelectNode, so they yield nothing until ObjectProperties arrives.
+ static LLSD getObjectPermissionsLLSD(LLViewerObject* object);
+ static bool computeCanSaveBack(LLViewerObject* root, LLUUID& source_task_id);
+ static LLSD makePermissionsLLSD(U32 owner_mask, U32 next_owner_mask);
+
bool buildLinksetUpdateLLSD(const LLUUID& root_id, LLSD& update) const;
bool reconcileLinksetChildAdded(const LLUUID& root_id, LLViewerObject* child, F64 request_start_sec);
bool reconcileLinksetChildRemoved(const LLUUID& root_id, const LLUUID& child_id);
@@ -275,6 +283,13 @@ public:
const std::string& name,
const std::string& desc,
LLSD& update);
+ bool applyPermissionsChange(
+ const LLUUID& root_id,
+ const LLUUID& prim_id,
+ const LLUUID& owner_id,
+ U32 owner_mask,
+ U32 next_owner_mask,
+ LLSD& update);
void beginPendingPublish(const LLUUID& object_id, const std::vector<LLViewerObject*>& prims);
bool hasPendingPublish(const LLUUID& object_id) const;
diff --git a/indra/newview/llscripteditorws.cpp b/indra/newview/llscripteditorws.cpp
index 93b1bd5eb0..da7c0cb0fa 100644
--- a/indra/newview/llscripteditorws.cpp
+++ b/indra/newview/llscripteditorws.cpp
@@ -69,9 +69,16 @@
#include "llviewertexteditor.h"
#include "llvoinventorylistener.h"
#include "roles_constants.h"
+#include "workqueue.h"
#include <array>
+#include <openssl/rand.h>
+
+#if LL_WINDOWS
+#include <aclapi.h>
+#endif
+
namespace
{
// Per-operation timeouts (seconds) for coroutine-based async RPC handlers.
@@ -80,6 +87,11 @@ namespace
constexpr F32 NOTECARD_UPLOAD_TIMEOUT = 30.0f;
constexpr F32 ITEM_CREATE_TIMEOUT = 30.0f;
+ // Seconds a client has to answer the handshake: plenty to read a
+ // file, and no longer than that is a connection kept that has not
+ // proven itself.
+ constexpr F64 HANDSHAKE_TIMEOUT = 30.0;
+
// Linkset flush coalescing delays (seconds).
constexpr F32 LINKSET_ADD_FLUSH_DELAY = 5.0f;
constexpr F32 LINKSET_REMOVE_FLUSH_DELAY = 0.2f;
@@ -453,7 +465,10 @@ void LLScriptEditorWSServer::onStopped()
LL_INFOS("ScriptEditorWS") << "Script editor WebSocket server stopped, all state cleaned up" << LL_ENDL;
- LLNotificationsUtil::add("ExternalEditorServerStopped");
+ if (!LLApp::isExiting())
+ {
+ LLNotificationsUtil::add("ExternalEditorServerStopped");
+ }
}
void LLScriptEditorWSServer::onConnectionOpened(const LLWebsocketMgr::WSConnection::ptr_t& connection)
@@ -627,27 +642,27 @@ void LLScriptEditorWSServer::setupConnectionMethods(LLJSONRPCConnection::ptr_t c
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",
+ // All per-connection methods dispatch to the main thread inside a
+ // coroutine (see LLJSONRPCConnection::processRequest).
+ script_connection->registerAsyncMethod("language.syntax.id",
bindHandler([](LLScriptEditorWSServer& s, auto&, auto&, auto&)
{
return s.handleLanguageIdRequest();
}));
- script_connection->registerMethod("language.syntax",
+ script_connection->registerAsyncMethod("language.syntax",
bindHandler([](LLScriptEditorWSServer& s, auto&, auto&, const LLSD& params)
{
return s.handleSyntaxRequest(params);
}));
- script_connection->registerMethod("language.syntax.cache",
+ script_connection->registerAsyncMethod("language.syntax.cache",
bindHandler([](LLScriptEditorWSServer& s, auto&, auto&, auto&)
{
return s.handleSyntaxCacheRequest();
}));
- script_connection->registerMethod("language.syntax.get",
+ script_connection->registerAsyncMethod("language.syntax.get",
bindHandler([](LLScriptEditorWSServer& s, auto&, auto&, const LLSD& params)
{
return s.handleSyntaxCacheFileRequest(params);
@@ -659,7 +674,7 @@ void LLScriptEditorWSServer::setupConnectionMethods(LLJSONRPCConnection::ptr_t c
return s.handleScriptSubscribe(connection_id, params);
}));
- script_connection->registerMethod("script.list",
+ script_connection->registerAsyncMethod("script.list",
bindHandler([](LLScriptEditorWSServer& s, auto&, auto&, auto&)
{
return s.handleFileWatcherFileListRequest();
@@ -671,7 +686,6 @@ void LLScriptEditorWSServer::setupConnectionMethods(LLJSONRPCConnection::ptr_t c
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)
{
@@ -1060,6 +1074,9 @@ LLSD LLScriptEditorWSServer::handleObjectModify(U32 connection_id, const LLSD& p
msg->sendReliable(host);
}
+ // Force a properties reply so the editor sees the change without an in-world selection.
+ LLSelectMgr::instance().requestObjectPropertiesFamily(prim);
+
// Step 4: Return Success Response
LLSD response;
response["success"] = true;
@@ -1253,7 +1270,7 @@ void LLScriptEditorWSServer::broadcastLanguageChange()
if (isRunning())
{
- broadcastNotification("language.syntax.change", params);
+ notifyAll("language.syntax.change", params);
}
}
}
@@ -2296,7 +2313,6 @@ void LLScriptEditorWSServer::forwardChatToIDE(
void LLScriptEditorWSServer::sendRuntimeEvent(
const LLPublishedObjectMgr::RuntimeChatEvent& event) const
{
-
std::string script_id;
if (event.mItemID.notNull())
{
@@ -2363,7 +2379,7 @@ void LLScriptEditorWSServer::notifyConnection(U32 connection_id, const std::stri
if (it != mActiveConnections.end())
{
auto connection = it->second.lock();
- if (connection)
+ if (connection && connection->isAuthenticated())
{
connection->notify(method, params);
}
@@ -2383,7 +2399,7 @@ void LLScriptEditorWSServer::notifyAll(const std::string& method, const LLSD& pa
for (const auto& pair : mActiveConnections)
{
auto connection = pair.second.lock();
- if (connection)
+ if (connection && connection->isAuthenticated())
{
connection->sendMessage(payload);
}
@@ -2392,7 +2408,7 @@ void LLScriptEditorWSServer::notifyAll(const std::string& method, const LLSD& pa
// static
-std::string LLScriptEditorWSServer::getPrimName(LLViewerObject* obj)
+std::string LLScriptEditorWSServer::getPrimName(LLViewerObject* obj, S32 link_number)
{
std::string name = nv_string(obj, "Name");
if (!name.empty())
@@ -2411,8 +2427,8 @@ std::string LLScriptEditorWSServer::getPrimName(LLViewerObject* obj)
return node->mName;
}
- // Never emit an empty prim/object name to downstream tooling.
- return obj->getID().asString();
+ // Real prim names need a full ObjectProperties reply, which requires selection.
+ return (link_number > 1) ? llformat("Link #%d", link_number) : std::string("Object");
}
bool LLScriptEditorWSServer::publishObject(const LLUUID& object_id)
@@ -2440,12 +2456,8 @@ bool LLScriptEditorWSServer::publishObject(const LLUUID& object_id)
// 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);
- }
+ // The sim coalesces properties-family requests to the root, so one request covers the linkset.
+ LLSelectMgr::instance().requestObjectPropertiesFamily(root);
// Set up a PendingPublish to coordinate inventory loading across all prims.
// We register a listener and call requestInventory() on every prim.
@@ -2510,24 +2522,14 @@ void LLScriptEditorWSServer::buildAndSendPublish(const LLUUID& object_id)
info.mOwnerID = root->mOwnerID;
info.mObjectName = pub["object_name"].asString();
info.mObjectDescription = pub["object_description"].asString();
+ info.mPermissions = LLPublishedObjectMgr::getObjectPermissionsLLSD(root);
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();
- }
+ LLUUID source_task_id;
+ info.mCanSaveBackToContents = LLPublishedObjectMgr::computeCanSaveBack(root, source_task_id);
+ info.mSourceTaskID = source_task_id;
S32 link_num = 1;
std::vector<LLViewerObject*> prims = collect_linkset(root);
@@ -2535,9 +2537,10 @@ void LLScriptEditorWSServer::buildAndSendPublish(const LLUUID& object_id)
{
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.mPrimName = getPrimName(prim, prim_info.mLinkNumber);
prim_info.mInventorySerial = static_cast<S16>(prim->getInventorySerial());
+ prim_info.mPermissions = LLPublishedObjectMgr::getObjectPermissionsLLSD(prim);
info.mPrims.push_back(prim_info);
}
@@ -2578,12 +2581,9 @@ void LLScriptEditorWSServer::buildAndSendPublish(const LLUUID& 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);
- }
+ // Re-request now that the object is published so onObjectPropertyChanged can emit
+ // object.update. The sim answers per linkset, so only the root needs asking.
+ LLSelectMgr::instance().requestObjectPropertiesFamily(root);
}
void LLScriptEditorWSServer::onLinksetChildAdded(const LLUUID& root_id, LLViewerObject* child)
@@ -2745,6 +2745,24 @@ void LLScriptEditorWSServer::onObjectPropertyChanged(
}
}
+void LLScriptEditorWSServer::onObjectPermissionsReceived(
+ const LLUUID& prim_id, const LLUUID& owner_id, U32 owner_mask, U32 next_owner_mask)
+{
+ LL_PROFILE_ZONE_SCOPED_CATEGORY_SCRIPTDEV;
+ LLViewerObject* prim = gObjectList.findObject(prim_id);
+ if (!prim)
+ {
+ return;
+ }
+
+ LLSD update;
+ if (mPublishedObjectManager.applyPermissionsChange(
+ prim->getRootEdit()->getID(), prim_id, owner_id, owner_mask, next_owner_mask, update))
+ {
+ notifyAll("object.update", update);
+ }
+}
+
void LLScriptEditorWSServer::unpublishObject(const LLUUID& object_id, const std::string& reason)
{
LL_PROFILE_ZONE_SCOPED_CATEGORY_SCRIPTDEV;
@@ -2782,6 +2800,26 @@ void LLScriptEditorWSConnection::onOpen()
LL_INFOS("ScriptEditorWS") << "Script editor JSON-RPC connection opened" << LL_ENDL;
+ // gAgent, LLVersionInfo, LLSyntaxDefCache, and challenge file I/O below
+ // are main-thread only; this callback runs on the WS I/O thread.
+ wptr_t that = weak_from_this();
+ if (!LL::WorkQueue::postToMainLoop([that]()
+ {
+ auto self = that.lock();
+ if (self)
+ {
+ self->sendHandshake();
+ }
+ }))
+ {
+ LL_WARNS("ScriptEditorWS") << "Main loop unavailable; dropping handshake for new connection" << LL_ENDL;
+ }
+}
+
+void LLScriptEditorWSConnection::sendHandshake()
+{
+ LL_PROFILE_ZONE_SCOPED_CATEGORY_SCRIPTDEV;
+
// Build hello data
LLSD handshake;
handshake["server_version"] = "1.0.0";
@@ -2792,11 +2830,14 @@ void LLScriptEditorWSConnection::onOpen()
handshake["agent_id"] = gAgent.getID();
handshake["agent_name"] = gAgentUsername;
- std::string challenge_file = generateChallenge();
- if (!challenge_file.empty())
+ const Challenge challenge = writeChallenge();
+ if (challenge.mFile.empty())
{
- handshake["challenge"] = challenge_file;
+ // Nothing to prove itself by, so nothing it may do.
+ sendDisconnect(DisconnectReason::INTERNAL_ERROR, "Unable to issue a challenge");
+ return;
}
+ handshake["challenge"] = challenge.mFile;
LLSD languages = LLSD::emptyArray();
languages.append("lsl");
@@ -2815,22 +2856,33 @@ void LLScriptEditorWSConnection::onOpen()
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())
+ // The answer comes once, whichever way: from the client, as the
+ // time runs out, or as the connection closes.
+ const LLSD sent = call(
+ "session.handshake", handshake,
+ [that, challenge](const LLSD& result, const LLSD& error)
{
+ // Whatever the answer, the file has done its work.
+ LLFile::remove(challenge.mFile);
auto self = that.lock();
- if (self)
+ if (!self)
{
- self->handleHandshakeResponse(result);
+ return;
}
- }
- else
- {
- LL_WARNS("ScriptEditorWS") << "Handshake failed: "
- << error["message"].asString() << LL_ENDL;
- }
- });
+ if (error.isDefined())
+ {
+ self->handleHandshakeError(error);
+ return;
+ }
+ self->handleHandshakeResponse(result, challenge.mSecret);
+ },
+ HANDSHAKE_TIMEOUT);
+ if (sent.isUndefined())
+ {
+ LLFile::remove(challenge.mFile);
+ handleHandshakeError(LLSD().with("code", LLJSONRPCConnection::RPCError::INTERNAL_ERROR).with("message", "Handshake not sent"));
+ return;
+ }
LL_INFOS("ScriptEditorWS") << "Sent handshake call to new editor client" << LL_ENDL;
}
@@ -2862,40 +2914,36 @@ void LLScriptEditorWSConnection::sendDisconnect(DisconnectReason reason, const s
closeConnection(1000, message);
}
-void LLScriptEditorWSConnection::handleHandshakeResponse(const LLSD& result)
+void LLScriptEditorWSConnection::handleHandshakeError(const LLSD& error)
{
- 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)
+ const S32 code = error["code"].asInteger();
+ if (code == LLJSONRPCConnection::RPCError::CONNECTION_CLOSED)
{
- valid_response = result.has("challenge_response") &&
- (result["challenge_response"].asUUID() == mChallenge);
- mChallenge.setNull();
+ // Gone already.
+ return;
}
+ LL_WARNS("ScriptEditorWS") << "Handshake failed: " << error["message"].asString() << LL_ENDL;
+ sendDisconnect(code == LLJSONRPCConnection::RPCError::REQUEST_TIMEOUT ? DisconnectReason::TIMEOUT : DisconnectReason::PROTOCOL_ERROR,
+ "Handshake failed");
+}
- // Always clean up the temporary challenge file if one was created,
- // regardless of validation outcome.
- if (!mChallengeFile.empty())
- {
- LLFile::remove(mChallengeFile);
- mChallengeFile.clear();
- }
+void LLScriptEditorWSConnection::handleHandshakeResponse(const LLSD& result, const LLUUID& secret)
+{
+ LL_PROFILE_ZONE_SCOPED_CATEGORY_SCRIPTDEV;
+ LL_INFOS("ScriptEditorWS") << "Processing handshake response from client" << LL_ENDL;
- if (challenge_issued && !valid_response)
+ // Only something that could read the file knows what it says.
+ if (!result.isMap() || result["challenge_response"].asUUID() != secret)
{
LL_WARNS("ScriptEditorWS") << "Invalid or missing challenge response from client" << LL_ENDL;
sendDisconnect(DisconnectReason::PROTOCOL_ERROR, "Invalid challenge response");
return;
}
+ mClientName = result["client_name"].asString();
+ mClientVersion = result["client_version"].asString();
+ mProtocolVersion = result["protocol_version"].asString();
+
if (mProtocolVersion != "1.0")
{
LL_WARNS("ScriptEditorWS") << "Protocol version mismatch. Expected: 1.0, Got: "
@@ -2921,29 +2969,134 @@ void LLScriptEditorWSConnection::handleHandshakeResponse(const LLSD& result)
}
}
+ // Let in before it is told so: what it asks on hearing it must be
+ // answered.
+ setAuthenticated(true);
notify("session.ok");
LL_INFOS("ScriptEditorWS") << "Handshake completed successfully." << LL_ENDL;
}
-std::string LLScriptEditorWSConnection::generateChallenge()
+// static
+LLScriptEditorWSConnection::Challenge LLScriptEditorWSConnection::writeChallenge()
{
- mChallenge.generate();
+ // A secret nobody could work out: a new id is made of the time and
+ // the network card's address, which are anybody's to know.
+ Challenge challenge;
+ if (RAND_bytes(challenge.mSecret.mData, UUID_BYTES) != 1)
+ {
+ LL_WARNS("ScriptEditorWS") << "Unable to make a challenge secret" << LL_ENDL;
+ return {};
+ }
- mChallengeFile = std::string(LLFile::tmpdir()) + "sl_script_challenge_" + mChallenge.asString() + ".tmp";
+ // Named for nothing it holds, made new rather than written over
+ // anything, or through a link, already by that name, and readable by
+ // the user alone: the name goes to whoever connects, and the temp
+ // folder can be everyone's.
+ const std::string file = LLFile::tmpdir() + "sl_script_challenge_" + LLUUID::generateNewID().asString() + ".tmp";
+ const std::string text = challenge.mSecret.asString();
+ if (!writeUserOnlyFile(file, text))
+ {
+ LL_WARNS("ScriptEditorWS") << "Unable to write challenge file " << file << LL_ENDL;
+ return {};
+ }
+
+ challenge.mFile = file;
+ return challenge;
+}
- llofstream file(mChallengeFile.c_str());
- if (!file.is_open())
+// static
+bool LLScriptEditorWSConnection::writeUserOnlyFile(const std::string& file, const std::string& text)
+{
+#if LL_WINDOWS
+ // LLFile::open() ignores the POSIX permission bits on Windows -- its
+ // decode_attributes() only maps them to a normal/read-only file
+ // attribute, never to an ACL -- so a mode like 0600 there does not
+ // keep other local accounts from reading the file. Build a DACL
+ // explicitly, granting only the current user's SID access.
+ HANDLE token = nullptr;
+ if (!OpenProcessToken(GetCurrentProcess(), TOKEN_QUERY, &token))
{
- LL_WARNS("ScriptEditorWS") << "Unable to open challenge file: " << mChallengeFile << LL_ENDL;
- mChallenge.setNull();
- mChallengeFile.clear();
- return std::string();
+ LL_WARNS("ScriptEditorWS") << "Unable to open the process token for the challenge file's ACL" << LL_ENDL;
+ return false;
+ }
+
+ DWORD needed = 0;
+ GetTokenInformation(token, TokenUser, nullptr, 0, &needed);
+ std::vector<BYTE> user_info(needed);
+ const bool got_user = needed != 0 && GetTokenInformation(token, TokenUser, user_info.data(), needed, &needed);
+ CloseHandle(token);
+ if (!got_user)
+ {
+ LL_WARNS("ScriptEditorWS") << "Unable to read the current user's SID for the challenge file's ACL" << LL_ENDL;
+ return false;
}
+ PSID user_sid = reinterpret_cast<PTOKEN_USER>(user_info.data())->User.Sid;
- file << mChallenge;
- file.close();
+ EXPLICIT_ACCESSW ea = {};
+ ea.grfAccessPermissions = GENERIC_READ | GENERIC_WRITE | DELETE;
+ ea.grfAccessMode = SET_ACCESS;
+ ea.grfInheritance = NO_INHERITANCE;
+ ea.Trustee.TrusteeForm = TRUSTEE_IS_SID;
+ ea.Trustee.TrusteeType = TRUSTEE_IS_USER;
+ ea.Trustee.ptstrName = reinterpret_cast<LPWSTR>(user_sid);
- return mChallengeFile;
+ PACL dacl = nullptr;
+ if (SetEntriesInAclW(1, &ea, nullptr, &dacl) != ERROR_SUCCESS)
+ {
+ LL_WARNS("ScriptEditorWS") << "Unable to build an ACL for the challenge file" << LL_ENDL;
+ return false;
+ }
+
+ SECURITY_DESCRIPTOR sd;
+ const bool sd_ok = InitializeSecurityDescriptor(&sd, SECURITY_DESCRIPTOR_REVISION) &&
+ SetSecurityDescriptorDacl(&sd, TRUE, dacl, FALSE);
+ if (!sd_ok)
+ {
+ LL_WARNS("ScriptEditorWS") << "Unable to build a security descriptor for the challenge file" << LL_ENDL;
+ LocalFree(dacl);
+ return false;
+ }
+
+ SECURITY_ATTRIBUTES sa = {};
+ sa.nLength = sizeof(sa);
+ sa.lpSecurityDescriptor = &sd;
+ sa.bInheritHandle = FALSE;
+
+ const std::wstring wfile = ll_convert<std::wstring>(file);
+ // CREATE_NEW: fails if the file already exists, matching LLFile::noreplace.
+ HANDLE handle = CreateFileW(wfile.c_str(), GENERIC_WRITE, 0, &sa, CREATE_NEW, FILE_ATTRIBUTE_NORMAL, nullptr);
+ LocalFree(dacl);
+ if (handle == INVALID_HANDLE_VALUE)
+ {
+ return false;
+ }
+
+ DWORD written = 0;
+ const bool ok = WriteFile(handle, text.data(), static_cast<DWORD>(text.size()), &written, nullptr) &&
+ written == text.size();
+ CloseHandle(handle);
+ if (!ok)
+ {
+ LLFile::remove(file);
+ }
+ return ok;
+#else
+ // POSIX open() honors the mode bits directly.
+ std::error_code ec;
+ LLFile out(file, LLFile::out | LLFile::noreplace, ec, 0600);
+ if (ec)
+ {
+ return false;
+ }
+ const bool written = out.write(text.data(), static_cast<S64>(text.size()), ec) == static_cast<S64>(text.size()) && !ec;
+ const bool closed = out.close(ec) == 0;
+ if (!written || !closed)
+ {
+ LLFile::remove(file);
+ return false;
+ }
+ return true;
+#endif
}
diff --git a/indra/newview/llscripteditorws.h b/indra/newview/llscripteditorws.h
index 6eb8385210..9919800ebe 100644
--- a/indra/newview/llscripteditorws.h
+++ b/indra/newview/llscripteditorws.h
@@ -69,6 +69,10 @@ public:
LLScriptEditorWSConnection(const LLWebsocketMgr::WSServer::ptr_t server, const LLWebsocketMgr::connection_h& handle) :
LLJSONRPCConnection(server, handle)
{
+ // Anything that can reach the port can connect: nothing it asks
+ // is answered until it has proven it can read the user's files.
+ setAuthenticated(false);
+
// Reserve id 0 as the "unassigned" sentinel used by EditorSubscription;
// on wrap, skip past it.
U32 id;
@@ -93,12 +97,33 @@ public:
private:
using string_set_t = std::set<std::string>;
+
+ // What the client proves itself with: a secret written to a file only
+ // the user can read, under a name that says nothing of it. The client
+ // is told where the file is and answers with what it says.
+ struct Challenge
+ {
+ LLUUID mSecret;
+ std::string mFile;
+ };
+ // Empty where no secret could be made or written.
+ static Challenge writeChallenge();
+
+ // Writes text to a new file at path, readable only by the current
+ // user. On Windows this means an explicit ACL, since LLFile::open()
+ // there ignores POSIX permission bits entirely. Fails if the file
+ // already exists.
+ static bool writeUserOnlyFile(const std::string& path, const std::string& text);
+
/**
* @brief Handle the handshake response from the client
* @param result The response data from the client containing client information
+ * @param secret What the challenge file said, which the client must answer with
*/
- void handleHandshakeResponse(const LLSD& result);
- std::string generateChallenge();
+ void handleHandshakeResponse(const LLSD& result, const LLUUID& secret);
+ // The handshake refused, unanswered in time, or never sent.
+ void handleHandshakeError(const LLSD& error);
+ void sendHandshake();
LLScriptEdContainer* getEditor() const;
std::shared_ptr<LLScriptEditorWSServer> getServer() const;
@@ -113,8 +138,6 @@ private:
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;
};
@@ -151,6 +174,11 @@ private:
* ## Security Considerations
*
* - Server binds to localhost only by default for security
+ * - A connection from a browser -- any upgrade request with an Origin -- is
+ * refused, so no web page, the viewer's own included, can reach it
+ * - Nothing a client asks is answered, and nothing is sent it, until it
+ * has answered the handshake's challenge with what a file only the user
+ * can read says; a wrong answer, or none within 30 seconds, closes it
* - JSON-RPC 2.0 structured protocol with validation
* - Rate limiting handled by base JSON-RPC server
* - Error handling with standardized JSON-RPC error codes
@@ -225,6 +253,7 @@ public:
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 onObjectPermissionsReceived(const LLUUID& prim_id, const LLUUID& owner_id, U32 owner_mask, U32 next_owner_mask);
void onLinksetChildAdded(const LLUUID& root_id, LLViewerObject* child);
void onLinksetChildRemoved(const LLUUID& root_id, const LLUUID& child_id);
@@ -280,7 +309,7 @@ protected:
ValidatedItem validatePublishedItem(const LLSD& params, U32 permMask) const;
// --- Object Content Publishing (helpers) ---
- static std::string getPrimName(LLViewerObject* obj);
+ static std::string getPrimName(LLViewerObject* obj, S32 link_number = 1);
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);
diff --git a/indra/newview/llselectmgr.cpp b/indra/newview/llselectmgr.cpp
index a09b54ffac..f9108fcc64 100644
--- a/indra/newview/llselectmgr.cpp
+++ b/indra/newview/llselectmgr.cpp
@@ -6108,11 +6108,14 @@ void LLSelectMgr::processObjectProperties(LLMessageSystem* msg, void** user_data
node->mInventorySerial = inv_serial;
node->mSitName.assign(sit_name);
node->mTouchName.assign(touch_name);
+ }
- if (auto ws_server = LLScriptEditorWSServer::getServer())
- {
- ws_server->onObjectPropertyChanged(id, name, desc, inv_serial);
- }
+ // Published objects need property updates even when not selected.
+ LLScriptEditorWSServer::ptr_t ws_server = LLScriptEditorWSServer::getServer();
+ if (ws_server)
+ {
+ ws_server->onObjectPropertyChanged(id, name, desc, inv_serial);
+ ws_server->onObjectPermissionsReceived(id, owner_id, owner_mask, next_owner_mask);
}
}
@@ -6213,6 +6216,7 @@ void LLSelectMgr::processObjectPropertiesFamily(LLMessageSystem* msg, void** use
if (auto ws_server = LLScriptEditorWSServer::getServer())
{
ws_server->onObjectPropertyChanged(id, name, desc);
+ ws_server->onObjectPermissionsReceived(id, owner_id, owner_mask, next_owner_mask);
}
}
diff --git a/indra/newview/llviewerfloaterreg.cpp b/indra/newview/llviewerfloaterreg.cpp
index 7d3b554709..fa10288132 100644
--- a/indra/newview/llviewerfloaterreg.cpp
+++ b/indra/newview/llviewerfloaterreg.cpp
@@ -36,7 +36,6 @@
#include "llfasttimerview.h"
#include "llfloater360capture.h"
#include "llfloaterabout.h"
-#include "llfloateraddpaymentmethod.h"
#include "llfloaterauction.h"
#include "llfloaterautoreplacesettings.h"
#include "llfloateravatarpicker.h"
@@ -242,7 +241,6 @@ public:
const std::list<std::string> blacklist_untrusted = {
"360capture",
"block_timers",
- "add_payment_method",
"appearance",
"associate_listing",
"avatar_picker",
@@ -332,7 +330,6 @@ void LLViewerFloaterReg::registerFloaters()
LLFloaterReg::add("360capture", "floater_360capture.xml", (LLFloaterBuildFunc)&LLFloaterReg::build<LLFloater360Capture>);
LLFloaterReg::add("block_timers", "floater_fast_timers.xml", (LLFloaterBuildFunc)&LLFloaterReg::build<LLFastTimerView>);
LLFloaterReg::add("about_land", "floater_about_land.xml", (LLFloaterBuildFunc)&LLFloaterReg::build<LLFloaterLand>);
- LLFloaterReg::add("add_payment_method", "floater_add_payment_method.xml", (LLFloaterBuildFunc)&LLFloaterReg::build<LLFloaterAddPaymentMethod>);
LLFloaterReg::add("appearance", "floater_my_appearance.xml", (LLFloaterBuildFunc)&LLFloaterReg::build<LLFloaterSidePanelContainer>);
LLFloaterReg::add("associate_listing", "floater_associate_listing.xml", (LLFloaterBuildFunc)&LLFloaterReg::build<LLFloaterAssociateListing>);
LLFloaterReg::add("auction", "floater_auction.xml", (LLFloaterBuildFunc)&LLFloaterReg::build<LLFloaterAuction>);
diff --git a/indra/newview/llviewerwindow.cpp b/indra/newview/llviewerwindow.cpp
index b549428062..dfd03ece35 100644
--- a/indra/newview/llviewerwindow.cpp
+++ b/indra/newview/llviewerwindow.cpp
@@ -1539,30 +1539,12 @@ void LLViewerWindow::handleCloseRequestCanceled()
void LLViewerWindow::handleSuspendRequest()
{
- static LLCachedControl<S32> os_hibernation_mode(gSavedSettings, "OSHibernationMode", 0);
- if (os_hibernation_mode == 0)
- {
- LL_INFOS() << "Got a 'suspend' event from OS" << LL_ENDL;
- // Viewer doesn't handle hibernation.
- // Just send statistics.
- LLAppViewer::instance()->sendViewerStatistics(false);
- }
- else
- {
- LL_INFOS() << "Got a 'suspend' event from OS, disconnecting" << LL_ENDL;
- // Viewer is set to prevent hibernation if agent isn't away.
- // If we got here, likely Agent 'went' away then viewer got
- // a hibernation message.
- // We have a limited timeframe. Sends stats then disconnect.
- LLViewerRegion* region = gAgent.getRegion();
- if (region)
- {
- LLAppViewer::instance()->sendViewerStatistics(true);
- LLAppViewer::instance()->metricsSend(!gDisconnected);
- // Make sure to show a message.
- LLAppViewer::instance()->forceDisconnect(LLTrans::getString("YouHaveBeenDisconnected"));
- }
- }
+ LLAppViewer::instance()->sendViewerStatistics(false);
+ // Todo: this should send a disconnect request as viewer
+ // can't keep heartbeat up while suspended and will get
+ // disconnected within a minute.
+ // Add a disconnect here once 'prevent OS from sleeping'
+ // feature is ready.
}
bool LLViewerWindow::handleCloseRequest(LLWindow *window, bool from_user)
diff --git a/indra/newview/llvoicewebrtc.cpp b/indra/newview/llvoicewebrtc.cpp
index 1cbce33cbd..fb463fc94e 100644
--- a/indra/newview/llvoicewebrtc.cpp
+++ b/indra/newview/llvoicewebrtc.cpp
@@ -289,7 +289,26 @@ void LLWebRTCVoiceClient::initWebRTC()
mWebRTCDeviceInterface = llwebrtc::getDeviceInterface();
mWebRTCDeviceInterface->setDevicesObserver(this);
- mMainQueue = LL::WorkQueue::getInstance("mainloop");
+
+ if (!mVoiceWorkQueue)
+ {
+ mVoiceWorkQueue = std::make_shared<LL::WorkQueue>("VoiceWebRTC");
+ // Service this queue every idle tick, on the main thread.
+ doOnIdleRepeating(
+ [queue = LL::WorkQueue::weak_t(mVoiceWorkQueue)]() -> bool
+ {
+ auto locked = queue.lock();
+ if (!locked || locked->isClosed())
+ {
+ // Done
+ return true;
+ }
+ locked->runPending();
+ // Returning false means "call me again next idle".
+ return false;
+ });
+ }
+
refreshDeviceLists();
}
@@ -352,11 +371,11 @@ void LLWebRTCVoiceClient::drainConnections()
LLTimer timer;
while (!sessionState::allSessionsClosed() && timer.getElapsedTimeF32() < DRAIN_TIMEOUT_SECONDS)
{
- // OnPeerConnectionClosed comes back through the main queue, so it has
+ // OnPeerConnectionClosed comes back through the queue, so it has
// to be pumped or the state machines never see connections finish.
- if (auto main_queue = mMainQueue.lock())
+ if (mVoiceWorkQueue)
{
- main_queue->runFor(std::chrono::milliseconds(DRAIN_POLL_MS));
+ mVoiceWorkQueue->runFor(std::chrono::milliseconds(DRAIN_POLL_MS));
}
sessionState::processSessionStates();
@@ -714,7 +733,7 @@ void LLWebRTCVoiceClient::voiceConnectionCoro()
updatePosition();
}
}
- LL::WorkQueue::postMaybe(mMainQueue,
+ LL::WorkQueue::postMaybe(mVoiceWorkQueue,
[=, this] {
if (sShuttingDown)
{
@@ -836,7 +855,7 @@ void LLWebRTCVoiceClient::OnDevicesChanged(const llwebrtc::LLWebRTCVoiceDeviceLi
const llwebrtc::LLWebRTCVoiceDeviceList& capture_devices)
{
- LL::WorkQueue::postMaybe(mMainQueue,
+ LL::WorkQueue::postMaybe(mVoiceWorkQueue,
[=, this]
{
OnDevicesChangedImpl(render_devices, capture_devices);
@@ -2524,7 +2543,7 @@ LLVoiceWebRTCConnection::LLVoiceWebRTCConnection(const LLUUID &regionID, const s
mWebRTCPeerConnectionInterface = llwebrtc::newPeerConnection();
mWebRTCPeerConnectionInterface->setSignalingObserver(this);
- mMainQueue = LL::WorkQueue::getInstance("mainloop");
+ mVoiceMainQueue = LLWebRTCVoiceClient::getInstance()->getVoiceWorkQueue();
}
LLVoiceWebRTCConnection::~LLVoiceWebRTCConnection()
@@ -2558,7 +2577,7 @@ LLVoiceWebRTCConnection::~LLVoiceWebRTCConnection()
// callback from llwebrtc
void LLVoiceWebRTCConnection::OnIceGatheringState(llwebrtc::LLWebRTCSignalingObserver::EIceGatheringState state)
{
- LL::WorkQueue::postMaybe(mMainQueue,
+ LL::WorkQueue::postMaybe(mVoiceMainQueue,
[=, this] {
LL_DEBUGS("Voice") << "Ice Gathering voice account. " << state << LL_ENDL;
@@ -2582,7 +2601,7 @@ void LLVoiceWebRTCConnection::OnIceGatheringState(llwebrtc::LLWebRTCSignalingObs
// callback from llwebrtc
void LLVoiceWebRTCConnection::OnIceCandidate(const llwebrtc::LLWebRTCIceCandidate& candidate)
{
- LL::WorkQueue::postMaybe(mMainQueue, [=, this] { mIceCandidates.push_back(candidate); });
+ LL::WorkQueue::postMaybe(mVoiceMainQueue, [=, this] { mIceCandidates.push_back(candidate); });
}
void LLVoiceWebRTCConnection::processIceUpdates()
@@ -2698,7 +2717,7 @@ void LLVoiceWebRTCConnection::processIceUpdatesCoro(connectionPtr_t connection)
void LLVoiceWebRTCConnection::OnOfferAvailable(const std::string &sdp)
{
connectionPtr_t connection = shared_from_this();
- LL::WorkQueue::postMaybe(mMainQueue,
+ LL::WorkQueue::postMaybe(mVoiceMainQueue,
[=] {
if (connection->mShutDown)
{
@@ -2726,7 +2745,7 @@ void LLVoiceWebRTCConnection::OnOfferAvailable(const std::string &sdp)
void LLVoiceWebRTCConnection::OnAudioEstablished(llwebrtc::LLWebRTCAudioInterface* audio_interface)
{
connectionPtr_t connection = shared_from_this();
- LL::WorkQueue::postMaybe(mMainQueue,
+ LL::WorkQueue::postMaybe(mVoiceMainQueue,
[=] {
if (connection->mShutDown)
{
@@ -2749,7 +2768,7 @@ void LLVoiceWebRTCConnection::OnAudioEstablished(llwebrtc::LLWebRTCAudioInterfac
// callback from llwebrtc
void LLVoiceWebRTCConnection::OnRenegotiationNeeded()
{
- LL::WorkQueue::postMaybe(mMainQueue,
+ LL::WorkQueue::postMaybe(mVoiceMainQueue,
[=, this] {
LL_DEBUGS("Voice") << "Voice channel requires renegotiation." << LL_ENDL;
setVoiceConnectionState(VOICE_STATE_SESSION_RETRY);
@@ -2760,7 +2779,7 @@ void LLVoiceWebRTCConnection::OnRenegotiationNeeded()
// callback from llwebrtc
void LLVoiceWebRTCConnection::OnPeerConnectionClosed()
{
- LL::WorkQueue::postMaybe(mMainQueue,
+ LL::WorkQueue::postMaybe(mVoiceMainQueue,
[=, this] {
LL_DEBUGS("Voice") << "Peer connection has closed." << LL_ENDL;
if (mVoiceConnectionState == VOICE_STATE_WAIT_FOR_CLOSE)
@@ -3271,7 +3290,7 @@ bool LLVoiceWebRTCConnection::connectionStateMachine()
// llwebrtc callback
void LLVoiceWebRTCConnection::OnDataReceived(const std::string& data, bool binary)
{
- LL::WorkQueue::postMaybe(mMainQueue, [=, this] { LLVoiceWebRTCConnection::OnDataReceivedImpl(data, binary); });
+ LL::WorkQueue::postMaybe(mVoiceMainQueue, [=, this] { LLVoiceWebRTCConnection::OnDataReceivedImpl(data, binary); });
}
//
@@ -3500,7 +3519,7 @@ void LLVoiceWebRTCConnection::OnDataReceivedImpl(const std::string &data, bool b
void LLVoiceWebRTCConnection::OnDataChannelReady(llwebrtc::LLWebRTCDataInterface *data_interface)
{
connectionPtr_t connection = shared_from_this();
- LL::WorkQueue::postMaybe(mMainQueue,
+ LL::WorkQueue::postMaybe(mVoiceMainQueue,
[=] {
if (connection->mShutDown)
{
@@ -3542,7 +3561,7 @@ void LLVoiceWebRTCConnection::sendJoin()
void LLVoiceWebRTCConnection::OnStatsDelivered(const llwebrtc::LLWebRTCStatsMap& stats_data)
{
- LL::WorkQueue::postMaybe(mMainQueue, [=, this]
+ LL::WorkQueue::postMaybe(mVoiceMainQueue, [=, this]
{
if (mShutDown)
{
diff --git a/indra/newview/llvoicewebrtc.h b/indra/newview/llvoicewebrtc.h
index 420d431fae..76e172ff3d 100644
--- a/indra/newview/llvoicewebrtc.h
+++ b/indra/newview/llvoicewebrtc.h
@@ -40,6 +40,7 @@ class LLWebRTCProtocolParser;
#include "llcoros.h"
#include "llparcel.h"
#include "llmutelist.h"
+#include "workqueue.h"
#include <queue>
#include "boost/json.hpp"
@@ -85,6 +86,8 @@ public:
// drainConnections() and ~LLVoiceWebRTCConnection().
static bool isWebRTCTerminated() { return sWebRTCTerminated; }
+ LL::WorkQueue::weak_t getVoiceWorkQueue() const { return mVoiceWorkQueue; }
+
const LLVoiceVersionInfo& getVersion() override;
void updateVersion();
@@ -468,7 +471,7 @@ private:
/// llwebrtc::terminate(). Bounded and best effort.
void drainConnections();
- LL::WorkQueue::weak_t mMainQueue;
+ LL::WorkQueue::ptr_t mVoiceWorkQueue;
F32 mTuningMicGain;
int mTuningSpeakerVolume;
@@ -693,7 +696,7 @@ class LLVoiceWebRTCConnection :
} EVoiceConnectionState;
EVoiceConnectionState mVoiceConnectionState;
- LL::WorkQueue::weak_t mMainQueue;
+ LL::WorkQueue::weak_t mVoiceMainQueue;
void setVoiceConnectionState(EVoiceConnectionState new_voice_connection_state)
{
diff --git a/indra/newview/luau.txt b/indra/newview/luau.txt
new file mode 100644
index 0000000000..45d93adef3
--- /dev/null
+++ b/indra/newview/luau.txt
@@ -0,0 +1,22 @@
+MIT License
+
+Copyright (c) 2019-2025 Roblox Corporation
+Copyright (c) 1994–2019 Lua.org, PUC-Rio.
+
+Permission is hereby granted, free of charge, to any person obtaining a copy of
+this software and associated documentation files (the "Software"), to deal in
+the Software without restriction, including without limitation the rights to
+use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
+of the Software, and to permit persons to whom the Software is furnished to do
+so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/indra/newview/skins/default/xui/da/panel_preferences_setup.xml b/indra/newview/skins/default/xui/da/panel_preferences_setup.xml
index 981c35800f..7be9a9d555 100644
--- a/indra/newview/skins/default/xui/da/panel_preferences_setup.xml
+++ b/indra/newview/skins/default/xui/da/panel_preferences_setup.xml
@@ -45,12 +45,4 @@
<combo_box.item label="Installér automatisk" name="Install_automatically"/>
<combo_box.item label="Hent og installér opdateringer manuelt" name="Install_manual"/>
</combo_box>
- <text name="os_hibernation_mode">
- Forhindr OS i at gå i dvale, hvis du ikke er væk:
- </text>
- <combo_box name="os_hibernation_combobox" tool_tip="Forhindrer kun OS i at gå i dvale, hvis brugeren ikke er væk">
- <combo_box.item label="Kan gå i dvale" name="can_hibernate"/>
- <combo_box.item label="Kan ikke gå i dvale, kan slukke skærmen" name="cant_hibernate"/>
- <combo_box.item label="Kan ikke gå i dvale, kan ikke slukke skærmen" name="cant_hibernate_screen"/>
- </combo_box>
</panel>
diff --git a/indra/newview/skins/default/xui/de/panel_preferences_setup.xml b/indra/newview/skins/default/xui/de/panel_preferences_setup.xml
index ef7ac09dba..a8509cabac 100644
--- a/indra/newview/skins/default/xui/de/panel_preferences_setup.xml
+++ b/indra/newview/skins/default/xui/de/panel_preferences_setup.xml
@@ -36,12 +36,4 @@
Proxy-Einstellungen:
</text>
<button label="Proxy-Einstellungen ändern" label_selected="Durchsuchen" name="set_proxy" width="160"/>
- <text name="os_hibernation_mode">
- OS-Ruhezustand verhindern, wenn nicht abwesend:
- </text>
- <combo_box name="os_hibernation_combobox" tool_tip="Verhindert den OS-Ruhezustand nur, wenn der Benutzer nicht abwesend ist">
- <combo_box.item label="Ruhezustand möglich" name="can_hibernate"/>
- <combo_box.item label="Kein Ruhezustand, Bildschirm kann ausgeschaltet werden" name="cant_hibernate"/>
- <combo_box.item label="Kein Ruhezustand, Bildschirm kann nicht ausgeschaltet werden" name="cant_hibernate_screen"/>
- </combo_box>
</panel>
diff --git a/indra/newview/skins/default/xui/en/floater_add_payment_method.xml b/indra/newview/skins/default/xui/en/floater_add_payment_method.xml
deleted file mode 100644
index 18f5f2af32..0000000000
--- a/indra/newview/skins/default/xui/en/floater_add_payment_method.xml
+++ /dev/null
@@ -1,142 +0,0 @@
-<?xml version="1.0" encoding="utf-8" standalone="yes"?>
-<floater
- height="405"
- width="900"
- layout="topleft"
- name="floater_add_payment_method"
- single_instance="true"
- show_title="false"
- legacy_header_height="0"
- header_height="0"
- background_visible="false"
- can_resize="false"
- can_drag_on_left="false"
- can_minimize="false"
- can_close="false">
- <floater.string
- name="continue_url">
- https://secondlife.com/my/lindex/buy.php?associate_for_viewer=1
- </floater.string>
- <floater.string
- name="continue_staging_url">
- https://secondlife-staging.com/my/lindex/buy.php?associate_for_viewer=1
- </floater.string>
- <panel
- background_opaque="false"
- bg_alpha_color="PanelGray"
- border_visible="false"
- background_visible="true"
- label="wrapper_panel"
- layout="topleft"
- left="0"
- name="wrapper_panel"
- top="0"
- height="405"
- width="900"
- follows="all"
- translate="false">
- <text
- type="string"
- length="1"
- follows="top|left|right"
- font="SansSerifLargeBold"
- text_color="White"
- layout="topleft"
- left="0"
- height="14"
- top_pad="30"
- width="900"
- halign="center"
- name="title_txt">
- Add a payment method to buy Linden dollars and enjoy more of Second Life.
- </text>
- <button
- follows="top|left|right"
- height="24"
- label="Get started"
- layout="topleft"
- left="320"
- name="continue_btn"
- image_unselected="PushButton_Login"
- image_pressed="PushButton_Login_Pressed"
- image_hover_unselected="PushButton_Login_Over"
- label_color="White"
- top_pad ="15"
- pad_bottom="1"
- width="140"/>
- <button
- follows="top|left|right"
- height="24"
- label="Later"
- layout="topleft"
- left_pad="9"
- pad_bottom="1"
- name="close_btn"
- width="90"/>
- <icon
- height="195"
- width="260"
- image_name="add_payment_image_left"
- layout="topleft"
- left="30"
- name="image_left"
- top_pad="30"
- use_draw_context_alpha="false"/>
- <icon
- height="195"
- width="260"
- image_name="add_payment_image_center"
- layout="topleft"
- left_pad="30"
- name="image_center"
- use_draw_context_alpha="false"/>
- <icon
- height="195"
- width="260"
- image_name="add_payment_image_right"
- layout="topleft"
- left_pad="30"
- name="image_right"
- use_draw_context_alpha="false"/>
- <text
- type="string"
- length="1"
- follows="top|left"
- text_color="White"
- height="75"
- width="260"
- top_pad="15"
- layout="topleft"
- word_wrap="true"
- left="30"
- name="image_left_desc">
- Create an avatar you love. Choose from millions of items in stores and the Second Life Marketplace. Your style is here.
- </text>
- <text
- type="string"
- length="1"
- follows="top|left"
- text_color="White"
- height="75"
- width="260"
- layout="topleft"
- word_wrap="true"
- left_pad="30"
- name="image_center_desc">
- Donate to your favorite performers and hosts. Your support helps them create great experiences.
- </text>
- <text
- type="string"
- length="1"
- follows="top|left"
- text_color="White"
- height="75"
- width="260"
- layout="topleft"
- word_wrap="true"
- left_pad="30"
- name="image_right_desc">
- Make the home of your dreams. With millions of items available to purchase, you can make your retreat unique.
- </text>
- </panel>
-</floater>
diff --git a/indra/newview/skins/default/xui/en/menu_viewer.xml b/indra/newview/skins/default/xui/en/menu_viewer.xml
index 97be1f0bf6..8a8ca26acb 100644
--- a/indra/newview/skins/default/xui/en/menu_viewer.xml
+++ b/indra/newview/skins/default/xui/en/menu_viewer.xml
@@ -1374,7 +1374,7 @@ function="World.EnvPreset"
parameter="script_debug" />
</menu_item_check>
<menu_item_call
- label="Recompile Scripts (Mono)"
+ label="Recompile LSL Scripts (Mono)"
name="Mono">
<menu_item_call.on_click
function="Tools.SelectedScriptAction"
@@ -1383,7 +1383,7 @@ function="World.EnvPreset"
function="EditableSelectedMono" />
</menu_item_call>
<menu_item_call
- label="Recompile Scripts (LSL)"
+ label="Recompile LSL Scripts (Legacy LSL)"
name="LSL">
<menu_item_call.on_click
function="Tools.SelectedScriptAction"
@@ -1392,20 +1392,20 @@ function="World.EnvPreset"
function="EditableSelected" />
</menu_item_call>
<menu_item_call
- label="Recompile Scripts (Lua)"
- name="Lua">
+ label="Recompile LSL Scripts (LSL on Lua)"
+ name="LSL on Luau">
<menu_item_call.on_click
function="Tools.SelectedScriptAction"
- parameter="compile lua" />
+ parameter="compile lsl-luau" />
<menu_item_call.on_enable
function="EditableSelectedLua" />
</menu_item_call>
<menu_item_call
- label="Recompile Scripts (LSL on Luau)"
- name="LSL on Luau">
+ label="Recompile Lua Scripts"
+ name="Lua">
<menu_item_call.on_click
function="Tools.SelectedScriptAction"
- parameter="compile lsl-luau" />
+ parameter="compile lua" />
<menu_item_call.on_enable
function="EditableSelectedLua" />
</menu_item_call>
diff --git a/indra/newview/skins/default/xui/en/panel_inbox_inventory.xml b/indra/newview/skins/default/xui/en/panel_inbox_inventory.xml
index eeb37eddcf..80d5bed4a5 100644
--- a/indra/newview/skins/default/xui/en/panel_inbox_inventory.xml
+++ b/indra/newview/skins/default/xui/en/panel_inbox_inventory.xml
@@ -1,4 +1,5 @@
<?xml version="1.0" encoding="utf-8" standalone="yes"?>
+<!-- preinitialize_views="false" will require explicitly building views elsewhere anyway, see viewer #6348 -->
<inbox_inventory_panel
accepts_drag_and_drop="false"
name="inventory_inbox"
@@ -13,7 +14,7 @@
border="false"
bevel_style="none"
show_item_link_overlays="true"
- preinitialize_views="false"
+ preinitialize_views="true"
>
<scroll reserve_scroll_corner="false" />
</inbox_inventory_panel>
diff --git a/indra/newview/skins/default/xui/en/panel_preferences_setup.xml b/indra/newview/skins/default/xui/en/panel_preferences_setup.xml
index d5f2f69df7..836c6094c5 100644
--- a/indra/newview/skins/default/xui/en/panel_preferences_setup.xml
+++ b/indra/newview/skins/default/xui/en/panel_preferences_setup.xml
@@ -246,41 +246,4 @@
<button.commit_callback
function="Pref.Proxy" />
</button>
- <text
- type="string"
- length="1"
- follows="left|top"
- height="10"
- layout="topleft"
- left="30"
- name="os_hibernation_mode"
- mouse_opaque="false"
- top_pad="8"
- width="300">
- Prevent OS from hibernating if not Away:
- </text>
- <combo_box
- control_name="OSHibernationMode"
- follows="left|top"
- height="23"
- layout="topleft"
- left_delta="50"
- top_pad="10"
- name="os_hibernation_combobox"
- width="300"
- tool_tip="Only prevents OS from sleeping if user is not away">
- <combo_box.item
- label="Can hibernate"
- name="can_hibernate"
- value="0" />
- <combo_box.item
- label="Can't hibernate, can turn screen off"
- name="cant_hibernate"
- value="1" />
- <combo_box.item
- label="Can't hibernate, can't turn screen off"
- name="cant_hibernate_screen"
- value="2" />
- </combo_box>
</panel>
-
diff --git a/indra/newview/skins/default/xui/es/panel_preferences_setup.xml b/indra/newview/skins/default/xui/es/panel_preferences_setup.xml
index 27b3c407cf..36888850c4 100644
--- a/indra/newview/skins/default/xui/es/panel_preferences_setup.xml
+++ b/indra/newview/skins/default/xui/es/panel_preferences_setup.xml
@@ -36,12 +36,4 @@
Configuración de proxy:
</text>
<button label="Ajustar la configuración de proxy" label_selected="Elegir" name="set_proxy"/>
- <text name="os_hibernation_mode">
- Evitar que el sistema entre en hibernación si no está Ausente:
- </text>
- <combo_box name="os_hibernation_combobox" tool_tip="Solo evita que el sistema entre en suspensión si el usuario no está ausente">
- <combo_box.item label="Puede hibernar" name="can_hibernate"/>
- <combo_box.item label="No puede hibernar, puede apagar la pantalla" name="cant_hibernate"/>
- <combo_box.item label="No puede hibernar, no puede apagar la pantalla" name="cant_hibernate_screen"/>
- </combo_box>
</panel>
diff --git a/indra/newview/skins/default/xui/fr/panel_preferences_setup.xml b/indra/newview/skins/default/xui/fr/panel_preferences_setup.xml
index 60cbe93e6e..c010fa9e90 100644
--- a/indra/newview/skins/default/xui/fr/panel_preferences_setup.xml
+++ b/indra/newview/skins/default/xui/fr/panel_preferences_setup.xml
@@ -36,12 +36,4 @@
Paramètres de proxy :
</text>
<button label="Définir" label_selected="Parcourir" name="set_proxy"/>
- <text name="os_hibernation_mode">
- Empêcher l’OS d’hiberner si vous n’êtes pas absent :
- </text>
- <combo_box name="os_hibernation_combobox" tool_tip="Empêche seulement la mise en veille de l’OS si l’utilisateur n’est pas absent">
- <combo_box.item label="Peut hiberner" name="can_hibernate"/>
- <combo_box.item label="Ne peut pas hiberner, peut éteindre l’écran" name="cant_hibernate"/>
- <combo_box.item label="Ne peut pas hiberner, ne peut pas éteindre l’écran" name="cant_hibernate_screen"/>
- </combo_box>
</panel>
diff --git a/indra/newview/skins/default/xui/it/panel_preferences_setup.xml b/indra/newview/skins/default/xui/it/panel_preferences_setup.xml
index 95ecf05617..c9d90539e1 100644
--- a/indra/newview/skins/default/xui/it/panel_preferences_setup.xml
+++ b/indra/newview/skins/default/xui/it/panel_preferences_setup.xml
@@ -36,12 +36,4 @@
Impostazioni proxy:
</text>
<button label="Regola impostazioni proxy" label_selected="Sfoglia" name="set_proxy" width="160"/>
- <text name="os_hibernation_mode">
- Impedisci al sistema operativo di ibernarsi se non sei Assente:
- </text>
- <combo_box name="os_hibernation_combobox" tool_tip="Impedisce al sistema operativo di andare in sospensione solo se l'utente non è assente">
- <combo_box.item label="Può ibernarsi" name="can_hibernate"/>
- <combo_box.item label="Non può ibernarsi, può spegnere lo schermo" name="cant_hibernate"/>
- <combo_box.item label="Non può ibernarsi, non può spegnere lo schermo" name="cant_hibernate_screen"/>
- </combo_box>
</panel>
diff --git a/indra/newview/skins/default/xui/ja/floater_add_payment_method.xml b/indra/newview/skins/default/xui/ja/floater_add_payment_method.xml
deleted file mode 100644
index b420bf7d20..0000000000
--- a/indra/newview/skins/default/xui/ja/floater_add_payment_method.xml
+++ /dev/null
@@ -1,25 +0,0 @@
-<?xml version="1.0" encoding="utf-8" standalone="yes"?>
-<floater name="floater_add_payment_method">
- <floater.string name="continue_url">
- https://secondlife.com/my/lindex/buy.php?associate_for_viewer=1&amp;lang=ja-JP
- </floater.string>
- <panel label="wrapper_panel" name="wrapper_panel">
- <text name="title_txt">
- 支払い方法を追加してリンデンドルを購入し、Second Lifeをもっと楽しみましょう。
- </text>
- <button label="始めましょう" name="continue_btn"/>
- <button label="また後で"/>
- <icon name="image_left"/>
- <icon name="image_center"/>
- <icon name="image_right"/>
- <text name="image_left_desc">
- お気に入りのアバターを作成しましょう。ストアやSecond Lifeマーケットプレイスにある何百万ものアイテムからお選びください。あなただけのスタイルがここにあります。
- </text>
- <text name="image_center_desc">
- お気に入りのパフォーマーやホストに寄付をしましょう。あなたのサポートは、彼らが素晴らしい体験を生み出すのに役立ちます。
- </text>
- <text name="image_right_desc">
- あなたの夢の家を作りましょう。何百万ものアイテムを購入できるので、あなただけの住まいを作ることができます。
- </text>
- </panel>
-</floater>
diff --git a/indra/newview/skins/default/xui/ja/panel_preferences_setup.xml b/indra/newview/skins/default/xui/ja/panel_preferences_setup.xml
index 8940ca5bc9..959f827a61 100644
--- a/indra/newview/skins/default/xui/ja/panel_preferences_setup.xml
+++ b/indra/newview/skins/default/xui/ja/panel_preferences_setup.xml
@@ -36,12 +36,4 @@
プロキシ設定:
</text>
<button label="プロキシ設定を調整" label_selected="参照" name="set_proxy"/>
- <text name="os_hibernation_mode">
- 離席中でない場合、OS の休止状態を防止:
- </text>
- <combo_box name="os_hibernation_combobox" tool_tip="ユーザーが離席中でない場合にのみ、OS のスリープを防止します">
- <combo_box.item label="休止状態を許可" name="can_hibernate"/>
- <combo_box.item label="休止状態を禁止(画面オフは可)" name="cant_hibernate"/>
- <combo_box.item label="休止状態を禁止(画面オフも不可)" name="cant_hibernate_screen"/>
- </combo_box>
</panel>
diff --git a/indra/newview/skins/default/xui/pl/floater_add_payment_method.xml b/indra/newview/skins/default/xui/pl/floater_add_payment_method.xml
deleted file mode 100644
index 2ef78d3d35..0000000000
--- a/indra/newview/skins/default/xui/pl/floater_add_payment_method.xml
+++ /dev/null
@@ -1,19 +0,0 @@
-<?xml version="1.0" encoding="utf-8" standalone="yes" ?>
-<floater name="floater_add_payment_method">
- <panel name="wrapper_panel">
- <text name="title_txt">
- Dodaj metodę płatności, aby kupić Lindeny i lepiej doświadczać Second Life.
- </text>
- <button label="Rozpocznij" name="continue_btn" />
- <button label="Później" name="close_btn" />
- <text name="image_left_desc">
- Stwórz awatara, którego pokochasz. Wybieraj spośród milionów sklepów i Second Life Marketplace. Twój styl jest tutaj.
- </text>
- <text name="image_center_desc">
- Wspieraj swoich ulubionych wykonawców i gospodarzy. Pomożesz im tworzyć wspaniałe doświadczenia.
- </text>
- <text name="image_right_desc">
- Stwórz dom swoich marzeń. Miliony przedmiotów dostępnych do kupienia mogą uczynić Twoje gniazdko wyjątkowym.
- </text>
- </panel>
-</floater>
diff --git a/indra/newview/skins/default/xui/pl/panel_preferences_setup.xml b/indra/newview/skins/default/xui/pl/panel_preferences_setup.xml
index 7147f024cb..bd36f10509 100644
--- a/indra/newview/skins/default/xui/pl/panel_preferences_setup.xml
+++ b/indra/newview/skins/default/xui/pl/panel_preferences_setup.xml
@@ -33,12 +33,4 @@
Ustawienia proxy:
</text>
<button label="Dostosuj ustawienia" label_selected="Przeglądaj" name="set_proxy" />
- <text name="os_hibernation_mode">
- Zapobiegaj hibernacji systemu, gdy użytkownik nie jest AFK:
- </text>
- <combo_box name="os_hibernation_combobox" tool_tip="Zapobiega usypianiu systemu tylko wtedy, gdy użytkownik nie jest AFK">
- <combo_box.item label="Może hibernować" name="can_hibernate"/>
- <combo_box.item label="Nie może hibernować, może wyłączyć ekran" name="cant_hibernate"/>
- <combo_box.item label="Nie może hibernować, nie może wyłączyć ekranu" name="cant_hibernate_screen"/>
- </combo_box>
</panel>
diff --git a/indra/newview/skins/default/xui/pt/panel_preferences_setup.xml b/indra/newview/skins/default/xui/pt/panel_preferences_setup.xml
index 101f58488d..56fc225bc0 100644
--- a/indra/newview/skins/default/xui/pt/panel_preferences_setup.xml
+++ b/indra/newview/skins/default/xui/pt/panel_preferences_setup.xml
@@ -36,12 +36,4 @@
Configurações de proxy:
</text>
<button label="Ajustar configurações de proxy" label_selected="Procurar" name="set_proxy" width="180"/>
- <text name="os_hibernation_mode">
- Impedir que o sistema entre em hibernação se não estiver Ausente:
- </text>
- <combo_box name="os_hibernation_combobox" tool_tip="Só impede que o sistema entre em suspensão se o utilizador não estiver ausente">
- <combo_box.item label="Pode hibernar" name="can_hibernate"/>
- <combo_box.item label="Não pode hibernar, pode desligar o ecrã" name="cant_hibernate"/>
- <combo_box.item label="Não pode hibernar, não pode desligar o ecrã" name="cant_hibernate_screen"/>
- </combo_box>
</panel>
diff --git a/indra/newview/skins/default/xui/pt/settings_comments.xml b/indra/newview/skins/default/xui/pt/settings_comments.xml
index 22f6487847..63d68a3f5b 100644
--- a/indra/newview/skins/default/xui/pt/settings_comments.xml
+++ b/indra/newview/skins/default/xui/pt/settings_comments.xml
@@ -1435,8 +1435,6 @@
<string>Número de logins bem-sucedidos no Second Life.</string>
<key>NvAPICreateApplicationProfile</key>
<string>Criar perfil de aplicativo NVIDIA para configurações otimizadas.</string>
- <key>OSHibernationMode</key>
- <string>Se deve impedir que o SO hiberne. 0 - pode hibernar; 1 - não pode hibernar, pode desligar a tela; 2 - não pode hibernar, não pode desligar a tela.</string>
<key>ObjectCacheEnabled</key>
<string>Ativar o cache de objetos.</string>
<key>ObjectCostHighColor</key>
diff --git a/indra/newview/skins/default/xui/ru/panel_preferences_setup.xml b/indra/newview/skins/default/xui/ru/panel_preferences_setup.xml
index cec72ebe21..137e047d97 100644
--- a/indra/newview/skins/default/xui/ru/panel_preferences_setup.xml
+++ b/indra/newview/skins/default/xui/ru/panel_preferences_setup.xml
@@ -36,12 +36,4 @@
Настройки прокси-сервера:
</text>
<button label="Задать настройки" label_selected="Обзор" name="set_proxy"/>
- <text name="os_hibernation_mode">
- Предотвращать гибернацию ОС, если пользователь не отошел:
- </text>
- <combo_box name="os_hibernation_combobox" tool_tip="Предотвращает переход ОС в сон, только если пользователь не отошел">
- <combo_box.item label="Гибернация разрешена" name="can_hibernate"/>
- <combo_box.item label="Гибернация запрещена, экран можно выключать" name="cant_hibernate"/>
- <combo_box.item label="Гибернация запрещена, экран нельзя выключать" name="cant_hibernate_screen"/>
- </combo_box>
</panel>
diff --git a/indra/newview/skins/default/xui/tr/panel_preferences_setup.xml b/indra/newview/skins/default/xui/tr/panel_preferences_setup.xml
index a3d99ac01f..a20d0e5ff1 100644
--- a/indra/newview/skins/default/xui/tr/panel_preferences_setup.xml
+++ b/indra/newview/skins/default/xui/tr/panel_preferences_setup.xml
@@ -36,12 +36,4 @@
Proxy Ayarları:
</text>
<button label="Proxy ayarlarını yap" label_selected="Gözat" name="set_proxy"/>
- <text name="os_hibernation_mode">
- Uzakta değilse işletim sisteminin hazırda beklemesini engelle:
- </text>
- <combo_box name="os_hibernation_combobox" tool_tip="Yalnızca kullanıcı uzakta değilse işletim sisteminin uykuya geçmesini engeller">
- <combo_box.item label="Hazırda bekleyebilir" name="can_hibernate"/>
- <combo_box.item label="Hazırda bekleyemez, ekran kapanabilir" name="cant_hibernate"/>
- <combo_box.item label="Hazırda bekleyemez, ekran kapanamaz" name="cant_hibernate_screen"/>
- </combo_box>
</panel>
diff --git a/indra/newview/skins/default/xui/zh/panel_preferences_setup.xml b/indra/newview/skins/default/xui/zh/panel_preferences_setup.xml
index c83a7512e0..6ef25ad96e 100644
--- a/indra/newview/skins/default/xui/zh/panel_preferences_setup.xml
+++ b/indra/newview/skins/default/xui/zh/panel_preferences_setup.xml
@@ -36,12 +36,4 @@
代理伺服器設定:
</text>
<button label="調整代理伺服器設定" label_selected="瀏覽" name="set_proxy"/>
- <text name="os_hibernation_mode">
- 不處於離開狀態時阻止作業系統休眠:
- </text>
- <combo_box name="os_hibernation_combobox" tool_tip="僅在使用者不處於離開狀態時阻止作業系統睡眠">
- <combo_box.item label="可休眠" name="can_hibernate"/>
- <combo_box.item label="不可休眠,可關閉螢幕" name="cant_hibernate"/>
- <combo_box.item label="不可休眠,不可關閉螢幕" name="cant_hibernate_screen"/>
- </combo_box>
</panel>
diff --git a/indra/newview/tests/llscripteditorws_test.cpp b/indra/newview/tests/llscripteditorws_test.cpp
new file mode 100644
index 0000000000..4cf1bde034
--- /dev/null
+++ b/indra/newview/tests/llscripteditorws_test.cpp
@@ -0,0 +1,130 @@
+/**
+ * @file llscripteditorws_test.cpp
+ * @brief Headless test fixture for LLScriptEditorWSServer with fake connection
+ *
+ * $LicenseInfo:firstyear=2026&license=viewerlgpl$
+ * Second Life Viewer Source Code
+ * Copyright (C) 2026, 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 "../test/lltut.h"
+
+namespace
+{
+class FakeScriptEditorWSConnection : public LLScriptEditorWSConnection
+{
+public:
+ explicit FakeScriptEditorWSConnection(const LLWebsocketMgr::WSServer::ptr_t& server)
+ : LLScriptEditorWSConnection(server, LLWebsocketMgr::connection_h())
+ {
+ }
+
+ using LLJSONRPCConnection::processMessage;
+};
+
+class HeadlessScriptEditorWSServer : public LLScriptEditorWSServer
+{
+public:
+ HeadlessScriptEditorWSServer()
+ : LLScriptEditorWSServer("script_editor_ws_test_server", 0, true)
+ {
+ }
+
+ using LLScriptEditorWSServer::setupConnectionMethods;
+};
+}
+
+namespace tut
+{
+ struct script_editor_ws_test_data
+ {
+ std::shared_ptr<HeadlessScriptEditorWSServer> mServer;
+ std::shared_ptr<FakeScriptEditorWSConnection> mConnection;
+
+ script_editor_ws_test_data()
+ {
+ mServer = std::make_shared<HeadlessScriptEditorWSServer>();
+ mConnection = std::make_shared<FakeScriptEditorWSConnection>(mServer);
+ mServer->setupConnectionMethods(mConnection);
+ }
+ };
+
+ typedef test_group<script_editor_ws_test_data> script_editor_ws_test_group;
+ typedef script_editor_ws_test_group::object script_editor_ws_test_object;
+ script_editor_ws_test_group script_editor_ws_tests("LLScriptEditorWSServer");
+
+ template<> template<>
+ void script_editor_ws_test_object::test<1>()
+ {
+ set_test_name("headless fixture constructs server and fake connection");
+ ensure("server should be created", mServer != nullptr);
+ ensure("fake connection should be created", mConnection != nullptr);
+ }
+
+ template<> template<>
+ void script_editor_ws_test_object::test<2>()
+ {
+ set_test_name("fake connection can drive JSON-RPC notification dispatch");
+
+ bool called = false;
+ mConnection->registerMethod(
+ "fixture.echo",
+ [&](const std::string& method, const LLSD& id, const LLSD& params) -> LLSD
+ {
+ called = true;
+ ensure_equals("method should match", method, "fixture.echo");
+ ensure("notification id should be undefined", id.isUndefined());
+ ensure_equals("payload should be forwarded", params["value"].asString(), "ok");
+ return LLSD();
+ });
+
+ LLSD params;
+ params["value"] = "ok";
+ LLSD notification = LLJSONRPCConnection::makeEnvelope(
+ LLSD(),
+ "fixture.echo",
+ params,
+ LLSD(),
+ LLSD());
+
+ mConnection->processMessage(notification);
+ ensure("fixture handler should be invoked", called);
+ }
+
+ template<> template<>
+ void script_editor_ws_test_object::test<3>()
+ {
+ set_test_name("fixture includes default session.ping handler");
+
+ LLSD params;
+ params["timestamp"] = 123;
+ LLSD notification = LLJSONRPCConnection::makeEnvelope(
+ LLSD(),
+ "system.ping",
+ params,
+ LLSD(),
+ LLSD());
+
+ mConnection->processMessage(notification);
+ ensure("session.ping dispatch should complete in headless mode", true);
+ }
+}
diff --git a/indra/newview/viewer_manifest.py b/indra/newview/viewer_manifest.py
index 326b3adaaf..f451b28475 100755
--- a/indra/newview/viewer_manifest.py
+++ b/indra/newview/viewer_manifest.py
@@ -143,6 +143,9 @@ class ViewerManifest(LLManifest):
"settings_install.xml",
src="environment")
+ # Include the full Luau license in the packaged viewer.
+ with self.prefix(src=pkgdir):
+ self.path("LICENSES/luau.txt")
with self.prefix(src_dst="character"):
self.path("*.llm")
diff --git a/scripts/packages-formatter.py b/scripts/packages-formatter.py
index 5d31702e76..340792158e 100755
--- a/scripts/packages-formatter.py
+++ b/scripts/packages-formatter.py
@@ -122,6 +122,10 @@ if any(pkgs for pkgs in list(dups.values())):
print("%s %s" % (args.channel, args.version))
print(viewer_copyright)
+# Luau is server-side and isn't listed in the viewer's autobuild packages.
+with open(os.path.join(os.path.dirname(__file__), "..", "indra", "newview", "luau.txt"), encoding="utf-8") as luau_license:
+ print("Luau " + " / ".join(line.strip() for line in luau_license if line.startswith("Copyright")))
+print()
version = list(info['versions'].items())
version.sort()
for pkg, pkg_version in version: