diff --git a/docs/rst/library/c_api/state_api.rst b/docs/rst/library/c_api/state_api.rst index 36e4dd4d..41d7d0b0 100644 --- a/docs/rst/library/c_api/state_api.rst +++ b/docs/rst/library/c_api/state_api.rst @@ -8,9 +8,15 @@ State Storage digraph inheritance { rankdir=LR; GObject -> WpState; + GObject -> WpObject -> WpStateMetadata; } .. doxygenstruct:: WpState .. doxygengroup:: wpstate :content-only: + +.. doxygenstruct:: WpStateMetadata + +.. doxygengroup:: wpstatemetadata + :content-only: diff --git a/lib/wp/state.c b/lib/wp/state.c index 85f9d400..6e393e77 100644 --- a/lib/wp/state.c +++ b/lib/wp/state.c @@ -315,6 +315,39 @@ wp_state_clear (WpState *self) wp_warning ("failed to remove %s: %s", self->location, g_strerror (errno)); } +static gboolean +wp_state_save_internal (const gchar *name, const gchar *location, + WpProperties *props, GError ** error) +{ + g_autoptr (GKeyFile) keyfile = g_key_file_new (); + g_autoptr (WpIterator) it = NULL; + g_auto (GValue) item = G_VALUE_INIT; + GError *err = NULL; + + g_return_val_if_fail (name, FALSE); + g_return_val_if_fail (location, FALSE); + g_return_val_if_fail (props, FALSE); + + /* Set the properties */ + for (it = wp_properties_new_iterator (props); + wp_iterator_next (it, &item); + g_value_unset (&item)) { + WpPropertiesItem *pi = g_value_get_boxed (&item); + const gchar *key = wp_properties_item_get_key (pi); + const gchar *val = wp_properties_item_get_value (pi); + g_autofree gchar *escaped_key = escape_string (key); + if (escaped_key) + g_key_file_set_string (keyfile, name, escaped_key, val); + } + + if (!g_key_file_save_to_file (keyfile, location, &err)) { + g_propagate_prefixed_error (error, err, "could not save %s: ", name); + return FALSE; + } + + return TRUE; +} + /*! * \brief Saves new properties in the state, overwriting all previous data. * \ingroup wpstate @@ -326,35 +359,10 @@ wp_state_clear (WpState *self) gboolean wp_state_save (WpState *self, WpProperties *props, GError ** error) { - g_autoptr (GKeyFile) keyfile = g_key_file_new (); - g_autoptr (WpIterator) it = NULL; - g_auto (GValue) item = G_VALUE_INIT; - GError *err = NULL; - g_return_val_if_fail (WP_IS_STATE (self), FALSE); - g_return_val_if_fail (props, FALSE); + wp_state_ensure_location (self); - - wp_info_object (self, "saving state into %s", self->location); - - /* Set the properties */ - for (it = wp_properties_new_iterator (props); - wp_iterator_next (it, &item); - g_value_unset (&item)) { - WpPropertiesItem *pi = g_value_get_boxed (&item); - const gchar *key = wp_properties_item_get_key (pi); - const gchar *val = wp_properties_item_get_value (pi); - g_autofree gchar *escaped_key = escape_string (key); - if (escaped_key) - g_key_file_set_string (keyfile, self->name, escaped_key, val); - } - - if (!g_key_file_save_to_file (keyfile, self->location, &err)) { - g_propagate_prefixed_error (error, err, "could not save %s: ", self->name); - return FALSE; - } - - return TRUE; + return wp_state_save_internal (self->name, self->location, props, error); } static gboolean @@ -408,6 +416,46 @@ wp_state_save_after_timeout (WpState *self, WpCore *core, WpProperties *props) G_OBJECT (self))); } +static WpProperties * +wp_state_load_internal (const gchar *name, const gchar *location) +{ + g_autoptr (GKeyFile) keyfile = NULL; + g_autoptr (WpProperties) props = NULL; + gchar ** keys = NULL; + + g_return_val_if_fail (name, NULL); + g_return_val_if_fail (location, NULL); + + props = wp_properties_new_empty (); + + /* Open */ + keyfile = g_key_file_new (); + if (!g_key_file_load_from_file (keyfile, location, + G_KEY_FILE_NONE, NULL)) + return g_steal_pointer (&props); + + /* Load all keys */ + keys = g_key_file_get_keys (keyfile, name, NULL, NULL); + if (!keys) + return g_steal_pointer (&props); + + for (guint i = 0; keys[i]; i++) { + g_autofree gchar *compressed_key = NULL; + const gchar *key = keys[i]; + g_autofree gchar *val = NULL; + val = g_key_file_get_string (keyfile, name, key, NULL); + if (!val) + continue; + compressed_key = compress_string (key); + if (compressed_key) + wp_properties_set (props, compressed_key, val); + } + + g_strfreev (keys); + + return g_steal_pointer (&props); +} + /*! * \brief Loads the state data from the file system * @@ -422,36 +470,437 @@ wp_state_save_after_timeout (WpState *self, WpCore *core, WpProperties *props) WpProperties * wp_state_load (WpState *self) { - g_autoptr (GKeyFile) keyfile = g_key_file_new (); - g_autoptr (WpProperties) props = wp_properties_new_empty (); - gchar ** keys = NULL; - g_return_val_if_fail (WP_IS_STATE (self), NULL); + wp_state_ensure_location (self); + return wp_state_load_internal (self->name, self->location); +} - /* Open */ - if (!g_key_file_load_from_file (keyfile, self->location, - G_KEY_FILE_NONE, NULL)) - return g_steal_pointer (&props); - /* Load all keys */ - keys = g_key_file_get_keys (keyfile, self->name, NULL, NULL); - if (!keys) - return g_steal_pointer (&props); +/* WpStateMetadata */ - for (guint i = 0; keys[i]; i++) { - g_autofree gchar *compressed_key = NULL; - const gchar *key = keys[i]; - g_autofree gchar *val = NULL; - val = g_key_file_get_string (keyfile, self->name, key, NULL); - if (!val) - continue; - compressed_key = compress_string (key); - if (compressed_key) - wp_properties_set (props, compressed_key, val); +/*! \defgroup wpstatemetadata WpStateMetadata */ +/*! + * \struct WpStateMetadata + * + * The WpStateMetadata class saves and loads properties from a file and reflects + * the state in a metadata object. + * + * \gproperties + * \gproperty{name, gchar *, G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY, + * The file name where the state will be stored.} + */ + +enum { + STATE_METADATA_PROP_0, + STATE_METADATA_PROP_NAME, + STATE_METADATA_PROP_TIMEOUT, +}; + +struct _WpStateMetadata +{ + WpObject parent; + + /* Props */ + gchar *name; + guint timeout; + + gchar *location; + WpProperties *metadata_props; + WpImplMetadata *metadata; + GSource *timeout_source; +}; + +G_DEFINE_TYPE (WpStateMetadata, wp_state_metadata, WP_TYPE_OBJECT) + +static void +wp_state_metadata_init (WpStateMetadata * self) +{ + self->timeout = DEFAULT_TIMEOUT_MS; +} + +static void +wp_state_metadata_set_property (GObject * object, guint property_id, + const GValue * value, GParamSpec * pspec) +{ + WpStateMetadata *self = WP_STATE_METADATA (object); + + switch (property_id) { + case STATE_METADATA_PROP_NAME: + g_clear_pointer (&self->name, g_free); + self->name = g_value_dup_string (value); + break; + case STATE_METADATA_PROP_TIMEOUT: + self->timeout = g_value_get_uint (value); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); + break; + } +} + +static void +wp_state_metadata_get_property (GObject * object, guint property_id, + GValue * value, GParamSpec * pspec) +{ + WpStateMetadata *self = WP_STATE_METADATA (object); + + switch (property_id) { + case STATE_METADATA_PROP_NAME: + g_value_set_string (value, self->name); + break; + case STATE_METADATA_PROP_TIMEOUT: + g_value_set_uint (value, self->timeout); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); + break; + } +} + +enum { + STEP_LOAD = WP_TRANSITION_STEP_CUSTOM_START, +}; + +static WpObjectFeatures +wp_state_metadata_get_supported_features (WpObject * self) +{ + return WP_STATE_METADATA_LOADED; +} + +static guint +wp_state_metadata_activate_get_next_step (WpObject * object, + WpFeatureActivationTransition * transition, guint step, + WpObjectFeatures missing) +{ + g_return_val_if_fail (missing == WP_STATE_METADATA_LOADED, + WP_TRANSITION_STEP_ERROR); + + return STEP_LOAD; +} + +static void +state_metadata_ensure_location (WpStateMetadata *self) +{ + if (!self->location) + self->location = get_new_location (self->name); + g_return_if_fail (self->location); +} + +static WpProperties * +state_metadata_load (WpStateMetadata *self) +{ + state_metadata_ensure_location (self); + return wp_state_load_internal (self->name, self->location); +} + +static gboolean +state_metadata_save (WpStateMetadata *self, WpProperties *props, + GError ** error) +{ + state_metadata_ensure_location (self); + return wp_state_save_internal (self->name, self->location, props, error); +} + +static gboolean +state_metadata_timeout_save_cb (WpStateMetadata *self) +{ + g_autoptr (GError) error = NULL; + + if (!state_metadata_save (self, self->metadata_props, &error)) + wp_warning_object (self, "%s", error->message); + + g_clear_pointer (&self->timeout_source, g_source_unref); + + wp_info_object (self, "saved changes on state metadata '%s'", self->name); + return G_SOURCE_REMOVE; +} + +static void +state_metadata_save_after_timeout (WpStateMetadata *self) +{ + g_autoptr (WpCore) core = wp_object_get_core (WP_OBJECT (self)); + g_return_if_fail (core); + + /* Clear the current timeout callback */ + if (self->timeout_source) + g_source_destroy (self->timeout_source); + g_clear_pointer (&self->timeout_source, g_source_unref); + + /* Add the timeout callback */ + wp_core_timeout_add_closure (core, &self->timeout_source, self->timeout, + g_cclosure_new_object (G_CALLBACK (state_metadata_timeout_save_cb), + G_OBJECT (self))); +} + +static void +state_metadata_clear (WpStateMetadata *self) +{ + if (self->metadata_props) + wp_properties_clear (self->metadata_props); + + state_metadata_ensure_location (self); + if (remove (self->location) < 0) + wp_warning ("failed to remove %s: %s", self->location, g_strerror (errno)); +} + +static void +on_state_metadata_changed (WpMetadata *m, guint32 subject, + const gchar *key, const gchar *type, const gchar *value, gpointer d) +{ + WpStateMetadata * self = WP_STATE_METADATA (d); + + /* Save new key after timeout if valid. Otherwise just clear the state */ + if (key) { + wp_properties_set (self->metadata_props, key, value); + if (value) + wp_info_object (self, "key changed on state metadata '%s': %s = %s", + self->name, key, value); + else + wp_info_object (self, "key removed on state metadata '%s': %s", + self->name, key); + state_metadata_save_after_timeout (self); + } else { + state_metadata_clear (self); + wp_info_object (self, "cleared state metadata '%s'", self->name); + } +} + +static void +on_metadata_activated (WpObject * proxy, GAsyncResult * res, + WpTransition * transition) +{ + WpStateMetadata *self = wp_transition_get_source_object (transition); + g_autoptr (GError) error = NULL; + g_autoptr (WpIterator) it = NULL; + GValue v = G_VALUE_INIT; + + /* Make sure there were no errors when activating the metadata */ + if (!wp_object_activate_finish (proxy, res, &error)) { + wp_transition_return_error (transition, g_error_new ( + WP_DOMAIN_LIBRARY, WP_LIBRARY_ERROR_OPERATION_FAILED, + "Failed to activate metadata for state %s: %s", self->name, + error->message)); + return; } - g_strfreev (keys); + /* Load the state keys into the metadata */ + g_return_if_fail (self->metadata_props == NULL); + self->metadata_props = state_metadata_load (self); + it = wp_properties_new_iterator (self->metadata_props); + while (wp_iterator_next (it, &v)) { + WpPropertiesItem *pi = g_value_get_boxed (&v); + const gchar *key = wp_properties_item_get_key (pi); + const gchar *value = wp_properties_item_get_value (pi); + wp_metadata_set (WP_METADATA (self->metadata), 0, key, NULL, value); + g_value_unset (&v); + } - return g_steal_pointer (&props); + /* Handle metadata changes */ + g_signal_connect_object (self->metadata, "changed", + G_CALLBACK (on_state_metadata_changed), self, 0); + + wp_object_update_features (WP_OBJECT (self), WP_STATE_METADATA_LOADED, 0); +} + +static void +wp_state_metadata_activate_execute_step (WpObject * object, + WpFeatureActivationTransition * transition, guint step, + WpObjectFeatures missing) +{ + WpStateMetadata * self = WP_STATE_METADATA (object); + g_autoptr (WpCore) core = wp_object_get_core (WP_OBJECT (self)); + + switch (step) { + case STEP_LOAD: { + g_return_if_fail (self->metadata == NULL); + self->metadata = wp_impl_metadata_new_full (core, self->name, + wp_properties_new ("wireplumber.state", "true", NULL)); + wp_object_activate_closure (WP_OBJECT (self->metadata), + WP_OBJECT_FEATURES_ALL, NULL, g_cclosure_new_object ( + (GCallback) on_metadata_activated, G_OBJECT (transition))); + break; + } + case WP_TRANSITION_STEP_ERROR: + break; + default: + g_assert_not_reached (); + } +} + +static void +wp_state_metadata_deactivate (WpObject * object, WpObjectFeatures features) +{ + WpStateMetadata *self = WP_STATE_METADATA (object); + + if (self->metadata) + g_signal_handlers_disconnect_by_data (self->metadata, self); + + g_clear_pointer (&self->metadata_props, wp_properties_unref); + g_clear_object (&self->metadata); + + wp_object_update_features (WP_OBJECT (self), 0, WP_OBJECT_FEATURES_ALL); +} + +static void +wp_state_metadata_finalize (GObject * object) +{ + WpStateMetadata * self = WP_STATE_METADATA (object); + + g_clear_pointer (&self->name, g_free); + g_clear_pointer (&self->location, g_free); + g_clear_pointer (&self->timeout_source, g_source_unref); + + G_OBJECT_CLASS (wp_state_metadata_parent_class)->finalize (object); +} + +static void +wp_state_metadata_class_init (WpStateMetadataClass * klass) +{ + GObjectClass *object_class = (GObjectClass *) klass; + WpObjectClass * wpobject_class = (WpObjectClass *) klass; + + object_class->finalize = wp_state_metadata_finalize; + object_class->set_property = wp_state_metadata_set_property; + object_class->get_property = wp_state_metadata_get_property; + + wpobject_class->get_supported_features = + wp_state_metadata_get_supported_features; + wpobject_class->activate_get_next_step = + wp_state_metadata_activate_get_next_step; + wpobject_class->activate_execute_step = + wp_state_metadata_activate_execute_step; + wpobject_class->deactivate = wp_state_metadata_deactivate; + + g_object_class_install_property (object_class, STATE_METADATA_PROP_NAME, + g_param_spec_string ("name", "name", + "The file name where the state metadata will be stored", NULL, + G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS)); + + g_object_class_install_property (object_class, STATE_METADATA_PROP_TIMEOUT, + g_param_spec_uint ("timeout", "timeout", + "The timeout in milliseconds to save the state metadata", 0, + G_MAXUINT, DEFAULT_TIMEOUT_MS, + G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS)); +} + +/*! + * \brief Constructs a new state metadata object + * \ingroup wpstatemetadata + * \param core the core associated with the state metadata + * \param name the state metadata name + * \returns (transfer full): the new WpStateMetadata + */ +WpStateMetadata * +wp_state_metadata_new (WpCore *core, const gchar *name) +{ + g_return_val_if_fail (core, NULL); + g_return_val_if_fail (name, NULL); + return g_object_new (wp_state_metadata_get_type (), + "core", core, + "name", name, + NULL); +} + +/*! + * \brief Gets the name of a state metadata object + * \ingroup wpstatemetadata + * \param self the state metadata + * \returns the name of this state metadata + */ +const gchar * +wp_state_metadata_get_name (WpStateMetadata *self) +{ + g_return_val_if_fail (WP_IS_STATE_METADATA (self), NULL); + + return self->name; +} + +/*! + * \brief Gets the location of a state metadata object + * \ingroup wpstatemetadata + * \param self the state metadata + * \returns the location of this state metadata + */ +const gchar * +wp_state_metadata_get_location (WpStateMetadata *self) +{ + g_return_val_if_fail (WP_IS_STATE_METADATA (self), NULL); + + state_metadata_ensure_location (self); + + return self->location; +} + +/*! + * \brief Clears the state metadata and removes its file + * + * If the state metadata has not been loaded, this won't do anything. + * + * \ingroup wpstatemetadata + * \param self the state metadata + */ +void +wp_state_metadata_clear (WpStateMetadata *self) +{ + g_return_if_fail (WP_IS_STATE_METADATA (self)); + + if (!(wp_object_get_active_features (WP_OBJECT (self)) & + WP_STATE_METADATA_LOADED)) + return; + + g_return_if_fail (self->metadata); + wp_metadata_clear (WP_METADATA (self->metadata)); +} + +/*! + * \brief Gets a value from the state metadata + * + * If the state metadata has not been loaded, this won't do anything. + * + * \ingroup wpstatemetadata + * \param self the state metadata + * \param key the key of the value + * \returns the value from the state metadata, or NULL if not found. + */ +const gchar * +wp_state_metadata_get (WpStateMetadata *self, const gchar *key) +{ + g_return_val_if_fail (WP_IS_STATE_METADATA (self), NULL); + g_return_val_if_fail (key, NULL); + + if (!(wp_object_get_active_features (WP_OBJECT (self)) & + WP_STATE_METADATA_LOADED)) + return NULL; + + g_return_val_if_fail (self->metadata, NULL); + return wp_metadata_find (WP_METADATA (self->metadata), 0, key, NULL); +} + +/*! + * \brief Sets a value into the state metadata + * + * If value is NULL, it will unset the given \a key. Note that this will also + * save the state after the timeout has elapsed. + * + * If the state metadata has not been loaded, this won't do anything. + * + * \ingroup wpstatemetadata + * \param self the metadata object + * \param key: the key to set. + * \param value (nullable): the value to set, or NULL to unset the given \a key + */ +void +wp_state_metadata_set (WpStateMetadata *self, const gchar *key, + const gchar *value) +{ + g_return_if_fail (WP_IS_STATE_METADATA (self)); + g_return_if_fail (key); + + if (!(wp_object_get_active_features (WP_OBJECT (self)) & + WP_STATE_METADATA_LOADED)) + return; + + g_return_if_fail (self->metadata); + wp_metadata_set (WP_METADATA (self->metadata), 0, key, NULL, value); } diff --git a/lib/wp/state.h b/lib/wp/state.h index 08caca5d..89956e5b 100644 --- a/lib/wp/state.h +++ b/lib/wp/state.h @@ -46,6 +46,46 @@ void wp_state_save_after_timeout (WpState *self, WpCore *core, WP_API WpProperties * wp_state_load (WpState *self); + +/* WpStateMetadata */ + +/*! + * \brief Flags to be used as WpObjectFeatures on WpStateMetadata. + * \ingroup wpstate + */ +typedef enum { /*< flags >*/ + /*! Loads the state metadata */ + WP_STATE_METADATA_LOADED = (1 << 0), +} WpStateMetadataFeatures; + +/*! + * \brief The WpStateMetadata GType + * \ingroup wpstatemetadata + */ +#define WP_TYPE_STATE_METADATA (wp_state_metadata_get_type ()) +WP_API +G_DECLARE_FINAL_TYPE (WpStateMetadata, wp_state_metadata, WP, STATE_METADATA, + WpObject) + +WP_API +WpStateMetadata * wp_state_metadata_new (WpCore *core, const gchar *name); + +WP_API +const gchar * wp_state_metadata_get_name (WpStateMetadata *self); + +WP_API +const gchar * wp_state_metadata_get_location (WpStateMetadata *self); + +WP_API +void wp_state_metadata_clear (WpStateMetadata *self); + +WP_API +const gchar * wp_state_metadata_get (WpStateMetadata *self, const gchar *key); + +WP_API +void wp_state_metadata_set (WpStateMetadata *self, const gchar *key, + const gchar *value); + G_END_DECLS #endif