/* * 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-enums.h" #include "../common/dconf-paths.h" #include "../common/dconf-gvdb-utils.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 if we were to immediately put all requests "in flight", * then we could end up in a situation where the service is kept * (needlessly) busy rewriting the database over and over again after a * sequence of fast changes on the client side. * * To avoid the issue we limit the number of in-flight requests to one. * If a request is already in flight, subsequent changes are merged into * a single aggregated pending change to be submitted as the next write * after the in-flight request completes. * * 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 three 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 queue (represented with two * fields pending and in_flight) used to implement the "fast" writes * described above. * * The third lock (subscription_count_lock) protects the two hash tables * that are used to keep track of the number of subscriptions held by * the client library to each path. * * If sources_lock and queue_lock are held at the same time then then * sources_lock must have been acquired first. * * subscription_count_lock is never held at the same time as * sources_lock or queue_lock */ 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 there are neither in-flight nor pending changes. */ DConfChangeset *pending; /* Yet to be sent on the wire. */ DConfChangeset *in_flight; /* Already sent but awaiting response. */ gchar *last_handled; /* reply tag from last item in in_flight */ /** * establishing and active, are hash tables storing the number * of subscriptions to each path in the two possible states */ /* This lock ensures that transactions involving subscription counts are atomic */ GMutex subscription_count_lock; /* active on the client side, but awaiting confirmation from the writer */ GHashTable *establishing; /* active on the client side, and with a D-Bus match rule established */ GHashTable *active; }; /* 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_queue (DConfEngine *engine) { g_mutex_lock (&engine->queue_lock); } static void dconf_engine_unlock_queue (DConfEngine *engine) { g_mutex_unlock (&engine->queue_lock); } /** * Adds the count of subscriptions to @path in @from_table to the * corresponding count in @to_table, creating it if it did not exist. * Removes the count from @from_table. */ static void dconf_engine_move_subscriptions (GHashTable *from_counts, GHashTable *to_counts, const gchar *path) { guint from_count = GPOINTER_TO_UINT (g_hash_table_lookup (from_counts, path)); guint old_to_count = GPOINTER_TO_UINT (g_hash_table_lookup (to_counts, path)); // Detect overflows g_assert (old_to_count <= G_MAXUINT - from_count); guint new_to_count = old_to_count + from_count; if (from_count != 0) { g_hash_table_remove (from_counts, path); g_hash_table_replace (to_counts, g_strdup (path), GUINT_TO_POINTER (new_to_count)); } } /** * Increments the reference count for the subscription to @path, or sets * it to 1 if it didn’t previously exist. * Returns the new reference count. */ static guint dconf_engine_inc_subscriptions (GHashTable *counts, const gchar *path) { guint old_count = GPOINTER_TO_UINT (g_hash_table_lookup (counts, path)); // Detect overflows g_assert (old_count < G_MAXUINT); guint new_count = old_count + 1; g_hash_table_replace (counts, g_strdup (path), GUINT_TO_POINTER (new_count)); return new_count; } /** * Decrements the reference count for the subscription to @path, or * removes it if the new value is 0. The count must exist and be greater * than 0. * Returns the new reference count, or 0 if it does not exist. */ static guint dconf_engine_dec_subscriptions (GHashTable *counts, const gchar *path) { guint old_count = GPOINTER_TO_UINT (g_hash_table_lookup (counts, path)); g_assert (old_count > 0); guint new_count = old_count - 1; if (new_count == 0) g_hash_table_remove (counts, path); else g_hash_table_replace (counts, g_strdup (path), GUINT_TO_POINTER (new_count)); return new_count; } /** * Returns the reference count for the subscription to @path, or 0 if it * does not exist. */ static guint dconf_engine_count_subscriptions (GHashTable *counts, const gchar *path) { return GPOINTER_TO_UINT (g_hash_table_lookup (counts, path)); } /** * Acquires the subscription counts lock, which must be held when * reading or writing to the subscription counts. */ static void dconf_engine_lock_subscription_counts (DConfEngine *engine) { g_mutex_lock (&engine->subscription_count_lock); } /** * Releases the subscription counts lock */ static void dconf_engine_unlock_subscription_counts (DConfEngine *engine) { g_mutex_unlock (&engine->subscription_count_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); g_mutex_init (&engine->subscription_count_lock); engine->establishing = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, NULL); engine->active = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, NULL); 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); g_clear_pointer (&engine->pending, dconf_changeset_unref); g_clear_pointer (&engine->in_flight, dconf_changeset_unref); for (i = 0; i < engine->n_sources; i++) dconf_engine_source_free (engine->sources[i]); g_free (engine->sources); g_hash_table_unref (engine->establishing); g_hash_table_unref (engine->active); g_mutex_clear (&engine->subscription_count_lock); 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 (const GQueue *queue, const gchar *key, GVariant **value) { GList *node; /* Tail to head... */ for (node = queue->tail; node; node = node->prev) if (dconf_changeset_get (node->data, key, value)) return TRUE; return FALSE; } GVariant * dconf_engine_read (DConfEngine *engine, DConfReadFlags flags, const 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 flags: * * If DCONF_READ_USER_VALUE is given then we completely ignore all * locks, returning the user value all the time, even if it is not * visible (because of a lock). This includes any pending value * that is in the read_through or pending queues. * * If DCONF_READ_DEFAULT_VALUE is given then we skip the writable * database and the queues (including read_through, which is * meaningless in this case) and skip directly to the non-writable * databases. This is defined as the value that the user would see * if they were to have just done a reset for that key. * * 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. */ if (~flags & DCONF_READ_USER_VALUE) 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; /* If the user has requested the default value only, then ensure * that we "find" a NULL value here. This is equivalent to the * user having reset the key, which is the definition of this * flag. */ if (flags & DCONF_READ_DEFAULT_VALUE) found_key = TRUE; /* Step 2. Check read_through. */ if (!found_key && 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_queue (engine); /* Check the pending first because those were submitted * more recently. */ if (engine->pending != NULL) found_key = dconf_changeset_get (engine->pending, key, &value); if (!found_key && engine->in_flight != NULL) found_key = dconf_changeset_get (engine->in_flight, key, &value); dconf_engine_unlock_queue (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. */ if (~flags & DCONF_READ_USER_VALUE) 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; } 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; } static gboolean dconf_engine_dir_has_writable_contents (DConfEngine *engine, const gchar *dir) { DConfChangeset *database; GHashTable *current_state; /* Read the on disk state */ if (engine->n_sources == 0 || !engine->sources[0]->writable) // If there are no writable sources, there won't be any pending writes either return FALSE; dconf_engine_acquire_sources (engine); database = dconf_gvdb_utils_changeset_from_table (engine->sources[0]->values); dconf_engine_release_sources (engine); /* Apply pending and in_flight changes to the on disk state */ dconf_engine_lock_queue (engine); if (engine->in_flight != NULL) dconf_changeset_change (database, engine->in_flight); if (engine->pending != NULL) { /** * We don't want to seal the pending changeset because it may still be changed, * and sealing the changeset would be a side effect of passing engine->pending * directly into dconf_changeset_change. */ DConfChangeset *changes = dconf_changeset_filter_changes (database, engine->pending); if (changes != NULL) { dconf_changeset_change (database, changes); dconf_changeset_unref (changes); } } dconf_engine_unlock_queue (engine); /* Check if there are writable contents at the given directory in the current state */ current_state = dconf_gvdb_utils_table_from_changeset (database); gboolean result = g_hash_table_contains (current_state, dir); g_hash_table_unref (current_state); dconf_changeset_unref (database); return result; } 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; gchar *path; } 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 under the path being watched changed. This case is * very rare, anyway... */ g_debug ("SHM invalidated while establishing subscription to %s - signalling change", ow->path); dconf_engine_change_notify (engine, ow->path, changes, NULL, FALSE, NULL, engine->user_data); } dconf_engine_lock_subscription_counts (engine); guint num_establishing = dconf_engine_count_subscriptions (engine->establishing, ow->path); g_debug ("watch_established: \"%s\" (establishing: %d)", ow->path, num_establishing); if (num_establishing > 0) // Subscription(s): establishing -> active dconf_engine_move_subscriptions (engine->establishing, engine->active, ow->path); dconf_engine_unlock_subscription_counts (engine); g_clear_pointer (&ow->path, g_free); dconf_engine_call_handle_free (handle); } void dconf_engine_watch_fast (DConfEngine *engine, const gchar *path) { dconf_engine_lock_subscription_counts (engine); guint num_establishing = dconf_engine_count_subscriptions (engine->establishing, path); guint num_active = dconf_engine_count_subscriptions (engine->active, path); g_debug ("watch_fast: \"%s\" (establishing: %d, active: %d)", path, num_establishing, num_active); if (num_active > 0) // Subscription: inactive -> active dconf_engine_inc_subscriptions (engine->active, path); else // Subscription: inactive -> establishing num_establishing = dconf_engine_inc_subscriptions (engine->establishing, path); dconf_engine_unlock_subscription_counts (engine); if (num_establishing > 1 || num_active > 0) return; 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->path = g_strdup (path); /* 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) { dconf_engine_lock_subscription_counts (engine); guint num_active = dconf_engine_count_subscriptions (engine->active, path); guint num_establishing = dconf_engine_count_subscriptions (engine->establishing, path); gint i; g_debug ("unwatch_fast: \"%s\" (active: %d, establishing: %d)", path, num_active, num_establishing); // Client code cannot unsubscribe if it is not subscribed g_assert (num_active > 0 || num_establishing > 0); if (num_active == 0) // Subscription: establishing -> inactive num_establishing = dconf_engine_dec_subscriptions (engine->establishing, path); else // Subscription: active -> inactive num_active = dconf_engine_dec_subscriptions (engine->active, path); dconf_engine_unlock_subscription_counts (engine); if (num_active > 0 || num_establishing > 0) return; 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_lock_subscription_counts (engine); guint num_active = dconf_engine_inc_subscriptions (engine->active, path); dconf_engine_unlock_subscription_counts (engine); g_debug ("watch_sync: \"%s\" (active: %d)", path, num_active - 1); if (num_active == 1) dconf_engine_handle_match_rule_sync (engine, "AddMatch", path); } void dconf_engine_unwatch_sync (DConfEngine *engine, const gchar *path) { dconf_engine_lock_subscription_counts (engine); guint num_active = dconf_engine_dec_subscriptions (engine->active, path); dconf_engine_unlock_subscription_counts (engine); g_debug ("unwatch_sync: \"%s\" (active: %d)", path, num_active + 1); if (num_active == 0) 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 the pending changeset to become the in-flight * changeset by sending the appropriate D-Bus message. * * Of course, this is only possible when there is a pending changeset * and no changeset is in-flight already. For this reason, this function * gets called in two situations: * * - when there is a new pending changeset (due to an API call) * * - when in-flight changeset had been delivered (due to a D-Bus * reply having been received) */ static void dconf_engine_manage_queue (DConfEngine *engine); /** * a #DConfChangesetPredicate which determines whether the given path and * value is already present in the given engine. "Already present" means * that setting that path to that value would have no effect on the * engine, including for directory resets. */ static gboolean dconf_engine_path_has_value_predicate (const gchar *path, GVariant *new_value, gpointer user_data) { DConfEngine *engine = user_data; // Path reset are handled specially if (g_str_has_suffix (path, "/")) return !dconf_engine_dir_has_writable_contents (engine, path); g_autoptr(GVariant) current_value = dconf_engine_read ( engine, DCONF_READ_USER_VALUE, NULL, path ); return ((current_value == NULL && new_value == NULL) || (current_value != NULL && new_value != NULL && g_variant_equal (current_value, new_value))); } 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; DConfChangeset *expected; dconf_engine_lock_queue (engine); expected = g_steal_pointer (&engine->in_flight); g_assert (expected && oc->change == expected); /* Another request could be sent now. Check for pending changes. */ dconf_engine_manage_queue (engine); dconf_engine_unlock_queue (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 (engine->pending != NULL && engine->in_flight == NULL) { OutstandingChange *oc; GVariant *parameters; oc = dconf_engine_call_handle_new (engine, dconf_engine_change_completed, G_VARIANT_TYPE ("(s)"), sizeof (OutstandingChange)); oc->change = engine->in_flight = g_steal_pointer (&engine->pending); dconf_changeset_seal (engine->in_flight); 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); } if (engine->in_flight == NULL) { /* The in-flight queue should not be empty if we have changes * pending... */ g_assert (engine->pending == NULL); 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) { g_debug ("change_fast"); if (dconf_changeset_is_empty (changeset)) return TRUE; gboolean has_no_effect = dconf_changeset_all (changeset, dconf_engine_path_has_value_predicate, engine); if (!dconf_engine_changeset_changes_only_writable_keys (engine, changeset, error)) return FALSE; dconf_changeset_seal (changeset); dconf_engine_lock_queue (engine); /* The pending changeset is kept unsealed so that it can be modified * by later calls to this functions. It wouldn't be a good idea to * repurpose the incoming changeset for this role, so create a new * one if necessary. */ if (engine->pending == NULL) engine->pending = dconf_changeset_new (); dconf_changeset_change (engine->pending, changeset); /* There might be no in-flight request yet, so we try to manage the * queue right away in order to try to promote pending changes there * (which causes the D-Bus message to actually be sent). */ dconf_engine_manage_queue (engine); dconf_engine_unlock_queue (engine); /* Emit the signal after dropping the lock to avoid deadlock on re-entry. */ if (!has_no_effect) 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; g_debug ("change_sync"); 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 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 will never be empty unless the pending is * also empty, so we only really need to check one of them... */ dconf_engine_lock_queue (engine); has = engine->in_flight != NULL; dconf_engine_unlock_queue (engine); return has; } void dconf_engine_sync (DConfEngine *engine) { g_debug ("sync"); dconf_engine_lock_queue (engine); while (engine->in_flight != NULL) g_cond_wait (&engine->queue_cond, &engine->queue_lock); dconf_engine_unlock_queue (engine); }