Graphical notifications for long-running tasks
In my dayjob I often have to perform long-running tasks that do not require constant attention (e.g. compiling a compiler) on Linux systems. When this happens, it is unavoidable to context switch to other tasks even if experts advice against it. Turns out that compilation scrolls are not always very interesting.
I would like to be able to resume working on the original task as soon as possible. So the idea is to receive a notification when the task ends.
Local notifications
If the time-consuming task is being run locally and we are using a graphical
environment we can use the tool notify-send
to send ourselves a notification
when the command ends. We can combine this in a convenient script like the one
below.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
#!/usr/bin/env bash
$*
result="$?"
if [ "$result" != "0" ];
then
icon="dialog-warning"
else
icon="dialog-information"
fi
notify-send "--icon=$icon" "$*"
exit "$result"
We execute the command and the we use notify-send
with the executed command
an appropriate icon based on the execution result.
$ runot very slow thing
< "very slow thing" runs >
< a notification appears >
How does this work?
Without entering into too much detail, notify-send
connects to D-Bus and sends
a notification, as specified in the Desktop Notifications
Specification.
A daemon configured by your desktop environment is waiting for the
notifications. Upon receiving one it graphically displays the notification.
Remote notifications
D-Bus is really cool technology that allows different applications to interoperate and is specially useful in a desktop environment. That said, the typical use case of D-Bus is typically scoped by user sessions on the same computer and, while not impossible, the message bus is not meant to span over several computers.
This means that if rather than working locally, we work over SSH on a
remote-machine
we will not be able to send notifications to our
local-machine
desktop straightforwardly. There are two options here that we
can use. Neither is perfect but will allow us to deliver notifications to our
desktop computer from a remote system.
- Forward the UNIX socket
- Use a remote notification daemon
Forward the UNIX socket
D-Bus clients know where to find the message bus by reading the environment
variable DBUS_SESSION_BUS_ADDRESS
. In most systems nowadays it looks like this
$ echo $DBUS_SESSION_BUS_ADDRESS
unix:path=/run/user/9999/bus
This syntax means the D-Bus server, initiated by some other application upon login, can be found at the specified path. In this case the specified path is a UNIX socket, so in principle only accessible to processes in the current machine.
We can forward a UNIX socket using ssh
, like we usually do with TCP ports.
(local-machine) $ ssh -R /some/well/known/path/dbus.socket:${DBUS_SESSION_BUS_ADDRESS/unix:path=/} user@remote-machine
(remote-machine) $ export DBUS_SESSION_BUS_ADDRESS=/some/well/known/path/dbus.socket
(remote-machine) $ notify-send "Hello world"
< notification appears in the local machine as if sent locally >
You can use any path for /some/well/known/path/dbus.socket
, including a
subdirectory of your home directory.
Pros
- The notification is reported as if it had been sent by a local process, so it integrates very well with the environment.
From a usability point of view this is the strongest point of this approach.
Cons
- This only works if
local-machine
andremote-machine
share the same UID and GID. This can be easy to achieve in corporate environments where all systems use a unified login system based on LDAP or Active Directory.
For security reasons, the default configuration of D-Bus only allows processes
of the same user to access the bus. The protocol checks that the uid
and
gid
of the process connecting to the bus match the uid
and gid
of the
process that started the D-Bus daemon. This avoids other local processes, not
belonging to our user, to connect to our D-Bus daemon.
This may be an importation limitations in many systems (e.g. my laptop at work is not integrated in the LDAP of other systems or, for security reasons, we have different credentials in development vs production systems).
- You need to remove the UNIX socket on the remote machine every time you start a session, but
not in subsequent
ssh
connections.
This can be mitigated by using a distinguished script to connect
to the remote machine as a way to initiate the “session”. You would run
this only for the first connection, the other ones would just use a regular
ssh
command.
1
2
3
4
5
#!/usr/bin/env bash
remote="$1"
ssh "$remote" "rm -f /some/well/known/path/dbus.socket"
exec ssh -R "/some/well/known/path/dbus.socket:${DBUS_SESSION_BUS_ADDRESS/unix:path=/}" "$remote"
(local-machine) $ ssh-session user@remote-machine
This script is a bit simplistic and assumes you can remotely execute commands
without having to enter a password (e.g. because you are using a SSH key). I
have not tried it, but perhaps using
ProxyCommand
this initial script
can be made more convenient without requiring entering the password twice.
Alternatively, if we can configure the SSH server on remote-machine
, we can
add the option StreamLocalBindUnlink yes
to /etc/ssh/sshd_config
. This will
remove (unlink) the /some/well/known/path/dbus.socket
upon exiting so we
don’t have to remove it beforehand.
Note that once you close the ssh connection that forwarded the UNIX socket,
notifications will stop working. So you probably want to close that one the
last in case you’re working with several ssh session to remote-machine
at the
same time.
- You need to set the
DBUS_SESSION_BUS_ADDRESS
environment variable first.
This can be addressed as described in this post by
Nikhil. We can add
the following to our .bashrc
file.
Use a remote notification daemon
This approach is a bit more involved but basically relies on forwarding X11,
running a notification daemon on remote-machine
that we will activate using
D-Bus itself. The notification daemon will then display the notifications using
X11 which will be displayed on our local-machine
as any other X11 forward
application does.
Note: this approach assumes the user is not running a graphical session on
remote-machine
. There are chances that this procedure may confuse the
graphical environment when sending notifications.
Pros
- Does not need uid/gid synchronisation between
local-machine
andremote-machine
.
This was the main limitation with the earlier approach.
Cons
- Needs X11 forwarding which may not always be available
We need to pass -X
when connecting to remote-machine
.
(local-machine) $ ssh -X remote-machine
Alternatively we can add a configuration entry to the ~/.ssh/config
of
local-machine
.
- Relies on systemd and D-Bus
These two components are present in most distributions these days, so they can be assumed.
We also assume that a D-Bus session is running when we connect to
remote-machine
(i.e. on remote-machine
, the environment variable
DBUS_SESSION_BUS_ADDRESS
points to some UNIX socket of remote-machine
).
Again, most distributions these days provide this functionality out of the box.
Setting this up is out of scope of this post.
- The result is less integrated as we use a notification daemon different to the
one in the graphical environment of
local-machine
.
There is a number of different notification daemons, some of which can be
configured to suit ones taste. In this example we will use
notification-daemon
which is a reference implementation of the notification protocol and seems to work
fine for our needs. The Arch wiki has a a list of notification
daemons.
Recall that the notification daemon runs on remote-machine
.
Activation via D-Bus
This means that every time we invoke notify-send
, if no notification daemon
is running, one will be started for us. If one is running already, that one
will be used by notify-send
.
There are two files that we need to create on remote-machine
to set up
D-Bus activation.
First ~/.local/share/dbus-1/services/org.Notifications.service
to tell D-Bus
what is the associated systemd unit and daemon.
1
2
3
4
[D-BUS Service]
Name=org.freedesktop.Notifications
Exec=/usr/lib/notification-daemon/notification-daemon
SystemdService=my-notification-daemon.service
Change the path of Exec
to the proper location of the notification-daemon
executable: the one shown corresponds to Ubuntu/Debian systems.
Now we need to create a systemd-unit in ~/.config/systemd/user/my-notification-daemon.service
1
2
3
4
5
6
7
[Unit]
Description=My notification daemon
[Service]
Type=dbus
BusName=org.freedesktop.Notifications
ExecStart=/usr/lib/notification-daemon/notification-daemon
The path of ExecStart
must be the same as Exec
above.
With all this, notify-send
run on remote-machine
will automatically
initiate the notification-daemon
if none is running.
However, this will not work yet because the notification-daemon
is a X11 application and needs some
environment information to proceed. We can do that by running the following command.
(remote-machine) $ dbus-update-activation-environment \
--systemd DBUS_SESSION_BUS_ADDRESS DISPLAY XAUTHORITY
This command above can be added to the .bashrc
of remote-machine
so it runs
automatically every time we connect. This must run before we activate the notification-daemon
for the first time, otherwise the activation will fail.
With all this in place, it should now be possible to send a test notification.
(remote-machine) $ notify-send "Hello world"
We should see how a new popup appears to the top right of our screen (possibly with an additional icon to our notification area).
This approach is a bit more involved so you may have to troubleshoot a bit. The following command will show us the dbus activations.
(remote-machine) $ journalctl --user --follow -g notif
In my experience the most common error is forgetting to run
dbus-update-activation-environment
, so notification-daemon
fails to
start and exits immediately.
Hope this useful :)