/* Spa dbus * * Copyright © 2022 Pauli Virtanen * * Permission is hereby granted, free of charge, to any person obtaining a * copy of this software and associated documentation files (the "Software"), * to deal in the Software without restriction, including without limitation * the rights to use, copy, modify, merge, publish, distribute, sublicense, * and/or sell copies of the Software, and to permit persons to whom the * Software is furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice (including the next * paragraph) shall be included in all copies or substantial portions of the * Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER * DEALINGS IN THE SOFTWARE. */ #include #include #include #include #include #include #include #include "dbus-monitor.h" #undef SPA_LOG_TOPIC_DEFAULT #define SPA_LOG_TOPIC_DEFAULT (&impl->log_topic) #define DBUS_INTERFACE_OBJECT_MANAGER "org.freedesktop.DBus.ObjectManager" struct impl { struct spa_dbus_monitor this; DBusConnection *conn; struct spa_log_topic log_topic; struct spa_log *log; DBusPendingCall *get_managed_objects_call; unsigned int filters_added:1; unsigned int objects_listed:1; struct spa_list objects[]; }; struct object { unsigned int removed:1; unsigned int updating:1; union { struct spa_dbus_object this; /* extra data follows 'this', force safer alignment */ long double _align; }; }; static void object_emit_update(struct object *o) { if (o->this.interface->update) o->this.interface->update(&o->this); } static void object_emit_remove(struct object *o) { if (o->this.interface->remove) o->this.interface->remove(&o->this); } static void object_emit_property(struct object *o, const char *key, DBusMessageIter *value) { if (o->this.interface->property) o->this.interface->property(&o->this, key, value); } static struct object *object_new(struct impl *impl, const struct spa_dbus_interface *iface, struct spa_list *object_list, const char *path) { struct object *o; spa_assert(iface->object_size >= sizeof(struct spa_dbus_object)); o = calloc(1, sizeof(struct object) + iface->object_size - sizeof(struct spa_dbus_object)); if (o == NULL) return NULL; o->this.path = strdup(path); o->this.interface = iface; o->this.user_data = impl->this.user_data; spa_assert(o->this.path); spa_list_append(object_list, &o->this.link); return o; } static void object_remove(struct object *o) { if (o->removed) return; o->updating = true; o->removed = true; object_emit_remove(o); spa_list_remove(&o->this.link); } static void object_destroy(struct object *o) { object_remove(o); free((void *)o->this.path); free(o); } static const struct spa_dbus_interface *interface_find(struct impl *impl, const char *interface, struct spa_list **objects) { const struct spa_dbus_interface *iface; size_t i; for (iface = impl->this.interfaces, i = 0; iface && iface->name; ++iface, ++i) { if (spa_streq(iface->name, interface)) { *objects = &impl->objects[i]; return iface; } } return NULL; } static struct object *object_find(struct spa_list *object_list, const char *path) { struct spa_dbus_object *object; spa_list_for_each(object, object_list, link) { struct object *o = SPA_CONTAINER_OF(object, struct object, this); if (spa_streq(o->this.path, path)) return o; } return NULL; } static int object_update_props(struct impl *impl, struct object *o, DBusMessageIter *props_iter, DBusMessageIter *invalidated_iter) { o->updating = true; if (o->this.interface->property) { while (invalidated_iter && dbus_message_iter_get_arg_type(invalidated_iter) != DBUS_TYPE_INVALID) { const char *key; dbus_message_iter_get_basic(invalidated_iter, &key); object_emit_property(o, key, NULL); if (o->removed) goto removed; dbus_message_iter_next(invalidated_iter); } while (props_iter && dbus_message_iter_get_arg_type(props_iter) != DBUS_TYPE_INVALID) { DBusMessageIter entry, value; const char *key; dbus_message_iter_recurse(props_iter, &entry); dbus_message_iter_get_basic(&entry, &key); dbus_message_iter_next(&entry); dbus_message_iter_recurse(&entry, &value); object_emit_property(o, key, &value); if (o->removed) goto removed; dbus_message_iter_next(props_iter); } } object_emit_update(o); if (o->removed) goto removed; o->updating = false; return 0; removed: object_destroy(o); return 0; } static void interface_added(struct impl *impl, const char *object_path, const char *interface_name, DBusMessageIter *props_iter) { struct object *o; const struct spa_dbus_interface *iface; struct spa_list *object_list; iface = interface_find(impl, interface_name, &object_list); if (!iface) { spa_log_trace(impl->log, "dbus: skip path=%s, interface=%s", object_path, interface_name); return; } spa_log_debug(impl->log, "dbus: added path=%s, interface=%s", object_path, interface_name); o = object_find(object_list, object_path); if (o == NULL) { o = object_new(impl, iface, object_list, object_path); if (o == NULL) { spa_log_warn(impl->log, "can't create object: %m"); return; } } object_update_props(impl, o, props_iter, NULL); } static void interfaces_added(struct impl *impl, DBusMessageIter *arg_iter) { DBusMessageIter it[3]; const char *object_path; dbus_message_iter_get_basic(arg_iter, &object_path); dbus_message_iter_next(arg_iter); dbus_message_iter_recurse(arg_iter, &it[0]); while (dbus_message_iter_get_arg_type(&it[0]) != DBUS_TYPE_INVALID) { const char *interface_name; dbus_message_iter_recurse(&it[0], &it[1]); dbus_message_iter_get_basic(&it[1], &interface_name); dbus_message_iter_next(&it[1]); dbus_message_iter_recurse(&it[1], &it[2]); interface_added(impl, object_path, interface_name, &it[2]); dbus_message_iter_next(&it[0]); } } static void interfaces_removed(struct impl *impl, DBusMessageIter *arg_iter) { const char *object_path; DBusMessageIter it; dbus_message_iter_get_basic(arg_iter, &object_path); dbus_message_iter_next(arg_iter); dbus_message_iter_recurse(arg_iter, &it); while (dbus_message_iter_get_arg_type(&it) != DBUS_TYPE_INVALID) { const char *interface_name; const struct spa_dbus_interface *iface; struct spa_list *object_list; dbus_message_iter_get_basic(&it, &interface_name); iface = interface_find(impl, interface_name, &object_list); if (iface) { struct object *o; spa_log_debug(impl->log, "dbus: removed path=%s, interface=%s", object_path, interface_name); o = object_find(object_list, object_path); if (o) object_destroy(o); } else { spa_log_trace(impl->log, "dbus: skip removed path=%s, interface=%s", object_path, interface_name); } dbus_message_iter_next(&it); } } static void get_managed_objects_reply(DBusPendingCall *pending, void *user_data) { struct impl *impl = user_data; DBusMessage *r; DBusMessageIter it[6]; spa_assert(pending == impl->get_managed_objects_call); impl->get_managed_objects_call = NULL; r = dbus_pending_call_steal_reply(pending); dbus_pending_call_unref(pending); if (r == NULL) return; if (dbus_message_is_error(r, DBUS_ERROR_UNKNOWN_METHOD)) { spa_log_warn(impl->log, "BlueZ D-Bus ObjectManager not available"); goto done; } if (dbus_message_get_type(r) == DBUS_MESSAGE_TYPE_ERROR) { spa_log_error(impl->log, "GetManagedObjects() failed: %s", dbus_message_get_error_name(r)); goto done; } if (!dbus_message_iter_init(r, &it[0]) || !spa_streq(dbus_message_get_signature(r), "a{oa{sa{sv}}}")) { spa_log_error(impl->log, "Invalid reply signature for GetManagedObjects()"); goto done; } /* Add fake object representing the service itself */ interface_added(impl, impl->this.path, SPA_DBUS_MONITOR_NAME_OWNER_INTERFACE, NULL); dbus_message_iter_recurse(&it[0], &it[1]); while (dbus_message_iter_get_arg_type(&it[1]) != DBUS_TYPE_INVALID) { dbus_message_iter_recurse(&it[1], &it[2]); interfaces_added(impl, &it[2]); dbus_message_iter_next(&it[1]); } impl->objects_listed = true; done: dbus_message_unref(r); return; } static int get_managed_objects(struct impl *impl) { DBusMessage *m = NULL; DBusPendingCall *call; int res = 0; if (impl->objects_listed || impl->get_managed_objects_call) return 0; m = dbus_message_new_method_call(impl->this.service, impl->this.path, DBUS_INTERFACE_OBJECT_MANAGER, "GetManagedObjects"); if (m == NULL) return -ENOMEM; dbus_message_set_auto_start(m, false); if (!dbus_connection_send_with_reply(impl->conn, m, &call, -1)) { res = -EIO; goto done; } dbus_pending_call_set_notify(call, get_managed_objects_reply, impl, NULL); impl->get_managed_objects_call = call; done: if (m) dbus_message_unref(m); return res; } static DBusHandlerResult filter_cb(DBusConnection *bus, DBusMessage *m, void *user_data) { struct impl *impl = user_data; DBusError err; dbus_error_init(&err); if (dbus_message_is_signal(m, DBUS_INTERFACE_DBUS, "NameOwnerChanged")) { const char *name, *old_owner, *new_owner; bool has_old_owner, has_new_owner; spa_log_debug(impl->log, "dbus: name owner changed %s", dbus_message_get_path(m)); if (!dbus_message_get_args(m, &err, DBUS_TYPE_STRING, &name, DBUS_TYPE_STRING, &old_owner, DBUS_TYPE_STRING, &new_owner, DBUS_TYPE_INVALID)) { spa_log_error(impl->log, "Failed to parse org.freedesktop.DBus.NameOwnerChanged: %s", err.message); goto done; } if (!spa_streq(name, impl->this.service)) goto done; has_old_owner = old_owner && *old_owner; has_new_owner = new_owner && *new_owner; if (has_old_owner) spa_log_debug(impl->log, "dbus: %s disappeared", impl->this.service); if (has_old_owner || has_new_owner) { size_t i; const struct spa_dbus_interface *iface; impl->objects_listed = false; for (iface = impl->this.interfaces, i = 0; iface && iface->name; ++iface, ++i) { struct spa_dbus_object *object; spa_list_consume(object, &impl->objects[i], link) { struct object *o = SPA_CONTAINER_OF(object, struct object, this); object_destroy(o); } } } if (has_new_owner) { spa_log_debug(impl->log, "dbus: %s appeared", impl->this.service); get_managed_objects(impl); } } else if (dbus_message_is_signal(m, DBUS_INTERFACE_OBJECT_MANAGER, "InterfacesAdded")) { DBusMessageIter it; spa_log_debug(impl->log, "dbus: interfaces added on path=%s", dbus_message_get_path(m)); if (!impl->objects_listed) goto done; if (!dbus_message_iter_init(m, &it) || !spa_streq(dbus_message_get_signature(m), "oa{sa{sv}}")) { spa_log_error(impl->log, "Invalid signature found in InterfacesAdded"); goto done; } interfaces_added(impl, &it); } else if (dbus_message_is_signal(m, DBUS_INTERFACE_OBJECT_MANAGER, "InterfacesRemoved")) { DBusMessageIter it; spa_log_debug(impl->log, "dbus: interfaces removed on path=%s", dbus_message_get_path(m)); if (!impl->objects_listed) goto done; if (!dbus_message_iter_init(m, &it) || !spa_streq(dbus_message_get_signature(m), "oas")) { spa_log_error(impl->log, "Invalid signature found in InterfacesRemoved"); goto done; } interfaces_removed(impl, &it); } else if (dbus_message_is_signal(m, DBUS_INTERFACE_PROPERTIES, "PropertiesChanged")) { DBusMessageIter args, props, invalidated; const char *interface_name, *path; const struct spa_dbus_interface *iface; struct spa_list *object_list; if (!impl->objects_listed) goto done; if (!dbus_message_iter_init(m, &args) || !spa_streq(dbus_message_get_signature(m), "sa{sv}as")) { spa_log_error(impl->log, "Invalid signature found in PropertiesChanged"); goto done; } path = dbus_message_get_path(m); dbus_message_iter_get_basic(&args, &interface_name); dbus_message_iter_next(&args); dbus_message_iter_recurse(&args, &props); dbus_message_iter_next(&args); dbus_message_iter_recurse(&args, &invalidated); iface = interface_find(impl, interface_name, &object_list); if (iface) { struct object *o; o = object_find(object_list, path); if (o == NULL) { spa_log_debug(impl->log, "Properties changed in unknown object %s", path); goto done; } spa_log_debug(impl->log, "dbus: properties changed in path=%s", path); object_update_props(impl, o, &props, &invalidated); } } done: dbus_error_free(&err); return DBUS_HANDLER_RESULT_NOT_YET_HANDLED; } static int add_filters(struct impl *impl) { DBusError err; char rule[1024]; const struct spa_dbus_interface *iface; if (impl->filters_added) return 0; if (!dbus_connection_add_filter(impl->conn, filter_cb, impl, NULL)) { spa_log_error(impl->log, "failed to add DBus filter"); return -EIO; } impl->filters_added = true; dbus_error_init(&err); spa_scnprintf(rule, sizeof(rule), "type='signal',sender='org.freedesktop.DBus'," "interface='org.freedesktop.DBus',member='NameOwnerChanged'," "arg0='%s'", impl->this.service); spa_log_trace(impl->log, "add match: %s", rule); dbus_bus_add_match(impl->conn, rule, &err); if (dbus_error_is_set(&err)) goto fail; spa_scnprintf(rule, sizeof(rule), "type='signal',sender='%s'," "interface='org.freedesktop.DBus.ObjectManager',member='InterfacesAdded'", impl->this.service); spa_log_trace(impl->log, "add match: %s", rule); dbus_bus_add_match(impl->conn, rule, &err); if (dbus_error_is_set(&err)) goto fail; spa_scnprintf(rule, sizeof(rule), "type='signal',sender='%s'," "interface='org.freedesktop.DBus.ObjectManager',member='InterfacesRemoved'", impl->this.service); spa_log_trace(impl->log, "add match: %s", rule); dbus_bus_add_match(impl->conn, rule, &err); if (dbus_error_is_set(&err)) goto fail; for (iface = impl->this.interfaces; iface && iface->name; ++iface) { spa_scnprintf(rule, sizeof(rule), "type='signal',sender='%s'," "interface='org.freedesktop.DBus.Properties',member='PropertiesChanged'," "arg0='%s'", impl->this.service, iface->name); spa_log_trace(impl->log, "add match: %s", rule); dbus_bus_add_match(impl->conn, rule, &err); if (dbus_error_is_set(&err)) goto fail; } dbus_error_free(&err); return 0; fail: spa_log_error(impl->log, "failed to add DBus match: %s", err.message); dbus_error_free(&err); return -EIO; } struct spa_dbus_monitor *spa_dbus_monitor_new(DBusConnection *conn, const char *service, const char *path, const struct spa_dbus_interface *interfaces, struct spa_log *log, void *user_data) { struct impl *impl; const struct spa_dbus_interface *iface; size_t num_interfaces = 0, i; for (iface = interfaces; iface && iface->name; ++iface) { spa_assert(iface->object_size >= sizeof(struct spa_dbus_object)); ++num_interfaces; } impl = calloc(1, sizeof(struct impl) + sizeof(struct spa_list) * num_interfaces); if (impl == NULL) return NULL; dbus_connection_ref(conn); impl->conn = conn; impl->this.service = strdup(service); impl->this.path = strdup(path); impl->this.interfaces = interfaces; impl->this.user_data = user_data; impl->log = log; impl->log_topic = SPA_LOG_TOPIC(0, "spa.bluez5.dbus"); spa_log_topic_init(impl->log, &impl->log_topic); for (i = 0; i < num_interfaces; ++i) spa_list_init(&impl->objects[i]); spa_assert(impl->this.service); spa_assert(impl->this.path); add_filters(impl); get_managed_objects(impl); return &impl->this; } void spa_dbus_monitor_destroy(struct spa_dbus_monitor *this) { struct impl *impl = SPA_CONTAINER_OF(this, struct impl, this); const struct spa_dbus_interface *iface; struct spa_dbus_object *object; size_t i; if (impl->filters_added) { dbus_connection_remove_filter(impl->conn, filter_cb, impl); impl->filters_added = false; } if (impl->get_managed_objects_call) { dbus_pending_call_cancel(impl->get_managed_objects_call); dbus_pending_call_unref(impl->get_managed_objects_call); impl->get_managed_objects_call = NULL; } for (iface = impl->this.interfaces, i = 0; iface && iface->name; ++iface, ++i) { spa_list_consume(object, &impl->objects[i], link) { struct object *o = SPA_CONTAINER_OF(object, struct object, this); object_destroy(o); } } dbus_connection_unref(impl->conn); free((void *)impl->this.service); free((void *)impl->this.path); free(impl); } struct spa_dbus_object *spa_dbus_monitor_find(struct spa_dbus_monitor *this, const char *path, const char *interface) { struct impl *impl = SPA_CONTAINER_OF(this, struct impl, this); struct object *o; const struct spa_dbus_interface *iface; struct spa_list *object_list; if (path == NULL) return NULL; spa_assert(interface); iface = interface_find(impl, interface, &object_list); if (!iface) return NULL; o = object_find(object_list, path); if (o) return &o->this; return NULL; } void spa_dbus_monitor_ignore_object(struct spa_dbus_monitor *this, struct spa_dbus_object *object) { struct impl *impl = SPA_CONTAINER_OF(this, struct impl, this); struct object *o = SPA_CONTAINER_OF(object, struct object, this); spa_log_trace(impl->log, "ignore path=%s", o->this.path); if (o->updating) object_remove(o); else object_destroy(o); } struct spa_list *spa_dbus_monitor_object_list(struct spa_dbus_monitor *this, const char *interface) { struct impl *impl = SPA_CONTAINER_OF(this, struct impl, this); const struct spa_dbus_interface *iface; struct spa_list *object_list; iface = interface_find(impl, interface, &object_list); if (!iface) return NULL; return object_list; }