程序如何正确识别USB设备
背景
在可视化客户端程序中,可能会存在需要自动读取和写入USB的场景,为了极致的用户体验,我们需要尽可能保证读写过程中的智能、灵敏、可靠、安全。这篇文章讲从我个人实践和思考出发提供一些思路。
识别USB并找到挂载点
这里要解决识别USB的插入和拔出,核心思路是通过执行系统命令来检索USB,根据两次检索结果的对比来发现USB插拔。
这里不同系统使用的命令不同,这里对各系统提供一个参考:
linux
- 检索设备和挂载点
Shell命令
# 检索所有设备和挂载点, -r参数能保证输出的内容是程序可读的,对于返回结果中存在的空格等特殊字符进行Ascii编码。
findmnt -r --output SOURCE,TARGET
结果
注意这里的结果中可能存在ASCII编码,在程序读取使用时可能需要将Ascall码转为字符。
- 识别设备是否为USB
Shell命令
# 命令用于获取指定设备的属性信息。这个命令可以提供关于设备的各种属性,例如设备的路径、设备类型、厂商信息、设备驱动程序等。返回的内容是一系列属性键值对,每一行表示一个属性。
udevadm info -q property -n '这里填上面获得的SOURCE'
结果
DEVPATH: 设备的路径。
DEVNAME: 设备的名称。
MAJOR: 设备的主设备号。
MINOR: 设备的次设备号。
DEVTYPE: 设备类型,例如 disk、partition、usb_device 等。
ID_VENDOR: 设备的制造商。
ID_MODEL: 设备的型号。
ID_SERIAL: 设备的序列号。
ID_FS_TYPE: 设备文件系统的类型。
ID_FS_LABEL: 设备的文件系统标签。
ID_BUS: 设备连接的总线类型,例如 usb、scsi 等。
DRIVER: 设备的驱动程序。
SUBSYSTEM: 设备所属的子系统,例如 block、input、net 等。
我们可以通过 ID_BUS = usb 来识别一个USB设备, 同时可以以 ID_SERIAL 作为设备标识。
mac
Shell命令
# 用于在 macOS 系统中检索有关 USB 设备的信息,并以 JSON 格式返回结果。返回的 JSON 结构提供了有关系统中连接的 USB 设备的详细信息。
system_profiler SPUSBDataType -json
结果
{
"SPUSBDataType" : [
{
"_name" : "USB31Bus",
"host_controller" : "AppleT8112USBXHCI"
},
{
"_name" : "USB31Bus",
"host_controller" : "AppleT8112USBXHCI"
},
{
"_items" : [
{
"_name" : "USB3.1 Hub",
"bcd_device" : "6.56",
"bus_power" : "900",
"bus_power_used" : "0",
"device_speed" : "super_speed",
"extra_current_used" : "0",
"location_id" : "0x00200000 / 1",
"manufacturer" : "GenesysLogic",
"product_id" : "0x0626",
"vendor_id" : "0x05e3 (Genesys Logic, Inc.)"
},
{
"_items" : [
{
"_name" : "USB Type-C Digital AV Adapter",
"bcd_device" : "0.00",
"bus_power" : "500",
"bus_power_used" : "496",
"device_speed" : "low_speed",
"extra_current_used" : "0",
"location_id" : "0x00140000 / 3",
"manufacturer" : "xxxxxxxx",
"serial_num" : "000000000000",
"vendor_id" : "0x343c"
},
{
"_name" : "U268",
"bcd_device" : "1.00",
"bus_power" : "500",
"bus_power_used" : "100",
"device_speed" : "high_speed",
"extra_current_used" : "0",
"location_id" : "0x00110000 / 4",
"manufacturer" : "aigo",
"Media" : [
{
"_name" : "U268",
"bsd_name" : "disk4",
"Logical Unit" : 0,
"partition_map_type" : "master_boot_record_partition_map_type",
"removable_media" : "yes",
"size" : "62.91 GB",
"size_in_bytes" : 62914560000,
"smart_status" : "Verified",
"USB Interface" : 0,
"volumes" : [
{
"_name" : "My-USB",
"bsd_name" : "disk4s1",
"file_system" : "ExFAT",
"free_space" : "62.85 GB",
"free_space_in_bytes" : 62854397952,
"iocontent" : "Windows_NTFS",
"mount_point" : "/Volumes/My-USB",
"size" : "62.91 GB",
"size_in_bytes" : 62914494464,
"volume_uuid" : "5568DA0E-4C7A-3E41-8392-4619F2F6FDEE",
"writable" : "yes"
}
]
}
],
"product_id" : "0x6387",
"serial_num" : "EA3EA27F",
"vendor_id" : "0x3535"
}
],
"_name" : "USB2.1 Hub",
"bcd_device" : "6.56",
"bus_power" : "500",
"bus_power_used" : "100",
"device_speed" : "high_speed",
"extra_current_used" : "0",
"location_id" : "0x00100000 / 2",
"manufacturer" : "GenesysLogic",
"product_id" : "0x0610",
"vendor_id" : "0x05e3 (Genesys Logic, Inc.)"
}
],
"_name" : "USB31Bus",
"host_controller" : "AppleT8112USBXHCI"
}
]
}
找到其中包含 Media
的内容即为USB信息,mount_point为挂载点。因为这里USB是通过Type-C适配器接入电脑的,所以返回信息包含了适配器的一些信息,导致层级较深,用程序识别时需注意识别算法的兼容性。
windows
Powershell命令
# The chain of objects is:
# PnPEntity (PnP = Plug and Play)
# -> USBController
# -> Some associator of USBController that has a related disk drive
# -> diskDrive
# -> diskPartition
# -> logicalDisk
# Find PnPEntity for given VID and PID
# $usbPnPEntity = (gwmi Win32_PnPEntity | where DeviceID -match $input)
$usbPnPEntitys = (gwmi Win32_PnPEntity | where Service -eq "USBSTOR")
foreach ($usbPnPEntity in $usbPnPEntitys) {
# Get USB Controller related to PnP Entity
$usbController = $usbPnPEntity.getRelated("Win32_USBController")
$usbControllerID = $usbController.DeviceID
# Find objects associated with the USB Controller
$query = "ASSOCIATORS OF {Win32_USBController.DeviceID=\$usbControllerID\}"
$associators = ([wmisearcher]$query).get()
# Search through associators
foreach ($associator in $associators) {
# Find associator that is related to a disk Drive
$assoDeviceID = $associator.DeviceID
$diskDrive = (gwmi win32_diskdrive | where PNPDeviceID -eq $assoDeviceID)
if($diskDrive){
# Get logical Disk related to the disk drive
$logicalDisk = $diskDrive.getRelated("Win32_DiskPartition").getRelated("Win32_LogicalDisk")
# Print device ID which is the drive letter (e.g. C:)
$usbPnpEntity.DeviceId,$logicalDisk.VolumeName,$diskDrive.Model,$associator.DeviceID,$logicalDisk.DeviceID -Join ",,,"
# break
}
}
}
检查正确性
现在我们已经发现了USB并确定了其挂载点,我们可以立刻通过挂载点来读写文件(需要注意挂载点中的空格等特殊字符对读写路径的影响)。 但是建议在做读写之前对挂载点做一次正确性验证,因为我们可能无法完全保证前面的脚本在所有环境和场景下都能如期运行。
如何确认挂载点是否正确呢?
-
通过挂载点反查设备信息
通过挂载点去查设备信息,如果查到的设备信息和前面获得的设备信息相同即表示挂载点无误。
Linux
findmnt --output SOURCE -n -T '此处填写挂载点'
-
尝试通过挂载点读取指定的文件内容
提前在USB中放入指定的文件,通过挂载点读取文件,若读取到正确内容则能间接证明挂载点可用。
检查合法性
在一些高安全级别操作中,我们可能需要只认可特定的USB操作,这就需要我们给USB附加权限属性。
两种思路如下:
- 提前在系统中写入USB序列号,并在系统中配置好权限。识别到USB后通过序列号校验是否有操作权限。
- 在系统外部根据USB信息签署一个License,License中有权限信息。 系统读取USB中的License文件并校验其签名合法性。
写入正确性检查
在我们向USB中写入数据后,我们如何确保数据正确写入了?
为什么会有这个问题?
我们可能会觉得这个问题是在画蛇添足,我们深入想想系统写入数据的过程:
- 系统找到我们提供的挂载目录
- 如果目录不存在则创建目录
- 创建文件
- 打开文件
- 向文件写入数据
- 关闭文件
在这个简略的写数据过程中,我们发现第二步并不符合我们的预期,“无报错”的情况无法判断挂载目录是否正确。如果我们这时用的挂载目录已经不对了,数据可能会写入到无法预期的一个目录中。
如何规避?
- 写入之前针对挂载点再进行一次**“合法性检查”**
- 写入数据后再从挂载点中读出全部或部分写入数据,验证写入数据存在且正确。