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); {