/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 | | }; |