1

To eventually put into a bash script and/or Makefile, I'm wanting to find the device node path of the last inserted USB drive on a Debian system.

It will most likely be something like /dev/sdb, but rather than assuming & hardcoding this, I'd like to find the device source programmatically, if possible.

I can see a list of mount points using findmnt and spot the USB drive by eye in that list, but there doesn't seem to be a robust way to search using findmnt unless you know either the USB drive's "target" or "source" field value exactly, (e.g. /media/user/Thumbdrive) which we can't know dynamically:

findmnt --noheadings --output source --target /media/user/Thumbdrive
/dev/sdb1

Some guides I looked at online mentioned you could trawl the output of dmesg to get information about the last inserted USB device, and that looks promising (because there's a sdb: sdb1 line, and entries are logged by time since boot), but how/what to pattern match for such output given it would probably be most sensible to look for entries after, say, usb-storage first for context, and it could be a variable amount of lines before we get to the sdX: X entry (as well as the possibility of being interleaved with other unrelated kernel messages)?

[58972.861628] usb 1-1: new high-speed USB device number 12 using xhci_hcd
[58973.017906] usb 1-1: New USB device found, idVendor=0930, idProduct=1400, bcdDevice= 1.00
[58973.017912] usb 1-1: New USB device strings: Mfr=1, Product=2, SerialNumber=3
[58973.017916] usb 1-1: Product: TOSHIBA USB DRV
[58973.017919] usb 1-1: Manufacturer: TOSHIBA
[58973.017921] usb 1-1: SerialNumber: 03A81B061C4C86
[58973.019758] usb-storage 1-1:1.0: USB Mass Storage device detected
[58973.020517] scsi host2: usb-storage 1-1:1.0
[58973.116352] [UFW BLOCK] IN=wlp1s0 OUT= MAC=...
[58974.118040] [UFW BLOCK] IN=wlp1s0 OUT= MAC=...
[58975.160865] scsi 2:0:0:0: Direct-Access     TOSHIBA  TOSHIBA USB DRV  PMAP PQ: 0 ANSI: 4
[58975.161608] sd 2:0:0:0: Attached scsi generic sg1 type 0
[58975.162908] sd 2:0:0:0: [sdb] 60555264 512-byte logical blocks: (31.0 GB/28.9 GiB)
[58975.163588] sd 2:0:0:0: [sdb] Write Protect is off
[58975.163598] sd 2:0:0:0: [sdb] Mode Sense: 23 00 00 00
[58975.164252] sd 2:0:0:0: [sdb] No Caching mode page found
[58975.164264] sd 2:0:0:0: [sdb] Assuming drive cache: write through
[58975.181161]  sdb: sdb1
[58975.196147] sd 2:0:0:0: [sdb] Attached SCSI removable disk
[58976.576602] [UFW BLOCK] IN=wlp1s0 OUT= MAC=...

For instance, this awk line is a start, but it's not robust.

dmesg -t | awk '/^\s+sd\w:\s.*/ {print "/dev/"$2}' | tail -1
/dev/sdb1

Any ideas? Or better yet, is there a tool specifically for this job (preferably one that bundles with Debian by default) that I am yet to discover?

Thanks!

Update

To be more clear, I'm looking for a solution that can be invoked inside a bash script or Makefile (and most likely without superuser access beforehand), and which assumes a disk has been inserted before the script runs, so solutions that require creating files to monitor kernel info beforehand are maybe not desirable.

Also, if a drive has been inserted and then removed before the script runs, that will also need to be considered at eventual runtime--i.e. the best solution would know the current system state (or at least one as close as possible to current).

The bash script/Makefile is attempting to make it easier for the user to write a disk image to the USB drive (by guessing what the USB drive might be and suggesting that), so trying to reduce false positives and so on will be a consideration.

7
  • 2
    I guess you can use a udev rule to write/append to a file the "kernel name" (devnode name) by matching with e.g. SUBSYSTEM=="block", ENV{DEVTYPE}=="disk", SUBSYSTEMS=="usb". ("singular" and "plural" have very different meaning in udev matching) See the man page of udev for more details regarding RUN and %k.
    – Tom Yan
    Commented Jun 13, 2022 at 6:31
  • 1
    Ditto. See this answer. Commented Jun 13, 2022 at 6:33
  • Thanks, but I think I'm gonna need a bit more help on what you're talking about!
    – algalg
    Commented Jun 13, 2022 at 9:27
  • Feedback. I removed my upvote because: (1) Your additional requirements made my answer no longer fit the question. (2) Cross-posting should not happen. Commented Jun 14, 2022 at 10:19
  • Fine, but the site is saying I can't delete this now because it has answers.
    – algalg
    Commented Jun 14, 2022 at 11:33

3 Answers 3

0

Preliminary note

This answer was written before the OP added the following:

I'm looking for a solution that can be invoked inside a bash script or Makefile (and most likely without superuser access beforehand), and which assumes a disk has been inserted before the script runs, so solutions that require creating files to monitor kernel info beforehand won't work here.

For the answer to work, you need to create a file with specific content. Root access is required for this, but creating the file is a one-time act. The file will survive reboots. After the file is created, the solution will work.


Basic solution

Create /etc/udev/rules.d/91-usb-info.rules with the following content:

SUBSYSTEM=="block", ENV{DEVTYPE}=="disk", SUBSYSTEMS=="usb", ACTION=="add", RUN+="/bin/sh -c 'echo \"$name\" >/dev/shm/usb-info'"

This writes (rather than appends) to /dev/shm/usb-info. The file will hold at most one name, the name of the last inserted USB drive. This means you don't need to parse nor filter the file, it's enough for you to read it.

/dev/shm is a temporary filesystem, tmpfs, it does not survive reboots. If the file is not there then you know no device has triggered the rule yet.

There are at least two problems:

  1. Redirection with > truncates the file first, writing happens later. It can happen you read the file when it's empty. A standard way to deal with this is to update the file by writing to another file on the same filesystem and then atomically moving (mv) to the desired name (I believe mv within tmpfs is atomic).

  2. If the device is removed then the name of it won't be removed from the file. Note technically it's still the name "of the last inserted USB drive", even if the drive is no longer there. So it's what you explicitly requested; I'm not sure if it's what you really wanted though.


Less basic solution

Use these instead (not along with) the above basic solution:

SUBSYSTEM=="block", ENV{DEVTYPE}=="disk", SUBSYSTEMS=="usb", ACTION=="add", RUN+="/bin/sh -c 'echo \"$name\" >>/dev/shm/usb-info'"
SUBSYSTEM=="block", ENV{DEVTYPE}=="disk", SUBSYSTEMS=="usb", ACTION=="remove", RUN+="/usr/bin/sed -i \"/^$name$/d\" /dev/shm/usb-info"

Now /dev/shm/usb-info is a newline-terminated list of names. Get the last one with tail -n 1 /dev/shm/usb-info 2>/dev/null. An empty result means "no device has been connected yet" or "all the devices previously connected has been disconnected already"; one way or another: "no suitable device is available".

The solution is not totally perfect. Few concerns:

  1. $name is embedded in the sh/sed code. An unfortunate name may break the code (compare to embedding {}, similar story). Names like sdb are fortunate though.

  2. An unfortunate name may misbehave with echo. Names like sdb are fortunate though.

  3. We expect that appending a short string with echo … >> … uses a single system call, so it's atomic. I mean tail -n 1 /dev/shm/usb-info will never give you a line not-yet-fully-written; it will see the file before echo or after echo, but not during echo. In general, if you wanted to write a longer string, then you should use sed -i here as well, or something like cp /dev/shm/usb-info /dev/shm/elsewhere && echo … >> /dev/shm/elsewhere && mv /dev/shm/elsewhere /dev/shm/usb-info.

  4. If many USB devices get connected/disconnected within few seconds, then it may happen many instances of the rules try to process /dev/shm/usb-info simultaneously. To avoid race conditions that may cause the file miss some devices (or miss their absence), we should make sure the rules run one at a time. Possibilities:

    • I'm not an expert with udev, but I suppose udevadm control -m 1 may be the simplest solution. See man 8 udevadm.

    • Alternatively what we run inside RUN+ should use flock. Hint: do not use /dev/shm/usb-info as a file targeted by flock, because sed -i replaces the file with a file with another inode and this will make the lock futile. Create a separate file exclusively for flock.

Still, in most cases the solution I gave you should behave well. Now you know some potential flaws, so improve the solution if you think you need to. I'm not going to create a totally robust solution here.

If you decide to improve the solution, consider putting code in a separate script (e.g. /usr/local/bin/usb_info_helper) and keep the udev rule simple (e.g. RUN+="/usr/local/bin/usb_info_helper add $name"). The point is the improved code will not be trivial. Having it in a separate file will allow you to keep it clean, multi-line, properly indented; additionally it will be easier to quote properly.

0
0

Using lsblk and some pattern matching and alphabetical sorting could be an interim approach:

lsblk --noheadings --raw --output rm,tran,type,path --sort path | awk '/^1 usb disk/ {d=$4} END {print d}'

With:

  • --noheadings to remove the column output headings
  • --raw to remove the columns formatting
  • --output rm,tran,type,path to return only column listings by:
    • rm removable devices flag (0 or 1)
    • tran device transport type (usb, sata, etc)
    • type device type (disk, partition, etc)
    • path path to the device node (/dev/sda, for example)
  • --sort path to sort the output list by the 'path' column alphabetically (i.e. sda sdb sdc)

Then pipe that output to awk to match:

  • lines that have a 1 flag for removable media,
  • match the usb 'tran' column, and are disk 'type'
  • then capture the final column, which is the path, into $4
  • traverse to the END of the list, and then print that so we have the best guess of the "last mounted" removable drive is.

Pros

  • A one liner using common tools, and no need for sudo.

Cons

  • It relies on alphabetical sorting, and so will not reliably match a device that has been disconnected/reconnected out of alphabetical order. i.e. It assumes the current machine state is one where the last USB drive has been given an ascending alphabetical device node path. For example, if USB 1 is inserted at /dev/sda and USB 2 at /dev/sdb and USB 1 is removed and reinserted (and the system assigns it to /dev/sda because that node path is now free again), this pattern will match USB 2 at /dev/sdb even though technically, USB 1 is the "last inserted USB drive."
0

Building on this answer, a more robust solution for a script might be to pattern match fdisk using awk:

last_disk="$(sudo fdisk --list | awk -F '[ :]' '$0 ~ "^Disk /dev/" {path=$2} END {print path}')"

will give you the device node path of the "last disk", which you could then test against lsblk to see if it's a USB device:

if [[ -z "${last_disk}" ]] || \
   ! lsblk --raw --noheadings --output tran,type,rm "${last_disk}" | grep --basic-regexp --quiet '^usb disk 1'; then
    >&2 echo "No usable device found. Please insert a USB disk and try again."
    exit 2
fi

where:

  • --output tran,type,rm returns only column listings by:
    • tran device transport type (usb, sata, etc)
    • type device type (disk, partition, etc)
    • rm removable devices flag (0 or 1)

so grep can do the following work:

  • return success if a line starts with the transport type "usb", and is a "disk" and also has removable device flag set to "1"

If all that is successful, the device node path in ${last_disk} is our last inserted USB device.

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .