summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorXavier Claessens <xavier.claessens@collabora.com>2015-11-20 16:01:00 (GMT)
committerXavier Claessens <xavier.claessens@collabora.com>2015-12-22 15:58:53 (GMT)
commited11cbe8effda26a187f0a3fd3627488fe41915b (patch)
tree1da9d735467bbef3284ea0066c685056d7fbe8fd
parentb23bc550ac33477f6547e31fa29920868211ebdc (diff)
downloadappservice-master.tar.gz
appservice-master.tar.xz
Add GSettingsBackend implementationHEADmaster
-rw-r--r--.gitignore17
-rw-r--r--Makefile.am9
-rw-r--r--configure.ac15
-rw-r--r--src/Makefile.am97
-rw-r--r--src/application.c436
-rw-r--r--src/application.h18
-rw-r--r--src/component.c105
-rw-r--r--src/component.h52
-rw-r--r--src/interface.xml88
-rw-r--r--src/main.c61
-rw-r--r--src/org.freedesktop.Application.Service.service.in3
-rw-r--r--src/security.c88
-rw-r--r--src/security.h30
-rw-r--r--src/settings-backend.c661
-rw-r--r--src/settings-component.c485
-rw-r--r--src/util.c83
-rw-r--r--src/util.h36
-rw-r--r--src/watch-tree.c21
-rw-r--r--src/watch-tree.h11
-rw-r--r--tests/Makefile.am38
-rw-r--r--tests/appservice.conf3
-rw-r--r--tests/database.d/locks/00-locks1
-rw-r--r--tests/database.d/system-db5
-rw-r--r--tests/dconf-profile.in4
-rw-r--r--tests/schemas/Makefile.am11
-rw-r--r--tests/schemas/org.freedesktop.Application.Service.Test.gschema.xml32
-rw-r--r--tests/services/ca.desrt.dconf.service3
-rw-r--r--tests/services/org.freedesktop.Application.Service.service.in3
-rw-r--r--tests/settings.c212
29 files changed, 2085 insertions, 543 deletions
diff --git a/.gitignore b/.gitignore
index 23e6b09..ba7f165 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,6 +1,8 @@
-*.o
-*.so
+*.[oa]
+*.lo
+*.la
.deps
+.libs
Makefile
Makefile.in
@@ -9,9 +11,20 @@ Makefile.in
/aux
/config.*
/configure
+/libtool
/stamp-h1
/m4
/src/appservice
+/src/org.freedesktop.Application.Service.service
/src/resources.c
/src/resources.h
+
+/tests/*.log
+/tests/*.trs
+/tests/dconf-profile
+/tests/system-db
+/tests/schemas/gschemas.compiled
+/tests/schemas/org.freedesktop.Application.Service.Test.gschema.valid
+/tests/services/org.freedesktop.Application.Service.service
+/tests/test-settings
diff --git a/Makefile.am b/Makefile.am
index af437a6..f234d0d 100644
--- a/Makefile.am
+++ b/Makefile.am
@@ -1 +1,8 @@
-SUBDIRS = src
+SUBDIRS = src tests
+
+ACLOCAL_AMFLAGS = -I m4
+
+EXTRA_DIST = \
+ COPYING \
+ autogen.sh \
+ $(NULL)
diff --git a/configure.ac b/configure.ac
index df1242d..ae52d9c 100644
--- a/configure.ac
+++ b/configure.ac
@@ -7,19 +7,32 @@ AC_CONFIG_HEADERS([config.h])
AM_INIT_AUTOMAKE([1.11.2 foreign -Wno-portability no-dist-gzip dist-xz])
AM_SILENT_RULES([yes])
+LT_INIT
# Check for programs
AC_PROG_CC
PKG_PROG_PKG_CONFIG
GLIB_COMPILE_RESOURCES=`$PKG_CONFIG gio-2.0 --variable=glib_compile_resources`
AC_SUBST(GLIB_COMPILE_RESOURCES)
+GLIB_GSETTINGS
+AC_PATH_PROG([DCONF], [dconf])
+AC_PATH_PROG(gio_QUERYMODULES, gio-querymodules, no)
# Dependencies
PKG_CHECK_MODULES(gio, gio-2.0)
-PKG_CHECK_MODULES(dconf, dconf)
+PKG_CHECK_MODULES(dconf, dconf >= 0.25.1)
+
+AC_ARG_WITH(gio_modules_dir,
+ [ --with-gio-modules-dir=PATH choose directory for the GIO module, [default=LIBDIR/gio/modules]],
+ giomodulesdir="$withval", giomodulesdir=${libdir}/gio/modules)
+AC_SUBST(giomodulesdir)
AC_CONFIG_FILES([
src/Makefile
+ tests/Makefile
+ tests/dconf-profile
+ tests/schemas/Makefile
+ tests/services/org.freedesktop.Application.Service.service
Makefile
])
AC_OUTPUT
diff --git a/src/Makefile.am b/src/Makefile.am
index 0b274c0..7ced798 100644
--- a/src/Makefile.am
+++ b/src/Makefile.am
@@ -1,35 +1,80 @@
-bin_PROGRAMS = appservice
-
-AM_CFLAGS = -Wall -g $(gio_CFLAGS) $(dconf_CFLAGS)
-LDADD = $(gio_LIBS) $(dconf_LIBS)
+libexec_PROGRAMS = appservice
+appservice_CFLAGS = \
+ $(gio_CFLAGS) \
+ $(dconf_CFLAGS) \
+ -Wall -g \
+ $(NULL)
+appservice_LDADD = \
+ $(gio_LIBS) \
+ $(dconf_LIBS) \
+ $(NULL)
appservice_SOURCES = \
- watch-tree.h \
- watch-tree.c \
- settings-component.c \
- application.h \
- application.c \
- component.h \
- component.c \
- security.h \
- security.c \
- main.c \
- resources.h \
- resources.c \
- $(NULL)
-
-resources.h: Makefile.am $(shell $(GLIB_COMPILE_RESOURCES) --sourcedir=$(srcdir) --generate-dependencies $(srcdir)/resources.xml)
+ application.c application.h \
+ component.c component.h \
+ main.c \
+ security.c security.h \
+ settings-component.c \
+ util.c util.h \
+ watch-tree.c watch-tree.h \
+ $(NULL)
+nodist_appservice_SOURCES = \
+ resources.c resources.h \
+ $(NULL)
+
+resources_files: $(shell $(GLIB_COMPILE_RESOURCES) --generate-dependencies --sourcedir=$(srcdir) $(srcdir)/resources.xml)
+resources.h: Makefile resources.xml $(resources_files)
$(AM_V_GEN) $(GLIB_COMPILE_RESOURCES) $(srcdir)/resources.xml \
--target=$@ --sourcedir=$(srcdir) --generate-header
-
-resources.c: Makefile.am $(shell $(GLIB_COMPILE_RESOURCES) --sourcedir=$(srcdir) --generate-dependencies $(srcdir)/resources.xml)
+resources.c: Makefile resources.xml $(resources_files)
$(AM_V_GEN) $(GLIB_COMPILE_RESOURCES) $(srcdir)/resources.xml \
--target=$@ --sourcedir=$(srcdir) --generate-source
-BUILT_SOURCES = \
- resources.h \
- resources.c \
- $(NULL)
+servicefiledir = $(datadir)/dbus-1/services
+servicefile_DATA = org.freedesktop.Application.Service.service
+org.freedesktop.Application.Service.service: org.freedesktop.Application.Service.service.in
+ $(AM_V_GEN) sed -e "s|[@]libexecdir[@]|$(libexecdir)|" $< > $@
+
+giomodules_LTLIBRARIES = libappservicesettings.la
+
+libappservicesettings_la_LIBADD = \
+ $(gio_LIBS) \
+ $(dconf_LIBS) \
+ $(NULL)
+libappservicesettings_la_CFLAGS = \
+ $(gio_CFLAGS) \
+ $(dconf_CFLAGS) \
+ -Wall -g -fPIC -DPIC \
+ $(NULL)
+libappservicesettings_la_LDFLAGS = \
+ -export_dynamic \
+ -avoid-version \
+ -module \
+ -no-undefined \
+ -export-symbols-regex "^g_io_module_(load|unload|query)" \
+ $(NULL)
+libappservicesettings_la_SOURCES = \
+ settings-backend.c \
+ util.c util.h \
+ $(NULL)
+
+uninstall-hook:
+ if test -z "$(DESTDIR)" -a "$(gio_QUERYMODULES)" != "no" ; then \
+ $(gio_QUERYMODULES) $(giomodulesdir) ; \
+ fi
+
+install-data-hook:
+ if test -z "$(DESTDIR)" -a "$(gio_QUERYMODULES)" != "no" ; then \
+ $(gio_QUERYMODULES) $(giomodulesdir) ; \
+ fi
+
+EXTRA_DIST = \
+ resources.xml \
+ interface.xml \
+ org.freedesktop.Application.Service.service.in \
+ $(NULL)
CLEANFILES = \
- $(BUILT_SOURCES)
+ $(nodist_appservice_SOURCES) \
+ $(servicefile_DATA) \
+ $(NULL)
diff --git a/src/application.c b/src/application.c
index 50b264f..65c0f21 100644
--- a/src/application.c
+++ b/src/application.c
@@ -1,5 +1,6 @@
/*
* Copyright © 2015 Canonical Limited
+ * Copyright © 2015 Collabora ltd.
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
@@ -14,150 +15,65 @@
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, see <http://www.gnu.org/licenses/>.
*
- * Author: Ryan Lortie <desrt@desrt.ca>
+ * Authors: Xavier Claessens <xavier.claessens@collabora.com>
+ * Allison Ryan Lortie <desrt@desrt.ca>
*/
+#include "config.h"
+
#include "application.h"
#include "component.h"
+#include "security.h"
+#include "util.h"
-static GType (* component_types[]) (void) = {
- settings_component_get_type
+typedef struct
+{
+ const gchar *interface;
+ GType (*get_type) (void);
+ gboolean (*is_confined) (const gchar *label);
+} ComponentData;
+
+static ComponentData component_datas[] = {
+ { SETTINGS_INTERFACE_NAME,
+ settings_component_get_type,
+ security_dconf_is_confined },
};
-#define N_COMPONENT_TYPES G_N_ELEMENTS (component_types)
+#define N_COMPONENTS G_N_ELEMENTS (component_datas)
+/* Borrowed unique name -> owned Application */
static GHashTable *applications;
struct _Application
{
GObject parent_instance;
- GDBusConnection *connection;
- gchar *app_id;
- guint watch_id;
- gchar *name_owner;
- gchar *security_context;
- GQueue invocations; /* outstanding start requests */
+ GDBusConnection *connection;
+ gchar *unique_name;
+ guint watch_id;
+ gchar *security_context;
+ Component *components[N_COMPONENTS];
+ GCancellable *cancellable;
+
GDBusMethodInvocation *start_invocation;
- GCancellable *cancellable;
- Component *components[N_COMPONENT_TYPES];
- gboolean running;
};
-typedef GObjectClass ApplicationClass;
-
G_DEFINE_TYPE (Application, application, G_TYPE_OBJECT)
-static GVariant *
-new_ay (GVariant *value)
-{
- GBytes *bytes;
- GVariant *ay;
- GVariant *v;
-
- v = g_variant_new_variant (value);
- bytes = g_variant_get_data_as_bytes (v);
- g_variant_unref (g_variant_ref_sink (v));
-
- ay = g_variant_new_from_bytes (G_VARIANT_TYPE_BYTESTRING, bytes, TRUE);
- g_bytes_unref (bytes);
-
- return ay;
-}
-
-static void
-application_send_error_to_invocations (Application *application)
-{
- GDBusMethodInvocation *invocation;
-
- while ((invocation = g_queue_pop_head (&application->invocations)))
- {
- /* If the invocation came from the name owner... */
- if (g_strcmp0 (application->name_owner, g_dbus_method_invocation_get_sender (invocation)) == 0)
- {
- g_dbus_method_invocation_return_error (invocation, G_DBUS_ERROR, G_DBUS_ERROR_FAILED,
- "The application has already been registered");
- }
- else
- {
- /* It is important that we handle all cases of registration
- * from an improper owner with the same error -- otherwise
- * applications could use this interface to probe if another
- * application is running.
- */
- g_dbus_method_invocation_return_error (invocation, G_DBUS_ERROR, G_DBUS_ERROR_ACCESS_DENIED,
- "You are not the owner of the application ID that you claim to be");
- }
-
- g_object_unref (invocation);
- }
-}
-
-static void
-application_startup (Application *application)
-{
- GDBusMethodInvocation *invocation;
- GVariantDict *arguments;
- GVariantBuilder result;
- guint i;
-
- g_assert (application->start_invocation != NULL);
- invocation = application->start_invocation;
-
- {
- GVariant *dict;
-
- dict = g_variant_get_child_value (g_dbus_method_invocation_get_parameters (invocation), 1);
- arguments = g_variant_dict_new (dict);
- g_variant_unref (dict);
- }
-
- g_variant_builder_init (&result, G_VARIANT_TYPE ("a{say}"));
-
- for (i = 0; i < N_COMPONENT_TYPES; i++)
- {
- const gchar *component_id;
- GVariant *reply;
- GVariant *args;
-
- application->components[i] = g_object_new (component_types[i] (), "application", application, NULL);
- component_id = component_get_id (application->components[i]);
-
- args = g_variant_dict_lookup_value (arguments, component_id, NULL);
- reply = component_startup (application->components[i], args);
- if (args)
- g_variant_unref (args);
-
- g_variant_builder_add (&result, "{s@ay}", component_id, new_ay (reply));
- g_variant_unref (reply);
- }
-
- g_dbus_method_invocation_return_value (invocation, g_variant_new ("(a{say})", &result));
- g_clear_object (&application->start_invocation);
-
- application->running = TRUE;
-}
-
static void
application_shutdown (Application *application)
{
- if (application->running)
- {
- gint i;
+ gint i;
- for (i = 0; i < N_COMPONENT_TYPES; i++)
- {
- component_shutdown (application->components[i]);
- g_clear_object (&application->components[i]);
- }
- }
+ for (i = 0; i < N_COMPONENTS; i++)
+ if (application->components[i] != NULL)
+ component_shutdown (application->components[i]);
g_cancellable_cancel (application->cancellable);
g_bus_unwatch_name (application->watch_id);
application->watch_id = 0;
- g_hash_table_remove (applications, application->app_id);
-
+ g_hash_table_remove (applications, application->unique_name);
g_object_unref (application);
}
@@ -166,40 +82,57 @@ get_connection_credentials_cb (GObject *source,
GAsyncResult *result,
gpointer user_data)
{
- Application *self = user_data;
- GVariant *reply;
- GVariant *child;
+ Application *application = user_data;
+ g_autoptr(GVariant) reply = NULL;
+ g_autoptr(GVariant) child = NULL;
+ GVariantBuilder builder;
+ guint i;
+ gboolean has_components = FALSE;
GError *error = NULL;
- reply = g_dbus_connection_call_finish (self->connection, result, &error);
+ reply = g_dbus_connection_call_finish (application->connection, result, &error);
if (reply == NULL)
{
- g_dbus_method_invocation_take_error (self->start_invocation, error);
- g_clear_object (&self->start_invocation);
+ g_dbus_method_invocation_take_error (application->start_invocation, error);
+ application_shutdown (application);
goto out;
}
child = g_variant_get_child_value (reply, 0);
- g_variant_lookup (child, "LinuxSecurityLabel", "^ay",
- &self->security_context);
- g_variant_unref (reply);
- g_variant_unref (child);
+ if (!g_variant_lookup (child, "LinuxSecurityLabel", "^ay",
+ &application->security_context))
+ {
+ g_dbus_method_invocation_return_error_literal (application->start_invocation,
+ G_DBUS_ERROR, G_DBUS_ERROR_FAILED,
+ "GetConnectionCredentials did not provide a LinuxSecurityLabel");
+ application_shutdown (application);
+ goto out;
+ }
- /* Application is now ready */
- application_startup (self);
+ g_variant_builder_init (&builder, G_VARIANT_TYPE ("as"));
+ for (i = 0; i < N_COMPONENTS; i++)
+ {
+ if (!component_datas[i].is_confined (application->security_context))
+ continue;
+
+ has_components = TRUE;
+ g_variant_builder_add (&builder, "s", component_datas[i].interface);
+ application->components[i] = g_object_new (component_datas[i].get_type (),
+ "application", application,
+ NULL);
+ }
-out:
- g_object_unref (self);
-}
+ /* If no component is confined, it's not an error but we don't need to keep
+ * track of this app. */
+ if (!has_components)
+ application_shutdown (application);
-static gint
-find_invocation_by_sender (gconstpointer a,
- gconstpointer b)
-{
- GDBusMethodInvocation *invocation = (gpointer) a;
- const gchar *sender = b;
+ g_dbus_method_invocation_return_value (application->start_invocation,
+ g_variant_new ("(as)", &builder));
- return g_strcmp0 (g_dbus_method_invocation_get_sender (invocation), sender);
+out:
+ g_clear_object (&application->start_invocation);
+ g_object_unref (application);
}
static void
@@ -209,45 +142,17 @@ application_appeared (GDBusConnection *connection,
gpointer user_data)
{
Application *application = user_data;
- GList *owner_node;
-
- g_assert (!application->name_owner);
- application->name_owner = g_strdup (name_owner);
-
- owner_node = g_queue_find_custom (&application->invocations, name_owner, find_invocation_by_sender);
- if (owner_node)
- {
- GDBusMethodInvocation *owner_invocation = owner_node->data;
-
- /* We have an invocation from the owner of the name. Let's get his
- * security credentials now.
- */
- application->connection = g_object_ref (connection);
- application->start_invocation = owner_invocation;
- g_queue_delete_link (&application->invocations, owner_node);
-
- g_dbus_connection_call (connection,
- "org.freedesktop.DBus", "/", "org.freedesktop.DBus",
- "GetConnectionCredentials",
- g_variant_new ("(s)", application->app_id),
- G_VARIANT_TYPE ("(a{sv})"),
- G_DBUS_CALL_FLAGS_NONE,
- -1,
- application->cancellable,
- get_connection_credentials_cb,
- g_object_ref (application));
- }
- /* If there are any other invocations, make sure they get error
- * messages sent to them.
- */
- application_send_error_to_invocations (application);
-
- if (!owner_node)
- /* There is no owner, so we need to make sure we tear down this
- * application object.
- */
- application_shutdown (application);
+ g_dbus_connection_call (connection,
+ "org.freedesktop.DBus", "/", "org.freedesktop.DBus",
+ "GetConnectionCredentials",
+ g_variant_new ("(s)", application->unique_name),
+ G_VARIANT_TYPE ("(a{sv})"),
+ G_DBUS_CALL_FLAGS_NONE,
+ -1,
+ application->cancellable,
+ get_connection_credentials_cb,
+ g_object_ref (application));
}
static void
@@ -257,15 +162,6 @@ application_vanished (GDBusConnection *connection,
{
Application *application = user_data;
- g_assert (application->name_owner);
- g_clear_pointer (&application->name_owner, g_free);
-
- /* Either the application has quit, or it was never running.
- *
- * Send an error to any outstanding invocations and shut the app down
- * (removing it from the global hashtable and freeing it).
- */
- application_send_error_to_invocations (application);
application_shutdown (application);
}
@@ -273,16 +169,19 @@ static void
application_finalize (GObject *object)
{
Application *application = (Application *) object;
+ gint i;
- g_assert (g_queue_is_empty (&application->invocations));
- g_assert (!application->name_owner);
- g_assert (!application->watch_id);
+ g_assert (!g_hash_table_contains (applications, application->unique_name));
+ g_assert (application->start_invocation == NULL);
- g_message ("Application %s exits", application->app_id);
- g_free (application->app_id);
+ g_free (application->unique_name);
g_free (application->security_context);
+ g_object_unref (application->connection);
g_object_unref (application->cancellable);
+ for (i = 0; i < N_COMPONENTS; i++)
+ g_clear_object (&application->components[i]);
+
G_OBJECT_CLASS (application_parent_class)->finalize (object);
}
@@ -295,69 +194,136 @@ application_init (Application *application)
static void
application_class_init (ApplicationClass *class)
{
- class->finalize = application_finalize;
+ GObjectClass *object_class = G_OBJECT_CLASS (class);
+
+ object_class->finalize = application_finalize;
+}
+
+GDBusConnection *
+application_get_connection (Application *self)
+{
+ return self->connection;
}
-void
-application_start (GDBusMethodInvocation *invocation)
+const gchar *
+application_get_unique_name (Application *self)
{
- Application *application;
- const gchar *app_id;
+ return self->unique_name;
+}
- g_variant_get_child (g_dbus_method_invocation_get_parameters (invocation), 0, "&s", &app_id);
+const gchar *
+application_get_security_context (Application *self)
+{
+ return self->security_context;
+}
- if (!applications)
+static void
+application_method_call (GDBusConnection *connection,
+ const gchar *sender,
+ const gchar *object_path,
+ const gchar *interface_name,
+ const gchar *method_name,
+ GVariant *parameters,
+ GDBusMethodInvocation *invocation,
+ gpointer user_data)
+{
+ Application *application;
+ guint i;
+
+ if (applications == NULL)
applications = g_hash_table_new (g_str_hash, g_str_equal);
- /* This is the initial request from the application, but we may be in
- * a situation where _another_ app requested the name (and didn't have
- * the right to it). Our check for the name's owner may still be
- * underway.
- *
- * We need to reply to both messages -- one in the affirmative -- once
- * we have discovered the owner. We queue up both invocations on the
- * object until that time.
- */
- application = g_hash_table_lookup (applications, app_id);
+ application = g_hash_table_lookup (applications, sender);
- if (application == NULL)
+ if (g_str_equal (interface_name, INTERFACE_NAME))
{
+ g_assert (g_str_equal (method_name, "Start"));
+
+ if (application != NULL)
+ {
+ g_dbus_method_invocation_return_error_literal (invocation,
+ G_DBUS_ERROR, G_DBUS_ERROR_FAILED,
+ "Application already called 'Start' method");
+ return;
+ }
+
application = g_object_new (TYPE_APPLICATION, NULL);
- application->app_id = g_strdup (app_id);
- application->watch_id = g_bus_watch_name_on_connection (g_dbus_method_invocation_get_connection (invocation),
- application->app_id, G_BUS_NAME_WATCHER_FLAGS_NONE,
- application_appeared, application_vanished,
- application, NULL);
- g_hash_table_insert (applications, application->app_id, application); /* transfer ref */
+ application->connection = g_object_ref (connection);
+ application->unique_name = g_strdup (sender);
+ application->start_invocation = g_object_ref (invocation);
+ application->watch_id = g_bus_watch_name_on_connection (connection,
+ application->unique_name, G_BUS_NAME_WATCHER_FLAGS_NONE,
+ application_appeared, application_vanished,
+ application, NULL);
+
+ /* transfer ownership */
+ g_hash_table_insert (applications, application->unique_name, application);
+
+ return;
}
- g_queue_push_tail (&application->invocations, g_object_ref (invocation));
+ if (application == NULL)
+ {
+ g_dbus_method_invocation_return_error_literal (invocation,
+ G_DBUS_ERROR, G_DBUS_ERROR_FAILED,
+ "Application must register itself first using 'Start' method");
+ return;
+ }
- /* It may be the case that the owner is already known -- in that case
- * we can reply immediately (and always with an error of some kind).
- */
- if (application->name_owner)
- application_send_error_to_invocations (application);
+ if (application->start_invocation != NULL)
+ {
+ g_dbus_method_invocation_return_error_literal (invocation,
+ G_DBUS_ERROR, G_DBUS_ERROR_FAILED,
+ "Application must wait for 'Start' method to return before doing "
+ "anything else");
+ return;
+ }
+
+ for (i = 0; i < N_COMPONENTS; i++)
+ {
+ if (g_str_equal (interface_name, component_datas[i].interface))
+ {
+ if (application->components[i] == NULL)
+ {
+ g_dbus_method_invocation_return_error_literal (invocation,
+ G_DBUS_ERROR, G_DBUS_ERROR_FAILED,
+ "Application shouldn't use unconfined component");
+ }
+ else
+ {
+ component_method_call (application->components[i],
+ connection, method_name, parameters, invocation);
+ }
+
+ return;
+ }
+ }
+
+ g_assert_not_reached ();
}
-const gchar *
-application_get_security_context (Application *application)
+static void
+register_interface (GDBusConnection *connection,
+ GDBusNodeInfo *info,
+ const gchar *interface)
{
- return application->security_context;
+ GDBusInterfaceVTable vtable = { application_method_call, NULL, NULL };
+ GDBusInterfaceInfo *iface;
+ GError *error = NULL;
+
+ iface = g_dbus_node_info_lookup_interface (info, interface);
+ g_dbus_connection_register_object (connection, OBJECT_PATH, iface, &vtable,
+ NULL, NULL, &error);
+ g_assert_no_error (error);
}
void
-application_update (Application *application,
- const gchar *component_id,
- GVariant *arguments)
+application_register (GDBusConnection *connection,
+ GDBusNodeInfo *info)
{
- g_dbus_connection_emit_signal (application->connection,
- application->name_owner,
- "/org/freedesktop/Application/Service",
- "org.freedesktop.Application.Service",
- "Update",
- g_variant_new ("(s@ay)",
- component_id,
- new_ay (arguments)),
- NULL);
+ guint i;
+
+ register_interface (connection, info, INTERFACE_NAME);
+ for (i = 0; i < N_COMPONENTS; i++)
+ register_interface (connection, info, component_datas[i].interface);
}
diff --git a/src/application.h b/src/application.h
index 54b0bb2..70fdbfc 100644
--- a/src/application.h
+++ b/src/application.h
@@ -22,18 +22,14 @@
#include <gio/gio.h>
-#define TYPE_APPLICATION (application_get_type ())
-#define APPLICATION(inst) (TYPE_CHECK_INSTANCE_CAST ((inst), TYPE_APPLICATION, Application))
-#define APPLICATION_CLASS(class) (TYPE_CHECK_CLASS_CAST ((class), TYPE_APPLICATION, ApplicationClass))
-#define IS_APPLICATION(inst) (TYPE_CHECK_INSTANCE_TYPE ((inst), TYPE_APPLICATION))
-#define IS_APPLICATION_CLASS(class) (TYPE_CHECK_CLASS_TYPE ((class), TYPE_APPLICATION))
-#define APPLICATION_GET_CLASS(inst) (TYPE_INSTANCE_GET_CLASS ((inst), TYPE_APPLICATION, ApplicationClass))
+#define TYPE_APPLICATION (application_get_type ())
+G_DECLARE_FINAL_TYPE (Application, application,, APPLICATION, GObject)
-typedef struct _Application Application;
+void application_register (GDBusConnection *connection,
+ GDBusNodeInfo *info);
-GType application_get_type (void);
-void application_start (GDBusMethodInvocation *invocation);
-const gchar * application_get_security_context (Application *application);
-void application_update (Application *application, const gchar *component_id, GVariant *arguments);
+GDBusConnection *application_get_connection (Application *self);
+const gchar *application_get_unique_name (Application *self);
+const gchar *application_get_security_context (Application *self);
#endif /* _application_h_ */
diff --git a/src/component.c b/src/component.c
index 96516c7..c1a5416 100644
--- a/src/component.c
+++ b/src/component.c
@@ -17,63 +17,64 @@
* Author: Ryan Lortie <desrt@desrt.ca>
*/
-#include "component.h"
-
-G_DEFINE_TYPE (Component, component, G_TYPE_OBJECT)
+#include "config.h"
-const gchar *
-component_get_id (Component *component)
-{
- return COMPONENT_GET_CLASS (component)->get_id (component);
-}
+#include "component.h"
-GVariant *
-component_startup (Component *component,
- GVariant *argument)
+typedef struct
{
- return g_variant_take_ref (COMPONENT_GET_CLASS (component)->startup (component, argument));
-}
+ Application *application;
+} ComponentPrivate;
-void
-component_shutdown (Component *component)
+enum
{
- COMPONENT_GET_CLASS (component)->shutdown (component);
+ PROP_0,
+ PROP_APPLICATION,
+ N_PROPS
+};
- g_clear_object (&component->application);
-}
+static GParamSpec *properties[N_PROPS] = { NULL, };
-void
-component_emit (Component *component,
- GVariant *arguments)
-{
- application_update (component->application, component_get_id (component), arguments);
-}
+G_DEFINE_TYPE_WITH_PRIVATE (Component, component, G_TYPE_OBJECT)
+#define priv ((ComponentPrivate *) component_get_instance_private (self))
static void
component_set_property (GObject *object, guint prop_id,
const GValue *value, GParamSpec *pspec)
{
- Component *component = COMPONENT (object);
-
- g_assert (prop_id == 1);
- g_assert (!component->application);
-
- component->application = g_value_dup_object (value);
+ Component *self = _COMPONENT (object);
+
+ switch (prop_id)
+ {
+ case PROP_APPLICATION:
+ g_assert (priv->application == NULL);
+ priv->application = g_value_dup_object (value);
+ break;
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+ break;
+ }
}
static void
component_get_property (GObject *object, guint prop_id,
GValue *value, GParamSpec *pspec)
{
- Component *component = COMPONENT (object);
-
- g_assert (prop_id == 1);
-
- g_value_set_object (value, component->application);
+ Component *self = _COMPONENT (object);
+
+ switch (prop_id)
+ {
+ case PROP_APPLICATION:
+ g_value_set_object (value, priv->application);
+ break;
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+ break;
+ }
}
static void
-component_init (Component *component)
+component_init (Component *self)
{
}
@@ -85,7 +86,33 @@ component_class_init (ComponentClass *class)
object_class->set_property = component_set_property;
object_class->get_property = component_get_property;
- g_object_class_install_property (object_class, 1,
- g_param_spec_object ("application", "", "", TYPE_APPLICATION,
- G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY));
+ properties[PROP_APPLICATION] = g_param_spec_object ("application", "", "",
+ TYPE_APPLICATION, G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY);
+
+ g_object_class_install_properties (object_class,
+ G_N_ELEMENTS (properties), properties);
+}
+
+void
+component_shutdown (Component *self)
+{
+ _COMPONENT_GET_CLASS (self)->shutdown (self);
+ g_clear_object (&priv->application);
+}
+
+void
+component_method_call (Component *self,
+ GDBusConnection *connection,
+ const gchar *method_name,
+ GVariant *parameters,
+ GDBusMethodInvocation *invocation)
+{
+ _COMPONENT_GET_CLASS (self)->method_call (self, connection, method_name,
+ parameters, invocation);
+}
+
+Application *
+component_get_application (Component *self)
+{
+ return priv->application;
}
diff --git a/src/component.h b/src/component.h
index 9f43087..ba3689a 100644
--- a/src/component.h
+++ b/src/component.h
@@ -24,46 +24,32 @@
#include "application.h"
-#define TYPE_COMPONENT (component_get_type ())
-#define COMPONENT(inst) (G_TYPE_CHECK_INSTANCE_CAST ((inst), TYPE_COMPONENT, Component))
-#define COMPONENT_CLASS(class) (G_TYPE_CHECK_CLASS_CAST ((class), TYPE_COMPONENT, ComponentClass))
-#define IS_COMPONENT(inst) (G_TYPE_CHECK_INSTANCE_TYPE ((inst), TYPE_COMPONENT))
-#define IS_COMPONENT_CLASS(class) (G_TYPE_CHECK_CLASS_TYPE ((class), TYPE_COMPONENT))
-#define COMPONENT_GET_CLASS(inst) (G_TYPE_INSTANCE_GET_CLASS ((inst), TYPE_COMPONENT, ComponentClass))
+#define TYPE_COMPONENT (component_get_type ())
+G_DECLARE_DERIVABLE_TYPE (Component, component,, COMPONENT, GObject)
-typedef struct
-{
- GObject parent_instance;
-
- Application *application;
-} Component;
-
-typedef struct
+struct _ComponentClass
{
GObjectClass parent_class;
- const GVariantType * (* get_request_type) (Component *component);
- void (* request) (Component *component,
- GVariant *request,
- GDBusMethodInvocation *invocation);
- const gchar * (* get_id) (Component *component);
- void (* shutdown) (Component *component);
- GVariant * (* startup) (Component *component,
- GVariant *arguments);
-} ComponentClass;
-
-GType component_get_type (void);
-
-const gchar * component_get_id (Component *component);
+ void (*method_call) (Component *self,
+ GDBusConnection *connection,
+ const gchar *method_name,
+ GVariant *parameters,
+ GDBusMethodInvocation *invocation);
+ void (*shutdown) (Component *self);
+};
-GVariant * component_startup (Component *component,
- GVariant *arguments);
+void component_method_call (Component *self,
+ GDBusConnection *connection,
+ const gchar *method_name,
+ GVariant *parameters,
+ GDBusMethodInvocation *invocation);
-void component_shutdown (Component *component);
+void component_shutdown (Component *self);
-void component_emit (Component *component,
- GVariant *arguments);
+Application *component_get_application (Component *self);
-GType settings_component_get_type (void);
+/* subclasses */
+GType settings_component_get_type (void);
#endif /* _component_h_ */
diff --git a/src/interface.xml b/src/interface.xml
index c23253c..e743426 100644
--- a/src/interface.xml
+++ b/src/interface.xml
@@ -1,16 +1,94 @@
<node>
+ <!-- org.freedesktop.Application.Service:
+ @short_description: DBus service for confined/contained apps
+ @since: UNRELEASED
+
+ This D-Bus interface is used by confined/contained apps to access
+ restricted system components.
+ -->
<interface name="org.freedesktop.Application.Service">
+ <!--
+ Start:
+ @interfaces: A set of interfaces that the application can use to access
+ restricted resources.
+ @since: UNRELEASED
+ Method used by an application at startup to register itself with the
+ Application Service.
+ -->
<method name="Start">
- <arg direction="in" type="s" name="app_id"/>
- <arg direction="in" type="a{sv}" name="args"/>
- <arg direction="out" type="a{say}" name="reply"/>
+ <arg direction="out" type="as" name="interfaces"/>
+ </method>
+ </interface>
+
+ <!-- org.freedesktop.Application.Service.Settings:
+ @short_description: Interface proxying DConf for confined applications
+ @since: UNRELEASED
+
+ This D-Bus interface is used by applications having restricted access to
+ DConf. It is used when the application is only permitted to read or write
+ a subset of all user settings.
+ -->
+ <interface name="org.freedesktop.Application.Service.Settings">
+ <!--
+ GetAll:
+ @user_database: A serialised #GSettingsBackendChangeset with the
+ user value of each readable setting.
+ @default_database: A serialised #GSettingsBackendChangeset with the
+ default value of each readable setting.
+ @writable: A set of writable setting paths.
+ @locks: A set of locked setting paths.
+ @since: UNRELEASED
+
+ Get the current state of all readable setting for this application.
+ -->
+ <method name="GetAll">
+ <arg direction="out" type="v" name="user_database"/>
+ <arg direction="out" type="v" name="default_database"/>
+ <arg direction="out" type="as" name="writable"/>
+ <arg direction="out" type="as" name="locks"/>
</method>
+ <!--
+ Write:
+ @changeset: A serialised #GSettingsBackendChangeset.
+ @since: UNRELEASED
+ Change the value of a set of writable settings.
+ -->
+ <method name="Write">
+ <arg direction="in" type="v" name="changeset"/>
+ </method>
+ <!--
+ Sync:
+ @since: UNRELEASED
+
+ Ensure all changes gets written on disk.
+ -->
+ <method name="Sync"/>
+ <!--
+ Update:
+ @user_changeset: A serialised #GSettingsBackendChangeset with the
+ new user value of each readable setting.
+ @default_changeset: A serialised #GSettingsBackendChangeset with the
+ new default value of each readable setting.
+ @since: UNRELEASED
+
+ Update notification.
+ -->
<signal name="Update">
- <arg name="aspect" type="s"/>
- <arg name="update" type="v"/>
+ <arg type="v" name="user_changeset"/>
+ <arg type="v" name="default_changeset"/>
</signal>
+ <!--
+ WritabilityUpdate:
+ @locks: The new set of locked setting paths
+ @since: UNRELEASED
+ Signal emitted when the set of locked keys changes.
+ -->
+ <signal name="WritabilityUpdate">
+ <arg type="as" name="locks"/>
+ </signal>
</interface>
+
</node>
diff --git a/src/main.c b/src/main.c
index 9cda05b..ed43cf5 100644
--- a/src/main.c
+++ b/src/main.c
@@ -1,5 +1,6 @@
/*
* Copyright © 2015 Canonical Limited
+ * Copyright © 2015 Collabora ltd.
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
@@ -14,39 +15,26 @@
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, see <http://www.gnu.org/licenses/>.
*
- * Author: Ryan Lortie <desrt@desrt.ca>
+ * Authors: Xavier Claessens <xavier.claessens@collabora.com>
+ * Allison Ryan Lortie <desrt@desrt.ca>
*/
+#include "config.h"
+
#include <stdlib.h>
#include "application.h"
+#include "util.h"
-static void
-method_call (GDBusConnection *connection,
- const gchar *sender,
- const gchar *object_path,
- const gchar *interface_name,
- const gchar *method_name,
- GVariant *parameters,
- GDBusMethodInvocation *invocation,
- gpointer user_data)
-{
- g_assert_cmpstr (method_name, ==, "Start");
-
- application_start (invocation);
-}
+static GMainLoop *loop = NULL;
static void
bus_acquired (GDBusConnection *connection,
const gchar *name,
gpointer user_data)
{
- static const GDBusInterfaceVTable vtable = {
- .method_call = method_call
- };
- GBytes *bytes;
- GDBusNodeInfo *info;
- GDBusInterfaceInfo *iface;
+ g_autoptr(GBytes) bytes;
+ g_autoptr(GDBusNodeInfo) info;
GError *error = NULL;
bytes = g_resources_lookup_data (
@@ -58,16 +46,7 @@ bus_acquired (GDBusConnection *connection,
info = g_dbus_node_info_new_for_xml (g_bytes_get_data (bytes, NULL), &error);
g_assert_no_error (error);
- iface = g_dbus_node_info_lookup_interface (info,
- "org.freedesktop.Application.Service");
- g_assert_nonnull (iface);
-
- g_dbus_connection_register_object (connection,
- "/org/freedesktop/Application/Service",
- iface, &vtable, NULL, NULL, NULL);
-
- g_dbus_node_info_unref (info);
- g_bytes_unref (bytes);
+ application_register (connection, info);
}
static void
@@ -82,8 +61,15 @@ name_lost (GDBusConnection *connection,
const gchar *name,
gpointer user_data)
{
- g_warning ("The application service is already running ('org.freedesktop.Application.Service' name is owned");
- exit (1);
+ /* Unit tests are run on a temporary DBus session that gets closed at the end.
+ * Do a clean exit in that case. */
+ if (connection != NULL)
+ {
+ g_warning ("The application service is already running, '"BUS_NAME"' name is owned");
+ exit (1);
+ }
+
+ g_main_loop_quit (loop);
}
int
@@ -91,11 +77,14 @@ main (int argc,
char **argv)
{
g_bus_own_name (G_BUS_TYPE_SESSION,
- "org.freedesktop.Application.Service",
+ BUS_NAME,
G_BUS_NAME_OWNER_FLAGS_NONE,
bus_acquired, name_acquired, name_lost,
NULL, NULL);
- while (TRUE)
- g_main_context_iteration (NULL, TRUE);
+ loop = g_main_loop_new (NULL, FALSE);
+ g_main_loop_run (loop);
+ g_main_loop_unref (loop);
+
+ return 0;
}
diff --git a/src/org.freedesktop.Application.Service.service.in b/src/org.freedesktop.Application.Service.service.in
new file mode 100644
index 0000000..1badee6
--- /dev/null
+++ b/src/org.freedesktop.Application.Service.service.in
@@ -0,0 +1,3 @@
+[D-BUS Service]
+Name=org.freedesktop.Application.Service
+Exec=@libexecdir@/appservice
diff --git a/src/security.c b/src/security.c
index 8d0daba..3c2bfaf 100644
--- a/src/security.c
+++ b/src/security.c
@@ -1,12 +1,90 @@
+/*
+ * Copyright © 2015 Canonical Limited
+ * Copyright © 2015 Collabora ltd.
+ *
+ * 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 <http://www.gnu.org/licenses/>.
+ *
+ * Authors: Xavier Claessens <xavier.claessens@collabora.com>
+ * Allison Ryan Lortie <desrt@desrt.ca>
+ */
+
+#include "config.h"
+
#include "security.h"
+#include "util.h"
+
+static GKeyFile *keyfile = NULL;
+
+static void
+ensure_keyfile (void)
+{
+ if (keyfile == NULL)
+ {
+ const gchar *path;
+
+ keyfile = g_key_file_new ();
+
+ path = g_getenv ("APPSERVICE_CONF_PATH");
+ if (path == NULL)
+ path = "/etc/appservice.conf";
+
+ g_key_file_load_from_file (keyfile, path, G_KEY_FILE_NONE, NULL);
+ }
+}
+
+static gchar **
+get_string_list (const gchar *group,
+ const gchar *key)
+{
+ gchar **out;
+
+ out = g_key_file_get_string_list (keyfile, group, key, NULL, NULL);
+
+ return out ? out : g_new0 (gchar *, 1);
+}
+
+gboolean
+security_dconf_is_confined (const gchar *label)
+{
+ return security_dconf_get_paths (label, NULL, NULL);
+}
gboolean
-security_module_get_dconf_paths (const gchar *id,
- gchar ***readable,
- gchar ***readwritable)
+security_dconf_get_paths (const gchar *label,
+ gchar ***readable,
+ gchar ***writable)
{
- *readable = g_strsplit ("/ca/desrt/dconf-editor/", ";", -1);
- *readwritable = g_strsplit ("/ca/desrt/dconf-editor/", ";", -1);
+ ensure_keyfile ();
+
+ /* Empty label means unconfined */
+ if (g_str_equal (label, ""))
+ return FALSE;
+
+ /* We could still have configs for unconfined apps. That's for example the
+ * case of unit tests. */
+ if (g_str_equal (label, "unconfined") &&
+ !g_key_file_has_group (keyfile, label))
+ return FALSE;
+
+ if (g_key_file_get_boolean (keyfile, label, "dconf-unconfined", NULL))
+ return FALSE;
+
+ if (readable)
+ *readable = get_string_list (label, "dconf-readable");
+
+ if (writable)
+ *writable = get_string_list (label, "dconf-writable");
return TRUE;
}
diff --git a/src/security.h b/src/security.h
index 94df155..1af47ca 100644
--- a/src/security.h
+++ b/src/security.h
@@ -1,7 +1,27 @@
-#include <gio/gio.h>
-
-gboolean security_module_get_dconf_paths (const gchar *id,
- gchar ***readable,
- gchar ***readwritable);
+/*
+ * Copyright © 2015 Canonical Limited
+ * Copyright © 2015 Collabora ltd.
+ *
+ * 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 <http://www.gnu.org/licenses/>.
+ *
+ * Authors: Xavier Claessens <xavier.claessens@collabora.com>
+ * Allison Ryan Lortie <desrt@desrt.ca>
+ */
+#include <gio/gio.h>
+gboolean security_dconf_is_confined (const gchar *label);
+gboolean security_dconf_get_paths (const gchar *label,
+ gchar ***readable,
+ gchar ***writable);
diff --git a/src/settings-backend.c b/src/settings-backend.c
new file mode 100644
index 0000000..6bd5f7b
--- /dev/null
+++ b/src/settings-backend.c
@@ -0,0 +1,661 @@
+/*
+ * Copyright © 2010 Codethink Limited
+ * Copyright © 2012 Canonical Limited
+ * Copyright © 2015 Collabora ltd.
+ *
+ * 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 <http://www.gnu.org/licenses/>.
+ *
+ * Authors: Xavier Claessens <xavier.claessens@collabora.com>
+ * Allison Ryan Lortie <desrt@desrt.ca>
+ */
+
+#include "config.h"
+
+#include <gio/gio.h>
+#include <dconf.h>
+
+#define G_SETTINGS_ENABLE_BACKEND
+#include <gio/gsettingsbackend.h>
+
+#include "util.h"
+
+#define MAX_IN_FLIGHT 2
+
+#define TYPE_SETTINGS_BACKEND settings_backend_get_type()
+G_DECLARE_FINAL_TYPE(SettingsBackend, settings_backend,, SETTINGS_BACKEND, GSettingsBackend)
+
+struct _SettingsBackend
+{
+ GSettingsBackend parent;
+
+ GThread *thread;
+ GMainLoop *loop;
+ GMainContext *context;
+ GMutex mutex;
+ GCond cond;
+ gboolean initialized;
+ GError *initialization_error;
+
+ GDBusConnection *connection;
+ guint update_id;
+ guint writability_update_id;
+
+ DConfChangeset *user_database;
+ DConfChangeset *default_database;
+ gchar **writable;
+ gchar **locks;
+
+ GQueue pending;
+ GQueue in_flight;
+};
+
+static void initable_iface_init (GInitableIface *iface);
+G_DEFINE_TYPE_WITH_CODE (SettingsBackend, settings_backend, G_TYPE_SETTINGS_BACKEND,
+ G_IMPLEMENT_INTERFACE (G_TYPE_INITABLE, initable_iface_init))
+
+static gpointer
+thread_func (gpointer user_data)
+{
+ SettingsBackend *self = user_data;
+
+ g_main_context_push_thread_default (self->context);
+ g_main_loop_run (self->loop);
+ g_main_context_pop_thread_default (self->context);
+
+ return NULL;
+}
+
+static void
+thread_sync (SettingsBackend *self)
+{
+ g_mutex_lock (&self->mutex);
+ while (!g_queue_is_empty (&self->in_flight) ||
+ !g_queue_is_empty (&self->pending))
+ g_cond_wait (&self->cond, &self->mutex);
+ g_mutex_unlock (&self->mutex);
+}
+
+typedef struct
+{
+ GWeakRef weak_ref;
+ DConfChangeset *changeset;
+} WriteData;
+
+static WriteData *
+write_data_new (SettingsBackend *self,
+ DConfChangeset *changeset)
+{
+ WriteData *data;
+
+ data = g_slice_new0 (WriteData);
+ g_weak_ref_init (&data->weak_ref, self);
+ data->changeset = dconf_changeset_ref (changeset);
+
+ return data;
+}
+
+static void
+write_data_free (WriteData *data)
+{
+ g_weak_ref_clear (&data->weak_ref);
+ dconf_changeset_unref (data->changeset);
+ g_slice_free (WriteData, data);
+}
+
+G_DEFINE_AUTOPTR_CLEANUP_FUNC (WriteData, write_data_free);
+
+static void pending_queue_continue (SettingsBackend *self);
+
+static void
+emit_changed (SettingsBackend *self,
+ DConfChangeset *changeset,
+ gpointer origin_tag)
+{
+ GSettingsBackend *backend = (GSettingsBackend *) self;
+ const gchar *prefix;
+ const gchar * const *paths;
+
+ if (dconf_changeset_is_empty (changeset))
+ return;
+
+ dconf_changeset_describe (changeset, &prefix, &paths, NULL);
+
+ if (paths[1] == NULL)
+ {
+ if (g_str_has_suffix (prefix, "/"))
+ g_settings_backend_path_changed (backend, prefix, origin_tag);
+ else
+ g_settings_backend_changed (backend, prefix, origin_tag);
+ }
+ else
+ {
+ g_settings_backend_keys_changed (backend, prefix, paths, origin_tag);
+ }
+}
+
+static void
+write_cb (GObject *source,
+ GAsyncResult *result,
+ gpointer user_data)
+{
+ g_autoptr(WriteData) data = user_data;
+ g_autoptr(GVariant) reply = NULL;
+ SettingsBackend *self;
+ GError *error = NULL;
+
+ self = g_weak_ref_get (&data->weak_ref);
+ if (self == NULL)
+ return;
+
+ reply = g_dbus_connection_call_finish (self->connection, result, &error);
+
+ g_mutex_lock (&self->mutex);
+ if (reply)
+ {
+ DConfChangeset *expected = g_queue_pop_head (&self->in_flight);
+
+ g_assert (error == NULL);
+ g_assert (data->changeset == expected);
+
+ dconf_changeset_change (self->user_database, data->changeset);
+ }
+ else
+ {
+ gboolean found;
+
+ g_warning ("Error: %s", error->message);
+ found = g_queue_remove (&self->in_flight, data->changeset);
+ g_assert (found);
+ }
+ pending_queue_continue (self);
+ g_mutex_unlock (&self->mutex);
+
+ /* If there was an error, notify the rollback. Otherwise the change was
+ * already notified. */
+ if (error)
+ emit_changed (self, data->changeset, NULL);
+
+ /* Drop the ref held by the queue */
+ dconf_changeset_unref (data->changeset);
+}
+
+static void
+pending_queue_continue (SettingsBackend *self)
+{
+ if (!g_queue_is_empty (&self->pending) &&
+ g_queue_get_length (&self->in_flight) < MAX_IN_FLIGHT)
+ {
+ DConfChangeset *changeset;
+ WriteData *data;
+
+ changeset = g_queue_pop_head (&self->pending);
+ g_queue_push_tail (&self->in_flight, changeset);
+
+ data = write_data_new (self, changeset);
+ g_dbus_connection_call (self->connection,
+ BUS_NAME, OBJECT_PATH, SETTINGS_INTERFACE_NAME,
+ "Write",
+ g_variant_new ("(v)", value_to_ay (dconf_changeset_serialise (changeset))),
+ G_VARIANT_TYPE_UNIT,
+ G_DBUS_CALL_FLAGS_NONE,
+ -1, NULL,
+ write_cb, data);
+ }
+
+ if (g_queue_is_empty (&self->in_flight))
+ {
+ g_assert (g_queue_is_empty (&self->pending));
+ g_cond_broadcast (&self->cond);
+ }
+}
+
+static gboolean
+pending_queue_continue_invoke_cb (gpointer user_data)
+{
+ SettingsBackend *self = user_data;
+
+ g_mutex_lock (&self->mutex);
+ pending_queue_continue (self);
+ g_mutex_unlock (&self->mutex);
+
+ return G_SOURCE_REMOVE;
+}
+
+static gboolean
+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;
+}
+
+static GVariant *
+settings_backend_read (GSettingsBackend *backend,
+ const gchar *key,
+ const GVariantType *expected_type,
+ gboolean default_value)
+{
+ SettingsBackend *self = (SettingsBackend *) backend;
+ GVariant *value = NULL;
+ gboolean found = FALSE;
+
+ g_mutex_lock (&self->mutex);
+
+ if (!default_value)
+ {
+ found = find_key_in_queue (&self->pending, key, &value) ||
+ find_key_in_queue (&self->in_flight, key, &value) ||
+ dconf_changeset_get (self->user_database, key, &value);
+ }
+
+ /* We could have found a NULL value, in the case a reset is in flight,
+ * in that case we want to fallback to the default value as well. */
+ if (!found || value == NULL)
+ dconf_changeset_get (self->default_database, key, &value);
+
+ g_mutex_unlock (&self->mutex);
+
+ return value;
+}
+
+static gboolean
+settings_backend_get_writable (GSettingsBackend *backend,
+ const gchar *path)
+{
+ SettingsBackend *self = (SettingsBackend *) backend;
+
+ return in_paths ((const gchar * const *) self->writable, path) &&
+ !in_paths ((const gchar * const *) self->locks, path);
+}
+
+gboolean
+change_allowed_predicate (const gchar *path,
+ GVariant *value,
+ gpointer user_data)
+{
+ SettingsBackend *self = user_data;
+
+ /* Reset never fails */
+ if (value == NULL)
+ return TRUE;
+
+ return in_paths ((const gchar * const *) self->writable, path) &&
+ !in_paths ((const gchar * const *) self->locks, path);
+}
+
+static gboolean
+change_fast (SettingsBackend *self,
+ DConfChangeset *changeset,
+ gpointer origin_tag)
+{
+ GList *l;
+
+ if (dconf_changeset_is_empty (changeset))
+ return TRUE;
+
+ if (!dconf_changeset_all (changeset, change_allowed_predicate, self))
+ return FALSE;
+
+ g_mutex_lock (&self->mutex);
+ for (l = g_queue_peek_head_link (&self->pending); l != NULL; l = l->next)
+ {
+ DConfChangeset *queued_changeset = l->data;
+
+ if (dconf_changeset_is_similar_to (changeset, queued_changeset))
+ {
+ g_queue_delete_link (&self->pending, l);
+ dconf_changeset_unref (queued_changeset);
+ break;
+ }
+ }
+ g_queue_push_tail (&self->pending, dconf_changeset_ref (changeset));
+ g_mutex_unlock (&self->mutex);
+
+ g_main_context_invoke (self->context, pending_queue_continue_invoke_cb, self);
+
+ emit_changed (self, changeset, origin_tag);
+
+ return TRUE;
+}
+
+static gboolean
+settings_backend_write (GSettingsBackend *backend,
+ const gchar *key,
+ GVariant *value,
+ gpointer origin_tag)
+{
+ SettingsBackend *self = (SettingsBackend *) backend;
+ g_autoptr(DConfChangeset) changeset;
+
+ changeset = dconf_changeset_new_write (key, value);
+
+ return change_fast (self, changeset, origin_tag);
+}
+
+static gboolean
+write_tree_foreach (gpointer key,
+ gpointer value,
+ gpointer user_data)
+{
+ DConfChangeset *changeset = user_data;
+
+ dconf_changeset_set (changeset, key, value);
+
+ return FALSE;
+}
+
+static gboolean
+settings_backend_write_tree (GSettingsBackend *backend,
+ GTree *tree,
+ gpointer origin_tag)
+{
+ SettingsBackend *self = (SettingsBackend *) backend;
+ g_autoptr(DConfChangeset) changeset;
+
+ changeset = dconf_changeset_new ();
+ g_tree_foreach (tree, write_tree_foreach, changeset);
+
+ return change_fast (self, changeset, origin_tag);
+}
+
+static void
+settings_backend_reset (GSettingsBackend *backend,
+ const gchar *key,
+ gpointer origin_tag)
+{
+ settings_backend_write (backend, key, NULL, origin_tag);
+}
+
+static void
+settings_backend_sync (GSettingsBackend *backend)
+{
+ SettingsBackend *self = (SettingsBackend *) backend;
+ g_autoptr(GVariant) reply;
+ g_autoptr(GError) error = NULL;
+
+ thread_sync (self);
+
+ reply = g_dbus_connection_call_sync (self->connection,
+ BUS_NAME, OBJECT_PATH, SETTINGS_INTERFACE_NAME,
+ "Sync",
+ g_variant_new ("()"),
+ G_VARIANT_TYPE_UNIT,
+ G_DBUS_CALL_FLAGS_NONE, -1, NULL, &error);
+
+ if (error != NULL)
+ g_warning ("Error: %s", error->message);
+}
+
+static void
+update_cb (GDBusConnection *connection,
+ const gchar *sender_name,
+ const gchar *object_path,
+ const gchar *interface_name,
+ const gchar *signal_name,
+ GVariant *parameters,
+ gpointer user_data)
+{
+ SettingsBackend *self = user_data;
+ g_autoptr(GVariant) user_changeset_ay = NULL;
+ g_autoptr(GVariant) user_changeset_serialised = NULL;
+ g_autoptr(GVariant) default_changeset_ay = NULL;
+ g_autoptr(GVariant) default_changeset_serialised = NULL;
+ g_autoptr(DConfChangeset) user_changeset = NULL;
+ g_autoptr(DConfChangeset) default_changeset = NULL;
+
+ g_return_if_fail (g_variant_is_of_type (parameters, G_VARIANT_TYPE ("(vv)")));
+
+ g_variant_get (parameters, "(vv)", &user_changeset_ay, &default_changeset_ay);
+ user_changeset_serialised = ay_to_value (user_changeset_ay);
+ default_changeset_serialised = ay_to_value (default_changeset_ay);
+ user_changeset = dconf_changeset_deserialise (user_changeset_serialised);
+ default_changeset = dconf_changeset_deserialise (default_changeset_serialised);
+
+ g_mutex_lock (&self->mutex);
+ dconf_changeset_change (self->user_database, user_changeset);
+ dconf_changeset_change (self->default_database, default_changeset);
+ g_mutex_unlock (&self->mutex);
+
+ emit_changed (self, user_changeset, NULL);
+}
+
+static void
+writability_update_cb (GDBusConnection *connection,
+ const gchar *sender_name,
+ const gchar *object_path,
+ const gchar *interface_name,
+ const gchar *signal_name,
+ GVariant *parameters,
+ gpointer user_data)
+{
+ SettingsBackend *self = user_data;
+ GSettingsBackend *backend = (GSettingsBackend *) self;
+ guint i;
+
+ g_return_if_fail (g_variant_is_of_type (parameters, G_VARIANT_TYPE ("(as)")));
+
+ g_strfreev (self->locks);
+ g_variant_get (parameters, "(^as)", &self->locks);
+
+ for (i = 0; self->locks[i] != NULL; i++)
+ {
+ if (g_str_has_suffix (self->locks[i], "/"))
+ g_settings_backend_path_writable_changed (backend, self->locks[i]);
+ else
+ g_settings_backend_writable_changed (backend, self->locks[i]);
+ }
+}
+
+static gboolean
+initialize_invoke_cb (gpointer user_data)
+{
+ SettingsBackend *self = user_data;
+ g_autoptr(GVariant) reply = NULL;
+ g_autoptr(GVariant) user_changeset_ay = NULL;
+ g_autoptr(GVariant) user_changeset_serialised = NULL;
+ g_autoptr(GVariant) default_changeset_ay = NULL;
+ g_autoptr(GVariant) default_changeset_serialised = NULL;
+ g_autoptr(DConfChangeset) user_changeset = NULL;
+ g_autoptr(DConfChangeset) default_changeset = NULL;
+
+ self->connection = g_bus_get_sync (G_BUS_TYPE_SESSION, NULL,
+ &self->initialization_error);
+ if (self->connection == NULL)
+ goto out;
+
+ self->update_id = g_dbus_connection_signal_subscribe (self->connection,
+ BUS_NAME, SETTINGS_INTERFACE_NAME, "Update", OBJECT_PATH, NULL,
+ G_DBUS_SIGNAL_FLAGS_NONE,
+ update_cb, self, NULL);
+ self->writability_update_id = g_dbus_connection_signal_subscribe (self->connection,
+ BUS_NAME, SETTINGS_INTERFACE_NAME, "WritabilityUpdate", OBJECT_PATH, NULL,
+ G_DBUS_SIGNAL_FLAGS_NONE,
+ writability_update_cb, self, NULL);
+
+ reply = g_dbus_connection_call_sync (self->connection,
+ BUS_NAME, OBJECT_PATH, SETTINGS_INTERFACE_NAME,
+ "GetAll",
+ g_variant_new ("()"),
+ G_VARIANT_TYPE ("(vvasas)"),
+ G_DBUS_CALL_FLAGS_NONE,
+ -1, NULL, &self->initialization_error);
+ if (reply == NULL)
+ goto out;
+
+ g_variant_get (reply, "(vv^as^as)",
+ &user_changeset_ay,
+ &default_changeset_ay,
+ &self->writable,
+ &self->locks);
+
+ user_changeset_serialised = ay_to_value (user_changeset_ay);
+ default_changeset_serialised = ay_to_value (default_changeset_ay);
+ user_changeset = dconf_changeset_deserialise (user_changeset_serialised);
+ default_changeset = dconf_changeset_deserialise (default_changeset_serialised);
+
+ dconf_changeset_change (self->user_database, user_changeset);
+ dconf_changeset_change (self->default_database, default_changeset);
+
+out:
+ g_mutex_lock (&self->mutex);
+ self->initialized = TRUE;
+ g_cond_broadcast (&self->cond);
+ g_mutex_unlock (&self->mutex);
+
+ return G_SOURCE_REMOVE;
+}
+
+static gboolean
+initable_init (GInitable *initable,
+ GCancellable *cancellable,
+ GError **error)
+{
+ SettingsBackend *self = (SettingsBackend *) initable;
+ GApplication *app;
+
+ app = g_application_get_default ();
+ if (app != NULL)
+ {
+ const gchar * const *interfaces;
+
+ interfaces = g_application_get_services (app);
+ if (!g_strv_contains (interfaces, SETTINGS_INTERFACE_NAME))
+ {
+ g_set_error_literal (error, G_DBUS_ERROR, G_DBUS_ERROR_UNKNOWN_INTERFACE,
+ "Settings are not confined");
+ return FALSE;
+ }
+ }
+
+ g_mutex_lock (&self->mutex);
+
+ if (self->thread == NULL)
+ {
+ self->thread = g_thread_new ("appservice-settings-thread",
+ thread_func, self);
+ g_main_context_invoke (self->context, initialize_invoke_cb, self);
+ }
+
+ while (!self->initialized)
+ g_cond_wait (&self->cond, &self->mutex);
+
+ g_mutex_unlock (&self->mutex);
+
+ if (self->initialization_error != NULL)
+ {
+ g_propagate_error (error, g_error_copy (self->initialization_error));
+ return FALSE;
+ }
+
+ return TRUE;
+}
+
+static void
+settings_backend_init (SettingsBackend *self)
+{
+ g_mutex_init (&self->mutex);
+ g_cond_init (&self->cond);
+ self->context = g_main_context_new ();
+ self->loop = g_main_loop_new (self->context, FALSE);
+ self->user_database = dconf_changeset_new_database (NULL);
+ self->default_database = dconf_changeset_new_database (NULL);
+}
+
+static void
+settings_backend_finalize (GObject *object)
+{
+ SettingsBackend *self = (SettingsBackend *) object;
+
+ if (self->initialized)
+ {
+ g_main_loop_quit (self->loop);
+ g_thread_join (self->thread);
+
+ /* FIXME: Should we call thread_sync() from finalize? */
+ g_warn_if_fail (g_queue_is_empty (&self->pending));
+ g_warn_if_fail (g_queue_is_empty (&self->in_flight));
+ g_queue_foreach (&self->pending, (GFunc) dconf_changeset_unref, NULL);
+ g_queue_foreach (&self->in_flight, (GFunc) dconf_changeset_unref, NULL);
+
+ g_dbus_connection_signal_unsubscribe (self->connection, self->update_id);
+ g_dbus_connection_signal_unsubscribe (self->connection, self->writability_update_id);
+ g_object_unref (self->connection);
+ }
+
+ g_mutex_clear (&self->mutex);
+ g_cond_clear (&self->cond);
+ g_main_context_unref (self->context);
+ g_main_loop_unref (self->loop);
+ dconf_changeset_unref (self->user_database);
+ dconf_changeset_unref (self->default_database);
+ g_strfreev (self->writable);
+ g_strfreev (self->locks);
+
+ G_OBJECT_CLASS (settings_backend_parent_class)->finalize (object);
+}
+
+static void
+settings_backend_class_init (SettingsBackendClass *class)
+{
+ GObjectClass *object_class = (GObjectClass *) class;
+ GSettingsBackendClass *base_class = (GSettingsBackendClass *) class;
+
+ object_class->finalize = settings_backend_finalize;
+
+ base_class->read = settings_backend_read;
+ base_class->get_writable = settings_backend_get_writable;
+ base_class->write = settings_backend_write;
+ base_class->write_tree = settings_backend_write_tree;
+ base_class->reset = settings_backend_reset;
+ base_class->sync = settings_backend_sync;
+}
+
+static void
+initable_iface_init (GInitableIface *iface)
+{
+ iface->init = initable_init;
+}
+
+void
+g_io_module_load (GIOModule *module)
+{
+ g_type_module_use (G_TYPE_MODULE (module));
+
+ /* Give an higher priority than DConfSettingsBackend so it gets tried first.
+ * If the application isn't confined g_initable_new() will return an error
+ * and another backend will be used. */
+ g_io_extension_point_implement (G_SETTINGS_BACKEND_EXTENSION_POINT_NAME,
+ TYPE_SETTINGS_BACKEND,
+ "appservice-settings",
+ 200);
+}
+
+void
+g_io_module_unload (GIOModule *module)
+{
+ g_assert_not_reached ();
+}
+
+gchar **
+g_io_module_query (void)
+{
+ return g_strsplit (G_SETTINGS_BACKEND_EXTENSION_POINT_NAME, "!", 0);
+}
diff --git a/src/settings-component.c b/src/settings-component.c
index 963ffe5..4aeae1f 100644
--- a/src/settings-component.c
+++ b/src/settings-component.c
@@ -1,5 +1,6 @@
/*
* Copyright © 2015 Canonical Limited
+ * Copyright © 2015 Collabora ltd.
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
@@ -14,135 +15,148 @@
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, see <http://www.gnu.org/licenses/>.
*
- * Author: Ryan Lortie <desrt@desrt.ca>
+ * Authors: Xavier Claessens <xavier.claessens@collabora.com>
+ * Allison Ryan Lortie <desrt@desrt.ca>
*/
-#include "component.h"
+#include "config.h"
+
+#include <string.h>
+#include <dconf.h>
#include "application.h"
+#include "component.h"
#include "security.h"
+#include "util.h"
#include "watch-tree.h"
-#include <string.h>
-#include <dconf.h>
-
static DConfClient *dconf_client;
static WatchTree *watch_tree;
-typedef struct
+#define TYPE_SETTINGS_COMPONENT settings_component_get_type ()
+G_DECLARE_FINAL_TYPE (SettingsComponent, settings_component, , SETTINGS_COMPONENT, Component)
+
+struct _SettingsComponent
{
Component parent_instance;
- DConfChangeset *existing_items;
- DConfChangeset *changes;
+ GPtrArray *changes;
gint idle;
+ gboolean ignore_change_signal;
+
gchar **readable;
gchar **writable;
-} SettingsComponent;
-
-typedef ComponentClass SettingsComponentClass;
+ gboolean initialized;
+};
G_DEFINE_TYPE (SettingsComponent, settings_component, TYPE_COMPONENT)
static void
-settings_component_traverse_path (DConfClient *client,
- DConfChangeset *changeset,
- const gchar *path)
+settings_component_traverse_path (SettingsComponent *self,
+ const gchar *path,
+ DConfChangeset *user_values,
+ DConfChangeset *default_values)
{
if (g_str_has_suffix (path, "/"))
{
- gchar **list;
+ g_auto(GStrv) list;
gint i;
- list = dconf_client_list (client, path, NULL);
+ list = dconf_client_list (dconf_client, path, NULL);
for (i = 0; list[i]; i++)
{
- gchar *full = g_strconcat (path, list[i], NULL);
- settings_component_traverse_path (client, changeset, full);
- g_free (full);
+ g_autofree gchar *full = g_strconcat (path, list[i], NULL);
+ settings_component_traverse_path (self, full, user_values, default_values);
}
-
- g_strfreev (list);
+ }
+ else if (in_paths ((const gchar * const *) self->writable, path))
+ {
+ g_autoptr(GVariant) user_value;
+ g_autoptr(GVariant) default_value;
+
+ user_value = dconf_client_read_full (dconf_client, path,
+ DCONF_READ_USER_VALUE, NULL);
+ default_value = dconf_client_read_full (dconf_client, path,
+ DCONF_READ_DEFAULT_VALUE, NULL);
+
+ if (user_value)
+ dconf_changeset_set (user_values, path, user_value);
+ if (default_value)
+ dconf_changeset_set (default_values, path, default_value);
}
else
{
- GVariant *value;
-
- value = dconf_client_read (client, path);
-
- /* The value could have been deleted.
- *
- * We already reset all values covered by the changes in
- * question before starting the recursion, so if we see NULL
- * here then just do nothing.
- *
- * It's also possible that we get NULL for a key that was
- * list()ed as existing but no longer exists by the time we
- * read it. In that case we should just do nothing -- we will
- * receive another change event later.
- */
- if (value)
- {
- dconf_changeset_set (changeset, path, value);
- g_variant_unref (value);
- }
+ g_autoptr(GVariant) effective_value;
+
+ /* If the key is not writable by this application, the effective value
+ * is considered a default value. */
+ effective_value = dconf_client_read (dconf_client, path);
+ if (effective_value)
+ dconf_changeset_set (default_values, path, effective_value);
}
}
static gboolean
settings_component_flush_changes (gpointer user_data)
{
- SettingsComponent *settings = user_data;
- DConfChangeset *new_items;
- DConfChangeset *diff;
+ SettingsComponent *self = user_data;
+ Application *application;
+ g_autoptr(DConfChangeset) user_changes;
+ g_autoptr(DConfChangeset) default_changes;
+ guint i;
- g_assert (settings->changes != NULL);
+ g_assert (self->changes != NULL);
- new_items = dconf_changeset_new_database (settings->existing_items);
- dconf_changeset_change (new_items, settings->changes);
- diff = dconf_changeset_diff (settings->existing_items, new_items);
-
- if (diff)
+ user_changes = dconf_changeset_new ();
+ default_changes = dconf_changeset_new ();
+ for (i = 0; i < self->changes->len; i++)
{
- component_emit (COMPONENT (settings), dconf_changeset_serialise (diff));
- dconf_changeset_unref (diff);
- }
+ const gchar *path = g_ptr_array_index (self->changes, i);
- g_clear_pointer (&settings->existing_items, dconf_changeset_unref);
- g_clear_pointer (&settings->changes, dconf_changeset_unref);
- settings->existing_items = new_items;
- settings->idle = 0;
+ dconf_changeset_set (user_changes, path, NULL);
+ dconf_changeset_set (default_changes, path, NULL);
+ settings_component_traverse_path (self, path, user_changes, default_changes);
+ }
- return FALSE;
+ application = component_get_application ((Component *) self);
+ g_dbus_connection_emit_signal (
+ application_get_connection (application),
+ application_get_unique_name (application),
+ OBJECT_PATH, SETTINGS_INTERFACE_NAME,
+ "Update",
+ g_variant_new ("(vv)",
+ value_to_ay (dconf_changeset_serialise (user_changes)),
+ value_to_ay (dconf_changeset_serialise (default_changes))),
+ NULL);
+
+ self->idle = 0;
+ g_clear_pointer (&self->changes, g_ptr_array_unref);
+
+ return G_SOURCE_REMOVE;
}
static void
-settings_component_handle_changes (SettingsComponent *settings,
- DConfClient *client,
+settings_component_handle_changes (SettingsComponent *self,
const gchar *path)
{
- if (!settings->changes)
+ if (self->ignore_change_signal)
+ return;
+
+ if (!self->changes)
{
- settings->changes = dconf_changeset_new ();
- settings->idle = g_idle_add (settings_component_flush_changes, settings);
+ self->changes = g_ptr_array_new_with_free_func (g_free);
+ self->idle = g_idle_add (settings_component_flush_changes, self);
}
- dconf_changeset_set (settings->changes, path, NULL);
- settings_component_traverse_path (client, settings->changes, path);
-}
-
-const gchar *
-settings_component_get_id (Component *component)
-{
- return "settings";
+ g_ptr_array_add (self->changes, g_strdup (path));
}
static void
settings_component_got_changes (DConfClient *client,
const gchar *prefix,
const gchar * const *changes,
- gpointer tag,
+ const gchar *tag,
gpointer user_data)
{
WatchTree *watch_tree = user_data;
@@ -150,7 +164,7 @@ settings_component_got_changes (DConfClient *client,
for (i = 0; changes[i]; i++)
{
- gchar *full = g_strconcat (prefix, changes[i], NULL);
+ g_autofree gchar *full = g_strconcat (prefix, changes[i], NULL);
gint full_length = strlen (full);
GSList *nodes;
@@ -169,101 +183,332 @@ settings_component_got_changes (DConfClient *client,
while (watchers)
{
- settings_component_handle_changes (watchers->data, client, effective);
+ settings_component_handle_changes (watchers->data, effective);
watchers = g_slist_remove_link (watchers, watchers);
}
nodes = g_slist_remove_link (nodes, nodes);
}
+ }
+}
+
+static GVariant *
+collect_locks (SettingsComponent *self)
+{
+ GVariantBuilder builder;
+ guint i, j;
+
+ /* For all writable paths, checks if some keys are locked. */
+ g_variant_builder_init (&builder, G_VARIANT_TYPE ("as"));
+ for (i = 0; self->writable[i]; i++)
+ {
+ g_auto(GStrv) locks = NULL;
+
+ if (!g_str_has_suffix (self->writable[i], "/"))
+ {
+ if (!dconf_client_is_writable (dconf_client, self->writable[i]))
+ g_variant_builder_add (&builder, "s", self->writable[i]);
+ continue;
+ }
- g_free (full);
+ locks = dconf_client_list_locks (dconf_client, self->writable[i], NULL);
+ for (j = 0; locks[j] != NULL; j++)
+ g_variant_builder_add (&builder, "s", locks[j]);
}
+
+ return g_variant_builder_end (&builder);
}
-GVariant *
-settings_component_startup (Component *component,
- GVariant *argument)
+static void
+settings_component_got_writability_changes (DConfClient *client,
+ const gchar *path,
+ gpointer user_data)
{
- SettingsComponent *settings = (SettingsComponent *) component;
- const gchar *con;
+ WatchTree *watch_tree = user_data;
+ g_autoptr(GHashTable) components;
+ g_autoptr(GSList) nodes;
+ GHashTableIter iter;
+ gpointer value;
+ GSList *l, *ll;
+
+ /* Collect all SettingsComponent objects affected by this change */
+ components = g_hash_table_new (NULL, NULL);
+ nodes = watch_tree_collect (watch_tree, path);
+ for (l = nodes; l != NULL; l = l->next)
+ {
+ g_autoptr(GSList) watchers;
+
+ watchers = watch_tree_get_watchers (l->data);
+ for (ll = watchers; ll != NULL; ll = ll->next)
+ g_hash_table_add (components, ll->data);
+ }
+
+ /* For each of them, send the new set of locks */
+ g_hash_table_iter_init (&iter, components);
+ while (g_hash_table_iter_next (&iter, NULL, &value))
+ {
+ SettingsComponent *self = value;
+ Application *application;
+
+ application = component_get_application ((Component *) self);
+ g_dbus_connection_emit_signal (
+ application_get_connection (application),
+ application_get_unique_name (application),
+ OBJECT_PATH, SETTINGS_INTERFACE_NAME,
+ "WritabilityUpdate",
+ collect_locks (self),
+ NULL);
+ }
+}
+
+static void
+ensure_initialized (SettingsComponent *self)
+{
+ Application *application;
+ const gchar *label;
gint i;
+ if (self->initialized)
+ {
+ g_assert (self->readable != NULL);
+ g_assert (self->writable != NULL);
+ return;
+ }
+
if (!dconf_client)
{
dconf_client = dconf_client_new ();
watch_tree = watch_tree_new ();
- g_signal_connect (dconf_client, "changed", G_CALLBACK (settings_component_got_changes), watch_tree);
+ g_signal_connect (dconf_client, "changed",
+ G_CALLBACK (settings_component_got_changes),
+ watch_tree);
+ g_signal_connect (dconf_client, "writability-changed",
+ G_CALLBACK (settings_component_got_writability_changes),
+ watch_tree);
}
- con = application_get_security_context (component->application);
- security_module_get_dconf_paths (con, &settings->readable, &settings->writable);
+ application = component_get_application ((Component *) self);
+ label = application_get_security_context (application);
+ security_dconf_get_paths (label, &self->readable, &self->writable);
+
+ /* Consistency check: writable keys should be readable as well. */
+ for (i = 0; self->writable[i] != NULL; i++)
+ {
+ if (!in_paths ((const gchar * const *) self->readable, self->writable[i]))
+ {
+ g_warning ("Path '%s' is writable but not readable for label '%s'",
+ self->writable[i], label);
+ }
+ }
- for (i = 0; settings->readable[i]; i++)
+ /* Watch all readable paths */
+ for (i = 0; self->readable[i]; i++)
{
- watch_tree_add (watch_tree, settings->readable[i], settings);
- dconf_client_watch_fast (dconf_client, settings->readable[i]);
- settings_component_traverse_path (dconf_client, settings->existing_items, settings->readable[i]);
+ if (watch_tree_add (watch_tree, self->readable[i], self))
+ dconf_client_watch_fast (dconf_client, self->readable[i]);
}
- return dconf_changeset_serialise (settings->existing_items);
+ self->initialized = TRUE;
}
void
-settings_component_shutdown (Component *component)
+settings_component_get_all (SettingsComponent *self,
+ GDBusConnection *connection,
+ GVariant *parameters,
+ GDBusMethodInvocation *invocation)
{
- SettingsComponent *settings = (SettingsComponent *) component;
+ g_autoptr(DConfChangeset) user_database;
+ g_autoptr(DConfChangeset) default_database;
gint i;
- for (i = 0; settings->readable[i]; i++)
+ ensure_initialized (self);
+
+ user_database = dconf_changeset_new_database (NULL);
+ default_database = dconf_changeset_new_database (NULL);
+ for (i = 0; self->readable[i]; i++)
+ {
+ settings_component_traverse_path (self, self->readable[i],
+ user_database, default_database);
+ }
+
+ g_dbus_method_invocation_return_value (invocation,
+ g_variant_new ("(vv^as@as)",
+ value_to_ay (dconf_changeset_serialise (user_database)),
+ value_to_ay (dconf_changeset_serialise (default_database)),
+ self->writable,
+ collect_locks (self)));
+}
+
+typedef struct
+{
+ SettingsComponent *self;
+ DConfChangeset *filtered_changeset;
+} PredicateData;
+
+static gboolean
+verify_and_filter_predicate (const gchar *path,
+ GVariant *value,
+ gpointer user_data)
+{
+ PredicateData *data = user_data;
+ SettingsComponent *self = data->self;
+
+ /* When value is NULL it means it's a reset of the key, or a directory.
+ * A reset is always allowed, but we should only reset writable keys. So we
+ * copy in data->filtered_changeset only resets on writable keys, other resets
+ * are ignored. It is however an error to set a non-NULL value on a
+ * non-writable key.
+ */
+
+ if (value == NULL && g_str_has_suffix (path, "/"))
+ {
+ guint i;
+
+ /* Resetting a directory means resetting all writable keys in that
+ * directory. */
+ for (i = 0; self->writable[i] != NULL; i++)
+ {
+ if (g_str_has_prefix (self->writable[i], path))
+ {
+ dconf_changeset_set (data->filtered_changeset, self->writable[i],
+ NULL);
+ }
+ }
+
+ return TRUE;
+ }
+
+ if (!in_paths ((const gchar * const *) self->writable, path))
+ return (value == NULL);
+
+ dconf_changeset_set (data->filtered_changeset, path, value);
+
+ return TRUE;
+}
+
+void
+settings_component_write (SettingsComponent *self,
+ GDBusConnection *connection,
+ GVariant *parameters,
+ GDBusMethodInvocation *invocation)
+{
+ g_autoptr(GVariant) ay = NULL;
+ g_autoptr(GVariant) serialised = NULL;
+ g_autoptr(DConfChangeset) changeset = NULL;
+ g_autoptr(DConfChangeset) filtered_changeset = NULL;
+ PredicateData data;
+ GError *error = NULL;
+
+ if (!self->initialized)
{
- watch_tree_remove (watch_tree, settings->readable[i], settings);
- dconf_client_unwatch_fast (dconf_client, settings->readable[i]);
+ g_dbus_method_invocation_return_error_literal (invocation,
+ G_DBUS_ERROR, G_DBUS_ERROR_FAILED,
+ "Settings component not yet initialized. Call GetAll first.");
+ return;
}
- dconf_changeset_unref (settings->existing_items);
+ g_variant_get (parameters, "(v)", &ay);
+ serialised = ay_to_value (ay);
+ changeset = dconf_changeset_deserialise (serialised);
+ filtered_changeset = dconf_changeset_new ();
+ data.self = self;
+ data.filtered_changeset = filtered_changeset;
- if (settings->changes)
- dconf_changeset_unref (settings->changes);
+ if (!dconf_changeset_all (changeset, verify_and_filter_predicate, &data))
+ {
+ g_dbus_method_invocation_return_error_literal (invocation,
+ G_DBUS_ERROR, G_DBUS_ERROR_FAILED,
+ "Attempted to modify non-writable key");
+ return;
+ }
+
+ self->ignore_change_signal = TRUE;
+ if (!dconf_client_change_fast (dconf_client, filtered_changeset, &error))
+ g_dbus_method_invocation_take_error (invocation, error);
+ else
+ g_dbus_method_invocation_return_value (invocation, g_variant_new ("()"));
+ self->ignore_change_signal = FALSE;
+}
- if (settings->idle)
- g_source_remove (settings->idle);
+void
+settings_component_sync (SettingsComponent *self,
+ GDBusConnection *connection,
+ GVariant *parameters,
+ GDBusMethodInvocation *invocation)
+{
+ if (self->initialized)
+ dconf_client_sync (dconf_client);
- g_strfreev (settings->readable);
- g_strfreev (settings->writable);
+ g_dbus_method_invocation_return_value (invocation, g_variant_new ("()"));
}
-const GVariantType *
-settings_component_get_request_type (Component *component)
+static void
+settings_component_method_call (Component *component,
+ GDBusConnection *connection,
+ const gchar *method_name,
+ GVariant *parameters,
+ GDBusMethodInvocation *invocation)
{
- return G_VARIANT_TYPE ("a{smv}");
+ SettingsComponent *self = (SettingsComponent *) component;
+
+ if (g_str_equal (method_name, "GetAll"))
+ settings_component_get_all (self, connection, parameters, invocation);
+ else if (g_str_equal (method_name, "Write"))
+ settings_component_write (self, connection, parameters, invocation);
+ else if (g_str_equal (method_name, "Sync"))
+ settings_component_sync (self, connection, parameters, invocation);
+ else
+ g_assert_not_reached ();
}
void
-settings_component_request (Component *component,
- GVariant *request,
- GDBusMethodInvocation *invocation)
+settings_component_shutdown (Component *component)
{
-#if 0
- DConfChangeset *changeset;
+ SettingsComponent *self = (SettingsComponent *) component;
+ gint i;
- changeset = dconf_changeset_deserialise (request);
- dconf_client_change_sync (dconf_client, changeset, NULL, NULL, NULL);
- dconf_changeset_unref (changeset);
-#endif
+ if (!self->initialized)
+ {
+ g_assert (self->readable == NULL);
+ g_assert (self->writable == NULL);
+ g_assert (self->idle == 0);
+ return;
+ }
+
+ for (i = 0; self->readable[i]; i++)
+ {
+ if (watch_tree_remove (watch_tree, self->readable[i], self))
+ dconf_client_unwatch_fast (dconf_client, self->readable[i]);
+ }
+
+ if (self->idle)
+ g_source_remove (self->idle);
}
static void
-settings_component_init (SettingsComponent *settings)
+settings_component_finalize (GObject *object)
+{
+ SettingsComponent *self = (SettingsComponent *) object;
+
+ g_clear_pointer (&self->changes, g_ptr_array_unref);
+ g_clear_pointer (&self->readable, g_strfreev);
+ g_clear_pointer (&self->writable, g_strfreev);
+
+ G_OBJECT_CLASS (settings_component_parent_class)->finalize (object);
+}
+
+static void
+settings_component_init (SettingsComponent *self)
{
- settings->existing_items = dconf_changeset_new_database (NULL);
}
static void
-settings_component_class_init (ComponentClass *class)
+settings_component_class_init (SettingsComponentClass *class)
{
- class->get_id = settings_component_get_id;
- class->shutdown = settings_component_shutdown;
- class->startup = settings_component_startup;
- class->request = settings_component_request;
- class->get_request_type = settings_component_get_request_type;
+ GObjectClass *object_class = G_OBJECT_CLASS (class);
+ ComponentClass *component_class = _COMPONENT_CLASS (class);
+
+ object_class->finalize = settings_component_finalize;
+ component_class->shutdown = settings_component_shutdown;
+ component_class->method_call = settings_component_method_call;
}
diff --git a/src/util.c b/src/util.c
new file mode 100644
index 0000000..3de1c07
--- /dev/null
+++ b/src/util.c
@@ -0,0 +1,83 @@
+/*
+ * Copyright © 2015 Collabora ltd.
+ *
+ * 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 <http://www.gnu.org/licenses/>.
+ *
+ * Author: Xavier Claessens <xavier.claessens@collabora.com>
+ */
+
+#include "config.h"
+
+#include "util.h"
+
+/**
+ * @value: (transfer consume): Any #GVariant.
+ *
+ * Serialize a variant into a variant. This is used to be able to transmit
+ * types not supported by dbus-1, like maybes. The returned variant is intended
+ * to be sent over DBus as "v" and deserialised using ay_to_value().
+ *
+ * Returns: (transfer consume): A "ay" GVariant.
+ */
+GVariant *
+value_to_ay (GVariant *value)
+{
+ g_autoptr(GBytes) bytes;
+ g_autoptr(GVariant) tmp;
+
+ tmp = g_variant_new_variant (value);
+ bytes = g_variant_get_data_as_bytes (tmp);
+ return g_variant_new_from_bytes (G_VARIANT_TYPE_BYTESTRING, bytes, TRUE);
+}
+
+/**
+ * @value: (transfer none): A "ay" GVariant.
+ *
+ * Deserialise a variant that was created by value_to_ay() and sent over DBus.
+ *
+ * Returns: (transfer consume): The original value as passed to value_to_ay().
+ */
+GVariant *
+ay_to_value (GVariant *ay)
+{
+ g_autoptr(GBytes) bytes;
+ g_autoptr(GVariant) tmp;
+
+ bytes = g_variant_get_data_as_bytes (ay);
+ tmp = g_variant_new_from_bytes (G_VARIANT_TYPE_VARIANT, bytes, FALSE);
+ return g_variant_get_variant (tmp);
+}
+
+gboolean
+in_paths (const gchar * const *paths,
+ const gchar *path)
+{
+ guint i;
+
+ for (i = 0; paths[i] != NULL; i++)
+ {
+ if (g_str_has_suffix (paths[i], "/"))
+ {
+ if (g_str_has_prefix (path, paths[i]))
+ return TRUE;
+ }
+ else
+ {
+ if (g_str_equal (path, paths[i]))
+ return TRUE;
+ }
+ }
+
+ return FALSE;
+}
diff --git a/src/util.h b/src/util.h
new file mode 100644
index 0000000..941c6ac
--- /dev/null
+++ b/src/util.h
@@ -0,0 +1,36 @@
+/*
+ * Copyright © 2015 Collabora ltd.
+ *
+ * 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 <http://www.gnu.org/licenses/>.
+ *
+ * Author: Xavier Claessens <xavier.claessens@collabora.com>
+ */
+
+#ifndef _util_h_
+#define _util_h_
+
+#include <gio/gio.h>
+
+#define OBJECT_PATH "/org/freedesktop/Application/Service"
+#define BUS_NAME "org.freedesktop.Application.Service"
+#define INTERFACE_NAME "org.freedesktop.Application.Service"
+#define SETTINGS_INTERFACE_NAME INTERFACE_NAME ".Settings"
+
+GVariant *value_to_ay (GVariant *value);
+GVariant *ay_to_value (GVariant *ay);
+
+gboolean in_paths (const gchar * const *paths,
+ const gchar *path);
+
+#endif /* _util_h_ */
diff --git a/src/watch-tree.c b/src/watch-tree.c
index 817c2aa..f84eeaa 100644
--- a/src/watch-tree.c
+++ b/src/watch-tree.c
@@ -120,7 +120,7 @@ tree_node_get_child (WatchTree *node,
return child;
}
-void
+gboolean
watch_tree_add (WatchTree *tree,
const gchar *path,
gpointer data)
@@ -145,15 +145,17 @@ watch_tree_add (WatchTree *tree,
}
node->watchers = g_slist_prepend (node->watchers, data);
+
+ return node->watchers->next == NULL;
}
-WatchTreeRemoveResult
+gboolean
watch_tree_remove (WatchTree *tree,
const gchar *path,
gpointer data)
{
WatchTree *root_node = (WatchTree *) tree;
- WatchTreeRemoveResult result;
+ gboolean result;
GSList *backtrack;
WatchTree *node;
StringRef ref;
@@ -181,18 +183,7 @@ watch_tree_remove (WatchTree *tree,
}
node->watchers = g_slist_remove (node->watchers, data);
-
- if (node->watchers != NULL)
- /* nothing changes */
- result = WATCH_TREE_REMOVE_RESULT_NONE;
-
- else if (node->children && g_hash_table_size (node->children))
- /* there are still child watches, so we have to check */
- result = WATCH_TREE_REMOVE_RESULT_SOME;
-
- else
- /* no watchers and no child watches -- remove all */
- result = WATCH_TREE_REMOVE_RESULT_ALL;
+ result = node->watchers == NULL;
while (node->watchers == NULL && (!node->children || g_hash_table_size (node->children)) == 0 && backtrack)
{
diff --git a/src/watch-tree.h b/src/watch-tree.h
index b76ae31..87e5523 100644
--- a/src/watch-tree.h
+++ b/src/watch-tree.h
@@ -22,13 +22,6 @@
#include <glib.h>
-typedef enum
-{
- WATCH_TREE_REMOVE_RESULT_NONE,
- WATCH_TREE_REMOVE_RESULT_SOME,
- WATCH_TREE_REMOVE_RESULT_ALL
-} WatchTreeRemoveResult;
-
typedef struct _WatchTree WatchTree;
WatchTree * watch_tree_new (void);
@@ -38,11 +31,11 @@ const gchar * watch_tree_intersect (WatchTree *tree,
GSList * watch_tree_get_watchers (WatchTree *tree);
-void watch_tree_add (WatchTree *tree,
+gboolean watch_tree_add (WatchTree *tree,
const gchar *path,
gpointer data);
-WatchTreeRemoveResult watch_tree_remove (WatchTree *tree,
+gboolean watch_tree_remove (WatchTree *tree,
const gchar *path,
gpointer data);
diff --git a/tests/Makefile.am b/tests/Makefile.am
new file mode 100644
index 0000000..1ee3f1d
--- /dev/null
+++ b/tests/Makefile.am
@@ -0,0 +1,38 @@
+SUBDIRS=schemas
+
+programs_list = \
+ test-settings \
+ $(NULL)
+
+TESTS = $(programs_list)
+noinst_PROGRAMS = $(programs_list)
+
+TESTS_ENVIRONMENT = \
+ G_SLICE=debug-blocks \
+ G_DEBUG=fatal_warnings,fatal_criticals$(maybe_gc_friendly) \
+ G_MESSAGES_DEBUG=all
+
+test_settings_SOURCES=settings.c
+
+AM_CFLAGS = \
+ $(gio_CFLAGS) \
+ -Wall -g \
+ -DTEST_MODULES=\""$(abs_top_builddir)/src:$(abs_top_builddir)/src/.libs"\" \
+ -DTEST_BUILDDIR=\""$(abs_top_builddir)/tests"\" \
+ -DTEST_SRCDIR=\""$(abs_top_srcdir)/tests"\" \
+ $(NULL)
+LDADD = $(gio_LIBS)
+
+system-db: Makefile $(srcdir)/database.d/system-db $(srcdir)/database.d/locks/00-locks
+ $(AM_V_GEN) $(DCONF) compile $@ $(srcdir)/database.d
+
+all-am: system-db
+
+EXTRA_DIST = \
+ appservice.conf \
+ database.d/system-db \
+ database.d/locks/00-locks \
+ services/ca.desrt.dconf.service \
+ $(NULL)
+
+CLEANFILES = system-db
diff --git a/tests/appservice.conf b/tests/appservice.conf
new file mode 100644
index 0000000..6b1513e
--- /dev/null
+++ b/tests/appservice.conf
@@ -0,0 +1,3 @@
+[unconfined]
+dconf-readable = /org/freedesktop/Application/Service/Test/
+dconf-writable = /org/freedesktop/Application/Service/Test/rw;/org/freedesktop/Application/Service/Test/dir/
diff --git a/tests/database.d/locks/00-locks b/tests/database.d/locks/00-locks
new file mode 100644
index 0000000..24ca440
--- /dev/null
+++ b/tests/database.d/locks/00-locks
@@ -0,0 +1 @@
+/org/freedesktop/Application/Service/Test/dir/locked
diff --git a/tests/database.d/system-db b/tests/database.d/system-db
new file mode 100644
index 0000000..44097dc
--- /dev/null
+++ b/tests/database.d/system-db
@@ -0,0 +1,5 @@
+[org/freedesktop/Application/Service/Test]
+rw=-200
+
+[org/freedesktop/Application/Service/Test/dir]
+rw=-300
diff --git a/tests/dconf-profile.in b/tests/dconf-profile.in
new file mode 100644
index 0000000..431c95e
--- /dev/null
+++ b/tests/dconf-profile.in
@@ -0,0 +1,4 @@
+user-db:user
+
+# Path is relative to "/etc/dconf/db"
+system-db:../../../@abs_top_builddir@/tests/system-db
diff --git a/tests/schemas/Makefile.am b/tests/schemas/Makefile.am
new file mode 100644
index 0000000..d6e332a
--- /dev/null
+++ b/tests/schemas/Makefile.am
@@ -0,0 +1,11 @@
+gsettings_SCHEMAS = \
+ org.freedesktop.Application.Service.Test.gschema.xml \
+ $(NULL)
+
+gschemas.compiled: Makefile $(gsettings_SCHEMAS)
+ $(AM_V_GEN) $(GLIB_COMPILE_SCHEMAS) --targetdir=. $(srcdir)
+
+all-am: gschemas.compiled
+
+EXTRA_DIST = $(gsettings_SCHEMAS)
+CLEANFILES = gschemas.compiled
diff --git a/tests/schemas/org.freedesktop.Application.Service.Test.gschema.xml b/tests/schemas/org.freedesktop.Application.Service.Test.gschema.xml
new file mode 100644
index 0000000..8387801
--- /dev/null
+++ b/tests/schemas/org.freedesktop.Application.Service.Test.gschema.xml
@@ -0,0 +1,32 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<schemalist>
+
+ <schema id="org.freedesktop.Application.Service.Test" path="/org/freedesktop/Application/Service/Test/">
+ <key name="ro" type="i">
+ <default>100</default>
+ <summary>Summary</summary>
+ <description>Description</description>
+ </key>
+ <key name="rw" type="i">
+ <default>200</default>
+ <summary>Summary</summary>
+ <description>Description</description>
+ </key>
+ <child name="dir" schema="org.freedesktop.Application.Service.Test.dir"/>
+ </schema>
+
+ <schema id="org.freedesktop.Application.Service.Test.dir" path="/org/freedesktop/Application/Service/Test/dir/">
+ <key name="rw" type="i">
+ <default>300</default>
+ <summary>Summary</summary>
+ <description>Description</description>
+ </key>
+ <key name="locked" type="i">
+ <default>400</default>
+ <summary>Summary</summary>
+ <description>Description</description>
+ </key>
+ </schema>
+
+</schemalist>
+
diff --git a/tests/services/ca.desrt.dconf.service b/tests/services/ca.desrt.dconf.service
new file mode 100644
index 0000000..7ce3879
--- /dev/null
+++ b/tests/services/ca.desrt.dconf.service
@@ -0,0 +1,3 @@
+[D-BUS Service]
+Name=ca.desrt.dconf
+Exec=/usr/lib/dconf/dconf-service
diff --git a/tests/services/org.freedesktop.Application.Service.service.in b/tests/services/org.freedesktop.Application.Service.service.in
new file mode 100644
index 0000000..e24f75f
--- /dev/null
+++ b/tests/services/org.freedesktop.Application.Service.service.in
@@ -0,0 +1,3 @@
+[D-BUS Service]
+Name=org.freedesktop.Application.Service
+Exec=@abs_top_builddir@/src/appservice
diff --git a/tests/settings.c b/tests/settings.c
new file mode 100644
index 0000000..8e2b0ba
--- /dev/null
+++ b/tests/settings.c
@@ -0,0 +1,212 @@
+/*
+ * Copyright © 2015 Collabora ltd.
+ *
+ * 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 <http://www.gnu.org/licenses/>.
+ *
+ * Author: Xavier Claessens <xavier.claessens@collabora.com>
+ */
+
+#include <gio/gio.h>
+
+typedef struct
+{
+ gchar *dir;
+ GMainLoop *loop;
+ GTestDBus *dbus;
+ GApplication *app;
+ GQueue changes_expected;
+} Fixture;
+
+static void
+setup (Fixture *self,
+ gconstpointer user_data)
+{
+ GError *error = NULL;
+
+ /* Make sure dconf doesn't mess up user's settings */
+ self->dir = g_mkdtemp (g_build_filename (g_get_tmp_dir (),
+ "appservice-test-XXXXXX", NULL));
+ g_setenv ("XDG_CONFIG_HOME", self->dir, TRUE);
+
+ self->loop = g_main_loop_new (NULL, FALSE);
+
+ /* Spawn a private DBus session */
+ self->dbus = g_test_dbus_new (G_TEST_DBUS_NONE);
+ g_test_dbus_add_service_dir (self->dbus, TEST_BUILDDIR "/services");
+ g_test_dbus_add_service_dir (self->dbus, TEST_SRCDIR "/services");
+ g_test_dbus_up (self->dbus);
+
+ self->app = g_application_new ("org.freedesktop.Application.Servier.Test",
+ G_APPLICATION_FLAGS_NONE);
+ g_application_register (self->app, NULL, &error);
+ g_assert_no_error (error);
+}
+
+static void
+teardown (Fixture *self,
+ gconstpointer user_data)
+{
+ /* FIXME: rm -rf self->dir */
+
+ g_clear_pointer (&self->dir, g_free);
+ g_clear_pointer (&self->loop, g_main_loop_unref);
+ g_clear_object (&self->app);
+
+ g_test_dbus_down (self->dbus);
+ g_clear_object (&self->dbus);
+}
+
+static void
+changed_cb (GSettings *settings,
+ const gchar *key,
+ Fixture *self)
+{
+ GList *l;
+
+ l = g_queue_find_custom (&self->changes_expected, key,
+ (GCompareFunc) g_strcmp0);
+ if (l == NULL)
+ {
+ g_warning ("Received unexpected 'changed' signal for '%s'", key);
+ g_assert_not_reached ();
+ }
+
+ g_queue_delete_link (&self->changes_expected, l);
+ if (g_queue_is_empty (&self->changes_expected))
+ g_main_loop_quit (self->loop);
+}
+
+static void
+child_changed_cb (GSettings *settings,
+ const gchar *key,
+ Fixture *self)
+{
+ g_autofree gchar *full;
+
+ full = g_strdup_printf ("dir/%s", key);
+ changed_cb (settings, full, self);
+}
+
+static void
+modify_value (Fixture *self,
+ const gchar *key,
+ const gchar *value)
+{
+ g_autoptr(GSubprocessLauncher) launcher;
+ g_autoptr(GSubprocess) proc;
+ GError *error = NULL;
+
+ /* FIXME: Is there a better way to modify the key in dconf directly? */
+ launcher = g_subprocess_launcher_new (G_SUBPROCESS_FLAGS_NONE);
+ g_subprocess_launcher_setenv (launcher, "GSETTINGS_BACKEND", "dconf", TRUE);
+ proc = g_subprocess_launcher_spawn (launcher, &error,
+ "gsettings", "set", "org.freedesktop.Application.Service.Test",
+ key, value, NULL);
+ g_assert_no_error (error);
+
+ g_subprocess_wait_check (proc, NULL, &error);
+ g_assert_no_error (error);
+}
+
+static void
+test_settings (Fixture *self,
+ gconstpointer user_data)
+{
+ g_autoptr(GSettings) settings;
+ g_autoptr(GSettings) child_settings;
+
+ settings = g_settings_new ("org.freedesktop.Application.Service.Test");
+ g_signal_connect (settings, "changed",
+ G_CALLBACK (changed_cb), self);
+
+ child_settings = g_settings_get_child (settings, "dir");
+ g_signal_connect (child_settings, "changed",
+ G_CALLBACK (child_changed_cb), self);
+
+ /* Verify initial value */
+ g_assert_cmpint (g_settings_get_int (settings, "ro"), ==, 100);
+ g_assert_cmpint (g_settings_get_int (settings, "rw"), ==, -200);
+ g_assert_cmpint (g_settings_get_int (child_settings, "rw"), ==, -300);
+ g_assert_cmpint (g_settings_get_int (child_settings, "locked"), ==, 400);
+ g_assert_false (g_settings_is_writable (settings, "ro"));
+ g_assert_true (g_settings_is_writable (settings, "rw"));
+ g_assert_true (g_settings_is_writable (child_settings, "rw"));
+ g_assert_false (g_settings_is_writable (child_settings, "locked"));
+
+ /* Verify update notification if the key change from the outside */
+ g_queue_push_tail (&self->changes_expected, "rw");
+ g_queue_push_tail (&self->changes_expected, "ro");
+ modify_value (self, "ro", "101");
+ modify_value (self, "rw", "201");
+ g_main_loop_run (self->loop);
+ g_assert_cmpint (g_settings_get_int (settings, "ro"), ==, 101);
+ g_assert_cmpint (g_settings_get_int (settings, "rw"), ==, 201);
+ g_assert (g_queue_is_empty (&self->changes_expected));
+
+ /* Verify we can change a writable key */
+ g_queue_push_tail (&self->changes_expected, "rw");
+ g_queue_push_tail (&self->changes_expected, "dir/rw");
+ g_settings_set_int (settings, "rw", 202);
+ g_settings_set_int (child_settings, "rw", 300);
+ g_assert_cmpint (g_settings_get_int (settings, "rw"), ==, 202);
+ g_assert (g_queue_is_empty (&self->changes_expected));
+
+ /* Verify we cannot change a readonly key */
+ g_settings_set_int (settings, "ro", 102);
+ g_settings_set_int (child_settings, "locked", 401);
+ g_assert_cmpint (g_settings_get_int (settings, "ro"), ==, 101);
+ g_assert_cmpint (g_settings_get_int (child_settings, "locked"), ==, 400);
+
+ /* Verify that resetting a dir only reset writable keys */
+ g_queue_push_tail (&self->changes_expected, "ro");
+ g_queue_push_tail (&self->changes_expected, "rw");
+ g_queue_push_tail (&self->changes_expected, "dir/locked");
+ g_queue_push_tail (&self->changes_expected, "dir/rw");
+ g_settings_reset (settings, "");
+ g_assert_cmpint (g_settings_get_int (settings, "ro"), ==, 101);
+ g_assert_cmpint (g_settings_get_int (settings, "rw"), ==, -200);
+ g_assert_cmpint (g_settings_get_int (child_settings, "rw"), ==, -300);
+ g_assert_cmpint (g_settings_get_int (child_settings, "locked"), ==, 400);
+ g_assert (g_queue_is_empty (&self->changes_expected));
+
+ /* Verify that resetting a readonly key has no effect */
+ g_queue_push_tail (&self->changes_expected, "ro");
+ g_settings_reset (settings, "ro");
+ g_assert_cmpint (g_settings_get_int (settings, "ro"), ==, 101);
+ g_assert (g_queue_is_empty (&self->changes_expected));
+
+ /* Verify everything is still has expected after the sync */
+ g_settings_sync ();
+ g_assert_cmpint (g_settings_get_int (settings, "rw"), ==, -200);
+ g_assert_cmpint (g_settings_get_int (settings, "ro"), ==, 101);
+ g_assert_cmpint (g_settings_get_int (child_settings, "rw"), ==, -300);
+ g_assert_cmpint (g_settings_get_int (child_settings, "locked"), ==, 400);
+}
+
+gint
+main (gint argc, gchar *argv[])
+{
+ g_test_init (&argc, &argv, NULL);
+
+ /* Setup env to use appservice's GSettingsBackend */
+ g_setenv ("GIO_EXTRA_MODULES", TEST_MODULES, TRUE);
+ g_setenv ("GSETTINGS_SCHEMA_DIR", TEST_BUILDDIR "/schemas", TRUE);
+ g_setenv ("DCONF_PROFILE", TEST_BUILDDIR "/dconf-profile", TRUE);
+ g_setenv ("APPSERVICE_CONF_PATH", TEST_SRCDIR "/appservice.conf", TRUE);
+
+ g_test_add("/appservice/settings", Fixture, NULL,
+ setup, test_settings, teardown);
+
+ return g_test_run();
+}