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.
This commit is contained in:
wm4 2020-02-19 22:18:15 +01:00
parent d06ebe2251
commit e2ab6b7f35
8 changed files with 177 additions and 21 deletions

View File

@ -297,3 +297,30 @@ Is equivalent to:
:: ::
{ "objkey": "value\n" } { "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.

View File

@ -213,10 +213,20 @@ void mp_input_sdl_gamepad_add(struct input_ctx *ictx);
struct mp_ipc_ctx; struct mp_ipc_ctx;
struct mp_client_api; struct mp_client_api;
struct mpv_handle;
// Platform specific implementation, provided by ipc-*.c. // Platform specific implementation, provided by ipc-*.c.
struct mp_ipc_ctx *mp_init_ipc(struct mp_client_api *client_api, struct mp_ipc_ctx *mp_init_ipc(struct mp_client_api *client_api,
struct mpv_global *global); 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); void mp_uninit_ipc(struct mp_ipc_ctx *ctx);
// Serialize the given mpv_event structure to JSON. Returns an allocated string. // Serialize the given mpv_event structure to JSON. Returns an allocated string.

View File

@ -8,6 +8,12 @@ struct mp_ipc_ctx *mp_init_ipc(struct mp_client_api *client_api,
return NULL; 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) void mp_uninit_ipc(struct mp_ipc_ctx *ctx)
{ {
} }

View File

@ -58,7 +58,7 @@ struct client_arg {
struct mp_log *log; struct mp_log *log;
struct mpv_handle *client; struct mpv_handle *client;
char *client_name; const char *client_name;
int client_fd; int client_fd;
bool close_client_fd; bool close_client_fd;
@ -215,8 +215,10 @@ done:
return NULL; 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)
{ {
if (!client->client)
client->client = mp_new_client(ctx->client_api, client->client_name); client->client = mp_new_client(ctx->client_api, client->client_name);
if (!client->client) if (!client->client)
goto err; 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)) if (pthread_create(&client_thr, NULL, client_thread, client))
goto err; goto err;
return; return true;
err: err:
if (free_on_init_fail) {
if (client->client) if (client->client)
mpv_destroy(client->client); mpv_destroy(client->client);
if (client->close_client_fd) if (client->close_client_fd)
close(client->client_fd); close(client->client_fd);
}
talloc_free(client); talloc_free(client);
return false;
} }
static void ipc_start_client_json(struct mp_ipc_ctx *ctx, int id, int fd) 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_name = talloc_asprintf(client, "ipc-%d", id),
.client_fd = fd, .client_fd = fd,
.close_client_fd = true, .close_client_fd = true,
.writable = 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) 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, .writable = writable,
}; };
ipc_start_client(ctx, client); ipc_start_client(ctx, client, true);
} }
static void *ipc_thread(void *p) static void *ipc_thread(void *p)

View File

@ -335,6 +335,12 @@ static void ipc_start_client_json(struct mp_ipc_ctx *ctx, int id, HANDLE h)
ipc_start_client(ctx, client); 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) static void *ipc_thread(void *p)
{ {
// Use PIPE_TYPE_MESSAGE | PIPE_READMODE_BYTE so message framing is // Use PIPE_TYPE_MESSAGE | PIPE_READMODE_BYTE so message framing is

View File

@ -123,6 +123,12 @@ void mp_subprocess_detached(struct mp_log *log, char **args)
talloc_free(p); 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 #endif
const char *mp_subprocess_err_str(int num) const char *mp_subprocess_err_str(int num)

View File

@ -630,6 +630,7 @@ struct mp_script_args {
struct mp_scripting { struct mp_scripting {
const char *name; // e.g. "lua script" const char *name; // e.g. "lua script"
const char *file_ext; // e.g. "lua" const char *file_ext; // e.g. "lua"
bool no_thread; // don't run load() on dedicated thread
int (*load)(struct mp_script_args *args); int (*load)(struct mp_script_args *args);
}; };
bool mp_load_scripts(struct MPContext *mpctx); bool mp_load_scripts(struct MPContext *mpctx);

View File

@ -22,14 +22,17 @@
#include <math.h> #include <math.h>
#include <pthread.h> #include <pthread.h>
#include <assert.h> #include <assert.h>
#include <unistd.h>
#include "config.h" #include "config.h"
#include "osdep/io.h" #include "osdep/io.h"
#include "osdep/subprocess.h"
#include "osdep/threads.h" #include "osdep/threads.h"
#include "common/common.h" #include "common/common.h"
#include "common/msg.h" #include "common/msg.h"
#include "input/input.h"
#include "options/m_config.h" #include "options/m_config.h"
#include "options/parse_configfile.h" #include "options/parse_configfile.h"
#include "options/path.h" #include "options/path.h"
@ -41,6 +44,7 @@
extern const struct mp_scripting mp_scripting_lua; extern const struct mp_scripting mp_scripting_lua;
extern const struct mp_scripting mp_scripting_cplugin; extern const struct mp_scripting mp_scripting_cplugin;
extern const struct mp_scripting mp_scripting_js; extern const struct mp_scripting mp_scripting_js;
extern const struct mp_scripting mp_scripting_run;
static const struct mp_scripting *const scripting_backends[] = { static const struct mp_scripting *const scripting_backends[] = {
#if HAVE_LUA #if HAVE_LUA
@ -52,6 +56,7 @@ static const struct mp_scripting *const scripting_backends[] = {
#if HAVE_JAVASCRIPT #if HAVE_JAVASCRIPT
&mp_scripting_js, &mp_scripting_js,
#endif #endif
&mp_scripting_run,
NULL NULL
}; };
@ -76,12 +81,8 @@ static char *script_name_from_filename(void *talloc_ctx, const char *fname)
return talloc_asprintf(talloc_ctx, "%s", name); 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]; char name[90];
snprintf(name, sizeof(name), "%s (%s)", arg->backend->name, snprintf(name, sizeof(name), "%s (%s)", arg->backend->name,
mpv_client_name(arg->client)); mpv_client_name(arg->client));
@ -92,6 +93,15 @@ static void *script_thread(void *p)
mpv_destroy(arg->client); mpv_destroy(arg->client);
talloc_free(arg); talloc_free(arg);
}
static void *script_thread(void *p)
{
pthread_detach(pthread_self());
struct mp_script_args *arg = p;
run_script(arg);
return NULL; return NULL;
} }
@ -176,12 +186,16 @@ static int mp_load_script(struct MPContext *mpctx, const char *fname)
MP_DBG(arg, "Loading %s %s...\n", backend->name, fname); MP_DBG(arg, "Loading %s %s...\n", backend->name, fname);
if (backend->no_thread) {
run_script(arg);
} else {
pthread_t thread; pthread_t thread;
if (pthread_create(&thread, NULL, script_thread, arg)) { if (pthread_create(&thread, NULL, script_thread, arg)) {
mpv_destroy(arg->client); mpv_destroy(arg->client);
talloc_free(arg); talloc_free(arg);
return -1; return -1;
} }
}
return 0; return 0;
} }
@ -311,3 +325,58 @@ const struct mp_scripting mp_scripting_cplugin = {
}; };
#endif #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,
};