systemd udev Rules to Detect USB Device Plugging (including Bus and Device Number)

Published by Philipp Schuster on

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.


Philipp Schuster

Hi, I'm Philipp and interested in Computer Science. I especially like low level development, making ugly things nice, and de-mystify "low level magic".

0 Comments

Leave a Reply

Your email address will not be published. Required fields are marked *