Coverage Report

Created: 2026-11-11 06:43

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/src/mpv/player/scripting.c
Line
Count
Source
1
/*
2
 * This file is part of mpv.
3
 *
4
 * mpv is free software; you can redistribute it and/or
5
 * modify it under the terms of the GNU Lesser General Public
6
 * License as published by the Free Software Foundation; either
7
 * version 2.1 of the License, or (at your option) any later version.
8
 *
9
 * mpv is distributed in the hope that it will be useful,
10
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
11
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12
 * GNU Lesser General Public License for more details.
13
 *
14
 * You should have received a copy of the GNU Lesser General Public
15
 * License along with mpv.  If not, see <http://www.gnu.org/licenses/>.
16
 */
17
18
#include <string.h>
19
#include <sys/types.h>
20
#include <math.h>
21
#include <assert.h>
22
23
#include "config.h"
24
25
#include "osdep/io.h"
26
#include "osdep/subprocess.h"
27
#include "osdep/threads.h"
28
29
#include "common/common.h"
30
#include "common/msg.h"
31
#include "input/input.h"
32
#include "options/m_config.h"
33
#include "options/parse_configfile.h"
34
#include "options/path.h"
35
#include "misc/bstr.h"
36
#include "core.h"
37
#include "client.h"
38
#include "mpv/client.h"
39
#include "mpv/render.h"
40
#include "mpv/stream_cb.h"
41
42
extern const struct mp_scripting mp_scripting_lua;
43
extern const struct mp_scripting mp_scripting_cplugin;
44
extern const struct mp_scripting mp_scripting_js;
45
extern const struct mp_scripting mp_scripting_run;
46
47
static const struct mp_scripting *const scripting_backends[] = {
48
#if HAVE_LUA
49
    &mp_scripting_lua,
50
#endif
51
#if HAVE_CPLUGINS
52
    &mp_scripting_cplugin,
53
#endif
54
#if HAVE_JAVASCRIPT
55
    &mp_scripting_js,
56
#endif
57
    &mp_scripting_run,
58
    NULL
59
};
60
61
static char *script_name_from_filename(void *talloc_ctx, const char *fname)
62
5.33k
{
63
5.33k
    fname = mp_basename(fname);
64
5.33k
    if (fname[0] == '@')
65
189
        fname += 1;
66
5.33k
    char *name = talloc_strdup(talloc_ctx, fname);
67
    // Drop file extension
68
5.33k
    char *dot = strrchr(name, '.');
69
5.33k
    if (dot)
70
3.92k
        *dot = '\0';
71
    // Turn it into a safe identifier - this is used with e.g. dispatching
72
    // input via: "send scriptname ..."
73
40.0k
    for (int n = 0; name[n]; n++) {
74
34.7k
        char c = name[n];
75
34.7k
        if (!(c >= 'A' && c <= 'Z') && !(c >= 'a' && c <= 'z') &&
76
20.6k
            !(c >= '0' && c <= '9'))
77
17.2k
            name[n] = '_';
78
34.7k
    }
79
5.33k
    return talloc_asprintf(talloc_ctx, "%s", name);
80
5.33k
}
81
82
static void run_script(struct mp_script_args *arg)
83
3.16k
{
84
3.16k
    char *name = talloc_asprintf(NULL, "%s/%s", arg->backend->name,
85
3.16k
                                 mpv_client_name(arg->client));
86
3.16k
    mp_thread_set_name(name);
87
3.16k
    talloc_free(name);
88
89
3.16k
    if (arg->backend->load(arg) < 0)
90
3.16k
        MP_ERR(arg, "Could not load %s script %s\n", arg->backend->name, arg->filename);
91
92
3.16k
    mpv_handle *client = arg->client;
93
3.16k
    talloc_free(arg);
94
3.16k
    mpv_destroy(client);
95
3.16k
}
96
97
static MP_THREAD_VOID script_thread(void *p)
98
805
{
99
805
    struct mp_script_args *arg = p;
100
805
    run_script(arg);
101
102
805
    MP_THREAD_RETURN();
103
805
}
104
105
static int64_t mp_load_script(struct MPContext *mpctx, const char *fname)
106
5.93k
{
107
5.93k
    bstr ext = mp_get_ext(bstr0(fname));
108
5.93k
    if (bstrcasecmp0(ext, "disable") == 0)
109
167
        return 0;
110
111
5.77k
    void *tmp = talloc_new(NULL);
112
113
5.77k
    const char *path = NULL;
114
5.77k
    const char *script_name = NULL;
115
5.77k
    const struct mp_scripting *backend = NULL;
116
117
5.77k
    struct stat s;
118
5.77k
    if (!stat(fname, &s) && S_ISDIR(s.st_mode)) {
119
438
        path = fname;
120
438
        fname = NULL;
121
122
1.31k
        for (int n = 0; scripting_backends[n]; n++) {
123
876
            const struct mp_scripting *b = scripting_backends[n];
124
876
            char *filename = mp_tprintf(80, "main.%s", b->file_ext);
125
876
            fname = mp_path_join(tmp, path, filename);
126
876
            if (!stat(fname, &s) && S_ISREG(s.st_mode)) {
127
0
                backend = b;
128
0
                break;
129
0
            }
130
876
            talloc_free((void *)fname);
131
876
            fname = NULL;
132
876
        }
133
134
438
        if (!fname) {
135
438
            MP_ERR(mpctx, "Cannot find main.* for any supported scripting "
136
438
                   "backend in: %s\n", path);
137
438
            talloc_free(tmp);
138
438
            return -1;
139
438
        }
140
141
0
        char *path_dup = talloc_strdup(tmp, path);
142
0
        mp_path_strip_trailing_separator(path_dup);
143
0
        script_name = mp_basename(path_dup);
144
5.33k
    } else {
145
11.9k
        for (int n = 0; scripting_backends[n]; n++) {
146
9.86k
            const struct mp_scripting *b = scripting_backends[n];
147
9.86k
            if (bstrcasecmp0(ext, b->file_ext) == 0) {
148
3.22k
                backend = b;
149
3.22k
                break;
150
3.22k
            }
151
9.86k
        }
152
5.33k
        script_name = script_name_from_filename(tmp, fname);
153
5.33k
    }
154
155
5.33k
    if (!backend) {
156
2.10k
        MP_ERR(mpctx, "Can't load unknown script: %s\n", fname);
157
2.10k
        talloc_free(tmp);
158
2.10k
        return -1;
159
2.10k
    }
160
161
3.22k
    struct mp_script_args *arg = talloc_ptrtype(NULL, arg);
162
3.22k
    *arg = (struct mp_script_args){
163
3.22k
        .mpctx = mpctx,
164
3.22k
        .filename = talloc_strdup(arg, fname),
165
3.22k
        .path = talloc_strdup(arg, path),
166
3.22k
        .backend = backend,
167
        // Create the client before creating the thread; otherwise a race
168
        // condition could happen, where MPContext is destroyed while the
169
        // thread tries to create the client.
170
3.22k
        .client = mp_new_weak_client(mpctx->clients, script_name),
171
3.22k
    };
172
173
3.22k
    talloc_free(tmp);
174
3.22k
    fname = NULL; // might have been freed so don't touch anymore
175
176
3.22k
    if (!arg->client) {
177
66
        MP_ERR(mpctx, "Failed to create client for script: %s\n", arg->filename);
178
66
        talloc_free(arg);
179
66
        return -1;
180
66
    }
181
182
3.16k
    arg->log = mp_client_get_log(arg->client);
183
3.16k
    int64_t id = mpv_client_id(arg->client);
184
185
3.16k
    MP_DBG(arg, "Loading %s script %s...\n", backend->name, arg->filename);
186
187
3.16k
    if (backend->no_thread) {
188
2.35k
        run_script(arg);
189
2.35k
    } else {
190
805
        mp_thread thread;
191
805
        if (mp_thread_create(&thread, script_thread, arg)) {
192
0
            mpv_destroy(arg->client);
193
0
            talloc_free(arg);
194
0
            return -1;
195
0
        }
196
805
        mp_thread_detach(thread);
197
805
    }
198
199
3.16k
    return id;
200
3.16k
}
201
202
int64_t mp_load_user_script(struct MPContext *mpctx, const char *fname)
203
5.93k
{
204
5.93k
    char *path = mp_get_user_path(NULL, mpctx->global, fname);
205
5.93k
    int64_t ret = mp_load_script(mpctx, path);
206
5.93k
    talloc_free(path);
207
5.93k
    return ret;
208
5.93k
}
209
210
static int compare_filename(const void *pa, const void *pb)
211
0
{
212
0
    char *a = (char *)pa;
213
0
    char *b = (char *)pb;
214
0
    return strcasecmp(a, b);
215
0
}
216
217
static char **list_script_files(void *talloc_ctx, char *path)
218
0
{
219
0
    char **files = NULL;
220
0
    int count = 0;
221
0
    DIR *dp = opendir(path);
222
0
    if (!dp)
223
0
        return NULL;
224
0
    struct dirent *ep;
225
0
    while ((ep = readdir(dp))) {
226
0
        if (ep->d_name[0] != '.') {
227
0
            char *fname = mp_path_join(talloc_ctx, path, ep->d_name);
228
0
            struct stat s;
229
0
            if (!stat(fname, &s) && (S_ISREG(s.st_mode) || S_ISDIR(s.st_mode)))
230
0
                MP_TARRAY_APPEND(talloc_ctx, files, count, fname);
231
0
        }
232
0
    }
233
0
    closedir(dp);
234
0
    if (files)
235
0
        qsort(files, count, sizeof(char *), compare_filename);
236
0
    MP_TARRAY_APPEND(talloc_ctx, files, count, NULL);
237
0
    return files;
238
0
}
239
240
static void load_builtin_script(struct MPContext *mpctx, int slot, bool enable,
241
                                const char *fname)
242
1.91M
{
243
1.91M
    mp_assert(slot < MP_ARRAY_SIZE(mpctx->builtin_script_ids));
244
1.91M
    int64_t *pid = &mpctx->builtin_script_ids[slot];
245
1.91M
    if (*pid > 0 && !mp_client_id_exists(mpctx, *pid)) {
246
0
        MP_DBG(mpctx, "Client for script %s is no longer alive. Marking as unloaded.\n", fname);
247
0
        *pid = 0; // died
248
0
    }
249
1.91M
    if ((*pid > 0) != enable) {
250
0
        if (enable) {
251
0
            *pid = mp_load_script(mpctx, fname);
252
0
        } else {
253
0
            char *name = mp_tprintf(22, "@%"PRIi64, *pid);
254
0
            MP_DBG(mpctx, "Unloading script %s (disabled by option)\n", fname);
255
0
            mp_client_send_event(mpctx, name, 0, MPV_EVENT_SHUTDOWN, NULL);
256
            // note: there is no synchronization of script exit
257
0
        }
258
0
    }
259
1.91M
}
260
261
void mp_load_builtin_scripts(struct MPContext *mpctx)
262
212k
{
263
212k
    load_builtin_script(mpctx, 0, mpctx->opts->lua_load_osc, "@osc.lua");
264
212k
    load_builtin_script(mpctx, 1, mpctx->opts->lua_load_ytdl, "@ytdl_hook.lua");
265
212k
    load_builtin_script(mpctx, 2, mpctx->opts->lua_load_stats, "@stats.lua");
266
212k
    load_builtin_script(mpctx, 3, mpctx->opts->lua_load_console, "@console.lua");
267
212k
    load_builtin_script(mpctx, 4, mpctx->opts->lua_load_auto_profiles,
268
212k
                        "@auto_profiles.lua");
269
212k
    load_builtin_script(mpctx, 5, mpctx->opts->lua_load_select, "@select.lua");
270
212k
    load_builtin_script(mpctx, 6, mpctx->opts->lua_load_positioning, "@positioning.lua");
271
212k
    load_builtin_script(mpctx, 7, mpctx->opts->lua_load_commands, "@commands.lua");
272
273
    // select.lua uses context_menu.lua in D3D11 exclusive fullscreen.
274
212k
    struct m_config_option *co = m_config_get_co(mpctx->mconfig, bstr0("d3d11-exclusive-fs"));
275
212k
    bool context_menu = mpctx->opts->lua_load_context_menu || (co && *(bool *)co->data);
276
212k
    load_builtin_script(mpctx, 8, context_menu, "@context_menu.lua");
277
212k
}
278
279
bool mp_load_scripts(struct MPContext *mpctx)
280
207k
{
281
207k
    bool ok = true;
282
283
    // Load scripts from options
284
207k
    char **files = mpctx->opts->script_files;
285
213k
    for (int n = 0; files && files[n]; n++) {
286
6.16k
        if (files[n][0])
287
5.93k
            ok &= mp_load_user_script(mpctx, files[n]) >= 0;
288
6.16k
    }
289
207k
    if (!mpctx->opts->auto_load_scripts)
290
1
        return ok;
291
292
    // Load all scripts
293
207k
    void *tmp = talloc_new(NULL);
294
207k
    char **scriptsdir = mp_find_all_config_files(tmp, mpctx->global, "scripts");
295
207k
    for (int i = 0; scriptsdir && scriptsdir[i]; i++) {
296
0
        files = list_script_files(tmp, scriptsdir[i]);
297
0
        for (int n = 0; files && files[n]; n++)
298
0
            ok &= mp_load_script(mpctx, files[n]) >= 0;
299
0
    }
300
207k
    talloc_free(tmp);
301
302
207k
    return ok;
303
207k
}
304
305
#if HAVE_CPLUGINS
306
307
#ifndef _WIN32
308
#include <dlfcn.h>
309
#endif
310
311
0
#define MPV_DLOPEN_FN "mpv_open_cplugin"
312
typedef int (*mpv_open_cplugin)(mpv_handle *handle);
313
314
0
static void init_sym_table(struct mp_script_args *args, void *lib) {
315
0
#define INIT_SYM(name)                                                         \
316
0
    {                                                                          \
317
0
        void **sym = (void **)dlsym(lib, "pfn_" #name);                        \
318
0
        if (sym) {                                                             \
319
0
            if (*sym && *sym != &name)                                         \
320
0
                MP_ERR(args, "Overriding already set function " #name "\n");   \
321
0
            *sym = &name;                                                      \
322
0
        }                                                                      \
323
0
    }
324
325
0
    INIT_SYM(mpv_client_api_version);
326
0
    INIT_SYM(mpv_error_string);
327
0
    INIT_SYM(mpv_free);
328
0
    INIT_SYM(mpv_client_name);
329
0
    INIT_SYM(mpv_client_id);
330
0
    INIT_SYM(mpv_create);
331
0
    INIT_SYM(mpv_initialize);
332
0
    INIT_SYM(mpv_destroy);
333
0
    INIT_SYM(mpv_terminate_destroy);
334
0
    INIT_SYM(mpv_create_client);
335
0
    INIT_SYM(mpv_create_weak_client);
336
0
    INIT_SYM(mpv_load_config_file);
337
0
    INIT_SYM(mpv_get_time_ns);
338
0
    INIT_SYM(mpv_get_time_us);
339
0
    INIT_SYM(mpv_free_node_contents);
340
0
    INIT_SYM(mpv_set_option);
341
0
    INIT_SYM(mpv_set_option_string);
342
0
    INIT_SYM(mpv_command);
343
0
    INIT_SYM(mpv_command_node);
344
0
    INIT_SYM(mpv_command_ret);
345
0
    INIT_SYM(mpv_command_string);
346
0
    INIT_SYM(mpv_command_async);
347
0
    INIT_SYM(mpv_command_node_async);
348
0
    INIT_SYM(mpv_abort_async_command);
349
0
    INIT_SYM(mpv_set_property);
350
0
    INIT_SYM(mpv_set_property_string);
351
0
    INIT_SYM(mpv_del_property);
352
0
    INIT_SYM(mpv_set_property_async);
353
0
    INIT_SYM(mpv_get_property);
354
0
    INIT_SYM(mpv_get_property_string);
355
0
    INIT_SYM(mpv_get_property_osd_string);
356
0
    INIT_SYM(mpv_get_property_async);
357
0
    INIT_SYM(mpv_observe_property);
358
0
    INIT_SYM(mpv_unobserve_property);
359
0
    INIT_SYM(mpv_event_name);
360
0
    INIT_SYM(mpv_event_to_node);
361
0
    INIT_SYM(mpv_request_event);
362
0
    INIT_SYM(mpv_request_log_messages);
363
0
    INIT_SYM(mpv_wait_event);
364
0
    INIT_SYM(mpv_wakeup);
365
0
    INIT_SYM(mpv_set_wakeup_callback);
366
0
    INIT_SYM(mpv_wait_async_requests);
367
0
    INIT_SYM(mpv_hook_add);
368
0
    INIT_SYM(mpv_hook_continue);
369
0
    INIT_SYM(mpv_get_wakeup_pipe);
370
371
0
    INIT_SYM(mpv_render_context_create);
372
0
    INIT_SYM(mpv_render_context_set_parameter);
373
0
    INIT_SYM(mpv_render_context_get_info);
374
0
    INIT_SYM(mpv_render_context_set_update_callback);
375
0
    INIT_SYM(mpv_render_context_update);
376
0
    INIT_SYM(mpv_render_context_render);
377
0
    INIT_SYM(mpv_render_context_report_swap);
378
0
    INIT_SYM(mpv_render_context_free);
379
380
0
    INIT_SYM(mpv_stream_cb_add_ro);
381
382
0
#undef INIT_SYM
383
0
}
384
385
static int load_cplugin(struct mp_script_args *args)
386
805
{
387
805
    void *lib = dlopen(args->filename, RTLD_NOW | RTLD_LOCAL);
388
805
    if (!lib)
389
805
        goto error;
390
    // Note: once loaded, we never unload, as unloading the libraries linked to
391
    //       the plugin can cause random serious problems.
392
0
    mpv_open_cplugin sym = (mpv_open_cplugin)dlsym(lib, MPV_DLOPEN_FN);
393
0
    if (!sym)
394
0
        goto error;
395
396
0
    init_sym_table(args, lib);
397
398
0
    return sym(args->client) ? -1 : 0;
399
805
error: ;
400
805
    char *err = dlerror();
401
805
    if (err)
402
805
        MP_ERR(args, "C plugin error: '%s'\n", err);
403
805
    return -1;
404
0
}
405
406
const struct mp_scripting mp_scripting_cplugin = {
407
    .name = "cplugin",
408
    #ifdef _WIN32
409
    .file_ext = "dll",
410
    #else
411
    .file_ext = "so",
412
    #endif
413
    .load = load_cplugin,
414
};
415
416
#endif
417
418
static int load_run(struct mp_script_args *args)
419
2.35k
{
420
    // The arg->client object might die and with it args->log, so duplicate it.
421
2.35k
    args->log = mp_log_new(args, args->log, NULL);
422
423
2.35k
    int fds[2];
424
2.35k
    if (!mp_ipc_start_anon_client(args->mpctx->ipc_ctx, args->client, fds))
425
0
        return -1;
426
2.35k
    args->client = NULL; // ownership lost
427
428
2.35k
    char *fdopt = fds[1] >= 0 ? mp_tprintf(80, "--mpv-ipc-fd=%d:%d", fds[0], fds[1])
429
2.35k
                              : mp_tprintf(80, "--mpv-ipc-fd=%d", fds[0]);
430
431
2.35k
    struct mp_subprocess_opts opts = {
432
2.35k
        .exe = (char *)args->filename,
433
2.35k
        .args = (char *[]){(char *)args->filename, fdopt, NULL},
434
2.35k
        .fds = {
435
            // Keep terminal stuff
436
2.35k
            {.fd = 0, .src_fd = 0,},
437
2.35k
            {.fd = 1, .src_fd = 1,},
438
2.35k
            {.fd = 2, .src_fd = 2,},
439
            // Just hope these don't step over each other (e.g. fds[1] could be
440
            // below 4, if the std FDs are missing).
441
2.35k
            {.fd = fds[0], .src_fd = fds[0], },
442
2.35k
            {.fd = fds[1], .src_fd = fds[1], },
443
2.35k
        },
444
2.35k
        .num_fds = fds[1] >= 0 ? 5 : 4,
445
2.35k
        .detach = true,
446
2.35k
    };
447
2.35k
    struct mp_subprocess_result res;
448
2.35k
    mp_subprocess(args->log, &opts, &res);
449
450
    // Closing these will (probably) make the client exit, if it really died.
451
2.35k
    close(fds[0]);
452
2.35k
    if (fds[1] >= 0)
453
0
        close(fds[1]);
454
455
2.35k
    return res.error;
456
2.35k
}
457
458
const struct mp_scripting mp_scripting_run = {
459
    .name = "ipc",
460
    .file_ext = "run",
461
    .no_thread = true,
462
    .load = load_run,
463
};