diff --git a/modules/meson.build b/modules/meson.build index 5a5f045f..c9fa565a 100644 --- a/modules/meson.build +++ b/modules/meson.build @@ -127,6 +127,7 @@ shared_library( 'wireplumber-module-lua-scripting', [ 'module-lua-scripting/module.c', + 'module-lua-scripting/script.c', 'module-lua-scripting/api/pod.c', 'module-lua-scripting/api/json.c', 'module-lua-scripting/api/api.c', diff --git a/modules/module-lua-scripting/api/api.c b/modules/module-lua-scripting/api/api.c index 8f717d23..d6c11ff7 100644 --- a/modules/module-lua-scripting/api/api.c +++ b/modules/module-lua-scripting/api/api.c @@ -35,7 +35,8 @@ get_wp_export_core (lua_State *L) WpCore *core = NULL; lua_pushliteral (L, "wireplumber_export_core"); lua_gettable (L, LUA_REGISTRYINDEX); - core = lua_touserdata (L, -1); + if (wplua_isobject (L, -1, WP_TYPE_CORE)) + core = wplua_toobject (L, -1); lua_pop (L, 1); return core ? core : get_wp_core(L); } diff --git a/modules/module-lua-scripting/module.c b/modules/module-lua-scripting/module.c index 96eee063..3390a802 100644 --- a/modules/module-lua-scripting/module.c +++ b/modules/module-lua-scripting/module.c @@ -10,6 +10,8 @@ #include #include +#include "script.h" + void wp_lua_scripting_api_init (lua_State *L); gboolean wp_lua_scripting_load_configuration (const gchar * conf_file, WpCore * core, GError ** error); @@ -18,41 +20,10 @@ struct _WpLuaScriptingPlugin { WpComponentLoader parent; - GArray *scripts; - WpCore *export_core; + GPtrArray *scripts; /* element-type: WpPlugin* */ lua_State *L; }; -struct ScriptData -{ - gchar *filename; - GVariant *args; -}; - -static void -script_data_clear (struct ScriptData * d) -{ - g_clear_pointer (&d->filename, g_free); - g_clear_pointer (&d->args, g_variant_unref); -} - -static gboolean -execute_script (lua_State *L, struct ScriptData * s, GError ** error) -{ - int nargs = 0; - nargs += wplua_push_sandbox (L); - - if (!wplua_load_path (L, s->filename, error)) { - lua_pop (L, nargs); - return FALSE; - } - if (s->args) { - wplua_gvariant_to_lua (L, s->args); - nargs++; - } - return wplua_pcall (L, nargs, 0, error); -} - static int wp_lua_scripting_package_loader (lua_State *L) { @@ -119,8 +90,7 @@ G_DEFINE_TYPE (WpLuaScriptingPlugin, wp_lua_scripting_plugin, static void wp_lua_scripting_plugin_init (WpLuaScriptingPlugin * self) { - self->scripts = g_array_new (FALSE, TRUE, sizeof (struct ScriptData)); - g_array_set_clear_func (self->scripts, (GDestroyNotify) script_data_clear); + self->scripts = g_ptr_array_new_with_free_func (g_object_unref); } static void @@ -128,7 +98,7 @@ wp_lua_scripting_plugin_finalize (GObject * object) { WpLuaScriptingPlugin * self = WP_LUA_SCRIPTING_PLUGIN (object); - g_clear_pointer (&self->scripts, g_array_unref); + g_clear_pointer (&self->scripts, g_ptr_array_unref); G_OBJECT_CLASS (wp_lua_scripting_plugin_parent_class)->finalize (object); } @@ -138,12 +108,7 @@ wp_lua_scripting_plugin_enable (WpPlugin * plugin, WpTransition * transition) { WpLuaScriptingPlugin * self = WP_LUA_SCRIPTING_PLUGIN (plugin); g_autoptr (WpCore) core = wp_object_get_core (WP_OBJECT (plugin)); - - /* initialize secondary connection to pipewire */ - self->export_core = - g_object_get_data (G_OBJECT (core), "wireplumber.export-core"); - if (self->export_core) - g_object_ref (self->export_core); + WpCore *export_core; /* init lua engine */ self->L = wplua_new (); @@ -152,23 +117,25 @@ wp_lua_scripting_plugin_enable (WpPlugin * plugin, WpTransition * transition) lua_pushlightuserdata (self->L, core); lua_settable (self->L, LUA_REGISTRYINDEX); - lua_pushliteral (self->L, "wireplumber_export_core"); - lua_pushlightuserdata (self->L, self->export_core); - lua_settable (self->L, LUA_REGISTRYINDEX); + /* initialize secondary connection to pipewire */ + export_core = g_object_get_data (G_OBJECT (core), "wireplumber.export-core"); + if (export_core) { + lua_pushliteral (self->L, "wireplumber_export_core"); + wplua_pushobject (self->L, export_core); + lua_settable (self->L, LUA_REGISTRYINDEX); + } wp_lua_scripting_api_init (self->L); wp_lua_scripting_enable_package_searcher (self->L); wplua_enable_sandbox (self->L, WP_LUA_SANDBOX_ISOLATE_ENV); - /* execute scripts that were queued in for loading */ + /* register scripts that were queued in for loading */ for (guint i = 0; i < self->scripts->len; i++) { - GError * error = NULL; - struct ScriptData * s = &g_array_index (self->scripts, struct ScriptData, i); - if (!execute_script (self->L, s, &error)) { - wp_transition_return_error (transition, error); - return; - } + WpPlugin *script = g_ptr_array_index (self->scripts, i); + g_object_set (script, "lua-engine", self->L, NULL); + wp_plugin_register (g_object_ref (script)); } + g_ptr_array_set_size (self->scripts, 0); wp_object_update_features (WP_OBJECT (self), WP_PLUGIN_FEATURE_ENABLED, 0); } @@ -177,9 +144,7 @@ static void wp_lua_scripting_plugin_disable (WpPlugin * plugin) { WpLuaScriptingPlugin * self = WP_LUA_SCRIPTING_PLUGIN (plugin); - g_clear_pointer (&self->L, wplua_unref); - g_clear_object (&self->export_core); } static gboolean @@ -190,8 +155,12 @@ wp_lua_scripting_plugin_supports_type (WpComponentLoader * cl, } static gchar * -find_script (const gchar * script, gboolean daemon) +find_script (const gchar * script, WpCore *core) { + g_autoptr (WpProperties) p = wp_core_get_properties (core); + const gchar *str = wp_properties_get (p, "wireplumber.daemon"); + gboolean daemon = !g_strcmp0 (str, "true"); + if ((!daemon || g_path_is_absolute (script)) && g_file_test (script, G_FILE_TEST_IS_REGULAR)) return g_strdup (script); @@ -212,25 +181,34 @@ wp_lua_scripting_plugin_load (WpComponentLoader * cl, const gchar * component, /* interpret component as a script */ if (!g_strcmp0 (type, "script/lua")) { - g_autoptr (WpProperties) p = wp_core_get_properties (core); - const gchar *str = wp_properties_get (p, "wireplumber.daemon"); - gboolean daemon = !g_strcmp0 (str, "true"); + g_autofree gchar *filename = NULL; + g_autofree gchar *pluginname = NULL; + g_autoptr (WpPlugin) script = NULL; - struct ScriptData s = {0}; - - s.filename = find_script (component, daemon); - if (!s.filename) { + filename = find_script (component, core); + if (!filename) { g_set_error (error, G_IO_ERROR, G_IO_ERROR_NOT_FOUND, "Could not locate script '%s'", component); return FALSE; } - if (args && g_variant_is_of_type (args, G_VARIANT_TYPE_VARDICT)) - s.args = g_variant_ref (args); + pluginname = g_strdup_printf ("script:%s", component); - /* keep in a list and delay loading until the plugin is enabled */ - g_array_append_val (self->scripts, s); - return self->L ? execute_script (self->L, &s, error) : TRUE; + script = g_object_new (WP_TYPE_LUA_SCRIPT, + "core", core, + "name", pluginname, + "filename", filename, + "arguments", args, + NULL); + + if (self->L) { + g_object_set (script, "lua-engine", self->L, NULL); + wp_plugin_register (g_steal_pointer (&script)); + } else { + /* keep in a list and delay registering until the plugin is enabled */ + g_ptr_array_add (self->scripts, g_steal_pointer (&script)); + } + return TRUE; } /* interpret component as a configuration file */ else if (!g_strcmp0 (type, "config/lua")) { diff --git a/modules/module-lua-scripting/script.c b/modules/module-lua-scripting/script.c new file mode 100644 index 00000000..5b7e9916 --- /dev/null +++ b/modules/module-lua-scripting/script.c @@ -0,0 +1,286 @@ +/* WirePlumber + * + * Copyright © 2022 Collabora Ltd. + * @author George Kiagiadakis + * + * SPDX-License-Identifier: MIT + */ + +#include "script.h" +#include + +/* + * This is a WpPlugin subclass that wraps a single lua script and acts like + * a handle for that script. When enabled, through the WpObject activation + * mechanism, the script is executed. It then provides an API for the script + * to declare when it has finished its activation procedure, which can be + * asynchronous (this is Script.finish_activation in Lua). + * When disabled, this class destroys the global environment that was used + * in the Lua engine for excecuting that script, effectively destroying all + * objects that were held in Lua as global variables. + */ + +struct _WpLuaScript +{ + WpPlugin parent; + + lua_State *L; + gchar *filename; + GVariant *args; +}; + +enum { + PROP_0, + PROP_LUA_ENGINE, + PROP_FILENAME, + PROP_ARGUMENTS, +}; + +G_DEFINE_TYPE (WpLuaScript, wp_lua_script, WP_TYPE_PLUGIN) + +static void +wp_lua_script_init (WpLuaScript * self) +{ +} + +static void +wp_lua_script_cleanup (WpLuaScript * self) +{ + /* LUA_REGISTRYINDEX[self] = nil */ + if (self->L) { + lua_pushnil (self->L); + lua_rawsetp (self->L, LUA_REGISTRYINDEX, self); + } +} + +static void +wp_lua_script_finalize (GObject * object) +{ + WpLuaScript *self = WP_LUA_SCRIPT (object); + + wp_lua_script_cleanup (self); + g_clear_pointer (&self->L, wplua_unref); + g_clear_pointer (&self->filename, g_free); + g_clear_pointer (&self->args, g_variant_unref); + + G_OBJECT_CLASS (wp_lua_script_parent_class)->finalize (object); +} + +static void +wp_lua_script_set_property (GObject * object, guint property_id, + const GValue * value, GParamSpec * pspec) +{ + WpLuaScript *self = WP_LUA_SCRIPT (object); + + switch (property_id) { + case PROP_LUA_ENGINE: + g_return_if_fail (self->L == NULL); + self->L = g_value_get_pointer (value); + if (self->L) + self->L = wplua_ref (self->L); + break; + case PROP_FILENAME: + self->filename = g_value_dup_string (value); + break; + case PROP_ARGUMENTS: + self->args = g_value_dup_variant (value); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); + break; + } +} + +static gboolean +wp_lua_script_check_async_activation (WpLuaScript * self) +{ + gboolean ret; + lua_rawgetp (self->L, LUA_REGISTRYINDEX, self); + lua_pushliteral (self->L, "Script"); + lua_gettable (self->L, -2); + lua_pushliteral (self->L, "async_activation"); + lua_gettable (self->L, -2); + ret = lua_toboolean (self->L, -1); + lua_pop (self->L, 3); + return ret; +} + +static void +wp_lua_script_detach_transition (WpLuaScript * self) +{ + lua_rawgetp (self->L, LUA_REGISTRYINDEX, self); + lua_pushliteral (self->L, "Script"); + lua_gettable (self->L, -2); + lua_pushliteral (self->L, "__transition"); + lua_pushnil (self->L); + lua_settable (self->L, -3); + lua_pop (self->L, 2); +} + +static int +script_finish_activation (lua_State * L) +{ + WpLuaScript *self; + + luaL_checktype (L, 1, LUA_TTABLE); + + lua_pushliteral (L, "__self"); + lua_gettable (L, 1); + luaL_checktype (L, -1, LUA_TLIGHTUSERDATA); + self = WP_LUA_SCRIPT ((gpointer) lua_topointer (L, -1)); + lua_pop (L, 2); + + wp_object_update_features (WP_OBJECT (self), WP_PLUGIN_FEATURE_ENABLED, 0); + return 0; +} + +static int +script_finish_activation_with_error (lua_State * L) +{ + WpTransition *transition = NULL; + const char *msg = NULL; + + luaL_checktype (L, 1, LUA_TTABLE); + msg = luaL_checkstring (L, 2); + + lua_getglobal (L, "Script"); + lua_pushliteral (L, "__transition"); + lua_gettable (L, 1); + if (lua_type (L, -1) == LUA_TLIGHTUSERDATA) + transition = WP_TRANSITION ((gpointer) lua_topointer (L, -1)); + lua_pop (L, 2); + + if (transition) + wp_transition_return_error (transition, g_error_new (WP_DOMAIN_LIBRARY, + WP_LIBRARY_ERROR_OPERATION_FAILED, "%s", msg)); + return 0; +} + +static const luaL_Reg script_api_methods[] = { + { "finish_activation", script_finish_activation }, + { "finish_activation_with_error", script_finish_activation_with_error }, + { NULL, NULL } +}; + +static int +wp_lua_script_sandbox (lua_State *L) +{ + luaL_checktype (L, 1, LUA_TLIGHTUSERDATA); // self + luaL_checktype (L, 2, LUA_TLIGHTUSERDATA); // transition + luaL_checktype (L, 3, LUA_TFUNCTION); // the script chunk + + /* create unique environment for this script */ + lua_getglobal (L, "create_sandbox_env"); + lua_call (L, 0, 1); + + /* create "Script" API */ + lua_pushliteral (L, "Script"); + luaL_newlib (L, script_api_methods); + lua_pushliteral (L, "__self"); + lua_pushvalue (L, 1); + lua_settable (L, -3); + lua_pushliteral (L, "__transition"); + lua_pushvalue (L, 2); + lua_settable (L, -3); + lua_settable (L, -3); + + /* store the environment */ + /* LUA_REGISTRYINDEX[self] = env */ + lua_pushvalue (L, 1); // self + lua_pushvalue (L, -2); // the table returned by create_sandbox_env + lua_rawset (L, LUA_REGISTRYINDEX); + + /* set it as the 1st upvalue (_ENV) on the loaded script chunk (at index 3) */ + lua_setupvalue (L, 3, 1); + + /* anything remaining on the stack are function arguments */ + int nargs = lua_gettop (L) - 3; + + /* execute script */ + lua_call (L, nargs, 0); + return 0; +} + +static void +wp_lua_script_enable (WpPlugin * plugin, WpTransition * transition) +{ + WpLuaScript *self = WP_LUA_SCRIPT (plugin); + g_autoptr (GError) error = NULL; + int top, nargs = 3; + + if (!self->L) { + error = g_error_new (WP_DOMAIN_LIBRARY, WP_LIBRARY_ERROR_INVALID_ARGUMENT, + "No lua state open; lua-scripting plugin is not enabled"); + wp_transition_return_error (transition, g_steal_pointer (&error)); + return; + } + + top = lua_gettop (self->L); + lua_pushcfunction (self->L, wp_lua_script_sandbox); + lua_pushlightuserdata (self->L, self); + lua_pushlightuserdata (self->L, transition); + + /* load script */ + if (!wplua_load_path (self->L, self->filename, &error)) { + lua_settop (self->L, top); + wp_transition_return_error (transition, g_steal_pointer (&error)); + return; + } + + /* push script arguments */ + if (self->args) { + wplua_gvariant_to_lua (self->L, self->args); + nargs++; + } + + /* execute script */ + if (!wplua_pcall (self->L, nargs, 0, &error)) { + lua_settop (self->L, top); + wp_transition_return_error (transition, g_steal_pointer (&error)); + wp_lua_script_cleanup (self); + return; + } + + if (!wp_lua_script_check_async_activation (self)) { + wp_lua_script_detach_transition (self); + wp_object_update_features (WP_OBJECT (self), WP_PLUGIN_FEATURE_ENABLED, 0); + } else { + g_signal_connect_object (transition, "notify::completed", + (GCallback) wp_lua_script_detach_transition, self, G_CONNECT_SWAPPED); + } + + lua_settop (self->L, top); +} + +static void +wp_lua_script_disable (WpPlugin * plugin) +{ + WpLuaScript *self = WP_LUA_SCRIPT (plugin); + wp_lua_script_cleanup (self); +} + +static void +wp_lua_script_class_init (WpLuaScriptClass * klass) +{ + GObjectClass *object_class = (GObjectClass *) klass; + WpPluginClass *plugin_class = (WpPluginClass *) klass; + + object_class->finalize = wp_lua_script_finalize; + object_class->set_property = wp_lua_script_set_property; + + plugin_class->enable = wp_lua_script_enable; + plugin_class->disable = wp_lua_script_disable; + + g_object_class_install_property (object_class, PROP_LUA_ENGINE, + g_param_spec_pointer ("lua-engine", "lua-engine", "lua-engine", + G_PARAM_WRITABLE | G_PARAM_STATIC_STRINGS)); + + g_object_class_install_property (object_class, PROP_FILENAME, + g_param_spec_string ("filename", "filename", "filename", NULL, + G_PARAM_WRITABLE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS)); + + g_object_class_install_property (object_class, PROP_ARGUMENTS, + g_param_spec_variant ("arguments", "arguments", "arguments", + G_VARIANT_TYPE_VARDICT, NULL, + G_PARAM_WRITABLE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS)); +} diff --git a/modules/module-lua-scripting/script.h b/modules/module-lua-scripting/script.h new file mode 100644 index 00000000..826ea017 --- /dev/null +++ b/modules/module-lua-scripting/script.h @@ -0,0 +1,22 @@ +/* WirePlumber + * + * Copyright © 2022 Collabora Ltd. + * @author George Kiagiadakis + * + * SPDX-License-Identifier: MIT + */ + +#ifndef __MODULE_LUA_SCRIPTING_SCRIPT_H__ +#define __MODULE_LUA_SCRIPTING_SCRIPT_H__ + +#include +#include + +G_BEGIN_DECLS + +#define WP_TYPE_LUA_SCRIPT (wp_lua_script_get_type ()) +G_DECLARE_FINAL_TYPE (WpLuaScript, wp_lua_script, WP, LUA_SCRIPT, WpPlugin) + +G_END_DECLS + +#endif diff --git a/src/main.c b/src/main.c index 92d20322..8c791834 100644 --- a/src/main.c +++ b/src/main.c @@ -48,6 +48,7 @@ enum { STEP_CHECK_MEDIA_SESSION, STEP_ACTIVATE_PLUGINS, STEP_ACTIVATE_SCRIPTS, + STEP_CLEANUP, }; G_DECLARE_FINAL_TYPE (WpInitTransition, wp_init_transition, @@ -67,7 +68,7 @@ wp_init_transition_get_next_step (WpTransition * transition, guint step) case STEP_LOAD_COMPONENTS: return STEP_CONNECT; case STEP_CONNECT: return STEP_CHECK_MEDIA_SESSION; case STEP_CHECK_MEDIA_SESSION:return STEP_ACTIVATE_PLUGINS; - case STEP_ACTIVATE_SCRIPTS: return WP_TRANSITION_STEP_NONE; + case STEP_CLEANUP: return WP_TRANSITION_STEP_NONE; case STEP_ACTIVATE_PLUGINS: { WpInitTransition *self = WP_INIT_TRANSITION (transition); @@ -77,6 +78,14 @@ wp_init_transition_get_next_step (WpTransition * transition, guint step) return STEP_ACTIVATE_PLUGINS; } + case STEP_ACTIVATE_SCRIPTS: { + WpInitTransition *self = WP_INIT_TRANSITION (transition); + if (self->pending_plugins == 0) + return STEP_CLEANUP; + else + return STEP_ACTIVATE_SCRIPTS; + } + default: g_return_val_if_reached (WP_TRANSITION_STEP_ERROR); } @@ -280,6 +289,17 @@ wp_init_transition_execute_step (WpTransition * transition, guint step) "script engine '%s' is not loaded", engine)); return; } + + self->pending_plugins = 1; + + self->om = wp_object_manager_new (); + wp_object_manager_add_interest (self->om, WP_TYPE_PLUGIN, + WP_CONSTRAINT_TYPE_G_PROPERTY, "name", "#s", "script:*", + NULL); + g_signal_connect_object (self->om, "object-added", + G_CALLBACK (on_plugin_added), self, 0); + wp_core_install_object_manager (core, self->om); + wp_object_activate (WP_OBJECT (plugin), WP_PLUGIN_FEATURE_ENABLED, NULL, (GAsyncReadyCallback) on_plugin_activated, self); } else { @@ -288,6 +308,7 @@ wp_init_transition_execute_step (WpTransition * transition, guint step) break; } + case STEP_CLEANUP: case WP_TRANSITION_STEP_ERROR: g_clear_object (&self->om); break; diff --git a/tests/wplua/script-tester.c b/tests/wplua/script-tester.c index 659fa0b2..c5b257fc 100644 --- a/tests/wplua/script-tester.c +++ b/tests/wplua/script-tester.c @@ -29,6 +29,7 @@ script_run (ScriptRunnerFixture *f, gconstpointer data) { g_autoptr (WpPlugin) plugin = NULL; g_autoptr (GError) error = NULL; + g_autofree gchar *pluginname = NULL; /* TODO: we could do some more stuff here to provide the test script with an API to deal with the main loop and test asynchronous stuff, if necessary */ @@ -41,10 +42,19 @@ script_run (ScriptRunnerFixture *f, gconstpointer data) wp_object_activate (WP_OBJECT (plugin), WP_PLUGIN_FEATURE_ENABLED, NULL, (GAsyncReadyCallback) test_object_activate_finish_cb, f); g_main_loop_run (f->base.loop); + g_clear_object (&plugin); wp_core_load_component (f->base.core, (const gchar *) data, "script/lua", NULL, &error); g_assert_no_error (error); + + pluginname = g_strdup_printf ("script:%s", (const gchar *) data); + + plugin = wp_plugin_find (f->base.core, pluginname); + g_assert_nonnull (plugin); + wp_object_activate (WP_OBJECT (plugin), WP_PLUGIN_FEATURE_ENABLED, + NULL, (GAsyncReadyCallback) test_object_activate_finish_cb, f); + g_main_loop_run (f->base.loop); } gint