1215 lines · 37.3 KB · C
RawDownload
/* * This file is part of mpv. * * mpv is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 2.1 of the License, or (at your option) any later version. * * mpv is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with mpv.  If not, see <http://www.gnu.org/licenses/>. */ #include <assert.h>#include <string.h>#include <strings.h>#include <sys/types.h>#include <sys/stat.h>#include <unistd.h>#include <dirent.h>#include <math.h>#include <stdint.h> #include <mujs.h> #include "osdep/io.h"#include "mpv_talloc.h"#include "common/common.h"#include "options/m_property.h"#include "common/msg.h"#include "common/msg_control.h"#include "options/m_option.h"#include "input/input.h"#include "options/path.h"#include "misc/bstr.h"#include "osdep/timer.h"#include "osdep/threads.h"#include "osdep/getpid.h"#include "stream/stream.h"#include "sub/osd.h"#include "core.h"#include "command.h"#include "client.h"#include "libmpv/client.h" // List of builtin modules and their contents as strings.// All these are generated from player/javascript/*.jsstatic const char *const builtin_files[][3] = {    {"@/defaults.js",#   include "generated/player/javascript/defaults.js.inc"    },    {0}}; // Represents a loaded script. Each has its own js state.struct script_ctx {    const char *filename;    const char *path; // NULL if single file    struct mpv_handle *client;    struct MPContext *mpctx;    struct mp_log *log;    char *last_error_str;}; static struct script_ctx *jctx(js_State *J){    return (struct script_ctx *)js_getcontext(J);} static mpv_handle *jclient(js_State *J){    return jctx(J)->client;} static void pushnode(js_State *J, mpv_node *node);static void makenode(void *ta_ctx, mpv_node *dst, js_State *J, int idx);static int jsL_checkint(js_State *J, int idx);static uint64_t jsL_checkuint64(js_State *J, int idx); /********************************************************************** *  conventions, MuJS notes and vm errors *********************************************************************/// - push_foo functions are called from C and push a value to the vm stack.//// - JavaScript C functions are code which the vm can call as a js function.//   By convention, script_bar and script__baz are js C functions. The former//   is exposed to end users as bar, and _baz is for internal use.//// - js C functions get a fresh vm stack with their arguments, and may//   manipulate their stack as they see fit. On exit, the vm considers the//   top value of their stack as their return value, and GC the rest.//// - js C function's stack[0] is "this", and the rest (1, 2, ...) are the args.//   On entry the stack has at least the number of args defined for the func,//   padded with undefined if called with less, or bigger if called with more.//// - Almost all vm APIs (js_*) may throw an error - a longjmp to the last//   recovery/catch point, which could skip releasing resources. Use protected//   code (e.g. js_pcall) between aquisition and release. Alternatively, use//   the autofree mechanism to manage it more easily. See more details below.//// - Unless named s_foo, all the functions at this file (inc. init) which//   touch the vm may throw, but either cleanup resources regardless (mostly//   autofree) or leave allocated resources on caller-provided talloc context//   which the caller should release, typically with autofree (e.g. makenode).//// - Functions named s_foo (safe foo) never throw, return 0 on success, else 1. /********************************************************************** *  mpv scripting API error handling *********************************************************************/// - Errors may be thrown on some cases - the reason is at the exception.//// - Some APIs also set last error which can be fetched with mp.last_error(),//   where empty string (false-y) is success, or an error string otherwise.//// - The rest of the APIs are guaranteed to return undefined on error or a//   true-thy value on success and may or may not set last error.//// - push_success, push_failure, push_status and pushed_error set last error. // iserr as true indicates an error, and if so, str may indicate a reason.// Internally ctx->last_error_str is never NULL, and empty indicates success.static void set_last_error(struct script_ctx *ctx, bool iserr, const char *str){    ctx->last_error_str[0] = 0;    if (!iserr)        return;    if (!str || !str[0])        str = "Error";    ctx->last_error_str = talloc_strdup_append(ctx->last_error_str, str);} // For use only by wrappers at defaults.js.// arg: error string. Use empty string to indicate success.static void script__set_last_error(js_State *J){    const char *e = js_tostring(J, 1);    set_last_error(jctx(J), e[0], e);} // mp.last_error() . args: none. return the last error without modifying it.static void script_last_error(js_State *J){    js_pushstring(J, jctx(J)->last_error_str);} // Generic success for APIs which don't return an actual value.static void push_success(js_State *J){    set_last_error(jctx(J), 0, NULL);    js_pushboolean(J, true);} // Doesn't (intentionally) throw. Just sets last_error and pushes undefinedstatic void push_failure(js_State *J, const char *str){    set_last_error(jctx(J), 1, str);    js_pushundefined(J);} // Most of the scripting APIs are either sending some values and getting status// code in return, or requesting some value while providing a default in case an// error happened. These simplify the C code for that and always set last_error. static void push_status(js_State *J, int err){    if (err >= 0) {        push_success(J);    } else {        push_failure(J, mpv_error_string(err));    }}  // If err is success then return 0, else push the item at def and return 1static bool pushed_error(js_State *J, int err, int def){    bool iserr = err < 0;    set_last_error(jctx(J), iserr, iserr ? mpv_error_string(err) : NULL);    if (!iserr)        return false;     js_copy(J, def);    return true;} /********************************************************************** *  Autofree - care-free resource deallocation on vm errors, and otherwise *********************************************************************/// - Autofree (af) functions are called with a talloc context argument which is//   freed after the function exits - either normally or because it threw an//   error, on the latter case it then re-throws the error after the cleanup.////   Autofree js C functions should have an additional void* talloc arg and//   inserted into the vm using af_newcfunction, but otherwise used normally.////  To wrap an autofree function af_TARGET in C://  1. Create a wrapper s_TARGET which runs af_TARGET safely inside js_try.//  2. Use s_TARGET like so (always autofree, and throws if af_TARGET threw)://       void *af = talloc_new(NULL);//       int r = s_TARGET(J, ..., af);  // use J, af where the callee expects.//       talloc_free(af);//       if (r)//           js_throw(J); // add_af_file, add_af_dir, add_af_mpv_alloc take a valid FILE*/DIR*/char* value// respectively, and fclose/closedir/mpv_free it when the parent is freed. static void destruct_af_file(void *p){    fclose(*(FILE**)p);} static void add_af_file(void *parent, FILE *f){    FILE **pf = talloc(parent, FILE*);    *pf = f;    talloc_set_destructor(pf, destruct_af_file);} static void destruct_af_dir(void *p){    closedir(*(DIR**)p);} static void add_af_dir(void *parent, DIR *d){    DIR **pd = talloc(parent, DIR*);    *pd = d;    talloc_set_destructor(pd, destruct_af_dir);} static void destruct_af_mpv_alloc(void *p){    mpv_free(*(char**)p);} static void add_af_mpv_alloc(void *parent, char *ma){    char **p = talloc(parent, char*);    *p = ma;    talloc_set_destructor(p, destruct_af_mpv_alloc);} static void destruct_af_mpv_node(void *p){    mpv_free_node_contents((mpv_node*)p);  // does nothing for MPV_FORMAT_NONE} // returns a new zeroed allocated struct mpv_node, and free it and its content// when the parent is freed.static mpv_node *new_af_mpv_node(void *parent){    mpv_node *p = talloc_zero(parent, mpv_node);  // .format == MPV_FORMAT_NONE    talloc_set_destructor(p, destruct_af_mpv_node);    return p;} // Prototype for autofree functions which can be called from inside the vm.typedef void (*af_CFunction)(js_State*, void*); // safely run autofree js c function directlystatic int s_run_af_jsc(js_State *J, af_CFunction fn, void *af){    if (js_try(J))        return 1;    fn(J, af);    js_endtry(J);    return 0;} // The trampoline function through which all autofree functions are called from// inside the vm. Obtains the target function address and autofree-call it.static void script__autofree(js_State *J){    // The target function is at the "af_" property of this function instance.    js_currentfunction(J);    js_getproperty(J, -1, "af_");    af_CFunction fn = (af_CFunction)js_touserdata(J, -1, "af_fn");    js_pop(J, 2);     void *af = talloc_new(NULL);    int r = s_run_af_jsc(J, fn, af);    talloc_free(af);    if (r)        js_throw(J);} // Identical to js_newcfunction, but the function is inserted with an autofree// wrapper, and its prototype should have the additional af argument.static void af_newcfunction(js_State *J, af_CFunction fn, const char *name,                            int length){    js_newcfunction(J, script__autofree, name, length);    js_pushnull(J);  // a prototype for the userdata object    js_newuserdata(J, "af_fn", fn, NULL);  // uses a "af_fn" verification tag    js_defproperty(J, -2, "af_", JS_READONLY | JS_DONTENUM | JS_DONTCONF);} /********************************************************************** *  Initialization and file loading *********************************************************************/ static const char *get_builtin_file(const char *name){    for (int n = 0; builtin_files[n][0]; n++) {        if (strcmp(builtin_files[n][0], name) == 0)            return builtin_files[n][1];    }    return NULL;} // Push up to limit bytes of file fname: from builtin_files, else from the OS.static void af_push_file(js_State *J, const char *fname, int limit, void *af){    char *filename = mp_get_user_path(af, jctx(J)->mpctx->global, fname);    MP_VERBOSE(jctx(J), "Reading file '%s'\n", filename);    if (limit < 0)        limit = INT_MAX - 1;     const char *builtin = get_builtin_file(filename);    if (builtin) {        js_pushlstring(J, builtin, MPMIN(limit, strlen(builtin)));        return;    }     FILE *f = fopen(filename, "rb");    if (!f)        js_error(J, "cannot open file: '%s'", filename);    add_af_file(af, f);     int len = MPMIN(limit, 32 * 1024);  // initial allocation, size*2 strategy    int got = 0;    char *s = NULL;    while ((s = talloc_realloc(af, s, char, len))) {        int want = len - got;        int r = fread(s + got, 1, want, f);         if (feof(f) || (len == limit && r == want)) {            js_pushlstring(J, s, got + r);            return;        }        if (r != want)            js_error(J, "cannot read data from file: '%s'", filename);         got = got + r;        len = MPMIN(limit, len * 2);    }     js_error(J, "cannot allocate %d bytes for file: '%s'", len, filename);} // Safely run af_push_file.static int s_push_file(js_State *J, const char *fname, int limit, void *af){    if (js_try(J))        return 1;    af_push_file(J, fname, limit, af);    js_endtry(J);    return 0;} // Called directly, push up to limit bytes of file fname (from builtin/os).static void push_file_content(js_State *J, const char *fname, int limit){    void *af = talloc_new(NULL);    int r = s_push_file(J, fname, limit, af);    talloc_free(af);    if (r)        js_throw(J);} // utils.read_file(..). args: fname [,max]. returns [up to max] bytes as string.static void script_read_file(js_State *J){    int limit = js_isundefined(J, 2) ? -1 : jsL_checkint(J, 2);    push_file_content(J, js_tostring(J, 1), limit);} // Runs a file with the caller's this, leaves the stack as is.static void run_file(js_State *J, const char *fname){    MP_VERBOSE(jctx(J), "Loading file %s\n", fname);    push_file_content(J, fname, -1);    js_loadstring(J, fname, js_tostring(J, -1));    js_copy(J, 0);  // use the caller's this    js_call(J, 0);    js_pop(J, 2);  // result, file content} // The spec defines .name and .message for Error objects. Most engines also set// a very convenient .stack = name + message + trace, but MuJS instead sets// .stackTrace = trace only. Normalize by adding such .stack if required.// Run this before anything such that we can get traces on any following errors.static const char *norm_err_proto_js = "\    if (Error().stackTrace && !Error().stack) {\        Object.defineProperty(Error.prototype, 'stack', {\            get: function() {\                return this.name + ': ' + this.message + this.stackTrace;\            }\        });\    }\"; static void add_functions(js_State*, struct script_ctx*); // args: none. called as script, setup and run the main scriptstatic void script__run_script(js_State *J){    js_loadstring(J, "@/norm_err.js", norm_err_proto_js);    js_copy(J, 0);    js_pcall(J, 0);     struct script_ctx *ctx = jctx(J);    add_functions(J, ctx);    run_file(J, "@/defaults.js");    run_file(J, ctx->filename);  // the main file to run     if (!js_hasproperty(J, 0, "mp_event_loop") || !js_iscallable(J, -1))        js_error(J, "no event loop function");    js_copy(J, 0);    js_call(J, 0); // mp_event_loop} // Safely set last error from stack top: stack trace or toString or generic.// May leave items on stack - the caller should detect and pop if it cares.static void s_top_to_last_error(struct script_ctx *ctx, js_State *J){    set_last_error(ctx, 1, "unknown error");    if (js_try(J))        return;    if (js_isobject(J, -1))        js_hasproperty(J, -1, "stack");  // fetches it if exists    set_last_error(ctx, 1, js_tostring(J, -1));    js_endtry(J);} // MuJS can report warnings through this.static void report_handler(js_State *J, const char *msg){    MP_WARN(jctx(J), "[JS] %s\n", msg);} // Safely setup the js vm for calling run_script.static int s_init_js(js_State *J, struct script_ctx *ctx){    if (js_try(J))        return 1;    js_setcontext(J, ctx);    js_setreport(J, report_handler);    js_newcfunction(J, script__run_script, "run_script", 0);    js_pushglobal(J);  // 'this' for script__run_script    js_endtry(J);    return 0;} /********************************************************************** *  Initialization - booting the script *********************************************************************/// s_load_javascript: (entry point) creates the js vm, runs the script, returns//                    on script exit or uncaught js errors. Never throws.// script__run_script: - loads the built in functions and vars into the vm//                     - runs the default file[s] and the main script file//                     - calls mp_event_loop, returns on script-exit or throws.//// Note: init functions don't need autofree. They can use ctx as a talloc// context and free normally. If they throw - ctx is freed right afterwards.static int s_load_javascript(struct mp_script_args *args){    struct script_ctx *ctx = talloc_ptrtype(NULL, ctx);    *ctx = (struct script_ctx) {        .client = args->client,        .mpctx = args->mpctx,        .log = args->log,        .last_error_str = talloc_strdup(ctx, "Cannot initialize JavaScript"),        .filename = args->filename,        .path = args->path,    };     int r = -1;    js_State *J = js_newstate(NULL, NULL, 0);    if (!J || s_init_js(J, ctx))        goto error_out;     set_last_error(ctx, 0, NULL);    if (js_pcall(J, 0)) {  // script__run_script        s_top_to_last_error(ctx, J);        goto error_out;    }     r = 0; error_out:    if (r)        MP_FATAL(ctx, "%s\n", ctx->last_error_str);    if (J)

Lines 1–500 of 1215.

Keyboard shortcuts

General

Show keyboard shortcuts
?
Focus the current search
/

Issues and pull requests

Open filters
f

Issues, pull requests and Inbox

Select next item
j↓
Select previous item
k↑
Open selected item
Enter
Dismiss filters or clear selection
Esc

Repository code

Go to file
t