From e2ab6b7f3567542a2a1b5aab053d513737e72878 Mon Sep 17 00:00:00 2001 From: wm4 Date: Wed, 19 Feb 2020 22:18:15 +0100 Subject: [PATCH] scripting: add a way to run sub processes as "scripts" This is just a more convenient way to start IPC client scripts per mpv instance. Does not work on Windows, although it could if the subprocess and IPC parts are implemented (and I guess .exe/.bat suffixes are required). Also untested whether it builds on Windows. A lot of other things are untested too, so don't complain. --- DOCS/man/ipc.rst | 27 ++++++++++++++ input/input.h | 10 ++++++ input/ipc-dummy.c | 6 ++++ input/ipc-unix.c | 53 +++++++++++++++++++++------ input/ipc-win.c | 6 ++++ osdep/subprocess.c | 6 ++++ player/core.h | 1 + player/scripting.c | 89 ++++++++++++++++++++++++++++++++++++++++------ 8 files changed, 177 insertions(+), 21 deletions(-) diff --git a/DOCS/man/ipc.rst b/DOCS/man/ipc.rst index 2aa406b190..42e67f6e4a 100644 --- a/DOCS/man/ipc.rst +++ b/DOCS/man/ipc.rst @@ -297,3 +297,30 @@ Is equivalent to: :: { "objkey": "value\n" } + +Alternative ways of starting clients +------------------------------------ + +You can create an anonymous IPC connection without having to set +``--input-ipc-server``. This is achieved through a mpv pseudo scripting backend +that starts processes. + +You can put ``.run`` file extension in the mpv scripts directory in its config +directory (see the `FILES`_ section for details), or load them through other +means (see `Script location`_). These scripts are simply executed with the OS +native mechanism (as if you ran them in the shell). They must have a proper +shebang and have the executable bit set. + +When executed, a socket (the IPC connection) is passed to them through file +descriptor inheritance. The file descriptor is indicated as the special command +line argument ``--mpv-ipc-fd=N``, where ``N`` is the numeric file descriptor. +Currently, this is hardcoded as ``--mpv-ipc-fd=3``, and the intention is that +it will always be ``3``. (This was a promise between keeping it as simple as +possible, and not doing too much implicitly. Also, since there is a chance that +this will change anyway, you should at least validate that you got the expected +argument.) + +The rest is the same as with a normal ``--input-ipc-server`` IPC connection. mpv +does not attempt to observe or other interact with the started script process. + +This does not work in Windows yet. diff --git a/input/input.h b/input/input.h index df51cb7ed4..1a90a47d42 100644 --- a/input/input.h +++ b/input/input.h @@ -213,10 +213,20 @@ void mp_input_sdl_gamepad_add(struct input_ctx *ictx); struct mp_ipc_ctx; struct mp_client_api; +struct mpv_handle; // Platform specific implementation, provided by ipc-*.c. struct mp_ipc_ctx *mp_init_ipc(struct mp_client_api *client_api, struct mpv_global *global); +// Start a thread for the given handle and return a socket in out_fd[0] that +// is served by this thread. If the FD is not full-duplex, then out_fd[0] is +// the user's read-end, and out_fd[1] the write-end, otherwise out_fd[1] is set +// to -1. +// returns: +// true: out_fd[0] and out_fd[1] are set, ownership of h is transferred +// false: out_fd are not touched, caller retains ownership of h +bool mp_ipc_start_anon_client(struct mp_ipc_ctx *ctx, struct mpv_handle *h, + int out_fd[2]); void mp_uninit_ipc(struct mp_ipc_ctx *ctx); // Serialize the given mpv_event structure to JSON. Returns an allocated string. diff --git a/input/ipc-dummy.c b/input/ipc-dummy.c index d9c31c046c..f0232b2f6e 100644 --- a/input/ipc-dummy.c +++ b/input/ipc-dummy.c @@ -8,6 +8,12 @@ struct mp_ipc_ctx *mp_init_ipc(struct mp_client_api *client_api, return NULL; } +bool mp_ipc_start_anon_client(struct mp_ipc_ctx *ctx, struct mpv_handle *h, + int out_fd[2]) +{ + return false; +} + void mp_uninit_ipc(struct mp_ipc_ctx *ctx) { } diff --git a/input/ipc-unix.c b/input/ipc-unix.c index ef478ba35e..0a7f2a5838 100644 --- a/input/ipc-unix.c +++ b/input/ipc-unix.c @@ -58,7 +58,7 @@ struct client_arg { struct mp_log *log; struct mpv_handle *client; - char *client_name; + const char *client_name; int client_fd; bool close_client_fd; @@ -215,9 +215,11 @@ done: return NULL; } -static void ipc_start_client(struct mp_ipc_ctx *ctx, struct client_arg *client) +static bool ipc_start_client(struct mp_ipc_ctx *ctx, struct client_arg *client, + bool free_on_init_fail) { - client->client = mp_new_client(ctx->client_api, client->client_name); + if (!client->client) + client->client = mp_new_client(ctx->client_api, client->client_name); if (!client->client) goto err; @@ -227,16 +229,19 @@ static void ipc_start_client(struct mp_ipc_ctx *ctx, struct client_arg *client) if (pthread_create(&client_thr, NULL, client_thread, client)) goto err; - return; + return true; err: - if (client->client) - mpv_destroy(client->client); + if (free_on_init_fail) { + if (client->client) + mpv_destroy(client->client); - if (client->close_client_fd) - close(client->client_fd); + if (client->close_client_fd) + close(client->client_fd); + } talloc_free(client); + return false; } static void ipc_start_client_json(struct mp_ipc_ctx *ctx, int id, int fd) @@ -246,11 +251,37 @@ static void ipc_start_client_json(struct mp_ipc_ctx *ctx, int id, int fd) .client_name = talloc_asprintf(client, "ipc-%d", id), .client_fd = fd, .close_client_fd = true, - .writable = true, }; - ipc_start_client(ctx, client); + ipc_start_client(ctx, client, true); +} + +bool mp_ipc_start_anon_client(struct mp_ipc_ctx *ctx, struct mpv_handle *h, + int out_fd[2]) +{ + int pair[2]; + if (socketpair(AF_UNIX, SOCK_STREAM, 0, pair)) + return false; + + struct client_arg *client = talloc_ptrtype(NULL, client); + *client = (struct client_arg){ + .client = h, + .client_name = mpv_client_name(h), + .client_fd = pair[1], + .close_client_fd = true, + .writable = true, + }; + + if (!ipc_start_client(ctx, client, false)) { + close(pair[0]); + close(pair[1]); + return false; + } + + out_fd[0] = pair[0]; + out_fd[1] = -1; + return true; } static void ipc_start_client_text(struct mp_ipc_ctx *ctx, const char *path) @@ -292,7 +323,7 @@ static void ipc_start_client_text(struct mp_ipc_ctx *ctx, const char *path) .writable = writable, }; - ipc_start_client(ctx, client); + ipc_start_client(ctx, client, true); } static void *ipc_thread(void *p) diff --git a/input/ipc-win.c b/input/ipc-win.c index 727a8cca73..9672ec85fe 100644 --- a/input/ipc-win.c +++ b/input/ipc-win.c @@ -335,6 +335,12 @@ static void ipc_start_client_json(struct mp_ipc_ctx *ctx, int id, HANDLE h) ipc_start_client(ctx, client); } +bool mp_ipc_start_anon_client(struct mp_ipc_ctx *ctx, struct mpv_handle *h, + int out_fd[2]) +{ + return false; +} + static void *ipc_thread(void *p) { // Use PIPE_TYPE_MESSAGE | PIPE_READMODE_BYTE so message framing is diff --git a/osdep/subprocess.c b/osdep/subprocess.c index 8bb2acd0b7..8a930a60df 100644 --- a/osdep/subprocess.c +++ b/osdep/subprocess.c @@ -123,6 +123,12 @@ void mp_subprocess_detached(struct mp_log *log, char **args) talloc_free(p); } +void mp_subprocess2(struct mp_subprocess_opts *opts, + struct mp_subprocess_result *res) +{ + *res = (struct mp_subprocess_result){.error = MP_SUBPROCESS_EUNSUPPORTED}; +} + #endif const char *mp_subprocess_err_str(int num) diff --git a/player/core.h b/player/core.h index 70fc33448e..d669d2d031 100644 --- a/player/core.h +++ b/player/core.h @@ -630,6 +630,7 @@ struct mp_script_args { struct mp_scripting { const char *name; // e.g. "lua script" const char *file_ext; // e.g. "lua" + bool no_thread; // don't run load() on dedicated thread int (*load)(struct mp_script_args *args); }; bool mp_load_scripts(struct MPContext *mpctx); diff --git a/player/scripting.c b/player/scripting.c index d83c4d241f..c42706a0b9 100644 --- a/player/scripting.c +++ b/player/scripting.c @@ -22,14 +22,17 @@ #include #include #include +#include #include "config.h" #include "osdep/io.h" +#include "osdep/subprocess.h" #include "osdep/threads.h" #include "common/common.h" #include "common/msg.h" +#include "input/input.h" #include "options/m_config.h" #include "options/parse_configfile.h" #include "options/path.h" @@ -41,6 +44,7 @@ extern const struct mp_scripting mp_scripting_lua; extern const struct mp_scripting mp_scripting_cplugin; extern const struct mp_scripting mp_scripting_js; +extern const struct mp_scripting mp_scripting_run; static const struct mp_scripting *const scripting_backends[] = { #if HAVE_LUA @@ -52,6 +56,7 @@ static const struct mp_scripting *const scripting_backends[] = { #if HAVE_JAVASCRIPT &mp_scripting_js, #endif + &mp_scripting_run, NULL }; @@ -76,12 +81,8 @@ static char *script_name_from_filename(void *talloc_ctx, const char *fname) return talloc_asprintf(talloc_ctx, "%s", name); } -static void *script_thread(void *p) +static void run_script(struct mp_script_args *arg) { - pthread_detach(pthread_self()); - - struct mp_script_args *arg = p; - char name[90]; snprintf(name, sizeof(name), "%s (%s)", arg->backend->name, mpv_client_name(arg->client)); @@ -92,6 +93,15 @@ static void *script_thread(void *p) mpv_destroy(arg->client); talloc_free(arg); +} + +static void *script_thread(void *p) +{ + pthread_detach(pthread_self()); + + struct mp_script_args *arg = p; + run_script(arg); + return NULL; } @@ -176,11 +186,15 @@ static int mp_load_script(struct MPContext *mpctx, const char *fname) MP_DBG(arg, "Loading %s %s...\n", backend->name, fname); - pthread_t thread; - if (pthread_create(&thread, NULL, script_thread, arg)) { - mpv_destroy(arg->client); - talloc_free(arg); - return -1; + if (backend->no_thread) { + run_script(arg); + } else { + pthread_t thread; + if (pthread_create(&thread, NULL, script_thread, arg)) { + mpv_destroy(arg->client); + talloc_free(arg); + return -1; + } } return 0; @@ -311,3 +325,58 @@ const struct mp_scripting mp_scripting_cplugin = { }; #endif + +static int load_run(struct mp_script_args *args) +{ + int fds[2]; + if (!mp_ipc_start_anon_client(args->mpctx->ipc_ctx, args->client, fds)) + return -1; + args->client = NULL; // ownership lost + + // Hardcode them (according to opts.fds[]), because we want to allow clients + // to hardcode them if they want. Sue me. + char *fdopt = fds[1] >= 0 ? "--mpv-ipc-fd=3:4" + : "--mpv-ipc-fd=3"; + + struct mp_subprocess_opts opts = { + .exe = (char *)args->filename, + .args = (char *[]){(char *)args->filename, fdopt, NULL}, + .fds = { + // Keep terminal stuff + {.fd = 0, .src_fd = 0,}, + {.fd = 1, .src_fd = 1,}, + {.fd = 2, .src_fd = 2,}, + // Just hope these don't step over each other (e.g. fds[1] is not + // below 4, if the std FDs are missing). + {.fd = 3, .src_fd = fds[0], }, + {.fd = 4, .src_fd = fds[1], }, + }, + .num_fds = fds[1] >= 0 ? 4 : 5, + .detach = true, + }; + struct mp_subprocess_result res; + mp_subprocess2(&opts, &res); + + // Closing these will (probably) make the client exit, if it really died. + // They _should_ be CLOEXEC, but are not, because + // posix_spawn_file_actions_adddup2() may not clear the CLOEXEC flag + // properly if by coincidence fd==src_fd. + close(fds[0]); + if (fds[1] >= 0) + close(fds[1]); + + if (res.error < 0) { + MP_ERR(args, "Starting '%s' failed: %s\n", args->filename, + mp_subprocess_err_str(res.error)); + return -1; + } + + return 0; +} + +const struct mp_scripting mp_scripting_run = { + .name = "spawned IPC process", + .file_ext = "run", + .no_thread = true, + .load = load_run, +};