From 60382df63f145a1bda3956faadccb4ad07dd25f0 Mon Sep 17 00:00:00 2001 From: George Kiagiadakis Date: Wed, 28 Feb 2024 12:11:38 +0200 Subject: [PATCH] conf: refactor configuration loading Changes: - Configuration files are no longer located by libpipewire, which allows us to control the paths that are being looked up. This is a requirement for installations where pipewire and wireplumber are built using different prefixes, in which case the configuration files of wireplumber end up being installed in a place that libpipewire doesn't look into... - The location of conf files is now again $prefix/share/wireplumber, /etc/wireplumber and $XDG_CONFIG_HOME/wireplumber, instead of using the pipewire directories. Also, since the previous commits, we now also support $XDG_CONFIG_DIRS/wireplumber (typically /etc/xdg/wireplumber) and $XDG_DATA_DIRS/wireplumber for system-wide configuration. - Since libpipewire doesn't expose the parser, we now also do the parsing of sections ourselves. This has the advantage that we can optimize it a bit for our use case. - The WpConf API has changed to not be a singleton and it is a property of WpCore instead. The configuration is now expected to be opened before the core is created, which allows the caller to identify configuration errors in advance. By not being a singleton, we can also reuse the WpConf API to open other SPA-JSON files. - WpConf also now has a lazy loading mechanism. The configuration files are mmap'ed and the various sections are located in advance, but not parsed until they are actually requested. Also, the sections are not copied in memory, unlike what happens in libpipewire. They are only copied when merging is needed. - WpCore now disables loading of a configuration file in pw_context, if a WpConf is provided. This is to have complete control here. The 'context.spa-libs' and 'context.modules' sections are still loaded, but we load them in WpConf and pass them down to pw_context for parsing. If a WpConf is not provided, pw_context is left to load the default configuration file (client.conf normally). --- lib/wp/conf.c | 475 +++++++++++++++++++------ lib/wp/conf.h | 28 +- lib/wp/core.c | 86 ++++- lib/wp/core.h | 7 +- lib/wp/private/internal-comp-loader.c | 2 +- lib/wp/private/parse-conf-section.c | 158 ++++++++ modules/module-lua-scripting/api/api.c | 42 ++- modules/module-settings.c | 4 +- src/main.c | 19 +- src/tools/wpctl.c | 2 +- src/tools/wpexec.c | 2 +- tests/common/base-test-fixture.h | 15 +- tests/wp/conf.c | 13 +- tests/wp/settings.c | 2 +- 14 files changed, 710 insertions(+), 145 deletions(-) create mode 100644 lib/wp/private/parse-conf-section.c diff --git a/lib/wp/conf.c b/lib/wp/conf.c index f1db5151..5c995536 100644 --- a/lib/wp/conf.c +++ b/lib/wp/conf.c @@ -6,13 +6,14 @@ * SPDX-License-Identifier: MIT */ -#include "core.h" #include "conf.h" #include "log.h" -#include "object-interest.h" #include "json-utils.h" +#include "base-dirs.h" +#include "error.h" #include +#include WP_DEFINE_LOCAL_LOG_TOPIC ("wp-conf") @@ -26,19 +27,38 @@ WP_DEFINE_LOCAL_LOG_TOPIC ("wp-conf") * configuration. */ +typedef struct _WpConfSection WpConfSection; +struct _WpConfSection +{ + gchar *name; + WpSpaJson *value; + gchar *location; +}; + +static void +wp_conf_section_clear (WpConfSection * section) +{ + g_free (section->name); + g_clear_pointer (§ion->value, wp_spa_json_unref); + g_free (section->location); +} +G_DEFINE_AUTO_CLEANUP_CLEAR_FUNC (WpConfSection, wp_conf_section_clear) + struct _WpConf { GObject parent; /* Props */ - GWeakRef core; + gchar *name; - GHashTable *sections; + /* Private */ + GArray *conf_sections; /* element-type: WpConfSection */ + GPtrArray *files; /* element-type: GMappedFile* */ }; enum { PROP_0, - PROP_CORE, + PROP_NAME, }; G_DEFINE_TYPE (WpConf, wp_conf, G_TYPE_OBJECT) @@ -46,10 +66,9 @@ G_DEFINE_TYPE (WpConf, wp_conf, G_TYPE_OBJECT) static void wp_conf_init (WpConf * self) { - g_weak_ref_init (&self->core, NULL); - - self->sections = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, - (GDestroyNotify) wp_spa_json_unref); + self->conf_sections = g_array_new (FALSE, FALSE, sizeof (WpConfSection)); + g_array_set_clear_func (self->conf_sections, (GDestroyNotify) wp_conf_section_clear); + self->files = g_ptr_array_new_with_free_func ((GDestroyNotify) g_mapped_file_unref); } static void @@ -59,8 +78,8 @@ wp_conf_set_property (GObject * object, guint property_id, WpConf *self = WP_CONF (object); switch (property_id) { - case PROP_CORE: - g_weak_ref_set (&self->core, g_value_get_object (value)); + case PROP_NAME: + self->name = g_value_dup_string (value); break; default: G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); @@ -75,8 +94,8 @@ wp_conf_get_property (GObject * object, guint property_id, WpConf *self = WP_CONF (object); switch (property_id) { - case PROP_CORE: - g_value_take_object (value, g_weak_ref_get (&self->core)); + case PROP_NAME: + g_value_set_string (value, self->name); break; default: G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); @@ -89,8 +108,10 @@ wp_conf_finalize (GObject * object) { WpConf *self = WP_CONF (object); - g_clear_pointer (&self->sections, g_hash_table_unref); - g_weak_ref_clear (&self->core); + wp_conf_close (self); + g_clear_pointer (&self->conf_sections, g_array_unref); + g_clear_pointer (&self->files, g_ptr_array_unref); + g_clear_pointer (&self->name, g_free); G_OBJECT_CLASS (wp_conf_parent_class)->finalize (object); } @@ -104,119 +125,282 @@ wp_conf_class_init (WpConfClass * klass) object_class->set_property = wp_conf_set_property; object_class->get_property = wp_conf_get_property; - g_object_class_install_property (object_class, PROP_CORE, - g_param_spec_object ("core", "core", "The WpCore", WP_TYPE_CORE, - G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS)); + g_object_class_install_property (object_class, PROP_NAME, + g_param_spec_string ("name", "name", "The name of the configuration file", + NULL, G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS)); } /*! - * \brief Returns the WpConf instance that is associated with the - * given core. + * \brief Creates a new WpConf object * - * This method will also create the instance and register it with the core - * if it had not been created before. + * This does not open the files, it only creates the object. For most use cases, + * you should use wp_conf_new_open() instead. * * \ingroup wpconf - * \param core the core - * \returns (transfer full): the WpConf instance + * \param name the name of the configuration file + * \param properties (transfer full) (nullable): unused, reserved for future use + * \returns (transfer full): a new WpConf object */ WpConf * -wp_conf_get_instance (WpCore *core) +wp_conf_new (const gchar * name, WpProperties * properties) { - WpConf *conf = wp_core_find_object (core, - (GEqualFunc) WP_IS_CONF, NULL); - - if (G_UNLIKELY (!conf)) { - conf = g_object_new (WP_TYPE_CONF, - "core", core, - NULL); - - wp_core_register_object (core, g_object_ref (conf)); - - wp_info_object (conf, "created wpconf object"); - } - - return conf; + g_return_val_if_fail (name, NULL); + g_clear_pointer (&properties, wp_properties_unref); + return g_object_new (WP_TYPE_CONF, "name", name, NULL); } -static gint -merge_section_cb (void *data, const char *location, const char *section, - const char *str, size_t len) +/*! + * \brief Creates a new WpConf object and opens the configuration file and its + * fragments, keeping them mapped in memory for further access. + * + * \ingroup wpconf + * \param name the name of the configuration file + * \param properties (transfer full) (nullable): unused, reserved for future use + * \param error (out) (nullable): return location for a GError, or NULL + * \returns (transfer full) (nullable): a new WpConf object, or NULL + * if an error occurred + */ +WpConf * +wp_conf_new_open (const gchar * name, WpProperties * properties, GError ** error) { - WpSpaJson **res_section = (WpSpaJson **)data; - g_autoptr (WpSpaJson) json = NULL; - gboolean override; + g_return_val_if_fail (name, NULL); - g_return_val_if_fail (res_section, -EINVAL); + g_autoptr (WpConf) self = wp_conf_new (name, properties); + if (!wp_conf_open (self, error)) + return NULL; + return g_steal_pointer (&self); +} - override = g_str_has_prefix (section, OVERRIDE_SECTION_PREFIX); - if (override) - section += strlen (OVERRIDE_SECTION_PREFIX); +static gboolean +open_and_load_sections (WpConf * self, const gchar *path, GError ** error) +{ + g_autoptr (GMappedFile) file = g_mapped_file_new (path, FALSE, error); + if (!file) + return FALSE; - wp_debug ("loading section %s (override=%d) from %s", section, override, - location); + g_autoptr (WpSpaJson) json = wp_spa_json_new_wrap_stringn ( + g_mapped_file_get_contents (file), g_mapped_file_get_length (file)); + g_autoptr (WpSpaJsonParser) parser = wp_spa_json_parser_new_undefined (json); + g_autoptr (GArray) sections = g_array_new (FALSE, FALSE, sizeof (WpConfSection)); - /* Only allow sections to be objects or arrays */ - json = wp_spa_json_new_wrap_stringn (str, len); - if (!wp_spa_json_is_container (json)) { - wp_warning ( - "skipping section %s from %s as it is not JSON object or array", - section, location); - return 0; - } + g_array_set_clear_func (sections, (GDestroyNotify) wp_conf_section_clear); - /* Merge section if it was defined previously and the 'override.' prefix is - * not used */ - if (!override && *res_section) { - g_autoptr (WpSpaJson) merged = - wp_json_utils_merge_containers (*res_section, json); - if (!merged) { - wp_warning ( - "skipping merge of %s from %s as JSON values are not compatible", - section, location); - return 0; + while (TRUE) { + g_auto (WpConfSection) section = { 0, }; + g_autoptr (WpSpaJson) tmp = NULL; + + /* parse the section name */ + tmp = wp_spa_json_parser_get_json (parser); + if (!tmp) + break; + + if (wp_spa_json_is_container (tmp) || + wp_spa_json_is_int (tmp) || + wp_spa_json_is_float (tmp) || + wp_spa_json_is_boolean (tmp) || + wp_spa_json_is_null (tmp)) + { + g_set_error (error, WP_DOMAIN_LIBRARY, WP_LIBRARY_ERROR_INVALID_ARGUMENT, + "invalid section name (not a string): %.*s", + (int) wp_spa_json_get_size (tmp), wp_spa_json_get_data (tmp)); + return FALSE; } - g_clear_pointer (res_section, wp_spa_json_unref); - *res_section = g_steal_pointer (&merged); - wp_debug ("section %s from %s loaded", location, section); + section.name = wp_spa_json_parse_string (tmp); + g_clear_pointer (&tmp, wp_spa_json_unref); + + /* parse the section contents */ + tmp = wp_spa_json_parser_get_json (parser); + if (!tmp) { + g_set_error (error, WP_DOMAIN_LIBRARY, WP_LIBRARY_ERROR_INVALID_ARGUMENT, + "section '%s' has no value", section.name); + return FALSE; + } + + section.value = g_steal_pointer (&tmp); + section.location = g_strdup (path); + g_array_append_val (sections, section); + memset (§ion, 0, sizeof (section)); } - /* Otherwise always replace */ - else { - g_clear_pointer (res_section, wp_spa_json_unref); - *res_section = g_steal_pointer (&json); - wp_debug ("section %s from %s loaded", location, section); - } + /* store the mapped file and the sections; note that the stored WpSpaJson + still point to the data in the GMappedFile, so this is why we keep the + GMappedFile alive */ + g_ptr_array_add (self->files, g_steal_pointer (&file)); + g_array_append_vals (self->conf_sections, sections->data, sections->len); + g_array_set_clear_func (sections, NULL); - return 0; + return TRUE; } -static void -ensure_section_loaded (WpConf *self, const gchar *section) +/*! + * \brief Opens the configuration file and its fragments and keeps them + * mapped in memory for further access. + * + * \ingroup wpconf + * \param self the configuration + * \param error (out)(nullable): return location for a GError, or NULL + * \returns TRUE on success, FALSE on error + */ +gboolean +wp_conf_open (WpConf * self, GError ** error) { - g_autoptr (WpCore) core = NULL; - struct pw_context *pw_ctx = NULL; - g_autoptr (WpSpaJson) json_section = NULL; - g_autofree gchar *override_section = NULL; + g_return_val_if_fail (WP_IS_CONF (self), FALSE); - if (g_hash_table_contains (self->sections, section)) - return; + g_autofree gchar *path = NULL; + g_autoptr (WpIterator) iterator = NULL; + g_auto (GValue) value = G_VALUE_INIT; - core = g_weak_ref_get (&self->core); - g_return_if_fail (core); - pw_ctx = wp_core_get_pw_context (core); - g_return_if_fail (pw_ctx); + /* open the main file */ + path = wp_base_dirs_find_file (WP_BASE_DIRS_CONFIGURATION, NULL, self->name); + if (path) { + wp_info_object (self, "opening main file: %s", path); + if (!open_and_load_sections (self, path, error)) + return FALSE; + } + g_clear_pointer (&path, g_free); - pw_context_conf_section_for_each (pw_ctx, section, merge_section_cb, - &json_section); - override_section = g_strdup_printf (OVERRIDE_SECTION_PREFIX "%s", section); - pw_context_conf_section_for_each (pw_ctx, override_section, merge_section_cb, - &json_section); + /* open the .conf.d/ fragments */ + path = g_strdup_printf ("%s.d", self->name); + iterator = wp_base_dirs_new_files_iterator (WP_BASE_DIRS_CONFIGURATION, path, + ".conf"); - if (json_section) - g_hash_table_insert (self->sections, g_strdup (section), - g_steal_pointer (&json_section)); + for (; wp_iterator_next (iterator, &value); g_value_unset (&value)) { + const gchar *filename = g_value_get_string (&value); + + wp_info_object (self, "opening fragment file: %s", filename); + + g_autoptr (GError) e = NULL; + if (!open_and_load_sections (self, filename, &e)) { + wp_warning_object (self, "failed to open '%s': %s", filename, e->message); + continue; + } + } + + if (self->files->len == 0) { + g_set_error (error, G_IO_ERROR, G_IO_ERROR_NOT_FOUND, + "Could not locate configuration file '%s'", self->name); + return FALSE; + } + + return TRUE; +} + +/*! + * \brief Closes the configuration file and its fragments + * + * \ingroup wpconf + * \param self the configuration + */ +void +wp_conf_close (WpConf * self) +{ + g_return_if_fail (WP_IS_CONF (self)); + + g_array_set_size (self->conf_sections, 0); + g_ptr_array_set_size (self->files, 0); +} + +/*! + * \brief Tests if the configuration files are open + * + * \ingroup wpconf + * \param self the configuration + * \returns TRUE if the configuration files are open, FALSE otherwise + */ +gboolean +wp_conf_is_open (WpConf * self) +{ + g_return_val_if_fail (WP_IS_CONF (self), FALSE); + return self->files->len > 0; +} + +/*! + * \brief Gets the name of the configuration file + * + * \ingroup wpconf + * \param self the configuration + * \returns the name of the configuration file + */ +const gchar * +wp_conf_get_name (WpConf * self) +{ + g_return_val_if_fail (WP_IS_CONF (self), NULL); + return self->name; +} + +static WpSpaJson * +ensure_merged_section (WpConf * self, const gchar *section) +{ + g_autoptr (WpSpaJson) merged = NULL; + WpConfSection *merged_section = NULL; + + /* check if the section is already merged */ + for (guint i = 0; i < self->conf_sections->len; i++) { + WpConfSection *s = &g_array_index (self->conf_sections, WpConfSection, i); + if (g_str_equal (s->name, section)) { + if (!s->location) { + wp_debug_object (self, "section %s is already merged", section); + return wp_spa_json_ref (s->value); + } + } + } + + /* Iterate over the sections and merge them */ + for (guint i = 0; i < self->conf_sections->len; i++) { + WpConfSection *s = &g_array_index (self->conf_sections, WpConfSection, i); + const gchar *s_name = s->name; + + /* skip the "override." prefix and take a note */ + gboolean override = g_str_has_prefix (s_name, OVERRIDE_SECTION_PREFIX); + if (override) + s_name += strlen (OVERRIDE_SECTION_PREFIX); + + if (g_str_equal (s_name, section)) { + /* Merge sections if a previous value exists and + the 'override.' prefix is not present */ + if (!override && merged) { + g_autoptr (WpSpaJson) new_merged = + wp_json_utils_merge_containers (merged, s->value); + if (!merged) { + wp_warning_object (self, + "skipping merge of '%s' from '%s' as JSON containers are not compatible", + section, s->location); + continue; + } + + g_clear_pointer (&merged, wp_spa_json_unref); + merged = g_steal_pointer (&new_merged); + merged_section = NULL; + } + /* Otherwise always replace */ + else { + g_clear_pointer (&merged, wp_spa_json_unref); + merged = wp_spa_json_ref (s->value); + merged_section = s; + } + } + } + + /* cache the result */ + if (merged_section) { + /* if the merged json came from a single location, just clear + the location from that WpConfSection to mark it as the result */ + wp_info_object (self, "section '%s' is used as-is from '%s'", section, + merged_section->location); + g_clear_pointer (&merged_section->location, g_free); + } else if (merged) { + /* if the merged json came from multiple locations, create a new + WpConfSection to store it */ + WpConfSection s = { g_strdup (section), wp_spa_json_ref (merged), NULL }; + g_array_append_val (self->conf_sections, s); + wp_info_object (self, "section '%s' is merged from multiple locations", + section); + } else { + wp_info_object (self, "section '%s' is not defined", section); + } + + return g_steal_pointer (&merged); } /*! @@ -235,18 +419,16 @@ ensure_section_loaded (WpConf *self, const gchar *section) WpSpaJson * wp_conf_get_section (WpConf *self, const gchar *section, WpSpaJson *fallback) { - WpSpaJson *s; + g_autoptr (WpSpaJson) s = NULL; g_autoptr (WpSpaJson) fb = fallback; g_return_val_if_fail (WP_IS_CONF (self), NULL); - ensure_section_loaded (self, section); - - s = g_hash_table_lookup (self->sections, section); + s = ensure_merged_section (self, section); if (!s) return fb ? g_steal_pointer (&fb) : NULL; - return wp_spa_json_ref (s); + return g_steal_pointer (&s); } /*! @@ -447,3 +629,88 @@ wp_conf_get_value_string (WpConf *self, const gchar *section, return_fallback: return fallback ? g_strdup (fallback) : NULL; } + +/*! + * \brief Updates the given properties with the values of a specific section + * from the configuration. + * + * \ingroup wpconf + * \param self the configuration + * \param section the section name + * \param props the properties to update + * \returns the number of properties updated + */ +gint +wp_conf_section_update_props (WpConf *self, const gchar *section, + WpProperties *props) +{ + g_autoptr (WpSpaJson) json = NULL; + + g_return_val_if_fail (WP_IS_CONF (self), -1); + g_return_val_if_fail (section, -1); + g_return_val_if_fail (props, -1); + + json = wp_conf_get_section (self, section, NULL); + if (!json) + return 0; + return wp_properties_update_from_json (props, json); +} + +#include "private/parse-conf-section.c" + +/*! + * \brief Parses standard pw_context sections from \a conf + * + * \ingroup wpconf + * \param self the configuration + * \param context the associated pw_context + */ +void +wp_conf_parse_pw_context_sections (WpConf * self, struct pw_context * context) +{ + gint res; + WpProperties *conf_wp; + struct pw_properties *conf_pw; + + g_return_if_fail (WP_IS_CONF (self)); + g_return_if_fail (context); + + /* convert needed sections into a pipewire-style conf dictionary */ + conf_wp = wp_properties_new ("config.path", "wpconf", NULL); + { + g_autoptr (WpSpaJson) j = wp_conf_get_section (self, "context.spa-libs", NULL); + if (j) { + g_autofree gchar *js = wp_spa_json_parse_string (j); + wp_properties_set (conf_wp, "context.spa-libs", js); + } + } + { + g_autoptr (WpSpaJson) j = wp_conf_get_section (self, "context.modules", NULL); + if (j) { + g_autofree gchar *js = wp_spa_json_parse_string (j); + wp_properties_set (conf_wp, "context.modules", js); + } + } + conf_pw = wp_properties_unref_and_take_pw_properties (conf_wp); + + /* parse sections */ + if ((res = _pw_context_parse_conf_section (context, conf_pw, "context.spa-libs")) < 0) + goto error; + wp_info_object (self, "parsed %d context.spa-libs items", res); + + if ((res = _pw_context_parse_conf_section (context, conf_pw, "context.modules")) < 0) + goto error; + if (res > 0) + wp_info_object (self, "parsed %d context.modules items", res); + else + wp_warning_object (self, "no modules loaded from context.modules"); + +out: + pw_properties_free (conf_pw); + return; + +error: + wp_critical_object (self, "failed to parse pw_context sections: %s", + spa_strerror (res)); + goto out; +} diff --git a/lib/wp/conf.h b/lib/wp/conf.h index ad00db0c..a3385e70 100644 --- a/lib/wp/conf.h +++ b/lib/wp/conf.h @@ -14,6 +14,8 @@ G_BEGIN_DECLS +struct pw_context; + /*! * \brief The WpConf GType * \ingroup wpconf @@ -24,7 +26,23 @@ WP_API G_DECLARE_FINAL_TYPE (WpConf, wp_conf, WP, CONF, GObject) WP_API -WpConf * wp_conf_get_instance (WpCore * core); +WpConf * wp_conf_new (const gchar * name, WpProperties * properties); + +WP_API +WpConf * wp_conf_new_open (const gchar * name, WpProperties * properties, + GError ** error); + +WP_API +gboolean wp_conf_open (WpConf * self, GError ** error); + +WP_API +void wp_conf_close (WpConf * self); + +WP_API +gboolean wp_conf_is_open (WpConf * self); + +WP_API +const gchar * wp_conf_get_name (WpConf * self); WP_API WpSpaJson * wp_conf_get_section (WpConf *self, const gchar *section, @@ -50,6 +68,14 @@ WP_API gchar *wp_conf_get_value_string (WpConf *self, const gchar *section, const gchar *key, const gchar *fallback); +WP_API +gint wp_conf_section_update_props (WpConf * self, const gchar * section, + WpProperties * props); + +WP_API +void wp_conf_parse_pw_context_sections (WpConf * self, + struct pw_context * context); + G_END_DECLS #endif diff --git a/lib/wp/core.c b/lib/wp/core.c index dae44f96..f2fa1407 100644 --- a/lib/wp/core.c +++ b/lib/wp/core.c @@ -96,6 +96,25 @@ wp_loop_source_new (void) * objects that appear in the registry, making them accessible through * the WpObjectManager API. * + * The core is also responsible for loading components, which are defined in + * the main configuration file. Components are loaded when + * WP_CORE_FEATURE_COMPONENTS is activated. + * + * \b Configuration + * + * The main configuration file needs to be created and opened before the core + * is created, using the WpConf API. It is then passed to the core as an + * argument in the constructor. + * + * If a configuration file is not provided, the core will let the underlying + * `pw_context` load its own configuration, based on the rules that apply to + * all pipewire clients (e.g. it respects the `PIPEWIRE_CONFIG_NAME` environment + * variable and loads "client.conf" as a last resort). + * + * If a configuration file is provided, the core does not let the underlying + * `pw_context` load any configuration and instead uses the provided WpConf + * object. + * * \gproperties * * \gproperty{g-main-context, GMainContext *, G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY, @@ -110,6 +129,9 @@ wp_loop_source_new (void) * \gproperty{pw-core, gpointer (struct pw_core *), G_PARAM_READABLE, * The pipewire core} * + * \gproperty{conf, WpConf *, G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY, + * The main configuration file} + * * \gsignals * * \par connected @@ -151,6 +173,9 @@ struct _WpCore struct spa_hook core_listener; struct spa_hook proxy_core_listener; + /* the main configuration file */ + WpConf *conf; + WpRegistry registry; GHashTable *async_tasks; // }; @@ -161,6 +186,7 @@ enum { PROP_PROPERTIES, PROP_PW_CONTEXT, PROP_PW_CORE, + PROP_CONF, }; enum { @@ -287,6 +313,25 @@ wp_core_constructed (GObject *object) struct pw_properties *p = NULL; const gchar *str = NULL; + /* use our own configuration file, if specified */ + if (self->conf) { + wp_info_object (self, "using configuration file: %s", + wp_conf_get_name (self->conf)); + + /* ensure we have our very own properties set, + since we are going to modify it */ + self->properties = self->properties ? + wp_properties_ensure_unique_owner (self->properties) : + wp_properties_new_empty (); + + /* load context.properties */ + wp_conf_section_update_props (self->conf, "context.properties", + self->properties); + + /* disable loading of a configuration file in pw_context */ + wp_properties_set (self->properties, PW_KEY_CONFIG_NAME, "null"); + } + /* properties are fully stored in the pw_context, no need to keep a copy */ p = self->properties ? wp_properties_unref_and_take_pw_properties (self->properties) : NULL; @@ -304,6 +349,10 @@ wp_core_constructed (GObject *object) wp_warning ("ignoring invalid log.level in config file: %s", str); } + /* parse pw_context specific configuration sections */ + if (self->conf) + wp_conf_parse_pw_context_sections (self->conf, self->pw_context); + /* Init refcount */ grefcount *rc = pw_context_get_user_data (self->pw_context); g_return_if_fail (rc); @@ -345,6 +394,7 @@ wp_core_finalize (GObject * obj) g_clear_pointer (&self->properties, wp_properties_unref); g_clear_pointer (&self->g_main_context, g_main_context_unref); g_clear_pointer (&self->async_tasks, g_hash_table_unref); + g_clear_object (&self->conf); wp_debug_object (self, "WpCore destroyed"); @@ -370,6 +420,9 @@ wp_core_get_property (GObject * object, guint property_id, case PROP_PW_CORE: g_value_set_pointer (value, self->pw_core); break; + case PROP_CONF: + g_value_set_object (value, self->conf); + break; default: G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); break; @@ -392,6 +445,9 @@ wp_core_set_property (GObject * object, guint property_id, case PROP_PW_CONTEXT: self->pw_context = g_value_get_pointer (value); break; + case PROP_CONF: + self->conf = g_value_dup_object (value); + break; default: G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); break; @@ -541,6 +597,11 @@ wp_core_class_init (WpCoreClass * klass) g_param_spec_pointer ("pw-core", "pw-core", "The pipewire core", G_PARAM_READABLE | G_PARAM_STATIC_STRINGS)); + g_object_class_install_property (object_class, PROP_CONF, + g_param_spec_object ("conf", "conf", "The main configuration file", + WP_TYPE_CONF, + G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS)); + signals[SIGNAL_CONNECTED] = g_signal_new ("connected", G_TYPE_FROM_CLASS (klass), G_SIGNAL_RUN_LAST, 0, NULL, NULL, NULL, G_TYPE_NONE, 0); @@ -555,16 +616,20 @@ wp_core_class_init (WpCoreClass * klass) * * \ingroup wpcore * \param context (transfer none) (nullable): the GMainContext to use for events - * \param properties (transfer full) (nullable): additional properties, which are - * passed to pw_context_new() and pw_context_connect() + * \param conf (transfer full) (nullable): the main configuration file + * \param properties (transfer full) (nullable): additional properties, which + * are also passed to pw_context_new() and pw_context_connect() * \returns (transfer full): a new WpCore */ WpCore * -wp_core_new (GMainContext *context, WpProperties * properties) +wp_core_new (GMainContext * context, WpConf * conf, WpProperties * properties) { + g_autoptr (WpConf) c = conf; g_autoptr (WpProperties) props = properties; + return g_object_new (WP_TYPE_CORE, "g-main-context", context, + "conf", conf, "properties", properties, "pw-context", NULL, NULL); @@ -583,6 +648,7 @@ wp_core_clone (WpCore * self) return g_object_new (WP_TYPE_CORE, "core", self, "g-main-context", self->g_main_context, + "conf", self->conf, "properties", self->properties, "pw-context", self->pw_context, NULL); @@ -618,6 +684,20 @@ wp_core_get_export_core (WpCore * self) return wp_core_find_object (self, find_export_core, NULL); } +/*! + * \brief Gets the main configuration file of the core + * + * \ingroup wpcore + * \param self the core + * \returns (transfer full) (nullable): the main configuration file + */ +WpConf * +wp_core_get_conf (WpCore * self) +{ + g_return_val_if_fail (WP_IS_CORE (self), NULL); + return self->conf ? g_object_ref (self->conf) : NULL; +} + /*! * \brief Gets the GMainContext of the core * diff --git a/lib/wp/core.h b/lib/wp/core.h index 0cacd3ed..76bc518f 100644 --- a/lib/wp/core.h +++ b/lib/wp/core.h @@ -12,6 +12,7 @@ #include "object.h" #include "properties.h" #include "spa-json.h" +#include "conf.h" G_BEGIN_DECLS @@ -41,7 +42,8 @@ G_DECLARE_FINAL_TYPE (WpCore, wp_core, WP, CORE, WpObject) /* Basic */ WP_API -WpCore * wp_core_new (GMainContext *context, WpProperties * properties); +WpCore * wp_core_new (GMainContext * context, WpConf * conf, + WpProperties * properties); WP_API WpCore * wp_core_clone (WpCore * self); @@ -49,6 +51,9 @@ WpCore * wp_core_clone (WpCore * self); WP_API WpCore * wp_core_get_export_core (WpCore * self); +WP_API +WpConf * wp_core_get_conf (WpCore * self); + WP_API GMainContext * wp_core_get_g_main_context (WpCore * self); diff --git a/lib/wp/private/internal-comp-loader.c b/lib/wp/private/internal-comp-loader.c index 17ff327e..22bf0863 100644 --- a/lib/wp/private/internal-comp-loader.c +++ b/lib/wp/private/internal-comp-loader.c @@ -751,7 +751,7 @@ wp_internal_comp_loader_load (WpComponentLoader * self, WpCore * core, if (g_str_equal (type, "profile")) { /* component name is the profile name; component list and profile features are loaded from config */ - g_autoptr (WpConf) conf = wp_conf_get_instance (core); + g_autoptr (WpConf) conf = wp_core_get_conf (core); g_autoptr (WpSpaJson) profile_json = NULL; profile_json = diff --git a/lib/wp/private/parse-conf-section.c b/lib/wp/private/parse-conf-section.c new file mode 100644 index 00000000..3b62467f --- /dev/null +++ b/lib/wp/private/parse-conf-section.c @@ -0,0 +1,158 @@ +/* PipeWire */ +/* SPDX-FileCopyrightText: Copyright © 2021 Wim Taymans */ +/* SPDX-License-Identifier: MIT */ + +/* + This is a partial copy of functions from libpipewire's conf.c that is meant to + live here temporarily until pw_context_parse_conf_section() is fixed upstream. + See https://gitlab.freedesktop.org/pipewire/pipewire/-/merge_requests/1925 +*/ + +#include + +#include +#include +#include +#include + +#include + +struct data { + struct pw_context *context; + struct pw_properties *props; + int count; +}; + +/* context.spa-libs = { + * = + * } + */ +static int parse_spa_libs(void *user_data, const char *location, + const char *section, const char *str, size_t len) +{ + struct data *d = user_data; + struct pw_context *context = d->context; + struct spa_json it[2]; + char key[512], value[512]; + + spa_json_init(&it[0], str, len); + if (spa_json_enter_object(&it[0], &it[1]) < 0) { + pw_log_error("config file error: context.spa-libs is not an object"); + return -EINVAL; + } + + while (spa_json_get_string(&it[1], key, sizeof(key)) > 0) { + if (spa_json_get_string(&it[1], value, sizeof(value)) > 0) { + pw_context_add_spa_lib(context, key, value); + d->count++; + } + } + return 0; +} + + + +static int load_module(struct pw_context *context, const char *key, const char *args, const char *flags) +{ + if (pw_context_load_module(context, key, args, NULL) == NULL) { + if (errno == ENOENT && flags && strstr(flags, "ifexists") != NULL) { + pw_log_info("%p: skipping unavailable module %s", + context, key); + } else if (flags == NULL || strstr(flags, "nofail") == NULL) { + pw_log_error("%p: could not load mandatory module \"%s\": %m", + context, key); + return -errno; + } else { + pw_log_info("%p: could not load optional module \"%s\": %m", + context, key); + } + } else { + pw_log_info("%p: loaded module %s", context, key); + } + return 0; +} + +/* + * context.modules = [ + * { name = + * ( args = { = ... } ) + * ( flags = [ ( ifexists ) ( nofail ) ] + * ( condition = [ { key = value, .. } .. ] ) + * } + * ] + */ +static int parse_modules(void *user_data, const char *location, + const char *section, const char *str, size_t len) +{ + struct data *d = user_data; + struct pw_context *context = d->context; + struct spa_json it[4]; + char key[512]; + int res = 0; + + spa_autofree char *s = strndup(str, len); + spa_json_init(&it[0], s, len); + if (spa_json_enter_array(&it[0], &it[1]) < 0) { + pw_log_error("config file error: context.modules is not an array"); + return -EINVAL; + } + + while (spa_json_enter_object(&it[1], &it[2]) > 0) { + char *name = NULL, *args = NULL, *flags = NULL; + bool have_match = true; + + while (spa_json_get_string(&it[2], key, sizeof(key)) > 0) { + const char *val; + int len; + + if ((len = spa_json_next(&it[2], &val)) <= 0) + break; + + if (spa_streq(key, "name")) { + name = (char*)val; + spa_json_parse_stringn(val, len, name, len+1); + } else if (spa_streq(key, "args")) { + if (spa_json_is_container(val, len)) + len = spa_json_container_len(&it[2], val, len); + + args = (char*)val; + spa_json_parse_stringn(val, len, args, len+1); + } else if (spa_streq(key, "flags")) { + if (spa_json_is_container(val, len)) + len = spa_json_container_len(&it[2], val, len); + flags = (char*)val; + spa_json_parse_stringn(val, len, flags, len+1); + } + } + if (!have_match) + continue; + + if (name != NULL) + res = load_module(context, name, args, flags); + + if (res < 0) + break; + + d->count++; + } + + return res; +} + +static int _pw_context_parse_conf_section(struct pw_context *context, + struct pw_properties *conf, const char *section) +{ + struct data data = { .context = context }; + int res; + + if (spa_streq(section, "context.spa-libs")) + res = pw_conf_section_for_each(&conf->dict, section, + parse_spa_libs, &data); + else if (spa_streq(section, "context.modules")) + res = pw_conf_section_for_each(&conf->dict, section, + parse_modules, &data); + else + res = -EINVAL; + + return res == 0 ? data.count : res; +} diff --git a/modules/module-lua-scripting/api/api.c b/modules/module-lua-scripting/api/api.c index 498a28f1..d9dd2542 100644 --- a/modules/module-lua-scripting/api/api.c +++ b/modules/module-lua-scripting/api/api.c @@ -1621,12 +1621,15 @@ impl_module_new (lua_State *L) static int conf_get_section (lua_State *L) { - g_autoptr (WpConf) conf = wp_conf_get_instance (get_wp_core (L)); + g_autoptr (WpConf) conf = wp_core_get_conf (get_wp_core (L)); const char *section; g_autoptr (WpSpaJson) fb = NULL; g_autoptr (WpSpaJson) s = NULL; - g_return_val_if_fail (conf, 0); + if (!conf) { + lua_pushnil (L); + return 1; + } section = luaL_checkstring (L, 1); if (lua_isuserdata (L, 2)) { @@ -1646,13 +1649,16 @@ conf_get_section (lua_State *L) static int conf_get_value (lua_State *L) { - g_autoptr (WpConf) conf = wp_conf_get_instance (get_wp_core (L)); + g_autoptr (WpConf) conf = wp_core_get_conf (get_wp_core (L)); const char *section; const char *key; g_autoptr (WpSpaJson) fb = NULL; g_autoptr (WpSpaJson) v = NULL; - g_return_val_if_fail (conf, 0); + if (!conf) { + lua_pushnil (L); + return 1; + } section = luaL_checkstring (L, 1); key = luaL_checkstring (L, 2); @@ -1673,12 +1679,15 @@ conf_get_value (lua_State *L) static int conf_get_value_boolean (lua_State *L) { - g_autoptr (WpConf) conf = wp_conf_get_instance (get_wp_core (L)); + g_autoptr (WpConf) conf = wp_core_get_conf (get_wp_core (L)); const char *section; const char *key; gboolean fb; - g_return_val_if_fail (conf, 0); + if (!conf) { + lua_pushnil (L); + return 1; + } section = luaL_checkstring (L, 1); key = luaL_checkstring (L, 2); @@ -1691,12 +1700,15 @@ conf_get_value_boolean (lua_State *L) static int conf_get_value_int (lua_State *L) { - g_autoptr (WpConf) conf = wp_conf_get_instance (get_wp_core (L)); + g_autoptr (WpConf) conf = wp_core_get_conf (get_wp_core (L)); const char *section; const char *key; gint fb; - g_return_val_if_fail (conf, 0); + if (!conf) { + lua_pushnil (L); + return 1; + } section = luaL_checkstring (L, 1); key = luaL_checkstring (L, 2); @@ -1709,12 +1721,15 @@ conf_get_value_int (lua_State *L) static int conf_get_value_float (lua_State *L) { - g_autoptr (WpConf) conf = wp_conf_get_instance (get_wp_core (L)); + g_autoptr (WpConf) conf = wp_core_get_conf (get_wp_core (L)); const char *section; const char *key; float fb; - g_return_val_if_fail (conf, 0); + if (!conf) { + lua_pushnil (L); + return 1; + } section = luaL_checkstring (L, 1); key = luaL_checkstring (L, 2); @@ -1727,13 +1742,16 @@ conf_get_value_float (lua_State *L) static int conf_get_value_string (lua_State *L) { - g_autoptr (WpConf) conf = wp_conf_get_instance (get_wp_core (L)); + g_autoptr (WpConf) conf = wp_core_get_conf (get_wp_core (L)); const char *section; const char *key; const char *fb; g_autofree gchar *str = NULL; - g_return_val_if_fail (conf, 0); + if (!conf) { + lua_pushnil (L); + return 1; + } section = luaL_checkstring (L, 1); key = luaL_checkstring (L, 2); diff --git a/modules/module-settings.c b/modules/module-settings.c index fc4af2f9..1dcddede 100644 --- a/modules/module-settings.c +++ b/modules/module-settings.c @@ -86,7 +86,7 @@ load_configuration_settings (WpSettingsPlugin *self) g_autoptr (WpProperties) res = wp_properties_new_empty (); g_return_val_if_fail (core, NULL); - conf = wp_conf_get_instance (core); + conf = wp_core_get_conf (core); g_return_val_if_fail (conf, NULL); json = wp_conf_get_section (conf, "wireplumber.settings", NULL); @@ -254,7 +254,7 @@ on_schema_metadata_activated (WpMetadata * m, GAsyncResult * res, WpTransition *transition = WP_TRANSITION (user_data); WpSettingsPlugin *self = wp_transition_get_source_object (transition); g_autoptr (WpCore) core = wp_object_get_core (WP_OBJECT (self)); - g_autoptr (WpConf) conf = wp_conf_get_instance (core); + g_autoptr (WpConf) conf = wp_core_get_conf (core); g_autoptr (GError) error = NULL; g_autoptr (WpSpaJson) schema_json = NULL; diff --git a/src/main.c b/src/main.c index a1785025..59b9ca42 100644 --- a/src/main.c +++ b/src/main.c @@ -32,7 +32,7 @@ static GOptionEntry entries[] = { "version", 'v', G_OPTION_FLAG_NONE, G_OPTION_ARG_NONE, &show_version, "Show version", NULL }, { "config-file", 'c', G_OPTION_FLAG_NONE, G_OPTION_ARG_STRING, &config_file, - "The context configuration file", NULL }, + "The configuration file to use", NULL }, { "profile", 'p', G_OPTION_FLAG_NONE, G_OPTION_ARG_STRING, &profile, "The profile to load", NULL }, { NULL } @@ -129,7 +129,7 @@ main (gint argc, gchar **argv) g_autoptr (GOptionContext) context = NULL; g_autoptr (GError) error = NULL; g_autoptr (WpProperties) properties = NULL; - const gchar *conf_env; + g_autoptr (WpConf) conf = NULL; setlocale (LC_ALL, ""); setlocale (LC_NUMERIC, "C"); @@ -157,13 +157,15 @@ main (gint argc, gchar **argv) if (!profile) profile = "main"; - /* Forward WIREPLUMBER_CONFIG_DIR to PIPEWIRE_CONFIG_DIR */ - conf_env = g_getenv ("WIREPLUMBER_CONFIG_DIR"); - if (conf_env) - g_setenv ("PIPEWIRE_CONFIG_DIR", conf_env, TRUE); + /* load configuration */ + conf = wp_conf_new_open (config_file, NULL, &error); + if (!conf) { + fprintf (stderr, "Failed to load configuration: %s\n", error->message); + return WP_EXIT_CONFIG; + } + /* prepare core properties */ properties = wp_properties_new ( - PW_KEY_CONFIG_NAME, config_file, PW_KEY_APP_NAME, "WirePlumber", "wireplumber.daemon", "true", "wireplumber.profile", profile, @@ -176,7 +178,8 @@ main (gint argc, gchar **argv) /* init wireplumber daemon */ d.loop = g_main_loop_new (NULL, FALSE); - d.core = wp_core_new (NULL, g_steal_pointer (&properties)); + d.core = wp_core_new (NULL, g_steal_pointer (&conf), + g_steal_pointer (&properties)); g_signal_connect (d.core, "disconnected", G_CALLBACK (on_disconnected), &d); /* watch for exit signals */ diff --git a/src/tools/wpctl.c b/src/tools/wpctl.c index e42efe87..29ef2c61 100644 --- a/src/tools/wpctl.c +++ b/src/tools/wpctl.c @@ -1888,7 +1888,7 @@ main (gint argc, gchar **argv) ctl.context = g_option_context_new ( "COMMAND [COMMAND_OPTIONS] - WirePlumber Control CLI"); ctl.loop = g_main_loop_new (NULL, FALSE); - ctl.core = wp_core_new (NULL, NULL); + ctl.core = wp_core_new (NULL, NULL, NULL); ctl.om = wp_object_manager_new (); /* find the subcommand */ diff --git a/src/tools/wpexec.c b/src/tools/wpexec.c index fdf4d1cf..c50d0a4c 100644 --- a/src/tools/wpexec.c +++ b/src/tools/wpexec.c @@ -232,7 +232,7 @@ main (gint argc, gchar **argv) /* init wireplumber core */ d.loop = g_main_loop_new (NULL, FALSE); - d.core = wp_core_new (NULL, wp_properties_new ( + d.core = wp_core_new (NULL, NULL, wp_properties_new ( PW_KEY_APP_NAME, "wpexec", NULL)); g_signal_connect_swapped (d.core, "disconnected", diff --git a/tests/common/base-test-fixture.h b/tests/common/base-test-fixture.h index c1925911..1fdc854c 100644 --- a/tests/common/base-test-fixture.h +++ b/tests/common/base-test-fixture.h @@ -72,6 +72,7 @@ static void wp_base_test_fixture_setup (WpBaseTestFixture * self, WpBaseTestFlags flags) { g_autoptr (WpProperties) props = NULL; + g_autoptr (WpConf) conf = NULL; /* init test server */ wp_test_server_setup (&self->server); @@ -90,10 +91,15 @@ wp_base_test_fixture_setup (WpBaseTestFixture * self, WpBaseTestFlags flags) /* init our core */ props = wp_properties_new (PW_KEY_REMOTE_NAME, self->server.name, NULL); - if (self->conf_file) - wp_properties_set (props, PW_KEY_CONFIG_NAME, self->conf_file); + if (self->conf_file) { + g_autoptr (GError) error = NULL; + conf = wp_conf_new_open (self->conf_file, NULL, &error); + g_assert_no_error (error); + g_assert_nonnull (conf); + } - self->core = wp_core_new (self->context, wp_properties_ref (props)); + self->core = wp_core_new (self->context, g_steal_pointer (&conf), + wp_properties_ref (props)); g_assert_true (self->core); g_signal_connect (self->core, "disconnected", @@ -108,7 +114,8 @@ wp_base_test_fixture_setup (WpBaseTestFixture * self, WpBaseTestFlags flags) /* init the second client's core */ if (flags & WP_BASE_TEST_FLAG_CLIENT_CORE) { - self->client_core = wp_core_new (self->context, wp_properties_ref (props)); + self->client_core = wp_core_new (self->context, NULL, + wp_properties_ref (props)); g_signal_connect (self->client_core, "disconnected", (GCallback) disconnected_callback, self); diff --git a/tests/wp/conf.c b/tests/wp/conf.c index 5e0ce44f..423cd02f 100644 --- a/tests/wp/conf.c +++ b/tests/wp/conf.c @@ -5,27 +5,28 @@ * * SPDX-License-Identifier: MIT */ -#include "../common/base-test-fixture.h" + +#include "../common/test-log.h" typedef struct { - WpBaseTestFixture base; WpConf *conf; } TestConfFixture; static void test_conf_setup (TestConfFixture *self, gconstpointer user_data) { - self->base.conf_file = + g_autoptr (GError) error = NULL; + g_autofree gchar *file = g_strdup_printf ("%s/conf/wireplumber.conf", g_getenv ("G_TEST_SRCDIR")); - wp_base_test_fixture_setup (&self->base, WP_BASE_TEST_FLAG_CLIENT_CORE); - self->conf = wp_conf_get_instance (self->base.core); + self->conf = wp_conf_new_open (file, NULL, &error); + g_assert_no_error (error); + g_assert_nonnull (self->conf); } static void test_conf_teardown (TestConfFixture *self, gconstpointer user_data) { g_clear_object (&self->conf); - wp_base_test_fixture_teardown (&self->base); } static void diff --git a/tests/wp/settings.c b/tests/wp/settings.c index de4305ff..d29884b5 100644 --- a/tests/wp/settings.c +++ b/tests/wp/settings.c @@ -73,7 +73,7 @@ test_parsing_setup (TestSettingsFixture *self, gconstpointer user_data) { test_conf_file_setup (self, user_data); - g_autoptr (WpConf) conf = wp_conf_get_instance (self->base.core); + g_autoptr (WpConf) conf = wp_core_get_conf (self->base.core); g_assert_nonnull (conf); {