Search code examples
cdbusgdbus

Is there a way to check if someone listens to dbus signal?


Is there a way to check for listening clients in DBus?

Is it even possible to do? I'm using gdbus.

Background

I'm creating service which interfaces with serial ports and I want to implicitly open serial ports if someone is listening and automatically close it if last client disconnects. I could do it with open/close methods but there is risk that one client closes connection when other is still listening.

Another solution to my problem would be connection counting, but there is also risk that client forgets to close port or crashes.

Do you have any other idea how to implement this?

My code (shortened)

Based on: https://github.com/bratsche/glib/blob/master/gio/tests/gdbus-example-server.c

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

#ifdef G_OS_UNIX
#include <unistd.h>
#endif

/* ---------------------------------------------------------------------------------------------------- */

static GDBusNodeInfo *introspection_data = NULL;

/* Introspection data for the service we are exporting */
static const gchar introspection_xml[] =
  "<node>"
  "  <interface name='info.skorepa.serial.port'>"
  "    <signal name='DataRecieved'>"
  "      <arg type='ay' name='data'/>"
  "    </signal>"
  "  </interface>"
  "</node>";

/* ---------------------------------------------------------------------------------------------------- */

static void
handle_method_call (GDBusConnection       *connection,
                    const gchar           *sender,
                    const gchar           *object_path,
                    const gchar           *interface_name,
                    const gchar           *method_name,
                    GVariant              *parameters,
                    GDBusMethodInvocation *invocation,
                    gpointer               user_data)
{
  // nothing - signal only
}

static GVariant *
handle_get_property (GDBusConnection  *connection,
                     const gchar      *sender,
                     const gchar      *object_path,
                     const gchar      *interface_name,
                     const gchar      *property_name,
                     GError          **error,
                     gpointer          user_data)
{
  // nothing - signal only
}

static gboolean
handle_set_property (GDBusConnection  *connection,
                     const gchar      *sender,
                     const gchar      *object_path,
                     const gchar      *interface_name,
                     const gchar      *property_name,
                     GVariant         *value,
                     GError          **error,
                     gpointer          user_data)
{
  // nothing - no properties
}


/* for now */
static const GDBusInterfaceVTable interface_vtable =
{
  handle_method_call,
  handle_get_property,
  handle_set_property
};

/* ---------------------------------------------------------------------------------------------------- */
// Here I emit signal - for now I just emit every 2 seconds
static gboolean
on_timeout_cb (gpointer user_data)
{
  GDBusConnection *connection = G_DBUS_CONNECTION (user_data);
  GVariantBuilder *builder;
  GVariantBuilder *invalidated_builder;
  GError *error;

  error = NULL;
  printf("Constructing array\n");
  builder = g_variant_builder_new (G_VARIANT_TYPE ("ay"));
  printf("Adding 65\n");
  g_variant_builder_add (builder,
                         "y",
                         65);
  printf("Adding 66\n");
  g_variant_builder_add (builder,
                         "y",
                         66);
  printf("Emitting signal\n");
  g_dbus_connection_emit_signal (connection,
                                 NULL,
                                 "/info/skorepa/TestObject",
                                 "info.skorepa.serial.port",
                                 "DataRecieved",
                                 g_variant_new ("(ay)",
                                                builder),
                                 &error);
  printf("Checking for errors\n");
  g_assert_no_error (error);


  return TRUE;
}

/* ---------------------------------------------------------------------------------------------------- */

static void
on_bus_acquired (GDBusConnection *connection,
                 const gchar     *name,
                 gpointer         user_data)
{
  guint registration_id;

  registration_id = g_dbus_connection_register_object (connection,
                                                       "/info/skorepa/TestObject",
                                                       introspection_data->interfaces[0],
                                                       &interface_vtable,
                                                       NULL,  /* user_data */
                                                       NULL,  /* user_data_free_func */
                                                       NULL); /* GError** */
  g_assert (registration_id > 0);

  /* swap value of properties Foo and Bar every two seconds */
  g_timeout_add_seconds (2,
                         on_timeout_cb,
                         connection);
}

static void
on_name_acquired (GDBusConnection *connection,
                  const gchar     *name,
                  gpointer         user_data)
{
}

static void
on_name_lost (GDBusConnection *connection,
              const gchar     *name,
              gpointer         user_data)
{
  exit (1);
}

int
main (int argc, char *argv[])
{
  guint owner_id;
  GMainLoop *loop;

  g_type_init ();

  introspection_data = g_dbus_node_info_new_for_xml (introspection_xml, NULL);
  g_assert (introspection_data != NULL);

  owner_id = g_bus_own_name (G_BUS_TYPE_SESSION,
                             "info.skorepa.serial",
                             G_BUS_NAME_OWNER_FLAGS_NONE,
                             on_bus_acquired,
                             on_name_acquired,
                             on_name_lost,
                             NULL,
                             NULL);

  loop = g_main_loop_new (NULL, FALSE);
  g_main_loop_run (loop);

  g_bus_unown_name (owner_id);

  g_dbus_node_info_unref (introspection_data);

  return 0;
}

Compiled using:

gcc signal-sample.c `pkg-config --cflags --libs glib-2.0 gio-2.0` -o test

Thank you


Solution

  • Is there a way to check for listening clients in DBus?

    No, it is not possible, due to the way D-Bus is designed.

    When a client wants to subscribe to a signal, they send an AddMatch method call to the D-Bus daemon, which registers that state internally. When your service emits a signal, it sends the signal to the D-Bus daemon, which then forwards it to the clients who have subscribed to that signal (subject to various policy rules about broadcasts and permissions). Your service can’t know about the internal subscription state in the D-Bus daemon.

    The pattern for handling this kind of thing is for your service to explicitly expose a subscribe or open method which clients must call in order to open a serial port. You can have a second method for closing the serial port when the client is done with it; and you can also listen for client disconnections to close the ports automatically. (In GDBus, this is done using g_bus_watch_name() and passing it the unique name of the client, which looks something like :1.5.)