Files
wireplumber/lib/wp/state.c
George Kiagiadakis eb2d6efcd4 state: add save_after_timeout() method to replace all custom timeout code
This was a common pattern that we had in many places, so it makes sense
to consolidate it.
2024-01-04 16:38:33 +02:00

458 lines
11 KiB
C

/* WirePlumber
*
* Copyright © 2020 Collabora Ltd.
* @author Julian Bouzas <julian.bouzas@collabora.com>
*
* SPDX-License-Identifier: MIT
*/
#include <stdio.h>
#include <errno.h>
#include "log.h"
#include "state.h"
#include "wp.h"
WP_DEFINE_LOCAL_LOG_TOPIC ("wp-state")
#define DEFAULT_TIMEOUT_MS 1000
#define ESCAPED_CHARACTER '\\'
static char *
escape_string (const gchar *str)
{
char *res = NULL;
size_t str_size, i, j;
g_return_val_if_fail (str, NULL);
str_size = strlen (str);
g_return_val_if_fail (str_size > 0, NULL);
res = g_malloc_n ((str_size * 2) + 1, sizeof(gchar));
j = 0;
for (i = 0; i < str_size; i++) {
switch (str[i]) {
case ESCAPED_CHARACTER:
res[j++] = ESCAPED_CHARACTER;
res[j++] = ESCAPED_CHARACTER;
break;
case ' ':
res[j++] = ESCAPED_CHARACTER;
res[j++] = 's';
break;
case '=':
res[j++] = ESCAPED_CHARACTER;
res[j++] = 'e';
break;
case '[':
res[j++] = ESCAPED_CHARACTER;
res[j++] = 'o';
break;
case ']':
res[j++] = ESCAPED_CHARACTER;
res[j++] = 'c';
break;
default:
res[j++] = str[i];
break;
}
}
res[j++] = '\0';
return res;
}
static char *
compress_string (const gchar *str)
{
char *res = NULL;
size_t str_size, i, j;
g_return_val_if_fail (str, NULL);
str_size = strlen (str);
g_return_val_if_fail (str_size > 0, NULL);
res = g_malloc_n (str_size + 1, sizeof(gchar));
j = 0;
for (i = 0; i < str_size - 1; i++) {
if (str[i] == ESCAPED_CHARACTER) {
switch (str[i + 1]) {
case ESCAPED_CHARACTER:
res[j++] = ESCAPED_CHARACTER;
break;
case 's':
res[j++] = ' ';
break;
case 'e':
res[j++] = '=';
break;
case 'o':
res[j++] = '[';
break;
case 'c':
res[j++] = ']';
break;
default:
res[j++] = str[i];
break;
}
i++;
} else {
res[j++] = str[i];
}
}
if (i < str_size)
res[j++] = str[i];
res[j++] = '\0';
return res;
}
/*! \defgroup wpstate WpState */
/*!
* \struct WpState
*
* The WpState class saves and loads properties from a file
*
* \gproperties
* \gproperty{name, gchar *, G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY,
* The file name where the state will be stored.}
*/
enum {
PROP_0,
PROP_NAME,
PROP_TIMEOUT,
};
struct _WpState
{
GObject parent;
/* Props */
gchar *name;
guint timeout;
gchar *location;
GSource *timeout_source;
WpProperties *timeout_props;
};
G_DEFINE_TYPE (WpState, wp_state, G_TYPE_OBJECT)
/* Gets the full path to the WirePlumber XDG_STATE_HOME subdirectory */
static const gchar *
wp_get_xdg_state_dir (void)
{
static gchar xdg_dir[PATH_MAX] = {0};
if (xdg_dir[0] == '\0') {
g_autofree gchar *path = NULL;
g_autofree gchar *base = g_strdup (g_getenv ("XDG_STATE_HOME"));
if (!base)
base = g_build_filename (g_get_home_dir (), ".local", "state", NULL);
path = g_build_filename (base, "wireplumber", NULL);
(void) g_strlcpy (xdg_dir, path, sizeof (xdg_dir));
}
return xdg_dir;
}
static char *
get_new_location (const char *name)
{
const gchar *path = wp_get_xdg_state_dir ();
/* Create the directory if it doesn't exist */
if (g_mkdir_with_parents (path, 0700) < 0)
wp_warning ("failed to create directory %s: %s", path, g_strerror (errno));
return g_build_filename (path, name, NULL);
}
static void
wp_state_ensure_location (WpState *self)
{
if (!self->location)
self->location = get_new_location (self->name);
g_return_if_fail (self->location);
}
static void
wp_state_set_property (GObject * object, guint property_id,
const GValue * value, GParamSpec * pspec)
{
WpState *self = WP_STATE (object);
switch (property_id) {
case PROP_NAME:
g_clear_pointer (&self->name, g_free);
self->name = g_value_dup_string (value);
break;
case 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_get_property (GObject * object, guint property_id, GValue * value,
GParamSpec * pspec)
{
WpState *self = WP_STATE (object);
switch (property_id) {
case PROP_NAME:
g_value_set_string (value, self->name);
break;
case PROP_TIMEOUT:
g_value_set_uint (value, self->timeout);
break;
default:
G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
break;
}
}
static void
wp_state_finalize (GObject * object)
{
WpState * self = WP_STATE (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_clear_pointer (&self->timeout_props, wp_properties_unref);
G_OBJECT_CLASS (wp_state_parent_class)->finalize (object);
}
static void
wp_state_init (WpState * self)
{
self->timeout = DEFAULT_TIMEOUT_MS;
}
static void
wp_state_class_init (WpStateClass * klass)
{
GObjectClass *object_class = (GObjectClass *) klass;
object_class->finalize = wp_state_finalize;
object_class->set_property = wp_state_set_property;
object_class->get_property = wp_state_get_property;
g_object_class_install_property (object_class, PROP_NAME,
g_param_spec_string ("name", "name",
"The file name where the state will be stored", NULL,
G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS));
g_object_class_install_property (object_class, PROP_TIMEOUT,
g_param_spec_uint ("timeout", "timeout",
"The timeout in milliseconds to save the state", 0, G_MAXUINT,
DEFAULT_TIMEOUT_MS, G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
}
/*!
* \brief Constructs a new state object
* \ingroup wpstate
* \param name the state name
* \returns (transfer full): the new WpState
*/
WpState *
wp_state_new (const gchar *name)
{
g_return_val_if_fail (name, NULL);
return g_object_new (wp_state_get_type (),
"name", name,
NULL);
}
/*!
* \brief Gets the name of a state object
* \ingroup wpstate
* \param self the state
* \returns the name of this state
*/
const gchar *
wp_state_get_name (WpState *self)
{
g_return_val_if_fail (WP_IS_STATE (self), NULL);
return self->name;
}
/*!
* \brief Gets the location of a state object
* \ingroup wpstate
* \param self the state
* \returns the location of this state
*/
const gchar *
wp_state_get_location (WpState *self)
{
g_return_val_if_fail (WP_IS_STATE (self), NULL);
wp_state_ensure_location (self);
return self->location;
}
/*!
* \brief Clears the state removing its file
* \ingroup wpstate
* \param self the state
*/
void
wp_state_clear (WpState *self)
{
g_return_if_fail (WP_IS_STATE (self));
wp_state_ensure_location (self);
if (remove (self->location) < 0)
wp_warning ("failed to remove %s: %s", self->location, g_strerror (errno));
}
/*!
* \brief Saves new properties in the state, overwriting all previous data.
* \ingroup wpstate
* \param self the state
* \param props (transfer none): the properties to save
* \param error (out)(optional): return location for a GError, or NULL
* \returns TRUE if the properties could be saved, FALSE otherwise
*/
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;
}
static gboolean
timeout_save_state_callback (WpState *self)
{
g_autoptr (GError) error = NULL;
if (!wp_state_save (self, self->timeout_props, &error))
wp_warning_object (self, "%s", error->message);
g_clear_pointer (&self->timeout_source, g_source_unref);
g_clear_pointer (&self->timeout_props, wp_properties_unref);
return G_SOURCE_REMOVE;
}
/*!
* \brief Saves new properties in the state, overwriting all previous data,
* after a timeout
*
* This is similar to wp_state_save() but it will save the state after a timeout
* has elapsed. If the state is saved again before the timeout elapses, the
* timeout is reset.
*
* This function is useful to avoid saving the state too often. When called
* consecutively, it will save the state only once. Every time it is called,
* it will cancel the previous timer and start a new one, resulting in timing
* out only after the last call.
*
* \ingroup wpstate
* \param self the state
* \param core the core, used to add the timeout callback to the main loop
* \param props (transfer none): the properties to save. This object will be
* referenced and kept alive until the timeout elapses, but not deep copied.
* \since 0.5.0
*/
void
wp_state_save_after_timeout (WpState *self, WpCore *core, WpProperties *props)
{
/* Clear the current timeout callback */
if (self->timeout_source)
g_source_destroy (self->timeout_source);
g_clear_pointer (&self->timeout_source, g_source_unref);
g_clear_pointer (&self->timeout_props, wp_properties_unref);
self->timeout_props = wp_properties_ref (props);
/* Add the timeout callback */
wp_core_timeout_add_closure (core, &self->timeout_source, self->timeout,
g_cclosure_new_object (G_CALLBACK (timeout_save_state_callback),
G_OBJECT (self)));
}
/*!
* \brief Loads the state data from the file system
*
* This function will never fail. If it cannot load the state, for any reason,
* it will simply return an empty WpProperties, behaving as if there was no
* previous state stored.
*
* \ingroup wpstate
* \param self the state
* \returns (transfer full): a new WpProperties containing the state data
*/
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);
/* 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);
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);
}
g_strfreev (keys);
return g_steal_pointer (&props);
}