diff options
author | Ryan Lortie <desrt@desrt.ca> | 2012-07-13 14:14:04 -0400 |
---|---|---|
committer | Ryan Lortie <desrt@desrt.ca> | 2012-07-13 14:14:04 -0400 |
commit | 68a2895ca9896c795aa1644d30ac1ea8f129805b (patch) | |
tree | 9202d21fffcdcbb13304afff1959090f20355a94 /engine/dconf-engine.c | |
parent | bdce5c2b1780b9f6b4381fe2877f472335ecb050 (diff) | |
parent | 0bd2f8ee907b2c3e74b20c3cd58b0da1b6a586fb (diff) | |
download | dconf-68a2895ca9896c795aa1644d30ac1ea8f129805b.tar.gz |
Merge branch 'wip/reorg'
Conflicts:
bin/dconf-dump.vala
configure.ac
editor/Makefile.am
Diffstat (limited to 'engine/dconf-engine.c')
-rw-r--r-- | engine/dconf-engine.c | 1451 |
1 files changed, 930 insertions, 521 deletions
diff --git a/engine/dconf-engine.c b/engine/dconf-engine.c index aedabcf..9e44f46 100644 --- a/engine/dconf-engine.c +++ b/engine/dconf-engine.c @@ -20,9 +20,9 @@ */ #define _XOPEN_SOURCE 600 -#include "dconf-shmdir.h" #include "dconf-engine.h" -#include <gvdb-reader.h> + +#include "../gvdb/gvdb-reader.h" #include <string.h> #include <stdlib.h> #include <errno.h> @@ -31,191 +31,276 @@ #include <fcntl.h> #include <sys/mman.h> -void -dconf_engine_message_destroy (DConfEngineMessage *dcem) -{ - gint i; +#include "dconf-engine-profile.h" - for (i = 0; dcem->parameters[i]; i++) - g_variant_unref (dcem->parameters[i]); - g_free (dcem->parameters); -} +/* The engine has zero or more sources. + * + * If it has zero sources then things are very uninteresting. Nothing + * is writable, nothing will ever be written and reads will always + * return NULL. + * + * There are two interesting cases when there is a non-zero number of + * sources. Writing only ever occurs to the first source, if at all. + * Non-first sources are never writable. + * + * The first source may or may not be writable. In the usual case the + * first source is the one in the user's home directory and is writable, + * but it may be that the profile was setup for read-only access to + * system sources only. + * + * In the case that the first source is not writable (and therefore + * there are no writable sources), is_writable() will always return + * FALSE and no writes will ever be performed. + * + * It's possible to request changes in three ways: + * + * - synchronous: the D-Bus message is immediately sent to the + * dconf service and we block until we receive the reply. The change + * signal will follow soon thereafter (when we receive the signal on + * D-Bus). + * + * - asynchronous: typical asynchronous operation: we send the request + * and return immediately, notifying using a callback when the + * request is completed (and the new value is in the database). The + * change signal follows in the same way as with synchronous. + * + * - fast: we record the value locally and signal the change, returning + * immediately, as if the value is already in the database (from the + * viewpoint of the local process). We keep note of the new value + * locally until the service has confirmed that the write was + * successful. If the write fails, we emit a change signal. From + * the view of the program it looks like the value was successfully + * changed but then quickly changed back again by some external + * agent. + * + * In fast mode we have to do some management of the queue. If we + * immediately put all requests "in flight" then we can end up in a + * situation where the application writes many values for the same key + * and the service is kept (needlessly) busy writing over and over to + * the same key for some time after the requests stop coming in. + * + * If we limit the number of in-flight requests and put the other ones + * into a pending queue then we can perform merging of similar changes. + * If we notice that an item in the pending queue writes to the same + * keys as the newly-added request then we can simply drop the existing + * request (since its effect will be nullified by the new request). + * + * We want to keep the number of in-flight requests low in order to + * maximise our chance of dropping pending items, but we probably want + * it higher than 1 so that we can pipeline to hide latency. + * + * In order to minimise complexity, all changes go first to the pending + * queue. Changes are dispatched from the pending queue (and moved to + * the in-flight queue) when the number of requests in-flight is lower + * than the maximum. + * + * For both 'in_flight' and 'pending' queues we push to the tail and pop + * from the head. This puts the first operation on the head and the + * most recent operation on the tail. + * + * Since new operation go first to the pending queue, we find the most + * recent operations at the tail of that queue. Since we want to return + * the most-recently written value, we therefore scan for values + * starting at the tail of the pending queue and ending at the head of + * the in-flight queue. + * + * NB: I tell a lie. Async is not supported yet. + * + * Notes about threading: + * + * The engine is oblivious to threads and main contexts. + * + * What this means is that the engine has no interaction with GMainLoop + * and will not schedule idles or anything of the sort. All calls made + * by the engine to the client library will be made in response to + * incoming method calls, from the same thread as the incoming call. + * + * If dconf_engine_call_handle_reply() or + * dconf_engine_handle_dbus_signal() are called from 'exotic' threads + * (as will often be the case) then the resulting calls to + * dconf_engine_change_notify() will come from the same thread. That's + * left for the client library to deal with. + * + * All that said, the engine is completely threadsafe. The client + * library can call any method from any thread at any time -- as long as + * it is willing to deal with receiving the change notifies in those + * threads. + * + * Thread-safety is implemented using two locks. + * + * The first lock (sources_lock) protects the sources. Although the + * sources are only ever read from, it is necessary to lock them because + * it is not safe to read during a refresh (when the source is being + * closed and reopened). Accordingly, sources_lock need only be + * acquired when accessing the parts of the sources that are subject to + * change as a result of refreshes; the static parts (like bus type, + * object path, etc) can be accessed without holding the lock. The + * 'sources' array itself (and 'n_sources') are set at construction and + * never change after that. + * + * The second lock (queue_lock) protects the various queues that are + * used to implement the "fast" writes described above. + * + * If both locks are held at the same time thne the sources lock must + * have been acquired first. + */ -void -dconf_engine_message_copy (DConfEngineMessage *orig, - DConfEngineMessage *copy) +#define MAX_IN_FLIGHT 2 + +static GSList *dconf_engine_global_list; +static GMutex dconf_engine_global_lock; + +struct _DConfEngine { - gint i, n; + gpointer user_data; /* Set at construct time */ + GDestroyNotify free_func; + gint ref_count; - *copy = *orig; + GMutex sources_lock; /* This lock is for the sources (ie: refreshing) and state. */ + guint64 state; /* Counter that changes every time a source is refreshed. */ + DConfEngineSource **sources; /* Array never changes, but each source changes internally. */ + gint n_sources; - for (n = 0; orig->parameters[n]; n++); - copy->parameters = g_new (GVariant *, n + 1); - for (i = 0; i < n; i++) - copy->parameters[i] = g_variant_ref (orig->parameters[i]); - copy->parameters[i] = NULL; -} + GMutex queue_lock; /* This lock is for pending, in_flight, queue_cond */ + GCond queue_cond; /* Signalled when the queues empty */ + GQueue pending; /* DConfChangeset */ + GQueue in_flight; /* DConfChangeset */ -static const gchar * -dconf_engine_get_session_dir (void) + gchar *last_handled; /* reply tag from last item in in_flight */ +}; + +/* When taking the sources lock we check if any of the databases have + * had updates. + * + * Anything that is accessing the database (even only reading) needs to + * be holding the lock (since refreshes could be happening in another + * thread), so this makes sense. + * + * We could probably optimise this to avoid checking some databases in + * certain cases (ie: we do not need to check the user's database when + * we are only interested in checking writability) but this works well + * enough for now and is less prone to errors. + * + * We could probably change to a reader/writer situation that is only + * holding the write lock when actually making changes during a refresh + * but the engine is probably only ever really in use by two threads at + * a given time (main thread doing reads, DBus worker thread clearing + * the queue) so it seems unlikely that lock contention will become an + * issue. + * + * If it does, we can revisit this... + */ +static void +dconf_engine_acquire_sources (DConfEngine *engine) { - static const gchar *session_dir; - static gsize initialised; + gint i; - if (g_once_init_enter (&initialised)) - { - session_dir = dconf_shmdir_from_environment (); - g_once_init_leave (&initialised, 1); - } + g_mutex_lock (&engine->sources_lock); - return session_dir; + for (i = 0; i < engine->n_sources; i++) + if (dconf_engine_source_refresh (engine->sources[i])) + engine->state++; } -struct _DConfEngine +static void +dconf_engine_release_sources (DConfEngine *engine) { - GMutex lock; - guint64 state; - + g_mutex_unlock (&engine->sources_lock); +} - GvdbTable **gvdbs; - GvdbTable **lock_tables; - guint8 **shm; - gchar **object_paths; - gchar *bus_types; - gchar **names; - gint n_dbs; -}; +static void +dconf_engine_lock_queues (DConfEngine *engine) +{ + g_mutex_lock (&engine->queue_lock); +} static void -dconf_engine_setup_user (DConfEngine *engine, - gint i) +dconf_engine_unlock_queues (DConfEngine *engine) { - /* invariant: we never have user gvdb without shm */ - g_assert ((engine->gvdbs[i] == NULL) >= (engine->shm[i] == NULL)); + g_mutex_unlock (&engine->queue_lock); +} - if (engine->names[i]) - { - const gchar *session_dir = dconf_engine_get_session_dir (); +DConfEngine * +dconf_engine_new (gpointer user_data, + GDestroyNotify free_func) +{ + DConfEngine *engine; - if (session_dir) - { - gchar *filename; - gint fd; - - filename = g_build_filename (session_dir, - engine->names[i], - NULL); - fd = open (filename, O_RDWR | O_CREAT, 0600); - g_free (filename); - - if (fd >= 0) - { - if (ftruncate (fd, 1) == 0) - { - engine->shm[i] = mmap (NULL, 1, PROT_READ, MAP_SHARED, fd, 0); - - if (engine->shm[i] == MAP_FAILED) - engine->shm[i] = NULL; - } - - close (fd); - } - } + engine = g_slice_new0 (DConfEngine); + engine->user_data = user_data; + engine->free_func = free_func; + engine->ref_count = 1; - if (engine->shm[i]) - { - gchar *filename; - - filename = g_build_filename (g_get_user_config_dir (), - "dconf", - engine->names[i], - NULL); - engine->gvdbs[i] = gvdb_table_new (filename, FALSE, NULL); - g_free (filename); - } - } + g_mutex_init (&engine->sources_lock); + g_mutex_init (&engine->queue_lock); + g_cond_init (&engine->queue_cond); + + engine->sources = dconf_engine_profile_open (NULL, &engine->n_sources); + + g_mutex_lock (&dconf_engine_global_lock); + dconf_engine_global_list = g_slist_prepend (dconf_engine_global_list, engine); + g_mutex_unlock (&dconf_engine_global_lock); - g_assert ((engine->gvdbs[i] == NULL) >= (engine->shm[i] == NULL)); + return engine; } -static void -dconf_engine_refresh_user (DConfEngine *engine, - gint i) +void +dconf_engine_unref (DConfEngine *engine) { - g_assert ((engine->gvdbs[i] == NULL) >= (engine->shm[i] == NULL)); + gint ref_count; - /* if we failed the first time, fail forever */ - if (engine->shm[i] && *engine->shm[i] == 1) + again: + ref_count = engine->ref_count; + + if (ref_count == 1) { - if (engine->gvdbs[i]) + gint i; + + /* We are about to drop the last reference, but there is a chance + * that a signal may be happening at this very moment, causing the + * engine to gain another reference (due to its position in the + * global engine list). + * + * Acquiring the lock here means that either we will remove this + * engine from the list first or we will notice the reference + * count has increased (and skip the free). + */ + g_mutex_lock (&dconf_engine_global_lock); + if (engine->ref_count != 1) { - gvdb_table_unref (engine->gvdbs[i]); - engine->gvdbs[i] = NULL; + g_mutex_unlock (&dconf_engine_global_lock); + goto again; } + dconf_engine_global_list = g_slist_remove (dconf_engine_global_list, engine); + g_mutex_unlock (&dconf_engine_global_lock); - munmap (engine->shm[i], 1); - engine->shm[i] = NULL; + g_mutex_clear (&engine->sources_lock); + g_mutex_clear (&engine->queue_lock); + g_cond_clear (&engine->queue_cond); - dconf_engine_setup_user (engine, i); - engine->state++; - } + g_free (engine->last_handled); - g_assert ((engine->gvdbs[i] == NULL) >= (engine->shm[i] == NULL)); -} + for (i = 0; i < engine->n_sources; i++) + dconf_engine_source_free (engine->sources[i]); -static void -dconf_engine_refresh_system (DConfEngine *engine, - gint i) -{ - if (engine->gvdbs[i] && !gvdb_table_is_valid (engine->gvdbs[i])) - { - if (engine->lock_tables[i]) - { - gvdb_table_unref (engine->lock_tables[i]); - engine->lock_tables[i] = NULL; - } + g_free (engine->sources); - gvdb_table_unref (engine->gvdbs[i]); - engine->gvdbs[i] = NULL; - } + if (engine->free_func) + engine->free_func (engine->user_data); - if (engine->gvdbs[i] == NULL) - { - gchar *filename = g_build_filename ("/etc/dconf/db", - engine->names[i], NULL); - engine->gvdbs[i] = gvdb_table_new (filename, TRUE, NULL); - if (engine->gvdbs[i] == NULL) - g_error ("Unable to open '%s', specified in dconf profile\n", - filename); - engine->lock_tables[i] = gvdb_table_get_table (engine->gvdbs[i], - ".locks"); - g_free (filename); - engine->state++; + g_slice_free (DConfEngine, engine); } -} -static void -dconf_engine_refresh (DConfEngine *engine) -{ - gint i; - - for (i = 0; i < engine->n_dbs; i++) - if (engine->bus_types[i] == 'e') - dconf_engine_refresh_user (engine, i); - else - dconf_engine_refresh_system (engine, i); + else if (!g_atomic_int_compare_and_exchange (&engine->ref_count, ref_count, ref_count - 1)) + goto again; } -static void -dconf_engine_setup (DConfEngine *engine) +static DConfEngine * +dconf_engine_ref (DConfEngine *engine) { - gint i; + g_atomic_int_inc (&engine->ref_count); - for (i = 0; i < engine->n_dbs; i++) - if (engine->bus_types[i] == 'e') - dconf_engine_setup_user (engine, i); - else - dconf_engine_refresh_system (engine, i); + return engine; } guint64 @@ -223,535 +308,859 @@ dconf_engine_get_state (DConfEngine *engine) { guint64 state; - g_mutex_lock (&engine->lock); - - dconf_engine_refresh (engine); + dconf_engine_acquire_sources (engine); state = engine->state; - - g_mutex_unlock (&engine->lock); + dconf_engine_release_sources (engine); return state; } static gboolean -dconf_engine_load_profile (const gchar *profile, - gchar **bus_types, - gchar ***names, - gint *n_dbs, - GError **error) -{ - gchar *filename; - gint allocated; - char line[80]; - FILE *f; - - /* DCONF_PROFILE starting with '/' gives an absolute path to a profile */ - if (profile[0] != '/') - filename = g_build_filename ("/etc/dconf/profile", profile, NULL); - else - filename = g_strdup (profile); - - f = fopen (filename, "r"); - - if (f == NULL) - { - gint saved_errno = errno; +dconf_engine_is_writable_internal (DConfEngine *engine, + const gchar *key) +{ + gint i; + + /* We must check several things: + * + * - we have at least one source + * + * - the first source is writable + * + * - the key is not locked in a non-writable (ie: non-first) source + */ + if (engine->n_sources == 0) + return FALSE; - g_set_error (error, G_FILE_ERROR, - g_file_error_from_errno (saved_errno), - "open '%s': %s", filename, g_strerror (saved_errno)); - g_free (filename); + if (engine->sources[0]->writable == FALSE) + return FALSE; + + /* Ignore locks in the first source. + * + * Either it is writable and therefore ignoring locks is the right + * thing to do, or it's non-writable and we caught that case above. + */ + for (i = 1; i < engine->n_sources; i++) + if (engine->sources[i]->locks && gvdb_table_has_value (engine->sources[i]->locks, key)) return FALSE; - } - allocated = 4; - *bus_types = g_new (gchar, allocated); - *names = g_new (gchar *, allocated); - *n_dbs = 0; + return TRUE; +} - /* quick and dirty is good enough for now */ - while (fgets (line, sizeof line, f)) - { - const gchar *end; - const gchar *sep; +gboolean +dconf_engine_is_writable (DConfEngine *engine, + const gchar *key) +{ + gboolean writable; - end = strchr (line, '\n'); + dconf_engine_acquire_sources (engine); + writable = dconf_engine_is_writable_internal (engine, key); + dconf_engine_release_sources (engine); - if (end == NULL) - g_error ("long line in %s", filename); + return writable; +} - if (end == line) - continue; +static gboolean +dconf_engine_find_key_in_queue (GQueue *queue, + const gchar *key, + GVariant **value) +{ + GList *node; - if (line[0] == '#') - continue; + /* Tail to head... */ + for (node = g_queue_peek_tail_link (queue); node; node = node->prev) + if (dconf_changeset_get (node->data, key, value)) + return TRUE; - if (*n_dbs == allocated) - { - allocated *= 2; - *names = g_renew (gchar *, *names, allocated); - *bus_types = g_renew (gchar, *bus_types, allocated); - } + return FALSE; +} + +GVariant * +dconf_engine_read (DConfEngine *engine, + DConfChangesetList *read_through, + const gchar *key) +{ + GVariant *value = NULL; + gint lock_level = 0; + gint i; + + dconf_engine_acquire_sources (engine); - sep = strchr (line, ':'); + /* There are a number of situations that this function has to deal + * with and they interact in unusual ways. We attempt to write the + * rules for all cases here: + * + * With respect to the steady-state condition with no locks: + * + * This is the case where there are no changes queued, no + * read_through and no locks. + * + * The value returned is the one from the lowest-index source that + * contains that value. + * + * With respect to locks: + * + * If a lock is present (except in source #0 where it is ignored) + * then we will only return a value found in the source where the + * lock was present, or a higher-index source (following the normal + * rule that sources with lower indexes take priority). + * + * This statement includes read_through and queued changes. If a + * lock is found, we will ignore those. + * + * With respect to read_through and queued changed: + * + * We only consider read_through and queued changes in the event + * that we have a writable source. This will possibly cause us to + * ignore read_through and will have no real effect on the queues + * (since they will be empty anyway if we have no writable source). + * + * We only consider read_through and queued changes in the event + * that we have not found any locks. + * + * If there is a non-NULL value found in read_through or the queued + * changes then we will return that value. + * + * If there is a NULL value (ie: a reset) found in read_through or + * the queued changes then we will only ignore any value found in + * the first source (which must be writable, or else we would not + * have been considering read_through and the queues). This is + * consistent with the fact that a reset will unset any value found + * in this source but will not affect values found in lower sources. + * + * Put another way: if a non-writable source contains a value for a + * particular key then it is impossible for this function to return + * NULL. + * + * We implement the above rules as follows. We have three state + * tracking variables: + * + * - lock_level: records if and where we found a lock + * + * - found_key: records if we found the key in any queue + * + * - value: records the value of the found key (NULL for resets) + * + * We take these steps: + * + * 1. check for lockdown. If we find a lock then we prevent any + * other sources (including read_through and pending/in-flight) + * from affecting the value of the key. + * + * We record the result of this in the lock_level variable. Zero + * means that no locks were found. Non-zero means that a lock was + * found in the source with the index given by the variable. + * + * 2. check the uncommited changes in the read_through list as the + * highest priority. This is only done if we have a writable + * source and no locks were found. + * + * If we found an entry in the read_through then we set + * 'found_key' to TRUE and set 'value' to the value that we found + * (which will be NULL in the case of finding a reset request). + * + * 3. check our pending and in-flight "fast" changes (in that order). + * This is only done if we have a writable source and no locks + * were found. It is also only done if we did not find the key in + * the read_through. + * + * 4. check the first source, if there is one. + * + * This is only done if 'found_key' is FALSE. If 'found_key' is + * TRUE then it means that the first database was writable and we + * either found a value that will replace it (value != NULL) or + * found a pending reset (value == NULL) that will unset it. + * + * We only actually do this step if we have a writable first + * source and no locks found, otherwise we just let step 5 do all + * the checking. + * + * 5. check the remaining sources. + * + * We do this until we have value != NULL. Even if found_key was + * TRUE, the reset that was requested will not have affected the + * lower-level databases. + */ - if (sep) + /* Step 1. Check for locks. + * + * Note: i > 0 (strictly). Ignore locks for source #0. + */ + for (i = engine->n_sources - 1; i > 0; i--) + if (engine->sources[i]->locks && gvdb_table_has_value (engine->sources[i]->locks, key)) + { + lock_level = i; + break; + } + + /* Only do steps 2 to 4 if we have no locks and we have a writable source. */ + if (!lock_level && engine->n_sources != 0 && engine->sources[0]->writable) + { + gboolean found_key = FALSE; + + /* Step 2. Check read_through. */ + if (read_through) + found_key = dconf_engine_find_key_in_queue (&read_through->queue, key, &value); + + /* Step 3. Check queued changes if we didn't find it in read_through. + * + * NB: We may want to optimise this to avoid taking the lock in + * the case that we know both queues are empty. + */ + if (!found_key) { - /* strings MUST be 'user-db' or 'system-db'. we do the check - * this way here merely because it is the fastest. + dconf_engine_lock_queues (engine); + + /* Check the pending queue first because those were submitted + * more recently. */ - (*bus_types)[*n_dbs] = (line[0] == 'u') ? 'e' : 'y'; - (*names)[*n_dbs] = g_strndup (sep + 1, end - (sep + 1)); - } - else - { - /* default is for first DB to be user and rest to be system */ - (*bus_types)[*n_dbs] = (*n_dbs == 0) ? 'e' : 'y'; - (*names)[*n_dbs] = g_strndup (line, end - line); + found_key = dconf_engine_find_key_in_queue (&engine->pending, key, &value) || + dconf_engine_find_key_in_queue (&engine->in_flight, key, &value); + + dconf_engine_unlock_queues (engine); } - (*n_dbs)++; + /* Step 4. Check the first source. */ + if (!found_key && engine->sources[0]->values) + value = gvdb_table_get_value (engine->sources[0]->values, key); + + /* We already checked source #0 (or ignored it, as appropriate). + * + * Abuse the lock_level variable to get step 5 to skip this one. + */ + lock_level = 1; } - *bus_types = g_renew (gchar, *bus_types, *n_dbs); - *names = g_renew (gchar *, *names, *n_dbs); - g_free (filename); - fclose (f); + /* Step 5. Check the remaining sources, until value != NULL. */ + for (i = lock_level; value == NULL && i < engine->n_sources; i++) + { + if (engine->sources[i]->values == NULL) + continue; - return TRUE; + if ((value = gvdb_table_get_value (engine->sources[i]->values, key))) + break; + } + + dconf_engine_release_sources (engine); + + return value; } -DConfEngine * -dconf_engine_new (const gchar *profile) +gchar ** +dconf_engine_list (DConfEngine *engine, + const gchar *dir, + gint *length) { - DConfEngine *engine; + GHashTable *results; + GHashTableIter iter; + gchar **list; + gint n_items; + gpointer key; gint i; - engine = g_slice_new (DConfEngine); - g_mutex_init (&engine->lock); + /* This function is unreliable in the presence of pending changes. + * Here's why: + * + * Consider the case that we list("/a/") and a pending request has a + * reset request recorded for "/a/b/c". The question of if "b/" + * should appear in the output rests on if "/a/b/d" also exists. + * + * Put another way: If "/a/b/c" is the only key in "/a/b/" then + * resetting it would mean that "/a/b/" stops existing (and we should + * not include it in the output). If there are others keys then it + * will continue to exist and we should include it. + * + * Instead of trying to sort this out, we just ignore the pending + * requests and report what the on-disk file says. + */ + + results = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, NULL); - if (profile == NULL) - profile = getenv ("DCONF_PROFILE"); + dconf_engine_acquire_sources (engine); - if (profile) + for (i = 0; i < engine->n_sources; i++) { - GError *error = NULL; + gchar **partial_list; + gint j; - if (!dconf_engine_load_profile (profile, &engine->bus_types, &engine->names, &engine->n_dbs, &error)) - g_error ("Error loading dconf profile '%s': %s\n", - profile, error->message); - } - else - { - if (!dconf_engine_load_profile ("user", &engine->bus_types, &engine->names, &engine->n_dbs, NULL)) + partial_list = gvdb_table_list (engine->sources[i]->values, dir); + + if (partial_list != NULL) { - engine->names = g_new (gchar *, 1); - engine->names[0] = g_strdup ("user"); - engine->bus_types = g_strdup ("e"); - engine->n_dbs = 1; + for (j = 0; partial_list[j]; j++) + /* Steal the keys from the list. */ + g_hash_table_add (results, partial_list[j]); + + /* Free only the list. */ + g_free (partial_list); } } - if (strcmp (engine->names[0], "-") == 0) + dconf_engine_release_sources (engine); + + n_items = g_hash_table_size (results); + list = g_new (gchar *, n_items + 1); + + i = 0; + g_hash_table_iter_init (&iter, results); + while (g_hash_table_iter_next (&iter, &key, NULL)) { - g_free (engine->names[0]); - engine->names[0] = NULL; + g_hash_table_iter_steal (&iter); + list[i++] = key; } + list[i] = NULL; + g_assert_cmpint (i, ==, n_items); - engine->object_paths = g_new (gchar *, engine->n_dbs); - engine->gvdbs = g_new0 (GvdbTable *, engine->n_dbs); - engine->lock_tables = g_new0 (GvdbTable *, engine->n_dbs); - engine->shm = g_new0 (guint8 *, engine->n_dbs); - engine->state = 0; - - for (i = 0; i < engine->n_dbs; i++) - if (engine->names[i]) - engine->object_paths[i] = g_strjoin (NULL, - "/ca/desrt/dconf/Writer/", - engine->names[i], - NULL); - else - engine->object_paths[i] = NULL; + if (length) + *length = n_items; - dconf_engine_setup (engine); + g_hash_table_unref (results); - return engine; + return list; } -void -dconf_engine_free (DConfEngine *engine) +typedef void (* DConfEngineCallHandleCallback) (DConfEngine *engine, + gpointer handle, + GVariant *parameter, + const GError *error); + +struct _DConfEngineCallHandle { - gint i; + DConfEngine *engine; + DConfEngineCallHandleCallback callback; + const GVariantType *expected_reply; +}; - for (i = 0; i < engine->n_dbs; i++) - { - g_free (engine->object_paths[i]); - g_free (engine->names[i]); +static gpointer +dconf_engine_call_handle_new (DConfEngine *engine, + DConfEngineCallHandleCallback callback, + const GVariantType *expected_reply, + gsize size) +{ + DConfEngineCallHandle *handle; - if (engine->gvdbs[i]) - gvdb_table_unref (engine->gvdbs[i]); + g_assert (engine != NULL); + g_assert (callback != NULL); + g_assert (size >= sizeof (DConfEngineCallHandle)); - if (engine->lock_tables[i]) - gvdb_table_unref (engine->lock_tables[i]); + handle = g_malloc0 (size); + handle->engine = dconf_engine_ref (engine); + handle->callback = callback; + handle->expected_reply = expected_reply; - if (engine->shm[i]) - munmap (engine->shm[i], 1); - } + return handle; +} +const GVariantType * +dconf_engine_call_handle_get_expected_type (DConfEngineCallHandle *handle) +{ + return handle->expected_reply; +} - g_mutex_clear (&engine->lock); +void +dconf_engine_call_handle_reply (DConfEngineCallHandle *handle, + GVariant *parameter, + const GError *error) +{ + if (handle == NULL) + return; - g_free (engine->object_paths); - g_free (engine->bus_types); - g_free (engine->names); - g_free (engine->gvdbs); - g_free (engine->lock_tables); - g_free (engine->shm); + (* handle->callback) (handle->engine, handle, parameter, error); +} - g_slice_free (DConfEngine, engine); +static void +dconf_engine_call_handle_free (DConfEngineCallHandle *handle) +{ + dconf_engine_unref (handle->engine); + g_free (handle); } +/* returns floating */ static GVariant * -dconf_engine_read_internal (DConfEngine *engine, - const gchar *key, - gboolean user, - gboolean system) +dconf_engine_make_match_rule (DConfEngineSource *source, + const gchar *path) { - GVariant *value = NULL; - gint lowest; - gint limit; - gint i; + GVariant *params; + gchar *rule; - g_mutex_lock (&engine->lock); + rule = g_strdup_printf ("type='signal'," + "interface='ca.desrt.dconf.Writer'," + "path='%s'," + "arg0path='%s'", + source->object_path, + path); - dconf_engine_refresh (engine); + params = g_variant_new ("(s)", rule); - /* Bound the search space depending on the databases that we are - * interested in. - */ - limit = system ? engine->n_dbs : 1; - lowest = user ? 0 : 1; - - /* We want i equal to the index of the highest database containing a - * lock, or i == lowest if there is no lock. For that reason, we - * don't actually check the lowest database for a lock. That makes - * sense, because even if it had a lock, it would not change our - * search policy (which would be to check the lowest one first). - * - * Note that we intentionally dishonour 'limit' here -- we want to - * ensure that values in the user database are always ignored when - * locks are present. - */ - for (i = MAX (engine->n_dbs - 1, lowest); lowest < i; i--) - if (engine->lock_tables[i] != NULL && - gvdb_table_has_value (engine->lock_tables[i], key)) - break; + g_free (rule); - while (i < limit && value == NULL) - { - if (engine->gvdbs[i] != NULL) - value = gvdb_table_get_value (engine->gvdbs[i], key); - i++; - } + return params; +} - g_mutex_unlock (&engine->lock); +typedef struct +{ + DConfEngineCallHandle handle; - return value; -} + guint64 state; + gint pending; +} OutstandingWatch; -GVariant * -dconf_engine_read (DConfEngine *engine, - const gchar *key) +static void +dconf_engine_watch_established (DConfEngine *engine, + gpointer handle, + GVariant *reply, + const GError *error) { - return dconf_engine_read_internal (engine, key, TRUE, TRUE); + OutstandingWatch *ow = handle; + + /* ignore errors */ + + if (--ow->pending) + /* more on the way... */ + return; + + if (ow->state != dconf_engine_get_state (engine)) + { + /* Our recorded state does not match the current state. Something + * must have changed while our watch requests were on the wire. + * + * We don't know what changed, so we can just say that potentially + * everything changed. This case is very rare, anyway... + */ + dconf_engine_change_notify (engine, "/", NULL, engine->user_data, NULL); + } + + dconf_engine_call_handle_free (handle); } -GVariant * -dconf_engine_read_default (DConfEngine *engine, - const gchar *key) +void +dconf_engine_watch_fast (DConfEngine *engine, + const gchar *path) { - return dconf_engine_read_internal (engine, key, FALSE, TRUE); + OutstandingWatch *ow; + gint i; + + if (engine->n_sources == 0) + return; + + /* It's possible (although rare) that the dconf database could change + * while our match rule is on the wire. + * + * Since we returned immediately (suggesting to the user that the + * watch was already established) we could have a race. + * + * To deal with this, we use the current state counter to ensure that nothing + * changes while the watch requests are on the wire. + */ + ow = dconf_engine_call_handle_new (engine, dconf_engine_watch_established, + G_VARIANT_TYPE_UNIT, sizeof (OutstandingWatch)); + ow->state = dconf_engine_get_state (engine); + ow->pending = engine->n_sources; + + for (i = 0; i < engine->n_sources; i++) + dconf_engine_dbus_call_async_func (engine->sources[i]->bus_type, "org.freedesktop.DBus", + "/org/freedesktop/DBus", "org.freedesktop.DBus", "AddMatch", + dconf_engine_make_match_rule (engine->sources[i], path), + &ow->handle, NULL); } -GVariant * -dconf_engine_read_no_default (DConfEngine *engine, - const gchar *key) +void +dconf_engine_unwatch_fast (DConfEngine *engine, + const gchar *path) { - return dconf_engine_read_internal (engine, key, TRUE, FALSE); + gint i; + + for (i = 0; i < engine->n_sources; i++) + dconf_engine_dbus_call_async_func (engine->sources[i]->bus_type, "org.freedesktop.DBus", + "/org/freedesktop/DBus", "org.freedesktop.DBus", "RemoveMatch", + dconf_engine_make_match_rule (engine->sources[i], path), NULL, NULL); } static void -dconf_engine_make_match_rule (DConfEngine *engine, - DConfEngineMessage *dcem, - const gchar *name, - const gchar *method_name) +dconf_engine_handle_match_rule_sync (DConfEngine *engine, + const gchar *method_name, + const gchar *path) { gint i; - dcem->bus_name = "org.freedesktop.DBus"; - dcem->object_path = "/org/freedesktop/DBus"; - dcem->interface_name = "org.freedesktop.DBus"; - dcem->method_name = method_name; + /* We need not hold any locks here because we are only touching static + * things: the number of sources, and static properties of each source + * itself. + * + * This function silently ignores all errors. + */ - dcem->parameters = g_new (GVariant *, engine->n_dbs + 1); - for (i = 0; i < engine->n_dbs; i++) + for (i = 0; i < engine->n_sources; i++) { - gchar *rule; - - rule = g_strdup_printf ("type='signal'," - "interface='ca.desrt.dconf.Writer'," - "path='%s'," - "arg0path='%s'", - engine->object_paths[i], - name); - dcem->parameters[i] = g_variant_new ("(s)", rule); - g_variant_ref_sink (dcem->parameters[i]); - g_free (rule); - } - dcem->parameters[i] = NULL; + GVariant *result; + + result = dconf_engine_dbus_call_sync_func (engine->sources[i]->bus_type, "org.freedesktop.DBus", + "/org/freedesktop/DBus", "org.freedesktop.DBus", method_name, + dconf_engine_make_match_rule (engine->sources[i], path), + G_VARIANT_TYPE_UNIT, NULL); - dcem->bus_types = engine->bus_types; - dcem->n_messages = engine->n_dbs; - dcem->reply_type = G_VARIANT_TYPE_UNIT; + if (result) + g_variant_unref (result); + } } void -dconf_engine_watch (DConfEngine *engine, - const gchar *name, - DConfEngineMessage *dcem) +dconf_engine_watch_sync (DConfEngine *engine, + const gchar *path) { - dconf_engine_make_match_rule (engine, dcem, name, "AddMatch"); + dconf_engine_handle_match_rule_sync (engine, "AddMatch", path); } void -dconf_engine_unwatch (DConfEngine *engine, - const gchar *name, - DConfEngineMessage *dcem) +dconf_engine_unwatch_sync (DConfEngine *engine, + const gchar *path) { - dconf_engine_make_match_rule (engine, dcem, name, "RemoveMatch"); + dconf_engine_handle_match_rule_sync (engine, "RemoveMatch", path); } -gboolean -dconf_engine_is_writable (DConfEngine *engine, - const gchar *name) +typedef struct { - gboolean writable = TRUE; + DConfEngineCallHandle handle; - /* Only check if we have more than one database */ - if (engine->n_dbs > 1) - { - gint i; + DConfChangeset *change; +} OutstandingChange; - g_mutex_lock (&engine->lock); +static GVariant * +dconf_engine_prepare_change (DConfEngine *engine, + DConfChangeset *change) +{ + GVariant *serialised; - dconf_engine_refresh (engine); + serialised = dconf_changeset_serialise (change); - /* Don't check for locks in the top database (i == 0). */ - for (i = engine->n_dbs - 1; 0 < i; i--) - if (engine->lock_tables[i] != NULL && - gvdb_table_has_value (engine->lock_tables[i], name)) - { - writable = FALSE; - break; - } + return g_variant_new_from_data (G_VARIANT_TYPE ("(ay)"), + g_variant_get_data (serialised), g_variant_get_size (serialised), TRUE, + (GDestroyNotify) g_variant_unref, g_variant_ref_sink (serialised)); +} - g_mutex_unlock (&engine->lock); - } +/* This function promotes changes from the pending queue to the + * in-flight queue by sending the appropriate D-Bus message. + * + * Of course, this is only possible when there are pending items and + * room in the in-flight queue. For this reason, this function gets + * called in two situations: + * + * - an item has been added to the pending queue (due to an API call) + * + * - an item has been removed from the inflight queue (due to a D-Bus + * reply having been received) + * + * It will move a maximum of one item. + */ +static void dconf_engine_manage_queue (DConfEngine *engine); - return writable; +static void +dconf_engine_emit_changes (DConfEngine *engine, + DConfChangeset *changeset) +{ + const gchar *prefix; + const gchar * const *changes; + + if (dconf_changeset_describe (changeset, &prefix, &changes, NULL)) + dconf_engine_change_notify (engine, prefix, changes, NULL, engine->user_data); } -/* be conservative and fast: false negatives are OK */ -static gboolean -is_dbusable (GVariant *value) +static void +dconf_engine_change_completed (DConfEngine *engine, + gpointer handle, + GVariant *reply, + const GError *error) { - const gchar *type; + OutstandingChange *oc = handle; + DConfChangeset *expected; - type = g_variant_get_type_string (value); + dconf_engine_lock_queues (engine); - /* maybe definitely won't work. - * variant? too lazy to check inside... + /* D-Bus guarantees ordered delivery of messages. + * + * The dconf-service handles requests in-order. + * + * The reply we just received should therefore be at the head of + * our 'in flight' queue. */ - if (strchr (type, 'v') || strchr (type, 'm')) - return FALSE; + expected = g_queue_pop_head (&engine->in_flight); + g_assert (expected && oc->change == expected); - /* XXX: we could also check for '{}' not inside an array... - * but i'm not sure we want to support that anyway. + /* We just popped a change from the in-flight queue, possibly + * making room for another to be added. Check that. */ + dconf_engine_manage_queue (engine); + dconf_engine_unlock_queues (engine); - /* this will avoid any too-deeply-nested limits */ - return strlen (type) < 32; -} - -static GVariant * -fake_maybe (GVariant *value) -{ - GVariantBuilder builder; - - g_variant_builder_init (&builder, G_VARIANT_TYPE ("av")); - - if (value != NULL) + /* Deal with the reply we got. */ + if (reply) { - if (is_dbusable (value)) - g_variant_builder_add (&builder, "v", value); + /* The write worked. + * + * We already sent a change notification for this item when we + * added it to the pending queue and we don't want to send another + * one again. At the same time, it's very likely that we're just + * about to receive a change signal from the service. + * + * The tag sent as part of the reply to the Change call will be + * the same tag as on the change notification signal. Record that + * tag so that we can ignore the signal when it comes. + * + * last_handled is only ever touched from the worker thread + */ + g_free (engine->last_handled); + g_variant_get (reply, "(s)", &engine->last_handled); + } - else - { - GVariant *variant; - GVariant *ay; - - variant = g_variant_new_variant (value); - ay = g_variant_new_from_data (G_VARIANT_TYPE_BYTESTRING, - g_variant_get_data (variant), - g_variant_get_size (variant), - TRUE, - (GDestroyNotify) g_variant_unref, - variant); - g_variant_builder_add (&builder, "v", ay); - - g_variant_builder_add (&builder, "v", - g_variant_new_string ("serialised GVariant")); - } + if (error) + { + /* Some kind of unexpected failure occured while attempting to + * commit the change. + * + * There's not much we can do here except to drop our local copy + * of the change (and notify that it is gone) and print the error + * message as a warning. + */ + g_warning ("failed to commit changes to dconf: %s", error->message); + dconf_engine_emit_changes (engine, oc->change); } - return g_variant_builder_end (&builder); + dconf_changeset_unref (oc->change); + dconf_engine_call_handle_free (handle); } static void -dconf_engine_dcem (DConfEngine *engine, - DConfEngineMessage *dcem, - const gchar *method_name, - const gchar *format_string, - ...) +dconf_engine_manage_queue (DConfEngine *engine) { - va_list ap; + if (!g_queue_is_empty (&engine->pending) && g_queue_get_length (&engine->in_flight) < MAX_IN_FLIGHT) + { + OutstandingChange *oc; + GVariant *parameters; - dcem->bus_name = "ca.desrt.dconf"; - dcem->object_path = engine->object_paths[0]; - dcem->interface_name = "ca.desrt.dconf.Writer"; - dcem->method_name = method_name; - dcem->parameters = g_new (GVariant *, 2); - dcem->n_messages = 1; + oc = dconf_engine_call_handle_new (engine, dconf_engine_change_completed, + G_VARIANT_TYPE ("(s)"), sizeof (OutstandingChange)); - va_start (ap, format_string); - dcem->parameters[0] = g_variant_new_va (format_string, NULL, &ap); - g_variant_ref_sink (dcem->parameters[0]); - dcem->parameters[1] = NULL; - va_end (ap); + oc->change = g_queue_pop_head (&engine->pending); - dcem->bus_types = engine->bus_types; - dcem->reply_type = G_VARIANT_TYPE ("(s)"); + parameters = dconf_engine_prepare_change (engine, oc->change); + + dconf_engine_dbus_call_async_func (engine->sources[0]->bus_type, + engine->sources[0]->bus_name, + engine->sources[0]->object_path, + "ca.desrt.dconf.Writer", "Change", + parameters, &oc->handle, NULL); + + g_queue_push_tail (&engine->in_flight, oc->change); + } + + if (g_queue_is_empty (&engine->in_flight)) + { + /* The in-flight queue should not be empty if we have changes + * pending... + */ + g_assert (g_queue_is_empty (&engine->pending)); + + g_cond_broadcast (&engine->queue_cond); + } } -gboolean -dconf_engine_write (DConfEngine *engine, - const gchar *name, - GVariant *value, - DConfEngineMessage *dcem, - GError **error) +static gboolean +dconf_engine_is_writable_changeset_predicate (const gchar *key, + GVariant *value, + gpointer user_data) { - dconf_engine_dcem (engine, dcem, - "Write", "(s@av)", - name, fake_maybe (value)); + DConfEngine *engine = user_data; - return TRUE; + /* Resets absolutely always succeed -- even in the case that there is + * not even a writable database. + */ + return value == NULL || dconf_engine_is_writable_internal (engine, key); } -gboolean -dconf_engine_write_many (DConfEngine *engine, - const gchar *prefix, - const gchar * const *keys, - GVariant **values, - DConfEngineMessage *dcem, - GError **error) +static gboolean +dconf_engine_changeset_changes_only_writable_keys (DConfEngine *engine, + DConfChangeset *changeset, + GError **error) { - GVariantBuilder builder; - gsize i; + gboolean success = TRUE; - g_variant_builder_init (&builder, G_VARIANT_TYPE ("a(sav)")); + dconf_engine_acquire_sources (engine); - for (i = 0; keys[i]; i++) - g_variant_builder_add (&builder, "(s@av)", - keys[i], fake_maybe (values[i])); + if (!dconf_changeset_all (changeset, dconf_engine_is_writable_changeset_predicate, engine)) + { + g_set_error_literal (error, DCONF_ERROR, DCONF_ERROR_NOT_WRITABLE, + "The operation attempted to modify one or more non-writable keys"); + success = FALSE; + } - dconf_engine_dcem (engine, dcem, "WriteMany", "(sa(sav))", prefix, &builder); + dconf_engine_release_sources (engine); - return TRUE; + return success; } -gchar ** -dconf_engine_list (DConfEngine *engine, - const gchar *dir, - gint *length) +gboolean +dconf_engine_change_fast (DConfEngine *engine, + DConfChangeset *changeset, + GError **error) { - gchar **list; + GList *node; + + if (!dconf_engine_changeset_changes_only_writable_keys (engine, changeset, error)) + return FALSE; - g_mutex_lock (&engine->lock); + /* Check for duplicates in the pending queue. + * + * Note: order doesn't really matter here since "similarity" is an + * equivalence class and we've ensured that there are no pairwise + * similar changes in the queue already (ie: at most we will have only + * one similar item to the one we are adding). + */ + dconf_engine_lock_queues (engine); - dconf_engine_refresh (engine); + for (node = g_queue_peek_head_link (&engine->pending); node; node = node->next) + { + DConfChangeset *queued_change = node->data; - if (engine->gvdbs[0]) - list = gvdb_table_list (engine->gvdbs[0], dir); - else - list = NULL; + if (dconf_changeset_is_similar_to (changeset, queued_change)) + { + /* We found a similar item in the queue. + * + * We want to drop the one that's in the queue already since + * we want our new (more recent) change to take precedence. + * + * The pending queue owned the changeset, so free it. + */ + g_queue_delete_link (&engine->pending, node); + dconf_changeset_unref (queued_change); - if (list == NULL) - list = g_new0 (char *, 1); + /* There will only have been one, so stop looking. */ + break; + } + } - if (length) - *length = g_strv_length (list); + /* No matter what we're going to queue up this change, so put it in + * the pending queue now. + * + * There may be room in the in_flight queue, so we try to manage the + * queue right away in order to try to promote it there (which causes + * the D-Bus message to actually be sent). + * + * The change might get tossed before being sent if the loop above + * finds it on a future call. + */ + g_queue_push_tail (&engine->pending, dconf_changeset_ref (changeset)); + dconf_engine_manage_queue (engine); - g_mutex_unlock (&engine->lock); + dconf_engine_unlock_queues (engine); - return list; + /* Emit the signal after dropping the lock to avoid deadlock on re-entry. */ + dconf_engine_emit_changes (engine, changeset); + + return TRUE; } gboolean -dconf_engine_decode_notify (DConfEngine *engine, - const gchar *anti_expose, - const gchar **path, - const gchar ***rels, - guint bus_type, - const gchar *sender, - const gchar *iface, - const gchar *method, - GVariant *body) -{ - if (strcmp (iface, "ca.desrt.dconf.Writer") || strcmp (method, "Notify")) +dconf_engine_change_sync (DConfEngine *engine, + DConfChangeset *changeset, + gchar **tag, + GError **error) +{ + GVariant *reply; + + if (!dconf_engine_changeset_changes_only_writable_keys (engine, changeset, error)) return FALSE; - if (!g_variant_is_of_type (body, G_VARIANT_TYPE ("(sass)"))) + /* we know that we have at least one source because we checked writability */ + reply = dconf_engine_dbus_call_sync_func (engine->sources[0]->bus_type, + engine->sources[0]->bus_name, + engine->sources[0]->object_path, + "ca.desrt.dconf.Writer", "Change", + dconf_engine_prepare_change (engine, changeset), + G_VARIANT_TYPE ("(s)"), error); + + if (reply == NULL) return FALSE; - if (anti_expose) + /* g_variant_get() is okay with NULL tag */ + g_variant_get (reply, "(s)", tag); + g_variant_unref (reply); + + return TRUE; +} + +void +dconf_engine_handle_dbus_signal (GBusType type, + const gchar *sender, + const gchar *path, + const gchar *member, + GVariant *body) +{ + if (g_str_equal (member, "Notify")) { - const gchar *ae; + const gchar *prefix; + const gchar **changes; + const gchar *tag; + GSList *engines; + + if (!g_variant_is_of_type (body, G_VARIANT_TYPE ("(sass)"))) + return; - g_variant_get_child (body, 2, "&s", &ae); + g_variant_get (body, "(&s^a&s&s)", &prefix, &changes, &tag); + + g_mutex_lock (&dconf_engine_global_lock); + engines = g_slist_copy_deep (dconf_engine_global_list, (GCopyFunc) dconf_engine_ref, NULL); + g_mutex_unlock (&dconf_engine_global_lock); + + while (engines) + { + DConfEngine *engine = engines->data; + + /* It's possible that this incoming change notify is for a + * change that we already announced to the client when we + * placed it in the pending queue. + * + * Check last_handled to determine if we should ignore it. + */ + if (!engine->last_handled || !g_str_equal (engine->last_handled, tag)) + dconf_engine_change_notify (engine, prefix, changes, tag, engine->user_data); + + engines = g_slist_delete_link (engines, engines); + + dconf_engine_unref (engine); + } - if (strcmp (ae, anti_expose) == 0) - return FALSE; + g_free (changes); } - g_variant_get (body, "(&s^a&ss)", path, rels, NULL); + else if (g_str_equal (member, "WritabilityNotify")) + { + if (!g_variant_is_of_type (body, G_VARIANT_TYPE ("(s)"))) + return; - return TRUE; + g_warning ("Need to handle writability changes"); /* XXX */ + } } gboolean -dconf_engine_decode_writability_notify (const gchar **path, - const gchar *iface, - const gchar *method, - GVariant *body) +dconf_engine_has_outstanding (DConfEngine *engine) { - if (strcmp (iface, "ca.desrt.dconf.Writer") || - strcmp (method, "WritabilityNotify")) - return FALSE; + gboolean has; - if (!g_variant_is_of_type (body, G_VARIANT_TYPE ("(s)"))) - return FALSE; + /* The in-flight queue will never be empty unless the pending queue is + * also empty, so we only really need to check one of them... + */ + dconf_engine_lock_queues (engine); + has = !g_queue_is_empty (&engine->in_flight); + dconf_engine_unlock_queues (engine); - g_variant_get_child (body, 0, "&s", path); + return has; +} - return TRUE; +void +dconf_engine_sync (DConfEngine *engine) +{ + dconf_engine_lock_queues (engine); + while (!g_queue_is_empty (&engine->in_flight)) + g_cond_wait (&engine->queue_cond, &engine->queue_lock); + dconf_engine_unlock_queues (engine); } |