diff options
Diffstat (limited to 'indra/newview/llluamanager.cpp')
-rw-r--r-- | indra/newview/llluamanager.cpp | 1347 |
1 files changed, 1347 insertions, 0 deletions
diff --git a/indra/newview/llluamanager.cpp b/indra/newview/llluamanager.cpp new file mode 100644 index 0000000000..2244fa32f6 --- /dev/null +++ b/indra/newview/llluamanager.cpp @@ -0,0 +1,1347 @@ +/** + * @file llluamanager.cpp + * @brief classes and functions for interfacing with LUA. + * + * $LicenseInfo:firstyear=2023&license=viewerlgpl$ + * Second Life Viewer Source Code + * Copyright (C) 2023, Linden Research, Inc. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; + * version 2.1 of the License only. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * + * Linden Research, Inc., 945 Battery Street, San Francisco, CA 94111 USA + * $/LicenseInfo$ + * + */ + +#include "llviewerprecompiledheaders.h" +#include "llluamanager.h" + +#include "llerror.h" +#include "lleventcoro.h" +#include "lleventfilter.h" +#include "llevents.h" +#include "llinstancetracker.h" +#include "llleaplistener.h" +#include "lluuid.h" +#include "stringize.h" + +// skip all these link dependencies for integration testing +#ifndef LL_TEST +#include "llagent.h" +#include "llappearancemgr.h" +#include "llcallbacklist.h" +#include "llfloaterreg.h" +#include "llfloaterimnearbychat.h" +#include "llfloatersidepanelcontainer.h" +#include "llnotificationsutil.h" +#include "llvoavatarself.h" +#include "llviewermenu.h" +#include "llviewermenufile.h" +#include "llviewerwindow.h" +#include "lluilistener.h" +#include "llanimationstates.h" +#include "llinventoryfunctions.h" +#include "lltoolplacer.h" +#include "llviewerregion.h" + +// FIXME extremely hacky way to get to the UI Listener framework. There's +// a cleaner way. +extern LLUIListener sUIListener; +#endif // ! LL_TEST + +#include <boost/algorithm/string/replace.hpp> + +extern "C" +{ +#include "lua/lua.h" +#include "lua/lauxlib.h" +#include "lua/lualib.h" +} + +#include <algorithm> +#include <cstdlib> // std::rand() +#include <cstring> // std::memcpy() +#include <map> +#include <memory> // std::unique_ptr +#include <sstream> +#include <string_view> +#include <vector> + +#if LL_WINDOWS +#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); + +/** + * 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), + mListener( + new LLLeapListener( + [L](LLEventPump& pump, const std::string& listener) + { return connect(L, pump, listener); })) + { + mReplyConnection = connect(L, mReplyPump, "LuaListener"); + } + + LuaListener(const LuaListener&) = delete; + LuaListener& operator=(const LuaListener&) = delete; + + ~LuaListener() + { + LL_DEBUGS("Lua") << "~LuaListener('" << mReplyPump.getName() << "')" << LL_ENDL; + } + + 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; + } + + static LLBoundListener connect(lua_State* L, LLEventPump& pump, const std::string& listener) + { + return pump.listen( + listener, + [L, pumpname=pump.getName()](const LLSD& data) + { return call_lua(L, pumpname, data); }); + } + + static bool call_lua(lua_State* L, const std::string& pump, const LLSD& data) + { + LL_INFOS("Lua") << "LuaListener::call_lua('" << pump << "', " << data << ")" << LL_ENDL; + if (! lua_checkstack(L, 3)) + { + LL_WARNS("Lua") << "Cannot extend Lua stack to call listen_events() callback" + << LL_ENDL; + return false; + } + // 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; +#ifndef LL_TEST + LLEventStream mReplyPump{ LLUUID::generateNewID().asString() }; +#else + LLEventLogProxyFor<LLEventStream> mReplyPump{ "luapump", false }; +#endif + 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. + */ +struct LuaPopper +{ + LuaPopper(lua_State* L, int count): + mState(L), + mCount(count) + {} + + LuaPopper(const LuaPopper&) = delete; + LuaPopper& operator=(const LuaPopper&) = delete; + + ~LuaPopper() + { + if (mCount) + { + lua_pop(mState, mCount); + } + } + + void disarm() { set(0); } + void set(int count) { mCount = count; } + + lua_State* mState; + int mCount; +}; + +/** + * LuaFunction is a base class containing a static registry of its static + * subclass call() methods. call() is NOT virtual: instead, each subclass + * constructor passes a pointer to its distinct call() method to the base- + * class constructor, along with a name by which to register that method. + * + * The init() method walks the registry and registers each such name with the + * passed lua_State. + */ +class LuaFunction +{ +public: + LuaFunction(const std::string_view& name, lua_CFunction function) + { + getRegistry().emplace(name, function); + } + + static void init(lua_State* L) + { + for (const auto& pair: getRegistry()) + { + lua_register(L, pair.first.c_str(), pair.second); + } + } + + static lua_CFunction get(const std::string& key) + { + // use find() instead of subscripting to avoid creating an entry for + // unknown key + const auto& registry{ getRegistry() }; + auto found{ registry.find(key) }; + return (found == registry.end())? nullptr : found->second; + } + +private: + using Registry = std::map<std::string, lua_CFunction>; + static Registry& getRegistry() + { + // use a function-local static to ensure it's initialized + static Registry registry; + return registry; + } +}; + +/** + * lua_function(name) is a macro to facilitate defining C++ functions + * available to Lua. It defines a subclass of LuaFunction and declares a + * static instance of that subclass, thereby forcing the compiler to call its + * constructor at module initialization time. The constructor passes the + * stringized instance name to its LuaFunction base-class constructor, along + * with a pointer to the static subclass call() method. It then emits the + * call() method definition header, to be followed by a method body enclosed + * in curly braces as usual. + */ +#define lua_function(name) \ +static struct name##_ : public LuaFunction \ +{ \ + name##_(): LuaFunction(#name, &call) {} \ + static int call(lua_State* L); \ +} name; \ +int name##_::call(lua_State* L) +// { +// ... supply method body here, referencing 'L' ... +// } + +// This function consumes ALL Lua stack arguments and returns concatenated +// message string +std::string lua_print_msg(lua_State* L, const std::string_view& level) +{ + // On top of existing Lua arguments, push 'where' info + luaL_checkstack(L, 1, nullptr); + luaL_where(L, 1); + // start with the 'where' info at the top of the stack + std::ostringstream out; + out << lua_tostring(L, -1); + lua_pop(L, 1); + const char* sep = ""; // 'where' info ends with ": " + // now iterate over arbitrary args, calling Lua tostring() on each and + // concatenating with separators + for (int p = 1; p <= lua_gettop(L); ++p) + { + out << sep; + sep = " "; + // push Lua tostring() function -- note, semantically different from + // lua_tostring()! + lua_getglobal(L, "tostring"); + // Now the stack is arguments 1 .. N, plus tostring(). + // Rotate downwards, producing stack args 2 .. N, tostring(), arg1. + lua_rotate(L, 1, -1); + // pop tostring() and arg1, pushing tostring(arg1) + // (ignore potential error code from lua_pcall() because, if there was + // an error, we expect the stack top to be an error message -- which + // we'll print) + lua_pcall(L, 1, 1, 0); + // stack now holds args 2 .. N, tostring(arg1) + out << lua_tostring(L, -1); + } + // pop everything + lua_settop(L, 0); + // capture message string + std::string msg{ out.str() }; + // put message out there for any interested party (*koff* LLFloaterLUADebug *koff*) + LLEventPumps::instance().obtain("lua output").post(stringize(level, ": ", msg)); + return msg; +} + +lua_function(print_debug) +{ + LL_DEBUGS("Lua") << lua_print_msg(L, "DEBUG") << LL_ENDL; + return 0; +} + +// also used for print(); see LuaState constructor +lua_function(print_info) +{ + LL_INFOS("Lua") << lua_print_msg(L, "INFO") << LL_ENDL; + return 0; +} + +lua_function(print_warning) +{ + LL_WARNS("Lua") << lua_print_msg(L, "WARN") << LL_ENDL; + return 0; +} + +#ifndef LL_TEST +lua_function(avatar_sit) +{ + gAgent.sitDown(); + return 0; +} + +lua_function(avatar_stand) +{ + gAgent.standUp(); + return 0; +} + +lua_function(nearby_chat_send) +{ + std::string msg(lua_tostring(L, 1)); + LLFloaterIMNearbyChat *nearby_chat = LLFloaterReg::findTypedInstance<LLFloaterIMNearbyChat>("nearby_chat"); + nearby_chat->sendChatFromViewer(msg, CHAT_TYPE_NORMAL, gSavedSettings.getBOOL("PlayChatAnim")); + + lua_pop(L, 1); + return 0; +} + +lua_function(wear_by_name) +{ + std::string folder_name(lua_tostring(L, 1)); + LLAppearanceMgr::instance().wearOutfitByName(folder_name); + + lua_pop(L, 1); + return 0; +} + +lua_function(open_floater) +{ + std::string floater_name(lua_tostring(L, 1)); + + LLSD key; + if (floater_name == "profile") + { + key["id"] = gAgentID; + } + LLFloaterReg::showInstance(floater_name, key); + + lua_pop(L, 1); + return 0; +} + +lua_function(close_floater) +{ + std::string floater_name(lua_tostring(L, 1)); + + LLSD key; + if (floater_name == "profile") + { + key["id"] = gAgentID; + } + LLFloaterReg::hideInstance(floater_name, key); + + lua_pop(L, 1); + return 0; +} + +lua_function(close_all_floaters) +{ + close_all_windows(); + return 0; +} + +lua_function(click_child) +{ + std::string parent_name(lua_tostring(L, 1)); + std::string child_name(lua_tostring(L, 2)); + + LLFloater *floater = LLFloaterReg::findInstance(parent_name); + LLUICtrl *child = floater->getChild<LLUICtrl>(child_name, true); + child->onCommit(); + + lua_pop(L, 2); + return 0; +} + +lua_function(snapshot_to_file) +{ + std::string filename(lua_tostring(L, 1)); + + //don't take snapshot from coroutine + doOnIdleOneTime([filename]() + { + gViewerWindow->saveSnapshot(filename, + gViewerWindow->getWindowWidthRaw(), + gViewerWindow->getWindowHeightRaw(), + gSavedSettings.getBOOL("RenderUIInSnapshot"), + gSavedSettings.getBOOL("RenderHUDInSnapshot"), + FALSE, + LLSnapshotModel::SNAPSHOT_TYPE_COLOR, + LLSnapshotModel::SNAPSHOT_FORMAT_PNG); + }); + + lua_pop(L, 1); + return 0; +} + +lua_function(open_wearing_tab) +{ + LLFloaterSidePanelContainer::showPanel("appearance", LLSD().with("type", "now_wearing")); + return 0; +} + +lua_function(set_debug_setting_bool) +{ + std::string setting_name(lua_tostring(L, 1)); + bool value(lua_toboolean(L, 2)); + + gSavedSettings.setBOOL(setting_name, value); + lua_pop(L, 2); + return 0; +} + +lua_function(get_avatar_name) +{ + std::string name = gAgentAvatarp->getFullname(); + luaL_checkstack(L, 1, nullptr); + lua_pushstdstring(L, name); + return 1; +} + +lua_function(is_avatar_flying) +{ + luaL_checkstack(L, 1, nullptr); + lua_pushboolean(L, gAgent.getFlying()); + return 1; +} + +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; + if (lua_gettop(L) > 1) + { + req = (EAnimRequest) (int) lua_tonumber(L, 2); + } + + LLInventoryModel::cat_array_t cat_array; + LLInventoryModel::item_array_t item_array; + LLNameItemCollector has_name(anim_name); + gInventory.collectDescendentsIf(gInventory.getRootFolderID(), + cat_array, + item_array, + LLInventoryModel::EXCLUDE_TRASH, + has_name); + for (auto& item: item_array) + { + if (item->getType() == LLAssetType::AT_ANIMATION) + { + LLUUID anim_id = item->getAssetUUID(); + LL_INFOS() << "Playing animation " << anim_id << LL_ENDL; + gAgent.sendAnimationRequest(anim_id, req); + return 0; + } + } + LL_WARNS() << "No animation found for name " << anim_name << LL_ENDL; + + return 0; +} + +lua_function(env_setting_event) +{ + handle_env_setting_event(lua_tostring(L, 1)); + lua_pop(L, 1); + return 0; +} + +void handle_notification_dialog(const LLSD ¬ification, const LLSD &response, lua_State *L, std::string response_cb) +{ + if (!response_cb.empty()) + { + S32 option = LLNotificationsUtil::getSelectedOption(notification, response); + + luaL_checkstack(L, 1, nullptr); + lua_pushinteger(L, option); + lua_setglobal(L, response_cb.c_str()); + } +} + +lua_function(show_notification) +{ + std::string notification(lua_tostring(L, 1)); + + if (lua_type(L, 2) == LUA_TTABLE) + { + LLSD args = lua_tollsd(L, 2); + + std::string response_cb; + if (lua_type(L, 3) == LUA_TSTRING) + { + response_cb = lua_tostring(L, 3); + } + + LLNotificationsUtil::add(notification, args, LLSD(), boost::bind(handle_notification_dialog, _1, _2, L, response_cb)); + } + else if (lua_type(L, 2) == LUA_TSTRING) + { + std::string response_cb = lua_tostring(L, 2); + LLNotificationsUtil::add(notification, LLSD(), LLSD(), boost::bind(handle_notification_dialog, _1, _2, L, response_cb)); + } + else + { + LLNotificationsUtil::add(notification); + } + + lua_settop(L, 0); + return 0; +} + +lua_function(add_menu_item) +{ + std::string menu(lua_tostring(L, 1)); + if (lua_type(L, 2) == LUA_TTABLE) + { + LLSD args = lua_tollsd(L, 2); + + LLMenuItemCallGL::Params item_params; + item_params.name = args["name"]; + item_params.label = args["label"]; + + LLUICtrl::CommitCallbackParam item_func; + item_func.function_name = args["function"]; + if (args.has("parameter")) + { + item_func.parameter = args["parameter"]; + } + item_params.on_click = item_func; + + LLMenuItemCallGL *menu_item = LLUICtrlFactory::create<LLMenuItemCallGL>(item_params); + gMenuBarView->findChildMenuByName(menu, true)->append(menu_item); + } + + lua_settop(L, 0); + return 0; +} + +lua_function(add_menu_separator) +{ + std::string menu(lua_tostring(L, 1)); + gMenuBarView->findChildMenuByName(menu, true)->addSeparator(); + + lua_pop(L, 1); + return 0; +} + +lua_function(add_menu) +{ + if (lua_type(L, 1) == LUA_TTABLE) + { + LLSD args = lua_tollsd(L, 1); + + LLMenuGL::Params item_params; + item_params.name = args["name"]; + item_params.label = args["label"]; + item_params.can_tear_off = args["tear_off"]; + + LLMenuGL *menu = LLUICtrlFactory::create<LLMenuGL>(item_params); + gMenuBarView->appendMenu(menu); + } + + lua_settop(L, 0); + return 0; +} + +lua_function(add_branch) +{ + std::string menu(lua_tostring(L, 1)); + if (lua_type(L, 2) == LUA_TTABLE) + { + LLSD args = lua_tollsd(L, 2); + + LLMenuGL::Params item_params; + item_params.name = args["name"]; + item_params.label = args["label"]; + item_params.can_tear_off = args["tear_off"]; + + LLMenuGL *branch = LLUICtrlFactory::create<LLMenuGL>(item_params); + gMenuBarView->findChildMenuByName(menu, true)->appendMenu(branch); + } + + lua_settop(L, 0); + return 0; +} + +// rez_prim({x, y}, prim_type) +// avatar is the reference point +lua_function(rez_prim) +{ + lua_rawgeti(L, 1, 1); + F32 x = lua_tonumber(L, -1); + lua_pop(L, 1); + lua_rawgeti(L, 1, 2); + F32 y = lua_tonumber(L, -1); + lua_pop(L, 1); + + S32 type(lua_tonumber(L, 2)); // primitive shapes 1-8 + + LLVector3 obj_pos = gAgent.getPositionAgent() + LLVector3(x, y, -0.5); + bool res = LLToolPlacer::rezNewObject(type, NULL, 0, TRUE, gAgent.getPositionAgent(), obj_pos, gAgent.getRegion(), 0); + + LL_INFOS() << "Rezing a prim: type " << LLPrimitive::pCodeToString(type) << ", coordinates: " << obj_pos << " Success: " << res << LL_ENDL; + + lua_settop(L, 0); + return 0; +} + + +void move_to_dest(const LLVector3d &target_global, lua_State *L, std::string response_cb) +{ + struct Data + { + lua_State *L; + std::string response_cb; + }; + + Data *data = new Data(); + data->L = L; + if (!response_cb.empty()) + { + data->response_cb = response_cb; + } + + auto handle_dest_reached = [](BOOL success, void *user_data) + { + Data *cb_data = static_cast<Data *>(user_data); + if (!cb_data->response_cb.empty()) + { + S32 result = success ? 1 : -1; + lua_pushinteger(cb_data->L, result); + lua_setglobal(cb_data->L, cb_data->response_cb.c_str()); + } + }; + + gAgent.startAutoPilotGlobal(target_global, std::string(), NULL, handle_dest_reached, data, 0.f, 0.03f, FALSE); +} + +// move_by({x,y}, "lua_cb_func") +// avatar is the reference point +lua_function(move_by) +{ + lua_rawgeti(L, 1, 1); + F32 x = lua_tonumber(L, -1); + lua_pop(L, 1); + lua_rawgeti(L, 1, 2); + F32 y = lua_tonumber(L, -1); + lua_pop(L, 1); + + LLVector3d dest = gAgent.getRegion()->getPosGlobalFromRegion(gAgent.getPositionAgent() + LLVector3(x, y, 0)); + + std::string response_cb; + if (lua_type(L, 2) == LUA_TSTRING) + { + response_cb = lua_tostring(L, 2); + } + move_to_dest(dest, L, response_cb); + + lua_settop(L, 0); + return 0; +} + +// move_to({x,y,z}, "lua_cb_func") +// region coordinates are used +lua_function(move_to) +{ + lua_rawgeti(L, 1, 1); + F32 x = lua_tonumber(L, -1); + lua_pop(L, 1); + lua_rawgeti(L, 1, 2); + F32 y = lua_tonumber(L, -1); + lua_rawgeti(L, 1, 3); + F32 z = lua_tonumber(L, -1); + lua_pop(L, 1); + + LLVector3d dest = gAgent.getRegion()->getPosGlobalFromRegion(LLVector3(x, y, z)); + + std::string response_cb; + if (lua_type(L, 2) == LUA_TSTRING) + { + response_cb = lua_tostring(L, 2); + } + move_to_dest(dest, L, response_cb); + + lua_settop(L, 0); + return 0; +} + +lua_function(run_ui_command) +{ + int top = lua_gettop(L); + std::string func_name; + if (top >= 1) + { + func_name = lua_tostring(L,1); + } + std::string parameter; + if (top >= 2) + { + parameter = lua_tostring(L,2); + } + LL_WARNS("LUA") << "running ui func " << func_name << " parameter " << parameter << LL_ENDL; + LLSD event; + event["function"] = func_name; + if (!parameter.empty()) + { + event["parameter"] = parameter; + } + sUIListener.call(event); + + lua_settop(L, 0); + return 0; +} +#endif // ! LL_TEST + +lua_function(post_on) +{ + std::string pumpname{ lua_tostdstring(L, 1) }; + LLSD data{ lua_tollsd(L, 2) }; + lua_pop(L, 2); + LL_INFOS("Lua") << "post_on('" << pumpname << "', " << data << ")" << LL_ENDL; + LLEventPumps::instance().obtain(pumpname).post(data); + return 0; +} + +lua_function(listen_events) +{ + if (! lua_isfunction(L, 1)) + { + return luaL_typeerror(L, 1, "function"); + } + luaL_checkstack(L, 2, nullptr); + + // 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_always(regtype == LUA_TTHREAD); + lua_State* mainthread{ lua_tothread(L, -1) }; + // pop the main thread + lua_pop(L, 1); + + luaL_checkstack(mainthread, 1, nullptr); + 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_TNUMBER); + if (keytype == LUA_TNUMBER) + { + // We do already have a LuaListener. Retrieve it. + int isint; + listener = LuaListener::getInstance(lua_tointegerx(mainthread, -1, &isint)); + // pop the int "event.listener" key + lua_pop(mainthread, 1); + // Nobody should have destroyed this LuaListener instance! + llassert(isint && listener); + } + else + { + // pop the nil "event.listener" key + lua_pop(mainthread, 1); + // instantiate a new LuaListener, binding the mainthread state -- but + // use a no-op deleter: we do NOT want to delete this new LuaListener + // on return from listen_events()! + listener.reset(new LuaListener(mainthread), [](LuaListener*){}); + // 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->getReplyName()); + lua_pushstdstring(L, listener->getCommandName()); + return 2; +} + +lua_function(await_event) +{ + // await_event(pumpname [, timeout [, value to return if timeout (default nil)]]) + auto pumpname{ lua_tostdstring(L, 1) }; + LLSD result; + if (lua_gettop(L) > 1) + { + auto timeout{ lua_tonumber(L, 2) }; + // with no 3rd argument, should be LLSD() + auto dftval{ lua_tollsd(L, 3) }; + lua_settop(L, 0); + result = llcoro::suspendUntilEventOnWithTimeout(pumpname, timeout, dftval); + } + else + { + // no timeout + lua_pop(L, 1); + result = llcoro::suspendUntilEventOn(pumpname); + } + lua_pushllsd(L, result); + return 1; +} + +/** + * RAII class to manage the lifespan of a lua_State + */ +class LuaState +{ +public: + LuaState(const std::string_view& desc, LLLUAmanager::script_finished_fn cb): + mDesc(desc), + mCallback(cb), + mState(luaL_newstate()) + { + luaL_openlibs(mState); + LuaFunction::init(mState); + // Try to make print() write to our log. + lua_register(mState, "print", LuaFunction::get("print_info")); + } + + LuaState(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_TNUMBER) + { + // We do have a LuaListener. Retrieve it. + int isint; + auto listener{ LuaListener::getInstance(lua_tointegerx(mState, -1, &isint)) }; + // pop the int "event.listener" key + lua_pop(mState, 1); + // if we got a LuaListener instance, destroy it + // (if (! isint), lua_tointegerx() returned 0, but key 0 might + // validly designate someone ELSE's LuaListener) + if (isint && 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; + LLLUAmanager::script_finished_fn mCallback; + lua_State* mState; + std::string mError; +}; + +void LLLUAmanager::runScriptFile(const std::string& filename, script_finished_fn cb) +{ + std::string desc{ stringize("runScriptFile('", filename, "')") }; + LLCoros::instance().launch(desc, [desc, filename, cb]() + { + LuaState L(desc, 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); + + if (L.checkLua(luaL_dofile(L, filename.c_str()))) + { + lua_getglobal(L, "call_once_func"); + if (lua_isfunction(L, -1)) + { + // call call_once_func(), setting internal error message if + // error + L.checkLua(lua_pcall(L, 0, 0, 0)); + } + } + }); +} + +void LLLUAmanager::runScriptLine(const std::string& cmd, script_finished_fn cb) +{ + // 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 = stringize(shortcmd.substr(0, shortlen), "..."); + + std::string desc{ stringize("runScriptLine('", shortcmd, "')") }; + LLCoros::instance().launch(desc, [desc, cmd, cb]() + { + LuaState L(desc, cb); + L.checkLua(luaL_dostring(L, cmd.c_str())); + }); +} + +void LLLUAmanager::runScriptOnLogin() +{ +#ifndef LL_TEST + std::string filename = gSavedSettings.getString("AutorunLuaScriptName"); + if (filename.empty()) + { + LL_INFOS() << "Script name wasn't set." << LL_ENDL; + return; + } + + filename = gDirUtilp->getExpandedFilename(LL_PATH_USER_SETTINGS, filename); + if (!gDirUtilp->fileExists(filename)) + { + LL_INFOS() << filename << " was not found." << LL_ENDL; + return; + } + + runScriptFile(filename); +#endif // ! LL_TEST +} + +std::string lua_tostdstring(lua_State* L, int index) +{ + size_t len; + const char* strval{ lua_tolstring(L, index, &len) }; + return { strval, len }; +} + +void lua_pushstdstring(lua_State* L, const std::string& str) +{ + luaL_checkstack(L, 1, nullptr); + lua_pushlstring(L, str.c_str(), str.length()); +} + +// By analogy with existing lua_tomumble() functions, return an LLSD object +// corresponding to the Lua object at stack index 'index' in state L. +// This function assumes that a Lua caller is fully aware that they're trying +// to call a viewer function. In other words, the caller must specifically +// construct Lua data convertible to LLSD. +// +// For proper error handling, we REQUIRE that the Lua runtime be compiled as +// C++ so errors are raised as C++ exceptions rather than as longjmp() calls: +// http://www.lua.org/manual/5.4/manual.html#4.4 +// "Internally, Lua uses the C longjmp facility to handle errors. (Lua will +// use exceptions if you compile it as C++; search for LUAI_THROW in the +// source code for details.)" +// Some blocks within this function construct temporary C++ objects in the +// expectation that these objects will be properly destroyed even if code +// reached by that block raises a Lua error. +LLSD lua_tollsd(lua_State* L, int index) +{ + switch (lua_type(L, index)) + { + case LUA_TNONE: + // Should LUA_TNONE be an error instead of returning isUndefined()? + case LUA_TNIL: + return {}; + + case LUA_TBOOLEAN: + return bool(lua_toboolean(L, index)); + + case LUA_TNUMBER: + { + // check if integer truncation leaves the number intact + int isint; + lua_Integer intval{ lua_tointegerx(L, index, &isint) }; + if (isint) + { + return LLSD::Integer(intval); + } + else + { + return lua_tonumber(L, index); + } + } + + case LUA_TSTRING: + return lua_tostdstring(L, index); + + case LUA_TUSERDATA: + { + LLSD::Binary binary(lua_rawlen(L, index)); + std::memcpy(binary.data(), lua_touserdata(L, index), binary.size()); + return binary; + } + + case LUA_TTABLE: + { + // A Lua table correctly constructed to convert to LLSD will have + // either consecutive integer keys starting at 1, which we represent + // as an LLSD array (with Lua key 1 at C++ index 0), or will have + // all string keys. + // + // In the belief that Lua table traversal skips "holes," that is, it + // doesn't report any key/value pair whose value is nil, we allow a + // table with integer keys >= 1 but with "holes." This produces an + // LLSD array with isUndefined() entries at unspecified keys. There + // would be no other way for a Lua caller to construct an + // isUndefined() LLSD array entry. However, to guard against crazy int + // keys, we forbid gaps larger than a certain size: crazy int keys + // could result in a crazy large contiguous LLSD array. + // + // Possible looseness could include: + // - A mix of integer and string keys could produce an LLSD map in + // which the integer keys are converted to string. (Key conversion + // must be performed in C++, not Lua, to avoid confusing + // lua_next().) + // - However, since in Lua t[0] and t["0"] are distinct table entries, + // do not consider converting numeric string keys to int to return + // an LLSD array. + // But until we get more experience with actual Lua scripts in + // practice, let's say that any deviation is a Lua coding error. + // An important property of the strict definition above is that most + // conforming data blobs can make a round trip across the language + // boundary and still compare equal. A non-conforming data blob would + // lose that property. + // Known exceptions to round trip identity: + // - Empty LLSD map and empty LLSD array convert to empty Lua table. + // But empty Lua table converts to isUndefined() LLSD object. + // - LLSD::Real with integer value returns as LLSD::Integer. + // - LLSD::UUID, LLSD::Date and LLSD::URI all convert to Lua string, + // and so return as LLSD::String. + + // This is the most important of the luaL_checkstack() calls because a + // deeply nested Lua structure will enter this case at each level, and + // we'll need another 2 stack slots to traverse each nested table. + luaL_checkstack(L, 2, nullptr); + lua_pushnil(L); // first key + if (! lua_next(L, index)) + { + // it's a table, but the table is empty -- no idea if it should be + // modeled as empty array or empty map -- return isUndefined(), + // which can be consumed as either + return {}; + } + // key is at stack index -2, value at index -1 + // from here until lua_next() returns 0, have to lua_pop(2) if we + // return early + LuaPopper popper(L, 2); + // Remember the type of the first key + auto firstkeytype{ lua_type(L, -2) }; + switch (firstkeytype) + { + case LUA_TNUMBER: + { + // First Lua key is a number: try to convert table to LLSD array. + // This is tricky because we don't know in advance the size of the + // array. The Lua reference manual says that lua_rawlen() is the + // same as the length operator '#'; but the length operator states + // that it might stop at any "hole" in the subject table. + // Moreover, the Lua next() function (and presumably lua_next()) + // traverses a table in unspecified order, even for numeric keys + // (emphasized in the doc). + // Make a preliminary pass over the whole table to validate and to + // collect keys. + std::vector<LLSD::Integer> keys; + // Try to determine the length of the table. If the length + // operator is truthful, avoid allocations while we grow the keys + // vector. Even if it's not, we can still grow the vector, albeit + // a little less efficiently. + keys.reserve(luaL_len(L, index)); + do + { + auto arraykeytype{ lua_type(L, -2) }; + switch (arraykeytype) + { + case LUA_TNUMBER: + { + int isint; + lua_Integer intkey{ lua_tointegerx(L, -2, &isint) }; + if (! isint) + { + // key isn't an integer - this doesn't fit our LLSD + // array constraints + return luaL_error(L, "Expected integer array key, got %f instead", + lua_tonumber(L, -2)); + } + if (intkey < 1) + { + return luaL_error(L, "array key %d out of bounds", int(intkey)); + } + + keys.push_back(LLSD::Integer(intkey)); + break; + } + + case LUA_TSTRING: + // break out strings specially to report the value + return luaL_error(L, "Cannot convert string array key '%s' to LLSD", + lua_tostring(L, -2)); + + default: + return luaL_error(L, "Cannot convert %s array key to LLSD", + lua_typename(L, arraykeytype)); + } + + // remove value, keep key for next iteration + lua_pop(L, 1); + } while (lua_next(L, index) != 0); + popper.disarm(); + // Table keys are all integers: are they reasonable integers? + // Arbitrary max: may bite us, but more likely to protect us + size_t array_max{ 10000 }; + if (keys.size() > array_max) + { + return luaL_error(L, "Conversion from Lua to LLSD array limited to %d entries", + int(array_max)); + } + // We know the smallest key is >= 1. Check the largest. We also + // know the vector is NOT empty, else we wouldn't have gotten here. + std::sort(keys.begin(), keys.end()); + LLSD::Integer highkey = *keys.rbegin(); + if ((highkey - LLSD::Integer(keys.size())) > 100) + { + // Looks like we've gone beyond intentional array gaps into + // crazy key territory. + return luaL_error(L, "Gaps in Lua table too large for conversion to LLSD array"); + } + // right away expand the result array to the size we'll need + LLSD result{ LLSD::emptyArray() }; + result[highkey - 1] = LLSD(); + // Traverse the table again, and this time populate result array. + lua_pushnil(L); // first key + while (lua_next(L, index)) + { + // key at stack index -2, value at index -1 + // We've already validated lua_tointegerx() for each key. + // Don't forget to subtract 1 from Lua key for LLSD subscript! + result[LLSD::Integer(lua_tointeger(L, -2)) - 1] = lua_tollsd(L, -1); + // remove value, keep key for next iteration + lua_pop(L, 1); + } + return result; + } + + case LUA_TSTRING: + { + // First Lua key is a string: try to convert table to LLSD map + LLSD result{ LLSD::emptyMap() }; + do + { + auto mapkeytype{ lua_type(L, -2) }; + if (mapkeytype != LUA_TSTRING) + { + return luaL_error(L, "Cannot convert %s map key to LLSD", + lua_typename(L, mapkeytype)); + } + + result[lua_tostdstring(L, -2)] = lua_tollsd(L, -1); + // remove value, keep key for next iteration + lua_pop(L, 1); + } while (lua_next(L, index) != 0); + popper.disarm(); + return result; + } + + default: + // First Lua key isn't number or string: sorry + return luaL_error(L, "Cannot convert %s table key to LLSD", + lua_typename(L, firstkeytype)); + } + } + + default: + // Other Lua entities (e.g. function, C function, light userdata, + // thread, userdata) are not convertible to LLSD, indicating a coding + // error in the caller. + return luaL_error(L, "Cannot convert type %s to LLSD", luaL_typename(L, index)); + } +} + +// By analogy with existing lua_pushmumble() functions, push onto state L's +// stack a Lua object corresponding to the passed LLSD object. +void lua_pushllsd(lua_State* L, const LLSD& data) +{ + // might need 2 slots for array or map + luaL_checkstack(L, 2, nullptr); + switch (data.type()) + { + case LLSD::TypeUndefined: + lua_pushnil(L); + break; + + case LLSD::TypeBoolean: + lua_pushboolean(L, data.asBoolean()); + break; + + case LLSD::TypeInteger: + lua_pushinteger(L, data.asInteger()); + break; + + case LLSD::TypeReal: + lua_pushnumber(L, data.asReal()); + break; + + case LLSD::TypeBinary: + { + auto binary{ data.asBinary() }; + std::memcpy(lua_newuserdata(L, binary.size()), + binary.data(), binary.size()); + break; + } + + case LLSD::TypeMap: + { + // push a new table with space for our non-array keys + lua_createtable(L, 0, data.size()); + for (const auto& pair: llsd::inMap(data)) + { + // push value -- so now table is at -2, value at -1 + lua_pushllsd(L, pair.second); + // pop value, assign to table[key] + lua_setfield(L, -2, pair.first.c_str()); + } + break; + } + + case LLSD::TypeArray: + { + // push a new table with space for array entries + lua_createtable(L, data.size(), 0); + lua_Integer key{ 0 }; + for (const auto& item: llsd::inArray(data)) + { + // push new array value: table at -2, value at -1 + lua_pushllsd(L, item); + // pop value, assign table[key] = value + lua_seti(L, -2, ++key); + } + break; + } + + case LLSD::TypeString: + case LLSD::TypeUUID: + case LLSD::TypeDate: + case LLSD::TypeURI: + default: + { + lua_pushstdstring(L, data.asString()); + break; + } + } +} |