1

The function g_dbus_connection_signal_subscribe works great for telling me when new DBus objects appear (or vanish) with InterfacesAdded signal (or InterfacesRemoved signal). But I need for to know about pre-existing objects/interfaces.

I wrote the following piece of C code to provide callbacks when DBus objects are added/removed from the bus. Error-checking omitted for simplicity.

#include <stdio.h>
#include <stdlib.h>
#include <gio/gio.h>

static void signal_cb(GDBusConnection *connection,
        const gchar *sender_name,const gchar *object_path,
        const gchar *interface_name,const gchar *signal_name,
        GVariant *parameters,gpointer user_data)
{
    printf("%s: %s.%s %s\n",object_path,interface_name,signal_name,
        g_variant_print(parameters,TRUE));
}

int main(int argc,char *argv[])
{
GDBusConnection *c;
GMainLoop *loop;
int filter_id;

    c = g_bus_get_sync(G_BUS_TYPE_SYSTEM,NULL,&err);
    loop = g_main_loop_new(NULL,0);
    filter_id = g_dbus_connection_signal_subscribe(c,
        "org.bluez",NULL,NULL,NULL,NULL,
        G_DBUS_SIGNAL_FLAGS_NONE,signal_cb,NULL,NULL);   
    g_main_loop_run (loop);
    g_main_loop_unref (loop);

    exit(0);
}

So what I'm trying to do is keep track of all the DBus objects that exist under the org.bluez branch of the tree. (These represent pluggable Bluetooth controllers and the devices that are discovered by each controller). I need to know about DBus objects that were already there before my program started, and I need to know about new objects that appear after my program starts.

My code above tells me about new objects, but nothing about the objects that are already there. Is there a way in the gdbus API to get an "InterfacesCreated" signal for objects that already exist? I suppose could read the entire DBus object hierarchy and then subscribe to changes, but that leads to race conditions where if an object appears between the time I read the object hierarchy and the time I subscribe, then I would miss those objects....

What's the best-practice way to accomplish this with the gdbus API?

deltamind106
  • 638
  • 7
  • 19
  • Have you looked at the introspection functions? https://developer.gnome.org/gio//2.50/gio-D-Bus-Introspection-Data.html – Paul Ogilvie Jan 10 '19 at 15:24
  • Those functions are for asking about the details of nodes whose names you already know. I need to actually discover what node names exist. Furthermore, even if they could provide the discovery of what nodes exist, it leads to two totally separate code paths for doing the same thing: one code path to discover pre-existing nodes and query them, and another code path to discover new nodes that are added. This seems like such poor design for an API, that surely there must be a better way. – deltamind106 Jan 10 '19 at 15:28
  • 1
    @deltamind106 Signals are meant for providing you hint when they appear after your program starts. By getting the existing objects/interfaces, you need to use `GetManagedObjects` in `org.freedesktop.DBus.ObjectManager` to scan for it. Register the signal handler before scanning for the existing interfaces, so when a new one appears, you will be notified with signal callback. But you need to maintain your synchronization or manage object/interface duplication inside your code. – Parthiban Jan 11 '19 at 10:01
  • @Parthiban That's exactly what I ended up doing: calling GetManagedObjects from the org.freedesktop.DBus.ObjectManager interface. This is far from intuitive by a simple examination of the documentation. I figured it out by looking at the source code to the bluetoothctl utility, which basically had to do the same thing I wanted to do. The return value from GetManagedObjects is a triple-nested dictionary, and wading through it with gdbus GVariant calls is painful, again mostly due to extremely poor documentation. – deltamind106 Jan 11 '19 at 18:51
  • You can find examples in linumiz.com and gist.github.com/parthitce which are related to Bluez. – Parthiban Jan 11 '19 at 18:55
  • @Parthiban I guess I still feel that the design of the gdbus subscribe API is lacking. Usually when you subscribe to something, you would often like to receive an initial event describing the initial state of whatever it is you're subscribing to. The gdbus API doesn't provide such a mechanism, hence the need to call GetManagedObjects, and dig through layers of nested dictionaries with GVariant. Not to mention handling the race conditions since you now need to use multiple threads. – deltamind106 Jan 11 '19 at 19:08
  • @deltamind106 there's no need to use threads with GDBus: everything can be done asynchronously. – Jussi Kukkonen Jan 11 '19 at 19:26
  • 1
    @jku Well, as soon as you make an asynchronous gdbus call... guess what? The callback that the library eventually invokes to provide you with the results you requested are.... wait for it.... invoked from a different thread. So no, the fact that there are asynchronous calls in the gdbus library does not magically eliminate the need to deal with race conditions caused by multithreading. Sure *your code* doesn't need to create other threads, but the library is doing it for you behind the scenes. – deltamind106 Jan 22 '19 at 17:24

1 Answers1

4

In case anyone happens upon this, as Parthiban noted, the solution (for D-Bus services which implement the org.freedesktop.DBus.ObjectManager interface, such as BlueZ) is to call the GetManagedObjects method. This is extremely painful to code in C with GDBus since it requires detailed understanding of GVariant typing; see the documentation. Also check out the docs on GVariant data type strings. But here's how it's done:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <pthread.h>

#include <gio/gio.h>


/* The GVariant must be of type "a{sa{sv}}" (array of interfaces, where each */
/* interface has an array of properties). */
/* This type what DBus provides with InterfacesAdded signal and is also */
/* part of the return value from the GetManagedObjects method. */
static void proc_interface_var(const gchar *objpath,GVariant *iflist)
{
GVariantIter *iter,*iter2;
gchar *ifname,*propname,*proptext;
GVariant *propvalue;

    g_variant_get(iflist,"a{sa{sv}}",&iter);
    while (g_variant_iter_loop(iter,"{sa{sv}}",&ifname,&iter2)) {
        if (strcmp(ifname,"org.bluez.Adatper1") != 0 &&
                                    strcmp(ifname,"org.bluez.Device1") != 0) {
            /* we only care about the Adatper1 and Device1 interfaces */
            continue;
        } /* if */
        printf("Interface %s added to object %s\n",ifname,objpath);
        while (g_variant_iter_loop(iter2,"{sv}",&propname,&propvalue)) {
            proptext = g_variant_print(propvalue,0);
            printf("\t%s=%s\n",propname,proptext);
            g_free(proptext);
        } /* while */
    } /* while */
    return;
} /* proc_interface_var */


/* The GVariant must be of type "a{sv}" (an array of properties). */
static void proc_property_var(const gchar *objpath,
                                        gchar *ifname,GVariant *proplist)
{
GVariantIter *iter;
gchar *propname,*proptext;
GVariant *propvalue;

    g_variant_get(proplist,"a{sv}",&iter);
    while (g_variant_iter_loop(iter,"{sv}",&propname,&propvalue)) {
        proptext = g_variant_print(propvalue,0);
        printf("\tProperty changed on object %s interface %s: %s=%s\n",
            objpath,ifname,propname,proptext);
        g_free(proptext);
    } /* while */
    return;
} /* proc_property_var */


static void signal_cb(GDBusConnection *c,
            const gchar *sender_name,const gchar *object_path,
            const gchar *interface_name,const gchar *signal_name,
            GVariant *parameters,gpointer user_data)
{
char fullsignal[200];
gchar *s,*objpath,*ifname,*propname,*proptext;
GVariant *ifvar,*propvalue;
GVariantIter *iter,*iter2;

    snprintf(fullsignal,200,"%s.%s",interface_name,signal_name);
    if (strcmp(fullsignal,
                "org.freedesktop.DBus.ObjectManager.InterfacesAdded") == 0) {
        g_variant_get(parameters,"(o*)",&objpath,&ifvar);
        proc_interface_var(objpath,ifvar);
    } else if (strcmp(fullsignal,
                "org.freedesktop.DBus.Properties.PropertiesChanged") == 0) {
        g_variant_get(parameters,"(s*as)",&ifname,&propvalue,&iter2);
        proc_property_var(object_path,ifname,propvalue);
        while (g_variant_iter_loop(iter2,"s",&propname)) {
            printf("\tProperty changed on object %s interface %s: "
                "%s is nil\n",object_path,ifname,propname);
        } /* while */
    } else {
        printf("Ignoring unsupported signal for object %s, "
            "signal=%s.%s, param type=%s\n",
            object_path,interface_name,signal_name,
            g_variant_get_type_string(parameters));
        s = g_variant_print(parameters,TRUE);
        printf("Unsupported signal: parameters %s\n",s);
        g_free(s);
    } /* else */

    return;
} /* signal_cb */


static void bt_discover(GDBusConnection *c,const char *ctlname,int on_off)
{
GError *err=NULL;
GVariant *result;
const char *method;
char ctlpath[80];

    snprintf(ctlpath,80,"/org/bluez/%s",ctlname);
    method = on_off ? "StartDiscovery" : "StopDiscovery";
    result = g_dbus_connection_call_sync(c,"org.bluez",ctlpath,
        "org.bluez.Adapter1",method,NULL,
        G_VARIANT_TYPE("()"),       /* return-type */
        G_DBUS_CALL_FLAGS_NONE,5000,NULL,&err);
    if (result==NULL) {
        if (err) fprintf(stderr,"g_dbus_connection_call error: %s\n",   
            err->message);
        exit(1);
    } /* if */
    g_variant_unref(result);
    return;
} /* bt_discover */


static void *receive_dbus_signals(void *arg)
{
GMainLoop *loop;

    printf("Receiving DBus signals...\n");
    loop = g_main_loop_new(NULL,0);
    g_main_loop_run(loop);
    g_main_loop_unref(loop);
    return NULL;
} /* receive_dbus_signals */


int main(int argc,char *argv[])
{
GError *err=NULL;
GVariant *result,*ifvar;
GVariantIter *iter;
GDBusConnection *c;
GDBusNodeInfo *node;
gchar *objpath;
pthread_t handle;
int filter_id;

    if ((c = g_bus_get_sync(G_BUS_TYPE_SYSTEM,NULL,&err)) == NULL) {
        if (err) fprintf(stderr,"g_bus_get error: %s\n",err->message);
        exit(1);
    } /* if */

    filter_id = g_dbus_connection_signal_subscribe(c,
        "org.bluez",NULL,NULL,NULL,NULL,
        G_DBUS_SIGNAL_FLAGS_NONE,signal_cb,NULL,NULL);
    if (pthread_create(&handle,NULL,receive_dbus_signals,NULL) != 0) {
        fprintf(stderr,"Failed to create DBus listen thread\n");
        exit(1);
    } /* if */

    result = g_dbus_connection_call_sync(c,"org.bluez","/",
        "org.freedesktop.DBus.ObjectManager","GetManagedObjects",NULL,
        G_VARIANT_TYPE("(a{oa{sa{sv}}})"),      /* return-type */
        G_DBUS_CALL_FLAGS_NONE,5000,NULL,&err);
    if (result==NULL) {
        if (err) fprintf(stderr,"g_dbus_connection_call error: %s\n",   
            err->message);
        exit(1);
    } /* if */

    g_variant_get(result,"(a{oa{sa{sv}}})",&iter);
    /* below we replace 'a{sa{sv}}' with '*' to get it as a GVariant */
    while (g_variant_iter_loop(iter,"{o*}",&objpath,&ifvar)) {
        proc_interface_var(objpath,ifvar);
    } /* while */
    g_variant_unref(result);

    bt_discover(c,"hci0",1);
    sleep(5);
    bt_discover(c,"hci0",0);
    sleep(5);

    exit(0);
}

For D-Bus services which don’t implement the org.freedesktop.DBus.ObjectManager interface, you need to use D-Bus introspection and parse the introspection XML to find the paths of the existing object nodes.

Philip Withnall
  • 5,293
  • 14
  • 28
deltamind106
  • 638
  • 7
  • 19
  • Note: The ‘extremely poor’ documentation will not improve unless you file an [upstream bug report](https://gitlab.gnome.org/GNOME/glib/issues/new?issue%5Bassignee_id%5D=&issue%5Bmilestone_id%5D=) about it, with specific issues to address, or suggestions for how to address them. – Philip Withnall Jan 15 '19 at 12:22
  • I improved your answer to mention what happens when the `org.freedesktop.DBus.ObjectManager` interface isn’t implemented by the service. – Philip Withnall Jan 15 '19 at 12:26
  • Yeah I've tried filing reports to improve documentation before, but it always goes nowhere. The trouble is not that people need to be told where the documentation need improvement, everyone knows that already. The problem is there is just no resource to do it. It's not like there are a few typos here and there that anyone could fix-- the documentation needs to be doubled with more explanations and more sample code. The developers are really the only ones who can adequately write this kind of documentation, and they're busy fixing bugs in the code and satisfying requests for improvements. – deltamind106 Jan 22 '19 at 17:15
  • 1
    The developers (like me) are also the least capable of knowing which documentation is inadequate, as they are so well acquainted with the API that they never read the documentation; and even if they did, they mentally fill in any gaps in it. We can act on specific feedback from users about problems with the documentation. :-) – Philip Withnall Jan 22 '19 at 18:14