diff options
author | nat-goodspeed <nat@lindenlab.com> | 2024-09-23 12:37:15 -0400 |
---|---|---|
committer | GitHub <noreply@github.com> | 2024-09-23 12:37:15 -0400 |
commit | 9289b96de48e0a3f57819ca173c5d5d51ad25c56 (patch) | |
tree | 510d0b10c3f9b7c74edce7be324a5931c1d736ca /indra/llcommon | |
parent | 4af7cd51e9cc22d9dc2fe42e378051c55515ac8e (diff) | |
parent | 34e89de9f9325aed3b06d1658888e973e19a17fc (diff) |
Merge pull request #2635 from secondlife/lua-script-args
Add ability to pass command-line arguments to a Lua script.
Diffstat (limited to 'indra/llcommon')
-rw-r--r-- | indra/llcommon/CMakeLists.txt | 2 | ||||
-rw-r--r-- | indra/llcommon/llleap.cpp | 45 | ||||
-rw-r--r-- | indra/llcommon/lua_function.cpp | 200 | ||||
-rw-r--r-- | indra/llcommon/lua_function.h | 21 | ||||
-rw-r--r-- | indra/llcommon/scriptcommand.cpp | 117 | ||||
-rw-r--r-- | indra/llcommon/scriptcommand.h | 64 | ||||
-rw-r--r-- | indra/llcommon/tests/llleap_test.cpp | 4 |
7 files changed, 354 insertions, 99 deletions
diff --git a/indra/llcommon/CMakeLists.txt b/indra/llcommon/CMakeLists.txt index f47136f781..9b99d86434 100644 --- a/indra/llcommon/CMakeLists.txt +++ b/indra/llcommon/CMakeLists.txt @@ -108,6 +108,7 @@ set(llcommon_SOURCE_FILES lua_function.cpp lualistener.cpp resultset.cpp + scriptcommand.cpp threadpool.cpp throttle.cpp u64.cpp @@ -257,6 +258,7 @@ set(llcommon_HEADER_FILES lua_function.h lualistener.h resultset.h + scriptcommand.h stdtypes.h stringize.h tempset.h diff --git a/indra/llcommon/llleap.cpp b/indra/llcommon/llleap.cpp index 306d4e48d0..ef43af6ceb 100644 --- a/indra/llcommon/llleap.cpp +++ b/indra/llcommon/llleap.cpp @@ -21,17 +21,18 @@ // external library headers // other Linden headers #include "llerror.h" -#include "llstring.h" -#include "llprocess.h" +#include "llerrorcontrol.h" #include "llevents.h" -#include "stringize.h" -#include "llsdutil.h" +#include "llexception.h" +#include "llleaplistener.h" +#include "llprocess.h" #include "llsdserialize.h" -#include "llerrorcontrol.h" +#include "llsdutil.h" +#include "llstring.h" #include "lltimer.h" #include "lluuid.h" -#include "llleaplistener.h" -#include "llexception.h" +#include "scriptcommand.h" +#include "stringize.h" #if LL_MSVC #pragma warning (disable : 4355) // 'this' used in initializer list: yes, intentionally @@ -107,7 +108,7 @@ public: // If that didn't work, no point in keeping this LLLeap object. if (! mChild) { - LLTHROW(Error(STRINGIZE("failed to run " << mDesc))); + LLTHROW(Error(stringize("failed to run ", mDesc))); } // Okay, launch apparently worked. Change our mDonePump listener. @@ -484,12 +485,24 @@ LLLeap* LLLeap::create(const std::string& desc, const std::vector<std::string>& LLLeap* LLLeap::create(const std::string& desc, const std::string& plugin, bool exc) { - // Use LLStringUtil::getTokens() to parse the command line - return create(desc, - LLStringUtil::getTokens(plugin, - " \t\r\n", // drop_delims - "", // no keep_delims - "\"'", // either kind of quotes - "\\"), // backslash escape - exc); + // Use ScriptCommand to parse the command line + ScriptCommand command(plugin); + auto error = command.error(); + if (! error.empty()) + { + if (exc) + { + LLTHROW(Error(error)); + } + return nullptr; + } + + LLProcess::Params params; + params.desc = desc; + params.executable = command.script; + for (const auto& arg : command.args) + { + params.args.add(arg); + } + return create(params, exc); } diff --git a/indra/llcommon/lua_function.cpp b/indra/llcommon/lua_function.cpp index 2557fd0cc9..a9f88f3170 100644 --- a/indra/llcommon/lua_function.cpp +++ b/indra/llcommon/lua_function.cpp @@ -60,8 +60,11 @@ namespace namespace lluau { -int dostring(lua_State* L, const std::string& desc, const std::string& text) +int dostring(lua_State* L, const std::string& desc, const std::string& text, + const std::vector<std::string>& args) { + // debug.traceback() + compiled chunk + args table + args... + slop + lluau_checkstack(L, 1 + 1 + 1 + int(args.size()) + 2); auto r = loadstring(L, desc, text); if (r != LUA_OK) return r; @@ -80,16 +83,71 @@ int dostring(lua_State* L, const std::string& desc, const std::string& text) // stack: compiled chunk, debug.traceback() lua_insert(L, -2); // stack: debug.traceback(), compiled chunk - LuaRemover cleanup(L, -2); + // capture absolute index of debug.traceback() + int traceback = lua_absindex(L, -2); + // remove it from stack on exit + LuaRemover cleanup(L, traceback); + + // Originally we just pushed 'args' to the Lua stack before entering the + // chunk. But that's awkward for the chunk: it must reference those + // arguments as '...', using any of a number of tactics to move them to + // named variables. This doesn't work as a Lua chunk expecting arguments: + // function(a, b, c) + // -- ... + // end + // because that code only *defines* a function: the function's body isn't + // entered by executing the chunk. + // + // Per https://www.lua.org/manual/5.1/manual.html#6 Lua Stand-alone, we + // now also create a global table called 'arg' whose [0] is the script + // name, ['n'] is the number of additional arguments and [1] through + // [['n']] are the additional arguments. We diverge from that spec in not + // creating any negative indices. + // + // Since the spec notes that the chunk can also reference args using + // '...', we also leave them on the stack. + + // stack: debug.traceback(), compiled chunk + // create arg table pre-sized to hold the args array, plus [0] and ['n'] + lua_createtable(L, narrow(args.size()), 2); + // stack: debug.traceback(), compiled chunk, arg table + int argi = lua_absindex(L, -1); + lua_Integer i = 0; + // store desc (e.g. script name) as arg[0] + lua_pushinteger(L, i); + lua_pushstdstring(L, desc); + lua_rawset(L, argi); // rawset() pops key and value + // store args.size() as arg.n + lua_pushinteger(L, narrow(args.size())); + lua_setfield(L, argi, "n"); // setfield() pops value + for (const auto& arg : args) + { + // push each arg in order + lua_pushstdstring(L, arg); + // push index + lua_pushinteger(L, ++i); + // duplicate arg[i] to store in arg table + lua_pushvalue(L, -2); + // stack: ..., arg[i], i, arg[i] + lua_rawset(L, argi); + // leave ..., arg[i] on stack + } + // stack: debug.traceback(), compiled chunk, arg, arg[1], arg[2], ... + // duplicate the arg table to store it + lua_pushvalue(L, argi); + lua_setglobal(L, "arg"); + lua_remove(L, argi); + // stack: debug.traceback(), compiled chunk, arg[1], arg[2], ... // It's important to pass LUA_MULTRET as the expected number of return // values: if we pass any fixed number, we discard any returned values // beyond that number. - return lua_pcall(L, 0, LUA_MULTRET, -2); + return lua_pcall(L, int(args.size()), LUA_MULTRET, traceback); } int loadstring(lua_State *L, const std::string &desc, const std::string &text) { + lluau_checkstack(L, 1); size_t bytecodeSize = 0; // The char* returned by luau_compile() must be freed by calling free(). // Use unique_ptr so the memory will be freed even if luau_load() throws. @@ -529,8 +587,7 @@ int lua_metaipair(lua_State* L); } // anonymous namespace -LuaState::LuaState(script_finished_fn cb): - mCallback(cb) +LuaState::LuaState() { /*---------------------------- feature flag ----------------------------*/ try @@ -715,80 +772,74 @@ LuaState::~LuaState() return; /*---------------------------- feature flag ----------------------------*/ - if (mFeature) + if (! mFeature) + return; /*---------------------------- feature flag ----------------------------*/ + + // We're just about to destroy this lua_State mState. Did this Lua chunk + // register any atexit() functions? + lluau_checkstack(mState, 3); + // look up Registry.atexit + lua_getfield(mState, LUA_REGISTRYINDEX, "atexit"); + // stack contains Registry.atexit + if (lua_istable(mState, -1)) { - // We're just about to destroy this lua_State mState. Did this Lua chunk - // register any atexit() functions? - lluau_checkstack(mState, 3); - // 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. + int len(lua_objlen(mState, -1)); + LL_DEBUGS("Lua") << LLCoros::getName() << ": Registry.atexit is a table with " + << len << " entries" << LL_ENDL; + + // Push debug.traceback() onto the stack as lua_pcall()'s error + // handler function. On error, lua_pcall() calls the specified error + // handler function with the original error message; the message + // returned by the error handler is then returned by lua_pcall(). + // Luau's debug.traceback() is called with a message to prepend to the + // returned traceback string. Almost as if they'd been designed to + // work together... + lua_getglobal(mState, "debug"); + lua_getfield(mState, -1, "traceback"); + // ditch "debug" + lua_remove(mState, -2); + // stack now contains atexit, debug.traceback() + + for (int i(len); i >= 1; --i) { - // 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. - int len(lua_objlen(mState, -1)); - LL_DEBUGS("Lua") << LLCoros::getName() << ": Registry.atexit is a table with " - << len << " entries" << LL_ENDL; - - // Push debug.traceback() onto the stack as lua_pcall()'s error - // handler function. On error, lua_pcall() calls the specified error - // handler function with the original error message; the message - // returned by the error handler is then returned by lua_pcall(). - // Luau's debug.traceback() is called with a message to prepend to the - // returned traceback string. Almost as if they'd been designed to - // work together... - lua_getglobal(mState, "debug"); - lua_getfield(mState, -1, "traceback"); - // ditch "debug" - lua_remove(mState, -2); - // stack now contains atexit, debug.traceback() - - for (int i(len); i >= 1; --i) + lua_pushinteger(mState, i); + // stack contains Registry.atexit, debug.traceback(), i + lua_gettable(mState, -3); + // stack contains Registry.atexit, debug.traceback(), 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. Pass debug.traceback() as + // the error handler function. + LL_DEBUGS("Lua") << LLCoros::getName() + << ": calling atexit(" << i << ")" << LL_ENDL; + if (lua_pcall(mState, 0, 0, -2) != LUA_OK) { - lua_pushinteger(mState, i); - // stack contains Registry.atexit, debug.traceback(), i - lua_gettable(mState, -3); - // stack contains Registry.atexit, debug.traceback(), 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. Pass debug.traceback() as - // the error handler function. - LL_DEBUGS("Lua") << LLCoros::getName() - << ": calling atexit(" << i << ")" << LL_ENDL; - if (lua_pcall(mState, 0, 0, -2) != LUA_OK) - { - auto error{ lua_tostdstring(mState, -1) }; - LL_WARNS("Lua") << LLCoros::getName() - << ": atexit(" << i << ") error: " << error << LL_ENDL; - // pop error message - lua_pop(mState, 1); - } - LL_DEBUGS("Lua") << LLCoros::getName() << ": atexit(" << i << ") done" << LL_ENDL; - // lua_pcall() has already popped atexit[i]: - // stack contains atexit, debug.traceback() + auto error{ lua_tostdstring(mState, -1) }; + LL_WARNS("Lua") << LLCoros::getName() + << ": atexit(" << i << ") error: " << error << LL_ENDL; + // pop error message + lua_pop(mState, 1); } - // pop debug.traceback() - lua_pop(mState, 1); + LL_DEBUGS("Lua") << LLCoros::getName() << ": atexit(" << i << ") done" << LL_ENDL; + // lua_pcall() has already popped atexit[i]: + // stack contains atexit, debug.traceback() } - // pop Registry.atexit (either table or nil) + // pop debug.traceback() lua_pop(mState, 1); - - // with the demise of this LuaState, remove sLuaStateMap entry - sLuaStateMap.erase(mState); - - lua_close(mState); } + // pop Registry.atexit (either table or nil) + lua_pop(mState, 1); - if (mCallback) - { - // mError potentially set by previous checkLua() call(s) - mCallback(mError); - } + // with the demise of this LuaState, remove sLuaStateMap entry + sLuaStateMap.erase(mState); + + lua_close(mState); } bool LuaState::checkLua(const std::string& desc, int r) @@ -804,7 +855,8 @@ bool LuaState::checkLua(const std::string& desc, int r) return true; } -std::pair<int, LLSD> LuaState::expr(const std::string& desc, const std::string& text) +std::pair<int, LLSD> LuaState::expr(const std::string& desc, const std::string& text, + const std::vector<std::string>& args) { /*---------------------------- feature flag ----------------------------*/ if (! mFeature) @@ -827,7 +879,7 @@ std::pair<int, LLSD> LuaState::expr(const std::string& desc, const std::string& }; LL_INFOS("Lua") << desc << " run" << LL_ENDL; - if (! checkLua(desc, lluau::dostring(mState, desc, text))) + if (! checkLua(desc, lluau::dostring(mState, desc, text, args))) { LL_WARNS("Lua") << desc << " error: " << mError << LL_ENDL; return { -1, mError }; @@ -952,8 +1004,8 @@ void LuaState::check_interrupts_counter() } else if (mInterrupts % INTERRUPTS_SUSPEND_LIMIT == 0) { - LL_DEBUGS("Lua") << LLCoros::getName() << " suspending at " << mInterrupts - << " interrupts" << LL_ENDL; + LL_DEBUGS("Lua.suspend") << LLCoros::getName() << " suspending at " + << mInterrupts << " interrupts" << LL_ENDL; llcoro::suspend(); } } diff --git a/indra/llcommon/lua_function.h b/indra/llcommon/lua_function.h index 10c201c234..ae6e0bf7ba 100644 --- a/indra/llcommon/lua_function.h +++ b/indra/llcommon/lua_function.h @@ -19,6 +19,7 @@ #include "fsyspath.h" #include "llerror.h" #include "llsd.h" +#include "scriptcommand.h" #include "stringize.h" #include <exception> // std::uncaught_exceptions() #include <memory> // std::shared_ptr @@ -26,6 +27,7 @@ #include <typeinfo> #include <unordered_map> #include <utility> // std::pair +#include <vector> class LuaListener; @@ -55,8 +57,10 @@ namespace lluau // luau removed lua_dostring(), but since we perform the equivalent luau // sequence in multiple places, encapsulate it. desc and text are strings // rather than string_views because dostring() needs pointers to nul- - // terminated char arrays. - int dostring(lua_State* L, const std::string& desc, const std::string& text); + // terminated char arrays. Any args are pushed to the Lua stack before + // calling the Lua chunk in text. + int dostring(lua_State* L, const std::string& desc, const std::string& text, + const std::vector<std::string>& args={}); int loadstring(lua_State* L, const std::string& desc, const std::string& text); fsyspath source_path(lua_State* L); @@ -79,9 +83,7 @@ void lua_pushllsd(lua_State* L, const LLSD& data); class LuaState { public: - typedef std::function<void(std::string msg)> script_finished_fn; - - LuaState(script_finished_fn cb={}); + LuaState(); LuaState(const LuaState&) = delete; LuaState& operator=(const LuaState&) = delete; @@ -92,13 +94,19 @@ public: // expr() is for when we want to capture any results left on the stack // by a Lua expression, possibly including multiple return values. + // Pass: + // desc = description used for logging et al. + // text = Lua chunk to execute, e.g. contents of a script file + // args = arguments, if any, to pass to script file + // Returns: // int < 0 means error, and LLSD::asString() is the error message. // int == 0 with LLSD::isUndefined() means the Lua expression returned no // results. // int == 1 means the Lua expression returned one result. // int > 1 with LLSD::isArray() means the Lua expression returned // multiple results, represented as the entries of the array. - std::pair<int, LLSD> expr(const std::string& desc, const std::string& text); + std::pair<int, LLSD> expr(const std::string& desc, const std::string& text, + const std::vector<std::string>& args={}); operator lua_State*() const { return mState; } @@ -118,7 +126,6 @@ private: /*---------------------------- feature flag ----------------------------*/ bool mFeature{ false }; /*---------------------------- feature flag ----------------------------*/ - script_finished_fn mCallback; lua_State* mState{ nullptr }; std::string mError; S32 mInterrupts{ 0 }; diff --git a/indra/llcommon/scriptcommand.cpp b/indra/llcommon/scriptcommand.cpp new file mode 100644 index 0000000000..79afbc2063 --- /dev/null +++ b/indra/llcommon/scriptcommand.cpp @@ -0,0 +1,117 @@ +/** + * @file scriptcommand.cpp + * @author Nat Goodspeed + * @date 2024-09-16 + * @brief Implementation for scriptcommand. + * + * $LicenseInfo:firstyear=2024&license=viewerlgpl$ + * Copyright (c) 2024, Linden Research, Inc. + * $/LicenseInfo$ + */ + +// Precompiled header +#include "linden_common.h" +// associated header +#include "scriptcommand.h" +// STL headers +// std headers +#include <sstream> +// external library headers +// other Linden headers +#include "fsyspath.h" +#include "llerror.h" +#include "llsdutil.h" +#include "llstring.h" +#include "stringize.h" + +ScriptCommand::ScriptCommand(const std::string& command, const LLSD& path, + const std::string& base) +{ + fsyspath basepath(base); + // Use LLStringUtil::getTokens() to parse the script command line + args = LLStringUtil::getTokens(command, + " \t\r\n", // drop_delims + "", // no keep_delims + "\"'", // either kind of quotes + "\\"); // backslash escape + // search for args[0] on paths + if (search(args[0], path, basepath)) + { + // The first token is in fact the script filename. Now that we've + // found the script file, we've consumed that token. The rest are + // command-line arguments. + args.erase(args.begin()); + return; + } + + // Parsing the command line produced a script file path we can't find. + // Maybe that's because there are spaces in the original pathname that + // were neither quoted nor escaped? See if we can find the whole original + // command line string. + if (search(command, path, basepath)) + { + // Here we found script, using the whole input command line as its + // pathname. Discard any parts of it we mistook for command-line + // arguments. + args.clear(); + return; + } + + // Couldn't find the script either way. Is it because we can't even check + // existence? + if (! mError.empty()) + { + return; + } + + // No, existence check works, we just can't find the script. + std::ostringstream msgstream; + msgstream << "Can't find script file " << std::quoted(args[0]); + if (command != args[0]) + { + msgstream << " or " << std::quoted(command); + } + if (path.size() > 0) + { + msgstream << " on " << path; + } + if (! base.empty()) + { + msgstream << " relative to " << base; + } + mError = msgstream.str(); + LL_WARNS("Lua") << mError << LL_ENDL; +} + +bool ScriptCommand::search(const fsyspath& script, const LLSD& paths, const fsyspath& base) +{ + for (const auto& path : llsd::inArray(paths)) + { + // If a path is already absolute, (otherpath / path) preserves it. + // Explicitly instantiate fsyspath for every string conversion to + // properly convert UTF-8 filename strings on Windows. + fsyspath absscript{ base / fsyspath(path.asString()) / script }; + bool exists; + try + { + exists = std::filesystem::exists(absscript); + } + catch (const std::filesystem::filesystem_error& exc) + { + mError = stringize("Can't check existence: ", exc.what()); + LL_WARNS("Lua") << mError << LL_ENDL; + return false; + } + if (exists) + { + this->script = absscript.string(); + return true; + } + } + return false; +} + +std::string ScriptCommand::error() const +{ + return mError; +} diff --git a/indra/llcommon/scriptcommand.h b/indra/llcommon/scriptcommand.h new file mode 100644 index 0000000000..dafed552bb --- /dev/null +++ b/indra/llcommon/scriptcommand.h @@ -0,0 +1,64 @@ +/** + * @file scriptcommand.h + * @author Nat Goodspeed + * @date 2024-09-16 + * @brief ScriptCommand parses a string into a script filepath plus arguments. + * + * $LicenseInfo:firstyear=2024&license=viewerlgpl$ + * Copyright (c) 2024, Linden Research, Inc. + * $/LicenseInfo$ + */ + +#if ! defined(LL_SCRIPTCOMMAND_H) +#define LL_SCRIPTCOMMAND_H + +#include "llsd.h" + +#include <string> +#include <vector> + +class fsyspath; + +class ScriptCommand +{ +public: + /** + * ScriptCommand accepts a command-line string to invoke an existing + * script, which may or may not include arguments to pass to that script. + * The constructor parses the command line into tokens using quoting and + * escaping rules similar to bash. The first token is assumed to be the + * script name; ScriptCommand tries to find that script on the filesystem, + * trying each directory listed in the LLSD array of strings 'path'. When + * it finds the script file, it stores its full pathname in 'script' and + * the remaining tokens in 'args'. + * + * But if the constructor can't find the first token on 'path', it + * considers the possibility that the whole command-line string is + * actually a single pathname containing unescaped spaces. If that script + * file is found on 'path', it stores its full pathname in 'script' and + * leaves 'args' empty. + * + * We accept 'path' as an LLSD array rather than (e.g.) + * std::vector<std::string> because the primary use case involves + * retrieving 'path' from settings. + * + * If you also pass 'base', any directory on 'path' may be specified + * relative to 'base'. Otherwise, every directory on 'path' must be + * absolute. + */ + ScriptCommand(const std::string& command, const LLSD& path=LLSD::emptyArray(), + const std::string& base={}); + + std::string script; + std::vector<std::string> args; + + // returns empty string if no error + std::string error() const; + +private: + bool search(const fsyspath& script, const LLSD& path, const fsyspath& base); + + std::string mError; +}; + +#endif /* ! defined(LL_SCRIPTCOMMAND_H) */ diff --git a/indra/llcommon/tests/llleap_test.cpp b/indra/llcommon/tests/llleap_test.cpp index ca1939c81e..a345608299 100644 --- a/indra/llcommon/tests/llleap_test.cpp +++ b/indra/llcommon/tests/llleap_test.cpp @@ -300,8 +300,8 @@ namespace tut std::string threw = catch_what<LLLeap::Error>([&BADPYTHON](){ LLLeap::create("bad exe", BADPYTHON); }); - ensure_contains("LLLeap::create() didn't throw", threw, "failed"); - log.messageWith("failed"); + ensure_contains("LLLeap::create() didn't throw", threw, "Can't find"); + log.messageWith("Can't find"); log.messageWith(BADPYTHON); // try the suppress-exception variant ensure("bad launch returned non-NULL", ! LLLeap::create("bad exe", BADPYTHON, false)); |