/* * Copyright © 2010 Codethink 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" #define _XOPEN_SOURCE 600 #include "dconf-engine.h" #include "../common/dconf-error.h" #include "../common/dconf-paths.h" #include "../gvdb/gvdb-reader.h" #include #include #include #include #include #include #include #include "dconf-engine-profile.h" /* 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 then the sources lock must * have been acquired first. */ #define MAX_IN_FLIGHT 2 static GSList *dconf_engine_global_list; static GMutex dconf_engine_global_lock; struct _DConfEngine { gpointer user_data; /* Set at construct time */ GDestroyNotify free_func; gint ref_count; 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; 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 */ 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) { gint i; g_mutex_lock (&engine->sources_lock); for (i = 0; i < engine->n_sources; i++) if (dconf_engine_source_refresh (engine->sources[i])) engine->state++; } static void dconf_engine_release_sources (DConfEngine *engine) { g_mutex_unlock (&engine->sources_lock); } static void dconf_engine_lock_queues (DConfEngine *engine) { g_mutex_lock (&engine->queue_lock); } static void dconf_engine_unlock_queues (DConfEngine *engine) { g_mutex_unlock (&engine->queue_lock); } DConfEngine * dconf_engine_new (const gchar *profile, gpointer user_data, GDestroyNotify free_func) { DConfEngine *engine; engine = g_slice_new0 (DConfEngine); engine->user_data = user_data; engine->free_func = free_func; engine->ref_count = 1; g_mutex_init (&engine->sources_lock); g_mutex_init (&engine->queue_lock); g_cond_init (&engine->queue_cond); engine->sources = dconf_engine_profile_open (profile, &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); return engine; } void dconf_engine_unref (DConfEngine *engine) { gint ref_count; again: ref_count = engine->ref_count; if (ref_count == 1) { 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) { 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); g_mutex_clear (&engine->sources_lock); g_mutex_clear (&engine->queue_lock); g_cond_clear (&engine->queue_cond); g_free (engine->last_handled); for (i = 0; i < engine->n_sources; i++) dconf_engine_source_free (engine->sources[i]); g_free (engine->sources); if (engine->free_func) engine->free_func (engine->user_data); g_slice_free (DConfEngine, engine); } else if (!g_atomic_int_compare_and_exchange (&engine->ref_count, ref_count, ref_count - 1)) goto again; } static DConfEngine * dconf_engine_ref (DConfEngine *engine) { g_atomic_int_inc (&engine->ref_count); return engine; } guint64 dconf_engine_get_state (DConfEngine *engine) { guint64 state; dconf_engine_acquire_sources (engine); state = engine->state; dconf_engine_release_sources (engine); return state; } static gboolean 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; 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; return TRUE; } gboolean dconf_engine_is_writable (DConfEngine *engine, const gchar *key) { gboolean writable; dconf_engine_acquire_sources (engine); writable = dconf_engine_is_writable_internal (engine, key); dconf_engine_release_sources (engine); return writable; } gchar ** dconf_engine_list_locks (DConfEngine *engine, const gchar *path, gint *length) { gchar **strv; if (dconf_is_dir (path, NULL)) { GHashTable *set; set = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, NULL); dconf_engine_acquire_sources (engine); if (engine->n_sources > 0 && engine->sources[0]->writable) { gint i, j; for (i = 1; i < engine->n_sources; i++) { if (engine->sources[i]->locks) { strv = gvdb_table_get_names (engine->sources[i]->locks, NULL); for (j = 0; strv[j]; j++) { /* It is not currently possible to lock dirs, so we * don't (yet) have to check the other direction. */ if (g_str_has_prefix (strv[j], path)) g_hash_table_add (set, strv[j]); else g_free (strv[j]); } g_free (strv); } } } else g_hash_table_add (set, g_strdup (path)); dconf_engine_release_sources (engine); strv = (gchar **) g_hash_table_get_keys_as_array (set, (guint *) length); g_hash_table_steal_all (set); g_hash_table_unref (set); } else { if (dconf_engine_is_writable (engine, path)) { strv = g_new0 (gchar *, 0 + 1); } else { strv = g_new0 (gchar *, 1 + 1); strv[0] = g_strdup (path); } } return strv; } static gboolean dconf_engine_find_key_in_queue (GQueue *queue, const gchar *key, GVariant **value) { GList *node; /* 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; return FALSE; } GVariant * dconf_engine_read (DConfEngine *engine, GQueue *read_through, const gchar *key) { GVariant *value = NULL; gint lock_level = 0; gint i; dconf_engine_acquire_sources (engine); /* 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 uncommitted 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. */ /* 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, 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) { dconf_engine_lock_queues (engine); /* Check the pending queue first because those were submitted * more recently. */ 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); } /* 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; } /* 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; if ((value = gvdb_table_get_value (engine->sources[i]->values, key))) break; } dconf_engine_release_sources (engine); return value; } GVariant * dconf_engine_read_user_value (DConfEngine *engine, GQueue *read_through, const gchar *key) { gboolean found_key = FALSE; GVariant *value = NULL; /* This is a simplified version of the above. We get to ignore locks * and system-level settings. * * NB: we may find "NULL", which is why we have a separate variable. */ /* Ignore the queues if we don't have a writable database */ if (engine->n_sources == 0 || !engine->sources[0]->writable) return NULL; dconf_engine_acquire_sources (engine); /* First check read-through */ if (read_through) found_key = dconf_engine_find_key_in_queue (read_through, key, &value); /* Next pending/in-flight */ if (!found_key) { dconf_engine_lock_queues (engine); /* Check the pending queue first because those were submitted * more recently. */ 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); } /* Finally, check the user database */ if (!found_key && engine->sources[0]->values) value = gvdb_table_get_value (engine->sources[0]->values, key); dconf_engine_release_sources (engine); return value; } gchar ** dconf_engine_list (DConfEngine *engine, const gchar *dir, gint *length) { GHashTable *results; GHashTableIter iter; gchar **list; gint n_items; gpointer key; gint i; /* 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); dconf_engine_acquire_sources (engine); for (i = 0; i < engine->n_sources; i++) { gchar **partial_list; gint j; if (engine->sources[i]->values == NULL) continue; partial_list = gvdb_table_list (engine->sources[i]->values, dir); if (partial_list != NULL) { 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); } } 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_hash_table_iter_steal (&iter); list[i++] = key; } list[i] = NULL; g_assert_cmpint (i, ==, n_items); if (length) *length = n_items; g_hash_table_unref (results); return list; } typedef void (* DConfEngineCallHandleCallback) (DConfEngine *engine, gpointer handle, GVariant *parameter, const GError *error); struct _DConfEngineCallHandle { DConfEngine *engine; DConfEngineCallHandleCallback callback; const GVariantType *expected_reply; }; static gpointer dconf_engine_call_handle_new (DConfEngine *engine, DConfEngineCallHandleCallback callback, const GVariantType *expected_reply, gsize size) { DConfEngineCallHandle *handle; g_assert (engine != NULL); g_assert (callback != NULL); g_assert (size >= sizeof (DConfEngineCallHandle)); handle = g_malloc0 (size); handle->engine = dconf_engine_ref (engine); handle->callback = callback; handle->expected_reply = expected_reply; return handle; } const GVariantType * dconf_engine_call_handle_get_expected_type (DConfEngineCallHandle *handle) { if (handle) return handle->expected_reply; else return NULL; } void dconf_engine_call_handle_reply (DConfEngineCallHandle *handle, GVariant *parameter, const GError *error) { if (handle == NULL) return; (* handle->callback) (handle->engine, handle, parameter, error); } static void dconf_engine_call_handle_free (DConfEngineCallHandle *handle) { dconf_engine_unref (handle->engine); g_free (handle); } /* returns floating */ static GVariant * dconf_engine_make_match_rule (DConfEngineSource *source, const gchar *path) { GVariant *params; gchar *rule; rule = g_strdup_printf ("type='signal'," "interface='ca.desrt.dconf.Writer'," "path='%s'," "arg0path='%s'", source->object_path, path); params = g_variant_new ("(s)", rule); g_free (rule); return params; } typedef struct { DConfEngineCallHandle handle; guint64 state; gint pending; } OutstandingWatch; static void dconf_engine_watch_established (DConfEngine *engine, gpointer handle, GVariant *reply, const GError *error) { OutstandingWatch *ow = handle; /* ignore errors */ if (--ow->pending) /* more on the way... */ return; if (ow->state != dconf_engine_get_state (engine)) { const gchar * const changes[] = { "", NULL }; /* 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, "/", changes, NULL, FALSE, NULL, engine->user_data); } dconf_engine_call_handle_free (handle); } void dconf_engine_watch_fast (DConfEngine *engine, const gchar *path) { 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); /* We start getting async calls returned as soon as we start dispatching them, * so we must not touch the 'ow' struct after we send the first one. */ for (i = 0; i < engine->n_sources; i++) if (engine->sources[i]->bus_type) ow->pending++; for (i = 0; i < engine->n_sources; i++) if (engine->sources[i]->bus_type) 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); } void dconf_engine_unwatch_fast (DConfEngine *engine, const gchar *path) { gint i; for (i = 0; i < engine->n_sources; i++) if (engine->sources[i]->bus_type) 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_handle_match_rule_sync (DConfEngine *engine, const gchar *method_name, const gchar *path) { gint i; /* 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. */ for (i = 0; i < engine->n_sources; i++) { GVariant *result; if (!engine->sources[i]->bus_type) continue; 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); if (result) g_variant_unref (result); } } void dconf_engine_watch_sync (DConfEngine *engine, const gchar *path) { dconf_engine_handle_match_rule_sync (engine, "AddMatch", path); } void dconf_engine_unwatch_sync (DConfEngine *engine, const gchar *path) { dconf_engine_handle_match_rule_sync (engine, "RemoveMatch", path); } typedef struct { DConfEngineCallHandle handle; DConfChangeset *change; } OutstandingChange; static GVariant * dconf_engine_prepare_change (DConfEngine *engine, DConfChangeset *change) { GVariant *serialised; serialised = dconf_changeset_serialise (change); 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)); } /* 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); static void dconf_engine_emit_changes (DConfEngine *engine, DConfChangeset *changeset, gpointer origin_tag) { const gchar *prefix; const gchar * const *changes; if (dconf_changeset_describe (changeset, &prefix, &changes, NULL)) dconf_engine_change_notify (engine, prefix, changes, NULL, FALSE, origin_tag, engine->user_data); } static void dconf_engine_change_completed (DConfEngine *engine, gpointer handle, GVariant *reply, const GError *error) { OutstandingChange *oc = handle; dconf_engine_lock_queues (engine); /* 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. * * Due to https://bugs.freedesktop.org/show_bug.cgi?id=59780 it is * possible that we receive an out-of-sequence error message, however, * so only assume that messages are in-order for positive replies. */ if (reply) { DConfChangeset *expected; expected = g_queue_pop_head (&engine->in_flight); g_assert (expected && oc->change == expected); } else { gboolean found; g_assert (error != NULL); found = g_queue_remove (&engine->in_flight, oc->change); g_assert (found); } /* 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); /* Deal with the reply we got. */ if (reply) { /* 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); } if (error) { /* Some kind of unexpected failure occurred 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, NULL); } dconf_changeset_unref (oc->change); dconf_engine_call_handle_free (handle); } static void dconf_engine_manage_queue (DConfEngine *engine) { if (!g_queue_is_empty (&engine->pending) && g_queue_get_length (&engine->in_flight) < MAX_IN_FLIGHT) { OutstandingChange *oc; GVariant *parameters; oc = dconf_engine_call_handle_new (engine, dconf_engine_change_completed, G_VARIANT_TYPE ("(s)"), sizeof (OutstandingChange)); oc->change = g_queue_pop_head (&engine->pending); 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); } } static gboolean dconf_engine_is_writable_changeset_predicate (const gchar *key, GVariant *value, gpointer user_data) { DConfEngine *engine = user_data; /* 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); } static gboolean dconf_engine_changeset_changes_only_writable_keys (DConfEngine *engine, DConfChangeset *changeset, GError **error) { gboolean success = TRUE; dconf_engine_acquire_sources (engine); 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_release_sources (engine); return success; } gboolean dconf_engine_change_fast (DConfEngine *engine, DConfChangeset *changeset, gpointer origin_tag, GError **error) { GList *node; if (dconf_changeset_is_empty (changeset)) return TRUE; if (!dconf_engine_changeset_changes_only_writable_keys (engine, changeset, error)) return FALSE; dconf_changeset_seal (changeset); /* 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); for (node = g_queue_peek_head_link (&engine->pending); node; node = node->next) { DConfChangeset *queued_change = node->data; 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); /* There will only have been one, so stop looking. */ break; } } /* 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); dconf_engine_unlock_queues (engine); /* Emit the signal after dropping the lock to avoid deadlock on re-entry. */ dconf_engine_emit_changes (engine, changeset, origin_tag); return TRUE; } gboolean dconf_engine_change_sync (DConfEngine *engine, DConfChangeset *changeset, gchar **tag, GError **error) { GVariant *reply; if (dconf_changeset_is_empty (changeset)) { if (tag) *tag = g_strdup (""); return TRUE; } if (!dconf_engine_changeset_changes_only_writable_keys (engine, changeset, error)) return FALSE; dconf_changeset_seal (changeset); /* 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; /* g_variant_get() is okay with NULL tag */ g_variant_get (reply, "(s)", tag); g_variant_unref (reply); return TRUE; } static gboolean dconf_engine_is_interested_in_signal (DConfEngine *engine, GBusType bus_type, const gchar *sender, const gchar *path) { gint i; for (i = 0; i < engine->n_sources; i++) { DConfEngineSource *source = engine->sources[i]; if (source->bus_type == bus_type && g_str_equal (source->object_path, path)) return TRUE; } return FALSE; } void dconf_engine_handle_dbus_signal (GBusType type, const gchar *sender, const gchar *object_path, const gchar *member, GVariant *body) { if (g_str_equal (member, "Notify")) { 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 (body, "(&s^a&s&s)", &prefix, &changes, &tag); /* Reject junk */ if (changes[0] == NULL) /* No changes? Do nothing. */ goto junk; if (dconf_is_key (prefix, NULL)) { /* If the prefix is a key then the changes must be ['']. */ if (changes[0][0] || changes[1]) goto junk; } else if (dconf_is_dir (prefix, NULL)) { /* If the prefix is a dir then we can have changes within that * dir, but they must be rel paths. * * ie: * * ('/a/', ['b', 'c/']) == ['/a/b', '/a/c/'] */ gint i; for (i = 0; changes[i]; i++) if (!dconf_is_rel_path (changes[i], NULL)) goto junk; } else /* Not a key or a dir? */ goto junk; 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)) if (dconf_engine_is_interested_in_signal (engine, type, sender, object_path)) dconf_engine_change_notify (engine, prefix, changes, tag, FALSE, NULL, engine->user_data); engines = g_slist_delete_link (engines, engines); dconf_engine_unref (engine); } junk: g_free (changes); } else if (g_str_equal (member, "WritabilityNotify")) { const gchar *empty_str_list[] = { "", NULL }; const gchar *path; GSList *engines; if (!g_variant_is_of_type (body, G_VARIANT_TYPE ("(s)"))) return; g_variant_get (body, "(&s)", &path); /* Rejecting junk here is relatively straightforward */ if (!dconf_is_path (path, NULL)) return; 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; if (dconf_engine_is_interested_in_signal (engine, type, sender, object_path)) dconf_engine_change_notify (engine, path, empty_str_list, "", TRUE, NULL, engine->user_data); engines = g_slist_delete_link (engines, engines); dconf_engine_unref (engine); } } } gboolean dconf_engine_has_outstanding (DConfEngine *engine) { gboolean has; /* 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); return has; } 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); }