summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.github/labeler.yaml3
-rw-r--r--autobuild.xml62
-rw-r--r--indra/cmake/CMakeLists.txt1
-rw-r--r--indra/cmake/Lualibs.cmake29
-rwxr-xr-xindra/cmake/run_build_test.py10
-rw-r--r--indra/llcommon/CMakeLists.txt4
-rwxr-xr-x[-rw-r--r--]indra/llcommon/hexdump.h (renamed from indra/test/hexdump.h)13
-rw-r--r--indra/llcommon/lleventdispatcher.h2
-rw-r--r--indra/llcommon/llevents.cpp17
-rw-r--r--indra/llcommon/llexception.h1
-rw-r--r--indra/llcommon/llformat.h2
-rw-r--r--indra/llcommon/llleap.cpp61
-rw-r--r--indra/llcommon/llleaplistener.cpp37
-rw-r--r--indra/llcommon/llleaplistener.h27
-rw-r--r--indra/llcommon/llrefcount.h1
-rw-r--r--indra/llcommon/llstring.h40
-rw-r--r--indra/llcommon/lua_function.cpp801
-rw-r--r--indra/llcommon/lua_function.h256
-rw-r--r--indra/llcommon/lualistener.cpp91
-rw-r--r--indra/llcommon/lualistener.h78
-rw-r--r--indra/llcommon/stringize.h6
-rw-r--r--indra/llcommon/tests/StringVec.h10
-rw-r--r--indra/llcommon/tests/lleventcoro_test.cpp95
-rw-r--r--indra/llcommon/tests/lleventdispatcher_test.cpp2
-rw-r--r--indra/llcommon/tests/lleventfilter_test.cpp2
-rw-r--r--indra/llcommon/tests/llleap_test.cpp2
-rw-r--r--indra/llcommon/tests/llsdserialize_test.cpp10
-rw-r--r--indra/llcommon/tests/threadsafeschedule_test.cpp4
-rw-r--r--indra/llfilesystem/lldir.cpp5
-rw-r--r--indra/llfilesystem/lldir.h1
-rw-r--r--indra/llprimitive/tests/llgltfmaterial_test.cpp2
-rw-r--r--indra/llui/llmenugl.h16
-rw-r--r--indra/llui/lltexteditor.cpp3
-rw-r--r--indra/llui/lltexteditor.h16
-rw-r--r--indra/newview/CMakeLists.txt32
-rw-r--r--indra/newview/app_settings/cmd_line.xml28
-rw-r--r--indra/newview/app_settings/settings.xml35
-rw-r--r--indra/newview/llappviewer.cpp60
-rw-r--r--indra/newview/llfilepicker.cpp5
-rw-r--r--indra/newview/llfilepicker.h1
-rw-r--r--indra/newview/llfloaterluadebug.cpp154
-rw-r--r--indra/newview/llfloaterluadebug.h72
-rw-r--r--indra/newview/llinventoryfunctions.cpp13
-rw-r--r--indra/newview/llinventoryfunctions.h16
-rw-r--r--indra/newview/llinventorymodel.cpp11
-rw-r--r--indra/newview/llluamanager.cpp546
-rw-r--r--indra/newview/llluamanager.h104
-rw-r--r--indra/newview/llstartup.cpp3
-rw-r--r--indra/newview/lltoolplacer.cpp8
-rw-r--r--indra/newview/lltoolplacer.h8
-rw-r--r--indra/newview/lluilistener.cpp2
-rw-r--r--indra/newview/lluilistener.h3
-rw-r--r--indra/newview/llviewerfloaterreg.cpp3
-rw-r--r--indra/newview/llviewermenu.cpp147
-rw-r--r--indra/newview/llviewermenu.h1
-rw-r--r--indra/newview/llviewermenufile.cpp18
-rw-r--r--indra/newview/llviewermenufile.h2
-rw-r--r--indra/newview/scripts/lua/Queue.lua40
-rw-r--r--indra/newview/scripts/lua/testmod.lua2
-rw-r--r--indra/newview/skins/default/xui/en/floater_lua_debug.xml117
-rw-r--r--indra/newview/skins/default/xui/en/menu_viewer.xml11
-rw-r--r--indra/newview/tests/llluamanager_test.cpp307
-rwxr-xr-xindra/newview/viewer_manifest.py3
-rw-r--r--indra/test/debug.h27
-rw-r--r--indra/test/io.cpp40
-rw-r--r--indra/test/lltut.h2
-rw-r--r--indra/test/print.h4
67 files changed, 3230 insertions, 305 deletions
diff --git a/.github/labeler.yaml b/.github/labeler.yaml
index d31a361baf..6e03801b65 100644
--- a/.github/labeler.yaml
+++ b/.github/labeler.yaml
@@ -76,3 +76,6 @@ c/cpp:
- '**/*.i'
- '**/*.inl'
- '**/*.y'
+
+'team:viewer':
+ - '*'
diff --git a/autobuild.xml b/autobuild.xml
index effafa31d3..e40f764ab0 100644
--- a/autobuild.xml
+++ b/autobuild.xml
@@ -1728,6 +1728,66 @@
<key>name</key>
<string>llphysicsextensions_tpv</string>
</map>
+ <key>luau</key>
+ <map>
+ <key>platforms</key>
+ <map>
+ <key>darwin64</key>
+ <map>
+ <key>archive</key>
+ <map>
+ <key>hash</key>
+ <string>59bf3d96f9df4b6981c406abac5c46ae276f9b15</string>
+ <key>hash_algorithm</key>
+ <string>sha1</string>
+ <key>url</key>
+ <string>https://github.com/secondlife/3p-luau/releases/download/v0.609-9567db9/luau-0.609-9567db9-darwin64-9567db9.tar.zst</string>
+ </map>
+ <key>name</key>
+ <string>darwin64</string>
+ </map>
+ <key>linux64</key>
+ <map>
+ <key>archive</key>
+ <map>
+ <key>hash</key>
+ <string>549516ada483ddf276183f66b0a7c3d01e4ef1aa</string>
+ <key>hash_algorithm</key>
+ <string>sha1</string>
+ <key>url</key>
+ <string>https://github.com/secondlife/3p-luau/releases/download/v0.609-9567db9/luau-0.609-9567db9-linux64-9567db9.tar.zst</string>
+ </map>
+ <key>name</key>
+ <string>linux64</string>
+ </map>
+ <key>windows64</key>
+ <map>
+ <key>archive</key>
+ <map>
+ <key>hash</key>
+ <string>43e2cc2e6e94299f89655435002864925b640e16</string>
+ <key>hash_algorithm</key>
+ <string>sha1</string>
+ <key>url</key>
+ <string>https://github.com/secondlife/3p-luau/releases/download/v0.609-9567db9/luau-0.609-9567db9-windows64-9567db9.tar.zst</string>
+ </map>
+ <key>name</key>
+ <string>windows64</string>
+ </map>
+ </map>
+ <key>license</key>
+ <string>MIT</string>
+ <key>license_file</key>
+ <string>LICENSES/luau.txt</string>
+ <key>copyright</key>
+ <string>Copyright (c) 2019-2023 Roblox Corporation, Copyright (c) 1994–2019 Lua.org, PUC-Rio.</string>
+ <key>version</key>
+ <string>0.609-9567db9</string>
+ <key>name</key>
+ <string>luau</string>
+ <key>description</key>
+ <string>Luau is a fast, small, safe, gradually typed embeddable scripting language derived from Lua.</string>
+ </map>
<key>mesa</key>
<map>
<key>platforms</key>
@@ -2585,8 +2645,6 @@ Copyright (c) 2012, 2014, 2015, 2016 nghttp2 contributors</string>
<string>https://bitbucket.org/lindenlab/3p-tracy</string>
<key>source_type</key>
<string>git</string>
- <key>version</key>
- <string>v0.8.1.578241</string>
</map>
<key>tut</key>
<map>
diff --git a/indra/cmake/CMakeLists.txt b/indra/cmake/CMakeLists.txt
index 05c51c018d..45c56f2d2c 100644
--- a/indra/cmake/CMakeLists.txt
+++ b/indra/cmake/CMakeLists.txt
@@ -44,6 +44,7 @@ set(cmake_SOURCE_FILES
LLTestCommand.cmake
LLWindow.cmake
Linking.cmake
+ Lualibs.cmake
Meshoptimizer.cmake
NDOF.cmake
OPENAL.cmake
diff --git a/indra/cmake/Lualibs.cmake b/indra/cmake/Lualibs.cmake
new file mode 100644
index 0000000000..7edfbfdc3d
--- /dev/null
+++ b/indra/cmake/Lualibs.cmake
@@ -0,0 +1,29 @@
+# -*- cmake -*-
+
+include_guard()
+
+include(Prebuilt)
+
+add_library( ll::lualibs INTERFACE IMPORTED )
+
+use_system_binary( lualibs )
+
+use_prebuilt_binary(luau)
+
+target_include_directories( ll::lualibs SYSTEM INTERFACE
+ ${LIBS_PREBUILT_DIR}/include
+)
+
+if (WINDOWS)
+ target_link_libraries(ll::lualibs INTERFACE ${ARCH_PREBUILT_DIRS_RELEASE}/Luau.Ast.lib)
+ target_link_libraries(ll::lualibs INTERFACE ${ARCH_PREBUILT_DIRS_RELEASE}/Luau.CodeGen.lib)
+ target_link_libraries(ll::lualibs INTERFACE ${ARCH_PREBUILT_DIRS_RELEASE}/Luau.Compiler.lib)
+ target_link_libraries(ll::lualibs INTERFACE ${ARCH_PREBUILT_DIRS_RELEASE}/Luau.Config.lib)
+ target_link_libraries(ll::lualibs INTERFACE ${ARCH_PREBUILT_DIRS_RELEASE}/Luau.VM.lib)
+elseif (DARWIN)
+ target_link_libraries(ll::lualibs INTERFACE ${ARCH_PREBUILT_DIRS_RELEASE}/libLuau.Ast.a)
+ target_link_libraries(ll::lualibs INTERFACE ${ARCH_PREBUILT_DIRS_RELEASE}/libLuau.CodeGen.a)
+ target_link_libraries(ll::lualibs INTERFACE ${ARCH_PREBUILT_DIRS_RELEASE}/libLuau.Compiler.a)
+ target_link_libraries(ll::lualibs INTERFACE ${ARCH_PREBUILT_DIRS_RELEASE}/libLuau.Config.a)
+ target_link_libraries(ll::lualibs INTERFACE ${ARCH_PREBUILT_DIRS_RELEASE}/libLuau.VM.a)
+endif (WINDOWS)
diff --git a/indra/cmake/run_build_test.py b/indra/cmake/run_build_test.py
index 1f040bded5..ef4d712254 100755
--- a/indra/cmake/run_build_test.py
+++ b/indra/cmake/run_build_test.py
@@ -122,19 +122,17 @@ def main(command, arguments=[], libpath=[], vars={}):
# Make sure we see all relevant output *before* child-process output.
sys.stdout.flush()
try:
- return subprocess.call(command_list)
- except OSError as err:
+ return subprocess.run(command_list).returncode
+ except FileNotFoundError as err:
# If the caller is trying to execute a test program that doesn't
# exist, we want to produce a reasonable error message rather than a
# traceback. This happens when the build is halted by errors, but
# CMake tries to proceed with testing anyway <eyeroll/>. However, do
# NOT attempt to handle any error but "doesn't exist."
- if err.errno != errno.ENOENT:
- raise
# In practice, the pathnames into CMake's build tree are so long as to
# obscure the name of the test program. Just log its basename.
- log.warn("No such program %s; check for preceding build errors" % \
- os.path.basename(command[0]))
+ log.warning("No such program %s; check for preceding build errors" %
+ os.path.basename(command[0]))
# What rc should we simulate for missing executable? Windows produces
# 9009.
return 9009
diff --git a/indra/llcommon/CMakeLists.txt b/indra/llcommon/CMakeLists.txt
index c947184dc8..aed9ee080b 100644
--- a/indra/llcommon/CMakeLists.txt
+++ b/indra/llcommon/CMakeLists.txt
@@ -108,6 +108,8 @@ set(llcommon_SOURCE_FILES
lluriparser.cpp
lluuid.cpp
llworkerthread.cpp
+ lua_function.cpp
+ lualistener.cpp
hbxxh.cpp
u64.cpp
threadpool.cpp
@@ -250,6 +252,8 @@ set(llcommon_HEADER_FILES
llwin32headers.h
llwin32headerslean.h
llworkerthread.h
+ lua_function.h
+ lualistener.h
hbxxh.h
lockstatic.h
stdtypes.h
diff --git a/indra/test/hexdump.h b/indra/llcommon/hexdump.h
index dd7cbaaa3c..234168cd61 100644..100755
--- a/indra/test/hexdump.h
+++ b/indra/llcommon/hexdump.h
@@ -1,8 +1,8 @@
/**
* @file hexdump.h
* @author Nat Goodspeed
- * @date 2023-09-08
- * @brief Provide hexdump() and hexmix() ostream formatters
+ * @date 2023-10-03
+ * @brief iostream manipulators to stream hex, or string with nonprinting chars
*
* $LicenseInfo:firstyear=2023&license=viewerlgpl$
* Copyright (c) 2023, Linden Research, Inc.
@@ -17,6 +17,9 @@
#include <iostream>
#include <string_view>
+namespace LL
+{
+
// Format a given byte string as 2-digit hex values, no separators
// Usage: std::cout << hexdump(somestring) << ...
class hexdump
@@ -30,6 +33,10 @@ public:
hexdump(reinterpret_cast<const unsigned char*>(data), len)
{}
+ hexdump(const std::vector<unsigned char>& data):
+ hexdump(data.data(), data.size())
+ {}
+
hexdump(const unsigned char* data, size_t len):
mData(data, data + len)
{}
@@ -94,4 +101,6 @@ private:
std::string mData;
};
+} // namespace LL
+
#endif /* ! defined(LL_HEXDUMP_H) */
diff --git a/indra/llcommon/lleventdispatcher.h b/indra/llcommon/lleventdispatcher.h
index a82bc7a69b..35bf9075a0 100644
--- a/indra/llcommon/lleventdispatcher.h
+++ b/indra/llcommon/lleventdispatcher.h
@@ -851,6 +851,8 @@ public:
ARGS&&... args);
virtual ~LLDispatchListener() {}
+ std::string getPumpName() const { return getName(); }
+
private:
bool process(const LLSD& event) const;
void call_one(const LLSD& name, const LLSD& event) const;
diff --git a/indra/llcommon/llevents.cpp b/indra/llcommon/llevents.cpp
index 1a305ec3dc..01bba7a620 100644
--- a/indra/llcommon/llevents.cpp
+++ b/indra/llcommon/llevents.cpp
@@ -43,6 +43,7 @@
#include <typeinfo>
#include <cmath>
#include <cctype>
+#include <iomanip> // std::quoted
// external library headers
#include <boost/range/iterator_range.hpp>
#if LL_WINDOWS
@@ -54,10 +55,11 @@
#pragma warning (pop)
#endif
// other Linden headers
-#include "stringize.h"
#include "llerror.h"
-#include "llsdutil.h"
+#include "lleventfilter.h"
#include "llexception.h"
+#include "llsdutil.h"
+#include "stringize.h"
#if LL_MSVC
#pragma warning (disable : 4702)
#endif
@@ -71,7 +73,9 @@ LLEventPumps::LLEventPumps():
{ "LLEventStream", [](const std::string& name, bool tweak, const std::string& /*type*/)
{ return new LLEventStream(name, tweak); } },
{ "LLEventMailDrop", [](const std::string& name, bool tweak, const std::string& /*type*/)
- { return new LLEventMailDrop(name, tweak); } }
+ { return new LLEventMailDrop(name, tweak); } },
+ { "LLEventLogProxy", [](const std::string& name, bool tweak, const std::string& /*type*/)
+ { return new LLEventLogProxyFor<LLEventStream>(name, tweak); } }
},
mTypes
{
@@ -186,8 +190,13 @@ bool LLEventPumps::post(const std::string&name, const LLSD&message)
PumpMap::iterator found = mPumpMap.find(name);
if (found == mPumpMap.end())
+ {
+ LL_DEBUGS("LLEventPumps") << "LLEventPump(" << std::quoted(name) << ") not found"
+ << LL_ENDL;
return false;
+ }
+// LL_DEBUGS("LLEventPumps") << "posting to " << name << ": " << message << LL_ENDL;
return (*found).second->post(message);
}
@@ -402,7 +411,7 @@ LLBoundListener LLEventPump::listen_impl(const std::string& name, const LLEventL
{
if (!mSignal)
{
- LL_WARNS() << "Can't connect listener" << LL_ENDL;
+ LL_WARNS("LLEventPump") << "Can't connect listener" << LL_ENDL;
// connect will fail, return dummy
return LLBoundListener();
}
diff --git a/indra/llcommon/llexception.h b/indra/llcommon/llexception.h
index 375bea4a57..9e322db86d 100644
--- a/indra/llcommon/llexception.h
+++ b/indra/llcommon/llexception.h
@@ -12,6 +12,7 @@
#if ! defined(LL_LLEXCEPTION_H)
#define LL_LLEXCEPTION_H
+#include "stdtypes.h"
#include <stdexcept>
#include <boost/exception/exception.hpp>
#include <boost/throw_exception.hpp>
diff --git a/indra/llcommon/llformat.h b/indra/llcommon/llformat.h
index fb8e7cd045..97ea3b7b78 100644
--- a/indra/llcommon/llformat.h
+++ b/indra/llcommon/llformat.h
@@ -28,6 +28,8 @@
#ifndef LL_LLFORMAT_H
#define LL_LLFORMAT_H
+#include "llpreprocessor.h"
+
// Use as follows:
// LL_INFOS() << llformat("Test:%d (%.2f %.2f)", idx, x, y) << LL_ENDL;
//
diff --git a/indra/llcommon/llleap.cpp b/indra/llcommon/llleap.cpp
index 8f88e728ce..f7f2981638 100644
--- a/indra/llcommon/llleap.cpp
+++ b/indra/llcommon/llleap.cpp
@@ -14,13 +14,11 @@
// associated header
#include "llleap.h"
// STL headers
-#include <sstream>
#include <algorithm>
+#include <memory>
+#include <sstream>
// std headers
// external library headers
-#include <boost/bind.hpp>
-#include <boost/scoped_ptr.hpp>
-#include <boost/tokenizer.hpp>
// other Linden headers
#include "llerror.h"
#include "llstring.h"
@@ -53,18 +51,19 @@ public:
// We expect multiple LLLeapImpl instances. Definitely tweak
// mDonePump's name for uniqueness.
mDonePump("LLLeap", true),
- // Troubling thought: what if one plugin intentionally messes with
- // another plugin? LLEventPump names are in a single global namespace.
- // Try to make that more difficult by generating a UUID for the reply-
- // pump name -- so it should NOT need tweaking for uniqueness.
- mReplyPump(LLUUID::generateNewID().asString()),
mExpect(0),
// Instantiate a distinct LLLeapListener for this plugin. (Every
// plugin will want its own collection of managed listeners, etc.)
- // Pass it a callback to our connect() method, so it can send events
+ // Pass it our wstdin() method as its callback, so it can send events
// from a particular LLEventPump to the plugin without having to know
// this class or method name.
- mListener(new LLLeapListener(boost::bind(&LLLeapImpl::connect, this, _1, _2)))
+ mListener(
+ new LLLeapListener(
+ "LLLeap",
+ // Serialize any reply event to our child's stdin, suitably
+ // enriched with the pump name on which it was received.
+ [this](const std::string& pump, const LLSD& data)
+ { return wstdin(pump, data); }))
{
// Rule out unpopulated Params block
if (! cparams.executable.isProvided())
@@ -93,7 +92,7 @@ public:
}
// Listen for child "termination" right away to catch launch errors.
- mDonePump.listen("LLLeap", boost::bind(&LLLeapImpl::bad_launch, this, _1));
+ mDonePump.listen("LLLeap", [this](const LLSD& data){ return bad_launch(data); });
// Okay, launch child.
// Get a modifiable copy of params block to set files and postend.
@@ -113,7 +112,7 @@ public:
// Okay, launch apparently worked. Change our mDonePump listener.
mDonePump.stopListening("LLLeap");
- mDonePump.listen("LLLeap", boost::bind(&LLLeapImpl::done, this, _1));
+ mDonePump.listen("LLLeap", [this](const LLSD& data){ return done(data); });
// Child might pump large volumes of data through either stdout or
// stderr. Don't bother copying all that data into notification event.
@@ -123,17 +122,14 @@ public:
childout.setLimit(20);
childerr.setLimit(20);
- // Serialize any event received on mReplyPump to our child's stdin.
- mStdinConnection = connect(mReplyPump, "LLLeap");
-
// Listening on stdout is stateful. In general, we're either waiting
// for the length prefix or waiting for the specified length of data.
// We address that with two different listener methods -- one of which
// is blocked at any given time.
mStdoutConnection = childout.getPump()
- .listen("prefix", boost::bind(&LLLeapImpl::rstdout, this, _1));
+ .listen("prefix", [this](const LLSD& data){ return rstdout(data); });
mStdoutDataConnection = childout.getPump()
- .listen("data", boost::bind(&LLLeapImpl::rstdoutData, this, _1));
+ .listen("data", [this](const LLSD& data){ return rstdoutData(data); });
mBlocker.reset(new LLEventPump::Blocker(mStdoutDataConnection));
// Log anything sent up through stderr. When a typical program
@@ -142,7 +138,7 @@ public:
// interpreter behaves that way. More generally, though, a plugin
// author can log whatever s/he wants to the viewer log using stderr.
mStderrConnection = childerr.getPump()
- .listen("LLLeap", boost::bind(&LLLeapImpl::rstderr, this, _1));
+ .listen("LLLeap", [this](const LLSD& data){ return rstderr(data); });
// For our lifespan, intercept any LL_ERRS so we can notify plugin
mRecorder = LLError::addGenericRecorder(
@@ -151,7 +147,7 @@ public:
// Send child a preliminary event reporting our own reply-pump name --
// which would otherwise be pretty tricky to guess!
- wstdin(mReplyPump.getName(),
+ wstdin(mListener->getReplyPump().getName(),
LLSDMap
("command", mListener->getName())
// Include LLLeap features -- this may be important for child to
@@ -198,7 +194,7 @@ public:
return false;
}
- // Listener for events on mReplyPump: send to child stdin
+ // Listener for reply events: send to child stdin
bool wstdin(const std::string& pump, const LLSD& data)
{
LLSD packet(LLSDMap("pump", pump)("data", data));
@@ -355,7 +351,7 @@ public:
// request, send a reply. We happen to know who originated
// this request, and the reply LLEventPump of interest.
// Not our problem if the plugin ignores the reply event.
- data["reply"] = mReplyPump.getName();
+ data["reply"] = mListener->getReplyPump().getName();
sendReply(llsd::map("error",
stringize(LLError::Log::classname(err), ": ", err.what())),
data);
@@ -428,7 +424,7 @@ public:
LLSD event;
event["type"] = "error";
event["error"] = error;
- mReplyPump.post(event);
+ mListener->getReplyPump().post(event);
// All the above really accomplished was to buffer the serialized
// event in our WritePipe. Have to pump mainloop a couple times to
@@ -445,27 +441,14 @@ public:
}
private:
- /// We always want to listen on mReplyPump with wstdin(); under some
- /// circumstances we'll also echo other LLEventPumps to the plugin.
- LLBoundListener connect(LLEventPump& pump, const std::string& listener)
- {
- // Serialize any event received on the specified LLEventPump to our
- // child's stdin, suitably enriched with the pump name on which it was
- // received.
- return pump.listen(listener,
- boost::bind(&LLLeapImpl::wstdin, this, pump.getName(), _1));
- }
-
std::string mDesc;
LLEventStream mDonePump;
- LLEventStream mReplyPump;
LLProcessPtr mChild;
- LLTempBoundListener
- mStdinConnection, mStdoutConnection, mStdoutDataConnection, mStderrConnection;
- boost::scoped_ptr<LLEventPump::Blocker> mBlocker;
+ LLTempBoundListener mStdoutConnection, mStdoutDataConnection, mStderrConnection;
+ std::unique_ptr<LLEventPump::Blocker> mBlocker;
LLProcess::ReadPipe::size_type mExpect;
LLError::RecorderPtr mRecorder;
- boost::scoped_ptr<LLLeapListener> mListener;
+ std::unique_ptr<LLLeapListener> mListener;
};
// These must follow the declaration of LLLeapImpl, so they may as well be last.
diff --git a/indra/llcommon/llleaplistener.cpp b/indra/llcommon/llleaplistener.cpp
index 471f52e91c..55e4752c5d 100644
--- a/indra/llcommon/llleaplistener.cpp
+++ b/indra/llcommon/llleaplistener.cpp
@@ -54,12 +54,19 @@
return features;
}
-LLLeapListener::LLLeapListener(const ConnectFunc& connect):
+LLLeapListener::LLLeapListener(const std::string_view& caller, const Callback& callback):
// Each LEAP plugin has an instance of this listener. Make the command
// pump name difficult for other such plugins to guess.
LLEventAPI(LLUUID::generateNewID().asString(),
"Operations relating to the LLSD Event API Plugin (LEAP) protocol"),
- mConnect(connect)
+ mCaller(caller),
+ mCallback(callback),
+ // Troubling thought: what if one plugin intentionally messes with
+ // another plugin? LLEventPump names are in a single global namespace.
+ // Try to make that more difficult by generating a UUID for the reply-
+ // pump name -- so it should NOT need tweaking for uniqueness.
+ mReplyPump(LLUUID::generateNewID().asString()),
+ mReplyConn(connect(mReplyPump, mCaller))
{
LLSD need_name(LLSDMap("name", LLSD()));
add("newpump",
@@ -133,8 +140,7 @@ void LLLeapListener::newpump(const LLSD& request)
reply["name"] = name;
// Now listen on this new pump with our plugin listener
- std::string myname("llleap");
- saveListener(name, myname, mConnect(new_pump, myname));
+ saveListener(name, mCaller, connect(new_pump, mCaller));
}
catch (const LLEventPumps::BadType& error)
{
@@ -170,7 +176,7 @@ void LLLeapListener::listen(const LLSD& request)
{
// "dest" unspecified means to direct events on "source"
// to our plugin listener.
- saveListener(source_name, listener_name, mConnect(source, listener_name));
+ saveListener(source_name, listener_name, connect(source, listener_name));
}
reply["status"] = true;
}
@@ -292,10 +298,27 @@ void LLLeapListener::getFeature(const LLSD& request) const
}
}
+LLBoundListener LLLeapListener::connect(LLEventPump& pump, const std::string& listener)
+{
+ // Connect to source pump with an adapter that calls our callback with the
+ // pump name as well as the event data.
+ return pump.listen(
+ listener,
+ [callback=mCallback, pump=pump.getName()]
+ (const LLSD& data)
+ { return callback(pump, data); });
+}
+
void LLLeapListener::saveListener(const std::string& pump_name,
const std::string& listener_name,
const LLBoundListener& listener)
{
- mListeners.insert(ListenersMap::value_type(ListenersMap::key_type(pump_name, listener_name),
- listener));
+ // Don't use insert() or emplace() because, if this (pump_name,
+ // listener_name) pair is already in mListeners, we *want* to overwrite it.
+ auto& listener_entry{ mListeners[ListenersMap::key_type(pump_name, listener_name)] };
+ // If we already stored a connection for this pump and listener name,
+ // disconnect it before overwriting it. But if this entry was newly
+ // created, disconnect() will be a no-op.
+ listener_entry.disconnect();
+ listener_entry = listener;
}
diff --git a/indra/llcommon/llleaplistener.h b/indra/llcommon/llleaplistener.h
index 0ca5893657..040fb737b7 100644
--- a/indra/llcommon/llleaplistener.h
+++ b/indra/llcommon/llleaplistener.h
@@ -13,10 +13,9 @@
#define LL_LLLEAPLISTENER_H
#include "lleventapi.h"
+#include <functional>
#include <map>
#include <string>
-#include <boost/function.hpp>
-#include <boost/ptr_container/ptr_map.hpp>
/// Listener class implementing LLLeap query/control operations.
/// See https://jira.lindenlab.com/jira/browse/DEV-31978.
@@ -24,18 +23,16 @@ class LLLeapListener: public LLEventAPI
{
public:
/**
- * Decouple LLLeap by dependency injection. Certain LLLeapListener
- * operations must be able to cause LLLeap to listen on a specified
- * LLEventPump with the LLLeap listener that wraps incoming events in an
- * outer (pump=, data=) map and forwards them to the plugin. Very well,
- * define the signature for a function that will perform that, and make
- * our constructor accept such a function.
+ * Certain LLLeapListener operations listen on a specified LLEventPump.
+ * Accept a bool(pump, data) callback from our caller for when any such
+ * event is received.
*/
- typedef boost::function<LLBoundListener(LLEventPump&, const std::string& listener)>
- ConnectFunc;
- LLLeapListener(const ConnectFunc& connect);
+ using Callback = std::function<bool(const std::string& pump, const LLSD& data)>;
+ LLLeapListener(const std::string_view& caller, const Callback& callback);
~LLLeapListener();
+ LLEventPump& getReplyPump() { return mReplyPump; }
+
static LLSD getFeatures();
private:
@@ -48,10 +45,16 @@ private:
void getFeatures(const LLSD&) const;
void getFeature(const LLSD&) const;
+ LLBoundListener connect(LLEventPump& pump, const std::string& listener);
void saveListener(const std::string& pump_name, const std::string& listener_name,
const LLBoundListener& listener);
- ConnectFunc mConnect;
+ // The relative order of these next declarations is important because the
+ // constructor will initialize in this order.
+ std::string mCaller;
+ Callback mCallback;
+ LLEventStream mReplyPump;
+ LLTempBoundListener mReplyConn;
// In theory, listen() could simply call the relevant LLEventPump's
// listen() method, stoplistening() likewise. Lifespan issues make us
diff --git a/indra/llcommon/llrefcount.h b/indra/llcommon/llrefcount.h
index 2080da1565..5f7e668c7f 100644
--- a/indra/llcommon/llrefcount.h
+++ b/indra/llcommon/llrefcount.h
@@ -29,6 +29,7 @@
#include <boost/noncopyable.hpp>
#include <boost/intrusive_ptr.hpp>
#include "llatomic.h"
+#include "llerror.h"
class LLMutex;
diff --git a/indra/llcommon/llstring.h b/indra/llcommon/llstring.h
index bfbf25d9ab..0eb2770004 100644
--- a/indra/llcommon/llstring.h
+++ b/indra/llcommon/llstring.h
@@ -38,6 +38,7 @@
#include <vector>
#include <map>
#include "llformat.h"
+#include "stdtypes.h"
#if LL_LINUX
#include <wctype.h>
@@ -519,9 +520,36 @@ struct ll_convert_impl
TO operator()(const FROM& in) const;
};
-// Use a function template to get the nice ll_convert<TO>(from_value) API.
+/**
+ * somefunction(ll_convert(data))
+ * target = ll_convert(data)
+ * totype otherfunc(const fromtype& data)
+ * {
+ * // ...
+ * return ll_convert(data);
+ * }
+ * all infer both the FROM type and the TO type.
+ */
+template <typename FROM>
+class ll_convert
+{
+private:
+ const FROM& mRef;
+
+public:
+ ll_convert(const FROM& ref): mRef(ref) {}
+
+ template <typename TO>
+ inline operator TO() const
+ {
+ return ll_convert_impl<TO, FROM>()(mRef);
+ }
+};
+
+// When the TO type must be explicit, use a function template to get
+// ll_convert_to<TO>(from_value) API.
template<typename TO, typename FROM>
-TO ll_convert(const FROM& in)
+TO ll_convert_to(const FROM& in)
{
return ll_convert_impl<TO, FROM>()(in);
}
@@ -577,8 +605,8 @@ inline size_t ll_convert_length<char> (const char* zstr) { return std::strl
// and longname(const string&, len) so calls written pre-ll_convert() will
// work. Most of these overloads will be unified once we turn on C++17 and can
// use std::string_view.
-// It also uses aliasmacro to ensure that both ll_convert<OUTSTR>(const char*)
-// and ll_convert<OUTSTR>(const string&) will work.
+// It also uses aliasmacro to ensure that both ll_convert(const char*)
+// and ll_convert(const string&) will work.
#define ll_convert_forms(aliasmacro, OUTSTR, INSTR, longname) \
LL_COMMON_API OUTSTR longname(const INSTR::value_type* in, size_t len); \
inline auto longname(const INSTR& in, size_t len) \
@@ -815,7 +843,7 @@ LL_COMMON_API std::string ll_convert_string_to_utf8_string(const std::string& in
template<typename STRING>
STRING windows_message(unsigned long error)
{
- return ll_convert<STRING>(windows_message<std::wstring>(error));
+ return ll_convert(windows_message<std::wstring>(error));
}
/// There's only one real implementation
@@ -1811,7 +1839,7 @@ auto LLStringUtilBase<T>::getoptenv(const std::string& key) -> boost::optional<s
if (found)
{
// return populated boost::optional
- return { ll_convert<string_type>(*found) };
+ return { ll_convert_to<string_type>(*found) };
}
else
{
diff --git a/indra/llcommon/lua_function.cpp b/indra/llcommon/lua_function.cpp
new file mode 100644
index 0000000000..78abb8ba7e
--- /dev/null
+++ b/indra/llcommon/lua_function.cpp
@@ -0,0 +1,801 @@
+/**
+ * @file lua_function.cpp
+ * @author Nat Goodspeed
+ * @date 2024-02-05
+ * @brief Implementation for lua_function.
+ *
+ * $LicenseInfo:firstyear=2024&license=viewerlgpl$
+ * Copyright (c) 2024, Linden Research, Inc.
+ * $/LicenseInfo$
+ */
+
+// Precompiled header
+#include "linden_common.h"
+// associated header
+#include "lua_function.h"
+// STL headers
+// std headers
+#include <algorithm>
+#include <iomanip> // std::quoted
+#include <map>
+#include <memory> // std::unique_ptr
+// external library headers
+// other Linden headers
+#include "hexdump.h"
+#include "lleventcoro.h"
+#include "llsd.h"
+#include "llsdutil.h"
+#include "lualistener.h"
+
+/*****************************************************************************
+* luau namespace
+*****************************************************************************/
+namespace
+{
+ // can't specify free function free() as a unique_ptr deleter
+ struct freer
+ {
+ void operator()(void* ptr){ free(ptr); }
+ };
+} // anonymous namespace
+
+int lluau::dostring(lua_State* L, const std::string& desc, const std::string& text)
+{
+ auto r = loadstring(L, desc, text);
+ if (r != LUA_OK)
+ return r;
+
+ // It's important to pass LUA_MULTRET as the expected number of return
+ // values: if we pass any fixed number, we discard any returned values
+ // beyond that number.
+ return lua_pcall(L, 0, LUA_MULTRET, 0);
+}
+
+int lluau::loadstring(lua_State *L, const std::string &desc, const std::string &text)
+{
+ size_t bytecodeSize = 0;
+ // The char* returned by luau_compile() must be freed by calling free().
+ // Use unique_ptr so the memory will be freed even if luau_load() throws.
+ std::unique_ptr<char[], freer> bytecode{
+ luau_compile(text.data(), text.length(), nullptr, &bytecodeSize)};
+ return luau_load(L, desc.data(), bytecode.get(), bytecodeSize, 0);
+}
+
+/*****************************************************************************
+* Lua <=> C++ conversions
+*****************************************************************************/
+std::string lua_tostdstring(lua_State* L, int index)
+{
+ size_t len;
+ const char* strval{ lua_tolstring(L, index, &len) };
+ return { strval, len };
+}
+
+void lua_pushstdstring(lua_State* L, const std::string& str)
+{
+ luaL_checkstack(L, 1, nullptr);
+ lua_pushlstring(L, str.c_str(), str.length());
+}
+
+// By analogy with existing lua_tomumble() functions, return an LLSD object
+// corresponding to the Lua object at stack index 'index' in state L.
+// This function assumes that a Lua caller is fully aware that they're trying
+// to call a viewer function. In other words, the caller must specifically
+// construct Lua data convertible to LLSD.
+//
+// For proper error handling, we REQUIRE that the Lua runtime be compiled as
+// C++ so errors are raised as C++ exceptions rather than as longjmp() calls:
+// http://www.lua.org/manual/5.4/manual.html#4.4
+// "Internally, Lua uses the C longjmp facility to handle errors. (Lua will
+// use exceptions if you compile it as C++; search for LUAI_THROW in the
+// source code for details.)"
+// Some blocks within this function construct temporary C++ objects in the
+// expectation that these objects will be properly destroyed even if code
+// reached by that block raises a Lua error.
+LLSD lua_tollsd(lua_State* L, int index)
+{
+ LL_DEBUGS("Lua") << "lua_tollsd(" << index << ") of " << lua_gettop(L) << " stack entries: "
+ << lua_what(L, index) << LL_ENDL;
+ switch (lua_type(L, index))
+ {
+ case LUA_TNONE:
+ // Should LUA_TNONE be an error instead of returning isUndefined()?
+ case LUA_TNIL:
+ return {};
+
+ case LUA_TBOOLEAN:
+ return bool(lua_toboolean(L, index));
+
+ case LUA_TNUMBER:
+ {
+ // Vanilla Lua supports lua_tointegerx(), which tells the caller
+ // whether the number at the specified stack index is or is not an
+ // integer. Apparently the function exists but does not work right in
+ // Luau: it reports even non-integer numbers as integers.
+ // Instead, check if integer truncation leaves the number intact.
+ lua_Number numval{ lua_tonumber(L, index) };
+ lua_Integer intval{ narrow(numval) };
+ if (lua_Number(intval) == numval)
+ {
+ return LLSD::Integer(intval);
+ }
+ else
+ {
+ return numval;
+ }
+ }
+
+ case LUA_TSTRING:
+ return lua_tostdstring(L, index);
+
+ case LUA_TUSERDATA:
+ {
+ LLSD::Binary binary(lua_rawlen(L, index));
+ std::memcpy(binary.data(), lua_touserdata(L, index), binary.size());
+ return binary;
+ }
+
+ case LUA_TTABLE:
+ {
+ // A Lua table correctly constructed to convert to LLSD will have
+ // either consecutive integer keys starting at 1, which we represent
+ // as an LLSD array (with Lua key 1 at C++ index 0), or will have
+ // all string keys.
+ //
+ // In the belief that Lua table traversal skips "holes," that is, it
+ // doesn't report any key/value pair whose value is nil, we allow a
+ // table with integer keys >= 1 but with "holes." This produces an
+ // LLSD array with isUndefined() entries at unspecified keys. There
+ // would be no other way for a Lua caller to construct an
+ // isUndefined() LLSD array entry. However, to guard against crazy int
+ // keys, we forbid gaps larger than a certain size: crazy int keys
+ // could result in a crazy large contiguous LLSD array.
+ //
+ // Possible looseness could include:
+ // - A mix of integer and string keys could produce an LLSD map in
+ // which the integer keys are converted to string. (Key conversion
+ // must be performed in C++, not Lua, to avoid confusing
+ // lua_next().)
+ // - However, since in Lua t[0] and t["0"] are distinct table entries,
+ // do not consider converting numeric string keys to int to return
+ // an LLSD array.
+ // But until we get more experience with actual Lua scripts in
+ // practice, let's say that any deviation is a Lua coding error.
+ // An important property of the strict definition above is that most
+ // conforming data blobs can make a round trip across the language
+ // boundary and still compare equal. A non-conforming data blob would
+ // lose that property.
+ // Known exceptions to round trip identity:
+ // - Empty LLSD map and empty LLSD array convert to empty Lua table.
+ // But empty Lua table converts to isUndefined() LLSD object.
+ // - LLSD::Real with integer value returns as LLSD::Integer.
+ // - LLSD::UUID, LLSD::Date and LLSD::URI all convert to Lua string,
+ // and so return as LLSD::String.
+ // - Lua does not store any table key whose value is nil. An LLSD
+ // array with isUndefined() entries produces a Lua table with
+ // "holes" in the int key sequence; this converts back to an LLSD
+ // array containing corresponding isUndefined() entries -- except
+ // when one or more of the final entries isUndefined(). These are
+ // simply dropped, producing a shorter LLSD array than the original.
+ // - For the same reason, any keys in an LLSD map whose value
+ // isUndefined() are simply discarded in the converted Lua table.
+ // This converts back to an LLSD map lacking those keys.
+ // - If it's important to preserve the original length of an LLSD
+ // array whose final entries are undefined, or the full set of keys
+ // for an LLSD map some of whose values are undefined, store an
+ // LLSD::emptyArray() or emptyMap() instead. These will be
+ // represented in Lua as empty table, which should convert back to
+ // undefined LLSD. Naturally, though, those won't survive a second
+ // round trip.
+
+ // This is the most important of the luaL_checkstack() calls because a
+ // deeply nested Lua structure will enter this case at each level, and
+ // we'll need another 2 stack slots to traverse each nested table.
+ luaL_checkstack(L, 2, nullptr);
+ // BEFORE we push nil to initialize the lua_next() traversal, convert
+ // 'index' to absolute! Our caller might have passed a relative index;
+ // we do, below: lua_tollsd(L, -1). If 'index' is -1, then when we
+ // push nil, what we find at index -1 is nil, not the table!
+ index = lua_absindex(L, index);
+ LL_DEBUGS("Lua") << "checking for empty table" << LL_ENDL;
+ lua_pushnil(L); // first key
+ LL_DEBUGS("Lua") << lua_stack(L) << LL_ENDL;
+ if (! lua_next(L, index))
+ {
+ // it's a table, but the table is empty -- no idea if it should be
+ // modeled as empty array or empty map -- return isUndefined(),
+ // which can be consumed as either
+ LL_DEBUGS("Lua") << "empty table" << LL_ENDL;
+ return {};
+ }
+ // key is at stack index -2, value at index -1
+ // from here until lua_next() returns 0, have to lua_pop(2) if we
+ // return early
+ LuaPopper popper(L, 2);
+ // Remember the type of the first key
+ auto firstkeytype{ lua_type(L, -2) };
+ LL_DEBUGS("Lua") << "table not empty, first key type " << lua_typename(L, firstkeytype)
+ << LL_ENDL;
+ switch (firstkeytype)
+ {
+ case LUA_TNUMBER:
+ {
+ // First Lua key is a number: try to convert table to LLSD array.
+ // This is tricky because we don't know in advance the size of the
+ // array. The Lua reference manual says that lua_rawlen() is the
+ // same as the length operator '#'; but the length operator states
+ // that it might stop at any "hole" in the subject table.
+ // Moreover, the Lua next() function (and presumably lua_next())
+ // traverses a table in unspecified order, even for numeric keys
+ // (emphasized in the doc).
+ // Make a preliminary pass over the whole table to validate and to
+ // collect keys.
+ std::vector<LLSD::Integer> keys;
+ // Try to determine the length of the table. If the length
+ // operator is truthful, avoid allocations while we grow the keys
+ // vector. Even if it's not, we can still grow the vector, albeit
+ // a little less efficiently.
+ keys.reserve(lua_objlen(L, index));
+ do
+ {
+ auto arraykeytype{ lua_type(L, -2) };
+ switch (arraykeytype)
+ {
+ case LUA_TNUMBER:
+ {
+ int isint;
+ lua_Integer intkey{ lua_tointegerx(L, -2, &isint) };
+ if (! isint)
+ {
+ // key isn't an integer - this doesn't fit our LLSD
+ // array constraints
+ return lluau::error(L, "Expected integer array key, got %f instead",
+ lua_tonumber(L, -2));
+ }
+ if (intkey < 1)
+ {
+ return lluau::error(L, "array key %d out of bounds", int(intkey));
+ }
+
+ keys.push_back(LLSD::Integer(intkey));
+ break;
+ }
+
+ case LUA_TSTRING:
+ // break out strings specially to report the value
+ return lluau::error(L, "Cannot convert string array key '%s' to LLSD",
+ lua_tostring(L, -2));
+
+ default:
+ return lluau::error(L, "Cannot convert %s array key to LLSD",
+ lua_typename(L, arraykeytype));
+ }
+
+ // remove value, keep key for next iteration
+ lua_pop(L, 1);
+ } while (lua_next(L, index) != 0);
+ popper.disarm();
+ // Table keys are all integers: are they reasonable integers?
+ // Arbitrary max: may bite us, but more likely to protect us
+ size_t array_max{ 10000 };
+ if (keys.size() > array_max)
+ {
+ return lluau::error(L, "Conversion from Lua to LLSD array limited to %d entries",
+ int(array_max));
+ }
+ // We know the smallest key is >= 1. Check the largest. We also
+ // know the vector is NOT empty, else we wouldn't have gotten here.
+ std::sort(keys.begin(), keys.end());
+ LLSD::Integer highkey = *keys.rbegin();
+ if ((highkey - LLSD::Integer(keys.size())) > 100)
+ {
+ // Looks like we've gone beyond intentional array gaps into
+ // crazy key territory.
+ return lluau::error(L, "Gaps in Lua table too large for conversion to LLSD array");
+ }
+ LL_DEBUGS("Lua") << "collected " << keys.size() << " keys, max " << highkey << LL_ENDL;
+ // right away expand the result array to the size we'll need
+ LLSD result{ LLSD::emptyArray() };
+ result[highkey - 1] = LLSD();
+ // Traverse the table again, and this time populate result array.
+ lua_pushnil(L); // first key
+ while (lua_next(L, index))
+ {
+ // key at stack index -2, value at index -1
+ // We've already validated lua_tointegerx() for each key.
+ auto key{ lua_tointeger(L, -2) };
+ LL_DEBUGS("Lua") << "key " << key << ':' << LL_ENDL;
+ // Don't forget to subtract 1 from Lua key for LLSD subscript!
+ result[LLSD::Integer(key) - 1] = lua_tollsd(L, -1);
+ // remove value, keep key for next iteration
+ lua_pop(L, 1);
+ }
+ return result;
+ }
+
+ case LUA_TSTRING:
+ {
+ // First Lua key is a string: try to convert table to LLSD map
+ LLSD result{ LLSD::emptyMap() };
+ do
+ {
+ auto mapkeytype{ lua_type(L, -2) };
+ if (mapkeytype != LUA_TSTRING)
+ {
+ return lluau::error(L, "Cannot convert %s map key to LLSD",
+ lua_typename(L, mapkeytype));
+ }
+
+ auto key{ lua_tostdstring(L, -2) };
+ LL_DEBUGS("Lua") << "map key " << std::quoted(key) << ':' << LL_ENDL;
+ result[key] = lua_tollsd(L, -1);
+ // remove value, keep key for next iteration
+ lua_pop(L, 1);
+ } while (lua_next(L, index) != 0);
+ popper.disarm();
+ return result;
+ }
+
+ default:
+ // First Lua key isn't number or string: sorry
+ return lluau::error(L, "Cannot convert %s table key to LLSD",
+ lua_typename(L, firstkeytype));
+ }
+ }
+
+ default:
+ // Other Lua entities (e.g. function, C function, light userdata,
+ // thread, userdata) are not convertible to LLSD, indicating a coding
+ // error in the caller.
+ return lluau::error(L, "Cannot convert type %s to LLSD", luaL_typename(L, index));
+ }
+}
+
+// By analogy with existing lua_pushmumble() functions, push onto state L's
+// stack a Lua object corresponding to the passed LLSD object.
+void lua_pushllsd(lua_State* L, const LLSD& data)
+{
+ // might need 2 slots for array or map
+ luaL_checkstack(L, 2, nullptr);
+ switch (data.type())
+ {
+ case LLSD::TypeUndefined:
+ lua_pushnil(L);
+ break;
+
+ case LLSD::TypeBoolean:
+ lua_pushboolean(L, data.asBoolean());
+ break;
+
+ case LLSD::TypeInteger:
+ lua_pushinteger(L, data.asInteger());
+ break;
+
+ case LLSD::TypeReal:
+ lua_pushnumber(L, data.asReal());
+ break;
+
+ case LLSD::TypeBinary:
+ {
+ auto binary{ data.asBinary() };
+ std::memcpy(lua_newuserdata(L, binary.size()),
+ binary.data(), binary.size());
+ break;
+ }
+
+ case LLSD::TypeMap:
+ {
+ // push a new table with space for our non-array keys
+ lua_createtable(L, 0, data.size());
+ for (const auto& pair: llsd::inMap(data))
+ {
+ // push value -- so now table is at -2, value at -1
+ lua_pushllsd(L, pair.second);
+ // pop value, assign to table[key]
+ lua_setfield(L, -2, pair.first.c_str());
+ }
+ break;
+ }
+
+ case LLSD::TypeArray:
+ {
+ // push a new table with space for array entries
+ lua_createtable(L, data.size(), 0);
+ lua_Integer key{ 0 };
+ for (const auto& item: llsd::inArray(data))
+ {
+ // push new array value: table at -2, value at -1
+ lua_pushllsd(L, item);
+ // pop value, assign table[key] = value
+ lua_rawseti(L, -2, ++key);
+ }
+ break;
+ }
+
+ case LLSD::TypeString:
+ case LLSD::TypeUUID:
+ case LLSD::TypeDate:
+ case LLSD::TypeURI:
+ default:
+ {
+ lua_pushstdstring(L, data.asString());
+ break;
+ }
+ }
+}
+
+/*****************************************************************************
+* LuaState class
+*****************************************************************************/
+LuaState::LuaState(script_finished_fn cb):
+ mCallback(cb),
+ mState(nullptr)
+{
+ initLuaState();
+}
+
+void LuaState::initLuaState()
+{
+ if (mState)
+ {
+ lua_close(mState);
+ }
+ mState = luaL_newstate();
+ luaL_openlibs(mState);
+ LuaFunction::init(mState);
+ // Try to make print() write to our log.
+ lua_register(mState, "print", LuaFunction::get("print_info"));
+}
+
+LuaState::~LuaState()
+{
+ // Did somebody call obtainListener() on this LuaState?
+ // That is, is there a LuaListener key in its registry?
+ auto listener{ getListener() };
+ if (listener)
+ {
+ // if we got a LuaListener instance, destroy it
+ auto lptr{ listener.get() };
+ listener.reset();
+ delete lptr;
+ }
+
+ lua_close(mState);
+
+ if (mCallback)
+ {
+ // mError potentially set by previous checkLua() call(s)
+ mCallback(mError);
+ }
+}
+
+bool LuaState::checkLua(const std::string& desc, int r)
+{
+ if (r != LUA_OK)
+ {
+ mError = lua_tostring(mState, -1);
+ lua_pop(mState, 1);
+
+ LL_WARNS() << desc << ": " << mError << LL_ENDL;
+ return false;
+ }
+ return true;
+}
+
+std::pair<int, LLSD> LuaState::expr(const std::string& desc, const std::string& text)
+{
+ if (! checkLua(desc, lluau::dostring(mState, desc, text)))
+ return { -1, mError };
+
+ // here we believe there was no error -- did the Lua fragment leave
+ // anything on the stack?
+ std::pair<int, LLSD> result{ lua_gettop(mState), {} };
+ if (! result.first)
+ return result;
+
+ // aha, at least one entry on the stack!
+ if (result.first == 1)
+ {
+ result.second = lua_tollsd(mState, 1);
+ // pop the result we claimed
+ lua_settop(mState, 0);
+ return result;
+ }
+
+ // multiple entries on the stack
+ for (int index = 1; index <= result.first; ++index)
+ {
+ result.second.append(lua_tollsd(mState, index));
+ }
+ // pop everything
+ lua_settop(mState, 0);
+ return result;
+}
+
+LuaListener::ptr_t LuaState::getListener(lua_State* L)
+{
+ // have to use one more stack slot
+ luaL_checkstack(L, 1, nullptr);
+ LuaListener::ptr_t listener;
+ // Does this lua_State already have a LuaListener stored in the registry?
+ auto keytype{ lua_getfield(L, LUA_REGISTRYINDEX, "event.listener") };
+ llassert(keytype == LUA_TNIL || keytype == LUA_TNUMBER);
+ if (keytype == LUA_TNUMBER)
+ {
+ // We do already have a LuaListener. Retrieve it.
+ int isint;
+ listener = LuaListener::getInstance(lua_tointegerx(L, -1, &isint));
+ // Nobody should have destroyed this LuaListener instance!
+ llassert(isint && listener);
+ }
+ // pop the int "event.listener" key
+ lua_pop(L, 1);
+ return listener;
+}
+
+LuaListener::ptr_t LuaState::obtainListener(lua_State* L)
+{
+ auto listener{ getListener(L) };
+ if (! listener)
+ {
+ // have to use one more stack slot
+ luaL_checkstack(L, 1, nullptr);
+ // instantiate a new LuaListener, binding the L state -- but use a
+ // no-op deleter: we do NOT want this ptr_t to manage the lifespan of
+ // this new LuaListener!
+ listener.reset(new LuaListener(L), [](LuaListener*){});
+ // set its key in the field where we'll look for it later
+ lua_pushinteger(L, listener->getKey());
+ lua_setfield(L, LUA_REGISTRYINDEX, "event.listener");
+ }
+ return listener;
+}
+
+/*****************************************************************************
+* LuaPopper class
+*****************************************************************************/
+LuaPopper::~LuaPopper()
+{
+ if (mCount)
+ {
+ lua_pop(mState, mCount);
+ }
+}
+
+/*****************************************************************************
+* LuaFunction class
+*****************************************************************************/
+LuaFunction::LuaFunction(const std::string_view& name, lua_CFunction function,
+ const std::string_view& helptext)
+{
+ const auto& [registry, lookup] = getState();
+ registry.emplace(name, Registry::mapped_type{ function, helptext });
+ lookup.emplace(function, name);
+}
+
+void LuaFunction::init(lua_State* L)
+{
+ const auto& [registry, lookup] = getRState();
+ for (const auto& [name, pair]: registry)
+ {
+ const auto& [funcptr, helptext] = pair;
+ lua_register(L, name.c_str(), funcptr);
+ }
+}
+
+lua_CFunction LuaFunction::get(const std::string& key)
+{
+ // use find() instead of subscripting to avoid creating an entry for
+ // unknown key
+ const auto& [registry, lookup] = getState();
+ auto found{ registry.find(key) };
+ return (found == registry.end())? nullptr : found->second.first;
+}
+
+std::pair<LuaFunction::Registry&, LuaFunction::Lookup&> LuaFunction::getState()
+{
+ // use function-local statics to ensure they're initialized
+ static Registry registry;
+ static Lookup lookup;
+ return { registry, lookup };
+}
+
+/*****************************************************************************
+* help()
+*****************************************************************************/
+lua_function(help,
+ "help(): list viewer's Lua functions\n"
+ "help(function): show help string for specific function")
+{
+ auto& luapump{ LLEventPumps::instance().obtain("lua output") };
+ const auto& [registry, lookup]{ LuaFunction::getRState() };
+ if (! lua_gettop(L))
+ {
+ // no arguments passed: list all lua_functions
+ for (const auto& [name, pair] : registry)
+ {
+ const auto& [fptr, helptext] = pair;
+ luapump.post(helptext);
+ }
+ }
+ else
+ {
+ // arguments passed: list each of the specified lua_functions
+ for (int idx = 1, top = lua_gettop(L); idx <= top; ++idx)
+ {
+ std::string arg{ stringize("<unknown ", lua_typename(L, lua_type(L, idx)), ">") };
+ if (lua_type(L, idx) == LUA_TSTRING)
+ {
+ arg = lua_tostdstring(L, idx);
+ }
+ else if (lua_type(L, idx) == LUA_TFUNCTION)
+ {
+ // Caller passed the actual function instead of its string
+ // name. A Lua function is an anonymous callable object; it
+ // has a name only by assigment. You can't ask Lua for a
+ // function's name, which is why our constructor maintains a
+ // reverse Lookup map.
+ auto function{ lua_tocfunction(L, idx) };
+ if (auto found = lookup.find(function); found != lookup.end())
+ {
+ // okay, pass found name to lookup below
+ arg = found->second;
+ }
+ }
+
+ if (auto found = registry.find(arg); found != registry.end())
+ {
+ luapump.post(found->second.second);
+ }
+ else
+ {
+ luapump.post(arg + ": NOT FOUND");
+ }
+ }
+ // pop all arguments
+ lua_settop(L, 0);
+ }
+ return 0; // void return
+}
+
+/*****************************************************************************
+* leaphelp()
+*****************************************************************************/
+lua_function(
+ leaphelp,
+ "leaphelp(): list viewer's LEAP APIs\n"
+ "leaphelp(api): show help for specific api string name")
+{
+ LLSD request;
+ int top{ lua_gettop(L) };
+ if (top)
+ {
+ request = llsd::map("op", "getAPI", "api", lua_tostdstring(L, 1));
+ }
+ else
+ {
+ request = llsd::map("op", "getAPIs");
+ }
+ // pop all args
+ lua_settop(L, 0);
+
+ auto& outpump{ LLEventPumps::instance().obtain("lua output") };
+ auto listener{ LuaState::obtainListener(L) };
+ LLEventStream replyPump("leaphelp", true);
+ // ask the LuaListener's LeapListener and suspend calling coroutine until reply
+ auto reply{ llcoro::postAndSuspend(request, listener->getCommandName(), replyPump, "reply") };
+ reply.erase("reqid");
+
+ if (auto error = reply["error"]; error.isString())
+ {
+ outpump.post(error.asString());
+ return 0;
+ }
+
+ if (top)
+ {
+ // caller wants a specific API
+ outpump.post(stringize(reply["name"].asString(), ":\n", reply["desc"].asString()));
+ for (const auto& opmap : llsd::inArray(reply["ops"]))
+ {
+ std::ostringstream reqstr;
+ auto req{ opmap["required"] };
+ if (req.isArray())
+ {
+ const char* sep = " (requires ";
+ for (const auto& [reqkey, reqval] : llsd::inMap(req))
+ {
+ reqstr << sep << reqkey;
+ sep = ", ";
+ }
+ reqstr << ")";
+ }
+ outpump.post(stringize("---- ", reply["key"].asString(), " == '",
+ opmap["name"].asString(), "'", reqstr.str(), ":\n",
+ opmap["desc"].asString()));
+ }
+ }
+ else
+ {
+ // caller wants a list of APIs
+ for (const auto& [name, data] : llsd::inMap(reply))
+ {
+ outpump.post(stringize("==== ", name, ":\n", data["desc"].asString()));
+ }
+ }
+ return 0; // void return
+}
+
+/*****************************************************************************
+* lua_what
+*****************************************************************************/
+std::ostream& operator<<(std::ostream& out, const lua_what& self)
+{
+ switch (lua_type(self.L, self.index))
+ {
+ case LUA_TNONE:
+ // distinguish acceptable but non-valid index
+ out << "none";
+ break;
+
+ case LUA_TNIL:
+ out << "nil";
+ break;
+
+ case LUA_TBOOLEAN:
+ {
+ auto oldflags { out.flags() };
+ out << std::boolalpha << lua_toboolean(self.L, self.index);
+ out.flags(oldflags);
+ break;
+ }
+
+ case LUA_TNUMBER:
+ out << lua_tonumber(self.L, self.index);
+ break;
+
+ case LUA_TSTRING:
+ out << std::quoted(lua_tostdstring(self.L, self.index));
+ break;
+
+ case LUA_TUSERDATA:
+ {
+ const S32 maxlen = 20;
+ S32 binlen{ lua_rawlen(self.L, self.index) };
+ LLSD::Binary binary(std::min(maxlen, binlen));
+ std::memcpy(binary.data(), lua_touserdata(self.L, self.index), binary.size());
+ out << LL::hexdump(binary);
+ if (binlen > maxlen)
+ {
+ out << "...(" << (binlen - maxlen) << " more)";
+ }
+ break;
+ }
+
+ case LUA_TLIGHTUSERDATA:
+ out << lua_touserdata(self.L, self.index);
+ break;
+
+ default:
+ // anything else, don't bother trying to report value, just type
+ out << lua_typename(self.L, lua_type(self.L, self.index));
+ break;
+ }
+ return out;
+}
+
+/*****************************************************************************
+* lua_stack
+*****************************************************************************/
+std::ostream& operator<<(std::ostream& out, const lua_stack& self)
+{
+ out << "stack: [";
+ const char* sep = "";
+ for (int index = 1; index <= lua_gettop(self.L); ++index)
+ {
+ out << sep << lua_what(self.L, index);
+ sep = ", ";
+ }
+ out << ']';
+ return out;
+}
diff --git a/indra/llcommon/lua_function.h b/indra/llcommon/lua_function.h
new file mode 100644
index 0000000000..07848e38af
--- /dev/null
+++ b/indra/llcommon/lua_function.h
@@ -0,0 +1,256 @@
+/**
+ * @file lua_function.h
+ * @author Nat Goodspeed
+ * @date 2024-02-05
+ * @brief Definitions useful for coding a new Luau entry point into C++
+ *
+ * $LicenseInfo:firstyear=2024&license=viewerlgpl$
+ * Copyright (c) 2024, Linden Research, Inc.
+ * $/LicenseInfo$
+ */
+
+#if ! defined(LL_LUA_FUNCTION_H)
+#define LL_LUA_FUNCTION_H
+
+#include "luau/luacode.h"
+#include "luau/lua.h"
+#include "luau/luaconf.h"
+#include "luau/lualib.h"
+#include "stringize.h"
+#include <exception> // std::uncaught_exceptions()
+#include <memory> // std::shared_ptr
+#include <utility> // std::pair
+
+class LuaListener;
+
+#define lua_register(L, n, f) (lua_pushcfunction(L, (f), n), lua_setglobal(L, (n)))
+#define lua_rawlen lua_objlen
+
+namespace lluau
+{
+ // luau defines luaL_error() as void, but we want to use the Lua idiom of
+ // 'return error(...)'. Wrap luaL_error() in an int function.
+#if __clang__
+#pragma clang diagnostic push
+#pragma clang diagnostic ignored "-Wformat-security"
+#endif // __clang__
+ template<typename... Args>
+ int error(lua_State* L, const char* format, Args&&... args)
+ {
+ luaL_error(L, format, std::forward<Args>(args)...);
+#ifndef LL_MSVC
+ return 0;
+#endif
+ }
+#if __clang__
+#pragma clang diagnostic pop
+#endif // __clang__
+
+ // luau removed lua_dostring(), but since we perform the equivalent luau
+ // sequence in multiple places, encapsulate it. desc and text are strings
+ // rather than string_views because dostring() needs pointers to nul-
+ // terminated char arrays.
+ int dostring(lua_State* L, const std::string& desc, const std::string& text);
+ int loadstring(lua_State* L, const std::string& desc, const std::string& text);
+} // namespace lluau
+
+std::string lua_tostdstring(lua_State* L, int index);
+void lua_pushstdstring(lua_State* L, const std::string& str);
+LLSD lua_tollsd(lua_State* L, int index);
+void lua_pushllsd(lua_State* L, const LLSD& data);
+
+/**
+ * RAII class to manage the lifespan of a lua_State
+ */
+class LuaState
+{
+public:
+ typedef std::function<void(std::string msg)> script_finished_fn;
+
+ LuaState(script_finished_fn cb={});
+
+ LuaState(const LuaState&) = delete;
+ LuaState& operator=(const LuaState&) = delete;
+
+ ~LuaState();
+
+ void initLuaState();
+
+ bool checkLua(const std::string& desc, int r);
+
+ // expr() is for when we want to capture any results left on the stack
+ // by a Lua expression, possibly including multiple return values.
+ // int < 0 means error, and LLSD::asString() is the error message.
+ // int == 0 with LLSD::isUndefined() means the Lua expression returned no
+ // results.
+ // int == 1 means the Lua expression returned one result.
+ // int > 1 with LLSD::isArray() means the Lua expression returned
+ // multiple results, represented as the entries of the array.
+ std::pair<int, LLSD> expr(const std::string& desc, const std::string& text);
+
+ operator lua_State*() const { return mState; }
+
+ // Return LuaListener for this LuaState if we already have one, else empty
+ // shared_ptr.
+ std::shared_ptr<LuaListener> getListener() { return getListener(mState); }
+ // Find or create LuaListener for this LuaState, returning its ptr_t.
+ std::shared_ptr<LuaListener> obtainListener() { return obtainListener(mState); }
+ // Return LuaListener for passed lua_State if we already have one, else
+ // empty shared_ptr.
+ static std::shared_ptr<LuaListener> getListener(lua_State* L);
+ // Find or create LuaListener for passed lua_State, returning its ptr_t.
+ static std::shared_ptr<LuaListener> obtainListener(lua_State* L);
+
+private:
+ script_finished_fn mCallback;
+ lua_State* mState;
+ std::string mError;
+};
+
+/**
+ * LuaPopper is an RAII struct whose role is to pop some number of entries
+ * from the Lua stack if the calling function exits early.
+ */
+struct LuaPopper
+{
+ LuaPopper(lua_State* L, int count):
+ mState(L),
+ mCount(count)
+ {}
+
+ LuaPopper(const LuaPopper&) = delete;
+ LuaPopper& operator=(const LuaPopper&) = delete;
+
+ ~LuaPopper();
+
+ void disarm() { set(0); }
+ void set(int count) { mCount = count; }
+
+ lua_State* mState;
+ int mCount;
+};
+
+/**
+ * LuaFunction is a base class containing a static registry of its static
+ * subclass call() methods. call() is NOT virtual: instead, each subclass
+ * constructor passes a pointer to its distinct call() method to the base-
+ * class constructor, along with a name by which to register that method.
+ *
+ * The init() method walks the registry and registers each such name with the
+ * passed lua_State.
+ */
+class LuaFunction
+{
+public:
+ LuaFunction(const std::string_view& name, lua_CFunction function,
+ const std::string_view& helptext);
+
+ static void init(lua_State* L);
+
+ static lua_CFunction get(const std::string& key);
+
+protected:
+ using Registry = std::map<std::string, std::pair<lua_CFunction, std::string>>;
+ using Lookup = std::map<lua_CFunction, std::string>;
+ static std::pair<const Registry&, const Lookup&> getRState() { return getState(); }
+
+private:
+ static std::pair<Registry&, Lookup&> getState();
+};
+
+/**
+ * lua_function(name, helptext) is a macro to facilitate defining C++ functions
+ * available to Lua. It defines a subclass of LuaFunction and declares a
+ * static instance of that subclass, thereby forcing the compiler to call its
+ * constructor at module initialization time. The constructor passes the
+ * stringized instance name to its LuaFunction base-class constructor, along
+ * with a pointer to the static subclass call() method. It then emits the
+ * call() method definition header, to be followed by a method body enclosed
+ * in curly braces as usual.
+ */
+#define lua_function(name, helptext) \
+static struct name##_luasub : public LuaFunction \
+{ \
+ name##_luasub(): LuaFunction(#name, &call, helptext) {} \
+ static int call(lua_State* L); \
+} name##_lua; \
+int name##_luasub::call(lua_State* L)
+// {
+// ... supply method body here, referencing 'L' ...
+// }
+
+// Usage: std::cout << lua_what(L, stackindex) << ...;
+// Reports on the Lua value found at the passed stackindex.
+// If cast to std::string, returns the corresponding string value.
+class lua_what
+{
+public:
+ lua_what(lua_State* state, int idx):
+ L(state),
+ index(idx)
+ {}
+
+ friend std::ostream& operator<<(std::ostream& out, const lua_what& self);
+
+ operator std::string() const { return stringize(*this); }
+
+private:
+ lua_State* L;
+ int index;
+};
+
+// Usage: std::cout << lua_stack(L) << ...;
+// Reports on the contents of the Lua stack.
+// If cast to std::string, returns the corresponding string value.
+class lua_stack
+{
+public:
+ lua_stack(lua_State* state):
+ L(state)
+ {}
+
+ friend std::ostream& operator<<(std::ostream& out, const lua_stack& self);
+
+ operator std::string() const { return stringize(*this); }
+
+private:
+ lua_State* L;
+};
+
+// adapted from indra/test/debug.h
+// can't generalize Debug::operator() target because it's a variadic template
+class LuaLog
+{
+public:
+ template <typename... ARGS>
+ LuaLog(lua_State* L, ARGS&&... args):
+ L(L),
+ mBlock(stringize(std::forward<ARGS>(args)...))
+ {
+ (*this)("entry ", lua_stack(L));
+ }
+
+ // non-copyable
+ LuaLog(const LuaLog&) = delete;
+ LuaLog& operator=(const LuaLog&) = delete;
+
+ ~LuaLog()
+ {
+ auto exceptional{ std::uncaught_exceptions()? "exceptional " : "" };
+ (*this)(exceptional, "exit ", lua_stack(L));
+ }
+
+ template <typename... ARGS>
+ void operator()(ARGS&&... args)
+ {
+ LL_INFOS("Lua") << mBlock << ' ';
+ stream_to(LL_CONT, std::forward<ARGS>(args)...);
+ LL_ENDL;
+ }
+
+private:
+ lua_State* L;
+ const std::string mBlock;
+};
+
+#endif /* ! defined(LL_LUA_FUNCTION_H) */
diff --git a/indra/llcommon/lualistener.cpp b/indra/llcommon/lualistener.cpp
new file mode 100644
index 0000000000..ed34133924
--- /dev/null
+++ b/indra/llcommon/lualistener.cpp
@@ -0,0 +1,91 @@
+/**
+ * @file lualistener.cpp
+ * @author Nat Goodspeed
+ * @date 2024-02-06
+ * @brief Implementation for lualistener.
+ *
+ * $LicenseInfo:firstyear=2024&license=viewerlgpl$
+ * Copyright (c) 2024, Linden Research, Inc.
+ * $/LicenseInfo$
+ */
+
+// Precompiled header
+#include "linden_common.h"
+// associated header
+#include "lualistener.h"
+// STL headers
+// std headers
+#include <cstdlib> // std::rand()
+#include <cstring> // std::memcpy()
+// external library headers
+#include "luau/lua.h"
+// other Linden headers
+#include "llerror.h"
+#include "llleaplistener.h"
+#include "lua_function.h"
+
+const int MAX_QSIZE = 1000;
+
+std::ostream& operator<<(std::ostream& out, const LuaListener& self)
+{
+ return out << "LuaListener(" << self.getReplyName() << ", " << self.getCommandName() << ")";
+}
+
+LuaListener::LuaListener(lua_State* L):
+ super(getUniqueKey()),
+ mListener(new LLLeapListener(
+ "LuaListener",
+ [this](const std::string& pump, const LLSD& data)
+ { return queueEvent(pump, data); }))
+{}
+
+LuaListener::~LuaListener()
+{}
+
+int LuaListener::getUniqueKey()
+{
+ // Find a random key that does NOT already correspond to a LuaListener
+ // instance. Passing a duplicate key to LLInstanceTracker would do Bad
+ // Things.
+ int key;
+ do
+ {
+ key = std::rand();
+ } while (LuaListener::getInstance(key));
+ // This is theoretically racy, if we were instantiating new
+ // LuaListeners on multiple threads. Don't.
+ return key;
+}
+
+std::string LuaListener::getReplyName() const
+{
+ return mListener->getReplyPump().getName();
+}
+
+std::string LuaListener::getCommandName() const
+{
+ return mListener->getPumpName();
+}
+
+bool LuaListener::queueEvent(const std::string& pump, const LLSD& data)
+{
+ // Our Lua script might be stalled, or just fail to retrieve events. Don't
+ // grow this queue indefinitely. But don't set MAX_QSIZE as the queue
+ // capacity or we'd block the post() call trying to propagate this event!
+ if (auto size = mQueue.size(); size > MAX_QSIZE)
+ {
+ LL_WARNS("Lua") << "LuaListener queue for " << getReplyName()
+ << " exceeds " << MAX_QSIZE << ": " << size
+ << " -- discarding event" << LL_ENDL;
+ }
+ else
+ {
+ mQueue.push(decltype(mQueue)::value_type(pump, data));
+ }
+ return false;
+}
+
+LuaListener::PumpData LuaListener::getNext()
+{
+ return mQueue.pop();
+}
diff --git a/indra/llcommon/lualistener.h b/indra/llcommon/lualistener.h
new file mode 100644
index 0000000000..c13b7bbd5f
--- /dev/null
+++ b/indra/llcommon/lualistener.h
@@ -0,0 +1,78 @@
+/**
+ * @file lualistener.h
+ * @author Nat Goodspeed
+ * @date 2024-02-06
+ * @brief Define LuaListener class
+ *
+ * $LicenseInfo:firstyear=2024&license=viewerlgpl$
+ * Copyright (c) 2024, Linden Research, Inc.
+ * $/LicenseInfo$
+ */
+
+#if ! defined(LL_LUALISTENER_H)
+#define LL_LUALISTENER_H
+
+#include "llinstancetracker.h"
+#include "llsd.h"
+#include "llthreadsafequeue.h"
+#include <iosfwd> // std::ostream
+#include <memory> // std::unique_ptr
+#include <string>
+#include <utility> // std::pair
+
+struct lua_State;
+class LLLeapListener;
+
+/**
+ * LuaListener is based on LLLeap. It serves an analogous function.
+ *
+ * Each LuaListener instance has an int key, generated randomly to
+ * inconvenience malicious Lua scripts wanting to mess with others. The idea
+ * is that a given lua_State stores in its Registry:
+ * - "event.listener": the int key of the corresponding LuaListener, if any
+ * The original thought was that LuaListener would itself store the Lua
+ * function -- but surprisingly, there is no C/C++ type in the API that stores
+ * a Lua function.
+ *
+ * (We considered storing in "event.listener" the LuaListener pointer itself
+ * as a light userdata, but the problem would be if Lua code overwrote that.
+ * We want to prevent any Lua script from crashing the viewer, intentionally
+ * or otherwise. Safer to use a key lookup.)
+ *
+ * Like LLLeap, each LuaListener instance also has an associated
+ * LLLeapListener to respond to LLEventPump management commands.
+ */
+class LuaListener: public LLInstanceTracker<LuaListener, int>
+{
+ using super = LLInstanceTracker<LuaListener, int>;
+public:
+ LuaListener(lua_State* L);
+
+ LuaListener(const LuaListener&) = delete;
+ LuaListener& operator=(const LuaListener&) = delete;
+
+ ~LuaListener();
+
+ std::string getReplyName() const;
+ std::string getCommandName() const;
+
+ /**
+ * LuaListener enqueues reply events from its LLLeapListener on mQueue.
+ * Call getNext() to retrieve the next such event. Blocks the calling
+ * coroutine if the queue is empty.
+ */
+ using PumpData = std::pair<std::string, LLSD>;
+ PumpData getNext();
+
+ friend std::ostream& operator<<(std::ostream& out, const LuaListener& self);
+
+private:
+ static int getUniqueKey();
+ bool queueEvent(const std::string& pump, const LLSD& data);
+
+ LLThreadSafeQueue<PumpData> mQueue;
+
+ std::unique_ptr<LLLeapListener> mListener;
+};
+
+#endif /* ! defined(LL_LUALISTENER_H) */
diff --git a/indra/llcommon/stringize.h b/indra/llcommon/stringize.h
index c0b13135f9..63d44a7272 100644
--- a/indra/llcommon/stringize.h
+++ b/indra/llcommon/stringize.h
@@ -88,13 +88,13 @@ struct gstringize_impl
};
// partially specialize for a single STRING argument -
-// note that ll_convert<T>(T) already handles the trivial case
+// note that ll_convert_to<T>(T) already handles the trivial case
template <typename OUTCHAR, typename INCHAR>
struct gstringize_impl<OUTCHAR, std::basic_string<INCHAR>>
{
auto operator()(const std::basic_string<INCHAR>& arg)
{
- return ll_convert<std::basic_string<OUTCHAR>>(arg);
+ return ll_convert_to<std::basic_string<OUTCHAR>>(arg);
}
};
@@ -105,7 +105,7 @@ struct gstringize_impl<OUTCHAR, INCHAR*>
{
auto operator()(const INCHAR* arg)
{
- return ll_convert<std::basic_string<OUTCHAR>>(arg);
+ return ll_convert_to<std::basic_string<OUTCHAR>>(arg);
}
};
diff --git a/indra/llcommon/tests/StringVec.h b/indra/llcommon/tests/StringVec.h
index a380b00a05..761956a012 100644
--- a/indra/llcommon/tests/StringVec.h
+++ b/indra/llcommon/tests/StringVec.h
@@ -18,6 +18,16 @@
typedef std::vector<std::string> StringVec;
+#if defined(LL_LLTUT_H)
+// Modern compilers require us to define operator<<(std::ostream&, StringVec)
+// before the definition of the ensure() template that engages it. The error
+// stating that the compiler can't find a viable operator<<() is so perplexing
+// that even though I've obviously hit it a couple times before, a new
+// instance still caused much head-scratching. This warning is intended to
+// demystify any inadvertent future recurrence.
+#warning "StringVec.h must be #included BEFORE lltut.h for ensure() to work"
+#endif
+
std::ostream& operator<<(std::ostream& out, const StringVec& strings)
{
out << '(';
diff --git a/indra/llcommon/tests/lleventcoro_test.cpp b/indra/llcommon/tests/lleventcoro_test.cpp
index 032923a108..6ff895c1c1 100644
--- a/indra/llcommon/tests/lleventcoro_test.cpp
+++ b/indra/llcommon/tests/lleventcoro_test.cpp
@@ -113,30 +113,27 @@ namespace tut
void test_data::explicit_wait(boost::shared_ptr<LLCoros::Promise<std::string>>& cbp)
{
- BEGIN
- {
- mSync.bump();
- // The point of this test is to verify / illustrate suspending a
- // coroutine for something other than an LLEventPump. In other
- // words, this shows how to adapt to any async operation that
- // provides a callback-style notification (and prove that it
- // works).
+ DEBUG;
+ mSync.bump();
+ // The point of this test is to verify / illustrate suspending a
+ // coroutine for something other than an LLEventPump. In other
+ // words, this shows how to adapt to any async operation that
+ // provides a callback-style notification (and prove that it
+ // works).
- // Perhaps we would send a request to a remote server and arrange
- // for cbp->set_value() to be called on response.
- // For test purposes, instead of handing 'callback' (or an
- // adapter) off to some I/O subsystem, we'll just pass it back to
- // our caller.
- cbp = boost::make_shared<LLCoros::Promise<std::string>>();
- LLCoros::Future<std::string> future = LLCoros::getFuture(*cbp);
+ // Perhaps we would send a request to a remote server and arrange
+ // for cbp->set_value() to be called on response.
+ // For test purposes, instead of handing 'callback' (or an
+ // adapter) off to some I/O subsystem, we'll just pass it back to
+ // our caller.
+ cbp = boost::make_shared<LLCoros::Promise<std::string>>();
+ LLCoros::Future<std::string> future = LLCoros::getFuture(*cbp);
- // calling get() on the future causes us to suspend
- debug("about to suspend");
- stringdata = future.get();
- mSync.bump();
- ensure_equals("Got it", stringdata, "received");
- }
- END
+ // calling get() on the future causes us to suspend
+ debug("about to suspend");
+ stringdata = future.get();
+ mSync.bump();
+ ensure_equals("Got it", stringdata, "received");
}
template<> template<>
@@ -163,13 +160,9 @@ namespace tut
void test_data::waitForEventOn1()
{
- BEGIN
- {
- mSync.bump();
- result = suspendUntilEventOn("source");
- mSync.bump();
- }
- END
+ mSync.bump();
+ result = suspendUntilEventOn("source");
+ mSync.bump();
}
template<> template<>
@@ -189,15 +182,11 @@ namespace tut
void test_data::coroPump()
{
- BEGIN
- {
- mSync.bump();
- LLCoroEventPump waiter;
- replyName = waiter.getName();
- result = waiter.suspend();
- mSync.bump();
- }
- END
+ mSync.bump();
+ LLCoroEventPump waiter;
+ replyName = waiter.getName();
+ result = waiter.suspend();
+ mSync.bump();
}
template<> template<>
@@ -217,16 +206,12 @@ namespace tut
void test_data::postAndWait1()
{
- BEGIN
- {
- mSync.bump();
- result = postAndSuspend(LLSDMap("value", 17), // request event
- immediateAPI.getPump(), // requestPump
- "reply1", // replyPump
- "reply"); // request["reply"] = name
- mSync.bump();
- }
- END
+ mSync.bump();
+ result = postAndSuspend(LLSDMap("value", 17), // request event
+ immediateAPI.getPump(), // requestPump
+ "reply1", // replyPump
+ "reply"); // request["reply"] = name
+ mSync.bump();
}
template<> template<>
@@ -240,15 +225,11 @@ namespace tut
void test_data::coroPumpPost()
{
- BEGIN
- {
- mSync.bump();
- LLCoroEventPump waiter;
- result = waiter.postAndSuspend(LLSDMap("value", 17),
- immediateAPI.getPump(), "reply");
- mSync.bump();
- }
- END
+ mSync.bump();
+ LLCoroEventPump waiter;
+ result = waiter.postAndSuspend(LLSDMap("value", 17),
+ immediateAPI.getPump(), "reply");
+ mSync.bump();
}
template<> template<>
diff --git a/indra/llcommon/tests/lleventdispatcher_test.cpp b/indra/llcommon/tests/lleventdispatcher_test.cpp
index b0c532887c..244cd07ac9 100644
--- a/indra/llcommon/tests/lleventdispatcher_test.cpp
+++ b/indra/llcommon/tests/lleventdispatcher_test.cpp
@@ -17,13 +17,13 @@
// std headers
// external library headers
// other Linden headers
+#include "StringVec.h"
#include "../test/lltut.h"
#include "lleventfilter.h"
#include "llsd.h"
#include "llsdutil.h"
#include "llevents.h"
#include "stringize.h"
-#include "StringVec.h"
#include "tests/wrapllerrs.h"
#include "../test/catch_and_store_what_in.h"
#include "../test/debug.h"
diff --git a/indra/llcommon/tests/lleventfilter_test.cpp b/indra/llcommon/tests/lleventfilter_test.cpp
index fa2cb03e95..ed7cb56506 100644
--- a/indra/llcommon/tests/lleventfilter_test.cpp
+++ b/indra/llcommon/tests/lleventfilter_test.cpp
@@ -34,10 +34,10 @@
// std headers
// external library headers
// other Linden headers
+#include "listener.h"
#include "../test/lltut.h"
#include "stringize.h"
#include "llsdutil.h"
-#include "listener.h"
#include "tests/wrapllerrs.h"
#include <typeinfo>
diff --git a/indra/llcommon/tests/llleap_test.cpp b/indra/llcommon/tests/llleap_test.cpp
index 7197dedfbf..6fe9e3446f 100644
--- a/indra/llcommon/tests/llleap_test.cpp
+++ b/indra/llcommon/tests/llleap_test.cpp
@@ -18,6 +18,7 @@
#include <functional>
// external library headers
// other Linden headers
+#include "StringVec.h"
#include "../test/lltut.h"
#include "../test/namedtempfile.h"
#include "../test/catch_and_store_what_in.h"
@@ -26,7 +27,6 @@
#include "llprocess.h"
#include "llstring.h"
#include "stringize.h"
-#include "StringVec.h"
#if defined(LL_WINDOWS)
#define sleep(secs) _sleep((secs) * 1000)
diff --git a/indra/llcommon/tests/llsdserialize_test.cpp b/indra/llcommon/tests/llsdserialize_test.cpp
index ac40125f75..730731a927 100644
--- a/indra/llcommon/tests/llsdserialize_test.cpp
+++ b/indra/llcommon/tests/llsdserialize_test.cpp
@@ -52,11 +52,11 @@ typedef U32 uint32_t;
#include "llformat.h"
#include "llmemorystream.h"
-#include "../test/hexdump.h"
+#include "hexdump.h"
+#include "StringVec.h"
#include "../test/lltut.h"
#include "../test/namedtempfile.h"
#include "stringize.h"
-#include "StringVec.h"
#include <functional>
typedef std::function<void(const LLSD& data, std::ostream& str)> FormatterFunction;
@@ -1921,12 +1921,12 @@ namespace tut
int bufflen{ static_cast<int>(buffstr.length()) };
out.write(reinterpret_cast<const char*>(&bufflen), sizeof(bufflen));
LL_DEBUGS() << "Wrote length: "
- << hexdump(reinterpret_cast<const char*>(&bufflen),
- sizeof(bufflen))
+ << LL::hexdump(reinterpret_cast<const char*>(&bufflen),
+ sizeof(bufflen))
<< LL_ENDL;
out.write(buffstr.c_str(), buffstr.length());
LL_DEBUGS() << "Wrote data: "
- << hexmix(buffstr.c_str(), buffstr.length())
+ << LL::hexmix(buffstr.c_str(), buffstr.length())
<< LL_ENDL;
}
}
diff --git a/indra/llcommon/tests/threadsafeschedule_test.cpp b/indra/llcommon/tests/threadsafeschedule_test.cpp
index c421cc7b1c..876281eae0 100644
--- a/indra/llcommon/tests/threadsafeschedule_test.cpp
+++ b/indra/llcommon/tests/threadsafeschedule_test.cpp
@@ -46,11 +46,11 @@ namespace tut
// the real time required for each push() call. Explicitly increment
// the timestamp for each one -- but since we're passing explicit
// timestamps, make the queue reorder them.
- queue.push(Queue::TimeTuple(Queue::Clock::now() + 200ms, "ghi"));
+ queue.push(Queue::TimeTuple(Queue::Clock::now() + 2000ms, "ghi"));
// Given the various push() overloads, you have to match the type
// exactly: conversions are ambiguous.
queue.push("abc"s);
- queue.push(Queue::Clock::now() + 100ms, "def");
+ queue.push(Queue::Clock::now() + 1000ms, "def");
queue.close();
auto entry = queue.pop();
ensure_equals("failed to pop first", std::get<0>(entry), "abc"s);
diff --git a/indra/llfilesystem/lldir.cpp b/indra/llfilesystem/lldir.cpp
index 69b23f9cf8..e3769c6afb 100644
--- a/indra/llfilesystem/lldir.cpp
+++ b/indra/llfilesystem/lldir.cpp
@@ -469,6 +469,7 @@ static std::string ELLPathToString(ELLPath location)
ENT(LL_PATH_DEFAULT_SKIN)
ENT(LL_PATH_FONTS)
ENT(LL_PATH_LAST)
+ ENT(LL_PATH_SCRIPTS)
;
#undef ENT
@@ -588,6 +589,10 @@ std::string LLDir::getExpandedFilename(ELLPath location, const std::string& subd
case LL_PATH_FONTS:
prefix = add(getAppRODataDir(), "fonts");
break;
+
+ case LL_PATH_SCRIPTS:
+ prefix = add(getAppRODataDir(), "scripts");
+ break;
default:
llassert(0);
diff --git a/indra/llfilesystem/lldir.h b/indra/llfilesystem/lldir.h
index b9a046ba33..7d418159d1 100644
--- a/indra/llfilesystem/lldir.h
+++ b/indra/llfilesystem/lldir.h
@@ -49,6 +49,7 @@ typedef enum ELLPath
LL_PATH_DEFAULT_SKIN = 17,
LL_PATH_FONTS = 18,
LL_PATH_DUMP = 19,
+ LL_PATH_SCRIPTS = 20,
LL_PATH_LAST
} ELLPath;
diff --git a/indra/llprimitive/tests/llgltfmaterial_test.cpp b/indra/llprimitive/tests/llgltfmaterial_test.cpp
index 88b6fae3a7..823a36ec3a 100644
--- a/indra/llprimitive/tests/llgltfmaterial_test.cpp
+++ b/indra/llprimitive/tests/llgltfmaterial_test.cpp
@@ -143,7 +143,7 @@ namespace tut
#if LL_WINDOWS
// If any fields are added/changed, these tests should be updated (consider also updating ASSET_VERSION in LLGLTFMaterial)
// This test result will vary between compilers, so only test a single platform
- ensure_equals("fields supported for GLTF (sizeof check)", sizeof(LLGLTFMaterial), 216);
+ ensure_equals("fields supported for GLTF (sizeof check)", sizeof(LLGLTFMaterial), 232);
#endif
#endif
ensure_equals("LLGLTFMaterial texture info count", (U32)LLGLTFMaterial::GLTF_TEXTURE_INFO_COUNT, 4);
diff --git a/indra/llui/llmenugl.h b/indra/llui/llmenugl.h
index 5c51cf8465..93d5e30ae1 100644
--- a/indra/llui/llmenugl.h
+++ b/indra/llui/llmenugl.h
@@ -562,14 +562,15 @@ public:
// add a context menu branch
BOOL appendContextSubMenu(LLMenuGL *menu);
+ // Add the menu item to this menu.
+ virtual BOOL append( LLMenuItemGL* item );
+
+ // add a menu - this will create a cascading menu
+ virtual BOOL appendMenu(LLMenuGL *menu);
+
protected:
void createSpilloverBranch();
void cleanupSpilloverBranch();
- // Add the menu item to this menu.
- virtual BOOL append( LLMenuItemGL* item );
-
- // add a menu - this will create a cascading menu
- virtual BOOL appendMenu( LLMenuGL* menu );
// Used in LLContextMenu and in LLTogleableMenu
// to add an item of context menu branch
@@ -805,9 +806,10 @@ public:
void resetMenuTrigger() { mAltKeyTrigger = FALSE; }
+ // add a menu - this will create a drop down menu.
+ virtual BOOL appendMenu(LLMenuGL *menu);
+
private:
- // add a menu - this will create a drop down menu.
- virtual BOOL appendMenu( LLMenuGL* menu );
// rearrange the child rects so they fit the shape of the menu
// bar.
virtual void arrange( void );
diff --git a/indra/llui/lltexteditor.cpp b/indra/llui/lltexteditor.cpp
index 092739a538..a43e86104c 100644
--- a/indra/llui/lltexteditor.cpp
+++ b/indra/llui/lltexteditor.cpp
@@ -1582,7 +1582,8 @@ void LLTextEditor::cleanStringForPaste(LLWString & clean_string)
}
-void LLTextEditor::pasteTextWithLinebreaks(LLWString & clean_string)
+template <>
+void LLTextEditor::pasteTextWithLinebreaks<LLWString>(const LLWString & clean_string)
{
std::basic_string<llwchar>::size_type start = 0;
std::basic_string<llwchar>::size_type pos = clean_string.find('\n',start);
diff --git a/indra/llui/lltexteditor.h b/indra/llui/lltexteditor.h
index 521405ec25..658bfb9295 100644
--- a/indra/llui/lltexteditor.h
+++ b/indra/llui/lltexteditor.h
@@ -34,6 +34,7 @@
#include "llstyle.h"
#include "lleditmenuhandler.h"
#include "llviewborder.h" // for params
+#include "llstring.h"
#include "lltextbase.h"
#include "lltextvalidate.h"
@@ -249,7 +250,9 @@ protected:
// Undoable operations
void addChar(llwchar c); // at mCursorPos
S32 addChar(S32 pos, llwchar wc);
+public:
void addLineBreakChar(BOOL group_together = FALSE);
+protected:
S32 overwriteChar(S32 pos, llwchar wc);
void removeChar();
S32 removeChar(S32 pos);
@@ -301,10 +304,19 @@ private:
//
// Methods
//
- void pasteHelper(bool is_primary);
+ void pasteHelper(bool is_primary);
void cleanStringForPaste(LLWString & clean_string);
- void pasteTextWithLinebreaks(LLWString & clean_string);
+public:
+ template <typename STRINGTYPE>
+ void pasteTextWithLinebreaks(const STRINGTYPE& clean_string)
+ {
+ pasteTextWithLinebreaks<LLWString>(ll_convert(clean_string));
+ }
+ template <>
+ void pasteTextWithLinebreaks<LLWString>(const LLWString & clean_string);
+
+private:
void onKeyStroke();
// Concrete TextCmd sub-classes used by the LLTextEditor base class
diff --git a/indra/newview/CMakeLists.txt b/indra/newview/CMakeLists.txt
index 355f35c558..b26ce7d06d 100644
--- a/indra/newview/CMakeLists.txt
+++ b/indra/newview/CMakeLists.txt
@@ -48,6 +48,7 @@ include(VulkanGltf)
include(ZLIBNG)
include(URIPARSER)
include(LLPrimitive)
+include(lualibs)
if (NOT HAVOK_TPV)
# When using HAVOK_TPV, the library is precompiled, so no need for this
@@ -241,6 +242,7 @@ set(viewer_SOURCE_FILES
llfloaterlandholdings.cpp
llfloaterlinkreplace.cpp
llfloaterloadprefpreset.cpp
+ llfloaterluadebug.cpp
llfloatermarketplacelistings.cpp
llfloatermap.cpp
llfloatermediasettings.cpp
@@ -370,6 +372,7 @@ set(viewer_SOURCE_FILES
lllogchat.cpp
llloginhandler.cpp
lllogininstance.cpp
+ llluamanager.cpp
llmachineid.cpp
llmanip.cpp
llmaniprotate.cpp
@@ -897,6 +900,7 @@ set(viewer_HEADER_FILES
llfloaterlandholdings.h
llfloaterlinkreplace.h
llfloaterloadprefpreset.h
+ llfloaterluadebug.h
llfloatermap.h
llfloatermarketplacelistings.h
llfloatermediasettings.h
@@ -1024,6 +1028,7 @@ set(viewer_HEADER_FILES
lllogchat.h
llloginhandler.h
lllogininstance.h
+ llluamanager.h
llmachineid.h
llmanip.h
llmaniprotate.h
@@ -1820,20 +1825,6 @@ if (WINDOWS)
if (PACKAGE)
add_custom_command(
- OUTPUT ${CMAKE_CURRENT_BINARY_DIR}/${CMAKE_CFG_INTDIR}/event_host.tar.bz2
- COMMAND ${PYTHON_EXECUTABLE}
- ARGS
- ${CMAKE_CURRENT_SOURCE_DIR}/event_host_manifest.py
- ${CMAKE_CURRENT_SOURCE_DIR}/..
- ${CMAKE_CURRENT_BINARY_DIR}
- ${CMAKE_CFG_INTDIR}
- DEPENDS
- lleventhost
- ${EVENT_HOST_SCRIPTS}
- ${CMAKE_CURRENT_SOURCE_DIR}/event_host_manifest.py
- )
-
- add_custom_command(
OUTPUT ${CMAKE_CFG_INTDIR}/touched.bat
COMMAND ${PYTHON_EXECUTABLE}
ARGS
@@ -1862,9 +1853,6 @@ if (WINDOWS)
add_custom_target(llpackage ALL DEPENDS
${CMAKE_CFG_INTDIR}/touched.bat
)
- # temporarily disable packaging of event_host until hg subrepos get
- # sorted out on the parabuild cluster...
- #${CMAKE_CURRENT_BINARY_DIR}/${CMAKE_CFG_INTDIR}/event_host.tar.bz2)
endif (PACKAGE)
elseif (DARWIN)
@@ -1925,14 +1913,15 @@ target_link_libraries(${VIEWER_BINARY_NAME}
llcorehttp
llcommon
llmeshoptimizer
- ll::ndof
lllogin
llprimitive
llappearance
${LLPHYSICSEXTENSIONS_LIBRARIES}
ll::bugsplat
- ll::tracy
ll::icu4c
+ ll::lualibs
+ ll::ndof
+ ll::tracy
)
if( TARGET ll::intel_memops )
@@ -2331,6 +2320,11 @@ if (LL_TESTS)
"${test_libs}"
)
+ LL_ADD_INTEGRATION_TEST(llluamanager
+ "llluamanager.cpp"
+ "${test_libs};ll::lualibs"
+ )
+
LL_ADD_INTEGRATION_TEST(llsechandler_basic
llsechandler_basic.cpp
"${test_libs}"
diff --git a/indra/newview/app_settings/cmd_line.xml b/indra/newview/app_settings/cmd_line.xml
index 340334aee8..534d1d594d 100644
--- a/indra/newview/app_settings/cmd_line.xml
+++ b/indra/newview/app_settings/cmd_line.xml
@@ -195,7 +195,33 @@
<string>LogPerformance</string>
</map>
- <key>multiple</key>
+ <key>lua</key>
+ <map>
+ <key>desc</key>
+ <string>Run specified Lua chunk</string>
+ <key>count</key>
+ <integer>1</integer>
+ <!-- you can specify multiple such chunks -->
+ <key>compose</key>
+ <boolean>true</boolean>
+ <key>map-to</key>
+ <string>LuaChunk</string>
+ </map>
+
+ <key>luafile</key>
+ <map>
+ <key>desc</key>
+ <string>Run specified Lua script</string>
+ <key>count</key>
+ <integer>1</integer>
+ <!-- you can specify multiple such scripts -->
+ <key>compose</key>
+ <boolean>true</boolean>
+ <key>map-to</key>
+ <string>LuaScript</string>
+ </map>
+
+ <key>multiple</key>
<map>
<key>desc</key>
<string>Allow multiple viewers.</string>
diff --git a/indra/newview/app_settings/settings.xml b/indra/newview/app_settings/settings.xml
index bd38527462..a84abe0e20 100644
--- a/indra/newview/app_settings/settings.xml
+++ b/indra/newview/app_settings/settings.xml
@@ -68,7 +68,7 @@
<key>Value</key>
<integer>1</integer>
</map>
- <key>CrashHostUrl</key>
+ <key>CrashHostUrl</key>
<map>
<key>Comment</key>
<string>A URL pointing to a crash report handler; overrides cluster negotiation to locate crash handler.</string>
@@ -5431,6 +5431,28 @@
<key>Value</key>
<string>http://wiki.secondlife.com/wiki/[LSL_STRING]</string>
</map>
+ <key>LuaChunk</key>
+ <map>
+ <key>Comment</key>
+ <string>Zero or more Lua chunks to run</string>
+ <key>Persist</key>
+ <integer>0</integer>
+ <key>Type</key>
+ <string>LLSD</string>
+ <key>Value</key>
+ <array />
+ </map>
+ <key>LuaScript</key>
+ <map>
+ <key>Comment</key>
+ <string>Zero or more Lua script files to run</string>
+ <key>Persist</key>
+ <integer>0</integer>
+ <key>Type</key>
+ <string>LLSD</string>
+ <key>Value</key>
+ <array />
+ </map>
<key>GridStatusRSS</key>
<map>
<key>Comment</key>
@@ -17361,6 +17383,17 @@
<key>Value</key>
<integer>3</integer>
</map>
+ <key>AutorunLuaScriptName</key>
+ <map>
+ <key>Comment</key>
+ <string>Script name to autorun after login.</string>
+ <key>Persist</key>
+ <integer>1</integer>
+ <key>Type</key>
+ <string>String</string>
+ <key>Value</key>
+ <string>default.lua</string>
+ </map>
<key>ResetUIScaleOnFirstRun</key>
<map>
<key>Comment</key>
diff --git a/indra/newview/llappviewer.cpp b/indra/newview/llappviewer.cpp
index 4a43133ff6..5ad6f1dc1e 100644
--- a/indra/newview/llappviewer.cpp
+++ b/indra/newview/llappviewer.cpp
@@ -60,6 +60,7 @@
#include "llslurl.h"
#include "llstartup.h"
#include "llfocusmgr.h"
+#include "llluamanager.h"
#include "llurlfloaterdispatchhandler.h"
#include "llviewerjoystick.h"
#include "llallocator.h"
@@ -386,6 +387,9 @@ static std::string gLaunchFileOnQuit;
// Used on Win32 for other apps to identify our window (eg, win_setup)
const char* const VIEWER_WINDOW_CLASSNAME = "Second Life";
+void processComposeSwitch(const std::string&, const std::string&,
+ const std::function<void(const LLSD&)>&);
+
//----------------------------------------------------------------------------
// List of entries from strings.xml to always replace
@@ -1194,22 +1198,10 @@ bool LLAppViewer::init()
}
#endif //LL_RELEASE_FOR_DOWNLOAD
- {
- // Iterate over --leap command-line options. But this is a bit tricky: if
- // there's only one, it won't be an array at all.
- LLSD LeapCommand(gSavedSettings.getLLSD("LeapCommand"));
- LL_DEBUGS("InitInfo") << "LeapCommand: " << LeapCommand << LL_ENDL;
- if (LeapCommand.isDefined() && !LeapCommand.isArray())
- {
- // If LeapCommand is actually a scalar value, make an array of it.
- // Have to do it in two steps because LeapCommand.append(LeapCommand)
- // trashes content! :-P
- LLSD item(LeapCommand);
- LeapCommand.append(item);
- }
- BOOST_FOREACH(const std::string& leap, llsd::inArray(LeapCommand))
+ processComposeSwitch(
+ "--leap", "LeapCommand",
+ [](const LLSD& leap)
{
- LL_INFOS("InitInfo") << "processing --leap \"" << leap << '"' << LL_ENDL;
// We don't have any better description of this plugin than the
// user-specified command line. Passing "" causes LLLeap to derive a
// description from the command line itself.
@@ -1217,8 +1209,21 @@ bool LLAppViewer::init()
// don't consider any one --leap command mission-critical, so if one
// fails, log it, shrug and carry on.
LLLeap::create("", leap, false); // exception=false
- }
- }
+ });
+ processComposeSwitch(
+ "--lua", "LuaChunk",
+ [](const LLSD& chunk)
+ {
+ // no completion callback: we don't need to know
+ LLLUAmanager::runScriptLine(chunk);
+ });
+ processComposeSwitch(
+ "--luafile", "LuaScript",
+ [](const LLSD& script)
+ {
+ // no completion callback: we don't need to know
+ LLLUAmanager::runScriptFile(script);
+ });
if (gSavedSettings.getBOOL("QAMode") && gSavedSettings.getS32("QAModeEventHostPort") > 0)
{
@@ -1291,6 +1296,27 @@ bool LLAppViewer::init()
return true;
}
+void processComposeSwitch(const std::string& option,
+ const std::string& setting,
+ const std::function<void(const LLSD&)>& action)
+{
+ // Iterate over 'option' command-line options. But this is a bit tricky:
+ // if there's only one, it won't be an array at all.
+ LLSD args(gSavedSettings.getLLSD(setting));
+ LL_DEBUGS("InitInfo") << option << ": " << args << LL_ENDL;
+ if (args.isDefined() && ! args.isArray())
+ {
+ // If args is actually a scalar value, make an array of it. Have to do
+ // it in two steps because args.append(args) trashes content! :-P
+ args.append(LLSD(args));
+ }
+ for (const auto& arg : llsd::inArray(args))
+ {
+ LL_INFOS("InitInfo") << "processing " << option << ' ' << arg << LL_ENDL;
+ action(arg);
+ }
+}
+
void LLAppViewer::initMaxHeapSize()
{
//set the max heap size.
diff --git a/indra/newview/llfilepicker.cpp b/indra/newview/llfilepicker.cpp
index 4ad136e13a..68952c96c5 100644
--- a/indra/newview/llfilepicker.cpp
+++ b/indra/newview/llfilepicker.cpp
@@ -64,6 +64,7 @@ LLFilePicker LLFilePicker::sInstance;
#define MATERIAL_TEXTURES_FILTER L"GLTF Import (*.gltf; *.glb; *.tga; *.bmp; *.jpg; *.jpeg; *.png)\0*.gltf;*.glb;*.tga;*.bmp;*.jpg;*.jpeg;*.png\0"
#define SCRIPT_FILTER L"Script files (*.lsl)\0*.lsl\0"
#define DICTIONARY_FILTER L"Dictionary files (*.dic; *.xcu)\0*.dic;*.xcu\0"
+#define LUA_FILTER L"Script files (*.lua)\0*.lua\0"
#endif
#ifdef LL_DARWIN
@@ -236,6 +237,10 @@ BOOL LLFilePicker::setupFilter(ELoadFilter filter)
mOFN.lpstrFilter = DICTIONARY_FILTER \
L"\0";
break;
+ case FFLOAD_LUA:
+ mOFN.lpstrFilter = LUA_FILTER \
+ L"\0";
+ break;
default:
res = FALSE;
break;
diff --git a/indra/newview/llfilepicker.h b/indra/newview/llfilepicker.h
index 38daff9937..f00f076d1c 100644
--- a/indra/newview/llfilepicker.h
+++ b/indra/newview/llfilepicker.h
@@ -89,6 +89,7 @@ public:
FFLOAD_EXE = 14, // Note: EXE will be treated as ALL on Windows and Linux but not on Darwin
FFLOAD_MATERIAL = 15,
FFLOAD_MATERIAL_TEXTURE = 16,
+ FFLOAD_LUA = 17,
};
enum ESaveFilter
diff --git a/indra/newview/llfloaterluadebug.cpp b/indra/newview/llfloaterluadebug.cpp
new file mode 100644
index 0000000000..b63600e0e6
--- /dev/null
+++ b/indra/newview/llfloaterluadebug.cpp
@@ -0,0 +1,154 @@
+/**
+ * @file llfloaterluadebug.cpp
+ *
+ * $LicenseInfo:firstyear=2023&license=viewerlgpl$
+ * Second Life Viewer Source Code
+ * Copyright (C) 2023, 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 "llfloaterluadebug.h"
+
+#include "llcheckboxctrl.h"
+#include "lllineeditor.h"
+#include "lltexteditor.h"
+#include "llviewermenufile.h" // LLFilePickerReplyThread
+
+#include "llagent.h"
+#include "llappearancemgr.h"
+#include "llfloaterreg.h"
+#include "llfloaterimnearbychat.h"
+
+#include "llluamanager.h"
+#include "llsdutil.h"
+#include "lua_function.h"
+#include "stringize.h"
+
+
+LLFloaterLUADebug::LLFloaterLUADebug(const LLSD &key)
+ : LLFloater(key)
+{
+}
+
+
+BOOL LLFloaterLUADebug::postBuild()
+{
+ mResultOutput = getChild<LLTextEditor>("result_text");
+ mLineInput = getChild<LLLineEditor>("lua_cmd");
+ mScriptPath = getChild<LLLineEditor>("script_path");
+ mOutConnection = LLEventPumps::instance().obtain("lua output")
+ .listen("LLFloaterLUADebug",
+ [mResultOutput=mResultOutput](const LLSD& data)
+ {
+ mResultOutput->pasteTextWithLinebreaks(data.asString());
+ mResultOutput->addLineBreakChar(true);
+ return false;
+ });
+
+ getChild<LLButton>("execute_btn")->setClickedCallback(boost::bind(&LLFloaterLUADebug::onExecuteClicked, this));
+ getChild<LLButton>("browse_btn")->setClickedCallback(boost::bind(&LLFloaterLUADebug::onBtnBrowse, this));
+ getChild<LLButton>("run_btn")->setClickedCallback(boost::bind(&LLFloaterLUADebug::onBtnRun, this));
+ mLineInput->setCommitCallback(boost::bind(&LLFloaterLUADebug::onExecuteClicked, this));
+ mLineInput->setSelectAllonCommit(false);
+
+ return TRUE;
+}
+
+LLFloaterLUADebug::~LLFloaterLUADebug()
+{}
+
+void LLFloaterLUADebug::onExecuteClicked()
+{
+ mResultOutput->setValue("");
+
+ std::string cmd = mLineInput->getText();
+ cleanLuaState();
+ LLLUAmanager::runScriptLine(mState, cmd, [this](int count, const LLSD& result)
+ {
+ completion(count, result);
+ });
+}
+
+void LLFloaterLUADebug::onBtnBrowse()
+{
+ LLFilePickerReplyThread::startPicker(boost::bind(&LLFloaterLUADebug::runSelectedScript, this, _1), LLFilePicker::FFLOAD_LUA, false);
+}
+
+void LLFloaterLUADebug::onBtnRun()
+{
+ std::vector<std::string> filenames;
+ std::string filepath = mScriptPath->getText();
+ if (!filepath.empty())
+ {
+ filenames.push_back(filepath);
+ runSelectedScript(filenames);
+ }
+}
+
+void LLFloaterLUADebug::runSelectedScript(const std::vector<std::string> &filenames)
+{
+ mResultOutput->setValue("");
+
+ std::string filepath = filenames[0];
+ if (!filepath.empty())
+ {
+ mScriptPath->setText(filepath);
+ cleanLuaState();
+ LLLUAmanager::runScriptFile(mState, filepath, [this](int count, const LLSD& result)
+ {
+ completion(count, result);
+ });
+ }
+}
+
+void LLFloaterLUADebug::completion(int count, const LLSD& result)
+{
+ if (count < 0)
+ {
+ // error: show error message
+ mResultOutput->insertText("*** ");
+ mResultOutput->pasteTextWithLinebreaks(result.asString());
+ return;
+ }
+ if (count == 1)
+ {
+ // single result
+ mResultOutput->pasteTextWithLinebreaks(stringize(result));
+ return;
+ }
+ // 0 or multiple results
+ const char* sep = "";
+ for (const auto& item : llsd::inArray(result))
+ {
+ mResultOutput->insertText(sep);
+ mResultOutput->pasteTextWithLinebreaks(stringize(item));
+ sep = ", ";
+ }
+}
+
+void LLFloaterLUADebug::cleanLuaState()
+{
+ if(getChild<LLCheckBoxCtrl>("clean_lua_state")->get())
+ {
+ //Reinit to clean lua_State
+ mState.initLuaState();
+ }
+}
diff --git a/indra/newview/llfloaterluadebug.h b/indra/newview/llfloaterluadebug.h
new file mode 100644
index 0000000000..7418174570
--- /dev/null
+++ b/indra/newview/llfloaterluadebug.h
@@ -0,0 +1,72 @@
+/**
+ * @file llfloaterluadebug.h
+ *
+ * $LicenseInfo:firstyear=2023&license=viewerlgpl$
+ * Second Life Viewer Source Code
+ * Copyright (C) 2023, 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_LLFLOATERLUADEBUG_H
+#define LL_LLFLOATERLUADEBUG_H
+
+#include "llevents.h"
+#include "llfloater.h"
+#include "lua_function.h"
+
+extern "C"
+{
+#include "luau/luacode.h"
+#include "luau/lua.h"
+#include "luau/luaconf.h"
+#include "luau/lualib.h"
+}
+
+class LLLineEditor;
+class LLTextEditor;
+
+class LLFloaterLUADebug :
+ public LLFloater
+{
+ public:
+ LLFloaterLUADebug(const LLSD& key);
+ virtual ~LLFloaterLUADebug();
+
+ /*virtual*/ BOOL postBuild();
+
+ void onExecuteClicked();
+ void onBtnBrowse();
+ void onBtnRun();
+
+ void runSelectedScript(const std::vector<std::string> &filenames);
+
+private:
+ void completion(int count, const LLSD& result);
+ void cleanLuaState();
+
+ LLTempBoundListener mOutConnection;
+
+ LLTextEditor* mResultOutput;
+ LLLineEditor* mLineInput;
+ LLLineEditor* mScriptPath;
+ LuaState mState;
+};
+
+#endif // LL_LLFLOATERLUADEBUG_H
+
diff --git a/indra/newview/llinventoryfunctions.cpp b/indra/newview/llinventoryfunctions.cpp
index 4aeacae6ed..56e680a602 100644
--- a/indra/newview/llinventoryfunctions.cpp
+++ b/indra/newview/llinventoryfunctions.cpp
@@ -2645,6 +2645,19 @@ bool LLNameCategoryCollector::operator()(
return false;
}
+bool LLNameItemCollector::operator()(
+ LLInventoryCategory* cat, LLInventoryItem* item)
+{
+ if(item)
+ {
+ if (!LLStringUtil::compareInsensitive(mName, item->getName()))
+ {
+ return true;
+ }
+ }
+ return false;
+}
+
bool LLFindCOFValidItems::operator()(LLInventoryCategory* cat,
LLInventoryItem* item)
{
diff --git a/indra/newview/llinventoryfunctions.h b/indra/newview/llinventoryfunctions.h
index 925217dda3..d63d4179cf 100644
--- a/indra/newview/llinventoryfunctions.h
+++ b/indra/newview/llinventoryfunctions.h
@@ -367,6 +367,22 @@ protected:
};
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+// Class LLNameItemCollector
+//
+// Collects items based on case-insensitive match of prefix
+//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+class LLNameItemCollector : public LLInventoryCollectFunctor
+{
+public:
+ LLNameItemCollector(const std::string& name) : mName(name) {}
+ virtual ~LLNameItemCollector() {}
+ virtual bool operator()(LLInventoryCategory* cat,
+ LLInventoryItem* item);
+protected:
+ std::string mName;
+};
+
+//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// Class LLFindCOFValidItems
//
// Collects items that can be legitimately linked to in the COF.
diff --git a/indra/newview/llinventorymodel.cpp b/indra/newview/llinventorymodel.cpp
index 05aa2e423f..1220f1c45e 100644
--- a/indra/newview/llinventorymodel.cpp
+++ b/indra/newview/llinventorymodel.cpp
@@ -4829,6 +4829,17 @@ std::string LLInventoryModel::getFullPath(const LLInventoryObject *obj) const
return result;
}
+/*
+const LLInventoryObject* LLInventoryModel::findByFullPath(const std::string& path)
+{
+ vector<std::string> path_elts;
+ boost::algorithm::split(path_elts, path, boost::is_any_of("/"));
+ for(path_elts, auto e)
+ {
+ }
+}
+*/
+
///----------------------------------------------------------------------------
/// Local function definitions
///----------------------------------------------------------------------------
diff --git a/indra/newview/llluamanager.cpp b/indra/newview/llluamanager.cpp
new file mode 100644
index 0000000000..cd663021a1
--- /dev/null
+++ b/indra/newview/llluamanager.cpp
@@ -0,0 +1,546 @@
+/**
+ * @file llluamanager.cpp
+ * @brief classes and functions for interfacing with LUA.
+ *
+ * $LicenseInfo:firstyear=2023&license=viewerlgpl$
+ * Second Life Viewer Source Code
+ * Copyright (C) 2023, 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 "llluamanager.h"
+
+#include "llcoros.h"
+#include "llerror.h"
+#include "lleventcoro.h"
+#include "lua_function.h"
+#include "lualistener.h"
+#include "stringize.h"
+
+// skip all these link dependencies for integration testing
+#ifndef LL_TEST
+#include "lluilistener.h"
+#include "llviewercontrol.h"
+
+// FIXME extremely hacky way to get to the UI Listener framework. There's
+// a cleaner way.
+extern LLUIListener sUIListener;
+#endif // ! LL_TEST
+
+#include <boost/algorithm/string/replace.hpp>
+#include <filesystem>
+
+#include "luau/luacode.h"
+#include "luau/lua.h"
+#include "luau/luaconf.h"
+#include "luau/lualib.h"
+
+#include <sstream>
+#include <string_view>
+#include <vector>
+
+lua_function(sleep, "sleep(seconds): pause the running coroutine")
+{
+ F32 seconds = lua_tonumber(L, -1);
+ lua_pop(L, 1);
+ llcoro::suspendUntilTimeout(seconds);
+ return 0;
+};
+
+// This function consumes ALL Lua stack arguments and returns concatenated
+// message string
+std::string lua_print_msg(lua_State* L, const std::string_view& level)
+{
+ // On top of existing Lua arguments, we're going to push tostring() and
+ // duplicate each existing stack entry so we can stringize each one.
+ luaL_checkstack(L, 2, nullptr);
+ luaL_where(L, 1);
+ // start with the 'where' info at the top of the stack
+ std::ostringstream out;
+ out << lua_tostring(L, -1);
+ lua_pop(L, 1);
+ const char* sep = ""; // 'where' info ends with ": "
+ // now iterate over arbitrary args, calling Lua tostring() on each and
+ // concatenating with separators
+ for (int p = 1, top = lua_gettop(L); p <= top; ++p)
+ {
+ out << sep;
+ sep = " ";
+ // push Lua tostring() function -- note, semantically different from
+ // lua_tostring()!
+ lua_getglobal(L, "tostring");
+ // Now the stack is arguments 1 .. N, plus tostring().
+ // Push a copy of the argument at index p.
+ lua_pushvalue(L, p);
+ // pop tostring() and arg-p, pushing tostring(arg-p)
+ // (ignore potential error code from lua_pcall() because, if there was
+ // an error, we expect the stack top to be an error message -- which
+ // we'll print)
+ lua_pcall(L, 1, 1, 0);
+ out << lua_tostring(L, -1);
+ lua_pop(L, 1);
+ }
+ // pop everything
+ lua_settop(L, 0);
+ // capture message string
+ std::string msg{ out.str() };
+ // put message out there for any interested party (*koff* LLFloaterLUADebug *koff*)
+ LLEventPumps::instance().obtain("lua output").post(stringize(level, ": ", msg));
+ return msg;
+}
+
+lua_function(print_debug, "print_debug(args...): DEBUG level logging")
+{
+ LL_DEBUGS("Lua") << lua_print_msg(L, "DEBUG") << LL_ENDL;
+ return 0;
+}
+
+// also used for print(); see LuaState constructor
+lua_function(print_info, "print_info(args...): INFO level logging")
+{
+ LL_INFOS("Lua") << lua_print_msg(L, "INFO") << LL_ENDL;
+ return 0;
+}
+
+lua_function(print_warning, "print_warning(args...): WARNING level logging")
+{
+ LL_WARNS("Lua") << lua_print_msg(L, "WARN") << LL_ENDL;
+ return 0;
+}
+
+#ifndef LL_TEST
+
+lua_function(run_ui_command,
+ "run_ui_command(name [, parameter]): "
+ "call specified UI command with specified parameter")
+{
+ int top = lua_gettop(L);
+ std::string func_name;
+ if (top >= 1)
+ {
+ func_name = lua_tostring(L,1);
+ }
+ std::string parameter;
+ if (top >= 2)
+ {
+ parameter = lua_tostring(L,2);
+ }
+ LL_WARNS("LUA") << "running ui func " << func_name << " parameter " << parameter << LL_ENDL;
+ LLSD event;
+ event["function"] = func_name;
+ if (!parameter.empty())
+ {
+ event["parameter"] = parameter;
+ }
+ sUIListener.call(event);
+
+ lua_settop(L, 0);
+ return 0;
+}
+#endif // ! LL_TEST
+
+lua_function(post_on, "post_on(pumpname, data): post specified data to specified LLEventPump")
+{
+ std::string pumpname{ lua_tostdstring(L, 1) };
+ LLSD data{ lua_tollsd(L, 2) };
+ lua_pop(L, 2);
+ LL_INFOS("Lua") << "post_on('" << pumpname << "', " << data << ")" << LL_ENDL;
+ LLEventPumps::instance().obtain(pumpname).post(data);
+ return 0;
+}
+
+lua_function(get_event_pumps,
+ "get_event_pumps():\n"
+ "Returns replypump, commandpump: names of LLEventPumps specific to this chunk.\n"
+ "Events posted to replypump are queued for get_event_next().\n"
+ "post_on(commandpump, ...) to engage LLEventAPI operations (see helpleap()).")
+{
+ luaL_checkstack(L, 2, nullptr);
+ auto listener{ LuaState::obtainListener(L) };
+ // return the reply pump name and the command pump name on caller's lua_State
+ lua_pushstdstring(L, listener->getReplyName());
+ lua_pushstdstring(L, listener->getCommandName());
+ return 2;
+}
+
+lua_function(get_event_next,
+ "get_event_next():\n"
+ "Returns the next (pumpname, data) pair from the replypump whose name\n"
+ "is returned by get_event_pumps(). Blocks the calling chunk until an\n"
+ "event becomes available.")
+{
+ luaL_checkstack(L, 2, nullptr);
+ auto listener{ LuaState::obtainListener(L) };
+ const auto& [pump, data]{ listener->getNext() };
+ lua_pushstdstring(L, pump);
+ lua_pushllsd(L, data);
+ return 2;
+}
+
+void LLLUAmanager::runScriptFile(const std::string& filename, script_finished_fn cb)
+{
+ // A script_finished_fn is used to initialize the LuaState.
+ // It will be called when the LuaState is destroyed.
+ LuaState L(cb);
+ runScriptFile(L, filename);
+}
+
+void LLLUAmanager::runScriptFile(const std::string& filename, script_result_fn cb)
+{
+ LuaState L;
+ // A script_result_fn will be called when LuaState::expr() completes.
+ runScriptFile(L, filename, cb);
+}
+
+LLCoros::Future<std::pair<int, LLSD>>
+LLLUAmanager::startScriptFile(LuaState& L, const std::string& filename)
+{
+ // Despite returning from startScriptFile(), we need this Promise to
+ // remain alive until the callback has fired.
+ auto promise{ std::make_shared<LLCoros::Promise<std::pair<int, LLSD>>>() };
+ runScriptFile(L, filename,
+ [promise](int count, LLSD result)
+ { promise->set_value({ count, result }); });
+ return LLCoros::getFuture(*promise);
+}
+
+std::pair<int, LLSD> LLLUAmanager::waitScriptFile(LuaState& L, const std::string& filename)
+{
+ return startScriptFile(L, filename).get();
+}
+
+void LLLUAmanager::runScriptFile(LuaState& L, const std::string& filename, script_result_fn cb)
+{
+ LLCoros::instance().launch(filename, [&L, filename, cb]()
+ {
+ llifstream in_file;
+ in_file.open(filename.c_str());
+
+ if (in_file.is_open())
+ {
+ std::string text{std::istreambuf_iterator<char>(in_file), {}};
+ auto [count, result] = L.expr(filename, text);
+ if (cb)
+ {
+ cb(count, result);
+ }
+ }
+ else
+ {
+ auto msg{ stringize("unable to open script file '", filename, "'") };
+ LL_WARNS("Lua") << msg << LL_ENDL;
+ if (cb)
+ {
+ cb(-1, msg);
+ }
+ }
+ });
+}
+
+void LLLUAmanager::runScriptLine(const std::string& chunk, script_finished_fn cb)
+{
+ // A script_finished_fn is used to initialize the LuaState.
+ // It will be called when the LuaState is destroyed.
+ LuaState L(cb);
+ runScriptLine(L, chunk);
+}
+
+void LLLUAmanager::runScriptLine(const std::string& chunk, script_result_fn cb)
+{
+ LuaState L;
+ // A script_result_fn will be called when LuaState::expr() completes.
+ runScriptLine(L, chunk, cb);
+}
+
+LLCoros::Future<std::pair<int, LLSD>>
+LLLUAmanager::startScriptLine(LuaState& L, const std::string& chunk)
+{
+ // Despite returning from startScriptLine(), we need this Promise to
+ // remain alive until the callback has fired.
+ auto promise{ std::make_shared<LLCoros::Promise<std::pair<int, LLSD>>>() };
+ runScriptLine(L, chunk,
+ [promise](int count, LLSD result)
+ { promise->set_value({ count, result }); });
+ return LLCoros::getFuture(*promise);
+}
+
+std::pair<int, LLSD> LLLUAmanager::waitScriptLine(LuaState& L, const std::string& chunk)
+{
+ return startScriptLine(L, chunk).get();
+}
+
+void LLLUAmanager::runScriptLine(LuaState& L, const std::string& chunk, script_result_fn cb)
+{
+ // find a suitable abbreviation for the chunk string
+ std::string shortchunk{ chunk };
+ const size_t shortlen = 40;
+ std::string::size_type eol = shortchunk.find_first_of("\r\n");
+ if (eol != std::string::npos)
+ shortchunk = shortchunk.substr(0, eol);
+ if (shortchunk.length() > shortlen)
+ shortchunk = stringize(shortchunk.substr(0, shortlen), "...");
+
+ std::string desc{ stringize("lua: ", shortchunk) };
+ LLCoros::instance().launch(desc, [&L, desc, chunk, cb]()
+ {
+ auto [count, result] = L.expr(desc, chunk);
+ if (cb)
+ {
+ cb(count, result);
+ }
+ });
+}
+
+void LLLUAmanager::runScriptOnLogin()
+{
+#ifndef LL_TEST
+ std::string filename = gSavedSettings.getString("AutorunLuaScriptName");
+ if (filename.empty())
+ {
+ LL_INFOS() << "Script name wasn't set." << LL_ENDL;
+ return;
+ }
+
+ filename = gDirUtilp->getExpandedFilename(LL_PATH_USER_SETTINGS, filename);
+ if (!gDirUtilp->fileExists(filename))
+ {
+ LL_INFOS() << filename << " was not found." << LL_ENDL;
+ return;
+ }
+
+ runScriptFile(filename);
+#endif // ! LL_TEST
+}
+
+std::string read_file(const std::string &name)
+{
+ llifstream in_file;
+ in_file.open(name.c_str());
+
+ if (in_file.is_open())
+ {
+ return std::string{std::istreambuf_iterator<char>(in_file), {}};
+ }
+
+ return {};
+}
+
+lua_function(require, "require(module_name) : load module_name.lua from known places")
+{
+ std::string name = lua_tostdstring(L, 1);
+ lua_pop(L, 1);
+
+ // resolveRequire() does not return in case of error.
+ LLRequireResolver::resolveRequire(L, name);
+
+ // resolveRequire() returned the newly-loaded module on the stack top.
+ // Return it.
+ return 1;
+}
+
+// push loaded module or throw Lua error
+void LLRequireResolver::resolveRequire(lua_State *L, std::string path)
+{
+ LLRequireResolver resolver(L, std::move(path));
+ // findModule() pushes the loaded module or throws a Lua error.
+ resolver.findModule();
+}
+
+LLRequireResolver::LLRequireResolver(lua_State *L, const std::string& path) :
+ mPathToResolve(path),
+ L(L)
+{
+ //Luau lua_Debug and lua_getinfo() are different compared to default Lua:
+ //see https://github.com/luau-lang/luau/blob/80928acb92d1e4b6db16bada6d21b1fb6fa66265/VM/include/lua.h
+ lua_Debug ar;
+ lua_getinfo(L, 1, "s", &ar);
+ mSourceChunkname = ar.source;
+
+ std::filesystem::path fs_path(mPathToResolve);
+ mPathToResolve = fs_path.lexically_normal().string();
+
+ if (fs_path.is_absolute())
+ luaL_argerrorL(L, 1, "cannot require a full path");
+}
+
+/**
+ * Remove a particular stack index on exit from enclosing scope.
+ * If you pass a negative index (meaning relative to the current stack top),
+ * converts to an absolute index. The point of LuaRemover is to remove the
+ * entry at the specified index regardless of subsequent pushes to the stack.
+ */
+class LuaRemover
+{
+public:
+ LuaRemover(lua_State* L, int index):
+ mState(L),
+ mIndex(lua_absindex(L, index))
+ {}
+ LuaRemover(const LuaRemover&) = delete;
+ LuaRemover& operator=(const LuaRemover&) = delete;
+ ~LuaRemover()
+ {
+ lua_remove(mState, mIndex);
+ }
+
+private:
+ lua_State* mState;
+ int mIndex;
+};
+
+// push the loaded module or throw a Lua error
+void LLRequireResolver::findModule()
+{
+ // If mPathToResolve is absolute, this replaces mSourceChunkname.parent_path.
+ auto absolutePath = (std::filesystem::path((mSourceChunkname)).parent_path() / mPathToResolve).u8string();
+
+ // Push _MODULES table on stack for checking and saving to the cache
+ luaL_findtable(L, LUA_REGISTRYINDEX, "_MODULES", 1);
+ // Remove that stack entry no matter how we exit
+ LuaRemover rm_MODULES(L, -1);
+
+ // Check if the module is already in _MODULES table, read from file
+ // otherwise.
+ // findModuleImpl() pushes module if found, nothing if not, may throw Lua
+ // error.
+ if (findModuleImpl(absolutePath))
+ return;
+
+ // not already cached - prep error message just in case
+ auto fail{
+ [L=L, path=mPathToResolve]()
+ { luaL_error(L, "could not find require('%s')", path.data()); }};
+
+ if (std::filesystem::path(mPathToResolve).is_absolute())
+ {
+ // no point searching known directories for an absolute path
+ fail();
+ }
+
+ std::vector<std::string> lib_paths {gDirUtilp->getExpandedFilename(LL_PATH_SCRIPTS, "lua")};
+
+ for (const auto& path : lib_paths)
+ {
+ std::string absolutePathOpt = (std::filesystem::path(path) / mPathToResolve).u8string();
+
+ if (absolutePathOpt.empty())
+ luaL_error(L, "error requiring module '%s'", mPathToResolve.data());
+
+ if (findModuleImpl(absolutePathOpt))
+ return;
+ }
+
+ // not found
+ fail();
+}
+
+// expects _MODULES table on stack top (and leaves it there)
+// - if found, pushes loaded module and returns true
+// - not found, pushes nothing and returns false
+// - may throw Lua error
+bool LLRequireResolver::findModuleImpl(const std::string& absolutePath)
+{
+ std::string possibleSuffixedPaths[] = {absolutePath + ".luau", absolutePath + ".lua"};
+
+ for (const auto& suffixedPath : possibleSuffixedPaths)
+ {
+ // Check _MODULES cache for module
+ lua_getfield(L, -1, suffixedPath.data());
+ if (!lua_isnil(L, -1))
+ {
+ return true;
+ }
+ lua_pop(L, 1);
+
+ // Try to read the matching file
+ std::string source = read_file(suffixedPath);
+ if (!source.empty())
+ {
+ // Try to run the loaded source. This will leave either a string
+ // error message or the module contents on the stack top.
+ runModule(suffixedPath, source);
+
+ // If the stack top is an error message string, raise it.
+ if (lua_isstring(L, -1))
+ lua_error(L);
+
+ // duplicate the new module: _MODULES newmodule newmodule
+ lua_pushvalue(L, -1);
+ // store _MODULES[found path] = newmodule
+ lua_setfield(L, -3, suffixedPath.data());
+
+ return true;
+ }
+ }
+
+ return false;
+}
+
+// push string error message or new module
+void LLRequireResolver::runModule(const std::string& desc, const std::string& code)
+{
+ // Here we just loaded a new module 'code', need to run it and get its result.
+ // Module needs to run in a new thread, isolated from the rest.
+ // Note: we create ML on main thread so that it doesn't inherit environment of L.
+ lua_State *GL = lua_mainthread(L);
+ lua_State *ML = lua_newthread(GL);
+ // lua_newthread() pushed the new thread object on GL's stack. Move to L's.
+ lua_xmove(GL, L, 1);
+
+ // new thread needs to have the globals sandboxed
+ luaL_sandboxthread(ML);
+
+ {
+ // If loadstring() returns (! LUA_OK) then there's an error message on
+ // the stack. If it returns LUA_OK then the newly-loaded module code
+ // is on the stack.
+ if (lluau::loadstring(ML, desc, code) == LUA_OK)
+ {
+ // luau uses Lua 5.3's version of lua_resume():
+ // run the coroutine on ML, "from" L, passing no arguments.
+ int status = lua_resume(ML, L, 0);
+
+ if (status == LUA_OK)
+ {
+ if (lua_gettop(ML) == 0)
+ lua_pushfstring(ML, "module %s must return a value", desc.data());
+ else if (!lua_istable(ML, -1) && !lua_isfunction(ML, -1))
+ lua_pushfstring(ML, "module %s must return a table or function, not %s",
+ desc.data(), lua_typename(ML, lua_type(ML, -1)));
+ }
+ else if (status == LUA_YIELD)
+ {
+ lua_pushfstring(ML, "module %s can not yield", desc.data());
+ }
+ else if (!lua_isstring(ML, -1))
+ {
+ lua_pushfstring(ML, "unknown error while running module %s", desc.data());
+ }
+ }
+ }
+ // There's now a return value (string error message or module) on top of ML.
+ // Move return value to L's stack.
+ lua_xmove(ML, L, 1);
+ // remove ML from L's stack
+ lua_remove(L, -2);
+// // DON'T call lua_close(ML)! Since ML is only a thread of L, corrupts L too!
+// lua_close(ML);
+}
diff --git a/indra/newview/llluamanager.h b/indra/newview/llluamanager.h
new file mode 100644
index 0000000000..fb9f1b8141
--- /dev/null
+++ b/indra/newview/llluamanager.h
@@ -0,0 +1,104 @@
+/**
+ * @file llluamanager.h
+ * @brief classes and functions for interfacing with LUA.
+ *
+ * $LicenseInfo:firstyear=2023&license=viewerlgpl$
+ * Second Life Viewer Source Code
+ * Copyright (C) 2023, 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_LLLUAMANAGER_H
+#define LL_LLLUAMANAGER_H
+
+#include "llcoros.h"
+#include "llsd.h"
+#include <functional>
+#include <string>
+#include <utility> // std::pair
+
+#include "luau/lua.h"
+#include "luau/lualib.h"
+
+class LuaState;
+
+class LLLUAmanager
+{
+public:
+ // Pass a callback with this signature to obtain the error message, if
+ // any, from running a script or source string. Empty msg means success.
+ typedef std::function<void(std::string msg)> script_finished_fn;
+ // Pass a callback with this signature to obtain the result, if any, of
+ // running a script or source string.
+ // count < 0 means error, and result.asString() is the error message.
+ // count == 0 with result.isUndefined() means the script returned no results.
+ // count == 1 means the script returned one result.
+ // count > 1 with result.isArray() means the script returned multiple
+ // results, represented as the entries of the result array.
+ typedef std::function<void(int count, const LLSD& result)> script_result_fn;
+
+ static void runScriptFile(const std::string &filename, script_finished_fn cb = {});
+ static void runScriptFile(const std::string &filename, script_result_fn cb);
+ static void runScriptFile(LuaState& L, const std::string &filename, script_result_fn cb = {});
+ // Start running a Lua script file, returning an LLCoros::Future whose
+ // get() method will pause the calling coroutine until it can deliver the
+ // (count, result) pair described above. Between startScriptFile() and
+ // Future::get(), the caller and the Lua script coroutine will run
+ // concurrently.
+ static LLCoros::Future<std::pair<int, LLSD>>
+ startScriptFile(LuaState& L, const std::string& filename);
+ // Run a Lua script file, and pause the calling coroutine until it completes.
+ // The return value is the (count, result) pair described above.
+ static std::pair<int, LLSD> waitScriptFile(LuaState& L, const std::string& filename);
+
+ static void runScriptLine(const std::string &chunk, script_finished_fn cb = {});
+ static void runScriptLine(const std::string &chunk, script_result_fn cb);
+ static void runScriptLine(LuaState& L, const std::string &chunk, script_result_fn cb = {});
+ // Start running a Lua chunk, returning an LLCoros::Future whose
+ // get() method will pause the calling coroutine until it can deliver the
+ // (count, result) pair described above. Between startScriptLine() and
+ // Future::get(), the caller and the Lua script coroutine will run
+ // concurrently.
+ static LLCoros::Future<std::pair<int, LLSD>>
+ startScriptLine(LuaState& L, const std::string& chunk);
+ // Run a Lua chunk, and pause the calling coroutine until it completes.
+ // The return value is the (count, result) pair described above.
+ static std::pair<int, LLSD> waitScriptLine(LuaState& L, const std::string& chunk);
+
+ static void runScriptOnLogin();
+};
+
+class LLRequireResolver
+{
+ public:
+ static void resolveRequire(lua_State *L, std::string path);
+
+ private:
+ std::string mPathToResolve;
+ std::string mSourceChunkname;
+
+ LLRequireResolver(lua_State *L, const std::string& path);
+
+ void findModule();
+ lua_State *L;
+
+ bool findModuleImpl(const std::string& absolutePath);
+ void runModule(const std::string& desc, const std::string& code);
+};
+#endif
diff --git a/indra/newview/llstartup.cpp b/indra/newview/llstartup.cpp
index d0b76848f7..6c5a8fab8c 100644
--- a/indra/newview/llstartup.cpp
+++ b/indra/newview/llstartup.cpp
@@ -208,6 +208,7 @@
#include "llstacktrace.h"
#include "threadpool.h"
+#include "llluamanager.h"
#include "llperfstats.h"
@@ -2422,6 +2423,8 @@ bool idle_startup()
LLPerfStats::StatsRecorder::setAutotuneInit();
+ LLLUAmanager::runScriptOnLogin();
+
return TRUE;
}
diff --git a/indra/newview/lltoolplacer.cpp b/indra/newview/lltoolplacer.cpp
index 7cdd7cc5c8..a4e6d20181 100644
--- a/indra/newview/lltoolplacer.cpp
+++ b/indra/newview/lltoolplacer.cpp
@@ -164,13 +164,17 @@ BOOL LLToolPlacer::addObject( LLPCode pcode, S32 x, S32 y, U8 use_physics )
BOOL b_hit_land = FALSE;
S32 hit_face = -1;
LLViewerObject* hit_obj = NULL;
- U8 state = 0;
BOOL success = raycastForNewObjPos( x, y, &hit_obj, &hit_face, &b_hit_land, &ray_start_region, &ray_end_region, &regionp );
if( !success )
{
return FALSE;
}
+ return rezNewObject(pcode,hit_obj, hit_face, b_hit_land, ray_start_region, ray_end_region, regionp, use_physics);
+}
+BOOL LLToolPlacer::rezNewObject(LLPCode pcode, LLViewerObject * hit_obj, S32 hit_face, BOOL b_hit_land, LLVector3 ray_start_region,
+ LLVector3 ray_end_region, LLViewerRegion* regionp, U8 use_physics)
+{
if( hit_obj && (hit_obj->isAvatar() || hit_obj->isAttachment()) )
{
// Can't create objects on avatars or attachments
@@ -194,6 +198,8 @@ BOOL LLToolPlacer::addObject( LLPCode pcode, S32 x, S32 y, U8 use_physics )
U8 material = LL_MCODE_WOOD;
BOOL create_selected = FALSE;
LLVolumeParams volume_params;
+
+ U8 state = 0;
switch (pcode)
{
diff --git a/indra/newview/lltoolplacer.h b/indra/newview/lltoolplacer.h
index ad59cb0daa..d20d682710 100644
--- a/indra/newview/lltoolplacer.h
+++ b/indra/newview/lltoolplacer.h
@@ -50,12 +50,16 @@ public:
static void setObjectType( LLPCode type ) { sObjectType = type; }
static LLPCode getObjectType() { return sObjectType; }
+ static BOOL addObject(LLPCode pcode, S32 x, S32 y, U8 use_physics);
+ static BOOL rezNewObject(LLPCode pcode, LLViewerObject* hit_obj, S32 hit_face, BOOL b_hit_land, LLVector3 ray_start_region,
+ LLVector3 ray_end_region, LLViewerRegion *regionp, U8 use_physics);
+
protected:
static LLPCode sObjectType;
private:
- BOOL addObject( LLPCode pcode, S32 x, S32 y, U8 use_physics );
- BOOL raycastForNewObjPos( S32 x, S32 y, LLViewerObject** hit_obj, S32* hit_face,
+
+ static BOOL raycastForNewObjPos(S32 x, S32 y, LLViewerObject **hit_obj, S32 *hit_face,
BOOL* b_hit_land, LLVector3* ray_start_region, LLVector3* ray_end_region, LLViewerRegion** region );
BOOL addDuplicate(S32 x, S32 y);
};
diff --git a/indra/newview/lluilistener.cpp b/indra/newview/lluilistener.cpp
index 956f5cf187..97a2be6aa3 100644
--- a/indra/newview/lluilistener.cpp
+++ b/indra/newview/lluilistener.cpp
@@ -67,7 +67,7 @@ void LLUIListener::call(const LLSD& event) const
// API: we provide no reply. Therefore, a typo in the script will
// provide no feedback whatsoever to that script. To rub the coder's
// nose in such an error, crump rather than quietly ignoring it.
- LL_ERRS("LLUIListener") << "function '" << event["function"] << "' not found" << LL_ENDL;
+ LL_WARNS("LLUIListener") << "function '" << event["function"] << "' not found" << LL_ENDL;
}
else
{
diff --git a/indra/newview/lluilistener.h b/indra/newview/lluilistener.h
index 08724024dc..e53984dae2 100644
--- a/indra/newview/lluilistener.h
+++ b/indra/newview/lluilistener.h
@@ -39,7 +39,8 @@ class LLUIListener: public LLEventAPI
public:
LLUIListener();
-private:
+// FIXME These fields are intended to be private, changed here to support very hacky code in llluamanager.cpp
+public:
void call(const LLSD& event) const;
void getValue(const LLSD&event) const;
};
diff --git a/indra/newview/llviewerfloaterreg.cpp b/indra/newview/llviewerfloaterreg.cpp
index 8353d4d1d7..f72ef71241 100644
--- a/indra/newview/llviewerfloaterreg.cpp
+++ b/indra/newview/llviewerfloaterreg.cpp
@@ -93,6 +93,7 @@
#include "llfloaterlandholdings.h"
#include "llfloaterlinkreplace.h"
#include "llfloaterloadprefpreset.h"
+#include "llfloaterluadebug.h"
#include "llfloatermap.h"
#include "llfloatermarketplacelistings.h"
#include "llfloatermediasettings.h"
@@ -399,6 +400,8 @@ void LLViewerFloaterReg::registerFloaters()
LLFloaterReg::add("land_holdings", "floater_land_holdings.xml", (LLFloaterBuildFunc)&LLFloaterReg::build<LLFloaterLandHoldings>);
LLFloaterReg::add("linkreplace", "floater_linkreplace.xml", (LLFloaterBuildFunc)&LLFloaterReg::build<LLFloaterLinkReplace>);
LLFloaterReg::add("load_pref_preset", "floater_load_pref_preset.xml", (LLFloaterBuildFunc)&LLFloaterReg::build<LLFloaterLoadPrefPreset>);
+
+ LLFloaterReg::add("lua_debug", "floater_lua_debug.xml", (LLFloaterBuildFunc) &LLFloaterReg::build<LLFloaterLUADebug>);
LLFloaterReg::add("mem_leaking", "floater_mem_leaking.xml", (LLFloaterBuildFunc)&LLFloaterReg::build<LLFloaterMemLeak>);
diff --git a/indra/newview/llviewermenu.cpp b/indra/newview/llviewermenu.cpp
index 2cf341f87f..f1a7dc9b5e 100644
--- a/indra/newview/llviewermenu.cpp
+++ b/indra/newview/llviewermenu.cpp
@@ -172,6 +172,8 @@ extern BOOL gShaderProfileFrame;
// Globals
//
+LLUIListener sUIListener;
+
LLMenuBarGL *gMenuBarView = NULL;
LLViewerMenuHolderGL *gMenuHolder = NULL;
LLMenuGL *gPopupMenuView = NULL;
@@ -348,8 +350,6 @@ public:
static LLMenuParcelObserver* gMenuParcelObserver = NULL;
-static LLUIListener sUIListener;
-
LLMenuParcelObserver::LLMenuParcelObserver()
{
LLViewerParcelMgr::getInstance()->addObserver(this);
@@ -8914,80 +8914,89 @@ class LLToolsSelectTool : public view_listener_t
};
/// WINDLIGHT callbacks
-class LLWorldEnvSettings : public view_listener_t
-{
- void defocusEnvFloaters()
+void defocusEnvFloaters()
+{
+ // currently there is only one instance of each floater
+ std::vector<std::string> env_floaters_names = {"env_edit_extdaycycle", "env_fixed_environmentent_water",
+ "env_fixed_environmentent_sky"};
+ for (std::vector<std::string>::const_iterator it = env_floaters_names.begin(); it != env_floaters_names.end(); ++it)
{
- //currently there is only one instance of each floater
- std::vector<std::string> env_floaters_names = { "env_edit_extdaycycle", "env_fixed_environmentent_water", "env_fixed_environmentent_sky" };
- for (std::vector<std::string>::const_iterator it = env_floaters_names.begin(); it != env_floaters_names.end(); ++it)
+ LLFloater *env_floater = LLFloaterReg::findTypedInstance<LLFloater>(*it);
+ if (env_floater)
{
- LLFloater* env_floater = LLFloaterReg::findTypedInstance<LLFloater>(*it);
- if (env_floater)
- {
- env_floater->setFocus(FALSE);
- }
+ env_floater->setFocus(FALSE);
}
}
+}
+
+bool handle_env_setting_event(std::string event_name)
+{
+ if (event_name == "sunrise")
+ {
+ LLEnvironment::instance().setEnvironment(LLEnvironment::ENV_LOCAL, LLEnvironment::KNOWN_SKY_SUNRISE,
+ LLEnvironment::TRANSITION_INSTANT);
+ LLEnvironment::instance().setSelectedEnvironment(LLEnvironment::ENV_LOCAL, LLEnvironment::TRANSITION_INSTANT);
+ defocusEnvFloaters();
+ }
+ else if (event_name == "noon")
+ {
+ LLEnvironment::instance().setEnvironment(LLEnvironment::ENV_LOCAL, LLEnvironment::KNOWN_SKY_MIDDAY,
+ LLEnvironment::TRANSITION_INSTANT);
+ LLEnvironment::instance().setSelectedEnvironment(LLEnvironment::ENV_LOCAL, LLEnvironment::TRANSITION_INSTANT);
+ defocusEnvFloaters();
+ }
+ else if (event_name == "legacy noon")
+ {
+ LLEnvironment::instance().setEnvironment(LLEnvironment::ENV_LOCAL, LLEnvironment::KNOWN_SKY_LEGACY_MIDDAY, LLEnvironment::TRANSITION_INSTANT);
+ LLEnvironment::instance().setSelectedEnvironment(LLEnvironment::ENV_LOCAL, LLEnvironment::TRANSITION_INSTANT);
+ defocusEnvFloaters();
+ }
+ else if (event_name == "sunset")
+ {
+ LLEnvironment::instance().setEnvironment(LLEnvironment::ENV_LOCAL, LLEnvironment::KNOWN_SKY_SUNSET,
+ LLEnvironment::TRANSITION_INSTANT);
+ LLEnvironment::instance().setSelectedEnvironment(LLEnvironment::ENV_LOCAL, LLEnvironment::TRANSITION_INSTANT);
+ defocusEnvFloaters();
+ }
+ else if (event_name == "midnight")
+ {
+ LLEnvironment::instance().setEnvironment(LLEnvironment::ENV_LOCAL, LLEnvironment::KNOWN_SKY_MIDNIGHT,
+ LLEnvironment::TRANSITION_INSTANT);
+ LLEnvironment::instance().setSelectedEnvironment(LLEnvironment::ENV_LOCAL, LLEnvironment::TRANSITION_INSTANT);
+ defocusEnvFloaters();
+ }
+ else if (event_name == "region")
+ {
+ // reset probe data when reverting back to region sky setting
+ gPipeline.mReflectionMapManager.reset();
+
+ LLEnvironment::instance().clearEnvironment(LLEnvironment::ENV_LOCAL);
+ LLEnvironment::instance().setSelectedEnvironment(LLEnvironment::ENV_LOCAL, LLEnvironment::TRANSITION_INSTANT);
+ defocusEnvFloaters();
+ }
+ else if (event_name == "pause_clouds")
+ {
+ if (LLEnvironment::instance().isCloudScrollPaused())
+ LLEnvironment::instance().resumeCloudScroll();
+ else
+ LLEnvironment::instance().pauseCloudScroll();
+ }
+ else if (event_name == "adjust_tool")
+ {
+ LLFloaterReg::showInstance("env_adjust_snapshot");
+ }
+ else if (event_name == "my_environs")
+ {
+ LLFloaterReg::showInstance("my_environments");
+ }
+ return true;
+}
+class LLWorldEnvSettings : public view_listener_t
+{
bool handleEvent(const LLSD& userdata)
{
- std::string event_name = userdata.asString();
-
- if (event_name == "sunrise")
- {
- LLEnvironment::instance().setEnvironment(LLEnvironment::ENV_LOCAL, LLEnvironment::KNOWN_SKY_SUNRISE, LLEnvironment::TRANSITION_INSTANT);
- LLEnvironment::instance().setSelectedEnvironment(LLEnvironment::ENV_LOCAL, LLEnvironment::TRANSITION_INSTANT);
- defocusEnvFloaters();
- }
- else if (event_name == "noon")
- {
- LLEnvironment::instance().setEnvironment(LLEnvironment::ENV_LOCAL, LLEnvironment::KNOWN_SKY_MIDDAY, LLEnvironment::TRANSITION_INSTANT);
- LLEnvironment::instance().setSelectedEnvironment(LLEnvironment::ENV_LOCAL, LLEnvironment::TRANSITION_INSTANT);
- defocusEnvFloaters();
- }
- else if (event_name == "legacy noon")
- {
- LLEnvironment::instance().setEnvironment(LLEnvironment::ENV_LOCAL, LLEnvironment::KNOWN_SKY_LEGACY_MIDDAY, LLEnvironment::TRANSITION_INSTANT);
- LLEnvironment::instance().setSelectedEnvironment(LLEnvironment::ENV_LOCAL, LLEnvironment::TRANSITION_INSTANT);
- defocusEnvFloaters();
- }
- else if (event_name == "sunset")
- {
- LLEnvironment::instance().setEnvironment(LLEnvironment::ENV_LOCAL, LLEnvironment::KNOWN_SKY_SUNSET, LLEnvironment::TRANSITION_INSTANT);
- LLEnvironment::instance().setSelectedEnvironment(LLEnvironment::ENV_LOCAL, LLEnvironment::TRANSITION_INSTANT);
- defocusEnvFloaters();
- }
- else if (event_name == "midnight")
- {
- LLEnvironment::instance().setEnvironment(LLEnvironment::ENV_LOCAL, LLEnvironment::KNOWN_SKY_MIDNIGHT, LLEnvironment::TRANSITION_INSTANT);
- LLEnvironment::instance().setSelectedEnvironment(LLEnvironment::ENV_LOCAL, LLEnvironment::TRANSITION_INSTANT);
- defocusEnvFloaters();
- }
- else if (event_name == "region")
- {
- // reset probe data when reverting back to region sky setting
- gPipeline.mReflectionMapManager.reset();
-
- LLEnvironment::instance().clearEnvironment(LLEnvironment::ENV_LOCAL);
- LLEnvironment::instance().setSelectedEnvironment(LLEnvironment::ENV_LOCAL, LLEnvironment::TRANSITION_INSTANT);
- defocusEnvFloaters();
- }
- else if (event_name == "pause_clouds")
- {
- if (LLEnvironment::instance().isCloudScrollPaused())
- LLEnvironment::instance().resumeCloudScroll();
- else
- LLEnvironment::instance().pauseCloudScroll();
- }
- else if (event_name == "adjust_tool")
- {
- LLFloaterReg::showInstance("env_adjust_snapshot");
- }
- else if (event_name == "my_environs")
- {
- LLFloaterReg::showInstance("my_environments");
- }
+ handle_env_setting_event(userdata.asString());
return true;
}
diff --git a/indra/newview/llviewermenu.h b/indra/newview/llviewermenu.h
index 7142763451..b43de3fe3f 100644
--- a/indra/newview/llviewermenu.h
+++ b/indra/newview/llviewermenu.h
@@ -143,6 +143,7 @@ void handle_give_money_dialog();
bool enable_pay_object();
bool enable_buy_object();
bool handle_go_to();
+bool handle_env_setting_event(std::string event_name);
// Export to XML or Collada
void handle_export_selected( void * );
diff --git a/indra/newview/llviewermenufile.cpp b/indra/newview/llviewermenufile.cpp
index 5461e0f362..a1dc7119ce 100644
--- a/indra/newview/llviewermenufile.cpp
+++ b/indra/newview/llviewermenufile.cpp
@@ -840,16 +840,22 @@ class LLFileEnableCloseAllWindows : public view_listener_t
}
};
+void close_all_windows()
+{
+ bool app_quitting = false;
+ gFloaterView->closeAllChildren(app_quitting);
+ LLFloaterSnapshot *floater_snapshot = LLFloaterSnapshot::findInstance();
+ if (floater_snapshot)
+ floater_snapshot->closeFloater(app_quitting);
+ if (gMenuHolder)
+ gMenuHolder->hideMenus();
+}
+
class LLFileCloseAllWindows : public view_listener_t
{
bool handleEvent(const LLSD& userdata)
{
- bool app_quitting = false;
- gFloaterView->closeAllChildren(app_quitting);
- LLFloaterSnapshot* floater_snapshot = LLFloaterSnapshot::findInstance();
- if (floater_snapshot)
- floater_snapshot->closeFloater(app_quitting);
- if (gMenuHolder) gMenuHolder->hideMenus();
+ close_all_windows();
return true;
}
};
diff --git a/indra/newview/llviewermenufile.h b/indra/newview/llviewermenufile.h
index ff2ee693fd..8f4755903b 100644
--- a/indra/newview/llviewermenufile.h
+++ b/indra/newview/llviewermenufile.h
@@ -72,6 +72,8 @@ void assign_defaults_and_show_upload_message(
const std::string& display_name,
std::string& description);
+void close_all_windows();
+
//consider moving all file pickers below to more suitable place
class LLFilePickerThread : public LLThread
{ //multi-threaded file picker (runs system specific file picker in background and calls "notify" from main thread)
diff --git a/indra/newview/scripts/lua/Queue.lua b/indra/newview/scripts/lua/Queue.lua
new file mode 100644
index 0000000000..e178ad9969
--- /dev/null
+++ b/indra/newview/scripts/lua/Queue.lua
@@ -0,0 +1,40 @@
+-- from https://create.roblox.com/docs/luau/queues#implementing-queues
+
+local Queue = {}
+Queue.__index = Queue
+
+function Queue.new()
+ local self = setmetatable({}, Queue)
+
+ self._first = 0
+ self._last = -1
+ self._queue = {}
+
+ return self
+end
+
+-- Check if the queue is empty
+function Queue:IsEmpty()
+ return self._first > self._last
+end
+
+-- Add a value to the queue
+function Queue:Enqueue(value)
+ local last = self._last + 1
+ self._last = last
+ self._queue[last] = value
+end
+
+-- Remove a value from the queue
+function Queue:Dequeue()
+ local first = self._first
+ if self:IsEmpty() then
+ return nil
+ end
+ local value = self._queue[first]
+ self._queue[first] = nil
+ self._first = first + 1
+ return value
+end
+
+return Queue
diff --git a/indra/newview/scripts/lua/testmod.lua b/indra/newview/scripts/lua/testmod.lua
new file mode 100644
index 0000000000..60f7f80db1
--- /dev/null
+++ b/indra/newview/scripts/lua/testmod.lua
@@ -0,0 +1,2 @@
+print('loaded scripts/lua/testmod.lua')
+return function () return 'hello from scripts/lua/testmod.lua' end
diff --git a/indra/newview/skins/default/xui/en/floater_lua_debug.xml b/indra/newview/skins/default/xui/en/floater_lua_debug.xml
new file mode 100644
index 0000000000..a028a1802c
--- /dev/null
+++ b/indra/newview/skins/default/xui/en/floater_lua_debug.xml
@@ -0,0 +1,117 @@
+<?xml version="1.0" encoding="utf-8" standalone="yes" ?>
+<floater
+ can_minimize="false"
+ can_resize="false"
+ can_close="true"
+ bevel_style="in"
+ height="220"
+ layout="topleft"
+ name="LUA debug"
+ save_rect="true"
+ title="LUA DEBUG"
+ single_instance="true"
+ width="535">
+ <text
+ type="string"
+ length="1"
+ follows="left|bottom"
+ font="SansSerif"
+ height="30"
+ layout="topleft"
+ left="10"
+ left_delta="10"
+ name="editor_path_label"
+ top="10"
+ width="100">
+ LUA string:
+ </text>
+ <check_box
+ follows="left|bottom"
+ height="15"
+ label="Use clean lua_State"
+ layout="topleft"
+ top="10"
+ right ="-70"
+ name="clean_lua_state"
+ width="70"/>
+ <line_editor
+ border_style="line"
+ border_thickness="1"
+ follows="left|bottom"
+ font="SansSerif"
+ height="20"
+ layout="topleft"
+ left="10"
+ max_length_bytes="300"
+ name="lua_cmd"
+ select_on_focus="true"
+ top_delta="30"
+ width="435" />
+ <button
+ follows="left|bottom"
+ height="25"
+ label="Execute"
+ layout="topleft"
+ left_pad="5"
+ name="execute_btn"
+ top_delta="-2"
+ width="75" />
+
+ <text_editor
+ enabled="false"
+ left="10"
+ height="95"
+ layout="topleft"
+ name="result_text"
+ follows="left|top"
+ max_length="65536"
+ width="515"
+ top_delta="40"
+ word_wrap="true" />
+ <text
+ type="string"
+ length="1"
+ follows="left|bottom"
+ font="SansSerif"
+ height="30"
+ layout="topleft"
+ left="10"
+ name="path_label"
+ top_pad="15"
+ width="100">
+ File Path:
+ </text>
+ <line_editor
+ border_style="line"
+ enabled="false"
+ border_thickness="1"
+ follows="left|bottom"
+ font="SansSerif"
+ height="20"
+ layout="topleft"
+ left_delta="65"
+ max_length_bytes="300"
+ name="script_path"
+ select_on_focus="true"
+ top_delta="-2"
+ width="320" />
+ <button
+ follows="left|bottom"
+ height="25"
+ label="Browse..."
+ label_selected="Browse..."
+ layout="topleft"
+ left_pad="5"
+ name="browse_btn"
+ top_delta="-2"
+ width="70" />
+ <button
+ follows="left|bottom"
+ height="25"
+ label="Run"
+ label_selected="Run"
+ layout="topleft"
+ left_pad="5"
+ name="run_btn"
+ width="50" />
+</floater>
diff --git a/indra/newview/skins/default/xui/en/menu_viewer.xml b/indra/newview/skins/default/xui/en/menu_viewer.xml
index 38763cd9a8..b259b101b4 100644
--- a/indra/newview/skins/default/xui/en/menu_viewer.xml
+++ b/indra/newview/skins/default/xui/en/menu_viewer.xml
@@ -2529,6 +2529,17 @@ function="World.EnvPreset"
parameter="scene monitor" />
</menu_item_check>
<menu_item_separator/>
+ <menu_item_check
+ label="LUA Debug Console"
+ name="LUA Debug Console">
+ <menu_item_check.on_check
+ function="Floater.Visible"
+ parameter="lua_debug" />
+ <menu_item_check.on_click
+ function="Floater.Toggle"
+ parameter="lua_debug" />
+ </menu_item_check>
+ <menu_item_separator/>
<menu_item_call
label="Region Info to Debug Console"
diff --git a/indra/newview/tests/llluamanager_test.cpp b/indra/newview/tests/llluamanager_test.cpp
new file mode 100644
index 0000000000..81ec186cf4
--- /dev/null
+++ b/indra/newview/tests/llluamanager_test.cpp
@@ -0,0 +1,307 @@
+/**
+ * @file llluamanager_test.cpp
+ * @author Nat Goodspeed
+ * @date 2023-09-28
+ * @brief Test for llluamanager.
+ *
+ * $LicenseInfo:firstyear=2023&license=viewerlgpl$
+ * Copyright (c) 2023, Linden Research, Inc.
+ * $/LicenseInfo$
+ */
+
+// Precompiled header
+//#include "llviewerprecompiledheaders.h"
+// associated header
+#include "../newview/llluamanager.h"
+// STL headers
+// std headers
+#include <vector>
+// external library headers
+// other Linden headers
+#include "../llcommon/tests/StringVec.h"
+#include "../test/lltut.h"
+#include "llapp.h"
+#include "lldate.h"
+#include "llevents.h"
+#include "lleventcoro.h"
+#include "llsdutil.h"
+#include "lluri.h"
+#include "lluuid.h"
+#include "lua_function.h"
+#include "stringize.h"
+
+class LLTestApp : public LLApp
+{
+public:
+ bool init() override { return true; }
+ bool cleanup() override { return true; }
+ bool frame() override { return true; }
+};
+
+template <typename CALLABLE>
+auto listener(CALLABLE&& callable)
+{
+ return [callable=std::forward<CALLABLE>(callable)]
+ (const LLSD& data)
+ {
+ callable(data);
+ return false;
+ };
+}
+
+/*****************************************************************************
+* TUT
+*****************************************************************************/
+namespace tut
+{
+ struct llluamanager_data
+ {
+ // We need an LLApp instance because LLLUAmanager uses coroutines,
+ // which suspend, and when a coroutine suspends it checks LLApp state,
+ // and if it's not APP_STATUS_RUNNING the coroutine terminates.
+ LLTestApp mApp;
+ };
+ typedef test_group<llluamanager_data> llluamanager_group;
+ typedef llluamanager_group::object object;
+ llluamanager_group llluamanagergrp("llluamanager");
+
+ static struct LuaExpr
+ {
+ std::string desc, expr;
+ LLSD expect;
+ } lua_expressions[] = {
+ { "nil", "nil", LLSD() },
+ { "true", "true", true },
+ { "false", "false", false },
+ { "int", "17", 17 },
+ { "real", "3.14", 3.14 },
+ { "string", "'string'", "string" },
+ // can't synthesize Lua userdata in Lua code: that can only be
+ // constructed by a C function
+ { "empty table", "{}", LLSD() },
+ { "nested empty table", "{ 1, 2, 3, {}, 5 }",
+ llsd::array(1, 2, 3, LLSD(), 5) },
+ { "nested non-empty table", "{ 1, 2, 3, {a=0, b=1}, 5 }",
+ llsd::array(1, 2, 3, llsd::map("a", 0, "b", 1), 5) },
+ };
+
+ template<> template<>
+ void object::test<1>()
+ {
+ set_test_name("test Lua results");
+ LuaState L;
+ for (auto& luax : lua_expressions)
+ {
+ auto [count, result] =
+ LLLUAmanager::waitScriptLine(L, "return " + luax.expr);
+ auto desc{ stringize("waitScriptLine(", luax.desc, ")") };
+ ensure_equals(desc + ".count", count, 1);
+ ensure_equals(desc + ".result", result, luax.expect);
+ }
+ }
+
+ void from_lua(const std::string& desc, const std::string_view& construct, const LLSD& expect)
+ {
+ LLSD fromlua;
+ LLEventStream replypump("testpump");
+ LLTempBoundListener conn(
+ replypump.listen("llluamanager_test",
+ listener([&fromlua](const LLSD& data){ fromlua = data; })));
+ const std::string lua(stringize(
+ "data = ", construct, "\n"
+ "post_on('testpump', data)\n"
+ ));
+ LuaState L;
+ auto [count, result] = LLLUAmanager::waitScriptLine(L, lua);
+ // We woke up again ourselves because the coroutine running Lua has
+ // finished. But our Lua chunk didn't actually return anything, so we
+ // expect count to be 0 and result to be undefined.
+ ensure_equals(desc + " count", count, 0);
+ ensure_equals(desc, fromlua, expect);
+ }
+
+ template<> template<>
+ void object::test<2>()
+ {
+ set_test_name("LLSD from post_on()");
+ for (auto& luax : lua_expressions)
+ {
+ from_lua(luax.desc, luax.expr, luax.expect);
+ }
+ }
+
+ template<> template<>
+ void object::test<3>()
+ {
+ set_test_name("test post_on(), get_event_pumps(), get_event_next()");
+ StringVec posts;
+ LLEventStream replypump("testpump");
+ LLTempBoundListener conn(
+ replypump.listen("test<3>",
+ listener([&posts](const LLSD& data)
+ { posts.push_back(data.asString()); })));
+ const std::string lua(
+ "-- test post_on,get_event_pumps,get_event_next\n"
+ "post_on('testpump', 'entry')\n"
+ "post_on('testpump', 'get_event_pumps()')\n"
+ "replypump, cmdpump = get_event_pumps()\n"
+ "post_on('testpump', replypump)\n"
+ "post_on('testpump', 'get_event_next()')\n"
+ "pump, data = get_event_next()\n"
+ "post_on('testpump', data)\n"
+ "post_on('testpump', 'exit')\n"
+ );
+ LuaState L;
+ // It's important to let the startScriptLine() coroutine run
+ // concurrently with ours until we've had a chance to post() our
+ // reply.
+ auto future = LLLUAmanager::startScriptLine(L, lua);
+ StringVec expected{
+ "entry",
+ "get_event_pumps()",
+ "",
+ "get_event_next()",
+ "message",
+ "exit"
+ };
+ expected[2] = posts.at(2);
+ LL_DEBUGS() << "Found pumpname '" << expected[2] << "'" << LL_ENDL;
+ LLEventPump& luapump{ LLEventPumps::instance().obtain(expected[2]) };
+ LL_DEBUGS() << "Found pump '" << luapump.getName() << "', type '"
+ << LLError::Log::classname(luapump)
+ << "': post('" << expected[4] << "')" << LL_ENDL;
+ luapump.post(expected[4]);
+ auto [count, result] = future.get();
+ ensure_equals("post_on() sequence", posts, expected);
+ }
+
+ void round_trip(const std::string& desc, const LLSD& send, const LLSD& expect)
+ {
+ LLSD reply;
+ LLEventStream replypump("testpump");
+ LLTempBoundListener conn(
+ replypump.listen("llluamanager_test",
+ listener([&reply](const LLSD& post){ reply = post; })));
+ const std::string lua(
+ "-- test LLSD round trip\n"
+ "replypump, cmdpump = get_event_pumps()\n"
+ "post_on('testpump', replypump)\n"
+ "pump, data = get_event_next()\n"
+ "return data\n"
+ );
+ LuaState L;
+ auto future = LLLUAmanager::startScriptLine(L, lua);
+ // We woke up again ourselves because the coroutine running Lua has
+ // reached the get_event_next() call, which suspends the calling C++
+ // coroutine (including the Lua code running on it) until we post
+ // something to that reply pump.
+ auto luapump{ reply.asString() };
+ reply.clear();
+ LLEventPumps::instance().post(luapump, send);
+ // The C++ coroutine running the Lua script is now ready to run. Run
+ // it so it will echo the LLSD back to us.
+ auto [count, result] = future.get();
+ ensure_equals(desc, result, expect);
+ }
+
+ // Define an RTItem to be used for round-trip LLSD testing: what it is,
+ // what we send to Lua, what we expect to get back. They could be the
+ // same.
+ struct RTItem
+ {
+ RTItem(const std::string& name, const LLSD& send, const LLSD& expect):
+ mName(name),
+ mSend(send),
+ mExpect(expect)
+ {}
+ RTItem(const std::string& name, const LLSD& both):
+ mName(name),
+ mSend(both),
+ mExpect(both)
+ {}
+
+ std::string mName;
+ LLSD mSend, mExpect;
+ };
+
+ template<> template<>
+ void object::test<4>()
+ {
+ set_test_name("LLSD round trip");
+ LLSD::Binary binary{ 3, 1, 4, 1, 5, 9, 2, 6, 5 };
+ const char* uuid{ "01234567-abcd-0123-4567-0123456789ab" };
+ const char* date{ "2023-10-04T21:06:00Z" };
+ const char* uri{ "https://secondlife.com/index.html" };
+ std::vector<RTItem> items{
+ RTItem("undefined", LLSD()),
+ RTItem("true", true),
+ RTItem("false", false),
+ RTItem("int", 17),
+ RTItem("real", 3.14),
+ RTItem("int real", 27.0, 27),
+ RTItem("string", "string"),
+ RTItem("binary", binary),
+ RTItem("empty array", LLSD::emptyArray(), LLSD()),
+ RTItem("empty map", LLSD::emptyMap(), LLSD()),
+ RTItem("UUID", LLUUID(uuid), uuid),
+ RTItem("date", LLDate(date), date),
+ RTItem("uri", LLURI(uri), uri)
+ };
+ // scalars
+ for (const auto& item: items)
+ {
+ round_trip(item.mName, item.mSend, item.mExpect);
+ }
+
+ // array
+ LLSD send_array{ LLSD::emptyArray() }, expect_array{ LLSD::emptyArray() };
+ for (const auto& item: items)
+ {
+ send_array.append(item.mSend);
+ expect_array.append(item.mExpect);
+ }
+ // exercise the array tail trimming below
+ send_array.append(items[0].mSend);
+ expect_array.append(items[0].mExpect);
+ // Lua takes a table value of nil to mean: don't store this key. An
+ // LLSD array containing undefined entries (converted to nil) leaves
+ // "holes" in the Lua table. These will be converted back to undefined
+ // LLSD entries -- except at the end. Trailing undefined entries are
+ // simply omitted from the table -- so the table converts back to a
+ // shorter LLSD array. We've constructed send_array and expect_array
+ // according to 'items' above -- but truncate from expect_array any
+ // trailing entries whose mSend will map to Lua nil.
+ while (expect_array.size() > 0 &&
+ send_array[expect_array.size() - 1].isUndefined())
+ {
+ expect_array.erase(expect_array.size() - 1);
+ }
+ round_trip("array", send_array, expect_array);
+
+ // map
+ LLSD send_map{ LLSD::emptyMap() }, expect_map{ LLSD::emptyMap() };
+ for (const auto& item: items)
+ {
+ send_map[item.mName] = item.mSend;
+ // see comment in the expect_array truncation loop above --
+ // Lua never stores table entries with nil values
+ if (item.mSend.isDefined())
+ {
+ expect_map[item.mName] = item.mExpect;
+ }
+ }
+ round_trip("map", send_map, expect_map);
+
+ // deeply nested map: exceed Lua's default stack space (20),
+ // i.e. verify that we have the right checkstack() calls
+ for (int i = 0; i < 20; ++i)
+ {
+ LLSD new_send_map{ send_map }, new_expect_map{ expect_map };
+ new_send_map["nested map"] = send_map;
+ new_expect_map["nested map"] = expect_map;
+ send_map = new_send_map;
+ expect_map = new_expect_map;
+ }
+ round_trip("nested map", send_map, expect_map);
+ }
+} // namespace tut
diff --git a/indra/newview/viewer_manifest.py b/indra/newview/viewer_manifest.py
index c7f32d0da9..653d27a4ec 100755
--- a/indra/newview/viewer_manifest.py
+++ b/indra/newview/viewer_manifest.py
@@ -166,6 +166,9 @@ class ViewerManifest(LLManifest):
self.path("*/*/*/*.js")
self.path("*/*/*.html")
+ with self.prefix(src_dst="scripts/lua"):
+ self.path("*.lua")
+
#build_data.json. Standard with exception handling is fine. If we can't open a new file for writing, we have worse problems
#platform is computed above with other arg parsing
build_data_dict = {"Type":"viewer","Version":'.'.join(self.args['version']),
diff --git a/indra/test/debug.h b/indra/test/debug.h
index 76dbb973b2..3c4f3cabb4 100644
--- a/indra/test/debug.h
+++ b/indra/test/debug.h
@@ -30,6 +30,8 @@
#define LL_DEBUG_H
#include "print.h"
+#include "stringize.h"
+#include <exception> // std::uncaught_exceptions()
/*****************************************************************************
* Debugging stuff
@@ -52,8 +54,9 @@
class Debug
{
public:
- Debug(const std::string& block):
- mBlock(block),
+ template <typename... ARGS>
+ Debug(ARGS&&... args):
+ mBlock(stringize(std::forward<ARGS>(args)...)),
mLOGTEST(getenv("LOGTEST")),
// debug output enabled when LOGTEST is set AND non-empty
mEnabled(mLOGTEST && *mLOGTEST)
@@ -63,10 +66,12 @@ public:
// non-copyable
Debug(const Debug&) = delete;
+ Debug& operator=(const Debug&) = delete;
~Debug()
{
- (*this)("exit");
+ auto exceptional{ std::uncaught_exceptions()? "exceptional " : "" };
+ (*this)(exceptional, "exit");
}
template <typename... ARGS>
@@ -88,20 +93,4 @@ private:
// of the Debug block.
#define DEBUG Debug debug(LL_PRETTY_FUNCTION)
-// These BEGIN/END macros are specifically for debugging output -- please
-// don't assume you must use such for coroutines in general! They only help to
-// make control flow (as well as exception exits) explicit.
-#define BEGIN \
-{ \
- DEBUG; \
- try
-
-#define END \
- catch (...) \
- { \
- debug("*** exceptional "); \
- throw; \
- } \
-}
-
#endif /* ! defined(LL_DEBUG_H) */
diff --git a/indra/test/io.cpp b/indra/test/io.cpp
index 40243a8ad6..027db50693 100644
--- a/indra/test/io.cpp
+++ b/indra/test/io.cpp
@@ -45,6 +45,7 @@
#include "llcommon.h"
#include "lluuid.h"
#include "llinstantmessage.h"
+#include "stringize.h"
namespace tut
{
@@ -1116,6 +1117,9 @@ namespace tut
template<> template<>
void fitness_test_object::test<5>()
{
+ skip("Test is strongly timing dependent, "
+ "and on slow CI machines it fails way too often.");
+ const int retries = 100;
// Set up the server
LLPumpIO::chain_t chain;
typedef LLCloneIOFactory<LLIOSleeper> sleeper_t;
@@ -1129,9 +1133,12 @@ namespace tut
chain.push_back(LLIOPipe::ptr_t(server));
mPump->addChain(chain, NEVER_CHAIN_EXPIRY_SECS);
// We need to tickle the pump a little to set up the listen()
- pump_loop(mPump, 0.1f);
+ for (int retry = 0; mPump->runningChains() < 1 && retry < retries; ++retry)
+ {
+ pump_loop(mPump, 0.1f);
+ }
U32 count = mPump->runningChains();
- ensure_equals("server chain onboard", count, 1);
+ ensure_equals("server chain 1 onboard", count, 1);
LL_DEBUGS() << "** Server is up." << LL_ENDL;
// Set up the client
@@ -1140,9 +1147,12 @@ namespace tut
bool connected = client->blockingConnect(server_host);
ensure("Connected to server", connected);
LL_DEBUGS() << "connected" << LL_ENDL;
- pump_loop(mPump,0.1f);
+ for (int retry = 0; mPump->runningChains() < 2 && retry < retries; ++retry)
+ {
+ pump_loop(mPump,0.1f);
+ }
count = mPump->runningChains();
- ensure_equals("server chain onboard", count, 2);
+ ensure_equals("server chain 2 onboard", count, 2);
LL_DEBUGS() << "** Client is connected." << LL_ENDL;
// We have connected, since the socket reader does not block,
@@ -1156,20 +1166,32 @@ namespace tut
chain.clear();
// pump for a bit and make sure all 3 chains are running
- pump_loop(mPump,0.1f);
+ for (int retry = 0; mPump->runningChains() < 3 && retry < retries; ++retry)
+ {
+ pump_loop(mPump, 0.1f);
+ }
count = mPump->runningChains();
- // ensure_equals("client chain onboard", count, 3); commented out because it fails frequently - appears to be timing sensitive
+ ensure_equals("client chain onboard", count, 3);
LL_DEBUGS() << "** request should have been sent." << LL_ENDL;
// pump for long enough the the client socket closes, and the
// server socket should not be closed yet.
- pump_loop(mPump,0.2f);
+ for (int retry = 0; mPump->runningChains() == 3 && retry < retries; ++retry)
+ {
+ pump_loop(mPump, 0.1f);
+ }
+ // We used to test for count == 2 here, but on a slow test machine it
+ // can happen that not just one but two chains close before we reach
+ // this point.
count = mPump->runningChains();
- ensure_equals("client chain timed out ", count, 2);
+ ensure(stringize("client chain timed out: count ", count), count < 3);
LL_DEBUGS() << "** client chain should be closed." << LL_ENDL;
// At this point, the socket should be closed by the timeout
- pump_loop(mPump,1.0f);
+ for (int retry = 0; mPump->runningChains() > 1 && retry < retries; ++retry)
+ {
+ pump_loop(mPump, 0.1f);
+ }
count = mPump->runningChains();
ensure_equals("accepted socked close", count, 1);
LL_DEBUGS() << "** Sleeper should have timed out.." << LL_ENDL;
diff --git a/indra/test/lltut.h b/indra/test/lltut.h
index 9835565bb6..4ce450057c 100644
--- a/indra/test/lltut.h
+++ b/indra/test/lltut.h
@@ -31,6 +31,8 @@
#include "is_approx_equal_fraction.h" // instead of llmath.h
#include <cstring>
+#include <string>
+#include <vector>
class LLDate;
class LLSD;
diff --git a/indra/test/print.h b/indra/test/print.h
index 08e36caddf..749603907e 100644
--- a/indra/test/print.h
+++ b/indra/test/print.h
@@ -23,7 +23,9 @@ struct NONL_t {};
inline
void print()
{
+#ifdef LL_TEST
std::cerr << std::endl;
+#endif
}
// print(NONL) is a no-op
@@ -35,8 +37,10 @@ void print(NONL_t)
template <typename T, typename... ARGS>
void print(T&& first, ARGS&&... rest)
{
+#ifdef LL_TEST
std::cerr << first;
print(std::forward<ARGS>(rest)...);
+#endif
}
#endif /* ! defined(LL_PRINT_H) */