diff --git a/DOCS/man/lua.rst b/DOCS/man/lua.rst index a01e429034..c49d2c388c 100644 --- a/DOCS/man/lua.rst +++ b/DOCS/man/lua.rst @@ -511,6 +511,9 @@ This built-in module provides generic helper functions for Lua, and have strictly speaking nothing to do with mpv or video/audio playback. They are provided for convenience. Most compensate for Lua's scarce standard library. +Be warned that any of these functions might disappear any time. They are not +strictly part of the guaranteed API. + ``utils.getcwd()`` Returns the directory that mpv was launched from. On error, ``nil, error`` is returned. @@ -551,6 +554,45 @@ provided for convenience. Most compensate for Lua's scarce standard library. Return the concatenation of the 2 paths. Tries to be clever. For example, if ```p2`` is an absolute path, p2 is returned without change. +``utils.subprocess(t)`` + Runs an external process and waits until it exits. Returns process status + and the captured output. + + This function is not available on Microsoft Windows. + + The paramater ``t`` is a table. The function reads the following entries: + + ``args`` + Array of strings. The first array entry is the executable. This + can be either an absolute path, or a filename with no path + components, in which case the ``PATH`` environment variable is + used to resolve the executable. The other array elements are + passed as command line arguments. + + ``cancellable`` + Optional. If set to ``true`` (default), then if the user stops + playback or goes to the next file while the process is running, + the process will be killed. + + ``max_size`` + Optional. The maximum size in bytes of the data that can be captured + from stdout. (Default: 16 MB.) + + The function returns a table as result with the following entries: + + ``status`` + The raw exit status of the process. It will be negative on error. + + ``stdout`` + Captured output stream as string, limited to ``max_size``. + + ``error`` + ``nil`` on success. The string ``killed`` if the process was + terminated in an unusual way. The string ``init`` if the process + could not be started. + + In all cases, ``mp.resume_all()`` is implicitly called. + Events ------ diff --git a/old-configure b/old-configure index cf2c4c9cde..26fee13711 100755 --- a/old-configure +++ b/old-configure @@ -989,6 +989,7 @@ cat > $TMPC << EOF #define HAVE_NANOSLEEP 1 #define HAVE_SDL1 0 #define HAVE_WAIO 0 +#define HAVE_POSIX_SPAWN 1 #define DEFAULT_CDROM_DEVICE "/dev/cdrom" #define DEFAULT_DVD_DEVICE "/dev/dvd" diff --git a/player/lua.c b/player/lua.c index 30e00a625d..b565dedfe2 100644 --- a/player/lua.c +++ b/player/lua.c @@ -42,6 +42,7 @@ #include "misc/bstr.h" #include "osdep/timer.h" #include "osdep/threads.h" +#include "stream/stream.h" #include "sub/osd.h" #include "core.h" #include "command.h" @@ -435,12 +436,16 @@ static int script_resume(lua_State *L) return 0; } -static int script_resume_all(lua_State *L) +static void resume_all(struct script_ctx *ctx) { - struct script_ctx *ctx = get_ctx(L); if (ctx->suspended) mpv_resume(ctx->client); ctx->suspended = 0; +} + +static int script_resume_all(lua_State *L) +{ + resume_all(get_ctx(L)); return 0; } @@ -1149,6 +1154,142 @@ static int script_join_path(lua_State *L) return 1; } +#if HAVE_POSIX_SPAWN +#include +#include +#include +#include +#include +#include + +// Normally, this must be declared manually, but glibc is retarded. +#ifndef __GLIBC__ +extern char **environ; +#endif + +static int script_subprocess(lua_State *L) +{ + void *tmp = mp_lua_PITA(L); + struct script_ctx *ctx = get_ctx(L); + luaL_checktype(L, 1, LUA_TTABLE); + + resume_all(ctx); + + lua_getfield(L, 1, "args"); // args + int num_args = lua_objlen(L, -1); + char *args[256]; + if (num_args > MP_ARRAY_SIZE(args) - 1) // last needs to be NULL + luaL_error(L, "too many arguments"); + if (num_args < 1) + luaL_error(L, "program name missing"); + for (int n = 0; n < num_args; n++) { + lua_pushinteger(L, n + 1); // args n + lua_gettable(L, -2); // args arg + args[n] = talloc_strdup(tmp, lua_tostring(L, -1)); + lua_pop(L, 1); // args + } + args[num_args] = NULL; + lua_pop(L, 1); // - + + lua_getfield(L, 1, "cancellable"); // c + struct mp_cancel *cancel = NULL; + if (lua_isnil(L, -1) ? true : lua_toboolean(L, -1)) + cancel = ctx->mpctx->playback_abort; + lua_pop(L, 1); // - + + lua_getfield(L, 1, "max_size"); // m + int64_t max_size = lua_isnil(L, -1) ? 16 * 1024 * 1024 : lua_tointeger(L, -1); + + // --- no Lua errors from here + + posix_spawn_file_actions_t fa; + bool fa_destroy = false; + bstr stdout = {0}; + int status = -1; + int pipes[2] = {-1, -1}; + pid_t pid = -1; + + if (pipe(pipes)) + goto done; + mp_set_cloexec(pipes[0]); + mp_set_cloexec(pipes[1]); + + if (posix_spawn_file_actions_init(&fa)) + goto done; + fa_destroy = true; + // redirect stdout, but not stderr or stdin + if (posix_spawn_file_actions_adddup2(&fa, pipes[1], 1)) + goto done; + + if (posix_spawnp(&pid, args[0], &fa, NULL, args, environ)) { + pid = -1; + goto done; + } + + close(pipes[1]); + pipes[1] = -1; + + bool eof = false; + while (!eof) { + struct pollfd fds[] = { + {.events = POLLIN, .fd = pipes[0]}, + {.events = POLLIN, .fd = cancel ? mp_cancel_get_fd(cancel) : -1}, + }; + if (poll(fds, fds[1].fd >= 0 ? 2 : 1, -1) < 0 && errno != EINTR) + break; + if (fds[1].revents) + break; + if (fds[0].revents) { + char buf[4096]; + ssize_t r = read(pipes[0], buf, sizeof(buf)); + if (r < 0 && errno == EINTR) + continue; + if (r > 0) + bstr_xappend(tmp, &stdout, (bstr){buf, r}); + eof = r == 0; + if (r <= 0) + break; + } + if (stdout.len >= max_size) + break; + } + + if (!eof || (cancel && mp_cancel_test(cancel))) + kill(pid, SIGKILL); + + // Note: it can happen that a child process closes the pipe, but does not + // terminate yet. In this case, we would have to run waitpid() in + // a separate thread and use pthread_cancel(), or use other weird + // and laborious tricks. So this isn't handled yet. + while (waitpid(pid, &status, 0) < 0 && errno == EINTR) {} + +done: + if (fa_destroy) + posix_spawn_file_actions_destroy(&fa); + close(pipes[0]); + close(pipes[1]); + + // --- Lua errors are ok again from here + char *error = NULL; + if (WIFEXITED(status) && WEXITSTATUS(status) != 127) { + status = WEXITSTATUS(status); + } else { + error = WEXITSTATUS(status) == 127 ? "init" : "killed"; + status = -1; + } + lua_newtable(L); // res + if (error) { + lua_pushstring(L, error); // res e + lua_setfield(L, -2, "error"); // res + } + lua_pushinteger(L, status); // res s + lua_setfield(L, -2, "status"); // res + lua_pushlstring(L, stdout.start, stdout.len); // res d + lua_setfield(L, -2, "stdout"); // res + return 1; +} +#endif + #define FN_ENTRY(name) {#name, script_ ## name} struct fn_entry { const char *name; @@ -1195,6 +1336,9 @@ static const struct fn_entry utils_fns[] = { FN_ENTRY(readdir), FN_ENTRY(split_path), FN_ENTRY(join_path), +#if HAVE_POSIX_SPAWN + FN_ENTRY(subprocess), +#endif {0} }; diff --git a/stream/stream.c b/stream/stream.c index fe80028091..8a3d981809 100644 --- a/stream/stream.c +++ b/stream/stream.c @@ -18,20 +18,16 @@ #include #include -#include - #include -#include -#ifndef __MINGW32__ -#include -#include -#endif -#include +#include +#include + #include #include #include #include "osdep/atomics.h" +#include "osdep/io.h" #include "talloc.h" @@ -988,12 +984,22 @@ struct bstr stream_read_complete(struct stream *s, void *talloc_ctx, struct mp_cancel { atomic_bool triggered; + int wakeup_pipe[2]; }; +static void cancel_destroy(void *p) +{ + struct mp_cancel *c = p; + close(c->wakeup_pipe[0]); + close(c->wakeup_pipe[1]); +} + struct mp_cancel *mp_cancel_new(void *talloc_ctx) { struct mp_cancel *c = talloc_ptrtype(talloc_ctx, c); + talloc_set_destructor(c, cancel_destroy); *c = (struct mp_cancel){.triggered = ATOMIC_VAR_INIT(false)}; + mp_make_wakeup_pipe(c->wakeup_pipe); return c; } @@ -1001,12 +1007,21 @@ struct mp_cancel *mp_cancel_new(void *talloc_ctx) void mp_cancel_trigger(struct mp_cancel *c) { atomic_store(&c->triggered, true); + write(c->wakeup_pipe[1], &(char){0}, 1); } // Restore original state. (Allows reusing a mp_cancel.) void mp_cancel_reset(struct mp_cancel *c) { atomic_store(&c->triggered, false); + // Flush it fully. + while (1) { + int r = read(c->wakeup_pipe[0], &(char[256]){0}, 256); + if (r < 0 && errno == EINTR) + continue; + if (r <= 0) + break; + } } // Return whether the caller should abort. @@ -1016,6 +1031,13 @@ bool mp_cancel_test(struct mp_cancel *c) return c ? atomic_load(&c->triggered) : false; } +// The FD becomes readable if mp_cancel_test() would return true. +// Don't actually read from it, just use it for poll(). +int mp_cancel_get_fd(struct mp_cancel *c) +{ + return c->wakeup_pipe[0]; +} + void stream_print_proto_list(struct mp_log *log) { int count = 0; diff --git a/stream/stream.h b/stream/stream.h index 56c431ed46..e7a0bb5dca 100644 --- a/stream/stream.h +++ b/stream/stream.h @@ -263,6 +263,7 @@ struct mp_cancel *mp_cancel_new(void *talloc_ctx); void mp_cancel_trigger(struct mp_cancel *c); bool mp_cancel_test(struct mp_cancel *c); void mp_cancel_reset(struct mp_cancel *c); +int mp_cancel_get_fd(struct mp_cancel *c); // stream_file.c char *mp_file_url_to_filename(void *talloc_ctx, bstr url); diff --git a/wscript b/wscript index e1e5fa4dab..c95984d331 100644 --- a/wscript +++ b/wscript @@ -196,6 +196,10 @@ iconv support use --disable-iconv.", 'desc': 'shm', 'func': check_statement(['sys/types.h', 'sys/ipc.h', 'sys/shm.h'], 'shmget(0, 0, 0); shmat(0, 0, 0); shmctl(0, 0, 0)') + }, { + 'name': 'posix-spawn', + 'desc': 'posix_spawn()', + 'func': check_statement('spawn.h', 'posix_spawnp(0,0,0,0,0,0)') }, { 'name': 'glob', 'desc': 'glob()',