diff options
Diffstat (limited to 'indra/newview/llluamanager.cpp')
-rw-r--r-- | indra/newview/llluamanager.cpp | 404 |
1 files changed, 404 insertions, 0 deletions
diff --git a/indra/newview/llluamanager.cpp b/indra/newview/llluamanager.cpp new file mode 100644 index 0000000000..c55c44e671 --- /dev/null +++ b/indra/newview/llluamanager.cpp @@ -0,0 +1,404 @@ +/** + * @file llluamanager.cpp + * @brief classes and functions for interfacing with LUA. + * + * $LicenseInfo:firstyear=2023&license=viewerlgpl$ + * Second Life Viewer Source Code + * Copyright (C) 2023, Linden Research, Inc. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; + * version 2.1 of the License only. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * + * Linden Research, Inc., 945 Battery Street, San Francisco, CA 94111 USA + * $/LicenseInfo$ + * + */ + +#include "llviewerprecompiledheaders.h" +#include "llluamanager.h" + +#include "llcoros.h" +#include "llerror.h" +#include "lleventcoro.h" +#include "lua_function.h" +#include "lualistener.h" +#include "stringize.h" + +// skip all these link dependencies for integration testing +#ifndef LL_TEST +#include "lluilistener.h" +#include "llviewercontrol.h" + +// FIXME extremely hacky way to get to the UI Listener framework. There's +// a cleaner way. +extern LLUIListener sUIListener; +#endif // ! LL_TEST + +#include <boost/algorithm/string/replace.hpp> + +#include "luau/luacode.h" +#include "luau/lua.h" +#include "luau/luaconf.h" +#include "luau/lualib.h" + +#include <sstream> +#include <string_view> +#include <vector> + +lua_function(sleep, "sleep(seconds): pause the running coroutine") +{ + F32 seconds = lua_tonumber(L, -1); + lua_pop(L, 1); + llcoro::suspendUntilTimeout(seconds); + return 0; +}; + +// This function consumes ALL Lua stack arguments and returns concatenated +// message string +std::string lua_print_msg(lua_State* L, const std::string_view& level) +{ + // On top of existing Lua arguments, we're going to push tostring() and + // duplicate each existing stack entry so we can stringize each one. + luaL_checkstack(L, 2, nullptr); + luaL_where(L, 1); + // start with the 'where' info at the top of the stack + std::ostringstream out; + out << lua_tostring(L, -1); + lua_pop(L, 1); + const char* sep = ""; // 'where' info ends with ": " + // now iterate over arbitrary args, calling Lua tostring() on each and + // concatenating with separators + for (int p = 1, top = lua_gettop(L); p <= top; ++p) + { + out << sep; + sep = " "; + // push Lua tostring() function -- note, semantically different from + // lua_tostring()! + lua_getglobal(L, "tostring"); + // Now the stack is arguments 1 .. N, plus tostring(). + // Push a copy of the argument at index p. + lua_pushvalue(L, p); + // pop tostring() and arg-p, pushing tostring(arg-p) + // (ignore potential error code from lua_pcall() because, if there was + // an error, we expect the stack top to be an error message -- which + // we'll print) + lua_pcall(L, 1, 1, 0); + out << lua_tostring(L, -1); + lua_pop(L, 1); + } + // pop everything + lua_settop(L, 0); + // capture message string + std::string msg{ out.str() }; + // put message out there for any interested party (*koff* LLFloaterLUADebug *koff*) + LLEventPumps::instance().obtain("lua output").post(stringize(level, ": ", msg)); + return msg; +} + +lua_function(print_debug, "print_debug(args...): DEBUG level logging") +{ + LL_DEBUGS("Lua") << lua_print_msg(L, "DEBUG") << LL_ENDL; + return 0; +} + +// also used for print(); see LuaState constructor +lua_function(print_info, "print_info(args...): INFO level logging") +{ + LL_INFOS("Lua") << lua_print_msg(L, "INFO") << LL_ENDL; + return 0; +} + +lua_function(print_warning, "print_warning(args...): WARNING level logging") +{ + LL_WARNS("Lua") << lua_print_msg(L, "WARN") << LL_ENDL; + return 0; +} + +#ifndef LL_TEST + +lua_function(run_ui_command, + "run_ui_command(name [, parameter]): " + "call specified UI command with specified parameter") +{ + int top = lua_gettop(L); + std::string func_name; + if (top >= 1) + { + func_name = lua_tostring(L,1); + } + std::string parameter; + if (top >= 2) + { + parameter = lua_tostring(L,2); + } + LL_WARNS("LUA") << "running ui func " << func_name << " parameter " << parameter << LL_ENDL; + LLSD event; + event["function"] = func_name; + if (!parameter.empty()) + { + event["parameter"] = parameter; + } + sUIListener.call(event); + + lua_settop(L, 0); + return 0; +} +#endif // ! LL_TEST + +lua_function(post_on, "post_on(pumpname, data): post specified data to specified LLEventPump") +{ + std::string pumpname{ lua_tostdstring(L, 1) }; + LLSD data{ lua_tollsd(L, 2) }; + lua_pop(L, 2); + LL_INFOS("Lua") << "post_on('" << pumpname << "', " << data << ")" << LL_ENDL; + LLEventPumps::instance().obtain(pumpname).post(data); + return 0; +} + +lua_function(listen_events, + "listen_events(callback): call callback(pumpname, data) with events received\n" + "on this Lua chunk's replypump.\n" + "Returns replypump, commandpump: names of LLEventPumps specific to this chunk.") +{ + if (! lua_isfunction(L, 1)) + { + luaL_typeerror(L, 1, "function"); + return 0; + } + 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_rawgeti(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); +|*==========================================================================*/ + // Luau is based on Lua 5.1, and Lua 5.1 apparently provides no way to get + // back to the main thread from a coroutine thread? + lua_State* mainthread{ L }; + + 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)]]):\n" + "pause the running Lua chunk until the next event on the named LLEventPump") +{ + 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; +} + +void LLLUAmanager::runScriptFile(const std::string& filename, script_finished_fn cb) +{ + // A script_finished_fn is used to initialize the LuaState. + // It will be called when the LuaState is destroyed. + LuaState L(cb); + runScriptFile(L, filename); +} + +void LLLUAmanager::runScriptFile(const std::string& filename, script_result_fn cb) +{ + LuaState L; + // A script_result_fn will be called when LuaState::expr() completes. + runScriptFile(L, filename, cb); +} + +LLCoros::Future<std::pair<int, LLSD>> +LLLUAmanager::startScriptFile(LuaState& L, const std::string& filename) +{ + // Despite returning from startScriptFile(), we need this Promise to + // remain alive until the callback has fired. + auto promise{ std::make_shared<LLCoros::Promise<std::pair<int, LLSD>>>() }; + runScriptFile(L, filename, + [promise](int count, LLSD result) + { promise->set_value({ count, result }); }); + return LLCoros::getFuture(*promise); +} + +std::pair<int, LLSD> LLLUAmanager::waitScriptFile(LuaState& L, const std::string& filename) +{ + return startScriptFile(L, filename).get(); +} + +void LLLUAmanager::runScriptFile(LuaState& L, const std::string& filename, script_result_fn cb) +{ + std::string desc{ stringize("runScriptFile('", filename, "')") }; + LLCoros::instance().launch(desc, [&L, desc, filename, cb]() + { + llifstream in_file; + in_file.open(filename.c_str()); + + if (in_file.is_open()) + { + std::string text{std::istreambuf_iterator<char>(in_file), + std::istreambuf_iterator<char>()}; + auto [count, result] = L.expr(desc, text); + if (cb) + { + cb(count, result); + } + } + else + { + auto msg{ stringize("unable to open script file '", filename, "'") }; + LL_WARNS("Lua") << msg << LL_ENDL; + if (cb) + { + cb(-1, msg); + } + } + }); +} + +void LLLUAmanager::runScriptLine(const std::string& chunk, script_finished_fn cb) +{ + // A script_finished_fn is used to initialize the LuaState. + // It will be called when the LuaState is destroyed. + LuaState L(cb); + runScriptLine(L, chunk); +} + +void LLLUAmanager::runScriptLine(const std::string& chunk, script_result_fn cb) +{ + LuaState L; + // A script_result_fn will be called when LuaState::expr() completes. + runScriptLine(L, chunk, cb); +} + +LLCoros::Future<std::pair<int, LLSD>> +LLLUAmanager::startScriptLine(LuaState& L, const std::string& chunk) +{ + // Despite returning from startScriptLine(), we need this Promise to + // remain alive until the callback has fired. + auto promise{ std::make_shared<LLCoros::Promise<std::pair<int, LLSD>>>() }; + runScriptLine(L, chunk, + [promise](int count, LLSD result) + { promise->set_value({ count, result }); }); + return LLCoros::getFuture(*promise); +} + +std::pair<int, LLSD> LLLUAmanager::waitScriptLine(LuaState& L, const std::string& chunk) +{ + return startScriptLine(L, chunk).get(); +} + +void LLLUAmanager::runScriptLine(LuaState& L, const std::string& chunk, script_result_fn cb) +{ + // find a suitable abbreviation for the chunk string + std::string shortchunk{ chunk }; + const size_t shortlen = 40; + std::string::size_type eol = shortchunk.find_first_of("\r\n"); + if (eol != std::string::npos) + shortchunk = shortchunk.substr(0, eol); + if (shortchunk.length() > shortlen) + shortchunk = stringize(shortchunk.substr(0, shortlen), "..."); + + std::string desc{ stringize("runScriptLine('", shortchunk, "')") }; + LLCoros::instance().launch(desc, [&L, desc, chunk, cb]() + { + auto [count, result] = L.expr(desc, chunk); + if (cb) + { + cb(count, result); + } + }); +} + +void LLLUAmanager::runScriptOnLogin() +{ +#ifndef LL_TEST + std::string filename = gSavedSettings.getString("AutorunLuaScriptName"); + if (filename.empty()) + { + LL_INFOS() << "Script name wasn't set." << LL_ENDL; + return; + } + + filename = gDirUtilp->getExpandedFilename(LL_PATH_USER_SETTINGS, filename); + if (!gDirUtilp->fileExists(filename)) + { + LL_INFOS() << filename << " was not found." << LL_ENDL; + return; + } + + runScriptFile(filename); +#endif // ! LL_TEST +} |