/* * This file is part of Cockpit. * * Copyright (C) 2014 Red Hat, Inc. * * Cockpit 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.1 of the License, or * (at your option) any later version. * * Cockpit 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 Cockpit; If not, see . */ #include "config.h" #include "cockpitdbuscache.h" #include "cockpitdbusrules.h" #include "cockpitpaths.h" #include #define DEBUG_BATCHES 0 /* * This is a cache of properties which tracks updates. The best way to do * this is via ObjectManager. But it also does introspection and uses that * to get both interface info, and information about which paths are present * and the interfaces they implement. * * One big complication is that all of this needs to have ordering guarantees, * including introspection. We keep track of which batch of properties we're * working on, and associate barrier callbacks which can only happen once * a given batch of properties has completed processing. * * Also information about an interface will be available before we notify * about properties on an interface. This is a further ordering guarantee. * * Since there are lots of strings, to help with allocation churn, we have our * own string intern table, where path, interface and property names are * stored while the cache is active. Each time we get a path etc. from an * external source source (such as GVariant) and we know we'll need it later * then we intern it, so it sticks around. */ struct _CockpitDBusCache { GObject parent; /* Cancelled on dispose */ GCancellable *cancellable; /* The connection to talk on */ GDBusConnection *connection; /* The readable DBus name, and actual unique owner */ gchar *logname; gchar *name; /* Introspection stuff */ GHashTable *introspected; GQueue *introspects; GHashTable *introsent; GList *trash; /* The main data cache: paths > interfaces -> properties -> values */ GHashTable *cache; /* The paths and interfaces we should watch */ CockpitDBusRules *rules; /* Accumulated information about these various paths */ GTree *managed; /* Signal Subscriptions */ gboolean subscribed; guint subscribe_properties; guint subscribe_manager; /* Barrier related stuff */ GQueue *batches; GQueue *barriers; guint number; gboolean barrier_progressing; guint barrier_progressing_rounds; GHashTable *update; /* Interned strings */ GHashTable *interned; }; enum { PROP_CONNECTION = 1, PROP_NAME, PROP_LOGNAME, PROP_INTERFACE_INFO }; static guint signal_meta; static guint signal_update; G_DEFINE_TYPE (CockpitDBusCache, cockpit_dbus_cache, G_TYPE_OBJECT); static void hash_table_unref_or_null (gpointer data) { if (data) g_hash_table_unref (data); } static const gchar * intern_string (CockpitDBusCache *self, const gchar *string) { const gchar * interned; gchar *copy; interned = g_hash_table_lookup (self->interned, string); if (!interned) { copy = g_strdup (string); g_hash_table_add (self->interned, copy); interned = copy; } return interned; } typedef struct { guint number; CockpitDBusBarrierFunc callback; gpointer user_data; } BarrierData; typedef struct { gint refs; guint number; gboolean orphan; #if DEBUG_BATCHES GSList *debug; #endif } BatchData; static void barrier_progress (CockpitDBusCache *self) { BarrierData *barrier; BatchData *batch; if (self->barrier_progressing) { /* It might happen that barrier_progress is called * recursively, while one of the barrier callbacks is * running. We need to detect this and avoid calling the * next barrier callback while there is already one * running. Allowing it might violate the ordering * guarantees if the running callback is doing more work * after causing the recursive call to barrier_progress. * * Concretely, process_interfaces below can cause * recursive calls in the middle of its work when it * processes multiple interfaces. */ self->barrier_progressing_rounds += 1; return; } self->barrier_progressing = TRUE; self->barrier_progressing_rounds = 1; while (self->barrier_progressing_rounds > 0) { self->barrier_progressing_rounds -= 1; batch = g_queue_peek_head (self->batches); for (;;) { barrier = g_queue_peek_head (self->barriers); if (!barrier) break; /* * If there is a batch being processed, we must block * barriers that have an equal or later batch number. */ if (batch && batch->number <= barrier->number) break; g_queue_pop_head (self->barriers); (barrier->callback) (self, barrier->user_data); g_slice_free (BarrierData, barrier); } } self->barrier_progressing = FALSE; } static void barrier_flush (CockpitDBusCache *self) { BarrierData *barrier; for (;;) { barrier = g_queue_pop_head (self->barriers); if (!barrier) return; (barrier->callback) (self, barrier->user_data); g_slice_free (BarrierData, barrier); } } #if DEBUG_BATCHES static void batch_dump (BatchData *batch) { GSList *l; g_printerr ("BATCH %u (refs %d)\n", batch->number, batch->refs); batch->debug = g_slist_reverse (batch->debug); for (l = batch->debug; l != NULL; l = g_slist_next (l)) g_printerr (" * %s\n", (gchar *)l->data); batch->debug = g_slist_reverse (batch->debug); } #endif /* DEBUG_BATCHES */ static void batch_free (BatchData *batch) { #if DEBUG_BATCHES g_slist_foreach (batch->debug, (GFunc)g_free, NULL); #endif g_slice_free (BatchData, batch); } static void batch_progress (CockpitDBusCache *self) { BatchData *batch; GHashTable *update; for (;;) { batch = g_queue_peek_head (self->batches); /* * Once a batch has completed it's refs field will be zero. * This means we can send notify of any property changes, * process any barriers waiting on this batch, and move on * to the next batch. */ if (!batch || batch->refs > 0) return; g_queue_pop_head (self->batches); update = self->update; self->update = NULL; if (update) { g_signal_emit (self, signal_update, 0, update); g_hash_table_unref (update); } batch_free (batch); barrier_progress (self); } } static void batch_flush (CockpitDBusCache *self) { BatchData *batch; for (;;) { batch = g_queue_pop_head (self->batches); if (!batch) return; if (batch->refs == 0) batch_free (batch); else batch->orphan = TRUE; } } static BatchData * batch_create (CockpitDBusCache *self) { BatchData *batch = g_slice_new0 (BatchData); batch->refs = 1; self->number++; batch->number = self->number; g_queue_push_tail (self->batches, batch); return batch; } static BatchData * _batch_ref (BatchData *batch, const gchar *function, gint line) { g_assert (batch != NULL); batch->refs++; #if DEBUG_BATCHES batch->debug = g_slist_prepend (batch->debug, g_strdup_printf (" * ref -> %d %s:%d", batch->refs, function, line)); #endif return batch; } #define batch_ref(batch) \ (_batch_ref((batch), G_STRFUNC, __LINE__)) static void _batch_unref (CockpitDBusCache *self, BatchData *batch, const gchar *function, gint line) { g_assert (batch != NULL); #if DEBUG_BATCHES if (!(batch->refs > 0)) batch_dump (batch); #endif g_assert (batch->refs > 0); batch->refs--; #if DEBUG_BATCHES batch->debug = g_slist_prepend (batch->debug, g_strdup_printf (" * unref -> %d %s:%d", batch->refs, function, line)); #endif if (batch->refs == 0 && batch->orphan) batch_free (batch); else batch_progress (self); } #define batch_unref(self, batch) \ (_batch_unref((self), (batch), G_STRFUNC, __LINE__)) static void cockpit_dbus_cache_init (CockpitDBusCache *self) { self->number = 1; self->cancellable = g_cancellable_new (); self->managed = cockpit_paths_new (); /* Becomes a whole tree of hash tables */ self->cache = g_hash_table_new_full (g_str_hash, g_str_equal, NULL, (GDestroyNotify)g_hash_table_unref); /* All of these are sets. ie: key and value identical */ self->rules = cockpit_dbus_rules_new (); self->introspects = g_queue_new (); self->introsent = g_hash_table_new (g_str_hash, g_str_equal); self->batches = g_queue_new (); self->barriers = g_queue_new (); /* Put allocations we need to keep around, but can't handily track */ self->trash = NULL; self->interned = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, NULL); } typedef struct { const gchar *interface; const gchar *path; CockpitDBusIntrospectFunc callback; gpointer user_data; BatchData *batch; gboolean introspecting; } IntrospectData; static void introspect_next (CockpitDBusCache *self); static void scrape_variant (CockpitDBusCache *self, BatchData *batch, GVariant *data); static void introspect_complete (CockpitDBusCache *self, IntrospectData *id) { GDBusInterfaceInfo *iface = NULL; if (id->interface) { iface = cockpit_dbus_interface_info_lookup (self->introspected, id->interface); if (!iface) { g_debug ("%s: introspect interface %s didn't work", self->logname, id->interface); /* * So we were expecting an interface that wasn't found at the expected * object. This means that something is wrong with the introspection * on the DBus service. We create a pretend empty interface, so that * the ordering guarantees are met. */ iface = g_new0 (GDBusInterfaceInfo, 1); iface->ref_count = 1; iface->name = g_strdup (id->interface); cockpit_dbus_interface_info_push (self->introspected, iface); g_dbus_interface_info_unref (iface); } } if (id->callback) (id->callback) (self, iface, id->user_data); /* Mark as having called, as a double check */ id->callback = NULL; batch_unref (self, id->batch); g_assert (id->callback == NULL); g_slice_free (IntrospectData, id); } static void process_introspect_node (CockpitDBusCache *self, BatchData *batch, const gchar *path, GDBusNodeInfo *node, gboolean recursive); static gboolean dbus_error_matches_unknown (GError *error) { gboolean ret = FALSE; if (g_error_matches (error, G_DBUS_ERROR, G_DBUS_ERROR_UNKNOWN_METHOD) || g_error_matches (error, G_DBUS_ERROR, G_DBUS_ERROR_ACCESS_DENIED) || g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CLOSED)) return TRUE; #if GLIB_CHECK_VERSION(2,42,0) ret = g_error_matches (error, G_DBUS_ERROR, G_DBUS_ERROR_UNKNOWN_INTERFACE) || g_error_matches (error, G_DBUS_ERROR, G_DBUS_ERROR_UNKNOWN_OBJECT) || g_error_matches (error, G_DBUS_ERROR, G_DBUS_ERROR_UNKNOWN_PROPERTY); #else gchar *remote = g_dbus_error_get_remote_error (error); if (remote) { /* * DBus used to only have the UnknownMethod error. It didn't have * specific errors for UnknownObject and UnknownInterface. So we're * pretty liberal on what we treat as an expected error here. * * HACK: GDBus also doesn't understand the newer error codes :S * * https://bugzilla.gnome.org/show_bug.cgi?id=727900 */ ret = (g_str_equal (remote, "org.freedesktop.DBus.Error.UnknownMethod") || g_str_equal (remote, "org.freedesktop.DBus.Error.UnknownObject") || g_str_equal (remote, "org.freedesktop.DBus.Error.UnknownInterface") || g_str_equal (remote, "org.freedesktop.DBus.Error.UnknownProperty")); g_free (remote); } #endif return ret; } static void on_introspect_reply (GObject *source, GAsyncResult *result, gpointer user_data) { CockpitDBusCache *self = user_data; IntrospectData *id; GDBusNodeInfo *node; GError *error = NULL; GVariant *retval; const gchar *xml; /* All done with this introspect */ id = g_queue_pop_head (self->introspects); /* Introspects have been flushed */ if (!id) { g_object_unref (self); return; } g_assert (id->introspecting); retval = g_dbus_connection_call_finish (G_DBUS_CONNECTION (source), result, &error); if (retval) { g_debug ("%s: reply from Introspect() at %s", self->logname, id->path); g_variant_get (retval, "(&s)", &xml); node = g_dbus_node_info_new_for_xml (xml, &error); if (node) { process_introspect_node (self, id->batch, id->path, node, id->interface == NULL); g_dbus_node_info_unref (node); } g_variant_unref (retval); } if (error) { if (!dbus_error_matches_unknown (error)) g_message ("%s: couldn't introspect %s: %s", self->logname, id->path, error->message); g_error_free (error); } introspect_complete (self, id); introspect_next (self); g_object_unref (self); } static void introspect_next (CockpitDBusCache *self) { IntrospectData *id; id = g_queue_peek_head (self->introspects); if (id && !id->introspecting) { if (g_cancellable_is_cancelled (self->cancellable)) { g_queue_pop_head (self->introspects); introspect_complete (self, id); } else { g_debug ("%s: calling Introspect() on %s", self->logname, id->path); id->introspecting = TRUE; g_dbus_connection_call (self->connection, self->name, id->path, "org.freedesktop.DBus.Introspectable", "Introspect", g_variant_new ("()"), G_VARIANT_TYPE ("(s)"), G_DBUS_CALL_FLAGS_NONE, -1, self->cancellable, on_introspect_reply, g_object_ref (self)); } } } static void introspect_flush (CockpitDBusCache *self) { gboolean note = FALSE; IntrospectData *id; GQueue *queue; queue = g_queue_new (); for (;;) { /* Steal everything, more could be added by callback */ for (;;) { id = g_queue_pop_tail (self->introspects); if (!id) break; g_queue_push_head (queue, id); } id = g_queue_pop_head (queue); if (!id) { g_queue_free (queue); return; } if (!note) g_debug ("%s: flushing introspect queue", self->logname); note = TRUE; introspect_complete (self, id); } } static void introspect_queue (CockpitDBusCache *self, BatchData *batch, const gchar *path, const gchar *interface, CockpitDBusIntrospectFunc callback, gpointer user_data) { IntrospectData *id; g_assert (path != NULL); g_assert (batch != NULL); id = g_slice_new0 (IntrospectData); id->interface = interface; id->batch = batch_ref (batch); id->path = path; id->callback = callback; id->user_data = user_data; g_debug ("%s: queueing introspect %s %s%s", self->logname, path, interface ? "for " : "", interface ? interface : ""); g_queue_push_tail (self->introspects, id); introspect_next (self); } static void introspect_maybe (CockpitDBusCache *self, BatchData *batch, const gchar *path, const gchar *interface, CockpitDBusIntrospectFunc callback, gpointer user_data) { GDBusInterfaceInfo *iface; g_assert (path); g_assert (interface); iface = cockpit_dbus_interface_info_lookup (self->introspected, interface); if (iface) { (callback) (self, iface, user_data); } else { if (batch == NULL) batch = batch_create (self); else batch = batch_ref (batch); introspect_queue (self, batch, intern_string (self, path), intern_string (self, interface), callback, user_data); batch_unref (self, batch); } } void cockpit_dbus_cache_introspect (CockpitDBusCache *self, const gchar *path, const gchar *interface, CockpitDBusIntrospectFunc callback, gpointer user_data) { g_return_if_fail (self != NULL); g_return_if_fail (path != NULL); introspect_maybe (self, NULL, path, interface, callback, user_data); } static GHashTable * emit_interfaces (CockpitDBusCache *self, const gchar *path) { GHashTable *interfaces; g_assert (path != NULL); if (!self->update) { self->update = g_hash_table_new_full (g_str_hash, g_str_equal, NULL, hash_table_unref_or_null); } interfaces = g_hash_table_lookup (self->update, path); if (!interfaces) { interfaces = g_hash_table_new_full (g_str_hash, g_str_equal, NULL, hash_table_unref_or_null); g_hash_table_replace (self->update, (gchar *)path, interfaces); } return interfaces; } static void emit_remove (CockpitDBusCache *self, const gchar *path, const gchar *interface) { GHashTable *interfaces = emit_interfaces (self, path); g_assert (interface != NULL); g_hash_table_replace (interfaces, (gchar *)interface, NULL); } static void emit_change (CockpitDBusCache *self, const gchar *path, GDBusInterfaceInfo *iface, const gchar *property, GVariant *value) { GHashTable *interfaces = emit_interfaces (self, path); GHashTable *properties; g_assert (iface != NULL); properties = g_hash_table_lookup (interfaces, iface->name); if (!properties) { properties = g_hash_table_new_full (g_str_hash, g_str_equal, NULL, (GDestroyNotify)g_variant_unref); g_hash_table_replace (interfaces, iface->name, properties); } if (property) { g_assert (value != NULL); g_hash_table_replace (properties, (gchar *)property, g_variant_ref (value)); } } static GHashTable * ensure_interfaces (CockpitDBusCache *self, const gchar *path) { GHashTable *interfaces; interfaces = g_hash_table_lookup (self->cache, path); if (!interfaces) { interfaces = g_hash_table_new_full (g_str_hash, g_str_equal, NULL, (GDestroyNotify)g_hash_table_unref); g_hash_table_replace (self->cache, (gchar *)path, interfaces); } return interfaces; } static GHashTable * ensure_properties (CockpitDBusCache *self, const gchar *path, GDBusInterfaceInfo *iface) { GHashTable *interfaces; GHashTable *properties; const gchar *name; interfaces = ensure_interfaces (self, path); properties = g_hash_table_lookup (interfaces, iface->name); if (!properties) { properties = g_hash_table_new_full (g_str_hash, g_str_equal, NULL, (GDestroyNotify)g_variant_unref); g_hash_table_replace (interfaces, iface->name, properties); g_debug ("%s: present %s at %s", self->logname, iface->name, path); emit_change (self, path, iface, NULL, NULL); } name = intern_string (self, iface->name); if (!g_hash_table_lookup (self->introsent, name)) { g_hash_table_add (self->introsent, (gpointer)name); g_signal_emit (self, signal_meta, 0, iface); } return properties; } static void process_value (CockpitDBusCache *self, GHashTable *properties, const gchar *path, GDBusInterfaceInfo *iface, const gchar *property, GVariant *variant) { gpointer prev; GVariant *value; gpointer key; value = g_variant_get_variant (variant); if (g_hash_table_lookup_extended (properties, property, &key, &prev)) { if (g_variant_equal (prev, value)) { g_variant_unref (value); return; } g_hash_table_steal (properties, key); g_hash_table_replace (properties, key, value); g_variant_unref (prev); } else { g_hash_table_replace (properties, (gchar *)property, value); } g_debug ("%s: changed %s %s at %s", self->logname, iface->name, property, path); emit_change (self, path, iface, property, value); } typedef struct { CockpitDBusCache *self; const gchar *path; const gchar *property; BatchData *batch; GDBusInterfaceInfo *iface; } GetData; static void process_get (CockpitDBusCache *self, BatchData *batch, const gchar *path, GDBusInterfaceInfo *iface, const gchar *property, GVariant *retval) { GHashTable *properties; GVariant *variant; g_variant_get (retval, "(@v)", &variant); properties = ensure_properties (self, path, iface); process_value (self, properties, path, iface, property, variant); cockpit_dbus_cache_scrape (self, variant); g_variant_unref (variant); } static void on_get_reply (GObject *source, GAsyncResult *result, gpointer user_data) { GetData *gd = user_data; CockpitDBusCache *self = gd->self; GVariant *retval; GError *error = NULL; retval = g_dbus_connection_call_finish (G_DBUS_CONNECTION (source), result, &error); if (error) { if (!g_cancellable_is_cancelled (self->cancellable)) { if (dbus_error_matches_unknown (error)) { g_debug ("%s: couldn't get property %s %s at %s: %s", self->logname, gd->iface->name, gd->property, gd->path, error->message); } else { g_message ("%s: couldn't get property %s %s at %s: %s", self->logname, gd->iface->name, gd->property, gd->path, error->message); } } g_error_free (error); } if (retval) { g_debug ("%s: reply from Get() on %s", self->logname, gd->path); process_get (self, gd->batch, gd->path, gd->iface, gd->property, retval); g_variant_unref (retval); } batch_unref (self, gd->batch); g_object_unref (gd->self); g_slice_free (GetData, gd); } static void process_properties (CockpitDBusCache *self, BatchData *batch, const gchar *path, GDBusInterfaceInfo *iface, GVariant *dict) { GHashTable *properties; const gchar *property; GVariant *variant; GVariantIter iter; properties = ensure_properties (self, path, iface); g_variant_iter_init (&iter, dict); while (g_variant_iter_loop (&iter, "{s@v}", &property, &variant)) { process_value (self, properties, path, iface, intern_string (self, property), variant); } } typedef struct { const gchar *path; GVariant *body; BatchData *batch; } PropertiesChangedData; static void process_properties_changed (CockpitDBusCache *self, GDBusInterfaceInfo *iface, gpointer user_data) { PropertiesChangedData *pcd = user_data; GetData *gd; GVariantIter iter; const gchar *property; GVariant *changed; GVariant *invalidated; g_variant_get (pcd->body, "(@s@a{sv}@as)", NULL, &changed, &invalidated); process_properties (self, pcd->batch, pcd->path, iface, changed); /* * These are properties which the service didn't want to broadcast because * they're either calculated per-peer or expensive to calculate if nobody * is listening to them. We want them ... so get them and include them * in the current batch. */ g_variant_iter_init (&iter, invalidated); while (g_variant_iter_loop (&iter, "&s", &property)) { g_debug ("%s: calling Get() for %s %s at %s", self->logname, iface->name, property, pcd->path); gd = g_slice_new0 (GetData); gd->self = g_object_ref (self); gd->property = intern_string (self, property); gd->batch = batch_ref (pcd->batch); gd->path = pcd->path; gd->iface = iface; g_dbus_connection_call (self->connection, self->name, gd->path, "org.freedesktop.DBus.Properties", "Get", g_variant_new ("(ss)", iface->name, property), G_VARIANT_TYPE ("(v)"), G_DBUS_CALL_FLAGS_NONE, -1, self->cancellable, on_get_reply, gd); } g_variant_unref (invalidated); g_variant_unref (changed); batch_unref (self, pcd->batch); g_variant_unref (pcd->body); g_slice_free (PropertiesChangedData, pcd); } static void process_properties_barrier (CockpitDBusCache *self, gpointer user_data) { PropertiesChangedData *pcd = user_data; const gchar *interface; GVariant *changed; BatchData *batch; g_variant_get (pcd->body, "(&s@a{sv}@as)", &interface, &changed, NULL); batch = batch_create (self); pcd->batch = batch_ref (batch); introspect_maybe (self, pcd->batch, pcd->path, interface, process_properties_changed, pcd); scrape_variant (self, batch, changed); batch_unref (self, batch); g_variant_unref (changed); } static void on_properties_signal (GDBusConnection *connection, const gchar *sender, const gchar *path, const gchar *properties_iface, const gchar *member, GVariant *body, gpointer user_data) { CockpitDBusCache *self = user_data; PropertiesChangedData *pcd; const gchar *interface; if (!g_variant_is_of_type (body, G_VARIANT_TYPE ("(sa{sv}as)"))) { g_debug ("%s: received PropertiesChanged with bad type", self->logname); return; } g_debug ("%s: signal PropertiesChanged at %s", self->logname, path); g_variant_get (body, "(&s@a{sv}@as)", &interface, NULL, NULL); if (!cockpit_dbus_rules_match (self->rules, path, interface, NULL, NULL)) return; pcd = g_slice_new0 (PropertiesChangedData); pcd->body = g_variant_ref (body); pcd->path = intern_string (self, path); cockpit_dbus_cache_barrier (self, process_properties_barrier, pcd); } typedef struct { const gchar *path; GVariant *dict; BatchData *batch; } ProcessInterfaceData; static void process_interface (CockpitDBusCache *self, GDBusInterfaceInfo *iface, gpointer user_data) { ProcessInterfaceData *pid = user_data; process_properties (self, pid->batch, pid->path, iface, pid->dict); batch_unref (self, pid->batch); g_variant_unref (pid->dict); g_slice_free (ProcessInterfaceData, pid); } static void process_interfaces (CockpitDBusCache *self, BatchData *batch, GHashTable *snapshot, const gchar *path, GVariant *dict) { ProcessInterfaceData *pid; GVariant *inner; const gchar *interface; GVariantIter iter; if (batch) batch = batch_ref (batch); g_variant_iter_init (&iter, dict); while (g_variant_iter_loop (&iter, "{s@a{sv}}", &interface, &inner)) { if (!cockpit_dbus_rules_match (self->rules, path, interface, NULL, NULL)) continue; if (!batch) batch = batch_create (self); if (snapshot) g_hash_table_remove (snapshot, interface); pid = g_slice_new0 (ProcessInterfaceData); pid->batch = batch_ref (batch); pid->path = path; pid->dict = g_variant_ref (inner); cockpit_dbus_cache_introspect (self, path, interface, process_interface, pid); scrape_variant (self, batch, inner); } if (batch) batch_unref (self, batch); } typedef struct { GVariant *body; const gchar *manager_added; } ProcessInterfacesData; static void retrieve_managed_objects (CockpitDBusCache *self, const gchar *namespace_path, BatchData *batch); static void process_interfaces_added (CockpitDBusCache *self, gpointer user_data) { ProcessInterfacesData *pis = user_data; BatchData *batch = NULL; GVariant *interfaces; const gchar *path; /* * We added a manager while processing this message, perform a full manager * load as part of the same batch. */ if (pis->manager_added) { batch = batch_create (self); retrieve_managed_objects (self, pis->manager_added, batch); } g_variant_get (pis->body, "(&o@a{sa{sv}})", &path, &interfaces); process_interfaces (self, batch, NULL, intern_string (self, path), interfaces); g_variant_unref (interfaces); if (batch) batch_unref (self, batch); g_variant_unref (pis->body); g_slice_free (ProcessInterfacesData, pis); } static void process_removed (CockpitDBusCache *self, const gchar *path, const gchar *interface) { GHashTable *interfaces; GHashTable *properties; interfaces = g_hash_table_lookup (self->cache, path); if (!interfaces) return; properties = g_hash_table_lookup (interfaces, interface); if (!properties) return; g_hash_table_remove (interfaces, interface); g_debug ("%s: removed %s at %s", self->logname, interface, path); emit_remove (self, path, interface); } static void process_interfaces_removed (CockpitDBusCache *self, gpointer user_data) { ProcessInterfacesData *pis = user_data; GVariant *array; const gchar *path; const gchar *interface; GVariantIter iter; BatchData *batch; batch = batch_create (self); /* * We added a manager while processing this message, perform a full manager * load as part of the same batch. */ if (pis->manager_added) retrieve_managed_objects (self, pis->manager_added, batch); g_variant_get (pis->body, "(&o@as)", &path, &array); path = intern_string (self, path); g_variant_iter_init (&iter, array); while (g_variant_iter_loop (&iter, "&s", &interface)) process_removed (self, path, intern_string (self, interface)); batch_unref (self, batch); g_variant_unref (array); g_variant_unref (pis->body); g_slice_free (ProcessInterfacesData, pis); } static void on_manager_signal (GDBusConnection *connection, const gchar *sender, const gchar *path, const gchar *interface, const gchar *member, GVariant *body, gpointer user_data) { CockpitDBusCache *self = user_data; CockpitDBusBarrierFunc barrier_func = NULL; const gchar * manager_added; ProcessInterfacesData *pis; /* Note that this is an ObjectManager */ manager_added = cockpit_paths_add (self->managed, path); if (g_str_equal (member, "InterfacesAdded")) { if (g_variant_is_of_type (body, G_VARIANT_TYPE ("(oa{sa{sv}})"))) { g_debug ("%s: signal InterfacesAdded at %s", self->logname, path); barrier_func = process_interfaces_added; } else { g_debug ("%s: received InterfacesAdded with bad type", self->logname); } } else if (g_str_equal (member, "InterfacesRemoved")) { if (g_variant_is_of_type (body, G_VARIANT_TYPE ("(oas)"))) { g_debug ("%s: signal InterfacesRemoved at %s", self->logname, path); barrier_func = process_interfaces_removed; } else { g_debug ("%s: received InterfacesRemoved with bad type", self->logname); } } if (barrier_func) { pis = g_slice_new0 (ProcessInterfacesData); pis->body = g_variant_ref (body); pis->manager_added = manager_added; cockpit_dbus_cache_barrier (self, barrier_func, pis); } } static void cockpit_dbus_cache_constructed (GObject *object) { CockpitDBusCache *self = COCKPIT_DBUS_CACHE (object); g_return_if_fail (self->connection != NULL); if (!self->introspected) self->introspected = cockpit_dbus_interface_info_new (); self->subscribe_properties = g_dbus_connection_signal_subscribe (self->connection, self->name, "org.freedesktop.DBus.Properties", "PropertiesChanged", NULL, /* object_path */ NULL, /* arg0 */ G_DBUS_SIGNAL_FLAGS_NONE, on_properties_signal, self, NULL); self->subscribe_manager = g_dbus_connection_signal_subscribe (self->connection, self->name, "org.freedesktop.DBus.ObjectManager", NULL, /* member */ NULL, /* object_path */ NULL, /* arg0 */ G_DBUS_SIGNAL_FLAGS_NONE, on_manager_signal, self, NULL); self->subscribed = TRUE; } static void cockpit_dbus_cache_set_property (GObject *obj, guint prop_id, const GValue *value, GParamSpec *pspec) { CockpitDBusCache *self = COCKPIT_DBUS_CACHE (obj); switch (prop_id) { case PROP_CONNECTION: self->connection = g_value_dup_object (value); break; case PROP_NAME: self->name = g_value_dup_string (value); break; case PROP_LOGNAME: self->logname = g_value_dup_string (value); break; case PROP_INTERFACE_INFO: self->introspected = g_value_dup_boxed (value); break; default: G_OBJECT_WARN_INVALID_PROPERTY_ID (obj, prop_id, pspec); break; } } static void cockpit_dbus_cache_dispose (GObject *object) { CockpitDBusCache *self = COCKPIT_DBUS_CACHE (object); g_cancellable_cancel (self->cancellable); if (self->subscribed) { g_dbus_connection_signal_unsubscribe (self->connection, self->subscribe_properties); g_dbus_connection_signal_unsubscribe (self->connection, self->subscribe_manager); self->subscribed = FALSE; } introspect_flush (self); batch_flush (self); barrier_flush (self); G_OBJECT_CLASS (cockpit_dbus_cache_parent_class)->dispose (object); } static void cockpit_dbus_cache_finalize (GObject *object) { CockpitDBusCache *self = COCKPIT_DBUS_CACHE (object); g_clear_object (&self->connection); g_object_unref (self->cancellable); g_free (self->name); g_free (self->logname); cockpit_dbus_rules_free (self->rules); g_tree_destroy (self->managed); g_queue_free (self->batches); g_queue_free (self->barriers); g_assert (self->introspects->head == NULL); g_queue_free (self->introspects); g_hash_table_unref (self->introsent); g_hash_table_unref (self->introspected); g_hash_table_unref (self->cache); g_hash_table_destroy (self->interned); g_list_free_full (self->trash, g_free); G_OBJECT_CLASS (cockpit_dbus_cache_parent_class)->finalize (object); } static void cockpit_dbus_cache_class_init (CockpitDBusCacheClass *klass) { GObjectClass *gobject_class = G_OBJECT_CLASS (klass); gobject_class->constructed = cockpit_dbus_cache_constructed; gobject_class->set_property = cockpit_dbus_cache_set_property; gobject_class->dispose = cockpit_dbus_cache_dispose; gobject_class->finalize = cockpit_dbus_cache_finalize; signal_meta = g_signal_new ("meta", COCKPIT_TYPE_DBUS_CACHE, G_SIGNAL_RUN_LAST, G_STRUCT_OFFSET (CockpitDBusCacheClass, meta), NULL, NULL, NULL, G_TYPE_NONE, 1, G_TYPE_DBUS_INTERFACE_INFO); signal_update = g_signal_new ("update", COCKPIT_TYPE_DBUS_CACHE, G_SIGNAL_RUN_LAST, G_STRUCT_OFFSET (CockpitDBusCacheClass, update), NULL, NULL, NULL, G_TYPE_NONE, 1, G_TYPE_HASH_TABLE); g_object_class_install_property (gobject_class, PROP_CONNECTION, g_param_spec_object ("connection", "connection", "connection", G_TYPE_DBUS_CONNECTION, G_PARAM_WRITABLE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS)); g_object_class_install_property (gobject_class, PROP_NAME, g_param_spec_string ("name", "name", "name", NULL, G_PARAM_WRITABLE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS)); g_object_class_install_property (gobject_class, PROP_LOGNAME, g_param_spec_string ("logname", "logname", "logname", "internal", G_PARAM_WRITABLE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS)); g_object_class_install_property (gobject_class, PROP_INTERFACE_INFO, g_param_spec_boxed ("interface-info", NULL, NULL, G_TYPE_HASH_TABLE, G_PARAM_WRITABLE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS)); } static GHashTable * snapshot_string_keys (GHashTable *table) { GHashTable *set; GHashTableIter iter; gpointer key; set = g_hash_table_new (g_str_hash, g_str_equal); if (table) { g_hash_table_iter_init (&iter, table); while (g_hash_table_iter_next (&iter, &key, NULL)) g_hash_table_add (set, key); } return set; } static void process_get_all (CockpitDBusCache *self, BatchData *batch, const gchar *path, GDBusInterfaceInfo *iface, GVariant *retval) { GVariant *dict; g_variant_get (retval, "(@a{sv})", &dict); process_properties (self, batch, path, iface, dict); scrape_variant (self, batch, dict); g_variant_unref (dict); } static void process_removed_path (CockpitDBusCache *self, const gchar *path) { GHashTable *interfaces; GHashTableIter iter; gpointer interface; GHashTable *snapshot; interfaces = g_hash_table_lookup (self->cache, path); if (interfaces) { snapshot = snapshot_string_keys (interfaces); g_hash_table_iter_init (&iter, snapshot); while (g_hash_table_iter_next (&iter, &interface, NULL)) process_removed (self, path, interface); g_hash_table_destroy (snapshot); } } static void process_paths (CockpitDBusCache *self, BatchData *batch, GHashTable *snapshot, GVariant *dict) { GVariant *inner; GHashTable *snap; const gchar *path; GVariantIter iter; GHashTableIter hter; gpointer key; g_variant_iter_init (&iter, dict); while (g_variant_iter_loop (&iter, "{o@a{sa{sv}}}", &path, &inner)) { snap = NULL; if (snapshot) { g_hash_table_remove (snapshot, path); snap = snapshot_string_keys (g_hash_table_lookup (self->cache, path)); } process_interfaces (self, batch, snap, intern_string (self, path), inner); if (snap) { g_hash_table_iter_init (&hter, snap); while (g_hash_table_iter_next (&hter, &key, NULL)) process_removed (self, path, key); g_hash_table_destroy (snap); } } } static void process_get_managed_objects (CockpitDBusCache *self, BatchData *batch, const gchar *manager_path, GVariant *retval) { /* * So here we handle things slightly differently than just pushing the * result through all the properties update mechanics. We get * indications of interfaces and entire paths disappearing here, * so we have to handle that. */ GVariant *inner; GHashTableIter iter; GHashTable *snapshot; gpointer path; /* Snapshot everything under control of the path of the object manager */ snapshot = g_hash_table_new (g_str_hash, g_str_equal); g_hash_table_iter_init (&iter, self->cache); while (g_hash_table_iter_next (&iter, &path, NULL)) { if (cockpit_path_has_ancestor (path, manager_path)) g_hash_table_add (snapshot, path); } g_variant_get (retval, "(@a{oa{sa{sv}}})", &inner); process_paths (self, batch, snapshot, inner); g_variant_unref (inner); g_hash_table_iter_init (&iter, snapshot); while (g_hash_table_iter_next (&iter, &path, NULL)) process_removed_path (self, path); g_hash_table_unref (snapshot); } static void process_introspect_children (CockpitDBusCache *self, BatchData *batch, const gchar *parent_path, GDBusNodeInfo *node) { GDBusNodeInfo *child; GHashTable *snapshot; GHashTableIter iter; gchar *child_path; gpointer path; guint i; /* Snapshot all direct children of path */ snapshot = g_hash_table_new (g_str_hash, g_str_equal); g_hash_table_iter_init (&iter, self->cache); while (g_hash_table_iter_next (&iter, &path, NULL)) { if (cockpit_path_has_parent (path, parent_path)) g_hash_table_add (snapshot, path); } /* Poke any additional child nodes discovered */ for (i = 0; node->nodes && node->nodes[i]; i++) { child = node->nodes[i]; /* If the child has no path then it's useless */ if (!child->path) continue; /* Figure out an object path for this node */ if (g_str_has_prefix (child->path, "/")) child_path = g_strdup (child->path); else if (g_str_equal (parent_path, "/")) child_path = g_strdup_printf ("/%s", child->path); else child_path = g_strdup_printf ("%s/%s", parent_path, child->path); /* Remove everything in the snapshot related to this child */ g_hash_table_remove (snapshot, child_path); if (cockpit_dbus_rules_match (self->rules, child_path, NULL, NULL, NULL) && !cockpit_paths_contain_or_ancestor (self->managed, child_path)) { /* Inline child interfaces are rare but possible */ if (child->interfaces && child->interfaces[0]) { process_introspect_node (self, batch, intern_string (self, child_path), child, TRUE); } /* If we have no knowledge of this child, then introspect it */ else { introspect_queue (self, batch, intern_string (self, child_path), NULL, NULL, NULL); } } g_free (child_path); } /* Anything remaining in snapshot stays */ g_hash_table_iter_init (&iter, snapshot); while (g_hash_table_iter_next (&iter, &path, NULL)) process_removed_path (self, path); g_hash_table_unref (snapshot); } typedef struct { CockpitDBusCache *self; const gchar *path; GDBusInterfaceInfo *iface; BatchData *batch; } GetAllData; static void on_get_all_reply (GObject *source, GAsyncResult *result, gpointer user_data) { GetAllData *gad = user_data; CockpitDBusCache *self = gad->self; GError *error = NULL; GVariant *retval; retval = g_dbus_connection_call_finish (G_DBUS_CONNECTION (source), result, &error); if (error) { if (!g_cancellable_is_cancelled (self->cancellable)) { if (dbus_error_matches_unknown (error)) { g_debug ("%s: couldn't get all properties of %s at %s: %s", self->logname, gad->iface->name, gad->path, error->message); } else { g_message ("%s: couldn't get all properties of %s at %s: %s", self->logname, gad->iface->name, gad->path, error->message); } } g_error_free (error); } if (retval) { g_debug ("%s: reply to GetAll() for %s at %s", self->logname, gad->iface->name, gad->path); process_get_all (self, gad->batch, gad->path, gad->iface, retval); g_variant_unref (retval); } /* Whether or not this failed, we know the interface exists */ ensure_properties (self, gad->path, gad->iface); emit_change (self, gad->path, gad->iface, NULL, NULL); batch_unref (self, gad->batch); g_object_unref (gad->self); g_slice_free (GetAllData, gad); } static void retrieve_properties (CockpitDBusCache *self, BatchData *batch, const gchar *path, GDBusInterfaceInfo *iface) { GetAllData *gad; /* Don't bother getting properties for this well known interface * that doesn't have any. Also, NetworkManager returns an error. */ if (g_strcmp0 (iface->name, "org.freedesktop.DBus.Properties") == 0) return; g_debug ("%s: calling GetAll() for %s at %s", self->logname, iface->name, path); gad = g_slice_new0 (GetAllData); gad->self = g_object_ref (self); gad->batch = batch_ref (batch); gad->path = path; gad->iface = iface; g_dbus_connection_call (self->connection, self->name, path, "org.freedesktop.DBus.Properties", "GetAll", g_variant_new ("(s)", iface->name), G_VARIANT_TYPE ("(a{sv})"), G_DBUS_CALL_FLAGS_NONE, -1, self->cancellable, on_get_all_reply, gad); } static void process_introspect_node (CockpitDBusCache *self, BatchData *batch, const gchar *path, GDBusNodeInfo *node, gboolean recursive) { GDBusInterfaceInfo *iface; GDBusInterfaceInfo *prev; GHashTable *snapshot; GHashTableIter iter; gpointer interface; guint i; if (cockpit_paths_contain_or_ancestor (self->managed, path)) recursive = FALSE; snapshot = snapshot_string_keys (g_hash_table_lookup (self->cache, path)); for (i = 0; node->interfaces && node->interfaces[i] != NULL; i++) { iface = node->interfaces[i]; if (!iface->name) { g_warning ("Received interface from %s at %s without name", self->logname, path); continue; } /* Cache this interface for later use elsewhere */ prev = cockpit_dbus_interface_info_lookup (self->introspected, iface->name); if (prev) { iface = prev; } else { cockpit_dbus_interface_info_push (self->introspected, iface); } /* Skip these interfaces */ if (g_str_has_prefix (iface->name, "org.freedesktop.DBus.")) { /* But make sure we track the fact that something is here */ ensure_interfaces (self, path); continue; } g_hash_table_remove (snapshot, iface->name); if (recursive && cockpit_dbus_rules_match (self->rules, path, iface->name, NULL, NULL)) retrieve_properties (self, batch, path, iface); } /* Remove any interfaces not seen */ g_hash_table_iter_init (&iter, snapshot); while (g_hash_table_iter_next (&iter, &interface, NULL)) process_removed (self, path, interface); g_hash_table_destroy (snapshot); if (recursive) process_introspect_children (self, batch, path, node); } typedef struct { CockpitDBusCache *self; const gchar *path; BatchData *batch; } GetManagedObjectsData; static void on_get_managed_objects_reply (GObject *source, GAsyncResult *result, gpointer user_data) { GetManagedObjectsData *gmod = user_data; CockpitDBusCache *self = gmod->self; GError *error = NULL; GVariant *retval; retval = g_dbus_connection_call_finish (G_DBUS_CONNECTION (source), result, &error); if (error) { if (!g_cancellable_is_cancelled (self->cancellable)) { /* Doesn't support ObjectManager? */ if (dbus_error_matches_unknown (error)) { g_debug ("%s: no ObjectManager at %s", self->logname, gmod->path); } else { g_message ("%s: couldn't get managed objects at %s: %s", self->logname, gmod->path, error->message); } } g_error_free (error); } if (retval) { g_debug ("%s: reply from GetManagedObjects() on %s", self->logname, gmod->path); /* Note that this is indeed an object manager */ cockpit_paths_add (self->managed, gmod->path); process_get_managed_objects (self, gmod->batch, gmod->path, retval); g_variant_unref (retval); } /* * The ObjectManager itself still needs introspecting ... since the * ObjectManager path itself cannot be included in the objects reported * by the ObjectManager ... dumb design decision in the dbus spec IMO. * * But we delay on this so that any children are treated as part of * object manager, and we don't go introspecting everything. */ introspect_queue (self, gmod->batch, gmod->path, NULL, NULL, NULL); batch_unref (self, gmod->batch); g_object_unref (gmod->self); g_slice_free (GetManagedObjectsData, gmod); } static void retrieve_managed_objects (CockpitDBusCache *self, const gchar *namespace_path, BatchData *batch) { GetManagedObjectsData *gmod; g_assert (namespace_path != NULL); gmod = g_slice_new0 (GetManagedObjectsData); gmod->batch = batch_ref (batch); gmod->path = namespace_path; gmod->self = g_object_ref (self); g_debug ("%s: calling GetManagedObjects() on %s", self->logname, namespace_path); g_dbus_connection_call (self->connection, self->name, namespace_path, "org.freedesktop.DBus.ObjectManager", "GetManagedObjects", g_variant_new ("()"), G_VARIANT_TYPE ("(a{oa{sa{sv}}})"), G_DBUS_CALL_FLAGS_NONE, -1, /* timeout */ self->cancellable, on_get_managed_objects_reply, gmod); } void cockpit_dbus_cache_watch (CockpitDBusCache *self, const gchar *path, gboolean is_namespace, const gchar *interface) { const gchar *namespace_path; BatchData *batch; if (!cockpit_dbus_rules_add (self->rules, path, is_namespace, interface, NULL, NULL)) return; if (!path) { path = "/"; is_namespace = TRUE; } batch = batch_create (self); path = intern_string (self, path); namespace_path = is_namespace ? path : NULL; if (!namespace_path) namespace_path = cockpit_paths_contain_or_ancestor (self->managed, path); if (namespace_path) { retrieve_managed_objects (self, namespace_path, batch); } else { introspect_queue (self, batch, path, NULL, NULL, NULL); } batch_unref (self, batch); } gboolean cockpit_dbus_cache_unwatch (CockpitDBusCache *self, const gchar *path, gboolean is_namespace, const gchar *interface) { return cockpit_dbus_rules_remove (self->rules, path, is_namespace, interface, NULL, NULL); } static void scrape_variant_paths (GVariant *data, GHashTable *paths) { GVariantIter iter; GVariant *child; if (g_variant_is_of_type (data, G_VARIANT_TYPE_OBJECT_PATH)) { g_hash_table_add (paths, (gchar *)g_variant_get_string (data, NULL)); } else if (g_variant_is_container (data)) { g_variant_iter_init (&iter, data); while ((child = g_variant_iter_next_value (&iter)) != NULL) { scrape_variant_paths (child, paths); g_variant_unref (child); } } } void cockpit_dbus_cache_barrier (CockpitDBusCache *self, CockpitDBusBarrierFunc callback, gpointer user_data) { BatchData *batch; BarrierData *barrier; g_return_if_fail (callback != NULL); batch = g_queue_peek_head (self->batches); if (batch) { barrier = g_slice_new0 (BarrierData); barrier->number = batch->number; barrier->callback = callback; barrier->user_data = user_data; g_queue_push_tail (self->barriers, barrier); } else { (callback) (self, user_data); } } static void scrape_variant (CockpitDBusCache *self, BatchData *batch, GVariant *data) { GHashTable *paths; GHashTableIter iter; gpointer path; paths = g_hash_table_new (g_str_hash, g_str_equal); scrape_variant_paths (data, paths); if (batch) batch = batch_ref (batch); g_hash_table_iter_init (&iter, paths); while (g_hash_table_iter_next (&iter, &path, NULL)) { /* Used as a NULL path, we never use it when scraped */ if (g_str_equal (path, "/")) continue; /* Do we have it already? */ if (g_hash_table_lookup (self->cache, path)) continue; /* Is it a managed path? */ if (cockpit_paths_contain_or_ancestor (self->managed, path)) continue; /* Does it fit our rules */ if (!cockpit_dbus_rules_match (self->rules, path, NULL, NULL, NULL)) continue; if (!batch) batch = batch_create (self); introspect_queue (self, batch, intern_string (self, path), NULL, NULL, NULL); } if (batch) batch_unref (self, batch); g_hash_table_destroy (paths); } void cockpit_dbus_cache_scrape (CockpitDBusCache *self, GVariant *data) { scrape_variant (self, NULL, data); } typedef struct { const gchar *path; BatchData *batch; } PokeData; static void process_poke (CockpitDBusCache *self, GDBusInterfaceInfo *iface, gpointer user_data) { PokeData *pd = user_data; retrieve_properties (self, pd->batch, pd->path, iface); batch_unref (self, pd->batch); g_slice_free (PokeData, pd); } void cockpit_dbus_cache_poke (CockpitDBusCache *self, const gchar *path, const gchar *interface) { GHashTable *interfaces; BatchData *batch; PokeData *pd; /* Check if we have this thing */ interfaces = g_hash_table_lookup (self->cache, path); if (interfaces) { if (!interface) return; else if (g_hash_table_lookup (interfaces, interface)) return; } /* Is it a managed path? */ if (cockpit_paths_contain_or_ancestor (self->managed, path)) return; /* Does it fit our rules */ if (!cockpit_dbus_rules_match (self->rules, path, interface, NULL, NULL)) return; batch = batch_create (self); path = intern_string (self, path); if (interface) { /* * A specific interface was poked. This means that we don't have to * go interspecting the entire path ... if we already have information * about the interface itself. So try that route. */ pd = g_slice_new0 (PokeData); pd->path = path; pd->batch = batch_ref (batch); introspect_maybe (self, batch, path, interface, process_poke, pd); } else { /* The entire path was poked, must introspect to find out about it */ introspect_queue (self, batch, path, NULL, NULL, NULL); } batch_unref (self, batch); } CockpitDBusCache * cockpit_dbus_cache_new (GDBusConnection *connection, const gchar *name, const gchar *logname, GHashTable *interface_info) { return g_object_new (COCKPIT_TYPE_DBUS_CACHE, "connection", connection, "name", name, "logname", logname, "interface-info", interface_info, NULL); } GHashTable * cockpit_dbus_interface_info_new (void) { /* The key is owned by the value */ return g_hash_table_new_full (g_str_hash, g_str_equal, NULL, (GDestroyNotify)g_dbus_interface_info_unref); } GDBusInterfaceInfo * cockpit_dbus_interface_info_lookup (GHashTable *interface_info, const gchar *interface_name) { g_return_val_if_fail (interface_info != NULL, NULL); g_return_val_if_fail (interface_name != NULL, NULL); return g_hash_table_lookup (interface_info, interface_name); } void cockpit_dbus_interface_info_push (GHashTable *interface_info, GDBusInterfaceInfo *iface) { g_return_if_fail (interface_info != NULL); g_return_if_fail (iface != NULL); g_hash_table_replace (interface_info, iface->name, g_dbus_interface_info_ref (iface)); }