From af423398c424a2a47ccadd6b61c0d1a7ef757c29 Mon Sep 17 00:00:00 2001 From: Julian Bouzas Date: Wed, 14 Oct 2020 14:36:12 -0400 Subject: [PATCH] lib: add new WpState API to save and load data from files --- lib/wp/meson.build | 2 + lib/wp/state.c | 320 +++++++++++++++++++++++++++++++++++++++++++ lib/wp/state.h | 47 +++++++ lib/wp/wp.h | 1 + tests/wp/meson.build | 8 ++ tests/wp/state.c | 141 +++++++++++++++++++ 6 files changed, 519 insertions(+) create mode 100644 lib/wp/state.c create mode 100644 lib/wp/state.h create mode 100644 tests/wp/state.c diff --git a/lib/wp/meson.build b/lib/wp/meson.build index e7a4e56f..307fa5ec 100644 --- a/lib/wp/meson.build +++ b/lib/wp/meson.build @@ -27,6 +27,7 @@ wp_lib_sources = files( 'si-interfaces.c', 'spa-pod.c', 'spa-type.c', + 'state.c', 'transition.c', 'wp.c', ) @@ -61,6 +62,7 @@ wp_lib_headers = files( 'si-interfaces.h', 'spa-pod.h', 'spa-type.h', + 'state.h', 'transition.h', 'wp.h', ) diff --git a/lib/wp/state.c b/lib/wp/state.c new file mode 100644 index 00000000..638be5e8 --- /dev/null +++ b/lib/wp/state.c @@ -0,0 +1,320 @@ +/* WirePlumber + * + * Copyright © 2020 Collabora Ltd. + * @author Julian Bouzas + * + * SPDX-License-Identifier: MIT + */ + +/** + * SECTION: WpState + * + * The #WpState class saves and loads properties from a file + */ + +#define G_LOG_DOMAIN "wp-state" + +#define WP_STATE_DIR_NAME "wireplumber" + +#include +#include +#include +#include +#include + +#include "debug.h" +#include "state.h" + +enum { + PROP_0, + PROP_NAME, +}; + +struct _WpState +{ + GObject parent; + + /* Props */ + gchar *name; + + gchar *location; +}; + +G_DEFINE_TYPE (WpState, wp_state, G_TYPE_OBJECT) + +static gboolean +path_exists (const char *path) +{ + struct stat info; + return stat (path, &info) == 0; +} + +static char * +get_new_location (const char *name) +{ + g_autofree gchar *path = NULL; + + /* Get the config path */ + path = g_build_filename (g_get_user_config_dir (), WP_STATE_DIR_NAME, NULL); + g_return_val_if_fail (path, NULL); + + /* Create the directory if it doesn't exist */ + if (!path_exists (path)) + g_mkdir_with_parents (path, 0700); + + 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; + 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; + 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_OBJECT_CLASS (wp_state_parent_class)->finalize (object); +} + +static void +wp_state_init (WpState * self) +{ +} + +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; + + /** + * WpState:name: + * The file name where the state will be stored. + */ + 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)); +} + +/** + * wp_state_new: + * @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); +} + +/** + * wp_state_get_name: + * @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; +} + +/** + * wp_state_get_location: + * @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; +} + +/** + * wp_state_clear: + * @self: the state + * + * Clears the state removing its file + */ +void +wp_state_clear (WpState *self) +{ + g_return_if_fail (WP_IS_STATE (self)); + wp_state_ensure_location (self); + + if (path_exists (self->location)) + remove (self->location); +} + +/** + * wp_state_save: + * @self: the state + * @props: (transfer none): the properties to save + * + * Saves new properties in the state, overwriting all previous data. + * + * Returns: TRUE if the properties could be saved, FALSE otherwise + */ +gboolean +wp_state_save (WpState *self, WpProperties *props) +{ + g_autoptr (WpIterator) it = NULL; + g_auto (GValue) item = G_VALUE_INIT; + g_autofree gchar *tmp_name = NULL, *tmp_location = NULL; + gulong tmp_size; + int fd; + FILE *f; + + g_return_val_if_fail (WP_IS_STATE (self), FALSE); + wp_state_ensure_location (self); + + wp_info_object (self, "saving state into %s", self->location); + + /* Get the temporary name and location */ + tmp_size = strlen (self->name) + 5; + tmp_name = g_malloc (tmp_size); + g_snprintf (tmp_name, tmp_size, "%s.tmp", self->name); + tmp_location = get_new_location (tmp_name); + g_return_val_if_fail (tmp_location, FALSE); + + /* Open */ + fd = open (tmp_location, O_CLOEXEC | O_CREAT | O_WRONLY | O_TRUNC, 0700); + if (fd == -1) { + wp_critical_object (self, "can't open %s", tmp_location); + return FALSE; + } + + /* Write */ + f = fdopen(fd, "w"); + for (it = wp_properties_iterate (props); + wp_iterator_next (it, &item); + g_value_unset (&item)) { + const gchar *p = wp_properties_iterator_item_get_key (&item); + while (*p) { + if (*p == ' ' || *p == '\\') + fputc('\\', f); + fprintf(f, "%c", *p++); + } + fprintf(f, " %s\n", wp_properties_iterator_item_get_value (&item)); + } + fclose(f); + + /* Rename temporary file */ + if (rename(tmp_location, self->location) < 0) { + wp_critical_object("can't rename temporary file '%s' to '%s'", tmp_name, + self->name); + return FALSE; + } + + return TRUE; +} + +/** + * wp_state_load: + * @self: the state + * + * Loads the state data into new properties. + * + * Returns (transfer full): the new properties with the state data + */ +WpProperties * +wp_state_load (WpState *self) +{ + g_autoptr (WpProperties) props = wp_properties_new_empty (); + int fd; + FILE *f; + char line[1024]; + + g_return_val_if_fail (WP_IS_STATE (self), NULL); + wp_state_ensure_location (self); + + /* Open */ + wp_info_object (self, "loading state from %s", self->location); + fd = open (self->location, O_CLOEXEC | O_RDONLY); + if (fd == -1) { + /* We consider empty state if fill does not exist */ + if (errno == ENOENT) + return g_steal_pointer (&props); + wp_critical_object (self, "can't open %s", self->location); + return NULL; + } + + /* Read */ + f = fdopen(fd, "r"); + while (fgets (line, sizeof(line)-1, f)) { + char *val, *key, *k, *p; + val = strrchr(line, '\n'); + if (val) + *val = '\0'; + + key = k = p = line; + while (*p) { + if (*p == ' ') + break; + if (*p == '\\') + p++; + *k++ = *p++; + } + *k = '\0'; + val = ++p; + wp_properties_set (props, key, val); + } + fclose(f); + + return g_steal_pointer (&props); +} diff --git a/lib/wp/state.h b/lib/wp/state.h new file mode 100644 index 00000000..8603777a --- /dev/null +++ b/lib/wp/state.h @@ -0,0 +1,47 @@ +/* WirePlumber + * + * Copyright © 2020 Collabora Ltd. + * @author Julian Bouzas + * + * SPDX-License-Identifier: MIT + */ + +#ifndef __WIREPLUMBER_STATE_H__ +#define __WIREPLUMBER_STATE_H__ + +#include "properties.h" + +G_BEGIN_DECLS + +/* WpState */ + +/** + * WP_TYPE_STATE: + * + * The #WpState #GType + */ +#define WP_TYPE_STATE (wp_state_get_type ()) +WP_API +G_DECLARE_FINAL_TYPE (WpState, wp_state, WP, STATE, GObject) + +WP_API +WpState * wp_state_new (const gchar *name); + +WP_API +const gchar * wp_state_get_name (WpState *self); + +WP_API +const gchar * wp_state_get_location (WpState *self); + +WP_API +void wp_state_clear (WpState *self); + +WP_API +gboolean wp_state_save (WpState *self, WpProperties *props); + +WP_API +WpProperties * wp_state_load (WpState *self); + +G_END_DECLS + +#endif diff --git a/lib/wp/wp.h b/lib/wp/wp.h index 519f11b8..eef06461 100644 --- a/lib/wp/wp.h +++ b/lib/wp/wp.h @@ -37,6 +37,7 @@ #include "si-interfaces.h" #include "spa-pod.h" #include "spa-type.h" +#include "state.h" #include "transition.h" #include "wpenums.h" #include "wpversion.h" diff --git a/tests/wp/meson.build b/tests/wp/meson.build index 11767a46..43d24f28 100644 --- a/tests/wp/meson.build +++ b/tests/wp/meson.build @@ -2,6 +2,7 @@ common_deps = [gobject_dep, gio_dep, wp_dep, pipewire_dep] common_env = [ 'G_TEST_SRCDIR=@0@'.format(meson.current_source_dir()), 'G_TEST_BUILDDIR=@0@'.format(meson.current_build_dir()), + 'XDG_CONFIG_HOME=@0@'.format(meson.current_source_dir() / '.config'), 'WIREPLUMBER_MODULE_DIR=@0@'.format(meson.current_build_dir() / '..' / '..' / 'modules'), 'WIREPLUMBER_DEBUG=7', ] @@ -85,6 +86,13 @@ test( env: common_env, ) +test( + 'test-state', + executable('test-state', 'state.c', + dependencies: common_deps, c_args: common_args), + env: common_env, +) + test( 'test-transition', executable('test-transition', 'transition.c', diff --git a/tests/wp/state.c b/tests/wp/state.c new file mode 100644 index 00000000..66a2f945 --- /dev/null +++ b/tests/wp/state.c @@ -0,0 +1,141 @@ +/* WirePlumber + * + * Copyright © 2020 Collabora Ltd. + * @author Julian Bouzas + * + * SPDX-License-Identifier: MIT + */ + +#include + +static void +test_state_basic (void) +{ + g_autoptr (WpState) state = wp_state_new ("basic"); + g_assert_nonnull (state); + + g_assert_cmpstr (wp_state_get_name (state), ==, "basic"); + g_assert_true (g_str_has_suffix (wp_state_get_location (state), "basic")); + + /* Save */ + { + g_autoptr (WpProperties) props = wp_properties_new_empty (); + wp_properties_set (props, "key1", "value1"); + wp_properties_set (props, "key2", "value2"); + wp_properties_set (props, "key3", "value3"); + g_assert_true (wp_state_save (state, props)); + } + + /* Load */ + { + g_autoptr (WpProperties) props = wp_state_load (state); + g_assert_nonnull (props); + g_assert_cmpstr (wp_properties_get (props, "key1"), ==, "value1"); + g_assert_cmpstr (wp_properties_get (props, "key2"), ==, "value2"); + g_assert_cmpstr (wp_properties_get (props, "key2"), ==, "value2"); + g_assert_null (wp_properties_get (props, "invalid")); + } + + /* Re-Save */ + { + g_autoptr (WpProperties) props = wp_properties_new_empty (); + wp_properties_set (props, "new-key", "new-value"); + g_assert_true (wp_state_save (state, props)); + } + + /* Re-Load */ + { + g_autoptr (WpProperties) props = wp_state_load (state); + g_assert_nonnull (props); + g_assert_cmpstr (wp_properties_get (props, "new-key"), ==, "new-value"); + g_assert_null (wp_properties_get (props, "key1")); + g_assert_null (wp_properties_get (props, "key2")); + g_assert_null (wp_properties_get (props, "key3")); + } + + wp_state_clear (state); + + /* Load empty */ + { + g_autoptr (WpProperties) props = wp_state_load (state); + g_assert_nonnull (props); + g_assert_null (wp_properties_get (props, "new-key")); + g_assert_null (wp_properties_get (props, "key1")); + g_assert_null (wp_properties_get (props, "key2")); + g_assert_null (wp_properties_get (props, "key3")); + } + + wp_state_clear (state); +} + +static void +test_state_empty (void) +{ + g_autoptr (WpState) state = wp_state_new ("empty"); + g_assert_nonnull (state); + + /* Save */ + { + g_autoptr (WpProperties) props = wp_properties_new_empty (); + wp_properties_set (props, "key", "value"); + g_assert_true (wp_state_save (state, props)); + } + + /* Load */ + { + g_autoptr (WpProperties) props = wp_state_load (state); + g_assert_nonnull (props); + g_assert_cmpstr (wp_properties_get (props, "key"), ==, "value"); + } + + /* Save empty */ + { + g_autoptr (WpProperties) props = wp_properties_new_empty (); + g_assert_true (wp_state_save (state, props)); + } + + /* Load empty */ + { + g_autoptr (WpProperties) props = wp_state_load (state); + g_assert_nonnull (props); + g_assert_null (wp_properties_get (props, "key")); + } + + wp_state_clear (state); +} + +static void +test_state_spaces (void) +{ + g_autoptr (WpState) state = wp_state_new ("spaces"); + g_assert_nonnull (state); + + /* Save */ + { + g_autoptr (WpProperties) props = wp_properties_new_empty (); + wp_properties_set (props, "key", "value with spaces"); + g_assert_true (wp_state_save (state, props)); + } + + /* Load */ + { + g_autoptr (WpProperties) props = wp_state_load (state); + g_assert_nonnull (props); + g_assert_cmpstr (wp_properties_get (props, "key"), ==, "value with spaces"); + } + + wp_state_clear (state); +} + +int +main (int argc, char *argv[]) +{ + g_test_init (&argc, &argv, NULL); + g_log_set_writer_func (wp_log_writer_default, NULL, NULL); + + g_test_add_func ("/wp/state/basic", test_state_basic); + g_test_add_func ("/wp/state/empty", test_state_empty); + g_test_add_func ("/wp/state/spaces", test_state_spaces); + + return g_test_run (); +}