systemd udev Rules to Detect USB Device Plugging (including Bus and Device Number)
The other day, I was working on some udev rules to invoke a script when a USB device is added or removed from my machine. I needed the USB bus and USB port number to further process that event. I decided to use udev for that. However, getting the relevant attributes was not as intuitive as I expected, and existing documentation wasn’t that good. Here’s what I did and how I solved it.
About
udev is a component of systemd, enabling device management in user-space on Linux-based systems. Using udev, one can create user-space handlers for certain events, such as invoking a script if a USB device is plugged or unplugged.
My System
I’m using a NixOS 23.11-based system with systemd / udev at 254.6
and Linux 6.8.1
. However, on other Linux distributions using systemd / udev, the process works similar except where you define the rules.
The Goal
Every time a USB device is plugged or unplugged into my machine, I want to get a notification and the associated USB bus and device number to process that event.
The Caveat
When using an ACTION=="add"
rule, one can query attributes using $attr{%ATTR_NAME%}
inside the rule string. This causes udev to query sysfs from the kernel with corresponding device attributes. However, $attr{%ATTR_NAME%}
on ACTION=="remove"
events results in an empty response, as the sysfs node of the USB device is already gone at that point.
The Solution
However, we just need to know that udev received the relevant information at that point from the kernel and passes the attributes of the device as environment variable to the script. The udev rules can be written like this:
SUBSYSTEM=="usb", ACTION=="add", ENV{DEVTYPE}=="usb_device", RUN+="/tmp/onUsbHotplugScript.sh add $attr{busnum} $attr{devnum}" SUBSYSTEM=="usb", ACTION=="remove", ENV{DEVTYPE}=="usb_device", RUN+="/tmp/onUsbHotplugScript.sh remove"
and onUsbHotplugScript.sh
might look like this:
#!/usr/bin/env sh set -eou pipefail CMD=$1 # 1st argument: add" or "remove" BUSNUM="${2:-$BUSNUM}" # use 2nd argument or ENV variable PORTNUM="${3:-$DEVNUM}" # use 3rd argument or ENV variable function print_date() { date +%Y-%m-%d_%H%M%S } echo "$(print_date) USB change detected: $CMD bus=$BUSNUM port=$PORTNUM" >> /tmp/on-usb-hotplug.txt
On NixOS, your configration might look like this:
services.udev.extraRules = let onUsbHotplugScript = pkgs.writeShellScript "on-usb-hotplug" (builtins.readFile ./on-usb-hotplug.sh); in '' SUBSYSTEM=="usb", ACTION=="add", ENV{DEVTYPE}=="usb_device", RUN+="${onUsbHotplugScript} add $attr{busnum} $attr{devnum}" SUBSYSTEM=="usb", ACTION=="remove", ENV{DEVTYPE}=="usb_device", RUN+="${onUsbHotplugScript} remove" '';
Trivia
The attribute names correspond to the attributes as you can find them in the output of $ sudo udevadm monitor --kernel --property
. You probably also can find them somewhere in sysfs
.
0 Comments