C#实现USB插入检测,移除

前言

 

尽管使用Windows shell(传说中的“命令行”)移除一个USB是非常容易的,但是想通过编程实现却非常恼火。你必须去了解很多内核驱动开发的底层概念,才能完成一个简单的任务。当我着手做这件事的时候,我真的不知道该从何入手。我很确定我不想在内核驱动控制代码,Windows Setup和Configuration Manager API,WMI...中转来转去了。

好吧,这就是我写这篇文章的主要原因,我认为这个主题的文章真的有必要。最后你将看到一个.Net WinForm的Demo,和一个可复用的代码。

 

背景

首先,让我来谈谈将要用到的windows API(Win32就免了吧):

1. The Windows DDK (Driver Development Kit)一个驱动开发包,提供编程环境,工具,开发案例,和相关的支持文档。

2. The Setup API很少被人所知的Windows APi。其中一个原因是它已经包含在DDK中了,但是,事实上,还是有很多常用的东西的,其中有些可以用于安装应用程序的时候,并且还有很多有趣的东西,就像CM_Request_Device_Eject函数,它就是我们这个UsbEject程序的核心所在。

3. The DeviceIoControl function发送一个控制代码直接到指定的设备驱动程序,导致相应的设备来执行相应的操作。它可以完成很多事情,但是文档不全。

4. WMI (Windows Management Instrumentation):事实上,UsbEject 并没有使用WMI,因为据我所知,WMI并不能解决设备的移除。

5. Windows消息: WM_DEVICECHANGE消息,当有设备变化(usb插入或者移除)的时候,产生WM_DEVICECHANGE消息。因此我们可以通过这个消息来刷新页面显示当前设备。

现在让我来介绍一些概念。这些概念是我自己定义的,并不是官方的(官方的定义确实很难找)。

6. Physical Disks:正如其名,它是用户正在操作的一块硬件。就像我们说Usb Disk,那就是一个U盘。

7. Volumes:事实上,有两种“卷”:一种是由操作系统本身理解的,像我们看到的从A到Z的驱动器号,也被称作逻辑磁盘,一个卷可以跨越几个物理磁盘。

8. Devices:在操作系统的DDK中,以上两个都被定义为Device(设备)。

我的代码中反映了上述情况,Devices是基类,Volumes继承于它。

(译者注:您很有必要理解上述概念,因为本文的后续部分将多次使用该概念。)

 

使用代码

 

在工程的Library文件夹中,是代码的核心。其中的类的关系如下图:

这里有5个主要的类:

1. DeviceClass:一个代表物理设备的抽象类,它包含一个Device的链表。

2. DiskDeviceClass:代表系统中所有的磁盘设备。

3. VolumeDeviceClass:代表系统中所有的卷

4. Device:代表一个通用设备的任何类型(磁盘,卷.....),注意没有Diskclass,因为在这个项目中,与Device相比,磁盘没有特定的属性。还请注意,代码已经被设计成可以会扩大到其他的设备,不仅是卷和磁盘。

5. Volume:代表一个操作系统的卷,一个卷分配了一个逻辑驱动器号(如C,D)。

你可以简单的移除USB像下面的例子那样:

VolumeDeviceClass volumeDeviceClass = new VolumeDeviceClass();
foreach (Volume device in volumeDeviceClass.Devices)
{
    // is this volume on USB disks?
    if (!device.IsUsb)
        continue;

    // is this volume a logical disk?
    if ((device.LogicalDrive == null) || (device.LogicalDrive.Length == 0))
        continue;

    device.Eject(true); // allow Windows to display any relevant UI
}


 

 

几个有趣的地方


CM_Request_Device_Ejec函数:

这是一个SetupSpi 函数用来移除一个设备(它可以移除任何设备)。它需要从第一个参数传入一个设备句柄(device instance handle)。函数会根据第二参数是否提供产生不同的效果。

如果提供,当移除USB的时候,不会产生弹出任何对话框(程序会默默的执行,不管移除成功还是失败)。相反,如果不提供该参数,当移除USB的时候,系统会以对话框或者气泡的方式提醒用户。

现在的主要问题是:对于一个给定的磁盘号(C,D,..H),如何找到哪个设备是将要移除的呢?

可以移除的设备一定是一个磁盘设备,而不是卷(至少对于USB磁盘是这样)。

1. Environment.GetLogicalDrives,使用该函数获得所有的逻辑磁盘。

2. 遍历所有的逻辑磁盘(根据磁盘号C,D......),根据GetVolumeNameForVolumeMountPoint确定真正的卷名(“卷名”将在下面讲到)。

3. 对于每一个卷,确定是否有一个逻辑磁盘号对应。

4. 对于每一个卷,确定其物理磁盘的组成(因为可以多个磁盘对应一个卷),可以使用DDK的IOCTL_VOLUME_GET_VOLUME_DISK_EXTENTS 

5. 因为即插即用(PNP)配置管理器构建了一个设备层次结构,一个物理磁盘设备不一定就是要移除的设备,于是对于每一个物理磁盘设备,应该移除它的上层设备。被移除的设备具有CM_DEVCAP_REMOVABLE 属性的上层设备。

(译者注:关于这5点,确实有点难懂,毕竟涉及到底层知识,只有根据代码慢慢体会了)

 

 

GetVolumeNameForVolumeMountPoint 函数:

为了可以根据逻辑磁盘(logical disk)找到相应的卷(volumes ),我们有一个小技巧。首先,我们要知道所有Windows的卷可以唯一地被指定,语法的格式为:"\\?\Volume{GUID}\",

GUID就是用来识别卷的。

在枚举所有的卷的时候,上述格式的字符串可以通过调用GetVolumeNameForVolumeMountPoint函数获得。

为了便于理解,我把作者使用该函数的代码放出来:

foreach (string drive in Environment.GetLogicalDrives())

{

          StringBuilder sb = new StringBuilder(1024);

          if (Native.GetVolumeNameForVolumeMountPoint(drive, sb, sb.Capacity))

          {

              _logicalDrives[sb.ToString()] = drive.Replace("\\", "");

          }

}


对于给定的类型枚举所有的设备

下面是我给出的一段.Net互操作枚举给定类型的所有设备的代码:

int index = 0;

while (true)

{

    // enumerate device interface for a given index

    SP_DEVICE_INTERFACE_DATA interfaceData = new SP_DEVICE_INTERFACE_DATA();

    if (!SetupDiEnumDeviceInterfaces(

        _deviceInfoSet, null, ref _classGuid, index, interfaceData))

    {

        int error = Marshal.GetLastWin32Error();

        // this is not really an error...

        if (error != Native.ERROR_NO_MORE_ITEMS)


            throw new Win32Exception(error);

        break;

    }


    SP_DEVINFO_DATA devData = new SP_DEVINFO_DATA();

    int size = 0;

    // get detail for all the device interface

    if (!SetupDiGetDeviceInterfaceDetail(

        _deviceInfoSet, interfaceData, IntPtr.Zero, 0, ref size, devData))

    {

        int error = Marshal.GetLastWin32Error();

        if (error != Native.ERROR_INSUFFICIENT_BUFFER)

        throw new Win32Exception(error);

    }


    // allocate unmanaged Win32 buffer

    IntPtr buffer = Marshal.AllocHGlobal(size);

    SP_DEVICE_INTERFACE_DETAIL_DATA detailData =

                    new SP_DEVICE_INTERFACE_DETAIL_DATA();

    detailData.cbSize = Marshal.SizeOf(

                        typeof(Native.SP_DEVICE_INTERFACE_DETAIL_DATA));


    // copy managed struct buffer into unmanager win32 buffer

    Marshal.StructureToPtr(detailData, buffer, false);


    if (!SetupDiGetDeviceInterfaceDetail(

        _deviceInfoSet, interfaceData, buffer, size, ref size, devData))

    {

        Marshal.FreeHGlobal(buffer); // don't forget to free memory

        throw new Win32Exception(Marshal.GetLastWin32Error());

    }


    // a bit of voodoo magic. This code is not 64 bits portable :-)

    IntPtr pDevicePath = (IntPtr)((int)buffer + Marshal.SizeOf(typeof(int)));

    string devicePath = Marshal.PtrToStringAuto(pDevicePath);

    Marshal.FreeHGlobal(buffer);

    index++;

}


刷新UI:当一个Disk(U盘)插入或者一移除

有很多方法可以完成这件事,但是我选择了一个最简单的方法:捕获WM_DEVICECHANGE消息。你需要做的仅仅是override 默认的窗体函数,如下:

protected override void WndProc(ref Message m)

{

    if (m.Msg == Native.WM_DEVICECHANGE)

    {

        if (!_loading)

        {

            LoadItems(); // do the refresh work here

        }

    }

    base.WndProc(ref m);

}


一串序列号(Serial Numbers)

这个和usb移除没有太大关系了,但是我还是想谈一点点这个,因为很多人问过关于它的问题。对于操作系统的卷或者磁盘,通常有至少有两个Serial Numbers:

卷的软件序列号:在格式化过程中被指定。这个32位的值可以很很轻松的通过win32 api GetVolumeInformation 获得。

磁盘的出厂硬件序列号:这个序列号是设置的供应商生产过程中。它是一个字符串。当然,它不能被改变。不幸的是,你必须知道,序列号是可选的,所以USB存储棒可能没有,事实上,很多都没有。

WMI: 到目前为止,它是完成此事的最简单的办法。

下面给出WMI获得Serial Numbers的代码:

// browse all USB WMI physical disks

foreach(ManagementObject drive in new ManagementObjectSearcher(

    "select * from Win32_DiskDrive where InterfaceType='USB'").Get())

{

    // associate physical disks with partitions

    foreach(ManagementObject partition in new ManagementObjectSearcher(

        "ASSOCIATORS OF {Win32_DiskDrive.DeviceID='" + drive["DeviceID"]

          + "'} WHERE AssocClass = 

                Win32_DiskDriveToDiskPartition").Get())

    {

        Console.WriteLine("Partition=" + partition["Name"]);


        // associate partitions with logical disks (drive letter volumes)

        foreach(ManagementObject disk in new ManagementObjectSearcher(

            "ASSOCIATORS OF {Win32_DiskPartition.DeviceID='"

              + partition["DeviceID"]

              + "'} WHERE AssocClass =

                Win32_LogicalDiskToPartition").Get())

        {

            Console.WriteLine("Disk=" + disk["Name"]);

        }

    }


    // this may display nothing if the physical disk

    // does not have a hardware serial number

    Console.WriteLine("Serial="

     + new ManagementObject("Win32_PhysicalMedia.Tag='"

     + drive["DeviceID"] + "'")["SerialNumber"]);

}


 说明

第一次发表译文,最近一直在研究U盘的插入识别和编程实现移除U盘,网上查了很多资料,最后找到了这篇文章,研究了一下,发现国内这方面的介绍实在太少,因此就有了将它翻译一下的想法,希望给大家一些启发,少走弯路吧。翻译得不好还请大家多多批评指正,但是请勿人身攻击哦。

原文出处:http://www.codeproject.com/KB/system/usbeject.aspx

由于原文代码已经无法下载,我把它整理了一下,用vs2012编译,在win7下运行成功(需要管理员权限),您可以在以下地址获得源代码:http://download.csdn.net/detail/kxloveh/4740293

 

你可能感兴趣的:(C#)