# Harnessing DBus ## Introduction I will discuss here how to run some script when specific DBus events happen. The subject has already been covered by various tutorials, see for instance: [how-do-i-run-a-script-on-a-dbus-signal](http://ubuntu.aspcode.net/view/635400140124705175242338/how-do-i-run-a-script-on-a-dbus-signal) or [using-dbus-to-take-action-on-a-usb-storage-device](http://www.danplanet.com/blog/2007/11/11/using-dbus-to-take-action-on-a-usb-storage-device/) but most of them are of the recipe type or need programming skills. I will explore a bit more deeply the subject here, using nothing more than shell scripts. I hope you will learn in the process useful stuff about how a modern Linux system works under the hood. Even if projects like [kdbus](http://www.linuxuser.co.uk/features/kernel-3-17-and-kdbus-the-kernel-column) take ground, the method described below will stay valid with only minor adjustements needed. More specifically, I will try to **trigger a script as soon as you switch [virtual consoles](https://en.wikipedia.org/wiki/Linux_Virtual_Consoles)**. I become interested in that subject because, due to some faulty video hardware or driver or what else, the [gamma](http://www.cambridgeincolour.com/tutorials/gamma-correction.htm) tuning of my latest screen becomes always too high when switching virtual consoles. The problem is not really visible in text mode, but it's obvious when going from text mode to graphic mode, or switching between two X sessions. No screen or video driver settings help. Manually issuing [xgamma](http://www.x.org/releases/X11R7.7/doc/man/man1/xgamma.1.xhtml) like that: xgamma -gamma 0.5 will solve the issue, but a way to automate that would be welcome. You probably don't have that specific problem, so in the script I will describe below you should perform some other useful action you may see fit: popup some message, play a tune... As a prerequisite, you are supposed to be a bit familiar with DBus concepts. If not, look first here: and the tutorial links there. If after reading this, you still have only an incomplete picture of DBus, don't worry: fuzzy specifications and poor documentation can be observed in most system tools in Linux today, and courtesy of systemd, that trend is not showing any sign of going away. I will use these DBus tools: dbus-monitor and dbus-send. You quite certainly have them and their (terse) man pages; if not, they can be found for instance in Debian package dbus. ## Details of the problem I have hinted above that DBus will be used in the solution to our problem. But a DBus daemon will run only if you have some advanced X environnement, which excludes pure startx/xinit environnement from the last millenium. So if you have some modern destkop environnement (be it a bare one like XFCE or a bloated one like Gnome) you can use the method below. In addition, a DBus session alone with no services will not allow us to catch interesting events (a.k.a. DBus signals send by processes or calls made by processes), and here we will need the DBus aware application [ConsoleKit or systemd-logind](http://www.freedesktop.org/wiki/Software/ConsoleKit/) running. No problem, it runs on every modern destkop environnement. It should be noted that switching virtual consoles happens in 4 cases: 1. At boot time when you are starting your X server: this triggers a switch to the virtual console (often /dev/tty7) where the X server runs; and then the gamma problem happens 2. When switching to a text console; going back to the X server screen will trigger the gamma problem 3. When switching between several X sessions (if the first X session starts in /dev/tty7 as said above, the second one will use the virtual console /dev/tty8 etc...); the new visible X session will exhibit the gamma problem 4. When resuming from suspended state; not sure if all video hardware goes first to text mode when resuming, but my hardware does, and the gamma problem becomes visible when the switch to graphical mode is triggered Some remarks about the 4 cases above: * Case 1: for the boot time case, putting in your $HOME/.xsessionrc that line: xgamma -gamma 0.5 is enough. But won't work for cases 2, 3 and 4. In a multiuser environment, it doesn't scale well. * Case 2 and 3: you can switch to a given virtual console using the command [chvt](http://nixdoc.net/man-pages/Linux/chvt.1.html), or with some [keyboard combination](http://www.tldp.org/HOWTO/Keyboard-and-Console-HOWTO-7.html). * Case 4: the suspend/hibernate system offers some hook points where commands can be run when resuming. But you must be sure when running that command that you are indeed in graphical mode: if not, the xgamma command won't work. But there is more. In a multiuser environment, you have to find out the correct X display (the one active and visible when suspended), and feed it to xgamma as an argument. So the correct command to run in my case is actually: XAUTHORITY=$USERHOME/.Xauthority xgamma -gamma 0.5 -display $DISPLAY where you cannot assume that DISPLAY has value :0. Here USERHOME is the home directory of the user logged in the X session of the active display, and XAUTHORITY will allow root (which is the user running the resume script) to access the credentials to connect to that display. And no, issuing xgamma on a non-visible and non-active display will not restore gamma for the active display. Actually, there is no easy way to find out the active display and it's corresponding session user while staying inside traditional X and the suspend/hibernate system. A workaround would be to loop over all active X displays; this is feasable (they can be found in /tmp/.X11-unix). You will also need to loop over all possible $HOME/.Xauthority, which is easy because you are root. At that point I hope you are convinced that that solution is clunky. I will present something more general below. ## Looking for interesting DBus events when switching virtual consoles Let's run dbus-monitor on the system DBus daemon (you need to do that as root in a terminal), and let's observe what happens when we switch to a different virtual console and back. The output looks somethink like (# being the root shell prompt): # dbus-monitor --system signal sender=org.freedesktop.DBus -> dest=:1.67 serial=2 path=/org/freedesktop/DBus; interface=org.freedesktop.DBus; member=NameAcquired string ":1.67" Now switch to a text console with the key combo Ctrl+Alt+F1, and back to your session (supposed running at /dev/tty7) with the combo Alt+F7. Something happened, we now have on the terminal: # dbus-monitor --system signal sender=org.freedesktop.DBus -> dest=:1.67 serial=2 path=/org/freedesktop/DBus; interface=org.freedesktop.DBus; member=NameAcquired string ":1.67" signal sender=:1.7 -> dest=(null destination) serial=418 path=/org/freedesktop/ConsoleKit/Session4; interface=org.freedesktop.ConsoleKit.Session; member=ActiveChanged boolean false signal sender=:1.7 -> dest=(null destination) serial=419 path=/org/freedesktop/ConsoleKit/Seat1; interface=org.freedesktop.ConsoleKit.Seat; member=ActiveSessionChanged string "" signal sender=:1.6 -> dest=(null destination) serial=195 path=/org/freedesktop/PolicyKit1/Authority; interface=org.freedesktop.PolicyKit1.Authority; member=Changed signal sender=:1.6 -> dest=(null destination) serial=196 path=/org/freedesktop/PolicyKit1/Authority; interface=org.freedesktop.PolicyKit1.Authority; member=Changed signal sender=:1.7 -> dest=(null destination) serial=430 path=/org/freedesktop/ConsoleKit/Session4; interface=org.freedesktop.ConsoleKit.Session; member=ActiveChanged boolean true signal sender=:1.7 -> dest=(null destination) serial=431 path=/org/freedesktop/ConsoleKit/Seat1; interface=org.freedesktop.ConsoleKit.Seat; member=ActiveSessionChanged string "/org/freedesktop/ConsoleKit/Session4" signal sender=:1.6 -> dest=(null destination) serial=203 path=/org/freedesktop/PolicyKit1/Authority; interface=org.freedesktop.PolicyKit1.Authority; member=Changed signal sender=:1.6 -> dest=(null destination) serial=204 path=/org/freedesktop/PolicyKit1/Authority; interface=org.freedesktop.PolicyKit1.Authority; member=Changed *Disclaimer: I will handle here the case where you use ConsoleKit. If you use a very recent desktop, you may be using the systemd equivalent of ConsoleKit called systemd-logind, where the messages below look a bit different, with interfaces like: org.freedesktop.login1 and paths like: /org/freedesktop/login1/session. I have so far only virtual machines running with systemd, so unable to explore console switching on them; I will update that page as soon as possible to cover the systemd case.* To not be flooded with messages, we can print only messages related to the org.freedesktop.ConsoleKit.Session interface by adding a watch expression argument to dbus-monitor. Redoing the previous experiment, we get now something much more short: # dbus-monitor --system interface=org.freedesktop.ConsoleKit.Session signal sender=org.freedesktop.DBus -> dest=:1.68 serial=2 path=/org/freedesktop/DBus; interface=org.freedesktop.DBus; member=NameAcquired string ":1.68" signal sender=:1.7 -> dest=(null destination) serial=450 path=/org/freedesktop/ConsoleKit/Session4; interface=org.freedesktop.ConsoleKit.Session; member=ActiveChanged boolean false signal sender=:1.7 -> dest=(null destination) serial=462 path=/org/freedesktop/ConsoleKit/Session4; interface=org.freedesktop.ConsoleKit.Session; member=ActiveChanged boolean true There are three 2 lines messages above. The first one is noise due to dbus-monitore registering on the DBus daemon. The second one signals to the world that your current session (called Session4) is no more active (boolean false) because no more visible as you switched to /dev/tty1. And the third one says it's visible and active again (boolean true). You will be able to verify that the same kind of message is triggered when switching to another X session runningfor instance on /dev/tty8, then going back: # dbus-monitor --system interface=org.freedesktop.ConsoleKit.Session signal sender=org.freedesktop.DBus -> dest=:1.82 serial=2 path=/org/freedesktop/DBus; interface=org.freedesktop.DBus; member=NameAcquired string ":1.82" signal sender=:1.7 -> dest=(null destination) serial=850 path=/org/freedesktop/ConsoleKit/Session4; interface=org.freedesktop.ConsoleKit.Session; member=ActiveChanged boolean false signal sender=:1.7 -> dest=(null destination) serial=851 path=/org/freedesktop/ConsoleKit/Session6; interface=org.freedesktop.ConsoleKit.Session; member=ActiveChanged boolean true signal sender=:1.7 -> dest=(null destination) serial=873 path=/org/freedesktop/ConsoleKit/Session6; interface=org.freedesktop.ConsoleKit.Session; member=ActiveChanged boolean false signal sender=:1.7 -> dest=(null destination) serial=874 path=/org/freedesktop/ConsoleKit/Session4; interface=org.freedesktop.ConsoleKit.Session; member=ActiveChanged boolean true Here the second X session is called Session6. You should now understand that the third and fourth messages happen when you hit Ctrl+Alt+F8: Session4 signals it becomes inactive because invisible, then Session6 becomes visible and active. And the last two 2 lines messages happen when you hit Ctrl+Alt+F7, always ending with Session4 signaling it's visible and active again. So our solution would be to run our xgamma command as soon as that last DBus signal is caught. ## Looking for interesting DBus events when resuming, and reacting on these events But wait, what about suspend/resume? Lets run dbus-monitor again and see what happens when you suspend, the resume: # dbus-monitor --system interface=org.freedesktop.ConsoleKit.Session signal sender=org.freedesktop.DBus -> dest=:1.83 serial=2 path=/org/freedesktop/DBus; interface=org.freedesktop.DBus; member=NameAcquired string ":1.83" signal sender=:1.7 -> dest=(null destination) serial=948 path=/org/freedesktop/ConsoleKit/Session4; interface=org.freedesktop.ConsoleKit.Session; member=IdleHintChanged boolean true signal sender=:1.7 -> dest=(null destination) serial=962 path=/org/freedesktop/ConsoleKit/Session4; interface=org.freedesktop.ConsoleKit.Session; member=IdleHintChanged boolean false That's even simpler, the second signal says we are going idle a.k.a. suspend state (boolean true) and then going live again (not idle, so boolean false). So we have two signals to monitor: ActiveChanged for direct console switching (and boot) and IdleHintChanged for console switching triggered by resume. To be completely sure to work only on those signals, we will use two specific watch expressions as arguments to dbus-monitor; several such watch arguments are or-ed, that is not obvious from dbus-monitor man page. Our final dbus-monitor becomes then: dbus-monitor --system "interface='org.freedesktop.ConsoleKit.Session',member='ActiveChanged'" "interface='org.freedesktop.ConsoleKit.Session',member='IdleHintChanged'" Following that [tutorial method](http://ubuntu.aspcode.net/view/635400140124705175242338/how-do-i-run-a-script-on-a-dbus-signal), plus some bashism like =~ and \[[ ]], we end-up with the following script, which will also use logger to trace what happens in syslog: #!/bin/bash interface=org.freedesktop.ConsoleKit.Session member=ActiveChanged resume=IdleHintChanged # beware the pipe | at end of next line dbus-monitor --system "interface='$interface',member='$member'" "interface='$interface',member='$resume'" | while read -r line; do # we loop over all messages; get first line of message in variable 'line' # reading second line of message in variables 'type' and 'value' read type value # 'ret' is 0 if 'line' contains string $member \[[ "$line" =~ "$member" ]]; ret=$? # test ActiveChanged to be true if [ "$ret" = "0" -a "$type" = "boolean" -a "$value" = "true" ]; then echo Boot or virtual console switching case: $line |& logger xgamma -gamma 0.5 |& logger fi # 'ret' is 0 if 'line' contains string $resume \[[ "$line" =~ "$resume" ]]; ret=$? # test IdleHintChanged to be false if [ "$ret" = "0" -a "$type" = "boolean" -a "$value" = "false" ]; then echo Resume case: $line |& logger xgamma -gamma 0.5 |& logger fi done If you start this script in background as root while in your X session, things will work fine. But not in X sessions owned by other users: because, as said above, you will be running it with the DISPLAY and XAUTHORITY of your session. But DBus can help you find out these values as we will see now. ## Obtaining information through DBus Let's interrogate the ConsoleKit session object for our current X session, which is in the example above has path /org/freedesktop/ConsoleKit/Session4. We will use for that dbus-send, and the requests and answers will look like this: # dbus-send --system --print-reply --type=method_call --dest=org.freedesktop.ConsoleKit "/org/freedesktop/ConsoleKit/Session4" org.freedesktop.ConsoleKit.Session.GetUser method return sender=:1.7 -> dest=:1.108 reply_serial=2 uint32 1000 # dbus-send --system --print-reply --type=method_call --dest=org.freedesktop.ConsoleKit "/org/freedesktop/ConsoleKit/Session4" org.freedesktop.ConsoleKit.Session.GetX11Display method return sender=:1.7 -> dest=:1.109 reply_serial=2 string ":0" It should be obvious that the first dbus-send command obtains the UID of the X session owner (1000), and the second one get's the X display, here ":0". So our final version of the script becomes: #!/bin/bash interface=org.freedesktop.ConsoleKit.Session member=ActiveChanged resume=IdleHintChanged run() { DISPLAY=`dbus-send --system --print-reply --type=method_call --dest=org.freedesktop.ConsoleKit $object_path ${interface}.GetX11Display | grep string | cut -d ' ' -f 5 | cut -d '"' -f 2` XUID=`dbus-send --system --print-reply --type=method_call --dest=org.freedesktop.ConsoleKit $object_path ${interface}.GetUser | grep uint32 | cut -d ' ' -f 5` if [ "$DISPLAY" = "" -o "$XUID" = "" ]; then echo DISPLAY=$DISPLAY and user-uid=$XUID not both non empty, impossible to run xgamma |& logger -t logger else XAUTHORITY=`grep :x:${XUID}: /etc/passwd | cut -d ':' -f 6`/.Xauthority # we don't run any X app if XAUTHORITY doesn't exist or is empty if [ -f $XAUTHORITY -a "`wc -c $XAUTHORITY | cut -d ' ' -f 1`" != "0" ]; then echo Running "XAUTHORITY=$XAUTHORITY xgamma -gamma 0.5 -display $DISPLAY" |& logger -t logger XAUTHORITY=$XAUTHORITY xgamma -gamma 0.5 -display $DISPLAY |& logger -t logger # in case your gamma stays ok, you can still test this script by uncommenting the next line #XAUTHORITY=$XAUTHORITY xeyes -display $DISPLAY & else echo $XAUTHORITY file non-existent or empty, impossible to run xgamma |& logger -t logger fi fi } # beware the pipe | at end of next line dbus-monitor --system "interface='$interface',member='$member'" "interface='$interface',member='$resume'" | while read -r line; do # we loop over all messages; get first line of message in variable 'line' # reading second line of message in variables 'type' and 'value' read type value # 'ret' is 0 if 'line' contains string $member \[[ "$line" =~ "$member" ]]; ret=$? # test ActiveChanged to be true if [ "$ret" = "0" -a "$type" = "boolean" -a "$value" = "true" ]; then object_path=`echo "$line" | cut -d ' ' -f 7 | cut -d '=' -f 2 | cut -d ';' -f 1` echo Boot or virtual console switching case for session $object_path |& logger -t logger run fi # 'ret' is 0 if 'line' contains string $resume \[[ "$line" =~ "$resume" ]]; ret=$? # test IdleHintChanged to be false if [ "$ret" = "0" -a "$type" = "boolean" -a "$value" = "false" ]; then object_path=`echo "$line" | cut -d ' ' -f 7 | cut -d '=' -f 2 | cut -d ';' -f 1` echo Resume case for session $object_path |& logger -t logger run fi done We finally have to start the script at boot time, which has to be done after the system DBus daemon is started, which in current stable Debian is done through /etc/init.d/dbus. I suggest to simply drop the path of the above script there, backgrounded with &. If you do that, reboot and look at /var/log/syslog, you will notice that the first call to xgamma (and xeyes) happens under the desktop manager user (this is for sure the case for lightdm which I use). Then, when you have logged in, xgamma (and xeyes) won't run because XAUTHORITY is empty. I suspect this is due to a lack of synchronicity between setting the DISPLAY in PolicyKit and setting it fully (with valid XAUTHORITY) in X. The $HOME/.xsessionrc trick discussed at the beginning above will save that case for you. After this first logon, things work as expected on resume and console switch. Notice that the screeensaver authorization screen never shows xeyes, if you have uncommented that line in the script. ## Conclusion We have gone a long way to finally devise a general solution to our problem of running a script when changing virtual consoles. It is fundamentally based on ConsoleKit or systemd-logind tracking these virtual consoles changes, and announcing them on the DBus. An improvement to the script would be to write it using a language with a DBus binding, like Python. ## Other interesting links: An in-depth view of the virtual consoles: A useful exploratory DBus tool is [d-feet](http://live.gnome.org/DFeet/). A d-feet Debian package exists. Programmers may be interested in [gdbus](http://dev.man-online.org/man1/gdbus/) available in Debian package libglib2.0-dev. [userland-dbus-event-manager](https://github.com/airtonix/userland-dbus-event-manager) is a general tool to react on DBus events.