summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorMnikolenko Productengine <mnikolenko@productengine.com>2024-06-24 11:58:33 +0300
committerMnikolenko Productengine <mnikolenko@productengine.com>2024-06-24 12:00:16 +0300
commit61fad5fd04f90542b2842dec86a6f74a4f801de7 (patch)
treea30cac169de8cde1f822af2c5d1806300b62197f
parentb8e0c16ab1eb3cdd1d11e869bd3fd6196de51d4b (diff)
parent75accbefdbe7741d57bf093690d65ad1100f82d4 (diff)
Merge branch 'release/luau-scripting' into lua-appearance-listener
-rw-r--r--indra/llcommon/CMakeLists.txt1
-rw-r--r--indra/llcommon/llcallbacklist.cpp27
-rw-r--r--indra/llcommon/llcoros.cpp6
-rw-r--r--indra/llcommon/lua_function.cpp154
-rw-r--r--indra/llcommon/lua_function.h249
-rw-r--r--indra/llcommon/lualistener.cpp30
-rw-r--r--indra/llcommon/lualistener.h20
-rwxr-xr-xindra/llcommon/tempset.h41
-rw-r--r--indra/newview/llappviewerlistener.cpp9
-rw-r--r--indra/newview/llappviewerlistener.h5
-rw-r--r--indra/newview/llfloaterluadebug.cpp32
-rw-r--r--indra/newview/llfloaterluadebug.h2
-rw-r--r--indra/newview/llluamanager.cpp91
-rw-r--r--indra/newview/llwindowlistener.cpp2
-rw-r--r--indra/newview/scripts/lua/LLNotification.lua15
-rw-r--r--indra/newview/scripts/lua/UI.lua16
-rw-r--r--indra/newview/scripts/lua/qtest.lua12
-rw-r--r--indra/newview/scripts/lua/require/ErrorQueue.lua (renamed from indra/newview/scripts/lua/ErrorQueue.lua)5
-rw-r--r--indra/newview/scripts/lua/require/Floater.lua (renamed from indra/newview/scripts/lua/Floater.lua)3
-rw-r--r--indra/newview/scripts/lua/require/LLChat.lua (renamed from indra/newview/scripts/lua/LLChat.lua)2
-rw-r--r--indra/newview/scripts/lua/require/LLChatListener.lua (renamed from indra/newview/scripts/lua/LLChatListener.lua)3
-rw-r--r--indra/newview/scripts/lua/require/LLDebugSettings.lua (renamed from indra/newview/scripts/lua/LLDebugSettings.lua)2
-rw-r--r--indra/newview/scripts/lua/require/LLFloaterAbout.lua (renamed from indra/newview/scripts/lua/LLFloaterAbout.lua)2
-rw-r--r--indra/newview/scripts/lua/require/LLGesture.lua (renamed from indra/newview/scripts/lua/LLGesture.lua)2
-rw-r--r--indra/newview/scripts/lua/require/Queue.lua (renamed from indra/newview/scripts/lua/Queue.lua)4
-rw-r--r--indra/newview/scripts/lua/require/UI.lua125
-rw-r--r--indra/newview/scripts/lua/require/WaitQueue.lua (renamed from indra/newview/scripts/lua/WaitQueue.lua)7
-rw-r--r--indra/newview/scripts/lua/require/coro.lua (renamed from indra/newview/scripts/lua/coro.lua)0
-rw-r--r--indra/newview/scripts/lua/require/fiber.lua (renamed from indra/newview/scripts/lua/fiber.lua)0
-rw-r--r--indra/newview/scripts/lua/require/inspect.lua (renamed from indra/newview/scripts/lua/inspect.lua)0
-rw-r--r--indra/newview/scripts/lua/require/leap.lua (renamed from indra/newview/scripts/lua/leap.lua)13
-rw-r--r--indra/newview/scripts/lua/require/login.lua19
-rw-r--r--indra/newview/scripts/lua/require/logout.lua7
-rw-r--r--indra/newview/scripts/lua/require/mapargs.lua73
-rw-r--r--indra/newview/scripts/lua/require/popup.lua53
-rw-r--r--indra/newview/scripts/lua/require/printf.lua (renamed from indra/newview/scripts/lua/printf.lua)0
-rw-r--r--indra/newview/scripts/lua/require/startup.lua (renamed from indra/newview/scripts/lua/startup.lua)9
-rw-r--r--indra/newview/scripts/lua/require/timers.lua (renamed from indra/newview/scripts/lua/timers.lua)3
-rw-r--r--indra/newview/scripts/lua/require/util.lua (renamed from indra/newview/scripts/lua/util.lua)25
-rw-r--r--indra/newview/scripts/lua/test_LLChatListener.lua2
-rw-r--r--indra/newview/scripts/lua/test_atexit.lua3
-rw-r--r--indra/newview/scripts/lua/test_login.lua7
-rw-r--r--indra/newview/scripts/lua/test_logout.lua3
-rw-r--r--indra/newview/scripts/lua/test_luafloater_demo.lua77
-rw-r--r--indra/newview/scripts/lua/test_luafloater_demo2.lua2
-rw-r--r--indra/newview/scripts/lua/test_luafloater_gesture_list.lua75
-rw-r--r--indra/newview/scripts/lua/test_luafloater_gesture_list2.lua2
-rw-r--r--indra/newview/scripts/lua/test_luafloater_speedometer.lua11
-rw-r--r--indra/newview/scripts/lua/test_mapargs.lua68
-rw-r--r--indra/newview/scripts/lua/test_popup.lua6
-rw-r--r--indra/newview/scripts/lua/test_timers.lua16
-rw-r--r--indra/newview/tests/llluamanager_test.cpp2
-rwxr-xr-xindra/newview/viewer_manifest.py4
-rwxr-xr-xscripts/packages-formatter.py2
54 files changed, 961 insertions, 388 deletions
diff --git a/indra/llcommon/CMakeLists.txt b/indra/llcommon/CMakeLists.txt
index d5440d6bc8..20670d7ebe 100644
--- a/indra/llcommon/CMakeLists.txt
+++ b/indra/llcommon/CMakeLists.txt
@@ -259,6 +259,7 @@ set(llcommon_HEADER_FILES
lualistener.h
stdtypes.h
stringize.h
+ tempset.h
threadpool.h
threadpool_fwd.h
threadsafeschedule.h
diff --git a/indra/llcommon/llcallbacklist.cpp b/indra/llcommon/llcallbacklist.cpp
index 015475a903..555c793333 100644
--- a/indra/llcommon/llcallbacklist.cpp
+++ b/indra/llcommon/llcallbacklist.cpp
@@ -29,6 +29,7 @@
#include "llerror.h"
#include "llexception.h"
#include "llsdutil.h"
+#include "tempset.h"
#include <boost/container_hash/hash.hpp>
#include <iomanip>
#include <vector>
@@ -292,32 +293,6 @@ void Timers::setTimeslice(F32 timeslice)
}
}
-// RAII class to set specified variable to specified value
-// only for the duration of containing scope
-template <typename VAR, typename VALUE>
-class TempSet
-{
-public:
- TempSet(VAR& var, const VALUE& value):
- mVar(var),
- mOldValue(mVar)
- {
- mVar = value;
- }
-
- TempSet(const TempSet&) = delete;
- TempSet& operator=(const TempSet&) = delete;
-
- ~TempSet()
- {
- mVar = mOldValue;
- }
-
-private:
- VAR& mVar;
- VALUE mOldValue;
-};
-
bool Timers::tick()
{
// Fetch current time only on entry, even though running some mQueue task
diff --git a/indra/llcommon/llcoros.cpp b/indra/llcommon/llcoros.cpp
index a6d7988256..c28baa5747 100644
--- a/indra/llcommon/llcoros.cpp
+++ b/indra/llcommon/llcoros.cpp
@@ -270,7 +270,7 @@ std::string LLCoros::launch(const std::string& prefix, const callable_t& callabl
// std::allocator_arg is a flag to indicate that the following argument is
// a StackAllocator.
// protected_fixedsize_stack sets a guard page past the end of the new
- // stack so that stack underflow will result in an access violation
+ // stack so that stack overflow will result in an access violation
// instead of weird, subtle, possibly undiagnosed memory stomps.
try
@@ -355,10 +355,12 @@ void LLCoros::toplevel(std::string name, callable_t callable)
// set it as current
mCurrent.reset(&corodata);
+ LL_DEBUGS("LLCoros") << "entering " << name << LL_ENDL;
// run the code the caller actually wants in the coroutine
try
{
sehandle(callable);
+ LL_DEBUGS("LLCoros") << "done " << name << LL_ENDL;
}
catch (const Stop& exc)
{
@@ -370,7 +372,7 @@ void LLCoros::toplevel(std::string name, callable_t callable)
// Any uncaught exception derived from LLContinueError will be caught
// here and logged. This coroutine will terminate but the rest of the
// viewer will carry on.
- LOG_UNHANDLED_EXCEPTION(stringize("coroutine ", name));
+ LOG_UNHANDLED_EXCEPTION("coroutine " + name);
}
catch (...)
{
diff --git a/indra/llcommon/lua_function.cpp b/indra/llcommon/lua_function.cpp
index 08bc65e0c5..255385b8c4 100644
--- a/indra/llcommon/lua_function.cpp
+++ b/indra/llcommon/lua_function.cpp
@@ -28,6 +28,7 @@
#include "lleventcoro.h"
#include "llsd.h"
#include "llsdutil.h"
+#include "llstring.h"
#include "lualistener.h"
#include "stringize.h"
@@ -75,8 +76,16 @@ fsyspath lluau::source_path(lua_State* 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);
+ // In particular:
+ // passing level=1 gets you info about the deepest function call
+ // passing level=lua_stackdepth() gets you info about the topmost script
+ // Empirically, lua_getinfo(level > 1) behaves strangely (including
+ // crashing the program) unless you iterate from 1 to desired level.
+ lua_Debug ar{};
+ for (int i(0), depth(lua_stackdepth(L)); i <= depth; ++i)
+ {
+ lua_getinfo(L, i, "s", &ar);
+ }
return ar.source;
}
@@ -489,9 +498,42 @@ void LuaState::initLuaState()
LuaState::~LuaState()
{
- // Did somebody call obtainListener() on this LuaState?
- // That is, is there a LuaListener key in its registry?
- LuaListener::destruct(getListener());
+ // We're just about to destroy this lua_State mState. lua_close() doesn't
+ // implicitly garbage-collect everything, so (for instance) any lingering
+ // objects with __gc metadata methods aren't cleaned up. This is why we
+ // provide atexit().
+ luaL_checkstack(mState, 2, nullptr);
+ // look up Registry.atexit
+ lua_getfield(mState, LUA_REGISTRYINDEX, "atexit");
+ // stack contains Registry.atexit
+ if (lua_istable(mState, -1))
+ {
+ // We happen to know that Registry.atexit is built by appending array
+ // entries using table.insert(). That's important because it means
+ // there are no holes, and therefore lua_objlen() should be correct.
+ // That's important because we walk the atexit table backwards, to
+ // destroy last the things we created (passed to LL.atexit()) first.
+ for (int i(lua_objlen(mState, -1)); i >= 1; --i)
+ {
+ lua_pushinteger(mState, i);
+ // stack contains Registry.atexit, i
+ lua_gettable(mState, -2);
+ // stack contains Registry.atexit, atexit[i]
+ // Call atexit[i](), no args, no return values.
+ // Use lua_pcall() because errors in any one atexit() function
+ // shouldn't cancel the rest of them.
+ if (lua_pcall(mState, 0, 0, 0) != LUA_OK)
+ {
+ auto error{ lua_tostdstring(mState, -1) };
+ LL_WARNS("Lua") << "atexit() function error: " << error << LL_ENDL;
+ // pop error message
+ lua_pop(mState, 1);
+ }
+ // lua_pcall() has already popped atexit[i]: stack contains atexit
+ }
+ }
+ // pop Registry.atexit (either table or nil)
+ lua_pop(mState, 1);
lua_close(mState);
@@ -509,7 +551,7 @@ bool LuaState::checkLua(const std::string& desc, int r)
mError = lua_tostring(mState, -1);
lua_pop(mState, 1);
- LL_WARNS() << desc << ": " << mError << LL_ENDL;
+ LL_WARNS("Lua") << desc << ": " << mError << LL_ENDL;
return false;
}
return true;
@@ -529,6 +571,7 @@ std::pair<int, LLSD> LuaState::expr(const std::string& desc, const std::string&
lluau::check_interrupts_counter(L);
};
+ LL_INFOS("Lua") << desc << " run" << LL_ENDL;
if (! checkLua(desc, lluau::dostring(mState, desc, text)))
{
LL_WARNS("Lua") << desc << " error: " << mError << LL_ENDL;
@@ -645,43 +688,61 @@ std::pair<int, LLSD> LuaState::expr(const std::string& desc, const std::string&
return result;
}
-LuaListener::ptr_t LuaState::getListener(lua_State* L)
+LuaListener& LuaState::obtainListener(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
+ luaL_checkstack(L, 2, nullptr);
+ lua_getfield(L, LUA_REGISTRYINDEX, "LuaListener");
+ // compare lua_type() because lua_isuserdata() also accepts light userdata
+ if (lua_type(L, -1) != LUA_TUSERDATA)
+ {
+ llassert(lua_type(L, -1) == LUA_TNIL);
+ lua_pop(L, 1);
+ // push a userdata containing new LuaListener, binding L
+ lua_emplace<LuaListener>(L, L);
+ // duplicate the top stack entry so we can store one copy
+ lua_pushvalue(L, -1);
+ lua_setfield(L, LUA_REGISTRYINDEX, "LuaListener");
+ }
+ // At this point, one way or the other, the stack top should be (a Lua
+ // userdata containing) our LuaListener.
+ LuaListener* listener{ lua_toclass<LuaListener>(L, -1) };
+ // userdata objects created by lua_emplace<T>() are bound on the atexit()
+ // queue, and are thus never garbage collected: they're destroyed only
+ // when ~LuaState() walks that queue. That's why we dare pop the userdata
+ // value off the stack while still depending on a pointer into its data.
lua_pop(L, 1);
- return listener;
+ return *listener;
}
-LuaListener::ptr_t LuaState::obtainListener(lua_State* L)
+/*****************************************************************************
+* atexit()
+*****************************************************************************/
+lua_function(atexit, "atexit(function): "
+ "register Lua function to be called at script termination")
{
- 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;
+ luaL_checkstack(L, 4, nullptr);
+ // look up the global name "table"
+ lua_getglobal(L, "table");
+ // stack contains function, table
+ // look up table.insert
+ lua_getfield(L, -1, "insert");
+ // stack contains function, table, table.insert
+ // ditch table
+ lua_replace(L, -2);
+ // stack contains function, table.insert
+ // find or create the "atexit" table in the Registry
+ luaL_newmetatable(L, "atexit");
+ // stack contains function, table.insert, Registry.atexit
+ // we were called with a Lua function to append to that Registry.atexit
+ // table -- push function
+ lua_pushvalue(L, 1); // or -3
+ // stack contains function, table.insert, Registry.atexit, function
+ // call table.insert(Registry.atexit, function)
+ // don't use pcall(): if there's an error, let it propagate
+ lua_call(L, 2, 0);
+ // stack contains function -- pop everything
+ lua_settop(L, 0);
+ return 0;
}
/*****************************************************************************
@@ -745,7 +806,7 @@ std::pair<LuaFunction::Registry&, LuaFunction::Lookup&> LuaFunction::getState()
/*****************************************************************************
* source_path()
*****************************************************************************/
-lua_function(source_path, "return the source path of the running Lua script")
+lua_function(source_path, "source_path(): return the source path of the running Lua script")
{
luaL_checkstack(L, 1, nullptr);
lua_pushstdstring(L, lluau::source_path(L).u8string());
@@ -755,7 +816,7 @@ lua_function(source_path, "return the source path of the running Lua script")
/*****************************************************************************
* source_dir()
*****************************************************************************/
-lua_function(source_dir, "return the source directory of the running Lua script")
+lua_function(source_dir, "source_dir(): return the source directory of the running Lua script")
{
luaL_checkstack(L, 1, nullptr);
lua_pushstdstring(L, lluau::source_path(L).parent_path().u8string());
@@ -765,7 +826,7 @@ lua_function(source_dir, "return the source directory of the running Lua script"
/*****************************************************************************
* abspath()
*****************************************************************************/
-lua_function(abspath,
+lua_function(abspath, "abspath(path): "
"for given filesystem path relative to running script, return absolute path")
{
auto path{ lua_tostdstring(L, 1) };
@@ -777,7 +838,7 @@ lua_function(abspath,
/*****************************************************************************
* check_stop()
*****************************************************************************/
-lua_function(check_stop, "ensure that a Lua script responds to viewer shutdown")
+lua_function(check_stop, "check_stop(): ensure that a Lua script responds to viewer shutdown")
{
LLCoros::checkStop();
return 0;
@@ -798,7 +859,7 @@ lua_function(help,
for (const auto& [name, pair] : registry)
{
const auto& [fptr, helptext] = pair;
- luapump.post(helptext);
+ luapump.post("LL." + helptext);
}
}
else
@@ -810,6 +871,7 @@ lua_function(help,
if (lua_type(L, idx) == LUA_TSTRING)
{
arg = lua_tostdstring(L, idx);
+ LLStringUtil::removePrefix(arg, "LL.");
}
else if (lua_type(L, idx) == LUA_TFUNCTION)
{
@@ -828,7 +890,7 @@ lua_function(help,
if (auto found = registry.find(arg); found != registry.end())
{
- luapump.post(found->second.second);
+ luapump.post("LL." + found->second.second);
}
else
{
@@ -863,10 +925,10 @@ lua_function(
lua_settop(L, 0);
auto& outpump{ LLEventPumps::instance().obtain("lua output") };
- auto listener{ LuaState::obtainListener(L) };
+ 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") };
+ auto reply{ llcoro::postAndSuspend(request, listener.getCommandName(), replyPump, "reply") };
reply.erase("reqid");
if (auto error = reply["error"]; error.isString())
diff --git a/indra/llcommon/lua_function.h b/indra/llcommon/lua_function.h
index e7013f92c6..9cdd5665dc 100644
--- a/indra/llcommon/lua_function.h
+++ b/indra/llcommon/lua_function.h
@@ -17,13 +17,19 @@
#include "luau/luaconf.h"
#include "luau/lualib.h"
#include "fsyspath.h"
+#include "llerror.h"
#include "stringize.h"
#include <exception> // std::uncaught_exceptions()
#include <memory> // std::shared_ptr
+#include <optional>
+#include <typeinfo>
#include <utility> // std::pair
class LuaListener;
+/*****************************************************************************
+* lluau namespace utility functions
+*****************************************************************************/
namespace lluau
{
// luau defines luaL_error() as void, but we want to use the Lua idiom of
@@ -62,6 +68,9 @@ 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);
+/*****************************************************************************
+* LuaState
+*****************************************************************************/
/**
* RAII class to manage the lifespan of a lua_State
*/
@@ -93,16 +102,10 @@ public:
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);
+ // Find or create LuaListener for this LuaState.
+ LuaListener& obtainListener() { return obtainListener(mState); }
+ // Find or create LuaListener for passed lua_State.
+ static LuaListener& obtainListener(lua_State* L);
private:
script_finished_fn mCallback;
@@ -110,6 +113,9 @@ private:
std::string mError;
};
+/*****************************************************************************
+* LuaPopper
+*****************************************************************************/
/**
* LuaPopper is an RAII struct whose role is to pop some number of entries
* from the Lua stack if the calling function exits early.
@@ -133,6 +139,9 @@ struct LuaPopper
int mCount;
};
+/*****************************************************************************
+* lua_function (and helper class LuaFunction)
+*****************************************************************************/
/**
* LuaFunction is a base class containing a static registry of its static
* subclass call() methods. call() is NOT virtual: instead, each subclass
@@ -182,6 +191,218 @@ int name##_luasub::call(lua_State* L)
// ... supply method body here, referencing 'L' ...
// }
+/*****************************************************************************
+* lua_emplace<T>(), lua_toclass<T>()
+*****************************************************************************/
+namespace {
+
+// this closure function retrieves its bound argument to pass to
+// lua_emplace_gc<T>()
+template <class T>
+int lua_emplace_call_gc(lua_State* L);
+// this will be the function called by the new userdata's metatable's __gc()
+template <class T>
+int lua_emplace_gc(lua_State* L);
+// name by which we'll store the new userdata's metatable in the Registry
+template <class T>
+std::string lua_emplace_metaname(const std::string& Tname = LLError::Log::classname<T>());
+
+} // anonymous namespace
+
+/**
+ * On the stack belonging to the passed lua_State, push a Lua userdata object
+ * with a newly-constructed C++ object std::optional<T>(args...). The new
+ * userdata has a metadata table with a __gc() function to ensure that when
+ * the userdata instance is garbage-collected, ~T() is called. Also call
+ * LL.atexit(lua_emplace_call_gc<T>(object)) to make ~LuaState() call ~T().
+ *
+ * We wrap the userdata object as std::optional<T> so we can explicitly
+ * destroy the contained T, and detect that we've done so.
+ *
+ * Usage:
+ * lua_emplace<T>(L, T constructor args...);
+ * // L's Lua stack top is now a userdata containing T
+ */
+template <class T, typename... ARGS>
+void lua_emplace(lua_State* L, ARGS&&... args)
+{
+ using optT = std::optional<T>;
+ luaL_checkstack(L, 5, nullptr);
+ auto ptr = lua_newuserdata(L, sizeof(optT));
+ // stack is uninitialized userdata
+ // For now, assume (but verify) that lua_newuserdata() returns a
+ // conservatively-aligned ptr. If that turns out not to be the case, we
+ // might have to discard the new userdata, overallocate its successor and
+ // perform manual alignment -- but only if we must.
+ llassert((uintptr_t(ptr) % alignof(optT)) == 0);
+ // Construct our T there using placement new
+ new (ptr) optT(std::in_place, std::forward<ARGS>(args)...);
+ // stack is now initialized userdata containing our T instance
+
+ // Find or create the metatable shared by all userdata instances holding
+ // C++ type T. We want it to be shared across instances, but it must be
+ // type-specific because its __gc field is lua_emplace_gc<T>.
+ auto Tname{ LLError::Log::classname<T>() };
+ auto metaname{ lua_emplace_metaname<T>(Tname) };
+ if (luaL_newmetatable(L, metaname.c_str()))
+ {
+ // just created it: populate it
+ auto gcname{ stringize("lua_emplace_gc<", Tname, ">") };
+ lua_pushcfunction(L, lua_emplace_gc<T>, gcname.c_str());
+ // stack is userdata, metatable, lua_emplace_gc<T>
+ lua_setfield(L, -2, "__gc");
+ }
+ // stack is userdata, metatable
+ lua_setmetatable(L, -2);
+ // Stack is now userdata, initialized with T(args),
+ // with metatable.__gc pointing to lua_emplace_gc<T>.
+
+ // But wait, there's more! Use our atexit() function to ensure that this
+ // C++ object is eventually destroyed even if the garbage collector never
+ // gets around to it.
+ lua_getglobal(L, "LL");
+ // stack contains userdata, LL
+ lua_getfield(L, -1, "atexit");
+ // stack contains userdata, LL, LL.atexit
+ // ditch LL
+ lua_replace(L, -2);
+ // stack contains userdata, LL.atexit
+
+ // We have a bit of a problem here. We want to allow the garbage collector
+ // to collect the userdata if it must; but we also want to register a
+ // cleanup function to destroy the value if (usual case) it has NOT been
+ // garbage-collected. The problem is that if we bind into atexit()'s queue
+ // a strong reference to the userdata, we ensure that the garbage
+ // collector cannot collect it, making our metatable with __gc function
+ // completely moot. And we must assume that lua_pushcclosure() binds a
+ // strong reference to each value passed as a closure.
+
+ // The solution is to use one more indirection: create a weak table whose
+ // sole entry is the userdata. If all other references to the new userdata
+ // are forgotten, so the only remaining reference is the weak table, the
+ // userdata can be collected. Then we can bind that weak table as the
+ // closure value for our cleanup function.
+ // The new weak table will have at most 1 array value, 0 other keys.
+ lua_createtable(L, 1, 0);
+ // stack contains userdata, LL.atexit, weak_table
+ if (luaL_newmetatable(L, "weak_values"))
+ {
+ // stack contains userdata, LL.atexit, weak_table, weak_values
+ // just created "weak_values" metatable: populate it
+ // Registry.weak_values = {__mode="v"}
+ lua_pushliteral(L, "v");
+ // stack contains userdata, LL.atexit, weak_table, weak_values, "v"
+ lua_setfield(L, -2, "__mode");
+ }
+ // stack contains userdata, LL.atexit, weak_table, weak_values
+ // setmetatable(weak_table, weak_values)
+ lua_setmetatable(L, -2);
+ // stack contains userdata, LL.atexit, weak_table
+ lua_pushinteger(L, 1);
+ // stack contains userdata, LL.atexit, weak_table, 1
+ // duplicate userdata
+ lua_pushvalue(L, -4);
+ // stack contains userdata, LL.atexit, weak_table, 1, userdata
+ // weak_table[1] = userdata
+ lua_settable(L, -3);
+ // stack contains userdata, LL.atexit, weak_table
+
+ // push a closure binding (lua_emplace_call_gc<T>, weak_table)
+ auto callgcname{ stringize("lua_emplace_call_gc<", Tname, ">") };
+ lua_pushcclosure(L, lua_emplace_call_gc<T>, callgcname.c_str(), 1);
+ // stack contains userdata, LL.atexit, closure
+ // Call LL.atexit(closure)
+ lua_call(L, 1, 0);
+ // stack contains userdata -- return that
+}
+
+namespace {
+
+// passed to LL.atexit(closure(lua_emplace_call_gc<T>, weak_table{userdata}));
+// retrieves bound userdata to pass to lua_emplace_gc<T>()
+template <class T>
+int lua_emplace_call_gc(lua_State* L)
+{
+ luaL_checkstack(L, 2, nullptr);
+ // retrieve the first (only) bound upvalue and push to stack top
+ lua_pushvalue(L, lua_upvalueindex(1));
+ // This is the weak_table bound by lua_emplace<T>(). Its one and only
+ // entry should be the lua_emplace<T>() userdata -- unless userdata has
+ // been garbage collected. Retrieve weak_table[1].
+ lua_pushinteger(L, 1);
+ // stack contains weak_table, 1
+ lua_gettable(L, -2);
+ // stack contains weak_table, weak_table[1]
+ // If our userdata was garbage-collected, there is no weak_table[1],
+ // and we just retrieved nil.
+ if (lua_isnil(L, -1))
+ {
+ lua_pop(L, 2);
+ return 0;
+ }
+ // stack contains weak_table, userdata
+ // ditch weak_table
+ lua_replace(L, -2);
+ // pass userdata to lua_emplace_gc<T>()
+ return lua_emplace_gc<T>(L);
+}
+
+// set as metatable(userdata).__gc to be called by the garbage collector
+template <class T>
+int lua_emplace_gc(lua_State* L)
+{
+ using optT = std::optional<T>;
+ // We're called with userdata on the stack holding an instance of type T.
+ auto ptr = lua_touserdata(L, -1);
+ llassert(ptr);
+ // Destroy the T object contained in optT at the void* address ptr. If
+ // in future lua_emplace() must manually align our optT* within the
+ // Lua-provided void*, derive optT* from ptr.
+ static_cast<optT*>(ptr)->reset();
+ // pop the userdata
+ lua_pop(L, 1);
+ return 0;
+}
+
+template <class T>
+std::string lua_emplace_metaname(const std::string& Tname)
+{
+ return stringize("lua_emplace_", Tname, "_meta");
+}
+
+} // anonymous namespace
+
+/**
+ * If the value at the passed acceptable index is a full userdata created by
+ * lua_emplace<T>() -- that is, the userdata contains a non-empty
+ * std::optional<T> -- return a pointer to the contained T instance. Otherwise
+ * (index is not a full userdata; userdata is not of type std::optional<T>;
+ * std::optional<T> is empty) return nullptr.
+ */
+template <class T>
+T* lua_toclass(lua_State* L, int index)
+{
+ using optT = std::optional<T>;
+ // recreate the name lua_emplace<T>() uses for its metatable
+ auto metaname{ lua_emplace_metaname<T>() };
+ // get void* pointer to userdata (if that's what it is)
+ void* ptr{ luaL_checkudata(L, index, metaname.c_str()) };
+ if (! ptr)
+ return nullptr;
+ // Derive the optT* from ptr. If in future lua_emplace() must manually
+ // align our optT* within the Lua-provided void*, adjust accordingly.
+ optT* tptr(static_cast<optT*>(ptr));
+ // make sure our optT isn't empty
+ if (! *tptr)
+ return nullptr;
+ // looks like we still have a non-empty optT: return the *address* of the
+ // value() reference
+ return &tptr->value();
+}
+
+/*****************************************************************************
+* lua_what()
+*****************************************************************************/
// 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.
@@ -202,6 +423,9 @@ private:
int index;
};
+/*****************************************************************************
+* lua_stack()
+*****************************************************************************/
// Usage: std::cout << lua_stack(L) << ...;
// Reports on the contents of the Lua stack.
// If cast to std::string, returns the corresponding string value.
@@ -220,6 +444,9 @@ private:
lua_State* L;
};
+/*****************************************************************************
+* LuaLog
+*****************************************************************************/
// adapted from indra/test/debug.h
// can't generalize Debug::operator() target because it's a variadic template
class LuaLog
@@ -246,7 +473,7 @@ public:
template <typename... ARGS>
void operator()(ARGS&&... args)
{
- LL_INFOS("Lua") << mBlock << ' ';
+ LL_DEBUGS("Lua") << mBlock << ' ';
stream_to(LL_CONT, std::forward<ARGS>(args)...);
LL_ENDL;
}
diff --git a/indra/llcommon/lualistener.cpp b/indra/llcommon/lualistener.cpp
index 5c4989e891..6cb87e8af2 100644
--- a/indra/llcommon/lualistener.cpp
+++ b/indra/llcommon/lualistener.cpp
@@ -15,8 +15,7 @@
#include "lualistener.h"
// STL headers
// std headers
-#include <cstdlib> // std::rand()
-#include <cstring> // std::memcpy()
+#include <iomanip> // std::quoted()
// external library headers
#include "luau/lua.h"
// other Linden headers
@@ -28,11 +27,11 @@ const int MAX_QSIZE = 1000;
std::ostream& operator<<(std::ostream& out, const LuaListener& self)
{
- return out << "LuaListener(" << self.getReplyName() << ", " << self.getCommandName() << ")";
+ return out << "LuaListener(" << std::quoted(self.mCoroName) << ", "
+ << self.getReplyName() << ", " << self.getCommandName() << ")";
}
LuaListener::LuaListener(lua_State* L):
- super(getUniqueKey()),
mCoroName(LLCoros::getName()),
mListener(new LLLeapListener(
"LuaListener",
@@ -49,24 +48,13 @@ LuaListener::LuaListener(lua_State* L):
// viewer shutdown, close the queue to wake up getNext().
mQueue.close();
}))
-{}
+{
+ LL_DEBUGS("Lua") << "LuaListener(" << std::quoted(mCoroName) << ")" << LL_ENDL;
+}
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;
+ LL_DEBUGS("Lua") << "~LuaListener(" << std::quoted(mCoroName) << ")" << LL_ENDL;
}
std::string LuaListener::getReplyName() const
@@ -86,7 +74,7 @@ bool LuaListener::queueEvent(const std::string& pump, const LLSD& data)
// 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()
+ LL_WARNS("Lua") << "LuaListener queue for " << mCoroName
<< " exceeds " << MAX_QSIZE << ": " << size
<< " -- discarding event" << LL_ENDL;
}
@@ -107,7 +95,7 @@ LuaListener::PumpData LuaListener::getNext()
catch (const LLThreadSafeQueueInterrupt&)
{
// mQueue has been closed. The only way that happens is when we detect
- // viewer shutdown. Terminate the calling coroutine.
+ // viewer shutdown. Terminate the calling Lua coroutine.
LLCoros::checkStop();
return {};
}
diff --git a/indra/llcommon/lualistener.h b/indra/llcommon/lualistener.h
index 85fb093cd6..68131dfa27 100644
--- a/indra/llcommon/lualistener.h
+++ b/indra/llcommon/lualistener.h
@@ -12,8 +12,7 @@
#if ! defined(LL_LUALISTENER_H)
#define LL_LUALISTENER_H
-#include "llevents.h"
-#include "llinstancetracker.h"
+#include "llevents.h" // LLTempBoundListener
#include "llsd.h"
#include "llthreadsafequeue.h"
#include <iosfwd> // std::ostream
@@ -27,25 +26,11 @@ 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>
+class LuaListener
{
- using super = LLInstanceTracker<LuaListener, int>;
public:
LuaListener(lua_State* L);
@@ -68,7 +53,6 @@ public:
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;
diff --git a/indra/llcommon/tempset.h b/indra/llcommon/tempset.h
new file mode 100755
index 0000000000..e1496bd5fc
--- /dev/null
+++ b/indra/llcommon/tempset.h
@@ -0,0 +1,41 @@
+/**
+ * @file tempset.h
+ * @author Nat Goodspeed
+ * @date 2024-06-12
+ * @brief Temporarily override a variable for scope duration, then restore
+ *
+ * $LicenseInfo:firstyear=2024&license=viewerlgpl$
+ * Copyright (c) 2024, Linden Research, Inc.
+ * $/LicenseInfo$
+ */
+
+#if ! defined(LL_TEMPSET_H)
+#define LL_TEMPSET_H
+
+// RAII class to set specified variable to specified value
+// only for the duration of containing scope
+template <typename VAR, typename VALUE>
+class TempSet
+{
+public:
+ TempSet(VAR& var, const VALUE& value):
+ mVar(var),
+ mOldValue(mVar)
+ {
+ mVar = value;
+ }
+
+ TempSet(const TempSet&) = delete;
+ TempSet& operator=(const TempSet&) = delete;
+
+ ~TempSet()
+ {
+ mVar = mOldValue;
+ }
+
+private:
+ VAR& mVar;
+ VALUE mOldValue;
+};
+
+#endif /* ! defined(LL_TEMPSET_H) */
diff --git a/indra/newview/llappviewerlistener.cpp b/indra/newview/llappviewerlistener.cpp
index 6d519b6fef..d02b1b4a79 100644
--- a/indra/newview/llappviewerlistener.cpp
+++ b/indra/newview/llappviewerlistener.cpp
@@ -42,6 +42,9 @@ LLAppViewerListener::LLAppViewerListener(const LLAppViewerGetter& getter):
mAppViewerGetter(getter)
{
// add() every method we want to be able to invoke via this event API.
+ add("userQuit",
+ "Ask to quit with user confirmation prompt",
+ &LLAppViewerListener::userQuit);
add("requestQuit",
"Ask to quit nicely",
&LLAppViewerListener::requestQuit);
@@ -50,6 +53,12 @@ LLAppViewerListener::LLAppViewerListener(const LLAppViewerGetter& getter):
&LLAppViewerListener::forceQuit);
}
+void LLAppViewerListener::userQuit(const LLSD& event)
+{
+ LL_INFOS() << "Listener requested user quit" << LL_ENDL;
+ mAppViewerGetter()->userQuit();
+}
+
void LLAppViewerListener::requestQuit(const LLSD& event)
{
LL_INFOS() << "Listener requested quit" << LL_ENDL;
diff --git a/indra/newview/llappviewerlistener.h b/indra/newview/llappviewerlistener.h
index 5ade3d3e04..e116175eb7 100644
--- a/indra/newview/llappviewerlistener.h
+++ b/indra/newview/llappviewerlistener.h
@@ -30,7 +30,7 @@
#define LL_LLAPPVIEWERLISTENER_H
#include "lleventapi.h"
-#include <boost/function.hpp>
+#include <functional>
class LLAppViewer;
class LLSD;
@@ -39,11 +39,12 @@ class LLSD;
class LLAppViewerListener: public LLEventAPI
{
public:
- typedef boost::function<LLAppViewer*(void)> LLAppViewerGetter;
+ typedef std::function<LLAppViewer*(void)> LLAppViewerGetter;
/// Bind the LLAppViewer instance to use (e.g. LLAppViewer::instance()).
LLAppViewerListener(const LLAppViewerGetter& getter);
private:
+ void userQuit(const LLSD& event);
void requestQuit(const LLSD& event);
void forceQuit(const LLSD& event);
diff --git a/indra/newview/llfloaterluadebug.cpp b/indra/newview/llfloaterluadebug.cpp
index 60571d6247..f715327ec8 100644
--- a/indra/newview/llfloaterluadebug.cpp
+++ b/indra/newview/llfloaterluadebug.cpp
@@ -41,6 +41,7 @@
#include "llsdutil.h"
#include "lua_function.h"
#include "stringize.h"
+#include "tempset.h"
LLFloaterLUADebug::LLFloaterLUADebug(const LLSD &key)
@@ -77,6 +78,17 @@ LLFloaterLUADebug::~LLFloaterLUADebug()
void LLFloaterLUADebug::onExecuteClicked()
{
+ // Empirically, running Lua code that indirectly invokes the
+ // "LLNotifications" listener can result (via mysterious labyrinthine
+ // viewer UI byways) in a recursive call to this handler. We've seen Bad
+ // Things happen to the viewer with a second call to runScriptLine() with
+ // the same cmd on the same LuaState.
+ if (mExecuting)
+ {
+ LL_DEBUGS("Lua") << "recursive call to onExecuteClicked()" << LL_ENDL;
+ return;
+ }
+ TempSet executing(mExecuting, true);
mResultOutput->setValue("");
std::string cmd = mLineInput->getText();
@@ -94,6 +106,12 @@ void LLFloaterLUADebug::onBtnBrowse()
void LLFloaterLUADebug::onBtnRun()
{
+ if (mExecuting)
+ {
+ LL_DEBUGS("Lua") << "recursive call to onBtnRun()" << LL_ENDL;
+ return;
+ }
+ TempSet executing(mExecuting, true);
std::vector<std::string> filenames;
std::string filepath = mScriptPath->getText();
if (!filepath.empty())
@@ -105,6 +123,12 @@ void LLFloaterLUADebug::onBtnRun()
void LLFloaterLUADebug::runSelectedScript(const std::vector<std::string> &filenames)
{
+ if (mExecuting)
+ {
+ LL_DEBUGS("Lua") << "recursive call to runSelectedScript()" << LL_ENDL;
+ return;
+ }
+ TempSet executing(mExecuting, true);
mResultOutput->setValue("");
std::string filepath = filenames[0];
@@ -129,13 +153,19 @@ void LLFloaterLUADebug::completion(int count, const LLSD& result)
mResultOutput->endOfDoc();
return;
}
+ if (count == 0)
+ {
+ // no results
+ mResultOutput->pasteTextWithLinebreaks(stringize("ok ", ++mAck));
+ return;
+ }
if (count == 1)
{
// single result
mResultOutput->pasteTextWithLinebreaks(stringize(result));
return;
}
- // 0 or multiple results
+ // multiple results
const char* sep = "";
for (const auto& item : llsd::inArray(result))
{
diff --git a/indra/newview/llfloaterluadebug.h b/indra/newview/llfloaterluadebug.h
index 7418174570..ae30b7cf25 100644
--- a/indra/newview/llfloaterluadebug.h
+++ b/indra/newview/llfloaterluadebug.h
@@ -66,6 +66,8 @@ private:
LLLineEditor* mLineInput;
LLLineEditor* mScriptPath;
LuaState mState;
+ U32 mAck{ 0 };
+ bool mExecuting{ false };
};
#endif // LL_LLFLOATERLUADEBUG_H
diff --git a/indra/newview/llluamanager.cpp b/indra/newview/llluamanager.cpp
index 97779a12ad..3ed72c34f3 100644
--- a/indra/newview/llluamanager.cpp
+++ b/indra/newview/llluamanager.cpp
@@ -139,10 +139,10 @@ lua_function(get_event_pumps,
"post_on(commandpump, ...) to engage LLEventAPI operations (see helpleap()).")
{
luaL_checkstack(L, 2, nullptr);
- auto listener{ LuaState::obtainListener(L) };
+ 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());
+ lua_pushstdstring(L, listener.getReplyName());
+ lua_pushstdstring(L, listener.getCommandName());
return 2;
}
@@ -153,8 +153,8 @@ lua_function(get_event_next,
"event becomes available.")
{
luaL_checkstack(L, 2, nullptr);
- auto listener{ LuaState::obtainListener(L) };
- const auto& [pump, data]{ listener->getNext() };
+ auto& listener{ LuaState::obtainListener(L) };
+ const auto& [pump, data]{ listener.getNext() };
lua_pushstdstring(L, pump);
lua_pushllsd(L, data);
lluau::set_interrupts_counter(L, 0);
@@ -254,7 +254,7 @@ void LLLUAmanager::runScriptLine(LuaState& L, const std::string& chunk, script_r
if (shortchunk.length() > shortlen)
shortchunk = stringize(shortchunk.substr(0, shortlen), "...");
- std::string desc{ stringize("lua: ", shortchunk) };
+ std::string desc{ "lua: " + shortchunk };
LLCoros::instance().launch(desc, [&L, desc, chunk, cb]()
{
auto [count, result] = L.expr(desc, chunk);
@@ -386,10 +386,10 @@ void LLRequireResolver::findModule()
std::vector<fsyspath> lib_paths
{
- gDirUtilp->getExpandedFilename(LL_PATH_SCRIPTS, "lua"),
+ gDirUtilp->getExpandedFilename(LL_PATH_SCRIPTS, "lua", "require"),
#ifdef LL_TEST
// Build-time tests don't have the app bundle - use source tree.
- fsyspath(__FILE__).parent_path() / "scripts" / "lua",
+ fsyspath(__FILE__).parent_path() / "scripts" / "lua" / "require",
#endif
};
@@ -454,45 +454,70 @@ bool LLRequireResolver::findModuleImpl(const std::string& absolutePath)
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);
- // Try loading modules on Lua's main thread instead.
- lua_State *ML = 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);
+ lua_State *ML = lua_mainthread(L);
{
// 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)
+ LL_DEBUGS("Lua") << "Loading module " << desc << LL_ENDL;
+ 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);
- // we expect one return value
- int status = lua_pcall(ML, 0, 1, 0);
+ // error message on stack top
+ LL_DEBUGS("Lua") << "Error loading module " << desc << ": "
+ << lua_tostring(ML, -1) << LL_ENDL;
+ lua_pushliteral(ML, "loadstring: ");
+ // stack contains error, "loadstring: "
+ // swap: insert stack top at position -2
+ lua_insert(ML, -2);
+ // stack contains "loadstring: ", error
+ lua_concat(ML, 2);
+ // stack contains "loadstring: " + error
+ }
+ else // module code on stack top
+ {
+ // push debug module
+ lua_getglobal(ML, "debug");
+ // push debug.traceback
+ lua_getfield(ML, -1, "traceback");
+ // stack contains module code, debug, debug.traceback
+ // ditch debug
+ lua_replace(ML, -2);
+ // stack contains module code, debug.traceback
+ // swap: insert stack top at position -2
+ lua_insert(ML, -2);
+ // stack contains debug.traceback, module code
+ LL_DEBUGS("Lua") << "Loaded module " << desc << ", running" << LL_ENDL;
+ // no arguments, one return value
+ // pass debug.traceback as the error function
+ int status = lua_pcall(ML, 0, 1, -2);
+ // lua_pcall() has popped the module code and replaced it with its
+ // return value. Regardless of status or the type of the stack
+ // top, get rid of debug.traceback on the stack.
+ lua_remove(ML, -2);
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))
+ auto top{ lua_gettop(ML) };
+ std::string type{ (top == 0)? "nothing"
+ : lua_typename(ML, lua_type(ML, -1)) };
+ LL_DEBUGS("Lua") << "Module " << desc << " returned " << type << LL_ENDL;
+ if ((top == 0) || ! (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)));
+ desc.data(), type.data());
+ }
}
else if (status == LUA_YIELD)
{
+ LL_DEBUGS("Lua") << "Module " << desc << " yielded" << LL_ENDL;
lua_pushfstring(ML, "module %s can not yield", desc.data());
}
- else if (!lua_isstring(ML, -1))
+ else
{
- lua_pushfstring(ML, "unknown error while running module %s", desc.data());
+ llassert(lua_isstring(ML, -1));
+ LL_DEBUGS("Lua") << "Module " << desc << " error: "
+ << lua_tostring(ML, -1) << LL_ENDL;
}
}
}
@@ -502,8 +527,4 @@ void LLRequireResolver::runModule(const std::string& desc, const std::string& co
{
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/llwindowlistener.cpp b/indra/newview/llwindowlistener.cpp
index 87d18cfc00..efe395d9ca 100644
--- a/indra/newview/llwindowlistener.cpp
+++ b/indra/newview/llwindowlistener.cpp
@@ -55,7 +55,7 @@ LLWindowListener::LLWindowListener(LLViewerWindow *window, const KeyboardGetter&
"Given [\"keysym\"], [\"keycode\"] or [\"char\"], inject the specified ";
std::string keyExplain =
"(integer keycode values, or keysym string from any addKeyName() call in\n"
- "http://bitbucket.org/lindenlab/viewer-release/src/tip/indra/llwindow/llkeyboard.cpp )\n";
+ "https://github.com/secondlife/viewer/blob/main/indra/llwindow/llkeyboard.cpp#L68-L124)\n";
std::string mask =
"Specify optional [\"mask\"] as an array containing any of \"CTL\", \"ALT\",\n"
"\"SHIFT\" or \"MAC_CONTROL\"; the corresponding modifier bits will be combined\n"
diff --git a/indra/newview/scripts/lua/LLNotification.lua b/indra/newview/scripts/lua/LLNotification.lua
deleted file mode 100644
index f47730d1cc..0000000000
--- a/indra/newview/scripts/lua/LLNotification.lua
+++ /dev/null
@@ -1,15 +0,0 @@
--- Engage the LLNotificationsListener LLEventAPI
-
-leap = require 'leap'
-
-local LLNotification = {}
-
-function LLNotification.add(name, substitutions)
- leap.send('LLNotifications', {op='requestAdd', name=name, substitutions=substitutions})
-end
-
-function LLNotification.requestAdd(name, substitutions)
- return leap.request('LLNotifications', {op='requestAdd', name=name, substitutions=substitutions})['response']
-end
-
-return LLNotification
diff --git a/indra/newview/scripts/lua/UI.lua b/indra/newview/scripts/lua/UI.lua
deleted file mode 100644
index 24f822bbd9..0000000000
--- a/indra/newview/scripts/lua/UI.lua
+++ /dev/null
@@ -1,16 +0,0 @@
--- Engage the UI LLEventAPI
-
-leap = require 'leap'
-
-local UI = {}
-
-function UI.call(func, parameter)
- -- 'call' is fire-and-forget
- leap.request('UI', {op='call', ['function']=func, parameter=parameter})
-end
-
-function UI.getValue(path)
- return leap.request('UI', {op='getValue', path=path})['value']
-end
-
-return UI
diff --git a/indra/newview/scripts/lua/qtest.lua b/indra/newview/scripts/lua/qtest.lua
index 009446d0c3..9526f58b04 100644
--- a/indra/newview/scripts/lua/qtest.lua
+++ b/indra/newview/scripts/lua/qtest.lua
@@ -21,8 +21,8 @@ function resume(co, ...)
end
-- ------------------ Queue variables are instance-specific ------------------
-q1 = Queue:new()
-q2 = Queue:new()
+q1 = Queue()
+q2 = Queue()
q1:Enqueue(17)
@@ -33,8 +33,8 @@ assert(q1:Dequeue() == nil)
assert(q2:Dequeue() == nil)
-- ----------------------------- test WaitQueue ------------------------------
-q1 = WaitQueue:new()
-q2 = WaitQueue:new()
+q1 = WaitQueue()
+q2 = WaitQueue()
result = {}
values = { 1, 1, 2, 3, 5, 8, 13, 21 }
@@ -76,7 +76,7 @@ print('result:', inspect(result))
assert(util.equal(values, result))
-- try incrementally enqueueing values
-q3 = WaitQueue:new()
+q3 = WaitQueue()
result = {}
values = { 'This', 'is', 'a', 'test', 'script' }
@@ -124,7 +124,7 @@ print(string.format('%q', table.concat(result, ' ')))
assert(util.equal(values, result))
-- ----------------------------- test ErrorQueue -----------------------------
-q4 = ErrorQueue:new()
+q4 = ErrorQueue()
result = {}
values = { 'This', 'is', 'a', 'test', 'script' }
diff --git a/indra/newview/scripts/lua/ErrorQueue.lua b/indra/newview/scripts/lua/require/ErrorQueue.lua
index 13e4e92941..e6e9a5ef48 100644
--- a/indra/newview/scripts/lua/ErrorQueue.lua
+++ b/indra/newview/scripts/lua/require/ErrorQueue.lua
@@ -5,8 +5,11 @@
local WaitQueue = require('WaitQueue')
local function dbg(...) end
-- local dbg = require('printf')
+local util = require('util')
-local ErrorQueue = WaitQueue:new()
+local ErrorQueue = WaitQueue()
+
+util.classctor(ErrorQueue)
function ErrorQueue:Error(message)
-- Setting Error() is a marker, like closing the queue. Once we reach the
diff --git a/indra/newview/scripts/lua/Floater.lua b/indra/newview/scripts/lua/require/Floater.lua
index 75696533e4..d057a74386 100644
--- a/indra/newview/scripts/lua/Floater.lua
+++ b/indra/newview/scripts/lua/require/Floater.lua
@@ -2,6 +2,7 @@
local leap = require 'leap'
local fiber = require 'fiber'
+local util = require 'util'
-- list of all the events that a LLLuaFloater might send
local event_list = leap.request("LLFloaterReg", {op="getFloaterEvents"}).events
@@ -45,6 +46,8 @@ function Floater:new(path, extra)
return obj
end
+util.classctor(Floater)
+
function Floater:show()
-- leap.eventstream() returns the first response, and launches a
-- background fiber to call the passed callback with all subsequent
diff --git a/indra/newview/scripts/lua/LLChat.lua b/indra/newview/scripts/lua/require/LLChat.lua
index 7db538e837..78dca765e8 100644
--- a/indra/newview/scripts/lua/LLChat.lua
+++ b/indra/newview/scripts/lua/require/LLChat.lua
@@ -1,4 +1,4 @@
-leap = require 'leap'
+local leap = require 'leap'
local LLChat = {}
diff --git a/indra/newview/scripts/lua/LLChatListener.lua b/indra/newview/scripts/lua/require/LLChatListener.lua
index b4e90d272c..428dca881e 100644
--- a/indra/newview/scripts/lua/LLChatListener.lua
+++ b/indra/newview/scripts/lua/require/LLChatListener.lua
@@ -1,6 +1,7 @@
local fiber = require 'fiber'
local inspect = require 'inspect'
local leap = require 'leap'
+local util = require 'util'
local LLChatListener = {}
local waitfor = {}
@@ -14,6 +15,8 @@ function LLChatListener:new()
return obj
end
+util.classctor(LLChatListener)
+
function LLChatListener:handleMessages(event_data)
print(inspect(event_data))
return true
diff --git a/indra/newview/scripts/lua/LLDebugSettings.lua b/indra/newview/scripts/lua/require/LLDebugSettings.lua
index c250019a00..cff1a63c21 100644
--- a/indra/newview/scripts/lua/LLDebugSettings.lua
+++ b/indra/newview/scripts/lua/require/LLDebugSettings.lua
@@ -1,4 +1,4 @@
-leap = require 'leap'
+local leap = require 'leap'
local LLDebugSettings = {}
diff --git a/indra/newview/scripts/lua/LLFloaterAbout.lua b/indra/newview/scripts/lua/require/LLFloaterAbout.lua
index 44afee2e5c..a6e42d364f 100644
--- a/indra/newview/scripts/lua/LLFloaterAbout.lua
+++ b/indra/newview/scripts/lua/require/LLFloaterAbout.lua
@@ -1,6 +1,6 @@
-- Engage the LLFloaterAbout LLEventAPI
-leap = require 'leap'
+local leap = require 'leap'
local LLFloaterAbout = {}
diff --git a/indra/newview/scripts/lua/LLGesture.lua b/indra/newview/scripts/lua/require/LLGesture.lua
index cb410446d7..343b611e2c 100644
--- a/indra/newview/scripts/lua/LLGesture.lua
+++ b/indra/newview/scripts/lua/require/LLGesture.lua
@@ -1,6 +1,6 @@
-- Engage the LLGesture LLEventAPI
-leap = require 'leap'
+local leap = require 'leap'
local LLGesture = {}
diff --git a/indra/newview/scripts/lua/Queue.lua b/indra/newview/scripts/lua/require/Queue.lua
index 5ab2a8a72c..5bc72e4057 100644
--- a/indra/newview/scripts/lua/Queue.lua
+++ b/indra/newview/scripts/lua/require/Queue.lua
@@ -7,6 +7,8 @@
-- But had to resist
-- For fear it might be too obscua.
+local util = require 'util'
+
local Queue = {}
function Queue:new()
@@ -20,6 +22,8 @@ function Queue:new()
return obj
end
+util.classctor(Queue)
+
-- Check if the queue is empty
function Queue:IsEmpty()
return self._first > self._last
diff --git a/indra/newview/scripts/lua/require/UI.lua b/indra/newview/scripts/lua/require/UI.lua
new file mode 100644
index 0000000000..eb1a4017c7
--- /dev/null
+++ b/indra/newview/scripts/lua/require/UI.lua
@@ -0,0 +1,125 @@
+-- Engage the viewer's UI
+
+local leap = require 'leap'
+local Timer = (require 'timers').Timer
+local mapargs = require 'mapargs'
+
+local UI = {}
+
+-- ***************************************************************************
+-- registered menu actions
+-- ***************************************************************************
+function UI.call(func, parameter)
+ -- 'call' is fire-and-forget
+ leap.request('UI', {op='call', ['function']=func, parameter=parameter})
+end
+
+function UI.getValue(path)
+ return leap.request('UI', {op='getValue', path=path})['value']
+end
+
+-- ***************************************************************************
+-- UI views
+-- ***************************************************************************
+-- Either:
+-- wreq{op='Something', a=1, b=2, ...}
+-- or:
+-- (args should be local, as this wreq() call modifies it)
+-- local args = {a=1, b=2, ...}
+-- wreq('Something', args)
+local function wreq(op_or_data, data_if_op)
+ if data_if_op ~= nil then
+ -- this is the wreq(op, data) form
+ data_if_op.op = op_or_data
+ op_or_data = data_if_op
+ end
+ return leap.request('LLWindow', op_or_data)
+end
+
+-- omit 'parent' to list all view paths
+function UI.listviews(parent)
+ return wreq{op='getPaths', under=parent}
+end
+
+function UI.viewinfo(path)
+ return wreq{op='getInfo', path=path}
+end
+
+-- ***************************************************************************
+-- mouse actions
+-- ***************************************************************************
+-- pass a table:
+-- UI.click{path=path
+-- [, button='LEFT' | 'CENTER' | 'RIGHT']
+-- [, x=x, y=y]
+-- [, hold=duration]}
+function UI.click(...)
+ local args = mapargs('path,button,x,y,hold', ...)
+ args.button = args.button or 'LEFT'
+ local hold = args.hold or 1.0
+ wreq('mouseMove', args)
+ wreq('mouseDown', args)
+ Timer(hold, 'wait')
+ wreq('mouseUp', args)
+end
+
+-- pass a table as for UI.click()
+function UI.doubleclick(...)
+ local args = mapargs('path,button,x,y', ...)
+ args.button = args.button or 'LEFT'
+ wreq('mouseDown', args)
+ wreq('mouseUp', args)
+ wreq('mouseDown', args)
+ wreq('mouseUp', args)
+end
+
+-- UI.drag{path=, xoff=, yoff=}
+function UI.drag(...)
+ local args = mapargs('path,xoff,yoff', ...)
+ -- query the specified path
+ local rect = UI.viewinfo(args.path).rect
+ local centerx = math.floor(rect.left + (rect.right - rect.left)/2)
+ local centery = math.floor(rect.bottom + (rect.top - rect.bottom)/2)
+ wreq{op='mouseMove', path=args.path, x=centerx, y=centery}
+ wreq{op='mouseDown', path=args.path, button='LEFT'}
+ wreq{op='mouseMove', path=args.path, x=centerx + args.xoff, y=centery + args.yoff}
+ wreq{op='mouseUp', path=args.path, button='LEFT'}
+end
+
+-- ***************************************************************************
+-- keyboard actions
+-- ***************************************************************************
+-- pass a table:
+-- UI.keypress{
+-- [path=path] -- if omitted, default input field
+-- [, char='x'] -- requires one of char, keycode, keysym
+-- [, keycode=120]
+-- keysym per https://github.com/secondlife/viewer/blob/main/indra/llwindow/llkeyboard.cpp#L68-L124
+-- [, keysym='Enter']
+-- [, mask={'SHIFT', 'CTL', 'ALT', 'MAC_CONTROL'}] -- some subset of these
+-- }
+function UI.keypress(...)
+ local args = mapargs('path,char,keycode,keysym,mask', ...)
+ if args.char == '\n' then
+ args.char = nil
+ args.keysym = 'Enter'
+ end
+ return wreq('keyDown', args)
+end
+
+-- UI.type{text=, path=}
+function UI.type(...)
+ local args = mapargs('text,path', ...)
+ if #args.text > 0 then
+ -- The caller's path may be specified in a way that requires recursively
+ -- searching parts of the LLView tree. No point in doing that more than
+ -- once. Capture the actual path found by that first call and use that for
+ -- subsequent calls.
+ local path = UI.keypress{path=args.path, char=string.sub(args.text, 1, 1)}.path
+ for i = 2, #args.text do
+ UI.keypress{path=path, char=string.sub(args.text, i, i)}
+ end
+ end
+end
+
+return UI
diff --git a/indra/newview/scripts/lua/WaitQueue.lua b/indra/newview/scripts/lua/require/WaitQueue.lua
index 6bcb9d62c2..7e10d03295 100644
--- a/indra/newview/scripts/lua/WaitQueue.lua
+++ b/indra/newview/scripts/lua/require/WaitQueue.lua
@@ -4,14 +4,15 @@
local fiber = require('fiber')
local Queue = require('Queue')
+local util = require('util')
local function dbg(...) end
-- local dbg = require('printf')
-local WaitQueue = Queue:new()
+local WaitQueue = Queue()
function WaitQueue:new()
- local obj = Queue:new()
+ local obj = Queue()
setmetatable(obj, self)
self.__index = self
@@ -20,6 +21,8 @@ function WaitQueue:new()
return obj
end
+util.classctor(WaitQueue)
+
function WaitQueue:Enqueue(value)
if self._closed then
error("can't Enqueue() on closed Queue")
diff --git a/indra/newview/scripts/lua/coro.lua b/indra/newview/scripts/lua/require/coro.lua
index 616a797e95..616a797e95 100644
--- a/indra/newview/scripts/lua/coro.lua
+++ b/indra/newview/scripts/lua/require/coro.lua
diff --git a/indra/newview/scripts/lua/fiber.lua b/indra/newview/scripts/lua/require/fiber.lua
index cae27b936b..cae27b936b 100644
--- a/indra/newview/scripts/lua/fiber.lua
+++ b/indra/newview/scripts/lua/require/fiber.lua
diff --git a/indra/newview/scripts/lua/inspect.lua b/indra/newview/scripts/lua/require/inspect.lua
index 9900a0b81b..9900a0b81b 100644
--- a/indra/newview/scripts/lua/inspect.lua
+++ b/indra/newview/scripts/lua/require/inspect.lua
diff --git a/indra/newview/scripts/lua/leap.lua b/indra/newview/scripts/lua/require/leap.lua
index 8caae24e94..82f91ce9e9 100644
--- a/indra/newview/scripts/lua/leap.lua
+++ b/indra/newview/scripts/lua/require/leap.lua
@@ -43,6 +43,7 @@ local ErrorQueue = require('ErrorQueue')
local inspect = require('inspect')
local function dbg(...) end
-- local dbg = require('printf')
+local util = require('util')
local leap = {}
@@ -129,7 +130,7 @@ local function requestSetup(pump, data)
-- because, unlike the WaitFor base class, WaitForReqid does not
-- self-register on our waitfors list. Instead, capture the new
-- WaitForReqid object in pending so dispatch() can find it.
- local waitfor = leap.WaitForReqid:new(reqid)
+ local waitfor = leap.WaitForReqid(reqid)
pending[reqid] = waitfor
-- Pass reqid to send() to stamp it into (a copy of) the request data.
dbg('requestSetup(%s, %s) storing %s', pump, data, waitfor.name)
@@ -432,7 +433,7 @@ function leap.WaitFor:new(priority, name)
self._id += 1
obj.name = 'WaitFor' .. self._id
end
- obj._queue = ErrorQueue:new()
+ obj._queue = ErrorQueue()
obj._registered = false
-- if no priority, then don't enable() - remember 0 is truthy
if priority then
@@ -442,6 +443,8 @@ function leap.WaitFor:new(priority, name)
return obj
end
+util.classctor(leap.WaitFor)
+
-- Re-enable a disable()d WaitFor object. New WaitFor objects are
-- enable()d by default.
function leap.WaitFor:enable()
@@ -514,13 +517,13 @@ function leap.WaitFor:exception(message)
end
-- ------------------------------ WaitForReqid -------------------------------
-leap.WaitForReqid = leap.WaitFor:new()
+leap.WaitForReqid = leap.WaitFor()
function leap.WaitForReqid:new(reqid)
-- priority is meaningless, since this object won't be added to the
-- priority-sorted waitfors list. Use the reqid as the debugging name
-- string.
- local obj = leap.WaitFor:new(nil, 'WaitForReqid(' .. reqid .. ')')
+ local obj = leap.WaitFor(nil, 'WaitForReqid(' .. reqid .. ')')
setmetatable(obj, self)
self.__index = self
@@ -529,6 +532,8 @@ function leap.WaitForReqid:new(reqid)
return obj
end
+util.classctor(leap.WaitForReqid)
+
function leap.WaitForReqid:filter(pump, data)
-- Because we expect to directly look up the WaitForReqid object of
-- interest based on the incoming ["reqid"] value, it's not necessary
diff --git a/indra/newview/scripts/lua/require/login.lua b/indra/newview/scripts/lua/require/login.lua
new file mode 100644
index 0000000000..0d8591cace
--- /dev/null
+++ b/indra/newview/scripts/lua/require/login.lua
@@ -0,0 +1,19 @@
+local UI = require 'UI'
+local leap = require 'leap'
+
+local function login(username, password)
+ if username and password then
+ local userpath = '//username_combo/Combo Text Entry'
+ local passpath = '//password_edit'
+ -- first clear anything presently in those text fields
+ for _, path in pairs({userpath, passpath}) do
+ UI.click(path)
+ UI.keypress{keysym='Backsp', path=path}
+ end
+ UI.type{path=userpath, text=username}
+ UI.type{path=passpath, text=password}
+ end
+ leap.send('LLPanelLogin', {op='onClickConnect'})
+end
+
+return login
diff --git a/indra/newview/scripts/lua/require/logout.lua b/indra/newview/scripts/lua/require/logout.lua
new file mode 100644
index 0000000000..63dcd7f01f
--- /dev/null
+++ b/indra/newview/scripts/lua/require/logout.lua
@@ -0,0 +1,7 @@
+local leap = require 'leap'
+
+local function logout()
+ leap.send('LLAppViewer', {op='userQuit'});
+end
+
+return logout
diff --git a/indra/newview/scripts/lua/require/mapargs.lua b/indra/newview/scripts/lua/require/mapargs.lua
new file mode 100644
index 0000000000..45f5a9c556
--- /dev/null
+++ b/indra/newview/scripts/lua/require/mapargs.lua
@@ -0,0 +1,73 @@
+-- Allow a calling function to be passed a mix of positional arguments with
+-- keyword arguments. Reference them as fields of a table.
+-- Don't use this for a function that can accept a single table argument.
+-- mapargs() assumes that a single table argument means its caller was called
+-- with f{table constructor} syntax, and maps that table to the specified names.
+-- Usage:
+-- function f(...)
+-- local a = mapargs({'a1', 'a2', 'a3'}, ...)
+-- ... a.a1 ... etc.
+-- end
+-- f(10, 20, 30) -- a.a1 == 10, a.a2 == 20, a.a3 == 30
+-- f{10, 20, 30} -- a.a1 == 10, a.a2 == 20, a.a3 == 30
+-- f{a3=300, a1=100} -- a.a1 == 100, a.a2 == nil, a.a3 == 300
+-- f{1, a3=3} -- a.a1 == 1, a.a2 == nil, a.a3 == 3
+-- f{a3=3, 1} -- a.a1 == 1, a.a2 == nil, a.a3 == 3
+local function mapargs(names, ...)
+ local args = table.pack(...)
+ local posargs = {}
+ local keyargs = {}
+ -- For a mixed table, no Lua operation will reliably tell you how many
+ -- array items it contains, if there are any holes. Track that by hand.
+ -- We must be able to handle f(1, nil, 3) calls.
+ local maxpos = 0
+
+ -- For convenience, allow passing 'names' as a string 'n0,n1,...'
+ if type(names) == 'string' then
+ names = string.split(names, ',')
+ end
+
+ if not (args.n == 1 and type(args[1]) == 'table') then
+ -- If caller passes more than one argument, or if the first argument
+ -- is not a table, then it's classic positional function-call syntax:
+ -- f(first, second, etc.). In that case we need not bother teasing
+ -- apart positional from keyword arguments.
+ posargs = args
+ maxpos = args.n
+ else
+ -- Single table argument implies f{mixed} syntax.
+ -- Tease apart positional arguments from keyword arguments.
+ for k, v in pairs(args[1]) do
+ if type(k) == 'number' then
+ posargs[k] = v
+ maxpos = math.max(maxpos, k)
+ else
+ if table.find(names, k) == nil then
+ error('unknown keyword argument ' .. tostring(k))
+ end
+ keyargs[k] = v
+ end
+ end
+ end
+
+ -- keyargs already has keyword arguments in place, just fill in positionals
+ args = keyargs
+ -- Don't exceed the number of parameter names. Loop explicitly over every
+ -- index value instead of using ipairs() so we can support holes (nils) in
+ -- posargs.
+ for i = 1, math.min(#names, maxpos) do
+ if posargs[i] ~= nil then
+ -- As in Python, make it illegal to pass an argument both positionally
+ -- and by keyword. This implementation permits func(17, first=nil), a
+ -- corner case about which I don't particularly care.
+ if args[names[i]] ~= nil then
+ error(string.format('parameter %s passed both positionally and by keyword',
+ tostring(names[i])))
+ end
+ args[names[i]] = posargs[i]
+ end
+ end
+ return args
+end
+
+return mapargs
diff --git a/indra/newview/scripts/lua/require/popup.lua b/indra/newview/scripts/lua/require/popup.lua
new file mode 100644
index 0000000000..3aaadf85ba
--- /dev/null
+++ b/indra/newview/scripts/lua/require/popup.lua
@@ -0,0 +1,53 @@
+local leap = require 'leap'
+local mapargs = require 'mapargs'
+
+-- notification is any name defined in notifications.xml as
+-- <notification name=>
+-- vars is a table providing values for [VAR] substitution keys in the
+-- notification body
+-- payload prepopulates the response table
+-- wait=false means fire and forget, otherwise wait for user response
+local popup_meta = {
+ -- setting this function as getmetatable(popup).__call() means this gets
+ -- called when a consumer calls popup(notification, vars, payload)
+ __call = function(self, ...)
+ local args = mapargs('notification,vars,payload,wait', ...)
+ -- we use convenience argument names different from 'LLNotifications'
+ -- listener
+ args.name = args.notification
+ args.notification = nil
+ args.substitutions = args.vars
+ args.vars = nil
+ local wait = args.wait
+ args.wait = nil
+ args.op = 'requestAdd'
+ -- Specifically test (wait == false), NOT (not wait), because we treat
+ -- nil (omitted, default true) differently than false (explicitly
+ -- DON'T wait).
+ if wait == false then
+ leap.send('LLNotifications', args)
+ else
+ return leap.request('LLNotifications', args).response
+ end
+ end
+}
+
+local popup = setmetatable({}, popup_meta)
+
+function popup:alert(message)
+ return self('GenericAlert', {MESSAGE=message})
+end
+
+function popup:alertOK(message)
+ return self('GenericAlertOK', {MESSAGE=message})
+end
+
+function popup:alertYesCancel(message)
+ return self('GenericAlertYesCancel', {MESSAGE=message})
+end
+
+function popup:tip(message)
+ self{'SystemMessageTip', {MESSAGE=message}, wait=false}
+end
+
+return popup
diff --git a/indra/newview/scripts/lua/printf.lua b/indra/newview/scripts/lua/require/printf.lua
index e84b2024df..e84b2024df 100644
--- a/indra/newview/scripts/lua/printf.lua
+++ b/indra/newview/scripts/lua/require/printf.lua
diff --git a/indra/newview/scripts/lua/startup.lua b/indra/newview/scripts/lua/require/startup.lua
index 4311bb9a60..c3040f94b8 100644
--- a/indra/newview/scripts/lua/startup.lua
+++ b/indra/newview/scripts/lua/require/startup.lua
@@ -12,6 +12,8 @@ local function dbg(...) end
-- local dbg = require 'printf'
-- ---------------------------------------------------------------------------
+local startup = {}
+
-- Get the list of startup states from the viewer.
local bynum = leap.request('LLStartUp', {op='getStateTable'})['table']
@@ -19,7 +21,7 @@ local byname = setmetatable(
{},
-- set metatable to throw an error if you look up invalid state name
{__index=function(t, k)
- local v = t[k]
+ local v = rawget(t, k)
if v then
return v
end
@@ -35,7 +37,7 @@ end
-- specialize a WaitFor to track the viewer's startup state
local startup_pump = 'StartupState'
-local waitfor = leap.WaitFor:new(0, startup_pump)
+local waitfor = leap.WaitFor(0, startup_pump)
function waitfor:filter(pump, data)
if pump == self.name then
return data
@@ -57,8 +59,6 @@ leap.request(leap.cmdpump(),
leap.send('LLStartUp', {op='postStartupState'})
-- ---------------------------------------------------------------------------
-startup = {}
-
-- wait for response from postStartupState
while not startup._state do
dbg('startup.state() waiting for first StartupState event')
@@ -98,4 +98,3 @@ function startup.wait(state)
end
return startup
-
diff --git a/indra/newview/scripts/lua/timers.lua b/indra/newview/scripts/lua/require/timers.lua
index e0d27a680d..e4938078dc 100644
--- a/indra/newview/scripts/lua/timers.lua
+++ b/indra/newview/scripts/lua/require/timers.lua
@@ -1,6 +1,7 @@
-- Access to the viewer's time-delay facilities
local leap = require 'leap'
+local util = require 'util'
local timers = {}
@@ -78,6 +79,8 @@ function timers.Timer:new(delay, callback, iterate)
return obj
end
+util.classctor(timers.Timer)
+
function timers.Timer:tick()
error('Pass a callback to Timer:new(), or override Timer:tick()')
end
diff --git a/indra/newview/scripts/lua/util.lua b/indra/newview/scripts/lua/require/util.lua
index a2191288f6..bfbfc8637c 100644
--- a/indra/newview/scripts/lua/util.lua
+++ b/indra/newview/scripts/lua/require/util.lua
@@ -2,6 +2,31 @@
local util = {}
+-- Allow MyClass(ctor args...) equivalent to MyClass:new(ctor args...)
+-- Usage:
+-- local MyClass = {}
+-- function MyClass:new(...)
+-- ...
+-- end
+-- ...
+-- util.classctor(MyClass)
+-- or if your constructor is named something other than MyClass:new(), e.g.
+-- MyClass:construct():
+-- util.classctor(MyClass, MyClass.construct)
+-- return MyClass
+function util.classctor(class, ctor)
+ -- get the metatable for the passed class
+ local mt = getmetatable(class)
+ if mt == nil then
+ -- if it doesn't already have a metatable, then create one
+ mt = {}
+ setmetatable(class, mt)
+ end
+ -- now that class has a metatable, set its __call method to the specified
+ -- constructor method (class.new if not specified)
+ mt.__call = ctor or class.new
+end
+
-- check if array-like table contains certain value
function util.contains(t, v)
return table.find(t, v) ~= nil
diff --git a/indra/newview/scripts/lua/test_LLChatListener.lua b/indra/newview/scripts/lua/test_LLChatListener.lua
index b9696e7cfc..18363ed43b 100644
--- a/indra/newview/scripts/lua/test_LLChatListener.lua
+++ b/indra/newview/scripts/lua/test_LLChatListener.lua
@@ -11,7 +11,7 @@ function openOrEcho(message)
end
end
-local listener = LLChatListener:new()
+local listener = LLChatListener()
function listener:handleMessages(event_data)
if string.find(event_data.message, '[LUA]') then
diff --git a/indra/newview/scripts/lua/test_atexit.lua b/indra/newview/scripts/lua/test_atexit.lua
new file mode 100644
index 0000000000..6fbc0f3eb1
--- /dev/null
+++ b/indra/newview/scripts/lua/test_atexit.lua
@@ -0,0 +1,3 @@
+LL.atexit(function() print('Third') end)
+LL.atexit(function() print('Second') end)
+LL.atexit(function() print('First') end)
diff --git a/indra/newview/scripts/lua/test_login.lua b/indra/newview/scripts/lua/test_login.lua
new file mode 100644
index 0000000000..a8c31807bc
--- /dev/null
+++ b/indra/newview/scripts/lua/test_login.lua
@@ -0,0 +1,7 @@
+startup = require 'startup'
+login = require 'login'
+
+startup.wait('STATE_LOGIN_WAIT')
+login()
+-- Fill in valid credentials as they would be entered on the login screen
+-- login('My Username', 'password')
diff --git a/indra/newview/scripts/lua/test_logout.lua b/indra/newview/scripts/lua/test_logout.lua
new file mode 100644
index 0000000000..b1ac59e38c
--- /dev/null
+++ b/indra/newview/scripts/lua/test_logout.lua
@@ -0,0 +1,3 @@
+logout = require 'logout'
+
+logout()
diff --git a/indra/newview/scripts/lua/test_luafloater_demo.lua b/indra/newview/scripts/lua/test_luafloater_demo.lua
deleted file mode 100644
index ab638dcdd1..0000000000
--- a/indra/newview/scripts/lua/test_luafloater_demo.lua
+++ /dev/null
@@ -1,77 +0,0 @@
-XML_FILE_PATH = LL.abspath("luafloater_demo.xml")
-
-scriptparts = string.split(LL.source_path(), '/')
-scriptname = scriptparts[#scriptparts]
-print('Running ' .. scriptname)
-
-leap = require 'leap'
-fiber = require 'fiber'
-startup = require 'startup'
-
---event pump for sending actions to the floater
-local COMMAND_PUMP_NAME = ""
-local reqid
---table of floater UI events
-event_list=leap.request("LLFloaterReg", {op="getFloaterEvents"}).events
-
-local function _event(event_name)
- if not table.find(event_list, event_name) then
- LL.print_warning("Incorrect event name: " .. event_name)
- end
- return event_name
-end
-
-function post(action)
- leap.send(COMMAND_PUMP_NAME, action)
-end
-
-function getCurrentTime()
- local currentTime = os.date("*t")
- return string.format("%02d:%02d:%02d", currentTime.hour, currentTime.min, currentTime.sec)
-end
-
-function handleEvents(event_data)
- post({action="add_text", ctrl_name="events_editor", value = event_data})
- if event_data.event == _event("commit") then
- if event_data.ctrl_name == "disable_ctrl" then
- post({action="set_enabled", ctrl_name="open_btn", value = (1 - event_data.value)})
- elseif event_data.ctrl_name == "title_cmb" then
- post({action="set_title", value= event_data.value})
- elseif event_data.ctrl_name == "open_btn" then
- floater_name = leap.request(COMMAND_PUMP_NAME, {action="get_value", ctrl_name='openfloater_cmd'})['value']
- leap.send("LLFloaterReg", {name = floater_name, op = "showInstance"})
- end
- elseif event_data.event == _event("double_click") then
- if event_data.ctrl_name == "show_time_lbl" then
- post({action="set_value", ctrl_name="time_lbl", value= getCurrentTime()})
- end
- elseif event_data.event == _event("floater_close") then
- LL.print_warning("Floater was closed")
- return false
- end
- return true
-end
-
-startup.wait('STATE_LOGIN_WAIT')
-local key = {xml_path = XML_FILE_PATH, op = "showLuaFloater"}
---sign for additional events for defined control {<control_name>= {action1, action2, ...}}
-key.extra_events={show_time_lbl = {_event("right_mouse_down"), _event("double_click")}}
-local resp = leap.request("LLFloaterReg", key)
-COMMAND_PUMP_NAME = resp.command_name
-reqid = resp.reqid
-
-catch_events = leap.WaitFor:new(-1, "all_events")
-function catch_events:filter(pump, data)
- if data.reqid == reqid then
- return data
- end
-end
-
-function process_events(waitfor)
- event_data = waitfor:wait()
- while event_data and handleEvents(event_data) do
- event_data = waitfor:wait()
- end
-end
-
-fiber.launch("catch_events", process_events, catch_events)
diff --git a/indra/newview/scripts/lua/test_luafloater_demo2.lua b/indra/newview/scripts/lua/test_luafloater_demo2.lua
index 9e24237d28..3903d01e65 100644
--- a/indra/newview/scripts/lua/test_luafloater_demo2.lua
+++ b/indra/newview/scripts/lua/test_luafloater_demo2.lua
@@ -2,7 +2,7 @@ local Floater = require 'Floater'
local leap = require 'leap'
local startup = require 'startup'
-local flt = Floater:new(
+local flt = Floater(
'luafloater_demo.xml',
{show_time_lbl = {"right_mouse_down", "double_click"}})
diff --git a/indra/newview/scripts/lua/test_luafloater_gesture_list.lua b/indra/newview/scripts/lua/test_luafloater_gesture_list.lua
deleted file mode 100644
index 3d9a9b0ad4..0000000000
--- a/indra/newview/scripts/lua/test_luafloater_gesture_list.lua
+++ /dev/null
@@ -1,75 +0,0 @@
-XML_FILE_PATH = LL.abspath("luafloater_gesture_list.xml")
-
-scriptparts = string.split(LL.source_path(), '/')
-scriptname = scriptparts[#scriptparts]
-print('Running ' .. scriptname)
-
-leap = require 'leap'
-fiber = require 'fiber'
-LLGesture = require 'LLGesture'
-startup = require 'startup'
-
---event pump for sending actions to the floater
-local COMMAND_PUMP_NAME = ""
-local reqid
---table of floater UI events
-event_list=leap.request("LLFloaterReg", {op="getFloaterEvents"}).events
-
-local function _event(event_name)
- if not table.find(event_list, event_name) then
- LL.print_warning("Incorrect event name: " .. event_name)
- end
- return event_name
-end
-
-function post(action)
- leap.send(COMMAND_PUMP_NAME, action)
-end
-
-function handleEvents(event_data)
- if event_data.event == _event("floater_close") then
- return false
- end
-
- if event_data.event == _event("post_build") then
- COMMAND_PUMP_NAME = event_data.command_name
- reqid = event_data.reqid
- gestures_uuid = LLGesture.getActiveGestures()
- local action_data = {}
- action_data.action = "add_list_element"
- action_data.ctrl_name = "gesture_list"
- gestures = {}
- for uuid, info in pairs(gestures_uuid) do
- table.insert(gestures, {value = uuid, columns ={column = "gesture_name", value = info.name}})
- end
- action_data.value = gestures
- post(action_data)
- elseif event_data.event == _event("double_click") then
- if event_data.ctrl_name == "gesture_list" then
- LLGesture.startGesture(event_data.value)
- end
- end
- return true
-end
-
-startup.wait('STATE_STARTED')
-local key = {xml_path = XML_FILE_PATH, op = "showLuaFloater"}
---receive additional events for defined control {<control_name>= {action1, action2, ...}}
-key.extra_events={gesture_list = {_event("double_click")}}
-handleEvents(leap.request("LLFloaterReg", key))
-
-catch_events = leap.WaitFor:new(-1, "all_events")
-function catch_events:filter(pump, data)
- if data.reqid == reqid then
- return data
- end
-end
-
-function process_events(waitfor)
- event_data = waitfor:wait()
- while event_data and handleEvents(event_data) do
- event_data = waitfor:wait()
- end
-end
-
-fiber.launch("catch_events", process_events, catch_events)
diff --git a/indra/newview/scripts/lua/test_luafloater_gesture_list2.lua b/indra/newview/scripts/lua/test_luafloater_gesture_list2.lua
index d702d09c51..bd397ef2a6 100644
--- a/indra/newview/scripts/lua/test_luafloater_gesture_list2.lua
+++ b/indra/newview/scripts/lua/test_luafloater_gesture_list2.lua
@@ -2,7 +2,7 @@ local Floater = require 'Floater'
local LLGesture = require 'LLGesture'
local startup = require 'startup'
-local flt = Floater:new(
+local flt = Floater(
"luafloater_gesture_list.xml",
{gesture_list = {"double_click"}})
diff --git a/indra/newview/scripts/lua/test_luafloater_speedometer.lua b/indra/newview/scripts/lua/test_luafloater_speedometer.lua
index a9d3a70330..af7189a2cb 100644
--- a/indra/newview/scripts/lua/test_luafloater_speedometer.lua
+++ b/indra/newview/scripts/lua/test_luafloater_speedometer.lua
@@ -1,10 +1,10 @@
local Floater = require 'Floater'
local leap = require 'leap'
-local LLNotification = require 'LLNotification'
+local popup = require 'popup'
local startup = require 'startup'
local Timer = (require 'timers').Timer
local max_speed = 0
-local flt = Floater:new("luafloater_speedometer.xml")
+local flt = Floater("luafloater_speedometer.xml")
startup.wait('STATE_STARTED')
local timer
@@ -13,8 +13,7 @@ function flt:floater_close(event_data)
if timer then
timer:cancel()
end
- msg = "Registered max speed: " .. string.format("%.2f", max_speed) .. " m/s";
- LLNotification.add('SystemMessageTip', {MESSAGE = msg})
+ popup:tip(string.format("Registered max speed: %.2f m/s", max_speed))
end
local function idle(event_data)
@@ -24,9 +23,9 @@ local function idle(event_data)
end
msg = 'Are you sure you want to run this "speedometer" script?'
-response = LLNotification.requestAdd('GenericAlertYesCancel', {MESSAGE = msg})
+response = popup:alertYesCancel(msg)
if response.OK_okcancelbuttons then
flt:show()
- timer = Timer:new(1, idle, true) -- iterate
+ timer = Timer(1, idle, true) -- iterate
end
diff --git a/indra/newview/scripts/lua/test_mapargs.lua b/indra/newview/scripts/lua/test_mapargs.lua
new file mode 100644
index 0000000000..999a57acb4
--- /dev/null
+++ b/indra/newview/scripts/lua/test_mapargs.lua
@@ -0,0 +1,68 @@
+local mapargs = require 'mapargs'
+local inspect = require 'inspect'
+
+function tabfunc(...)
+ local a = mapargs({'a1', 'a2', 'a3'}, ...)
+ print(inspect(a))
+end
+
+print('----------')
+print('f(10, 20, 30)')
+tabfunc(10, 20, 30)
+print('f(10, nil, 30)')
+tabfunc(10, nil, 30)
+print('f{10, 20, 30}')
+tabfunc{10, 20, 30}
+print('f{10, nil, 30}')
+tabfunc{10, nil, 30}
+print('f{a3=300, a1=100}')
+tabfunc{a3=300, a1=100}
+print('f{1, a3=3}')
+tabfunc{1, a3=3}
+print('f{a3=3, 1}')
+tabfunc{a3=3, 1}
+print('----------')
+
+if false then
+ -- the code below was used to explore ideas that became mapargs()
+ mixed = { '[1]', nil, '[3]', abc='[abc]', '[3]', def='[def]' }
+ local function showtable(desc, t)
+ print(string.format('%s (len %s)\n%s', desc, #t, inspect(t)))
+ end
+ showtable('mixed', mixed)
+
+ print('ipairs(mixed)')
+ for k, v in ipairs(mixed) do
+ print(string.format('[%s] = %s', k, tostring(v)))
+ end
+
+ print('table.pack(mixed)')
+ print(inspect(table.pack(mixed)))
+
+ local function nilarg(desc, a, b, c)
+ print(desc)
+ print('a = ' .. tostring(a))
+ print('b = ' .. tostring(b))
+ print('c = ' .. tostring(c))
+ end
+
+ nilarg('nilarg(1)', 1)
+ nilarg('nilarg(1, nil, 3)', 1, nil, 3)
+
+ local function nilargs(desc, ...)
+ args = table.pack(...)
+ showtable(desc, args)
+ end
+
+ nilargs('nilargs{a=1, b=2, c=3}', {a=1, b=2, c=3})
+ nilargs('nilargs(1, 2, 3)', 1, 2, 3)
+ nilargs('nilargs(1, nil, 3)', 1, nil, 3)
+ nilargs('nilargs{1, 2, 3}', {1, 2, 3})
+ nilargs('nilargs{1, nil, 3}', {1, nil, 3})
+
+ print('table.unpack({1, nil, 3})')
+ a, b, c = table.unpack({1, nil, 3})
+ print('a = ' .. tostring(a))
+ print('b = ' .. tostring(b))
+ print('c = ' .. tostring(c))
+end
diff --git a/indra/newview/scripts/lua/test_popup.lua b/indra/newview/scripts/lua/test_popup.lua
new file mode 100644
index 0000000000..e48f89c3a7
--- /dev/null
+++ b/indra/newview/scripts/lua/test_popup.lua
@@ -0,0 +1,6 @@
+popup = require 'popup'
+
+response = popup:alert('This just has a Close button')
+response = popup:alertOK(string.format('You said "%s", is that OK?', next(response)))
+response = popup:alertYesCancel(string.format('You said "%s"', next(response)))
+popup:tip(string.format('You said "%s"', next(response)))
diff --git a/indra/newview/scripts/lua/test_timers.lua b/indra/newview/scripts/lua/test_timers.lua
index ed0de070f7..be5001aa16 100644
--- a/indra/newview/scripts/lua/test_timers.lua
+++ b/indra/newview/scripts/lua/test_timers.lua
@@ -6,9 +6,9 @@ local timers = require 'timers'
-- is true, that timeUntilCall() is (true, close to 10), that cancel() returns
-- true. After that, isRunning() is false, timeUntilCall() returns (false, 0),
-- and a second cancel() returns false.
-print('t0:new(10)')
+print('t0(10)')
start = os.clock()
-t0 = timers.Timer:new(10, function() print('t0 fired at', os.clock() - start) end)
+t0 = timers.Timer(10, function() print('t0 fired at', os.clock() - start) end)
print('t0:isRunning(): ', t0:isRunning())
print('t0:timeUntilCall(): ', t0:timeUntilCall())
print('t0:cancel(): ', t0:cancel())
@@ -18,16 +18,16 @@ print('t0:cancel(): ', t0:cancel())
-- t1 is supposed to fire after 5 seconds, but it doesn't wait, so you see the
-- t2 messages immediately after.
-print('t1:new(5)')
+print('t1(5)')
start = os.clock()
-t1 = timers.Timer:new(5, function() print('t1 fired at', os.clock() - start) end)
+t1 = timers.Timer(5, function() print('t1 fired at', os.clock() - start) end)
-- t2 illustrates that instead of passing a callback to new(), you can
-- override the timer instance's tick() method. But t2 doesn't wait either, so
-- you see the Timer(5) message immediately.
-print('t2:new(2)')
+print('t2(2)')
start = os.clock()
-t2 = timers.Timer:new(2)
+t2 = timers.Timer(2)
function t2:tick()
print('t2 fired at', os.clock() - start)
end
@@ -37,7 +37,7 @@ end
-- then the t1 callback message before the Timer(5) completion message.
print('Timer(5) waiting')
start = os.clock()
-timers.Timer:new(5, 'wait')
+timers.Timer(5, 'wait')
print(string.format('Timer(5) waited %f seconds', os.clock() - start))
-- This test demonstrates a repeating timer. It also shows that you can (but
@@ -50,7 +50,7 @@ print(string.format('Timer(5) waited %f seconds', os.clock() - start))
-- it's worth knowing that a coroutine timer callback can be used to manage
-- more complex control flows.
start = os.clock()
-timers.Timer:new(
+timers.Timer(
2,
coroutine.wrap(function()
for i = 1,5 do
diff --git a/indra/newview/tests/llluamanager_test.cpp b/indra/newview/tests/llluamanager_test.cpp
index cf1bf25b5c..2d525f7913 100644
--- a/indra/newview/tests/llluamanager_test.cpp
+++ b/indra/newview/tests/llluamanager_test.cpp
@@ -398,7 +398,7 @@ namespace tut
LuaState L;
auto future = LLLUAmanager::startScriptLine(L, lua);
- auto replyname{ L.obtainListener()->getReplyName() };
+ auto replyname{ L.obtainListener().getReplyName() };
auto& replypump{ LLEventPumps::instance().obtain(replyname) };
// LuaState::expr() periodically interrupts a running chunk to ensure
// the rest of our coroutines get cycles. Nonetheless, for this test
diff --git a/indra/newview/viewer_manifest.py b/indra/newview/viewer_manifest.py
index 53039fbd99..20dced2341 100755
--- a/indra/newview/viewer_manifest.py
+++ b/indra/newview/viewer_manifest.py
@@ -169,6 +169,8 @@ class ViewerManifest(LLManifest):
with self.prefix(src_dst="scripts/lua"):
self.path("*.lua")
self.path("*.xml")
+ with self.prefix(src_dst='require'):
+ 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
@@ -285,7 +287,7 @@ class ViewerManifest(LLManifest):
# A line that starts with a non-whitespace character is a name; all others describe contributions, so collect the names
names = []
for line in lines :
- if re.match("\S", line) :
+ if re.match(r"\S", line) :
names.append(line.rstrip())
# It's not fair to always put the same people at the head of the list
random.shuffle(names)
diff --git a/scripts/packages-formatter.py b/scripts/packages-formatter.py
index 4449111e46..5d31702e76 100755
--- a/scripts/packages-formatter.py
+++ b/scripts/packages-formatter.py
@@ -42,7 +42,7 @@ _autobuild_env=os.environ.copy()
# Coerce stdout encoding to utf-8 as cygwin's will be detected as cp1252 otherwise.
_autobuild_env["PYTHONIOENCODING"] = "utf-8"
-pkg_line=re.compile('^([\w-]+):\s+(.*)$')
+pkg_line=re.compile(r'^([\w-]+):\s+(.*)$')
def autobuild(*args):
"""