From 98c5556a14454fa988b0b81ffb22cef9a67011e9 Mon Sep 17 00:00:00 2001 From: Lars Uebernickel Date: Fri, 2 Aug 2013 12:06:55 +0200 Subject: Use bus_watch_namespace() to monitor mpris players This function is more robust than the current code and uses glib's G_DBUS_SIGNAL_FLAGS_MATCH_ARG0_NAMESPACE instead of creating match rules itself. bus-watch-namespace.[ch] is shared with gnome-settings-daemon. --- src/Makefile.am | 6 +- src/bus-watch-namespace.c | 347 +++++++++++++++++++++++++++++++++++++++++++++ src/bus-watch-namespace.h | 34 +++++ src/media-player-list.vala | 22 +-- src/mpris2-watcher.vala | 200 -------------------------- vapi/bus-watcher.vapi | 25 ++++ 6 files changed, 423 insertions(+), 211 deletions(-) create mode 100644 src/bus-watch-namespace.c create mode 100644 src/bus-watch-namespace.h delete mode 100644 src/mpris2-watcher.vala create mode 100644 vapi/bus-watcher.vapi diff --git a/src/Makefile.am b/src/Makefile.am index 8af02ce..1a82a18 100644 --- a/src/Makefile.am +++ b/src/Makefile.am @@ -7,9 +7,10 @@ indicator_sound_service_SOURCES = \ media-player.vala \ media-player-list.vala \ mpris2-interfaces.vala \ - mpris2-watcher.vala \ freedesktop-interfaces.vala \ - sound-menu.vala + sound-menu.vala \ + bus-watch-namespace.c \ + bus-watch-namespace.h indicator_sound_service_VALAFLAGS = \ --ccode \ @@ -22,6 +23,7 @@ indicator_sound_service_VALAFLAGS = \ --pkg libxml-2.0 \ --pkg libpulse \ --pkg libpulse-mainloop-glib \ + --pkg bus-watcher \ --target-glib=2.36 # -w to disable warnings for vala-generated code diff --git a/src/bus-watch-namespace.c b/src/bus-watch-namespace.c new file mode 100644 index 0000000..1ffdff4 --- /dev/null +++ b/src/bus-watch-namespace.c @@ -0,0 +1,347 @@ +/* + * Copyright 2013 Canonical Ltd. + * + * This program 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 + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + * + * Author: Lars Uebernickel + */ + +#include +#include +#include "bus-watch-namespace.h" + +typedef struct +{ + guint id; + gchar *name_space; + GBusNameAppearedCallback appeared_handler; + GBusNameVanishedCallback vanished_handler; + gpointer user_data; + GDestroyNotify user_data_destroy; + + GDBusConnection *connection; + GCancellable *cancellable; + GHashTable *names; + guint subscription_id; +} NamespaceWatcher; + +typedef struct +{ + NamespaceWatcher *watcher; + gchar *name; +} GetNameOwnerData; + +static guint namespace_watcher_next_id; +static GHashTable *namespace_watcher_watchers; + +static void +namespace_watcher_stop (gpointer data) +{ + NamespaceWatcher *watcher = data; + + g_cancellable_cancel (watcher->cancellable); + g_object_unref (watcher->cancellable); + + if (watcher->subscription_id) + g_dbus_connection_signal_unsubscribe (watcher->connection, watcher->subscription_id); + + if (watcher->vanished_handler) + { + GHashTableIter it; + const gchar *name; + + g_hash_table_iter_init (&it, watcher->names); + while (g_hash_table_iter_next (&it, (gpointer *) &name, NULL)) + watcher->vanished_handler (watcher->connection, name, watcher->user_data); + } + + if (watcher->user_data_destroy) + watcher->user_data_destroy (watcher->user_data); + + if (watcher->connection) + { + g_signal_handlers_disconnect_by_func (watcher->connection, namespace_watcher_stop, watcher); + g_object_unref (watcher->connection); + } + + g_hash_table_unref (watcher->names); + + g_hash_table_remove (namespace_watcher_watchers, GUINT_TO_POINTER (watcher->id)); + if (g_hash_table_size (namespace_watcher_watchers) == 0) + g_clear_pointer (&namespace_watcher_watchers, g_hash_table_destroy); + + g_free (watcher); +} + +static void +namespace_watcher_name_appeared (NamespaceWatcher *watcher, + const gchar *name, + const gchar *owner) +{ + /* There's a race between NameOwnerChanged signals arriving and the + * ListNames/GetNameOwner sequence returning, so this function might + * be called more than once for the same name. To ensure that + * appeared_handler is only called once for each name, it is only + * called when inserting the name into watcher->names (each name is + * only inserted once there). + */ + if (g_hash_table_contains (watcher->names, name)) + return; + + g_hash_table_add (watcher->names, g_strdup (name)); + + if (watcher->appeared_handler) + watcher->appeared_handler (watcher->connection, name, owner, watcher->user_data); +} + +static void +namespace_watcher_name_vanished (NamespaceWatcher *watcher, + const gchar *name) +{ + if (g_hash_table_remove (watcher->names, name) && watcher->vanished_handler) + watcher->vanished_handler (watcher->connection, name, watcher->user_data); +} + +static gboolean +dbus_name_has_namespace (const gchar *name, + const gchar *name_space) +{ + gint len_name; + gint len_namespace; + + len_name = strlen (name); + len_namespace = strlen (name_space); + + if (len_name < len_namespace) + return FALSE; + + if (memcmp (name_space, name, len_namespace) != 0) + return FALSE; + + return len_namespace == len_name || name[len_namespace] == '.'; +} + +static void +name_owner_changed (GDBusConnection *connection, + const gchar *sender_name, + const gchar *object_path, + const gchar *interface_name, + const gchar *signal_name, + GVariant *parameters, + gpointer user_data) +{ + NamespaceWatcher *watcher = user_data; + const gchar *name; + const gchar *old_owner; + const gchar *new_owner; + + g_variant_get (parameters, "(&s&s&s)", &name, &old_owner, &new_owner); + + if (old_owner[0] != '\0') + namespace_watcher_name_vanished (watcher, name); + + if (new_owner[0] != '\0') + namespace_watcher_name_appeared (watcher, name, new_owner); +} + +static void +got_name_owner (GObject *object, + GAsyncResult *result, + gpointer user_data) +{ + GetNameOwnerData *data = user_data; + GError *error = NULL; + GVariant *reply; + const gchar *owner; + + reply = g_dbus_connection_call_finish (G_DBUS_CONNECTION (object), result, &error); + + if (g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED)) + { + g_error_free (error); + goto out; + } + + if (reply == NULL) + { + if (!g_error_matches (error, G_DBUS_ERROR, G_DBUS_ERROR_NAME_HAS_NO_OWNER)) + g_warning ("bus_watch_namespace: error calling org.freedesktop.DBus.GetNameOwner: %s", error->message); + g_error_free (error); + goto out; + } + + g_variant_get (reply, "(&s)", &owner); + namespace_watcher_name_appeared (data->watcher, data->name, owner); + + g_variant_unref (reply); + +out: + g_free (data->name); + g_slice_free (GetNameOwnerData, data); +} + +static void +names_listed (GObject *object, + GAsyncResult *result, + gpointer user_data) +{ + NamespaceWatcher *watcher; + GError *error = NULL; + GVariant *reply; + GVariantIter *iter; + const gchar *name; + + reply = g_dbus_connection_call_finish (G_DBUS_CONNECTION (object), result, &error); + + if (g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED)) + { + g_error_free (error); + return; + } + + watcher = user_data; + + if (reply == NULL) + { + g_warning ("bus_watch_namespace: error calling org.freedesktop.DBus.ListNames: %s", error->message); + g_error_free (error); + return; + } + + g_variant_get (reply, "(as)", &iter); + while (g_variant_iter_next (iter, "&s", &name)) + { + if (dbus_name_has_namespace (name, watcher->name_space)) + { + GetNameOwnerData *data = g_slice_new (GetNameOwnerData); + data->watcher = watcher; + data->name = g_strdup (name); + g_dbus_connection_call (watcher->connection, "org.freedesktop.DBus", "/", + "org.freedesktop.DBus", "GetNameOwner", + g_variant_new ("(s)", name), G_VARIANT_TYPE ("(s)"), + G_DBUS_CALL_FLAGS_NONE, -1, watcher->cancellable, + got_name_owner, data); + } + } + + g_variant_iter_free (iter); + g_variant_unref (reply); +} + +static void +connection_closed (GDBusConnection *connection, + gboolean remote_peer_vanished, + GError *error, + gpointer user_data) +{ + NamespaceWatcher *watcher = user_data; + + namespace_watcher_stop (watcher); +} + +static void +got_bus (GObject *object, + GAsyncResult *result, + gpointer user_data) +{ + GDBusConnection *connection; + NamespaceWatcher *watcher; + GError *error = NULL; + + connection = g_bus_get_finish (result, &error); + + if (g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED)) + { + g_error_free (error); + return; + } + + watcher = user_data; + + if (connection == NULL) + { + namespace_watcher_stop (watcher); + return; + } + + watcher->connection = connection; + g_signal_connect (watcher->connection, "closed", G_CALLBACK (connection_closed), watcher); + + watcher->subscription_id = + g_dbus_connection_signal_subscribe (watcher->connection, "org.freedesktop.DBus", + "org.freedesktop.DBus", "NameOwnerChanged", "/org/freedesktop/DBus", + watcher->name_space, G_DBUS_SIGNAL_FLAGS_MATCH_ARG0_NAMESPACE, + name_owner_changed, watcher, NULL); + + g_dbus_connection_call (watcher->connection, "org.freedesktop.DBus", "/", + "org.freedesktop.DBus", "ListNames", NULL, G_VARIANT_TYPE ("(as)"), + G_DBUS_CALL_FLAGS_NONE, -1, watcher->cancellable, + names_listed, watcher); +} + +guint +bus_watch_namespace (GBusType bus_type, + const gchar *name_space, + GBusNameAppearedCallback appeared_handler, + GBusNameVanishedCallback vanished_handler, + gpointer user_data, + GDestroyNotify user_data_destroy) +{ + NamespaceWatcher *watcher; + + /* same rules for interfaces and well-known names */ + g_return_val_if_fail (name_space != NULL && g_dbus_is_interface_name (name_space), 0); + g_return_val_if_fail (appeared_handler || vanished_handler, 0); + + watcher = g_new0 (NamespaceWatcher, 1); + watcher->id = namespace_watcher_next_id++; + watcher->name_space = g_strdup (name_space); + watcher->appeared_handler = appeared_handler; + watcher->vanished_handler = vanished_handler; + watcher->user_data = user_data; + watcher->user_data_destroy = user_data_destroy; + watcher->cancellable = g_cancellable_new ();; + watcher->names = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, NULL); + + if (namespace_watcher_watchers == NULL) + namespace_watcher_watchers = g_hash_table_new (g_direct_hash, g_direct_equal); + g_hash_table_insert (namespace_watcher_watchers, GUINT_TO_POINTER (watcher->id), watcher); + + g_bus_get (bus_type, watcher->cancellable, got_bus, watcher); + + return watcher->id; +} + +void +bus_unwatch_namespace (guint id) +{ + /* namespace_watcher_stop() might have already removed the watcher + * with @id in the case of a connection error. Thus, this function + * doesn't warn when @id is absent from the hash table. + */ + + if (namespace_watcher_watchers) + { + NamespaceWatcher *watcher; + + watcher = g_hash_table_lookup (namespace_watcher_watchers, GUINT_TO_POINTER (id)); + if (watcher) + { + /* make sure vanished() is not called as a result of this function */ + g_hash_table_remove_all (watcher->names); + + namespace_watcher_stop (watcher); + } + } +} diff --git a/src/bus-watch-namespace.h b/src/bus-watch-namespace.h new file mode 100644 index 0000000..215f6be --- /dev/null +++ b/src/bus-watch-namespace.h @@ -0,0 +1,34 @@ +/* + * Copyright 2013 Canonical Ltd. + * + * This program 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 + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + * + * Author: Lars Uebernickel + */ + +#ifndef __BUS_WATCH_NAMESPACE_H__ +#define __BUS_WATCH_NAMESPACE_H__ + +#include + +guint bus_watch_namespace (GBusType bus_type, + const gchar *name_space, + GBusNameAppearedCallback appeared_handler, + GBusNameVanishedCallback vanished_handler, + gpointer user_data, + GDestroyNotify user_data_destroy); + +void bus_unwatch_namespace (guint id); + +#endif diff --git a/src/media-player-list.vala b/src/media-player-list.vala index 6eb5fc9..62badc2 100644 --- a/src/media-player-list.vala +++ b/src/media-player-list.vala @@ -26,9 +26,7 @@ public class MediaPlayerList { public MediaPlayerList () { this._players = new HashTable (str_hash, str_equal); - this.mpris_watcher = new Mpris2Watcher (); - this.mpris_watcher.client_appeared.connect (this.player_appeared); - this.mpris_watcher.client_disappeared.connect (this.player_disappeared); + BusWatcher.watch_namespace (BusType.SESSION, "org.mpris.MediaPlayer2", this.player_appeared, this.player_disappeared); } /* only valid while the list is not changed */ @@ -113,15 +111,21 @@ public class MediaPlayerList { public signal void player_removed (MediaPlayer player); HashTable _players; - Mpris2Watcher mpris_watcher; - void player_appeared (string desktop_id, string dbus_name, bool use_playlists) { - var player = this.insert (desktop_id); - if (player != null) - player.attach (dbus_name); + void player_appeared (DBusConnection connection, string name, string owner) { + try { + MprisRoot mpris2_root = Bus.get_proxy_sync (BusType.SESSION, name, MPRIS_MEDIA_PLAYER_PATH); + + var player = this.insert (mpris2_root.DesktopEntry); + if (player != null) + player.attach (name); + } + catch (Error e) { + warning ("unable to create mpris proxy for '%s': %s", name, e.message); + } } - void player_disappeared (string dbus_name) { + void player_disappeared (DBusConnection connection, string dbus_name) { MediaPlayer? player = this._players.find ( (name, player) => { return player.dbus_name == dbus_name; }); diff --git a/src/mpris2-watcher.vala b/src/mpris2-watcher.vala deleted file mode 100644 index 4a1ab6e..0000000 --- a/src/mpris2-watcher.vala +++ /dev/null @@ -1,200 +0,0 @@ -/* -Copyright 2010 Canonical Ltd. - -Authors: - Conor Curran - -This program is free software: you can redistribute it and/or modify it -under the terms of the GNU General Public License version 3, as published -by the Free Software Foundation. - -This program is distributed in the hope that it will be useful, but -WITHOUT ANY WARRANTY; without even the implied warranties of -MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR -PURPOSE. See the GNU General Public License for more details. - -You should have received a copy of the GNU General Public License along -with this program. If not, see . -*/ - -using Xml; - -public class Mpris2Watcher : GLib.Object -{ - DBusConnection session_bus; - - public signal void client_appeared ( string desktop_file_name, - string dbus_name, - bool use_playlists ); - public signal void client_disappeared ( string dbus_name ); - - public Mpris2Watcher () - { - } - - construct - { - const string match_rule = "type='signal'," + - "sender='org.freedesktop.DBus'," + - "interface='org.freedesktop.DBus'," + - "member='NameOwnerChanged'," + - "path='/org/freedesktop/DBus'," + - "arg0namespace='org.mpris.MediaPlayer2'"; - try { - this.session_bus = Bus.get_sync (BusType.SESSION); - - this.session_bus.call_sync ("org.freedesktop.DBus", - "/", - "org.freedesktop.DBus", - "AddMatch", - new Variant ("(s)", match_rule), - VariantType.TUPLE, - DBusCallFlags.NONE, - -1); - - this.session_bus.signal_subscribe ("org.freedesktop.DBus", - "org.freedesktop.DBus", - "NameOwnerChanged", - "/org/freedesktop/DBus", - null, - DBusSignalFlags.NO_MATCH_RULE, - this.name_owner_changed); - - this.check_for_active_clients.begin(); - } - catch (GLib.Error e) { - warning ("unable to set up name watch for mrpis clients: %s", e.message); - } - } - - // At startup check to see if there are clients up that we are interested in - // More relevant for development and daemon's like mpd. - async void check_for_active_clients() - { - Variant interfaces; - - try { - interfaces = yield this.session_bus.call ("org.freedesktop.DBus", - "/", - "org.freedesktop.DBus", - "ListNames", - null, - new VariantType ("(as)"), - DBusCallFlags.NONE, - -1); - } - catch (GLib.Error e) { - warning ("unable to search for existing mpris clients: %s ", e.message); - return; - } - - foreach (var val in interfaces.get_child_value (0)) { - var address = (string) val; - if (address.has_prefix (MPRIS_PREFIX)){ - MprisRoot? mpris2_root = this.create_mpris_root(address); - if (mpris2_root == null) return; - bool use_playlists = this.supports_playlists ( address ); - client_appeared (mpris2_root.DesktopEntry + ".desktop", address, use_playlists); - } - } - } - - void name_owner_changed (DBusConnection con, string sender, string object_path, - string interface_name, string signal_name, Variant parameters) - { - string name, previous_owner, current_owner; - - parameters.get ("(sss)", out name, out previous_owner, out current_owner); - - MprisRoot? mpris2_root = this.create_mpris_root (name); - if (mpris2_root == null) return; - - if (previous_owner != "" && current_owner == "") { - debug ("Client '%s' gone down", name); - client_disappeared (name); - } - else if (previous_owner == "" && current_owner != "") { - debug ("Client '%s' has appeared", name); - bool use_playlists = this.supports_playlists ( name ); - client_appeared (mpris2_root.DesktopEntry + ".desktop", name, use_playlists); - } - } - - private MprisRoot? create_mpris_root ( string name ){ - MprisRoot mpris2_root = null; - if ( name.has_prefix (MPRIS_PREFIX) ){ - try { - mpris2_root = Bus.get_proxy_sync ( BusType.SESSION, - name, - MPRIS_MEDIA_PLAYER_PATH ); - } - catch (IOError e){ - warning( "Mpris2watcher could not create a root interface: %s", - e.message ); - } - } - return mpris2_root; - } - - private bool supports_playlists ( string name ) - { - FreeDesktopIntrospectable introspectable; - - try { - /* The dbusproxy flag parameter is needed to ensure Banshee does not - blow up. I suspect the issue is that if you - try to instantiate a dbus object which does not have any properties - associated with it, gdbus will attempt to fetch the properties (this is - in the documentation) but the banshee mpris dbus object more than likely - causes a crash because it doesn't check for the presence of properties - before attempting to access them. - */ - introspectable = Bus.get_proxy_sync ( BusType.SESSION, - name, - MPRIS_MEDIA_PLAYER_PATH, - GLib.DBusProxyFlags.DO_NOT_LOAD_PROPERTIES); - var results = introspectable.Introspect(); - return this.parse_interfaces (results); - } - catch (IOError e){ - warning( "Could not create an introspectable object: %s", - e.message ); - } - return false; - } - - private bool parse_interfaces( string interface_info ) - { - //parse the document from path - bool result = false; - Xml.Doc* xml_doc = Parser.parse_doc (interface_info); - if (xml_doc == null) { - warning ("Mpris2Watcher - parse-interfaces - failed to instantiate xml doc"); - return false; - } - //get the root node. notice the dereferencing operator -> instead of . - Xml.Node* root_node = xml_doc->get_root_element (); - if (root_node == null) { - //free the document manually before throwing because the garbage collector can't work on pointers - delete xml_doc; - warning ("Mpris2Watcher - the interface info xml is empty"); - return false; - } - - //let's parse those nodes - for (Xml.Node* iter = root_node->children; iter != null; iter = iter->next) { - //spaces btw. tags are also nodes, discard them - if (iter->type != ElementType.ELEMENT_NODE){ - continue; - } - Xml.Attr* attributes = iter->properties; //get the node's name - string interface_name = attributes->children->content; - debug ( "this dbus object has interface %s ", interface_name ); - if ( interface_name == MPRIS_PREFIX.concat("Playlists")){ - result = true; - } - } - delete xml_doc; - return result; - } -} diff --git a/vapi/bus-watcher.vapi b/vapi/bus-watcher.vapi new file mode 100644 index 0000000..60dacff --- /dev/null +++ b/vapi/bus-watcher.vapi @@ -0,0 +1,25 @@ +/* + * Copyright 2013 Canonical Ltd. + * + * This program 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; version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + * + * Authors: + * Lars Uebernickel + */ + +namespace BusWatcher { + [CCode (cheader_filename = "bus-watch-namespace.h", cname = "bus_watch_namespace")] + public static uint watch_namespace (GLib.BusType bus_type, string name_space, + [CCode (delegate_target_pos = 4.9)] owned GLib.BusNameAppearedCallback? name_appeared, + [CCode (delegate_target_pos = 4.9)] owned GLib.BusNameVanishedCallback? name_vanished); +} -- cgit v1.2.3