udev, thinkpad docks, sawfish

The other day someone gave me another dock for my thinkpad, and I eventually decided to use it at home. I've had a dock at the office for a long time, and docking there involved running a complex script configuring the network environment, running a window manager on some display on a desktop machine, and only exiting when the dock was supposed to end; its execution was triggered when the wake-up script noticed a dock was present.

Now, when there are two docks and one is for rather conventional at-home use (essentially, simply configuring a different monitor and network adapter), I decided to do it properly and go through udev. Which turned out to be tricky enough that I'll turn this note to my future self into a blog post.

udev

What turned out to be the most complicated part was figuring out the udev rules. That's because for ages I have been using:

udevadm info -a -p some/sysfs/path

to work out matchable attributes for a device. That's more or less fine if all you're after is rules for attaching devices. For the dock, however, the removal event is important, too. But when the removal event comes in, udev has forgotten essentially all of the attributes that come from info -a, and rules that work with add simply won't fire with remove.

So, here's my new policy: I'll use:

udevadm monitor --environment --udev

(where the udev option restricts events to udev rather than kernel events, which for the deluge of events coming from the dock seemed smart; I may want to revisit that). When you then plug in or out things, you'll directly see what events you can match against. Nice.

Except of course for the deluge of events just mentioned: A dock just has quite a few devices. The event for the device I consider most characteristic, however, makes two add events, and I've not found a good way to tell the two of them apart. Still, this is what I've put into /etc/udev/rules.d/95-docking.rules:

ACTION=="add", SUBSYSTEM=="usb", ENV{ID_VENDOR_ID}=="17ef", \
  ENV{ID_MODEL_ID}=="1010",  ENV{DEVTYPE}=="usb_device", \
  RUN+="/bin/su <your user id> -c '/full-path-to-dock-script start'"

ACTION=="remove",  ENV{SUBSYSTEM}=="usb", ENV{PRODUCT}=="17ef/1010/5040", \
  ENV{DEVTYPE}=="usb_device", \
  RUN+="/bin/su <your user id> -c '/full-path-to-dock-script stop'"

Important (and having forgotten about it again gave me quite a bit of frustration): Rather sensibly, udev has no idea of the shell path and will just fail silently when it cannot execute what's in RUN. Hence you must (essentially) always give full path names in udev RUN actions. In case of doubt, try RUN+="/usr/bin/logger 'rule fires'" in a rule and watch the syslog.

For this kind of thing, by the way, you'll rather certainly want to use su (or go through policykit, but I can't bring mayself to like it). You see, I want the dock script in my home directory and owned by me; having such a thing be executed as root (which udev does) would be a nice backdoor for emergencies, but will generally count as a bad idea.

On the double dock event… well, we're dealing with shell scripts here, so we'll hack around it.

Dock script: sawfish to the rescue

udev only lets you execute short scripts these days and rigorously kills everything spawned from udev rules when it has finished processing the events. I suppose that's a good policy for general system stability and reducing unpleasant surprises. But for little hacks like the one I'm after here, it seems to be a bit of a pain at first.

What it means in practice is that you need something else to execute the actual dock script. In my case, that thing is my window manager, sawfish, and having the window manager do this is rather satisfying, which reinforces my positive feeling towards udev's kill policy (although, truth be told, the actual implemenation is in shell rather than in sawfish's scheme).

To keep everything nicely together, the docking script at its core is a bash case statement, in essence:

!/bin/bash
# bookkeeping: we need to undock if that file is present
UNDOCK_FILE=~/.do-undock

# display for the window manager we talk to
export DISPLAY=:0

case $1 in
  start)
    sawfish-client -c "(system \"urxvt -geometry -0+0 -e $0 on &\")"
    ;;
  stop)
    sawfish-client -c "(system \"urxvt -geometry -0+0 -e $0 off &\")"
    ;;
  on)
    if [[ -f $UNDOCK_FILE &&
      $((`date +"%s"` - `date -r $UNDOCK_FILE +"%s"`)) -lt 20 ]]; then
        # debounce multiple dock requests
       exit 1
    fi
    touch $UNDOCK_FILE

    # let udev do its thing first; we're no longer running from udev
    # when we're here.
    udevadm settle

    # Commands to dock follow here
    ;;
  off)
    if [ -f ~/.do-undock ]; then
      rm ~/.do-undock
      # Commands to undock in here.
    fi
    ;;
  *)
    echo "Usage $0 (start|stop|on|off)"
    ;;
esac

The plan is: Udev calls the script with start and stop, which then arranges for sawfish to call the script from sawfish with the on and off arguments.

There's a bit of bookkeeping with a file in home I keep to see whether we need to undock (in my setup, that's not necessary at work), and which I use to unbounce the duplicate dock request from udev. That part could be improved by using lockfile(1), because the way it is written right now there are race conditions (between the -f, the date, and the touch) – perhaps I'll do it when next I have time budgeted for OS fiddling.

One think I like a lot is the udevadm settle; this basically lets my script rely on a defined state where all the devices it may want to talk to are guaranteed to be initialised as far as udev goes. This is so much nicer than that sleep 3 you can see in too many places.

What I do when docking

Why go into all this trouble and not let whatever automagic is active pick up the screen and the new network interface and be done with it? Well, partly because I don't run most of the software that does that magic. But of course having a shell script lets me say what I actually want:

  • disable sleep on lid closing (that's special to my own ACPI hacks from the depths of time)
  • configure the the external screen as primary (that's something like xrandr --output DP2-1 --off ; xrandr --fb 2048x1152 --output DP2-1 --auto for me; don't ask why I first need to switch off the display, but without it the --auto will get confused).
  • switch to an empty (“dock-only” if you will) page on the desktop (that's wmctrl -o 4096,1152 for me).
  • sure enough, switch on some desktop glitz that I'm too stingy for when off the grid.

One thing I'm currently doing in the dock script that I shouldn't be doing there: I'm now using a wacom bamboo pad I've inherited as a mouse replacement at home and was suprised that no middle mouse button (Button2) was configured automatically on it. Perhaps some search engine will pick this up and save a poor soul looking for a quick solution reading man pages and xsetwacom output:

xsetwacom set "Wacom BambooPT 2FG 4x5 Pad pad" Button 8 2
xsetwacom set "Wacom BambooPT 2FG 4x5 Pad pad" Button 9 2

(this makes both buttons in the middle middle mouse buttons; I'll see if I like that on the long run). Of course, in the end, these lines belong into a udev rule for the wacom tablet rather than a dock script. See above on these.

Kategorie: edv