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:
-
Fork a process, for example
notify-send
. I’ve been using this in my dot files formpd
andvolume
operations. Works great, but puts a dependency ofnotify-send
inpomito
. See [here][notify-send-ex] for an example. [notify-send-ex]: https://github.com/codito/configs/blob/master/.config/i3/config#L182 -
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/ -
Use the built-in
QtDBus
bindings inQt
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!