Files
wireplumber/modules/module-settings.c
Ashok Sidipotu 3011a1c07e m-settings: Fix the crash with empty settings
Add a null check when iterator gets the value to detect the end of the
JSON object, this will ensure the rest of the code that operates on the
value is bypassed.

Still it must be noted that an empty setting screws up the following
settings and behavior is undefined, this is the limiation in the
spa-json parsing.
2023-04-17 07:44:19 -04:00

412 lines
12 KiB
C

/* WirePlumber
*
* Copyright © 2022 Collabora Ltd.
* @author Ashok Sidipotu <ashok.sidipotu@collabora.com>
*
* SPDX-License-Identifier: MIT
*/
#include <wp/wp.h>
#include <pipewire/pipewire.h>
#include <spa/utils/json.h>
#include <spa/utils/defs.h>
/*
* This module parses the "wireplumber.settings" section from the .conf file.
*
* Creates "sm-settings"(default) metadata and pushes the settings to it.
* Looks out for changes done in the metadata via the pw-metadata interface.
*
* If persistent settings is enabled stores the settings in a state file
* and retains the settings from there on subsequent reboots ignoring the
* contents of .conf file.
*/
struct _WpSettingsPlugin
{
WpPlugin parent;
gchar *metadata_name;
WpImplMetadata *impl_metadata;
WpProperties *settings;
WpState *state;
GSource *timeout_source;
guint save_interval_ms;
gboolean use_persistent_storage;
};
enum {
PROP_0,
PROP_METADATA_NAME,
PROP_PROPERTIES,
};
G_DECLARE_FINAL_TYPE (WpSettingsPlugin, wp_settings_plugin,
WP, SETTINGS_PLUGIN, WpPlugin)
G_DEFINE_TYPE (WpSettingsPlugin, wp_settings_plugin, WP_TYPE_PLUGIN)
#define NAME "sm-settings"
#define PERSISTENT_SETTING "persistent.settings"
static void
wp_settings_plugin_init (WpSettingsPlugin * self)
{
}
static gboolean
timeout_save_state_callback (WpSettingsPlugin *self)
{
g_autoptr (GError) error = NULL;
if (!self->state)
self->state = wp_state_new (NAME);
if (!wp_state_save (self->state, self->settings, &error))
wp_warning_object (self, "%s", error->message);
g_clear_pointer (&self->timeout_source, g_source_unref);
return G_SOURCE_REMOVE;
}
static void
timer_start (WpSettingsPlugin *self)
{
if (!self->timeout_source && self->use_persistent_storage) {
g_autoptr (WpCore) core = wp_object_get_core (WP_OBJECT (self));
g_return_if_fail (core);
/* Add the timeout callback */
wp_core_timeout_add_closure (core, &self->timeout_source,
self->save_interval_ms,
g_cclosure_new_object (
G_CALLBACK (timeout_save_state_callback), G_OBJECT (self)));
}
}
static gboolean
settings_available_in_state_file (WpSettingsPlugin * self)
{
g_autoptr (WpState) state = wp_state_new (NAME);
g_autoptr (WpProperties) settings = wp_state_load (state);
guint count = wp_properties_get_count(settings);
if (count > 0) {
wp_info_object (self, "%d settings are available in state file", count);
self->state = g_steal_pointer (&state);
self->use_persistent_storage = true;
return true;
} else
wp_info_object (self, "no settings are available in state file");
return false;
}
static void
on_metadata_changed (WpMetadata *m, guint32 subject,
const gchar *setting, const gchar *type, const gchar *new_value, gpointer d)
{
WpSettingsPlugin *self = WP_SETTINGS_PLUGIN(d);
const gchar *old_value = wp_properties_get (self->settings, setting);
if (!old_value) {
wp_info_object (self, "new setting defined \"%s\" = \"%s\"",
setting, new_value);
} else {
wp_info_object (self, "setting \"%s\" new_value changed from \"%s\" ->"
" \"%s\"", setting, old_value, new_value);
}
wp_properties_set (self->settings, setting, new_value);
/* update the state */
timer_start (self);
}
struct data {
WpTransition *transition;
int count;
WpProperties *settings;
};
static int
do_parse_settings (void *data, const char *location,
const char *section, const char *str, size_t len)
{
struct data *d = data;
WpTransition *transition = d->transition;
WpSettingsPlugin *self = wp_transition_get_source_object (transition);
g_autoptr (WpSpaJson) json = wp_spa_json_new_from_stringn (str, len);
g_autoptr (WpIterator) iter = wp_spa_json_new_iterator (json);
g_auto (GValue) item = G_VALUE_INIT;
if (!wp_spa_json_is_object (json)) {
/* section has to be a JSON object element. */
wp_transition_return_error (transition, g_error_new (
WP_DOMAIN_LIBRARY, WP_LIBRARY_ERROR_OPERATION_FAILED,
"failed to parse the section"));
return -EINVAL;
}
while (wp_iterator_next (iter, &item)) {
WpSpaJson *j = g_value_get_boxed (&item);
g_autofree gchar *name = wp_spa_json_parse_string (j);
g_autofree gchar *value = NULL;
int len = 0;
g_value_unset (&item);
if (!wp_iterator_next (iter, &item))
break;
j = g_value_get_boxed (&item);
value = wp_spa_json_parse_string (j);
len = wp_spa_json_get_size (j);
g_value_unset (&item);
if (name && value) {
wp_debug_object (self, "%s(%d) = %s", name, len, value);
wp_properties_set (d->settings, name, value);
if (g_str_equal (name, PERSISTENT_SETTING) && spa_atob (value)) {
self->use_persistent_storage = true;
wp_info_object (self, "persistent settings enabled");
}
d->count++;
}
}
wp_info_object (self, "parsed %d settings & rules from conf file", d->count);
return 0;
}
static int
do_parse_endpoints (void *data, const char *location,
const char *section, const char *str, size_t len)
{
return do_parse_settings (data, location, section, str, len);
}
static void
on_metadata_activated (WpMetadata * m, GAsyncResult * res, gpointer user_data)
{
WpTransition *transition = WP_TRANSITION (user_data);
WpSettingsPlugin *self = wp_transition_get_source_object (transition);
g_autoptr (GError) error = NULL;
g_autoptr (WpCore) core = wp_object_get_core (WP_OBJECT (self));
struct pw_context *pw_ctx = wp_core_get_pw_context (core);
g_autoptr (WpProperties) settings = wp_properties_new_empty();
struct data data = { .transition = transition,
.settings = settings };
g_autoptr (WpIterator) it = NULL;
g_auto (GValue) item = G_VALUE_INIT;
if (!wp_object_activate_finish (WP_OBJECT (m), res, &error)) {
g_clear_object (&self->impl_metadata);
g_prefix_error (&error, "Failed to activate \"%s\": "
"Metadata object ", self->metadata_name);
wp_transition_return_error (transition, g_steal_pointer (&error));
return;
}
wp_object_update_features (WP_OBJECT (self), WP_PLUGIN_FEATURE_ENABLED, 0);
if (pw_context_conf_section_for_each (pw_ctx, "wireplumber.settings",
do_parse_settings, &data) < 0)
return;
if (data.count == 0) {
/*
* either the "wireplumber.settings" is not found or not defined as a
* valid JSON object element.
*/
wp_transition_return_error (transition, g_error_new (
WP_DOMAIN_LIBRARY, WP_LIBRARY_ERROR_OPERATION_FAILED,
"No settings present in the context conf file: settings "
"are not loaded"));
return;
}
if (pw_context_conf_section_for_each (pw_ctx, "wireplumber.endpoints",
do_parse_endpoints, &data) < 0)
return;
if (data.count == 0) {
/*
* either the "wireplumber.endpoints" is not found or not defined as a
* valid JSON object element.
*/
wp_transition_return_error (transition, g_error_new (
WP_DOMAIN_LIBRARY, WP_LIBRARY_ERROR_OPERATION_FAILED,
"No endpoints present in the context conf file: endpoints "
"are not loaded"));
return;
}
if (!self->use_persistent_storage) {
/* use parsed settings from .conf file */
self->settings = g_steal_pointer (&settings);
wp_info_object(self, "use settings from .conf file");
if (settings_available_in_state_file (self)) {
wp_info_object (self, "persistant storage is disabled clear the"
" settings in the state file");
wp_state_clear (self->state);
g_clear_object (&self->state);
}
} else if (self->use_persistent_storage) {
/* consider using settings from state file */
if (settings_available_in_state_file (self)) {
self->settings = wp_state_load (self->state);
wp_info_object (self, "persistant storage enabled and settings are found"
" in state file, use them");
}
else
{
wp_info_object (self, "persistant storage enabled but settings are"
" not found in state file so load from .conf file");
self->settings = g_steal_pointer (&settings);
/* save state after time out */
timer_start (self);
}
}
for (it = wp_properties_new_iterator (self->settings);
wp_iterator_next (it, &item);
g_value_unset (&item)) {
WpPropertiesItem *pi = g_value_get_boxed (&item);
const gchar *setting = wp_properties_item_get_key (pi);
const gchar *value = wp_properties_item_get_value (pi);
wp_debug_object (self, "%s(%lu) = %s", setting, strlen(value), value);
wp_metadata_set (m, 0, setting, "Spa:String:JSON", value);
}
wp_info_object (self, "loaded settings(%d) to \"%s\" metadata",
wp_properties_get_count (self->settings), self->metadata_name);
/* monitor changes in metadata. */
g_signal_connect_object (m, "changed", G_CALLBACK (on_metadata_changed),
self, 0);
}
static void
wp_settings_plugin_enable (WpPlugin * plugin, WpTransition * transition)
{
WpSettingsPlugin * self = WP_SETTINGS_PLUGIN (plugin);
g_autoptr (WpCore) core = wp_object_get_core (WP_OBJECT (plugin));
g_return_if_fail (core);
self->use_persistent_storage = false;
/* create metadata object */
self->impl_metadata = wp_impl_metadata_new_full (core, self->metadata_name,
NULL);
wp_object_activate (WP_OBJECT (self->impl_metadata),
WP_OBJECT_FEATURES_ALL,
NULL,
(GAsyncReadyCallback)on_metadata_activated,
transition);
}
static void
wp_settings_plugin_disable (WpPlugin * plugin)
{
WpSettingsPlugin * self = WP_SETTINGS_PLUGIN (plugin);
if (self->timeout_source)
g_source_destroy (self->timeout_source);
g_clear_pointer (&self->timeout_source, g_source_unref);
g_clear_pointer (&self->settings, wp_properties_unref);
g_clear_object (&self->impl_metadata);
g_clear_object (&self->state);
g_free (self->metadata_name);
}
static void
wp_settings_plugin_set_property (GObject * object, guint property_id,
const GValue * value, GParamSpec * pspec)
{
WpSettingsPlugin *self = WP_SETTINGS_PLUGIN (object);
switch (property_id) {
case PROP_METADATA_NAME:
self->metadata_name = g_strdup (g_value_get_string (value));
break;
default:
G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
break;
}
}
static void
wp_settings_plugin_get_property (GObject * object, guint property_id,
GValue * value, GParamSpec * pspec)
{
WpSettingsPlugin *self = WP_SETTINGS_PLUGIN (object);
switch (property_id) {
case PROP_METADATA_NAME:
g_value_set_string (value, self->metadata_name);
break;
default:
G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
break;
}
}
static void
wp_settings_plugin_class_init (WpSettingsPluginClass * klass)
{
WpPluginClass *plugin_class = (WpPluginClass *) klass;
GObjectClass *object_class = (GObjectClass *) klass;
plugin_class->enable = wp_settings_plugin_enable;
plugin_class->disable = wp_settings_plugin_disable;
object_class->set_property = wp_settings_plugin_set_property;
object_class->get_property = wp_settings_plugin_get_property;
g_object_class_install_property (object_class, PROP_METADATA_NAME,
g_param_spec_string ("metadata-name", "metadata-name",
"The metadata object to look after", NULL,
G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS));
}
WP_PLUGIN_EXPORT gboolean
wireplumber__module_init (WpCore * core, GVariant * args, GError ** error)
{
const gchar *metadata_name = "sm-settings";
if (args) {
metadata_name = g_variant_get_string(args, NULL);
}
wp_plugin_register (g_object_new (wp_settings_plugin_get_type (),
"name", "settings",
"core", core,
"metadata-name", metadata_name,
NULL));
return TRUE;
}