Notes on seeking wisdom and crafting software

Notifications in qt over DBus

Let’s talk about notifications in linux.

A notification is one of the simplest way to let the customer know about the status or progress of an activity that could be running in the background. Our use case was around pomito. A while ago, I wanted a simple way to show notifications on a window manager that doesn’t support a system tray.

DBus in ten lines

DBus is a message bus for applications to talk to each other. It provides a notification specification in addition to various other services. A human readable cheatsheet for DBus is here.

At it’s core, the DBus specification is quite generic. It means there’s a provider at one end (e.g. a notification server for our use case) and then there are the clients which send a message to the provider. A list of all providers can be found with the d-feet tool. A list of notification servers are in the archwiki.

For the curious, try peeking around a bit into DBus like

$ sudo pacman -S qt5-tools

# qdbus <servicename> <path>
$ qdbus org.freedesktop.Notifications /org/freedesktop/Notifications
method QString org.freedesktop.DBus.Introspectable.Introspect()
method QDBusVariant org.freedesktop.DBus.Properties.Get(QString interface, QString propname)
method QVariantMap org.freedesktop.DBus.Properties.GetAll(QString interface)
method void org.freedesktop.DBus.Properties.Set(QString interface, QString propname, QDBusVariant value)
method void org.freedesktop.Notifications.CloseNotification(uint id)
method QStringList org.freedesktop.Notifications.GetCapabilities()
method QString org.freedesktop.Notifications.GetServerInformation(QString& return_vendor, QString& return_version, QString& return_spec_version)
method uint org.freedesktop.Notifications.Notify(QString app_name, uint id, QString icon, QString summary, QString body, QStringList actions, QVariantMap hints, int timeout)

# Introspect (like Reflection) finds the details of various methods available in
# the service. Note the `interface` and `method` concepts here.
$ qdbus org.freedesktop.Notifications /org/freedesktop/Notifications org.freedesktop.DBus.Introspectable.Introspect
<!DOCTYPE node PUBLIC "-//freedesktop//DTD D-BUS Object Introspection 1.0//EN"
"http://www.freedesktop.org/standards/dbus/1.0/introspect.dtd">
<node>
  <interface name="org.freedesktop.DBus.Introspectable">
    <method name="Introspect">
      <arg name="data" direction="out" type="s"/>
    </method>
  </interface>
  <!-- trimmed data -->
  <interface name="org.freedesktop.Notifications">
      <!-- trimmed data -->
      <method name="Notify">
      <arg name="app_name" type="s" direction="in"/>
      <arg name="id" type="u" direction="in"/>
      <arg name="icon" type="s" direction="in"/>
      <arg name="summary" type="s" direction="in"/>
      <arg name="body" type="s" direction="in"/>
      <arg name="actions" type="as" direction="in"/>
      <arg name="hints" type="a{sv}" direction="in"/>
      <arg name="timeout" type="i" direction="in"/>
      <arg name="return_id" type="u" direction="out"/>
    </method>
  </interface>
</node>

Our options

Back to our use case. We need to invoke the Notify method on the org.freedesktop.Notifications interface in org.freedesktop.Notifications service. There are several approaches to do this:

  1. Fork a process, for example notify-send. I’ve been using this in my dot files for mpd and volume operations. Works great, but puts a dependency of notify-send in pomito. See [here][notify-send-ex] for an example. [notify-send-ex]: https://github.com/codito/configs/blob/master/.config/i3/config#L182

  2. Use another package. Most notable one was [notify2][]. Seems quite easy to use, requires dependency of python-dbus bindings which appeared to be [unstable][dbus-python-unstable]. [notify2]: https://pypi.python.org/pypi/notify2/0.3.1 [dbus-python-unstable]: https://pypi.python.org/pypi/dbus-python/

  3. Use the built-in QtDBus bindings in Qt framework.

Using the built-in qt bindings

There isn’t much documentation for this one. We will cover few diagnosis steps below. Anyways, here’s some code to send a notification from a PyQt app. I’ve annotated the code below to compare with notes above.

def notify(header, msg):
    # Below three properties are just identifying which method we need to invoke
    # on the DBus
    item = "org.freedesktop.Notifications"
    path = "/org/freedesktop/Notifications"
    interface = "org.freedesktop.Notifications"

    # Compare the below arguments with the `Notify` method signature
    # introspection output.
    #  <arg name="app_name" type="s" direction="in"/>
    # This one is easy. Python string == Qt string == DBus string == profit.
    app_name = "dbus_demo"
    #  <arg name="id" type="u" direction="in"/>
    # I'd have pulled out about a handful of hair to figure this out. There is
    # no concept of `uint` in python. We workaround creating a QVariant and
    # converting that to uint.
    v = QtCore.QVariant(12321)  # random int to identify all notifications
    if v.convert(QtCore.QVariant.UInt):
        id_replace = v
    #  <arg name="icon" type="s" direction="in"/>
    #  <arg name="summary" type="s" direction="in"/>
    #  <arg name="body" type="s" direction="in"/>
    # These are strings again.
    icon = ""
    title = header
    text = msg
    #  <arg name="actions" type="as" direction="in"/>
    #  <arg name="hints" type="a{sv}" direction="in"/>
    # Below two costed a good amount of time again. Requires some trial and
    # error, see lessons in below section
    actions_list = QtDBus.QDBusArgument([], QtCore.QMetaType.QStringList)
    hint = {}
    #  <arg name="timeout" type="i" direction="in"/>
    time = 100   # milliseconds for display timeout

    bus = QtDBus.QDBusConnection.sessionBus()
    if not bus.isConnected():
        print("Not connected to dbus!")
    notify = QtDBus.QDBusInterface(item, path, interface, bus)
    if notify.isValid():
        x = notify.call(QtDBus.QDBus.AutoDetect, "Notify", app_name,
                        id_replace, icon, title, text,
                        actions_list, hint, time)
        if x.errorName():
            print("Failed to send notification!")
            print(x.errorMessage())
    else:
        print("Invalid dbus interface")

A working demo is available in this gist.

Diagnosing DBus issues

If you read through the code above, almost all the annotations are around matching the data types! DBus discards any call for arg mismatch with an error like following (see the x.errorName call in sample above).

Failed to send notification!
Method "Notify" with signature "susssasavi" on interface "org.freedesktop.Notifications" doesn't exist

If you have dbus-monitor running, an error will show up as follows:

method call time=1506258356.612803 sender=:1.54 -> destination=org.freedesktop.DBus serial=9 path=/org/freedesktop/DBus; interface=org.freedesktop.DBus; member=AddMatch
   string "type='signal',sender='org.freedesktop.DBus',interface='org.freedesktop.DBus',member='NameOwnerChanged',arg0='org.freedesktop.Notifications'"
method call time=1506258356.612920 sender=:1.54 -> destination=org.freedesktop.Notifications serial=10 path=/org/freedesktop/Notifications; interface=org.freedesktop.Notifications; member=Notify
   string "dbus_demo"
   uint32 12321
   string ""
   string "hello header"
   string "some message!"
   array [
   ]
   array [
   ]
   int32 100
error time=1506258356.613038 sender=:1.26 -> destination=:1.54 error_name=org.freedesktop.DBus.Error.UnknownMethod reply_serial=10
   string "Method "Notify" with signature "susssasavi" on interface "org.freedesktop.Notifications" doesn't exist
"

Start dissecting the data types one by one: s u s s s as av i. This is what gets sent on the DBus. Let’s find out what’s the expected string:

$ qdbus org.freedesktop.Notifications /org/freedesktop/Notifications org.freedesktop.DBus.Introspectable.Introspect
...
      <method name="Notify">
      <arg name="app_name" type="s" direction="in"/>
      <arg name="id" type="u" direction="in"/>
      <arg name="icon" type="s" direction="in"/>
      <arg name="summary" type="s" direction="in"/>
      <arg name="body" type="s" direction="in"/>
      <arg name="actions" type="as" direction="in"/>
      <arg name="hints" type="a{sv}" direction="in"/>
      <arg name="timeout" type="i" direction="in"/>
      <arg name="return_id" type="u" direction="out"/>
...

And the expected data types are: s u s s s as a{sv} i. So something’s wrong with hints argument. According to dbus overview, a{sv} is an array(dictionary{string variants}). Now try if we can match that with just a {}.

Hope this article saves some time for you. Namaste!