wireplumber/lib/wp/conf.c
George Kiagiadakis 60382df63f 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).
2024-03-04 07:07:56 +00:00

716 lines
20 KiB
C

/* WirePlumber
*
* Copyright © 2022 Collabora Ltd.
* @author Julian Bouzas <julian.bouzas@collabora.com>
*
* SPDX-License-Identifier: MIT
*/
#include "conf.h"
#include "log.h"
#include "json-utils.h"
#include "base-dirs.h"
#include "error.h"
#include <pipewire/pipewire.h>
#include <spa/utils/result.h>
WP_DEFINE_LOCAL_LOG_TOPIC ("wp-conf")
#define OVERRIDE_SECTION_PREFIX "override."
/*! \defgroup wpconf WpConf */
/*!
* \struct WpConf
*
* WpConf allows accessing the different sections of the wireplumber
* 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 (&section->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 */
gchar *name;
/* Private */
GArray *conf_sections; /* element-type: WpConfSection */
GPtrArray *files; /* element-type: GMappedFile* */
};
enum {
PROP_0,
PROP_NAME,
};
G_DEFINE_TYPE (WpConf, wp_conf, G_TYPE_OBJECT)
static void
wp_conf_init (WpConf * self)
{
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
wp_conf_set_property (GObject * object, guint property_id,
const GValue * value, GParamSpec * pspec)
{
WpConf *self = WP_CONF (object);
switch (property_id) {
case PROP_NAME:
self->name = g_value_dup_string (value);
break;
default:
G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
break;
}
}
static void
wp_conf_get_property (GObject * object, guint property_id,
GValue * value, GParamSpec * pspec)
{
WpConf *self = WP_CONF (object);
switch (property_id) {
case PROP_NAME:
g_value_set_string (value, self->name);
break;
default:
G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
break;
}
}
static void
wp_conf_finalize (GObject * object)
{
WpConf *self = WP_CONF (object);
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);
}
static void
wp_conf_class_init (WpConfClass * klass)
{
GObjectClass * object_class = G_OBJECT_CLASS (klass);
object_class->finalize = wp_conf_finalize;
object_class->set_property = wp_conf_set_property;
object_class->get_property = wp_conf_get_property;
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 Creates a new WpConf object
*
* 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 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_new (const gchar * name, WpProperties * properties)
{
g_return_val_if_fail (name, NULL);
g_clear_pointer (&properties, wp_properties_unref);
return g_object_new (WP_TYPE_CONF, "name", name, NULL);
}
/*!
* \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)
{
g_return_val_if_fail (name, NULL);
g_autoptr (WpConf) self = wp_conf_new (name, properties);
if (!wp_conf_open (self, error))
return NULL;
return g_steal_pointer (&self);
}
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;
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));
g_array_set_clear_func (sections, (GDestroyNotify) wp_conf_section_clear);
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;
}
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 (&section, 0, sizeof (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 TRUE;
}
/*!
* \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_return_val_if_fail (WP_IS_CONF (self), FALSE);
g_autofree gchar *path = NULL;
g_autoptr (WpIterator) iterator = NULL;
g_auto (GValue) value = G_VALUE_INIT;
/* 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);
/* 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");
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);
}
/*!
* This method will get the JSON value of a specific section from the
* configuration. If the same section is defined in multiple locations, the
* sections with the same name will be either merged in case of arrays and
* objects, or overridden in case of boolean, int, double and strings. The
* passed fallback value will be returned if the section does not exist.
*
* \ingroup wpconf
* \param self the configuration
* \param section the section name
* \param fallback (transfer full)(nullable): the fallback value
* \returns (transfer full): the JSON value of the section
*/
WpSpaJson *
wp_conf_get_section (WpConf *self, const gchar *section, WpSpaJson *fallback)
{
g_autoptr (WpSpaJson) s = NULL;
g_autoptr (WpSpaJson) fb = fallback;
g_return_val_if_fail (WP_IS_CONF (self), NULL);
s = ensure_merged_section (self, section);
if (!s)
return fb ? g_steal_pointer (&fb) : NULL;
return g_steal_pointer (&s);
}
/*!
* This is a convenient function to access a JSON value from an object
* section in the configuration. If the section is an array, or the key does
* not exist in the object section, it will return the passed fallback value.
*
* \ingroup wpconf
* \param self the configuration
* \param section the section name
* \param key the key name
* \param fallback (transfer full)(nullable): the fallback value
* \returns (transfer full): the JSON value of the section's key if it exists,
* or the passed fallback value otherwise
*/
WpSpaJson *
wp_conf_get_value (WpConf *self, const gchar *section, const gchar *key,
WpSpaJson *fallback)
{
g_autoptr (WpSpaJson) s = NULL;
g_autoptr (WpSpaJson) fb = fallback;
WpSpaJson *v;
g_return_val_if_fail (WP_IS_CONF (self), NULL);
g_return_val_if_fail (section, NULL);
g_return_val_if_fail (key, NULL);
s = wp_conf_get_section (self, section, NULL);
if (!s)
goto return_fallback;
if (!wp_spa_json_is_object (s)) {
wp_warning_object (self,
"Cannot get JSON key %s from %s as section is not an JSON object",
key, section);
goto return_fallback;
}
if (wp_spa_json_object_get (s, key, "J", &v, NULL))
return v;
return_fallback:
return fb ? g_steal_pointer (&fb) : NULL;
}
/*!
* This is a convenient function to access a boolean value from an object
* section in the configuration. If the section is an array, or the key does
* not exist in the object section, it will return the passed fallback value.
*
* \ingroup wpconf
* \param self the configuration
* \param section the section name
* \param key the key name
* \param fallback the fallback value
* \returns the boolean value of the section's key if it exists and could be
* parsed, or the passed fallback value otherwise
*/
gboolean
wp_conf_get_value_boolean (WpConf *self, const gchar *section,
const gchar *key, gboolean fallback)
{
g_autoptr (WpSpaJson) s = NULL;
gboolean v;
g_return_val_if_fail (WP_IS_CONF (self), FALSE);
g_return_val_if_fail (section, FALSE);
g_return_val_if_fail (key, FALSE);
s = wp_conf_get_section (self, section, NULL);
if (!s)
return fallback;
if (!wp_spa_json_is_object (s)) {
wp_warning_object (self,
"Cannot get boolean key %s from %s as section is not an JSON object",
key, section);
return fallback;
}
return wp_spa_json_object_get (s, key, "b", &v, NULL) ? v : fallback;
}
/*!
* This is a convenient function to access a int value from an object
* section in the configuration. If the section is an array, or the key does
* not exist in the object section, it will return the passed fallback value.
*
* \ingroup wpconf
* \param self the configuration
* \param section the section name
* \param key the key name
* \param fallback the fallback value
* \returns the int value of the section's key if it exists and could be
* parsed, or the passed fallback value otherwise
*/
gint
wp_conf_get_value_int (WpConf *self, const gchar *section,
const gchar *key, gint fallback)
{
g_autoptr (WpSpaJson) s = NULL;
gint v;
g_return_val_if_fail (WP_IS_CONF (self), 0);
g_return_val_if_fail (section, 0);
g_return_val_if_fail (key, 0);
s = wp_conf_get_section (self, section, NULL);
if (!s)
return fallback;
if (!wp_spa_json_is_object (s)) {
wp_warning_object (self,
"Cannot get int key %s from %s as section is not an JSON object",
key, section);
return fallback;
}
return wp_spa_json_object_get (s, key, "i", &v, NULL) ? v : fallback;
}
/*!
* This is a convenient function to access a float value from an object
* section in the configuration. If the section is an array, or the key does
* not exist in the object section, it will return the passed fallback value.
*
* \ingroup wpconf
* \param self the configuration
* \param section the section name
* \param key the key name
* \param fallback the fallback value
* \returns the float value of the section's key if it exists and could be
* parsed, or the passed fallback value otherwise
*/
float
wp_conf_get_value_float (WpConf *self, const gchar *section,
const gchar *key, float fallback)
{
g_autoptr (WpSpaJson) s = NULL;
float v;
g_return_val_if_fail (WP_IS_CONF (self), 0);
g_return_val_if_fail (section, 0);
g_return_val_if_fail (key, 0);
s = wp_conf_get_section (self, section, NULL);
if (!s)
return fallback;
if (!wp_spa_json_is_object (s)) {
wp_warning_object (self,
"Cannot get float key %s from %s as section is not an JSON object",
key, section);
return fallback;
}
return wp_spa_json_object_get (s, key, "f", &v, NULL) ? v : fallback;
}
/*!
* This is a convenient function to access a string value from an object
* section in the configuration. If the section is an array, or the key does
* not exist in the object section, it will return the passed fallback value.
*
* \ingroup wpconf
* \param self the configuration
* \param section the section name
* \param key the key name
* \param fallback (nullable): the fallback value
* \returns (transfer full): the string value of the section's key if it exists
* and could be parsed, or the passed fallback value otherwise
*/
gchar *
wp_conf_get_value_string (WpConf *self, const gchar *section,
const gchar *key, const gchar *fallback)
{
g_autoptr (WpSpaJson) s = NULL;
gchar *v;
g_return_val_if_fail (WP_IS_CONF (self), NULL);
g_return_val_if_fail (section, NULL);
g_return_val_if_fail (key, NULL);
s = wp_conf_get_section (self, section, NULL);
if (!s)
goto return_fallback;
if (!wp_spa_json_is_object (s)) {
wp_warning_object (self,
"Cannot get string key %s from %s as section is not an JSON object",
key, section);
goto return_fallback;
}
if (wp_spa_json_object_get (s, key, "s", &v, NULL))
return v;
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;
}