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.

runot
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 and remote-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.

ssh-session
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.

.bashrc
# If the shell is running over SSH, override the session DBus socket to point
# to the one forwarded over SSH.
if  [ -n $SSH_CONNECTION ]; then
  export DBUS_SESSION_BUS_ADDRESS=/some/well/known/path/dbus.socket
fi

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 and remote-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.

~/.ssh/config

Host remote-machine
  HostName remote-machine.example.com
  ForwardX11 "yes"
  • 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.

~/.local/share/dbus-1/services/org.Notifications.service
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

~/.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 :)