Creating Udev Rules
Table of Contents
Scenario: Ignore a permanently connected backup drive ∆
Also see this related article.
I have a WD USB drive - it's some cheapo variant, never get one of these! It's in an enclosure but can't be taken out and connected directly, like they used to, and suffers a lack of configurability as hdparm shows. But it can be sent to sleep (hdparm -Y /dev/path_to_drive
), which is what my system does after every boot because this drive exists solely for automated weekly backups.
Nevertheless, udisks2
sees it and wants to deal with, so it has to wake it up, which takes ~30s, during which time whatever action I was doing is blocked (e.g. opening a filemanager).
So how can I tell udisks2 to ignore this drive completely? After some research I found that udev can give such hints to udisks2 (and also to systemd).
The archwiki has some information regarding this and this article provide important extra hints, as well as having a look at the rules in /usr/lib/udev/rules.d/
.
First I had a good look at the output of udevadm info /dev/path_to_drive
.
I decided it's probably best to a) not match the kernel assigned drive letters because they can change, and b) stick to just one unique ENV value from above command's output, as shown in the latter article.
In the end I wrote /etc/udev/rules.d/99-local.rules
(99 means it's almost the last to get executed) thusly:
c#ENV{ID_SERIAL}=="WD_XXXXXXX_1234567890", OPTIONS+="nowatch" ENV{ID_SERIAL}=="WD_XXXXXXX_1234567890", ENV{SYSTEMD_READY}="0" ENV{ID_SERIAL}=="WD_XXXXXXX_1234567890", ENV{UDISKS_IGNORE}="1" #ENV{ID_FS_UUID}=="nnnnnnnn-nnnn-nnnn-nnnn-nnnnnnnnnnnn", OPTIONS+="nowatch" ENV{ID_FS_UUID}=="nnnnnnnn-nnnn-nnnn-nnnn-nnnnnnnnnnnn", ENV{SYSTEMD_READY}="0" ENV{ID_FS_UUID}=="nnnnnnnn-nnnn-nnnn-nnnn-nnnnnnnnnnnn", ENV{UDISKS_IGNORE}="1"
The first, commented option is not recommended - it will make it almost impossible to get the drive back, even re-plugging it does not help. I had to edit the above file to add the comments, then restart udev with udevadm control -R
. Physically re-plugging the drive is not necessary though.
Anyhow, the first block addresses the drive itself.
The second block addresses the only partition on that drive.
SYSTEMD_READY=0
is something I found in /usr/lib/udev/rules.d/99-systemd.rules
, and the comments suggest that it makes systemd ignore it.
The last assignment is the correct one for udisks2, telling it to ignore the device identified by the match(es) preceding it.
It took a lot of trial and error (udevadm info
and udisksctl dump
are your friends) but now it seems to work as desired.
Unfortunately this was not sufficient after all and I added even more radical actions.
Scenario: Run a script when a smartphone is connected ∆
This scenario leans strongly on this older article. If you feel some information is missing here, you can probably find it there.
Important note: Most people will be using systemd now, so systemctl
should be used instead of init/service etc., most notably the command
/etc/init.d/udev restart
should be replaced with
systemctl restart udev
.
Gathering Information ∆
We can do this with udevadm info ...
, but we have to tell it which device we're interested in, by giving it a path.
But we know nothing about the device yet, so how do we find that path?
Maybe like this:
dmesg -w
Now connect the phone:
[ 2443.530093] usb 1-3: new high-speed USB device number 8 using xhci_hcd
OK, so currently it's usb 1-3
. But still no path. man udevadm
says something about /sys
, so let's try this:
$ find /sys -name '*1-3*'
/sys/devices/pci0000:00/0000:00:14.0/usb1/1-3
/sys/devices/pci0000:00/0000:00:14.0/usb1/1-3/1-3:1.0
/sys/devices/pci0000:00/0000:00:14.0/usb1/1-3/1-3:1.1
/sys/bus/usb/devices/1-3:1.0
/sys/bus/usb/devices/1-3
/sys/bus/usb/devices/1-3:1.1
/sys/bus/usb/drivers/usbfs/1-3:1.1
/sys/bus/usb/drivers/usb/1-3
Bingo! It looks like we can use any of these paths with udevadm info
, however:
the phone contains two devices (1-3:1.0
and 1-3:1.1
) with the same vendor and product ID, and the script could be called twice,
so we need to specify another value to trigger the script only once. Also some consequent actions might not work until the device is fully registered & initialised, so
let's assume that the device registered last is the one we want.
We investigate further by using udevadm info
on both devices (1-3:1.0
and 1-3:1.1
). One variable in the list reads USEC_INITIALIZED
. Sounds good, and 1-3:1.1
has a larger number than 1-3:1.0
, which proves that it is indeed the last device to get registered.
But we cannot use this value, it is likely to be different each time. Compare the outputs of udevadm info -a
(this time with the -a
switch), until we find something that will make this device unique.
There's various choices that are more-or-less device-specific, but to me the difference that made most sense was ATTR{interface}=="MTP"
(only present in the latter device), so I wrote the udev rule like this:
The Udev Rule ∆
cACTION=="add", SUBSYSTEM=="usb", \ ATTRS{idVendor}=="XXXX", ATTRS{idProduct}=="XXXX", \ ATTR{interface}=="MTP", \ RUN+="/path/to/script/phoneconnect"
saved as /etc/udev/rules.d/51-my-phone-add.rules
.
I had to do some testing, some variables did not work even though they seemed valid. It's also necessary to run udevadm control -R
after changing rules.
First Test: A Notification ∆
Now you can call this from inside the phoneconnect script with e.g.
shnotify "Phone connected:" "Device name & model"
This is only a proof-of-concept - the next step would be to make the phoneconnect
script execute things, like copy files to/from the phone etc.
A little improvement for Sending Notifications to the Current User from a Script Executed by Root ∆
To understand what I mean, see the original article.
Instead of calling a wrapper script as a different user, create a function inside your bash script:
shfunction notify { sudo -i -u username DBUS_SESSION_BUS_ADDRESS="unix:path=/run/user/1000/bus" notify-send "$1" "$2" }
The user is hardwired here; this is just a hack that will work on most systems.
Please note running sudo with the -i option!
Please note that, according to the article two more environment variables are needed, but DBUS_SESSION_BUS_ADDRESS was sufficient for me. YMMV.