January 10, 2005
Hotplug for USB Mass Storage (UMS)
For some reason, getting USB mass storage (UMS) to work with hotplugging on Linux seems to be a bit of a mystery -- as far as I can tell, there's no documentation at all for the user. In this "how-to" note I'm going to discuss how I managed to get it working on my system. This same script works for my UMS-style iRiver MP3 player.
I have hotplugging installed on my Debian 2.6.9 version Linux system. Inside the /etc/hotplug directory is one file to examine, one file to alter, and one file to add.
Hotplugging works as follows. When I plug something into the USB bus, it generates an "add" event, and when I remove something it generates a "remove" event. The usb.agent file runs on either of these events, and is called with a list of information about the event:
idVendor idProduct bcdDevice_lo bcdDevice_hi bDeviceClass bDeviceSubClass bDeviceProtocol bInterfaceClass bInterfaceSubClass bInterfaceProtocol driver_info
The usb script also has access to these variables:
# Kernel USB hotplug params include:
# ACTION=%s [add or remove]
# DEVPATH=%s [in 2.5 kernels, /sys/$DEVPATH]
# PRODUCT=%x/%x/%x
# INTERFACE=%d/%d/%d [ for interface 0, if TYPE=0/*/* ]
# TYPE=%d/%d/%d
# And if usbfs (originally called usbdevfs) is configured, also:
# DEVFS=/proc/bus/usb [gone in 2.5]
# DEVICE=/proc/bus/usb/%03d/%03d
There's no formal documentation, as far as I can tell, what these mean, even though they are crucial to get hotplugging to work. Don't despair -- this note supplies these missing links.
The usb.agent executable now decides what to do next. It looks in usb.distmap and usb.usermap to determine if there's anything to be done.
The entries in usb.usermap and usb.distmap are as follows:
usb module match_flags idVendor idProduct bcdDevice_lo bcdDevice_hi bDeviceClass bDeviceSubClass bDeviceProtocol bInterfaceClass bInterfaceSubClass bInterfaceProtocol driver_info
"usb_module" is the name of an executable to run *if* the current event matches the information in the rest of the line. What has to match? "match_flags" tells you which items from the list must match:
USB_MATCH_VENDOR=$((0x0001))
USB_MATCH_PRODUCT=$((0x0002))
USB_MATCH_DEV_LO=$((0x0004))
USB_MATCH_DEV_HI=$((0x0008))
USB_MATCH_DEV_CLASS=$((0x0010))
USB_MATCH_DEV_SUBCLASS=$((0x0020))
USB_MATCH_DEV_PROTOCOL=$((0x0040))
USB_MATCH_INT_CLASS=$((0x0080))
USB_MATCH_INT_SUBCLASS=$((0x0100))
USB_MATCH_INT_PROTOCOL=$((0x0200))
To pick a non-random example: Let's say I have a UMS flash drive and I want to run a particular script every time it's plugged in. I used "match vendor" and "match product", which makes match_flags = 0x3, and if that UMS device is plugged in, a particular script runs.
The real mystery is how find the idVendor, idProduct, etc. I found that by using lsusb -v:
Device Descriptor:
bLength 18
bDescriptorType 1
bcdUSB 2.00
bDeviceClass 0 (Defined at Interface level)
bDeviceSubClass 0
bDeviceProtocol 0
bMaxPacketSize0 64
idVendor 0x0ea0 Ours Technology, Inc.
idProduct 0x2168 Transcend JetFlash 2.0
bcdDevice 2.00
iManufacturer 1
iProduct 2
iSerial 3
bNumConfigurations 1
so the line in usb.usermap now looks like this:
ums (module to run) 0x3 (what to match) 0x0ea0 (vendor ID) 0x2168 (product ID)
and the rest all zeros. In actuality, the line looks like:
ums 0x0003 0x0ea0 0x2168 0x0000 0x0000 0x00 0x00 0x00 0x00 0x00 0x00 0x00000000
If I were feeling clever, and I may feel clever some day, I might decide to make a universal flash, mass storage mounting hotplug system. To do that, I'd use the rest of the lsusb -v information:
bLength 9
bDescriptorType 4
bInterfaceNumber 0
bAlternateSetting 0
bNumEndpoints 3
bInterfaceClass 8 Mass Storage
bInterfaceSubClass 6 SCSI
bInterfaceProtocol 80 Bulk (Zip)
iInterface 0
Clearly, could use bInterfaceClass to select "mass storage" and write a module of that type. But I digress.
So, when I plug in my UMS device, usb.agent looks into user.usermap, finds the matching idVendor and idProduct, sees that it need only match those two, and attempts to run the module named on that line. That module is called "ums," and it resides in /etc/hotplug/usb.
"ums" accomplishes two tasks: (a) it mounts the flash drive and (b) has a script to umount the flash drive on disconnect.
Here's the ums script, part by part. First I create a log file:
MOUNTDIR="/ums"
LOG=/var/log/usb-hotplug.log
Next I try to find where this usb device is mounted, which is *really hard* to do. On my system, on each hotplug, devfs and sd_mod conspire to mount the drive on a different part of the SCSI chain. My goal is to find the SCSI device and mount it.
I log the DEVICE and DEVPATH that's inherited from the usb.agent script:
# DEVICE seems pretty useless
# need to search for "host*" in DEVPATH
echo "$(date) $(time) $DEVICE, $DEVPATH requested" >> $LOG
Next, I search along DEVPATH to find a "host[0-9][0-9]" directory. DEVPATH looks like "/devices/pci0000:00/0000:00:10.3/usb4/4-2/4-2:1.0" , and it must be preceeded by "/sys" to point to a real directory structure. If I find a directory called, e.g., host21, I've seen on my system that this means that the flash drive is associated to /dev/scsi/host21/bus0/target0/lun0/disc.
NEWDEV=""
((count=10))
while [ -z $NEWDEV ]
do
# look for host* on DEVPATH
NEWDEV=$(find /sys${DEVPATH} -name "host*") # find scsi host name
NEWDEV=$(basename $NEWDEV) # find just that host name
[[ $count -le 0 ]] && break # stop if too many attempts
((count -=1))
echo "$(date) $(time) $DEVPATH was not ready, try again" >> $LOG
sleep 1
done
This goes in a loop. The USB event happens immediately, but it takes time for sd_mod to create the SCSI entry, and so if it's not ready first time around I wait a second and try again, up to 10 times. It's never taken more than once.
NEWDEV=/dev/scsi/${NEWDEV}/bus0/target0/lun0/disc
The name of the SCSI drive that corresponds to the usb flash drive
((count=10))
if [ ! -b $NEWDEV ]
then
echo "$(date) $(time) wait for $NEWDEV to show as SCSI device" >> $LOG
sleep 1
((count -= 1))
[[ $count -le 0 ]] && break
fi
Wait to make certain that the drive is created by the SCSI software.
Mount the drive if it exists:
if [ -b $NEWDEV ]
then
{
echo "$(date) $(time) mounting $NEWDEV" >> $LOG
# mount if plugged in
mount -t vfat -o user,sync,umask=0000 $NEWDEV $MOUNTDIR >>$LOG 2>&1
}
If it doesn't exist, write a faliure to the log file:
else
echo "$(date) $(time) $DEVICE requested, unable to mount or find $NEWDEV" >> $LOG
reason=$(file $NEWDEV)
echo "Reason: $reason" >> $LOG
fi
usb.agent expects a script named $REMOVER to exist which explains what to do when the device is unplugged. Create this script, which in our case will umount the drive, by echoing instructions into the file $REMOVER:
# create REMOVER script to umount when it unplugs
:> $REMOVER
echo "umount -f $MOUNTDIR >> $LOG" >> $REMOVER
echo "echo \"$(date) $(time) unmounting /ums\" >> $LOG" >> $REMOVER
chmod +x $REMOVER
If you're curious, REMOVER will be found in /var/run/usb.
The reason for this roundabout way of finding the SCSI disk is because there doesn't really seem to be any other way of extracting the information. There's no file that I've found in the USB chain that says "this USB device is represented by the SCSI deviced named 'X'."
Log output:
Mon Jan 10 10:10:50 CST 2005 /proc/bus/usb/004/023, /devices/pci0000:00/0000:00:10.3/usb4/4-2/4-2:1.0 requested
Mon Jan 10 10:10:50 CST 2005 /devices/pci0000:00/0000:00:10.3/usb4/4-2/4-2:1.0 was not ready, try again
Mon Jan 10 10:10:51 CST 2005 mounting /dev/scsi/host21/bus0/target0/lun0/disc
At the same time, from the messages log:
Jan 10 10:10:50 bagpipes usb.agent[12238]: usb-storage: already loaded
Jan 10 10:10:50 bagpipes kernel: SCSI device sdb: 128000 512-byte hdwr sectors (66 MB)
Jan 10 10:10:50 bagpipes kernel: sdb: assuming Write Enabled
Jan 10 10:10:50 bagpipes usb.agent[12238]: ums: loaded successfully
Jan 10 10:10:50 bagpipes kernel: /dev/scsi/host21/bus0/target0/lun0:
Jan 10 10:10:50 bagpipes kernel: Attached scsi removable disk sdb at scsi21, channel 0, id 0, lun 0
Jan 10 10:10:51 bagpipes scsi.agent[12240]: disk at /devices/pci0000:00/0000:00:10.3/usb4/4-2/4-2:1.0/host21/21:0:0:0
