From e9391b195fa904e770db20a0ca70c19ad9b6cec1 Mon Sep 17 00:00:00 2001 From: Ashok Sidipotu Date: Tue, 8 Mar 2022 06:58:00 +0530 Subject: [PATCH] m-settings: Module to handle JSON config settings. - parse settings from .conf file. - create "sm-settings" metadata and copy settings as key value pairs to it. - put in place "persistent" behavior, control it with a special setting. - when persistent behavior is enabled. - create a state file and update settings to it. - monitor changes in the metadata and update the settings to state file. --- modules/meson.build | 11 ++ modules/module-settings.c | 330 ++++++++++++++++++++++++++++++++++++ src/config/wireplumber.conf | 210 +++++++++++++++++++++++ 3 files changed, 551 insertions(+) create mode 100644 modules/module-settings.c diff --git a/modules/meson.build b/modules/meson.build index 4930bfae..a9b836c0 100644 --- a/modules/meson.build +++ b/modules/meson.build @@ -3,6 +3,17 @@ common_c_args = [ '-DG_LOG_USE_STRUCTURED', ] +shared_library( + 'wireplumber-module-settings', + [ + 'module-settings.c', + ], + c_args : [common_c_args, '-DG_LOG_DOMAIN="m-settings"'], + install : true, + install_dir : wireplumber_module_dir, + dependencies : [wp_dep, pipewire_dep], +) + shared_library( 'wireplumber-module-metadata', [ diff --git a/modules/module-settings.c b/modules/module-settings.c new file mode 100644 index 00000000..d887a4dd --- /dev/null +++ b/modules/module-settings.c @@ -0,0 +1,330 @@ +/* WirePlumber + * + * Copyright © 2022 Collabora Ltd. + * @author Ashok Sidipotu + * + * SPDX-License-Identifier: MIT + */ + +#include +#include +#include +#include + +/* + * This module parses the "wireplumber.settings" section from the .conf file. + * + * Creates "sm-settings" 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; + + WpImplMetadata *impl_metadata; + + WpProperties *settings; + + WpState *state; + + GSource *timeout_source; + guint save_interval_ms; + gboolean use_persistent_storage; +}; + +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)) { + /* "wireplumber.settings" 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 \"wireplumber.settings\" settings cannot be loaded")); + 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); + wp_iterator_next (iter, &item); + 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 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 \"sm-settings\": \ + Metadata object "); + 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 (!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 \"sm-settings\" metadata", + wp_properties_get_count (self->settings)); + + + /* 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, "sm-settings", 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); +} + +static void +wp_settings_plugin_class_init (WpSettingsPluginClass * klass) +{ + WpPluginClass *plugin_class = (WpPluginClass *) klass; + + plugin_class->enable = wp_settings_plugin_enable; + plugin_class->disable = wp_settings_plugin_disable; +} + +WP_PLUGIN_EXPORT gboolean +wireplumber__module_init (WpCore * core, GVariant * args, GError ** error) +{ + wp_plugin_register (g_object_new (wp_settings_plugin_get_type (), + "name", "settings", + "core", core, + NULL)); + return TRUE; +} diff --git a/src/config/wireplumber.conf b/src/config/wireplumber.conf index 35473e29..27b287ed 100644 --- a/src/config/wireplumber.conf +++ b/src/config/wireplumber.conf @@ -85,9 +85,219 @@ wireplumber.components = [ # The lua scripting engine { name = libwireplumber-module-lua-scripting, type = module } + # Parses all the wireplumber settings in the .conf file, loads them into a + # "sm-settings" pipewire metadata and updates the settings to a state file, + # when persitent behavior is enabled. + + { name = libwireplumber-module-settings, type = module } + # The lua configuration file(s) # Other components are loaded from there { name = main.lua, type = config/lua } { name = policy.lua, type = config/lua } { name = bluetooth.lua, type = config/lua } ] + +wireplumber.settings = { + # These settings and rules will be loaded during bootup. + # + # wireplumber JSON settings interface. + # = + # + # + # wireplumber rule/logic based JSON settings interface. + # = [ + # { + # matches = [ + # { + # = <~value*> + ## if a value starts with "~" then it triggers regular experssion evaluation + # = + ## If properties are grouped like this in a single object, that means they + ## must all must match ("AND" behaviour). If they are in separate objects + ## with in the same match, then any object is a separate match + ## ("OR" behaviour). + # } + # { : } + # { } + ## "=", ":" or just a space, all are interpreted the same. + ## Multiple properties matched in a single object are ANDed and across the + ## objects are ORed + # ] + # actions = { + # update-props = { + # = , + # = , + # } + # } + # } + # + # if "persistent.settings" is true, the settings will be read from conf file + # only once and for subsequent reboots they will be read from the state files, + # till the time the setting is set to false. + # + persistent.settings = false + access.enable-flatpak-portal = true + access = [ + { + matches = [ + { + pipewire.access = "flatpak" + media.category = "Manager" + } + ] + actions = { + update-props = { + default_permissions = "all", + } + } + } + + { + matches = [ + { + pipewire.access = "flatpak" + } + ] + actions = { + update-props = { + default_permissions = "rx", + } + } + } + + { + matches = [ + { + pipewire.access = "restricted" + } + ] + actions = { + quirks = { + default_permissions = "rx", + } + } + } + ] + # store preferences to the file system and restore them at startup; + # when set to false, default nodes and routes are selected based on + # their priorities and any runtime changes do not persist after restart + device.use-persistent-storage = true + + # the default volume to apply to ACP device nodes, in the linear scale + # default-volume = 0.4 + + # Whether to auto-switch to echo cancel sink and source nodes or not + device.auto-echo-cancel = true + + # Sets the default echo-cancel-sink node name to automatically switch to + device.echo-cancel-sink-name = echo-cancel-sink + + # Sets the default echo-cancel-source node name to automatically switch to + device.echo-cancel-source-name = echo-cancel-source + + device = [ + { + matches = [ + # Matches all devices + { device.name = "*" } + ] + actions = { + quirks = { + profile_names = [off pro-audio] + } + } + } + ] + + stream_default.restore-props = true + stream_default.restore-target = true + stream_default = [ + { + matches = [ + # Matches all devices + { application.name = "pw-play" } + ] + actions = { + update-props = { + state.restore-props = false + state.restore-target = false + } + } + } + ] + alsa_monitor.alsa.reserve = true + alsa_monitor.alsa.midi = true + alsa_monitor.alsa.monitoring = true + + alsa_monitor = [ + { + # An array of matches/actions to evaluate. + + # If you want to disable some devices or nodes, you can apply properties per device as the following example. + # The name can be found by running pw-cli ls Device, or pw-cli dump Device + # matches = [ + # { + # { device.name = "name_of_some_disabled_card" } + # } + # ] + # actions = { + # update-props = { + # device.disabled = true, + # } + # } + } + { + # Rules for matching a device or node. It is an array of + # properties that all need to match the regexp. If any of the + # matches work, the actions are executed for the object. + matches = [ + { + # This matches all cards. + { device.name = "alsa_card.*" } + } + ] + actions = { + update-props = { + #Use ALSA-Card-Profile devices. They use UCM or the profile + #configuration to configure the device and mixer settings. + api.alsa.use-acp = true, + + #Use UCM instead of profile when available. Can be + #disabled to skip trying to use the UCM profile. + #api.alsa.use-ucm = true + + #Don't use the hardware mixer for volume control. It + #will only use software volume. The mixer is still used + #to mute unused paths based on the selected port. + #api.alsa.soft-mixer = false + + #Ignore decibel settings of the driver. Can be used to + #work around buggy drivers that report wrong values. + #api.alsa.ignore-dB = false + + #The profile set to use for the device. Usually this is + #"default.conf" but can be changed with a udev rule or here. + #device.profile-set = "profileset-name", + + #The default active profile. Is by default set to "Off". + #device.profile = "default profile name", + + #Automatically select the best profile. This is the + #highest priority available profile. This is disabled + #here and instead implemented in the session manager + #where it can save and load previous preferences. + api.acp.auto-profile = false + + #Automatically switch to the highest priority available port. + #This is disabled here and implemented in the session manager instead. + api.acp.auto-port = false + + #Other properties can be set here. + #device.nick = "My Device" + } + } + } + + ] +}