From a57cb4d595d746d5e97a9a6307aef4358b92f84c Mon Sep 17 00:00:00 2001 From: Panu Matilainen Date: Mon, 12 Aug 2024 17:03:17 +0300 Subject: [PATCH] Add rpm.spawn() Lua API Turns out real-world usage needs more control than rpm.execute() delivers. This could be crammed into rpm.execute() but it'd be forward incompatible in a somewhat non-obvious way, we might just as well add a separate function for it. Supports redirecting stdin, stdout and stderr to a given path, support for file descriptors, other actions and spawn attributes can be added later. Fixes: #3192 (cherry picked from commit 6444f712869b1b81b4c5790ec390a6b98751336a) --- docs/manual/lua.md | 31 +++++++++++++++--- rpmio/lposix.c | 2 +- rpmio/rpmlua.c | 78 ++++++++++++++++++++++++++++++++++++++++++++++ tests/rpmmacro.at | 75 ++++++++++++++++++++++++++++++++++++++++++-- 4 files changed, 179 insertions(+), 7 deletions(-) diff --git a/docs/manual/lua.md b/docs/manual/lua.md index 786c54e4bc..4ea4f40b40 100644 --- a/docs/manual/lua.md +++ b/docs/manual/lua.md @@ -170,6 +170,8 @@ by optional number of arguments to pass to the command. rpm.execute('ls', '-l', '/') ``` +For better control over the process execution and output, see rpm.spawn(). + #### expand(arg) Perform rpm macro expansion on argument string. @@ -337,7 +339,28 @@ end ``` This function is deprecated and scheduled for removal in 6.0, -use `rpm.execute()` instead. +use `rpm.spawn()` or `rpm.execute()` instead. + +#### spawn({command} [, {actions}]) + +Spawn, aka execute, an external program. (rpm >= 4.20) + +`{command}` is a table consisting of the command and its arguments. +An optional second table can be used to pass various actions related +to the command execution, currently supported are: + +| Action | Argument(s) | Description +|---------|---------------------- +| `stdin` | path | Redirect standard input to path +| `stdout`| path | Redirect standard output to path +| `stderr`| path | Redirect standard error to path + +Returns the command exit status: zero on success, or a tuplet +of (nil, message, code) on failure. + +``` +rpm.spawn({'systemctl', 'restart', 'httpd'}, {stderr='/dev/null'}) +``` #### undefine(name) @@ -477,7 +500,7 @@ end Execute a program. This may only be performed after posix.fork(). This function is deprecated and scheduled for removal in 6.0, -use `rpm.execute()` instead. +use `rpm.spawn()` or `rpm.execute()` instead. #### files([path]) @@ -495,7 +518,7 @@ end Fork a new process. This function is deprecated and scheduled for removal in 6.0, -use `rpm.execute()` instead. +use `rpm.spawn()` or `rpm.execute()` instead. ``` pid = posix.fork() @@ -774,7 +797,7 @@ end ``` This function is deprecated and scheduled for removal in 6.0, -use `rpm.execute()` instead. +use `rpm.spawn()` or `rpm.execute()` instead. #### setenv(name, value [, overwrite]) diff --git a/rpmio/lposix.c b/rpmio/lposix.c index c886564198..bbfb48dcc3 100644 --- a/rpmio/lposix.c +++ b/rpmio/lposix.c @@ -45,7 +45,7 @@ extern int _rpmlua_have_forked; #define check_deprecated() \ fprintf(stderr, \ - "warning: posix.%s(): .fork(), .exec(), .wait() and .redirect2null() are deprecated, use rpm.execute() instead\n", __func__+1) + "warning: posix.%s(): .fork(), .exec(), .wait() and .redirect2null() are deprecated, use rpm.spawn() or rpm.execute() instead\n", __func__+1) static const char *filetype(mode_t m) { diff --git a/rpmio/rpmlua.c b/rpmio/rpmlua.c index 06f5da61d2..4d68a3fd33 100644 --- a/rpmio/rpmlua.c +++ b/rpmio/rpmlua.c @@ -813,6 +813,83 @@ static int rpm_exit(lua_State *L) exit(luaL_optinteger(L, 1, EXIT_SUCCESS)); } +static int do_spawnop(lua_State *L, posix_spawn_file_actions_t *fap) +{ + const char *key = luaL_checkstring(L, -2); + const char *val = luaL_checkstring(L, -1); + int rc = 0; + + if (rstreq(key, "stdin")) { + rc = posix_spawn_file_actions_addopen(fap, STDIN_FILENO, + val, O_RDONLY, 0644); + } else if (rstreq(key, "stdout")) { + rc = posix_spawn_file_actions_addopen(fap, STDOUT_FILENO, + val, O_WRONLY|O_APPEND|O_CREAT, 0644); + } else if (rstreq(key, "stderr")) { + rc = posix_spawn_file_actions_addopen(fap, STDERR_FILENO, + val, O_WRONLY|O_APPEND|O_CREAT, 0644); + } else { + luaL_error(L, "invalid spawn directive: %s", key); + } + + return rc; +} + +static int rpm_spawn(lua_State *L) +{ + int rc = 0, status; + pid_t pid; + posix_spawn_file_actions_t fa, *fap = NULL; + luaL_checktype(L, 1, LUA_TTABLE); + int argc = luaL_len(L, 1); + + if (argc == 0) + luaL_error(L, "command not supplied"); + + if (lua_istable(L, 2)) { + if ((rc = posix_spawn_file_actions_init(&fa))) + return pusherror(L, rc, "spawn file action init"); + fap = &fa; + + lua_pushnil(L); + while (!rc && lua_next(L, 2) != 0) { + rc = do_spawnop(L, &fa); + lua_pop(L, 1); + }; + if (rc) + return pusherror(L, rc, NULL); + } + + ARGV_t argv = NULL; + for (int i = 0; i < argc; i++) { + lua_rawgeti(L, 1, i + 1); + argvAdd(&argv, luaL_checkstring(L, -1)); + lua_pop(L, 1); + } + + rpmSetCloseOnExec(); + + status = posix_spawnp(&pid, argv[0], fap, NULL, argv, environ); + + argvFree(argv); + if (fap) + posix_spawn_file_actions_destroy(fap); + + if (status != 0) + return pusherror(L, status, NULL); + if (waitpid(pid, &status, 0) == -1) + return pusherror(L, errno, NULL); + if (status != 0) { + if (WIFSIGNALED(status)) { + return pusherror(L, WTERMSIG(status), "exit signal"); + } else { + return pusherror(L, WEXITSTATUS(status), "exit code"); + } + } + + return pushresult(L, WEXITSTATUS(status)); +} + static int rpm_execute(lua_State *L) { const char *file = luaL_checkstring(L, 1); @@ -1287,6 +1364,7 @@ static const luaL_Reg rpmlib[] = { {"unregister", rpm_unregister}, {"call", rpm_call}, {"interactive", rpm_interactive}, + {"spawn", rpm_spawn}, {"execute", rpm_execute}, {"redirect2null", rpm_redirect2null}, {"vercmp", rpm_vercmp}, diff --git a/tests/rpmmacro.at b/tests/rpmmacro.at index 571f1075ac..f96ec05eac 100644 --- a/tests/rpmmacro.at +++ b/tests/rpmmacro.at @@ -1475,13 +1475,84 @@ RPMTEST_CHECK([ cat stderr | sort ], [0], -[warning: posix.fork(): .fork(), .exec(), .wait() and .redirect2null() are deprecated, use rpm.execute() instead -warning: posix.wait(): .fork(), .exec(), .wait() and .redirect2null() are deprecated, use rpm.execute() instead +[warning: posix.fork(): .fork(), .exec(), .wait() and .redirect2null() are deprecated, use rpm.spawn() or rpm.execute() instead +warning: posix.wait(): .fork(), .exec(), .wait() and .redirect2null() are deprecated, use rpm.spawn() or rpm.execute() instead warning: runaway fork() in Lua script ], []) RPMTEST_CLEANUP +AT_SETUP([lua rpm spawn]) +AT_KEYWORDS([macros lua]) +RPMDB_INIT +RPMTEST_CHECK([ +runroot_other rpmlua -e "rpm.spawn({'echo', '1', '2', '3'})" +], +[0], +[1 2 3 +], +[]) + +RPMTEST_CHECK([ +runroot_other rpmlua \ + -e "rpm.spawn({'echo', '1', '2', '3'}, {stdout='/dev/null'})" +], +[0], +[], +[]) + +RPMTEST_CHECK([ +runroot_other rpmlua \ + -e "rpm.spawn({'ls', '/notthere'})" +], +[0], +[], +[ls: cannot access '/notthere': No such file or directory +]) + +RPMTEST_CHECK([ +runroot_other rpmlua \ + -e "rpm.spawn({'ls', '/notthere'}, {stderr='/dev/null'})" +], +[0], +[], +[]) + +RPMTEST_CHECK([ +runroot_other rpmlua \ + -e "rpm.spawn({'ls', '/notthere'}, {garbage='bbb'})" +], +[255], +[], +[[error: lua script failed: [string ""]:1: invalid spawn directive: garbage +]]) + +RPMTEST_CHECK([ +runroot_other rpmlua -e "rpm.spawn('echo', '1', '2', '3')" +], +[255], +[], +[[error: lua script failed: [string ""]:1: bad argument #1 to 'spawn' (table expected, got string) +]]) + +RPMTEST_CHECK([ +runroot_other rpmlua -e "print(rpm.spawn({'cat'}, {stdin='aaa'}))" +], +[0], +[nil No such file or directory 2.0 +], +[]) + +RPMTEST_CHECK([ +echo 1 2 3 > ${RPMTEST}/aaa +runroot_other rpmlua -e "rpm.spawn({'cat'}, {stdin='aaa'})" +], +[0], +[1 2 3 +], +[]) +RPMTEST_CLEANUP + AT_SETUP([rpmlua hooks]) AT_KEYWORDS([lua])