mardi 19 mai 2009

Adventures in udev land

I've mentionned a while ago the work that has been done on libgpod hal callout. It's working nicely, but with HAL being deprecated, I thought now might be a good time at looking at how to do things in the future, and to check if udev already lets us do what we do in the HAL callout. The good news is that it's working now and is pretty straightforward. However, I got stuck on a few details, so I thought it might be useful to others if I documented my findings here.

Currently, libgpod installs a .fdi file with the iPod vendor ID/device ID and a binary name. When HAL detects that a device that matches these 2 IDs is plugged in, it runs the binary. The binary issues a SCSI inquiry command to the iPod to get various information, and set some HAL properties using libhal.

My goal was to do something similar with udev, ie get udev to run a binary when an iPod is inserted and then associate some information with the iPod device in udev database so that other applications can access it.

iPod detection

Running a binary when the iPod is inserted wasn't too hard, it's done with a udev rule file (the format is documented in udev manpage, don't forget to read it if you have to write such a file! ) which goes to /lib/udev/rules.d. My first version was simple enough:

ACTION=="add", SUBSYSTEM=="block", ENV{ID_FS_USAGE}=="filesystem",
ATTRS{idVendor}=="05ac", ATTRS{idProduct}=="1204",
RUN+="/tmp/" is a simple shell script wrapping the actual udev callout. It makes it easier to dump various information to a log file (for example the callout environment which udev uses to pass useful information to the callout). And, lo and behold, after plugging an iPod, my shell script was run!

Adding information to the udev database

The next logical step was to add some values read from the iPod to udev database so that other apps can get this additionnal information. And this is one of the steps that gave me some troubles. I was a bit ashamed of finding a really informative post by Kay Sievers answering my question right after having sent this email...

In short, it's really easy to import new values in the udev database, all you have to do is to output key/value pairs on stdout. This is nice, since udev passes information through environment variables and adds information to its database by reading stdout, this means that your callout doesn't have to depend in anyway on libudev.

I quickly modified my test program to print a few key/value pairs to add to the environment, triggered an unplug/replug of my ipod with udevadm, and watch the 'block' subsystem devices with the devkit binary. But I was really disappointed not to find my values associated with the iPod :(

After double checking everything and fiddling a bit to try to figure out what was wrong, I read again Kay's email, and I saw there was another difference between his code and mine: he is using an IMPORT rule to run his binary while I was still using a RUN rule.

I changed my udev rule to an IMPORT rule and.... it still didn't work :;)After staring at udevadm monitor output, I noticed that when the iPod was plugged in, there was first an "add" udev event for the iPod device shortly followed by a "change" event. Since my rule was only catching the "add" event, I hypothetized maybe my changes to the udev database were first properly added, and then overwritten by the "change" event. So I changed my rule file to catch "change" events in addition to "add" events, and it finally worked!

ACTION=="add|change", SUBSYSTEM=="block", ENV{ID_FS_USAGE}=="filesystem",
ATTRS{idVendor}=="05ac", ATTRS{idProduct}=="1204",

I was very happy with my udev callout, however I shortly realized that when going from a RUN rule to an IMPORT rule, udev no longer passed me the device name (/dev/sd??) in the DEVNAME environment variable. I went to #udev on freenode to check if this was the expected behaviour, and Kay confirmed this is normal because when IMPORT rules are run, the final device doesn't exist yet.

However, the $tempnode variable can be used as an argument to the binary that is being run to give it access to a temporary device node which can be interacted with. And indeed, after adding this argument to my udev rule, I could do everything that I wanted to :)

Final polish

After this successful experiment, all that remained to be done was to make the udev callout as featureful as the HAL callout. This was pretty straightforward, I abstracted the information gathering part from the HAL callout. This generic code then uses some backend-specific code to set the values. The HAL backend does that by using libhal, the udev backend does that by just outputting values to stdout.

David Zeuthen was (as always) really helpful by pointing me at the udev/devicekit equivalent for info.desktop.icon and DKD_PRESENTATION_NAME and DKD_PRESENTATION_ICON_NAME. I also cooked up some variable names in a LIBGPOD namespace to have an udev equivalent to the stuff provided by podsleuth, let me know if it could be useful in your projects, it can be changed to fit your needs :)

End result

And here is the end result after plugging in my iPod:

# udevadm info --query=env --name=sdb2


Et voilà! The code is available from the devicekit branch of my libgpod git repo
and will be committed to libgpod svn soon.

2 commentaires:

Anonyme a dit…

The very last link is kinda broken. Also, would you maybe update this. Also, I was making guesses when commenting on this, because it's written in a strange language :)

Christophe a dit…

Indeed, it was moved to
I think if I edit the blog post to fix the links, it will reappear in the planets it's aggregated on, so I'll keep the new link in the comments :)

I'm not sure what you mean when you say that you had to make guesses because of the language? Was there French showing up in blogger interface to comment? Or is my English awkward in some parts of the blog post?

I think the blog post is still more or less accurate these days, are there some obvious changes to make ?