/* * Copyright © 2010 Codethink Limited * Copyright © 2012 Canonical Limited * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 2 of the licence, or (at your option) any later version. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this library; if not, see . * * Author: Ryan Lortie */ #include "config.h" #include "dconf-writer.h" #include #include #include #include typedef DConfWriterClass DConfKeyfileWriterClass; typedef struct { DConfWriter parent_instance; gchar *filename; gchar *lock_filename; gint lock_fd; GFileMonitor *monitor; guint scheduled_update; gchar *contents; GKeyFile *keyfile; } DConfKeyfileWriter; G_DEFINE_TYPE (DConfKeyfileWriter, dconf_keyfile_writer, DCONF_TYPE_WRITER) static DConfChangeset * dconf_keyfile_to_changeset (GKeyFile *keyfile, const gchar *filename_fyi) { DConfChangeset *changeset; gchar **groups; gint i; changeset = dconf_changeset_new_database (NULL); groups = g_key_file_get_groups (keyfile, NULL); for (i = 0; groups[i]; i++) { const gchar *group = groups[i]; gchar *key_prefix; gchar **keys; gint j; /* Special case the [/] group to be able to contain keys at the * root (/a, /b, etc.). All others must not start or end with a * slash (ie: group [x/y] contains keys such as /x/y/z). */ if (!g_str_equal (group, "/")) { if (g_str_has_prefix (group, "/") || g_str_has_suffix (group, "/") || strstr (group, "//")) { g_warning ("%s: ignoring invalid group name: %s\n", filename_fyi, group); continue; } key_prefix = g_strconcat ("/", group, "/", NULL); } else key_prefix = g_strdup ("/"); keys = g_key_file_get_keys (keyfile, group, NULL, NULL); g_assert (keys != NULL); for (j = 0; keys[j]; j++) { const gchar *key = keys[j]; GError *error = NULL; gchar *value_str; GVariant *value; gchar *path; if (strchr (key, '/')) { g_warning ("%s: [%s]: ignoring invalid key name: %s\n", filename_fyi, group, key); continue; } value_str = g_key_file_get_value (keyfile, group, key, NULL); g_assert (value_str != NULL); value = g_variant_parse (NULL, value_str, NULL, NULL, &error); g_free (value_str); if (value == NULL) { g_warning ("%s: [%s]: %s: skipping invalid value: %s (%s)\n", filename_fyi, group, key, value_str, error->message); g_error_free (error); continue; } path = g_strconcat (key_prefix, key, NULL); dconf_changeset_set (changeset, path, value); g_variant_unref (value); g_free (path); } g_free (key_prefix); g_strfreev (keys); } g_strfreev (groups); return changeset; } static void dconf_keyfile_writer_list (GHashTable *set) { const gchar *name; gchar *dirname; GDir *dir; dirname = g_build_filename (g_get_user_config_dir (), "dconf", NULL); dir = g_dir_open (dirname, 0, NULL); if (!dir) return; while ((name = g_dir_read_name (dir))) { const gchar *dottxt; dottxt = strstr (name, ".txt"); if (dottxt && dottxt[4] == '\0') g_hash_table_add (set, g_strndup (name, dottxt - name)); } g_dir_close (dir); } static gboolean dconf_keyfile_update (gpointer user_data); static void dconf_keyfile_changed (GFileMonitor *monitor, GFile *file, GFile *other_file, GFileMonitorEvent event_type, gpointer user_data) { DConfKeyfileWriter *kfw = user_data; if (event_type == G_FILE_MONITOR_EVENT_CHANGES_DONE_HINT || event_type == G_FILE_MONITOR_EVENT_CREATED) { if (!kfw->scheduled_update) kfw->scheduled_update = g_idle_add (dconf_keyfile_update, kfw); } } static gboolean dconf_keyfile_writer_begin (DConfWriter *writer, GError **error) { DConfKeyfileWriter *kfw = (DConfKeyfileWriter *) writer; GError *local_error = NULL; DConfChangeset *contents; DConfChangeset *changes; if (kfw->filename == NULL) { gchar *filename_base; GFile *file; filename_base = g_build_filename (g_get_user_config_dir (), "dconf", dconf_writer_get_name (writer), NULL); kfw->filename = g_strconcat (filename_base, ".txt", NULL); kfw->lock_filename = g_strconcat (kfw->filename, "-lock", NULL); g_free (filename_base); /* See https://bugzilla.gnome.org/show_bug.cgi?id=691618 */ file = g_vfs_get_file_for_path (g_vfs_get_local (), kfw->filename); kfw->monitor = g_file_monitor_file (file, G_FILE_MONITOR_NONE, NULL, NULL); g_object_unref (file); g_signal_connect (kfw->monitor, "changed", G_CALLBACK (dconf_keyfile_changed), kfw); } g_clear_pointer (&kfw->contents, g_free); kfw->lock_fd = open (kfw->lock_filename, O_RDWR | O_CREAT, 0666); if (kfw->lock_fd == -1) { gchar *dirname; /* Maybe it failed because the directory doesn't exist. Try * again, after mkdir(). */ dirname = g_path_get_dirname (kfw->lock_filename); g_mkdir_with_parents (dirname, 0700); g_free (dirname); kfw->lock_fd = open (kfw->lock_filename, O_RDWR | O_CREAT, 0666); if (kfw->lock_fd == -1) { gint saved_errno = errno; g_set_error (error, G_FILE_ERROR, g_file_error_from_errno (saved_errno), "%s: %s", kfw->lock_filename, g_strerror (saved_errno)); return FALSE; } } while (TRUE) { struct flock lock; lock.l_type = F_WRLCK; lock.l_whence = 0; lock.l_start = 0; lock.l_len = 0; /* lock all bytes */ if (fcntl (kfw->lock_fd, F_SETLKW, &lock) == 0) break; if (errno != EINTR) { gint saved_errno = errno; g_set_error (error, G_FILE_ERROR, g_file_error_from_errno (saved_errno), "%s: unable to fcntl(F_SETLKW): %s", kfw->lock_filename, g_strerror (saved_errno)); close (kfw->lock_fd); kfw->lock_fd = -1; return FALSE; } /* it was EINTR. loop again. */ } if (!g_file_get_contents (kfw->filename, &kfw->contents, NULL, &local_error)) { if (!g_error_matches (local_error, G_FILE_ERROR, G_FILE_ERROR_NOENT)) { g_propagate_error (error, local_error); return FALSE; } g_clear_error (&local_error); } kfw->keyfile = g_key_file_new (); if (kfw->contents) { if (!g_key_file_load_from_data (kfw->keyfile, kfw->contents, -1, G_KEY_FILE_KEEP_COMMENTS, &local_error)) { g_clear_pointer (&kfw->keyfile, g_key_file_free); g_clear_pointer (&kfw->contents, g_free); g_propagate_error (error, local_error); return FALSE; } } if (!DCONF_WRITER_CLASS (dconf_keyfile_writer_parent_class)->begin (writer, error)) { g_clear_pointer (&kfw->keyfile, g_key_file_free); return FALSE; } /* Diff the keyfile to the current contents of the database and apply * any changes that we notice. * * This will catch both the case of people outside of the service * making changes to the file and also the case of starting for the * first time. */ contents = dconf_keyfile_to_changeset (kfw->keyfile, kfw->filename); changes = dconf_writer_diff (writer, contents); if (changes) { DCONF_WRITER_CLASS (dconf_keyfile_writer_parent_class)->change (writer, changes, ""); dconf_changeset_unref (changes); } dconf_changeset_unref (contents); return TRUE; } static void dconf_keyfile_writer_change (DConfWriter *writer, DConfChangeset *changeset, const gchar *tag) { DConfKeyfileWriter *kfw = (DConfKeyfileWriter *) writer; const gchar *prefix; const gchar * const *paths; GVariant * const *values; guint n, i; DCONF_WRITER_CLASS (dconf_keyfile_writer_parent_class)->change (writer, changeset, tag); n = dconf_changeset_describe (changeset, &prefix, &paths, &values); for (i = 0; i < n; i++) { gchar *path = g_strconcat (prefix, paths[i], NULL); GVariant *value = values[i]; if (g_str_equal (path, "/")) { g_assert (value == NULL); /* This is a request to reset everything. * * Easiest way to do this: */ g_key_file_free (kfw->keyfile); kfw->keyfile = g_key_file_new (); } else if (g_str_has_suffix (path, "/")) { gchar *group_to_remove; gchar **groups; gint i; g_assert (value == NULL); /* Time to do a path reset. * * We must reset the group for the path plus any "subgroups". * * We dealt with the case of "/" above, so we know we have * something with at least a separate leading and trailing slash, * with the group name in the middle. */ group_to_remove = g_strndup (path + 1, strlen (path) - 2); g_key_file_remove_group (kfw->keyfile, group_to_remove, NULL); g_free (group_to_remove); /* Now the rest... * * For this case we check if the group is prefixed by the path * given to us, including the trailing slash (but not the leading * one). That means a reset on "/a/" (group "[a]") will match * group "[a/b]" but not will not match group "[another]". */ groups = g_key_file_get_groups (kfw->keyfile, NULL); for (i = 0; groups[i]; i++) if (g_str_has_prefix (groups[i], path + 1)) /* remove only leading slash */ g_key_file_remove_group (kfw->keyfile, groups[i], NULL); g_strfreev (groups); } else { /* A simple set or reset of a single key. */ const gchar *last_slash; gchar *group; gchar *key; last_slash = strrchr (path, '/'); /* If the last slash is the first one then the group will be the * special case: [/]. Otherwise we remove the leading and * trailing slashes. */ if (last_slash != path) group = g_strndup (path + 1, last_slash - (path + 1)); else group = g_strdup ("/"); /* Key is the non-empty part following the last slash (we know * that it's non-empty because we dealt with strings ending with * '/' above). */ key = g_strdup (last_slash + 1); if (value != NULL) { gchar *printed; printed = g_variant_print (value, TRUE); g_key_file_set_value (kfw->keyfile, group, key, printed); g_free (printed); } else g_key_file_remove_key (kfw->keyfile, group, key, NULL); g_free (group); g_free (key); } g_free (path); } } static gboolean dconf_keyfile_writer_commit (DConfWriter *writer, GError **error) { DConfKeyfileWriter *kfw = (DConfKeyfileWriter *) writer; /* Pretty simple. Write the keyfile. */ { gchar *data; gsize size; /* docs say: "Note that this function never reports an error" */ data = g_key_file_to_data (kfw->keyfile, &size, NULL); /* don't write it again if nothing changed */ if (!kfw->contents || !g_str_equal (kfw->contents, data)) { if (!g_file_set_contents (kfw->filename, data, size, error)) { gchar *dirname; /* Maybe it failed because the directory doesn't exist. Try * again, after mkdir(). */ dirname = g_path_get_dirname (kfw->filename); g_mkdir_with_parents (dirname, 0777); g_free (dirname); g_clear_error (error); if (!g_file_set_contents (kfw->filename, data, size, error)) { g_free (data); return FALSE; } } } g_free (data); } /* Failing to update the shm file after writing the keyfile is * unlikely to occur. It can only happen if the runtime dir hits * quota. * * If it does happen, we're in a bit of a bad spot because the on-disk * keyfile is now out-of-sync with the contents of the shm file. We * fail the write because the apps will see the old values in the shm * file. * * Meanwhile we keep the on-disk keyfile as-is. The next time we open * it we will notice that it's not in sync with the shm file and we'll * try to merge the two as if the changes were made by an outsider. * Eventually that may succeed... If it doesn't, what can we do? */ return DCONF_WRITER_CLASS (dconf_keyfile_writer_parent_class)->commit (writer, error); } static void dconf_keyfile_writer_end (DConfWriter *writer) { DConfKeyfileWriter *kfw = (DConfKeyfileWriter *) writer; DCONF_WRITER_CLASS (dconf_keyfile_writer_parent_class)->end (writer); g_clear_pointer (&kfw->keyfile, g_key_file_free); g_clear_pointer (&kfw->contents, g_free); close (kfw->lock_fd); kfw->lock_fd = -1; } static gboolean dconf_keyfile_update (gpointer user_data) { DConfKeyfileWriter *kfw = user_data; if (dconf_keyfile_writer_begin (DCONF_WRITER (kfw), NULL)) { dconf_keyfile_writer_commit (DCONF_WRITER (kfw), NULL); dconf_keyfile_writer_end (DCONF_WRITER (kfw)); } kfw->scheduled_update = 0; return G_SOURCE_REMOVE; } static void dconf_keyfile_writer_finalize (GObject *object) { DConfKeyfileWriter *kfw = (DConfKeyfileWriter *) object; if (kfw->scheduled_update) g_source_remove (kfw->scheduled_update); g_clear_object (&kfw->monitor); g_free (kfw->lock_filename); g_free (kfw->filename); G_OBJECT_CLASS (dconf_keyfile_writer_parent_class)->finalize (object); } static void dconf_keyfile_writer_init (DConfKeyfileWriter *kfw) { dconf_writer_set_basepath (DCONF_WRITER (kfw), "keyfile"); kfw->lock_fd = -1; } static void dconf_keyfile_writer_class_init (DConfWriterClass *class) { GObjectClass *object_class = G_OBJECT_CLASS (class); object_class->finalize = dconf_keyfile_writer_finalize; class->list = dconf_keyfile_writer_list; class->begin = dconf_keyfile_writer_begin; class->change = dconf_keyfile_writer_change; class->commit = dconf_keyfile_writer_commit; class->end = dconf_keyfile_writer_end; }