diff options
author | Nat Goodspeed <nat@lindenlab.com> | 2023-09-27 12:41:45 -0400 |
---|---|---|
committer | Nat Goodspeed <nat@lindenlab.com> | 2023-09-27 12:41:45 -0400 |
commit | 354585d6ad1e7cf5caaa6a35740966e238d14989 (patch) | |
tree | a0e53e1da3b6e518e05f9d2c9c36d3e0d42ea04d /indra/newview/llluamanager.cpp | |
parent | fa67efced89de1b7b432fca5a8d14f03b03ad88b (diff) |
DRTVWR-589: Add Lua-callable listen_events() function.
Add LuaListener, based on LLLeap. LuaListener has an int key so the second and
subsequent calls to listen_events() can find a previously-created one.
LuaListener listens on its LLEventPump and arranges to call the specified Lua
callback with any incoming event. It also instantiates an LLLeapListener.
listen_events() locates the main thread for its state: we only want to call
callbacks on the Lua chunk's main thread, not on a (possibly suspended)
coroutine. It finds or creates a LuaListener and stashes it in the main
thread's registry, along with the passed Lua callback function. Finally it
returns the names of the LuaListener's reply pump and the LLLeapListener's
command pump.
Add LuaState RAII class to manage the lifespan of each lua_State we create.
This encapsulates much of the boilerplate common to runScriptFile() and
runScriptLine(). In addition, LuaState's destructor checks for a LuaListener
key and, if found, destroys the referenced LuaListener.
LuaState's constructor requires a description to clarify log messages.
Move the checkLua() free function to a member of LuaState. This allows
capturing an error message to pass to the C++ completion callback, if any.
Use LuaState in runScriptFile() and runScriptLine(), synthesizing a suitable
description in each case.
Add print_debug() and print_info() logging calls, analogous to
print_warning(). Add luaL_where() prefix to every such message.
Add lua_pushstdstring(), like lua_tostdstring(): convenience for working with
the pointer and length used by lua_pushlstring() and lua_tolstring().
Clean up return values of lua_functions. A lua_CFunction returns the number of
return values it has pushed, so any 'void' lua_CFunction should pop its
arguments and return 0.
Diffstat (limited to 'indra/newview/llluamanager.cpp')
-rw-r--r-- | indra/newview/llluamanager.cpp | 372 |
1 files changed, 304 insertions, 68 deletions
diff --git a/indra/newview/llluamanager.cpp b/indra/newview/llluamanager.cpp index c783b1fab6..7838e57216 100644 --- a/indra/newview/llluamanager.cpp +++ b/indra/newview/llluamanager.cpp @@ -31,11 +31,15 @@ #include "llagent.h" #include "llappearancemgr.h" #include "llcallbacklist.h" +#include "llerror.h" #include "llevents.h" #include "llfloaterreg.h" #include "llfloaterimnearbychat.h" #include "llfloatersidepanelcontainer.h" +#include "llinstancetracker.h" +#include "llleaplistener.h" #include "llnotificationsutil.h" +#include "lluuid.h" #include "llvoavatarself.h" #include "llviewermenu.h" #include "llviewermenufile.h" @@ -43,6 +47,7 @@ #include "lluilistener.h" #include "llanimationstates.h" #include "llinventoryfunctions.h" +#include "stringize.h" #include <boost/algorithm/string/replace.hpp> @@ -54,8 +59,10 @@ extern "C" } #include <algorithm> +#include <cstdlib> // std::rand() #include <cstring> // std::memcpy() #include <map> +#include <memory> // std::unique_ptr #include <string_view> #include <vector> @@ -63,6 +70,8 @@ extern "C" #pragma comment(lib, "liblua54.a") #endif +std::string lua_tostdstring(lua_State* L, int index); +void lua_pushstdstring(lua_State* L, const std::string& str); LLSD lua_tollsd(lua_State* L, int index); void lua_pushllsd(lua_State* L, const LLSD& data); @@ -70,6 +79,98 @@ void lua_pushllsd(lua_State* L, const LLSD& data); extern LLUIListener sUIListener; /** + * 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 + * - "event.function": the Lua function to be called with incoming events + * The original thought was that LuaListener would itself store the Lua + * function -- but surprisingly, there is no C/C++ type in the API that stores + * a Lua function. + * + * (We considered storing in "event.listener" the LuaListener pointer itself + * as a light userdata, but the problem would be if Lua code overwrote that. + * We want to prevent any Lua script from crashing the viewer, intentionally + * or otherwise. Safer to use a key lookup.) + * + * Like LLLeap, each LuaListener instance also has an associated + * LLLeapListener to respond to LLEventPump management commands. + */ +class LuaListener: public LLInstanceTracker<LuaListener, int> +{ + using super = LLInstanceTracker<LuaListener, int>; +public: + LuaListener(lua_State* L): + super(getUniqueKey()), + mState(L), + mReplyPump(LLUUID::generateNewID().asString()), + mListener(new LLLeapListener(std::bind(&LuaListener::connect, this, _1, _2))) + { + mReplyConnection = connect(mReplyPump, "LuaListener"); + } + + std::string getReplyName() const { return mReplyPump.getName(); } + std::string getCommandName() const { return mListener->getPumpName(); } + +private: + static int 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; + } + + LLBoundListener connect(LLEventPump& pump, const std::string_view& listener) + { + return pump.listen(listener, + std::bind(&LuaListener::call_lua, mState, pump.getName(), _1)); + } + + static bool call_lua(lua_State* L, const std::string& pump, const LLSD& data) + { + // push the registered Lua callback function stored in our registry as + // "event.function" + lua_getfield(L, LUA_REGISTRYINDEX, "event.function"); + llassert(lua_isfunction(L, -1)); + // pass pump name + lua_pushstdstring(L, pump); + // then the data blob + lua_pushllsd(L, data); + // call the registered Lua listener function; allow it to return bool; + // no message handler + auto status = lua_pcall(L, 2, 1, 0); + bool result{ false }; + if (status != LUA_OK) + { + LL_WARNS("Lua") << "Error in listen_events() callback: " + << lua_tostdstring(L, -1) << LL_ENDL; + } + else + { + result = lua_toboolean(L, -1); + } + // discard either the error message or the bool return value + lua_pop(L, 1); + return result; + } + + lua_State* mState; + LLEventStream mReplyPump; + LLTempBoundListener mReplyConnection; + std::unique_ptr<LLLeapListener> mListener; +}; + +/** * LuaPopper is an RAII struct whose role is to pop some number of entries * from the Lua stack if the calling function exits early. */ @@ -150,37 +251,41 @@ static struct name##_ : public LuaFunction \ static int call(lua_State* L); \ } name; \ int name##_::call(lua_State* L) +// { +// ... supply method body here, referencing 'L' ... +// } -lua_function(print_warning) +lua_function(print_debug) { - std::string msg(lua_tostring(L, 1)); - - LL_WARNS() << msg << LL_ENDL; - return 1; + LL_DEBUGS("Lua") << luaL_where(L, 1) << ": " << lua_tostring(L, 1) << LL_ENDL; + lua_pop(L, 1); + return 0; } -bool checkLua(lua_State *L, int r, std::string &error_msg) +lua_function(print_info) { - if (r != LUA_OK) - { - error_msg = lua_tostring(L, -1); + LL_INFOS("Lua") << luaL_where(L, 1) << ": " << lua_tostring(L, 1) << LL_ENDL; + lua_pop(L, 1); + return 0; +} - LL_WARNS() << error_msg << LL_ENDL; - return false; - } - return true; +lua_function(print_warning) +{ + LL_WARNS("Lua") << luaL_where(L, 1) << ": " << lua_tostring(L, 1) << LL_ENDL; + lua_pop(L, 1); + return 0; } lua_function(avatar_sit) { gAgent.sitDown(); - return 1; + return 0; } lua_function(avatar_stand) { gAgent.standUp(); - return 1; + return 0; } lua_function(nearby_chat_send) @@ -189,7 +294,8 @@ lua_function(nearby_chat_send) LLFloaterIMNearbyChat *nearby_chat = LLFloaterReg::findTypedInstance<LLFloaterIMNearbyChat>("nearby_chat"); nearby_chat->sendChatFromViewer(msg, CHAT_TYPE_NORMAL, gSavedSettings.getBOOL("PlayChatAnim")); - return 1; + lua_pop(L, 1); + return 0; } lua_function(wear_by_name) @@ -197,7 +303,8 @@ lua_function(wear_by_name) std::string folder_name(lua_tostring(L, 1)); LLAppearanceMgr::instance().wearOutfitByName(folder_name); - return 1; + lua_pop(L, 1); + return 0; } lua_function(open_floater) @@ -211,7 +318,8 @@ lua_function(open_floater) } LLFloaterReg::showInstance(floater_name, key); - return 1; + lua_pop(L, 1); + return 0; } lua_function(close_floater) @@ -225,13 +333,14 @@ lua_function(close_floater) } LLFloaterReg::hideInstance(floater_name, key); - return 1; + lua_pop(L, 1); + return 0; } lua_function(close_all_floaters) { close_all_windows(); - return 1; + return 0; } lua_function(click_child) @@ -243,7 +352,8 @@ lua_function(click_child) LLUICtrl *child = floater->getChild<LLUICtrl>(child_name, true); child->onCommit(); - return 1; + lua_pop(L, 2); + return 0; } lua_function(snapshot_to_file) @@ -263,13 +373,14 @@ lua_function(snapshot_to_file) LLSnapshotModel::SNAPSHOT_FORMAT_PNG); }); - return 1; + lua_pop(L, 1); + return 0; } lua_function(open_wearing_tab) { LLFloaterSidePanelContainer::showPanel("appearance", LLSD().with("type", "now_wearing")); - return 1; + return 0; } lua_function(set_debug_setting_bool) @@ -278,7 +389,8 @@ lua_function(set_debug_setting_bool) bool value(lua_toboolean(L, 2)); gSavedSettings.setBOOL(setting_name, value); - return 1; + lua_pop(L, 2); + return 0; } lua_function(get_avatar_name) @@ -296,6 +408,9 @@ lua_function(is_avatar_flying) lua_function(play_animation) { + // on exit, pop all passed arguments, so always return 0 + LuaPopper popper(L, lua_gettop(L)); + std::string anim_name = lua_tostring(L,1); EAnimRequest req = ANIM_REQUEST_START; @@ -319,18 +434,19 @@ lua_function(play_animation) LLUUID anim_id = item->getAssetUUID(); LL_INFOS() << "Playing animation " << anim_id << LL_ENDL; gAgent.sendAnimationRequest(anim_id, req); - return 1; + return 0; } } LL_WARNS() << "No animation found for name " << anim_name << LL_ENDL; - return 1; + return 0; } lua_function(env_setting_event) { handle_env_setting_event(lua_tostring(L, 1)); - return 1; + lua_pop(L, 1); + return 0; } void handle_notification_dialog(const LLSD ¬ification, const LLSD &response, lua_State *L, std::string response_cb) @@ -370,7 +486,8 @@ lua_function(show_notification) LLNotificationsUtil::add(notification); } - return 1; + lua_pop(L, lua_gettop(L)); + return 0; } lua_function(add_menu_item) @@ -396,7 +513,8 @@ lua_function(add_menu_item) gMenuBarView->findChildMenuByName(menu, true)->append(menu_item); } - return 1; + lua_pop(L, lua_gettop(L)); + return 0; } lua_function(add_menu_separator) @@ -404,7 +522,8 @@ lua_function(add_menu_separator) std::string menu(lua_tostring(L, 1)); gMenuBarView->findChildMenuByName(menu, true)->addSeparator(); - return 1; + lua_pop(L, 1); + return 0; } lua_function(add_menu) @@ -422,7 +541,8 @@ lua_function(add_menu) gMenuBarView->appendMenu(menu); } - return 1; + lua_pop(L, lua_gettop(L)); + return 0; } lua_function(add_branch) @@ -441,7 +561,8 @@ lua_function(add_branch) gMenuBarView->findChildMenuByName(menu, true)->appendMenu(branch); } - return 1; + lua_pop(L, lua_gettop(L)); + return 0; } lua_function(run_ui_command) @@ -466,7 +587,8 @@ lua_function(run_ui_command) } sUIListener.call(event); - return 1; + lua_pop(L, top); + return 0; } lua_function(post_on_pump) @@ -475,18 +597,69 @@ lua_function(post_on_pump) LLSD data{ lua_tollsd(L, -1) }; lua_pop(L, 2); LLEventPumps::instance().obtain(pumpname).post(data); - return 1; + return 0; } lua_function(listen_events) { - if (! lua_isfunction(L, -1)) + if (! lua_isfunction(L, 1)) { return luaL_typeerror(L, 1, "function"); } - // return the distinct LLEventPump name so Lua code can post that with a - // request as the reply pump - return 1; + + // Get the lua_State* for the main thread of this state, in case we were + // called from a coroutine thread. We're going to make callbacks into Lua + // code, and we want to do it on the main thread rather than a (possibly + // suspended) coroutine thread. + // Registry table is at pseudo-index LUA_REGISTRYINDEX + // Main thread is at registry key LUA_RIDX_MAINTHREAD + auto regtype{ lua_geti(L, LUA_REGISTRYINDEX, LUA_RIDX_MAINTHREAD) }; + // Not finding the main thread at the documented place isn't a user error, + // it's a Problem + llassert(regtype == LUA_TTHREAD); + lua_State* mainthread{ lua_tothread(L, -1) }; + // pop the main thread + lua_pop(L, 1); + + LuaListener::ptr_t listener; + // Does the main thread already have a LuaListener stored in the registry? + // That is, has this Lua chunk already called listen_events()? + auto keytype{ lua_getfield(mainthread, LUA_REGISTRYINDEX, "event.listener") }; + llassert(keytype == LUA_TNIL || keytype == LUA_TINTEGER); + if (keytype == LUA_TINTEGER) + { + // We do already have a LuaListener. Retrieve it. + listener = LuaListener::getInstance(lua_tointeger(mainthread, -1)); + // pop the int "event.listener" key + lua_pop(mainthread, 1); + // Nobody should have destroyed this LuaListener instance! + llassert(listener); + } + else + { + // pop the nil "event.listener" key + lua_pop(mainthread, 1); + // instantiate a new LuaListener, binding the mainthread state + listener.reset(new LuaListener(mainthread)); + // set its key in the field where we'll look for it later + lua_pushinteger(mainthread, listener->getKey()); + lua_setfield(mainthread, LUA_REGISTRYINDEX, "event.listener"); + } + + // Now that we've found or created our LuaListener, store the passed Lua + // function as the callback. Beware: our caller passed the function on L's + // stack, but we want to store it on the mainthread registry. + if (L != mainthread) + { + // push 1 value (the Lua function) from L's stack to mainthread's + lua_xmove(L, mainthread, 1) + } + lua_setfield(mainthread, LUA_REGISTRYINDEX, "event.function"); + + // return the reply pump name and the command pump name on caller's lua_State + lua_pushstdstring(L, listener->getReplyPump()); + lua_pushstdstring(L, listener->getCommandPump()); + return 2; } void initLUA(lua_State *L) @@ -494,61 +667,119 @@ void initLUA(lua_State *L) LuaFunction::init(L); } -void LLLUAmanager::runScriptFile(const std::string &filename, script_finished_fn cb) +/** + * RAII class to manage the lifespan of a lua_State + */ +class LuaState +{ +public: + LuaState(const std::string_view& desc, script_finished_fn cb): + mDesc(desc), + mCallback(cb), + mState(luaL_newstate()) + { + luaL_openlibs(mState); + initLUA(mState); + } + + LuaState(const LuaState&) = delete; + LuaState& operator=(const LuaState&) = delete; + + ~LuaState() + { + // Did somebody call listen_events() on this LuaState? + // That is, is there a LuaListener key in its registry? + auto keytype{ lua_getfield(mState, LUA_REGISTRYINDEX, "event.listener") }; + if (keytype == LUA_TINTEGER) + { + // We do have a LuaListener. Retrieve it. + auto listener{ LuaListener::getInstance(lua_tointeger(mState, -1)) }; + // pop the int "event.listener" key + lua_pop(mState, 1); + // destroy this LuaListener instance + if (listener) + { + auto lptr{ listener.get() }; + listener.reset(); + delete lptr; + } + } + + lua_close(mState); + + if (mCallback) + { + // mError potentially set by previous checkLua() call(s) + mCallback(mError); + } + } + + bool checkLua(int r) + { + if (r != LUA_OK) + { + mError = lua_tostring(mState, -1); + lua_pop(mState, 1); + + LL_WARNS() << mDesc << ": " << mError << LL_ENDL; + return false; + } + return true; + } + + operator lua_State*() const { return mState; } + +private: + std::string mDesc; + script_finished_fn mCallback; + lua_State* mState; + std::string mError; +}; + +void LLLUAmanager::runScriptFile(const std::string_view &filename, script_finished_fn cb) { LLCoros::instance().launch("LUAScriptFileCoro", [filename, cb]() { - lua_State *L = luaL_newstate(); - luaL_openlibs(L); - initLUA(L); + LuaState L(stringize("runScriptFile('", filename, "')"), cb); auto LUA_sleep_func = [](lua_State *L) { F32 seconds = lua_tonumber(L, -1); + lua_pop(L, 1); llcoro::suspendUntilTimeout(seconds); return 0; }; lua_register(L, "sleep", LUA_sleep_func); - std::string lua_error; - if (checkLua(L, luaL_dofile(L, filename.c_str()), lua_error)) + if (L.checkLua(luaL_dofile(L, filename.c_str()))) { lua_getglobal(L, "call_once_func"); if (lua_isfunction(L, -1)) { - if (checkLua(L, lua_pcall(L, 0, 0, 0), lua_error)) {} + // call call_once_func(), setting internal error message if + // error + L.checkLua(lua_pcall(L, 0, 0, 0)); } } - lua_close(L); - - if (cb) - { - cb(lua_error); - } }); } -void LLLUAmanager::runScriptLine(const std::string &cmd, script_finished_fn cb) +void LLLUAmanager::runScriptLine(const std::string_view &cmd, script_finished_fn cb) { LLCoros::instance().launch("LUAScriptFileCoro", [cmd, cb]() { - lua_State *L = luaL_newstate(); - luaL_openlibs(L); - initLUA(L); - int r = luaL_dostring(L, cmd.c_str()); - - std::string lua_error; - if (r != LUA_OK) - { - lua_error = lua_tostring(L, -1); - } - lua_close(L); - - if (cb) - { - cb(lua_error); - } + // find a suitable abbreviation for the cmd string + std::string_view shortcmd{ cmd }; + const size_t shortlen = 40; + std::string::size_type eol = shortcmd.find_first_of("\r\n"); + if (eol != std::string::npos) + shortcmd = shortcmd.substr(0, eol); + if (shortcmd.length() > shortlen) + shortcmd = shortcmd.substr(0, shortlen) + "..."; + + LuaState L(stringize("runScriptLine('", shortcmd, "')"), cb); + L.checkLua(luaL_dostring(L, cmd.c_str())); }); } @@ -578,6 +809,11 @@ std::string lua_tostdstring(lua_State* L, int index) return { strval, len }; } +void lua_pushstdstring(lua_State* L, const std::string& str) +{ + lua_pushlstring(L, str.c_str(), str.length()); +} + // By analogy with existing lua_tomumble() functions, return an LLSD object // corresponding to the Lua object at stack index 'index' in state L. // This function assumes that a Lua caller is fully aware that they're trying |