/**
* Copyright © 2010 Canonical Limited
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the licence, or (at
* your option) any later version.
*
* This program 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
* General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see .
*
* Author: Ryan Lortie
**/
#include "dconf-dbus-1.h"
#include
#include
typedef struct _Outstanding Outstanding;
struct _DConfDBusClient
{
DBusConnection *session_bus;
DBusConnection *system_bus;
GSList *watches;
gint ref_count;
Outstanding *outstanding;
gchar *anti_expose_tag;
DConfEngine *engine;
};
static void
dconf_dbus_client_add_value_to_iter (DBusMessageIter *iter,
GVariant *value)
{
GVariantClass class;
class = g_variant_classify (value);
switch (class)
{
case G_VARIANT_CLASS_BOOLEAN:
{
dbus_bool_t boolean;
boolean = g_variant_get_boolean (value);
dbus_message_iter_append_basic (iter, 'b', &boolean);
}
break;
case G_VARIANT_CLASS_BYTE:
case G_VARIANT_CLASS_INT16:
case G_VARIANT_CLASS_UINT16:
case G_VARIANT_CLASS_INT32:
case G_VARIANT_CLASS_UINT32:
case G_VARIANT_CLASS_INT64:
case G_VARIANT_CLASS_UINT64:
case G_VARIANT_CLASS_DOUBLE:
dbus_message_iter_append_basic (iter, class,
g_variant_get_data (value));
break;
case G_VARIANT_CLASS_STRING:
case G_VARIANT_CLASS_OBJECT_PATH:
case G_VARIANT_CLASS_SIGNATURE:
{
const gchar *str;
str = g_variant_get_string (value, NULL);
dbus_message_iter_append_basic (iter, class, &str);
}
break;
case G_VARIANT_CLASS_ARRAY:
{
const gchar *contained;
DBusMessageIter sub;
gint i, n;
contained = g_variant_get_type_string (value) + 1;
n = g_variant_n_children (value);
dbus_message_iter_open_container (iter, 'a', contained, &sub);
for (i = 0; i < n; i++)
{
GVariant *child;
child = g_variant_get_child_value (value, i);
dconf_dbus_client_add_value_to_iter (&sub, child);
g_variant_unref (child);
}
dbus_message_iter_close_container (iter, &sub);
}
break;
case G_VARIANT_CLASS_TUPLE:
{
DBusMessageIter sub;
gint i, n;
n = g_variant_n_children (value);
dbus_message_iter_open_container (iter, 'r', NULL, &sub);
for (i = 0; i < n; i++)
{
GVariant *child;
child = g_variant_get_child_value (value, i);
dconf_dbus_client_add_value_to_iter (&sub, child);
g_variant_unref (child);
}
dbus_message_iter_close_container (iter, &sub);
}
break;
case G_VARIANT_CLASS_DICT_ENTRY:
{
DBusMessageIter sub;
gint i;
dbus_message_iter_open_container (iter, 'e', NULL, &sub);
for (i = 0; i < 2; i++)
{
GVariant *child;
child = g_variant_get_child_value (value, i);
dconf_dbus_client_add_value_to_iter (&sub, child);
g_variant_unref (child);
}
dbus_message_iter_close_container (iter, &sub);
}
break;
case G_VARIANT_CLASS_VARIANT:
{
DBusMessageIter sub;
GVariant *child;
child = g_variant_get_variant (value);
dbus_message_iter_open_container (iter, 'v',
g_variant_get_type_string (child),
&sub);
dconf_dbus_client_add_value_to_iter (&sub, child);
dbus_message_iter_close_container (iter, &sub);
g_variant_unref (child);
}
break;
default:
g_assert_not_reached ();
}
}
static void
dconf_dbus_client_send (DConfDBusClient *dcdbc,
DConfEngineMessage *dcem,
DBusPendingCallNotifyFunction callback,
gpointer user_data)
{
DBusConnection *connection;
gint i;
for (i = 0; i < dcem->n_messages; i++)
{
switch (dcem->bus_types[i])
{
case 'e':
connection = dcdbc->session_bus;
break;
case 'y':
connection = dcdbc->system_bus;
break;
default:
g_assert_not_reached ();
}
if (connection == NULL && callback != NULL)
callback (NULL, user_data);
if (connection != NULL)
{
DBusPendingCall *pending;
DBusMessageIter diter;
DBusMessage *message;
GVariantIter giter;
GVariant *child;
message = dbus_message_new_method_call (dcem->bus_name,
dcem->object_path,
dcem->interface_name,
dcem->method_name);
dbus_message_iter_init_append (message, &diter);
g_variant_iter_init (&giter, dcem->parameters[i]);
while ((child = g_variant_iter_next_value (&giter)))
{
dconf_dbus_client_add_value_to_iter (&diter, child);
g_variant_unref (child);
}
dbus_connection_send_with_reply (connection, message,
&pending, 120000);
dbus_pending_call_set_notify (pending, callback, user_data, NULL);
dbus_message_unref (message);
}
}
}
static GVariant *
dconf_dbus_client_send_finish (DBusPendingCall *pending)
{
DBusMessage *message;
GVariant *result;
if (pending == NULL)
return NULL;
message = dbus_pending_call_steal_reply (pending);
dbus_pending_call_unref (pending);
/* We only have to deal with two types of replies: () and (s) */
if (dbus_message_has_signature (message, "s"))
{
dbus_message_get_args (message, NULL,
DBUS_TYPE_STRING, &result,
DBUS_TYPE_INVALID);
result = g_variant_new ("(s)", result);
}
else
result = g_variant_new ("()");
dbus_message_unref (message);
return result;
}
struct _Outstanding
{
Outstanding *next;
DConfDBusClient *dcdbc;
DConfEngineMessage dcem;
gchar *set_key;
GVariant *set_value;
GTree *tree;
};
static void
dconf_dbus_client_outstanding_returned (DBusPendingCall *pending,
gpointer user_data)
{
Outstanding *outstanding = user_data;
DConfDBusClient *dcdbc;
GVariant *reply;
dcdbc = outstanding->dcdbc;
/* One way or another we no longer need this hooked into the list.
*/
{
Outstanding **tmp;
for (tmp = &dcdbc->outstanding; tmp; tmp = &(*tmp)->next)
if (*tmp == outstanding)
{
*tmp = outstanding->next;
break;
}
}
reply = dconf_dbus_client_send_finish (pending);
if (reply)
{
/* Success.
*
* We want to ensure that we don't emit an extra change
* notification signal when we see the signal that the service is
* about to send, so store the tag so we know to ignore it when
* the signal comes.
*
* No thread safety issue here since this variable is only
* accessed from the worker thread.
*/
g_free (dcdbc->anti_expose_tag);
if (g_variant_is_of_type (reply, G_VARIANT_TYPE ("(s)")))
g_variant_get_child (reply, 0, "s", dcdbc->anti_expose_tag);
g_variant_unref (reply);
}
else
{
/* An error of some kind.
*
* We already removed the outstanding entry from the list, so the
* unmodified database is now visible to the client. Change
* notify so that they see it.
*/
if (outstanding->set_key)
/* XXX emit */;
else
/* XXX emit */;
}
dconf_engine_message_destroy (&outstanding->dcem);
dconf_dbus_client_unref (outstanding->dcdbc);
g_free (outstanding->set_key);
if (outstanding->set_value)
g_variant_unref (outstanding->set_value);
if (outstanding->tree)
g_tree_unref (outstanding->tree);
g_slice_free (Outstanding, outstanding);
}
static void
dconf_dbus_client_queue (DConfDBusClient *dcdbc,
DConfEngineMessage *dcem,
const gchar *set_key,
GVariant *set_value,
GTree *tree)
{
Outstanding *outstanding;
outstanding = g_slice_new (Outstanding);
outstanding->dcdbc = dconf_dbus_client_ref (dcdbc);
outstanding->dcem = *dcem;
outstanding->set_key = g_strdup (set_key);
outstanding->set_value = set_value ? g_variant_ref_sink (set_value) : NULL;
outstanding->tree = tree ? g_tree_ref (tree) : NULL;
outstanding->next = dcdbc->outstanding;
dcdbc->outstanding = outstanding;
dconf_dbus_client_send (outstanding->dcdbc,
&outstanding->dcem,
dconf_dbus_client_outstanding_returned,
outstanding);
}
static gboolean
dconf_dbus_client_scan_outstanding_tree (GTree *tree,
const gchar *key,
gsize key_length,
gpointer *value)
{
gchar *mykey;
mykey = g_alloca (key_length + 1);
memcpy (mykey, key, key_length + 1);
while (!g_tree_lookup_extended (tree, mykey, NULL, value) &&
--key_length)
{
while (mykey[key_length - 1] != '/')
key_length--;
mykey[key_length] = '\0';
}
return key_length != 0;
}
static gboolean
dconf_dbus_client_scan_outstanding (DConfDBusClient *dcdbc,
const gchar *key,
GVariant **value)
{
gboolean found = FALSE;
Outstanding *node;
gsize length;
length = strlen (key);
if G_LIKELY (dcdbc->outstanding == NULL)
return FALSE;
for (node = dcdbc->outstanding; node; node = node->next)
{
if (node->set_key)
{
if (strcmp (key, node->set_key) == 0)
{
if (node->set_value != NULL)
*value = g_variant_ref (node->set_value);
else
*value = NULL;
found = TRUE;
break;
}
}
else
{
gpointer result;
if (dconf_dbus_client_scan_outstanding_tree (node->tree, key,
length, &result))
{
if (result)
*value = g_variant_ref (result);
else
*value = NULL;
found = TRUE;
break;
}
}
}
return found;
}
/* Watches are reference counted because they can be held both by the
* list of watches and by the pending watch registration. In the normal
* case, the registration completes before the watch is unsubscribed
* from but it might be the case that the watch is unsubscribed from
* before the AddMatch completes. For that reason, either thing could
* be responsible for freeing the watch structure; we solve that
* ambiguity using a reference count.
*
* We just initially set it to 2, since these are the only two users.
* That way we can skip having the ref() function.
*/
typedef struct
{
DConfDBusClient *dcdbc;
gchar *name;
DConfDBusNotify notify;
gpointer user_data;
guint64 initial_state;
gint ref_count;
} Watch;
static void
dconf_dbus_emit_change (DConfDBusClient *dcdbc,
const gchar *key)
{
GSList *iter;
for (iter = dcdbc->watches; iter; iter = iter->next)
{
Watch *watch = iter->data;
if (g_str_has_prefix (key, watch->name))
watch->notify (dcdbc, key, watch->user_data);
}
}
GVariant *
dconf_dbus_client_read (DConfDBusClient *dcdbc,
const gchar *key)
{
GVariant *value;
if (dconf_dbus_client_scan_outstanding (dcdbc, key, &value))
return value;
return dconf_engine_read (dcdbc->engine, key);
}
gboolean
dconf_dbus_client_write (DConfDBusClient *dcdbc,
const gchar *key,
GVariant *value)
{
DConfEngineMessage dcem;
if (!dconf_engine_write (dcdbc->engine, key, value, &dcem, NULL))
return FALSE;
dconf_dbus_client_queue (dcdbc, &dcem, key, value, NULL);
dconf_dbus_emit_change (dcdbc, key);
return TRUE;
}
static Watch *
watch_new (DConfDBusClient *dcdbc,
const gchar *name,
DConfDBusNotify notify,
gpointer user_data)
{
Watch *watch;
watch = g_slice_new (Watch);
watch->dcdbc = dconf_dbus_client_ref (dcdbc);
watch->user_data = user_data;
watch->name = g_strdup (name);
watch->notify = notify;
watch->initial_state = dconf_engine_get_state (dcdbc->engine);
watch->ref_count = 2;
dcdbc->watches = g_slist_prepend (dcdbc->watches, watch);
return watch;
}
static void
watch_unref (Watch *watch)
{
if (--watch->ref_count == 0)
{
dconf_dbus_client_unref (watch->dcdbc);
g_free (watch->name);
g_slice_free (Watch, watch);
}
}
static void
add_match_done (DBusPendingCall *pending,
gpointer user_data)
{
Watch *watch = user_data;
GError *error = NULL;
GVariant *reply;
reply = dconf_dbus_client_send_finish (pending);
if (reply == NULL)
{
/* This is extremely unlikely to happen and it happens
* asynchronous to the user's call. Since the user doesn't know
* that it happened, we pretend that it didn't (ie: we leave the
* watch structure in the list).
*/
g_critical ("DBus AddMatch for dconf path '%s': %s",
watch->name, error->message);
g_error_free (error);
watch_unref (watch);
return;
}
else
g_variant_unref (reply); /* it is just an empty tuple */
/* In the normal case we're done.
*
* There is a fleeting chance, however, that the database has changed
* in the meantime. In that case we can only assume that the subject
* of this watch changed in that time period and emit a signal to that
* effect.
*/
if (dconf_engine_get_state (watch->dcdbc->engine) != watch->initial_state)
watch->notify (watch->dcdbc, watch->name, watch->user_data);
watch_unref (watch);
}
void
dconf_dbus_client_subscribe (DConfDBusClient *dcdbc,
const gchar *name,
DConfDBusNotify notify,
gpointer user_data)
{
DConfEngineMessage dcem;
Watch *watch;
watch = watch_new (dcdbc, name, notify, user_data);
dconf_engine_watch (dcdbc->engine, name, &dcem);
dconf_dbus_client_send (dcdbc, &dcem, add_match_done, watch);
dconf_engine_message_destroy (&dcem);
}
void
dconf_dbus_client_unsubscribe (DConfDBusClient *dcdbc,
DConfDBusNotify notify,
gpointer user_data)
{
DConfEngineMessage dcem;
GSList **ptr;
for (ptr = &dcdbc->watches; *ptr; ptr = &(*ptr)->next)
{
Watch *watch = (*ptr)->data;
if (watch->notify == notify && watch->user_data == user_data)
{
*ptr = g_slist_remove (*ptr, *ptr);
dconf_engine_unwatch (dcdbc->engine, watch->name, &dcem);
dconf_dbus_client_send (dcdbc, &dcem, NULL, NULL);
dconf_engine_message_destroy (&dcem);
watch_unref (watch);
return;
}
}
g_warning ("No matching watch found to unsubscribe");
}
gboolean
dconf_dbus_client_has_pending (DConfDBusClient *dcdbc)
{
return dcdbc->outstanding != NULL;
}
static GVariant *
dconf_dbus_client_service_func (DConfEngineMessage *dcem)
{
g_assert_not_reached ();
}
static DBusHandlerResult
dconf_dbus_client_filter (DBusConnection *connection,
DBusMessage *message,
void *user_data)
{
DConfDBusClient *dcdbc = user_data;
if (dbus_message_is_signal (message, "ca.desrt.dconf.Writer", "Notify") &&
dbus_message_has_signature (message, "sass"))
{
DBusMessageIter iter, sub;
const gchar *path, *tag;
dbus_message_iter_init (message, &iter);
dbus_message_iter_get_basic (&iter, &path);
dbus_message_iter_next (&iter);
dbus_message_iter_recurse (&iter, &sub);
dbus_message_iter_next (&iter);
dbus_message_iter_get_basic (&iter, &tag);
dbus_message_iter_next (&iter);
/* Only emit the event if it hasn't been anti-exposed */
if (dcdbc->anti_expose_tag == NULL ||
strcmp (tag, dcdbc->anti_expose_tag) != 0)
{
/* Empty list means that only one key changed */
if (!dbus_message_iter_get_arg_type (&sub))
dconf_dbus_emit_change (dcdbc, path);
while (dbus_message_iter_get_arg_type (&sub) == 's')
{
const gchar *item;
gchar *full;
dbus_message_iter_get_basic (&iter, &item);
full = g_strconcat (path, item, NULL);
dconf_dbus_emit_change (dcdbc, full);
g_free (full);
dbus_message_iter_next (&sub);
}
}
}
return DBUS_HANDLER_RESULT_NOT_YET_HANDLED;
}
DConfDBusClient *
dconf_dbus_client_new (const gchar *profile,
DBusConnection *session,
DBusConnection *system)
{
DConfDBusClient *dcdbc;
if (session == NULL)
session = dbus_bus_get (DBUS_BUS_SESSION, NULL);
if (system == NULL)
system = dbus_bus_get (DBUS_BUS_SYSTEM, NULL);
dconf_engine_set_service_func (dconf_dbus_client_service_func);
dcdbc = g_slice_new (DConfDBusClient);
dcdbc->engine = dconf_engine_new (profile);
dcdbc->system_bus = dbus_connection_ref (system);
dcdbc->session_bus = dbus_connection_ref (session);
dcdbc->anti_expose_tag = NULL;
dcdbc->outstanding = NULL;
dcdbc->watches = NULL;
dcdbc->ref_count = 1;
dbus_connection_add_filter (system, dconf_dbus_client_filter, dcdbc, NULL);
dbus_connection_add_filter (session, dconf_dbus_client_filter, dcdbc, NULL);
return dcdbc;
}
void
dconf_dbus_client_unref (DConfDBusClient *dcdbc)
{
if (--dcdbc->ref_count == 0)
{
dbus_connection_remove_filter (dcdbc->session_bus,
dconf_dbus_client_filter, dcdbc);
dbus_connection_remove_filter (dcdbc->system_bus,
dconf_dbus_client_filter, dcdbc);
dbus_connection_unref (dcdbc->session_bus);
dbus_connection_unref (dcdbc->system_bus);
g_slice_free (DConfDBusClient, dcdbc);
}
}
DConfDBusClient *
dconf_dbus_client_ref (DConfDBusClient *dcdbc)
{
dcdbc->ref_count++;
return dcdbc;
}