diff options
author | Xavier Claessens <xavier.claessens@collabora.com> | 2015-11-20 16:01:00 (GMT) |
---|---|---|
committer | Xavier Claessens <xavier.claessens@collabora.com> | 2015-12-22 15:58:53 (GMT) |
commit | ed11cbe8effda26a187f0a3fd3627488fe41915b (patch) | |
tree | 1da9d735467bbef3284ea0066c685056d7fbe8fd | |
parent | b23bc550ac33477f6547e31fa29920868211ebdc (diff) | |
download | appservice-master.tar.gz appservice-master.tar.xz |
29 files changed, 2085 insertions, 543 deletions
@@ -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> @@ -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(); +} |