glib/gio/gmenuexporter.c
Ryan Lortie 212ffcc574 GMenuExporter: allow NULL bus on _name_vanished
GBusNameVanishedCallback is called with a NULL GDBusConnection in the
case that the connection has vanished.  We were doing an assert to
verify that it was the same as we had exported the menu on and that
assert was failing.

https://bugzilla.gnome.org/show_bug.cgi?id=685995
2012-10-16 12:31:23 -04:00

803 lines
24 KiB
C

/*
* Copyright © 2011 Canonical 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, write to the Free Software
* Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307,
* USA.
*
* Author: Ryan Lortie <desrt@desrt.ca>
*/
#include "gmenuexporter.h"
#include "gdbusmethodinvocation.h"
#include "gdbusintrospection.h"
#include "gdbusnamewatching.h"
#include "gdbuserror.h"
/**
* SECTION:gmenuexporter
* @title: GMenuModel exporter
* @short_description: Export GMenuModels on D-Bus
* @see_also: #GMenuModel, #GDBusMenuModel
*
* These functions support exporting a #GMenuModel on D-Bus.
* The D-Bus interface that is used is a private implementation
* detail.
*
* To access an exported #GMenuModel remotely, use
* g_dbus_menu_model_get() to obtain a #GDBusMenuModel.
*/
/* {{{1 D-Bus Interface description */
/* For documentation of this interface, see
* http://live.gnome.org/GTK+/GApplication-dbus-apis
*/
static GDBusInterfaceInfo *
org_gtk_Menus_get_interface (void)
{
static GDBusInterfaceInfo *interface_info;
if (interface_info == NULL)
{
GError *error = NULL;
GDBusNodeInfo *info;
info = g_dbus_node_info_new_for_xml ("<node>"
" <interface name='org.gtk.Menus'>"
" <method name='Start'>"
" <arg type='au' name='groups' direction='in'/>"
" <arg type='a(uuaa{sv})' name='content' direction='out'/>"
" </method>"
" <method name='End'>"
" <arg type='au' name='groups' direction='in'/>"
" </method>"
" <signal name='Changed'>"
" arg type='a(uuuuaa{sv})' name='changes'/>"
" </signal>"
" </interface>"
"</node>", &error);
if (info == NULL)
g_error ("%s\n", error->message);
interface_info = g_dbus_node_info_lookup_interface (info, "org.gtk.Menus");
g_assert (interface_info != NULL);
g_dbus_interface_info_ref (interface_info);
g_dbus_node_info_unref (info);
}
return interface_info;
}
/* {{{1 Forward declarations */
typedef struct _GMenuExporterMenu GMenuExporterMenu;
typedef struct _GMenuExporterLink GMenuExporterLink;
typedef struct _GMenuExporterGroup GMenuExporterGroup;
typedef struct _GMenuExporterRemote GMenuExporterRemote;
typedef struct _GMenuExporterWatch GMenuExporterWatch;
typedef struct _GMenuExporter GMenuExporter;
static gboolean g_menu_exporter_group_is_subscribed (GMenuExporterGroup *group);
static guint g_menu_exporter_group_get_id (GMenuExporterGroup *group);
static GMenuExporter * g_menu_exporter_group_get_exporter (GMenuExporterGroup *group);
static GMenuExporterMenu * g_menu_exporter_group_add_menu (GMenuExporterGroup *group,
GMenuModel *model);
static void g_menu_exporter_group_remove_menu (GMenuExporterGroup *group,
guint id);
static GMenuExporterGroup * g_menu_exporter_create_group (GMenuExporter *exporter);
static GMenuExporterGroup * g_menu_exporter_lookup_group (GMenuExporter *exporter,
guint group_id);
static void g_menu_exporter_report (GMenuExporter *exporter,
GVariant *report);
static void g_menu_exporter_remove_group (GMenuExporter *exporter,
guint id);
/* {{{1 GMenuExporterLink, GMenuExporterMenu */
struct _GMenuExporterMenu
{
GMenuExporterGroup *group;
guint id;
GMenuModel *model;
gulong handler_id;
GSequence *item_links;
};
struct _GMenuExporterLink
{
gchar *name;
GMenuExporterMenu *menu;
GMenuExporterLink *next;
};
static void
g_menu_exporter_menu_free (GMenuExporterMenu *menu)
{
g_menu_exporter_group_remove_menu (menu->group, menu->id);
if (menu->handler_id != 0)
g_signal_handler_disconnect (menu->model, menu->handler_id);
if (menu->item_links != NULL)
g_sequence_free (menu->item_links);
g_object_unref (menu->model);
g_slice_free (GMenuExporterMenu, menu);
}
static void
g_menu_exporter_link_free (gpointer data)
{
GMenuExporterLink *link = data;
while (link != NULL)
{
GMenuExporterLink *tmp = link;
link = tmp->next;
g_menu_exporter_menu_free (tmp->menu);
g_free (tmp->name);
g_slice_free (GMenuExporterLink, tmp);
}
}
static GMenuExporterLink *
g_menu_exporter_menu_create_links (GMenuExporterMenu *menu,
gint position)
{
GMenuExporterLink *list = NULL;
GMenuLinkIter *iter;
const char *name;
GMenuModel *model;
iter = g_menu_model_iterate_item_links (menu->model, position);
while (g_menu_link_iter_get_next (iter, &name, &model))
{
GMenuExporterGroup *group;
GMenuExporterLink *tmp;
/* keep sections in the same group, but create new groups
* otherwise
*/
if (!g_str_equal (name, "section"))
group = g_menu_exporter_create_group (g_menu_exporter_group_get_exporter (menu->group));
else
group = menu->group;
tmp = g_slice_new (GMenuExporterLink);
tmp->name = g_strconcat (":", name, NULL);
tmp->menu = g_menu_exporter_group_add_menu (group, model);
tmp->next = list;
list = tmp;
g_object_unref (model);
}
g_object_unref (iter);
return list;
}
static GVariant *
g_menu_exporter_menu_describe_item (GMenuExporterMenu *menu,
gint position)
{
GMenuAttributeIter *attr_iter;
GVariantBuilder builder;
GSequenceIter *iter;
GMenuExporterLink *link;
const char *name;
GVariant *value;
g_variant_builder_init (&builder, G_VARIANT_TYPE_VARDICT);
attr_iter = g_menu_model_iterate_item_attributes (menu->model, position);
while (g_menu_attribute_iter_get_next (attr_iter, &name, &value))
{
g_variant_builder_add (&builder, "{sv}", name, value);
g_variant_unref (value);
}
g_object_unref (attr_iter);
iter = g_sequence_get_iter_at_pos (menu->item_links, position);
for (link = g_sequence_get (iter); link; link = link->next)
g_variant_builder_add (&builder, "{sv}", link->name,
g_variant_new ("(uu)", g_menu_exporter_group_get_id (link->menu->group), link->menu->id));
return g_variant_builder_end (&builder);
}
static GVariant *
g_menu_exporter_menu_list (GMenuExporterMenu *menu)
{
GVariantBuilder builder;
gint i, n;
g_variant_builder_init (&builder, G_VARIANT_TYPE ("aa{sv}"));
n = g_sequence_get_length (menu->item_links);
for (i = 0; i < n; i++)
g_variant_builder_add_value (&builder, g_menu_exporter_menu_describe_item (menu, i));
return g_variant_builder_end (&builder);
}
static void
g_menu_exporter_menu_items_changed (GMenuModel *model,
gint position,
gint removed,
gint added,
gpointer user_data)
{
GMenuExporterMenu *menu = user_data;
GSequenceIter *point;
gint i;
g_assert (menu->model == model);
g_assert (menu->item_links != NULL);
g_assert (position + removed <= g_sequence_get_length (menu->item_links));
point = g_sequence_get_iter_at_pos (menu->item_links, position + removed);
g_sequence_remove_range (g_sequence_get_iter_at_pos (menu->item_links, position), point);
for (i = position; i < position + added; i++)
g_sequence_insert_before (point, g_menu_exporter_menu_create_links (menu, i));
if (g_menu_exporter_group_is_subscribed (menu->group))
{
GVariantBuilder builder;
g_variant_builder_init (&builder, G_VARIANT_TYPE ("(uuuuaa{sv})"));
g_variant_builder_add (&builder, "u", g_menu_exporter_group_get_id (menu->group));
g_variant_builder_add (&builder, "u", menu->id);
g_variant_builder_add (&builder, "u", position);
g_variant_builder_add (&builder, "u", removed);
g_variant_builder_open (&builder, G_VARIANT_TYPE ("aa{sv}"));
for (i = position; i < position + added; i++)
g_variant_builder_add_value (&builder, g_menu_exporter_menu_describe_item (menu, i));
g_variant_builder_close (&builder);
g_menu_exporter_report (g_menu_exporter_group_get_exporter (menu->group), g_variant_builder_end (&builder));
}
}
static void
g_menu_exporter_menu_prepare (GMenuExporterMenu *menu)
{
gint n_items;
g_assert (menu->item_links == NULL);
if (g_menu_model_is_mutable (menu->model))
menu->handler_id = g_signal_connect (menu->model, "items-changed",
G_CALLBACK (g_menu_exporter_menu_items_changed), menu);
menu->item_links = g_sequence_new (g_menu_exporter_link_free);
n_items = g_menu_model_get_n_items (menu->model);
if (n_items)
g_menu_exporter_menu_items_changed (menu->model, 0, 0, n_items, menu);
}
static GMenuExporterMenu *
g_menu_exporter_menu_new (GMenuExporterGroup *group,
guint id,
GMenuModel *model)
{
GMenuExporterMenu *menu;
menu = g_slice_new0 (GMenuExporterMenu);
menu->group = group;
menu->id = id;
menu->model = g_object_ref (model);
return menu;
}
/* {{{1 GMenuExporterGroup */
struct _GMenuExporterGroup
{
GMenuExporter *exporter;
guint id;
GHashTable *menus;
guint next_menu_id;
gboolean prepared;
gint subscribed;
};
static void
g_menu_exporter_group_check_if_useless (GMenuExporterGroup *group)
{
if (g_hash_table_size (group->menus) == 0 && group->subscribed == 0)
{
g_menu_exporter_remove_group (group->exporter, group->id);
g_hash_table_unref (group->menus);
g_slice_free (GMenuExporterGroup, group);
}
}
static void
g_menu_exporter_group_subscribe (GMenuExporterGroup *group,
GVariantBuilder *builder)
{
GHashTableIter iter;
gpointer key, val;
if (!group->prepared)
{
GMenuExporterMenu *menu;
/* set this first, so that any menus created during the
* preparation of the first menu also end up in the prepared
* state.
* */
group->prepared = TRUE;
menu = g_hash_table_lookup (group->menus, 0);
g_menu_exporter_menu_prepare (menu);
}
group->subscribed++;
g_hash_table_iter_init (&iter, group->menus);
while (g_hash_table_iter_next (&iter, &key, &val))
{
guint id = GPOINTER_TO_INT (key);
GMenuExporterMenu *menu = val;
if (g_sequence_get_length (menu->item_links))
{
g_variant_builder_open (builder, G_VARIANT_TYPE ("(uuaa{sv})"));
g_variant_builder_add (builder, "u", group->id);
g_variant_builder_add (builder, "u", id);
g_variant_builder_add_value (builder, g_menu_exporter_menu_list (menu));
g_variant_builder_close (builder);
}
}
}
static void
g_menu_exporter_group_unsubscribe (GMenuExporterGroup *group,
gint count)
{
g_assert (group->subscribed >= count);
group->subscribed -= count;
g_menu_exporter_group_check_if_useless (group);
}
static GMenuExporter *
g_menu_exporter_group_get_exporter (GMenuExporterGroup *group)
{
return group->exporter;
}
static gboolean
g_menu_exporter_group_is_subscribed (GMenuExporterGroup *group)
{
return group->subscribed > 0;
}
static guint
g_menu_exporter_group_get_id (GMenuExporterGroup *group)
{
return group->id;
}
static void
g_menu_exporter_group_remove_menu (GMenuExporterGroup *group,
guint id)
{
g_hash_table_remove (group->menus, GINT_TO_POINTER (id));
g_menu_exporter_group_check_if_useless (group);
}
static GMenuExporterMenu *
g_menu_exporter_group_add_menu (GMenuExporterGroup *group,
GMenuModel *model)
{
GMenuExporterMenu *menu;
guint id;
id = group->next_menu_id++;
menu = g_menu_exporter_menu_new (group, id, model);
g_hash_table_insert (group->menus, GINT_TO_POINTER (id), menu);
if (group->prepared)
g_menu_exporter_menu_prepare (menu);
return menu;
}
static GMenuExporterGroup *
g_menu_exporter_group_new (GMenuExporter *exporter,
guint id)
{
GMenuExporterGroup *group;
group = g_slice_new0 (GMenuExporterGroup);
group->menus = g_hash_table_new (NULL, NULL);
group->exporter = exporter;
group->id = id;
return group;
}
/* {{{1 GMenuExporterRemote */
struct _GMenuExporterRemote
{
GMenuExporter *exporter;
GHashTable *watches;
guint watch_id;
};
static void
g_menu_exporter_remote_subscribe (GMenuExporterRemote *remote,
guint group_id,
GVariantBuilder *builder)
{
GMenuExporterGroup *group;
guint count;
count = (gsize) g_hash_table_lookup (remote->watches, GINT_TO_POINTER (group_id));
g_hash_table_insert (remote->watches, GINT_TO_POINTER (group_id), GINT_TO_POINTER (count + 1));
group = g_menu_exporter_lookup_group (remote->exporter, group_id);
g_menu_exporter_group_subscribe (group, builder);
}
static void
g_menu_exporter_remote_unsubscribe (GMenuExporterRemote *remote,
guint group_id)
{
GMenuExporterGroup *group;
guint count;
count = (gsize) g_hash_table_lookup (remote->watches, GINT_TO_POINTER (group_id));
if (count == 0)
return;
if (count != 1)
g_hash_table_insert (remote->watches, GINT_TO_POINTER (group_id), GINT_TO_POINTER (count - 1));
else
g_hash_table_remove (remote->watches, GINT_TO_POINTER (group_id));
group = g_menu_exporter_lookup_group (remote->exporter, group_id);
g_menu_exporter_group_unsubscribe (group, 1);
}
static gboolean
g_menu_exporter_remote_has_subscriptions (GMenuExporterRemote *remote)
{
return g_hash_table_size (remote->watches) != 0;
}
static void
g_menu_exporter_remote_free (gpointer data)
{
GMenuExporterRemote *remote = data;
GHashTableIter iter;
gpointer key, val;
g_hash_table_iter_init (&iter, remote->watches);
while (g_hash_table_iter_next (&iter, &key, &val))
{
GMenuExporterGroup *group;
group = g_menu_exporter_lookup_group (remote->exporter, GPOINTER_TO_INT (key));
g_menu_exporter_group_unsubscribe (group, GPOINTER_TO_INT (val));
}
g_bus_unwatch_name (remote->watch_id);
g_hash_table_unref (remote->watches);
g_slice_free (GMenuExporterRemote, remote);
}
static GMenuExporterRemote *
g_menu_exporter_remote_new (GMenuExporter *exporter,
guint watch_id)
{
GMenuExporterRemote *remote;
remote = g_slice_new0 (GMenuExporterRemote);
remote->exporter = exporter;
remote->watches = g_hash_table_new (NULL, NULL);
remote->watch_id = watch_id;
return remote;
}
/* {{{1 GMenuExporter */
struct _GMenuExporter
{
GDBusConnection *connection;
gchar *object_path;
guint registration_id;
GHashTable *groups;
guint next_group_id;
GMenuExporterMenu *root;
GHashTable *remotes;
};
static void
g_menu_exporter_name_vanished (GDBusConnection *connection,
const gchar *name,
gpointer user_data)
{
GMenuExporter *exporter = user_data;
/* connection == NULL when we get called because the connection closed */
g_assert (exporter->connection == connection || connection == NULL);
g_hash_table_remove (exporter->remotes, name);
}
static GVariant *
g_menu_exporter_subscribe (GMenuExporter *exporter,
const gchar *sender,
GVariant *group_ids)
{
GMenuExporterRemote *remote;
GVariantBuilder builder;
GVariantIter iter;
guint32 id;
remote = g_hash_table_lookup (exporter->remotes, sender);
if (remote == NULL)
{
guint watch_id;
watch_id = g_bus_watch_name_on_connection (exporter->connection, sender, G_BUS_NAME_WATCHER_FLAGS_NONE,
NULL, g_menu_exporter_name_vanished, exporter, NULL);
remote = g_menu_exporter_remote_new (exporter, watch_id);
g_hash_table_insert (exporter->remotes, g_strdup (sender), remote);
}
g_variant_builder_init (&builder, G_VARIANT_TYPE ("(a(uuaa{sv}))"));
g_variant_builder_open (&builder, G_VARIANT_TYPE ("a(uuaa{sv})"));
g_variant_iter_init (&iter, group_ids);
while (g_variant_iter_next (&iter, "u", &id))
g_menu_exporter_remote_subscribe (remote, id, &builder);
g_variant_builder_close (&builder);
return g_variant_builder_end (&builder);
}
static void
g_menu_exporter_unsubscribe (GMenuExporter *exporter,
const gchar *sender,
GVariant *group_ids)
{
GMenuExporterRemote *remote;
GVariantIter iter;
guint32 id;
remote = g_hash_table_lookup (exporter->remotes, sender);
if (remote == NULL)
return;
g_variant_iter_init (&iter, group_ids);
while (g_variant_iter_next (&iter, "u", &id))
g_menu_exporter_remote_unsubscribe (remote, id);
if (!g_menu_exporter_remote_has_subscriptions (remote))
g_hash_table_remove (exporter->remotes, sender);
}
static void
g_menu_exporter_report (GMenuExporter *exporter,
GVariant *report)
{
GVariantBuilder builder;
g_variant_builder_init (&builder, G_VARIANT_TYPE_TUPLE);
g_variant_builder_open (&builder, G_VARIANT_TYPE_ARRAY);
g_variant_builder_add_value (&builder, report);
g_variant_builder_close (&builder);
g_dbus_connection_emit_signal (exporter->connection,
NULL,
exporter->object_path,
"org.gtk.Menus", "Changed",
g_variant_builder_end (&builder),
NULL);
}
static void
g_menu_exporter_remove_group (GMenuExporter *exporter,
guint id)
{
g_hash_table_remove (exporter->groups, GINT_TO_POINTER (id));
}
static GMenuExporterGroup *
g_menu_exporter_lookup_group (GMenuExporter *exporter,
guint group_id)
{
GMenuExporterGroup *group;
group = g_hash_table_lookup (exporter->groups, GINT_TO_POINTER (group_id));
if (group == NULL)
{
group = g_menu_exporter_group_new (exporter, group_id);
g_hash_table_insert (exporter->groups, GINT_TO_POINTER (group_id), group);
}
return group;
}
static GMenuExporterGroup *
g_menu_exporter_create_group (GMenuExporter *exporter)
{
GMenuExporterGroup *group;
guint id;
id = exporter->next_group_id++;
group = g_menu_exporter_group_new (exporter, id);
g_hash_table_insert (exporter->groups, GINT_TO_POINTER (id), group);
return group;
}
static void
g_menu_exporter_free (gpointer user_data)
{
GMenuExporter *exporter = user_data;
g_menu_exporter_menu_free (exporter->root);
g_hash_table_unref (exporter->remotes);
g_hash_table_unref (exporter->groups);
g_object_unref (exporter->connection);
g_free (exporter->object_path);
g_slice_free (GMenuExporter, exporter);
}
static void
g_menu_exporter_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)
{
GMenuExporter *exporter = user_data;
GVariant *group_ids;
group_ids = g_variant_get_child_value (parameters, 0);
if (g_str_equal (method_name, "Start"))
g_dbus_method_invocation_return_value (invocation, g_menu_exporter_subscribe (exporter, sender, group_ids));
else if (g_str_equal (method_name, "End"))
{
g_menu_exporter_unsubscribe (exporter, sender, group_ids);
g_dbus_method_invocation_return_value (invocation, NULL);
}
else
g_assert_not_reached ();
g_variant_unref (group_ids);
}
/* {{{1 Public API */
/**
* g_dbus_connection_export_menu_model:
* @connection: a #GDBusConnection
* @object_path: a D-Bus object path
* @menu: a #GMenuModel
* @error: return location for an error, or %NULL
*
* Exports @menu on @connection at @object_path.
*
* The implemented D-Bus API should be considered private.
* It is subject to change in the future.
*
* An object path can only have one action group exported on it. If this
* constraint is violated, the export will fail and 0 will be
* returned (with @error set accordingly).
*
* You can unexport the menu model using
* g_dbus_connection_unexport_menu_model() with the return value of
* this function.
*
* Returns: the ID of the export (never zero), or 0 in case of failure
*
* Since: 2.32
*/
guint
g_dbus_connection_export_menu_model (GDBusConnection *connection,
const gchar *object_path,
GMenuModel *menu,
GError **error)
{
const GDBusInterfaceVTable vtable = {
g_menu_exporter_method_call,
};
GMenuExporter *exporter;
guint id;
exporter = g_slice_new0 (GMenuExporter);
id = g_dbus_connection_register_object (connection, object_path, org_gtk_Menus_get_interface (),
&vtable, exporter, g_menu_exporter_free, error);
if (id == 0)
{
g_slice_free (GMenuExporter, exporter);
return 0;
}
exporter->connection = g_object_ref (connection);
exporter->object_path = g_strdup (object_path);
exporter->groups = g_hash_table_new (NULL, NULL);
exporter->remotes = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, g_menu_exporter_remote_free);
exporter->root = g_menu_exporter_group_add_menu (g_menu_exporter_create_group (exporter), menu);
return id;
}
/**
* g_dbus_connection_unexport_menu_model:
* @connection: a #GDBusConnection
* @export_id: the ID from g_dbus_connection_export_menu_model()
*
* Reverses the effect of a previous call to
* g_dbus_connection_export_menu_model().
*
* It is an error to call this function with an ID that wasn't returned
* from g_dbus_connection_export_menu_model() or to call it with the
* same ID more than once.
*
* Since: 2.32
*/
void
g_dbus_connection_unexport_menu_model (GDBusConnection *connection,
guint export_id)
{
g_dbus_connection_unregister_object (connection, export_id);
}
/* {{{1 Epilogue */
/* vim:set foldmethod=marker: */