diff options
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): """ |