Scott Hanselman
Corillian Corporation
注:该项目的源代码仅仅是一个开始。在 SourceForge 中,该代码会进一步发展。访问该站点,获得指导代码演变的帮助或下载 .NET Framework 2.0 或 1.1 的最新版本。
硬件 | |
与 USB 进行的交互和一些抽象 | |
消灭 Bug、阅读手册 | |
特定于用户的设置 | |
使用插件扩展应用程序(任何语言!) | |
小结 |
摘要:在“Some Assembly Required”专栏的第四期中,Scott Hanselman 和 Bryan Batchelder 发现了一个非常吸引人的硬件,但是它附带的软件却非常糟糕,以至于他们使用 .NET Framework 2.0 编写了自己的软件。您可以从一些在线零售商那里购买一个由无签名公司制造的带有 USB 接收器的小型无线 key fob(它被称为“无线 USB 安全设备”),这意味着可以在离开时锁定计算机,在返回时取消锁定。但是,它附带的软件很糟糕。因此,我们策划了“一些必需的程序集”。(希望该公司的相关人员会阅读这篇文章并开始使用我们的软件!)我们还将使用以 Visual Basic 编写的一些插件,通过全新的功能来扩展应用程序!
这是一个多么奇特的主意!将一个浅绿色按钮(在 NewEgg 只需 15 美元就能买到)系到您的钥匙圈上。它变身为您计算机的一个“存在”指示器。您到了,它知道;您离开,它也知道。它能够执行诸如锁定计算机、调低音量或运行特定任务这样的操作。棒极了,是吗?并非如此。在 1995 年左右,硬件功能已经很好了,但附带的软件却是古怪的小程序,“锁定”计算机不过是使用它自己的非标准大窗口来覆盖用户所有的应用程序,并强制用户输入密码来去掉这个窗口。不,这不是您的 Windows 登录密码,而是另一个完全不同的特定于应用程序的密码。天啊!
而且,这个小程序不能以任何方式扩展,看起来也没有包含任何 COM 或 .NET 库以轻松地接收设备事件。但是,这个主意 和这块硬件真是太吸引人了,我和 Greg Hughes 已经多次讨论过如何为这个小按钮编写更好的程序,只是还未付诸实践。Bryan Batchelder 对此也同样感兴趣,感谢他为我们所有人作出的尝试。
在浅绿色 USB 无线安全 Key Fob 背后有两块硬件:产生小范围 (10m) 信号的按钮(夹在您的钥匙上)和一个插入计算机的小型 USB 接收器。有趣的是,将接收器第一次插入时,它不需要驱动程序。Windows 可以自动识别这个小东西!这怎么可能?在运行 msinfo32.exe 时(您的 系统上也有这个不常见的应用程序,现在就运行它吧!),我注意到它将自己注册为一个“USB 人机接口设备”(鼠标)和一个“游戏控制器”,这两种设备是 Windows 已经识别的。这样做是有意义的,因为制造这些小型设备的公司会使用常见的 USB 芯片集,例如那些在便宜的鼠标中使用的 USB 芯片集。而且,无需编写自定义设备驱动程序。在下图中,请注意 USB 接收器设备的 PNP ID 是 “VID_04b4&PID_7417”。这个 ID 很重要:在后面,当我们以编程方式查找设备时,需要使用它。
与一个 USB 设备进行会话和与一个串行端口进行会话完全不同。串行端口的名称类似于“COM3”,而且无论什么设备插入 COM3,它都是 COM3。如果您想查找一个串行端口设备,但是不确定哪个端口是打开的,那么就必须以编程方式对系统上的每个串行端口“大喊”:“是你吗?”但在 USB 环境中,您知道要查找的设备类型,并且无需关心哪个端口是打开的。您只知道需要与端口进行会话。
遗憾的是,.NET 包含的基类库 (BCL) 不支持与 USB 设备进行会话。大多数情况下,如果要从 .NET 访问 USB 设备,需要使用设备制造商提供的高级类库。但如我们所说的那样,本例中制造商没有提供任何可以利用的类库,因此,我们将从头开始。但是,我们将使用一些源自 kernel32.dll、setupapi.dll、和 hid.dll(“hid”的意思是人机接口设备)的 Win32 API。
我们从构造几个抽象层开始,因为即使能够 仅从 UI 调用所有这些 Win32 函数,我们真正 想要的还是一个有用的“KeyFob”类,不是吗?下面是一个使用 Visual Studio 2005 专业版创建的类关系图。您可以从左至右阅读该关系图。您可能认为 KeyFob 是应用程序要使用的类,因为它是一个不错的小型设备逻辑表示图,我们很自然地想到使用它作为 key fob。但是,我们的应用程序真正关心的概念是存在,我们必须考虑现在有多个 key fob,其中的任何一个都可以与单个接收器一起使用。所以,我们需要一个 KeyFobCollection,它是 PresenceManager 将使用的一个通用 Dictionary。PresenceManager 将管理一个经过授权的 fob 列表,其中的 KeyFob 能够通过它们的存在影响系统。
KeyFob 具有 Status、SerialNumber 和 HandleMessage 方法以及诸如 IsAuthorized 和 LastHeartbeat 这样有帮助的信息。它保护我们无法直接使用的项目,最有趣的一个项目就是 KeyFobReceiver。该类整体封装在 KeyFob 类内部,为 KeyFob 类提供信息,但我们在外部不必了解这些信息的内容,例如字节数组 lastPacket。再往下,KeyFobReceiver 有一个 UsbStickReceiver 类的实例。这是正式标识该设备是一个 USB 设备的第一个类,在这里执行一些非常低级的 I/O。它有一个 USBSharp 类 的实例,该类是前面提到的所有 Win32 DLL API 周围的托管包装类。
真正奇妙而有趣的事发生在 UsbStickReceiver 之中,位于低级 API 的上一层。USBSharp 类负责处理各种需要传入和接收的 Windows SDK 数据类型的封送。下面,我们来看一下 UsbStickReceiver 的 FindReceiver 方法。在插入接收器之前,我们的应用程序不会发生什么事情,对吗?
在下列代码中,我加入了注释来解释正在发生的事情和我们尝试完成的操作。
public static UsbStickReceiver FindReceiver() { //We haven't found any devices int my_device_count = 0; //We have no idea where our device is (yet) string my_device_path = string.Empty; //But we know we'll need all these methods! USBSharp.USBSharp myUsb = new USBSharp.USBSharp(); //And we need to get the Human Interface Device GUID myUsb.CT_HidGuid(); //Let the system know we're looking for active devices myUsb.CT_SetupDiGetClassDevs(); //Get ready... int result = -1; int device_count = 0; int size = 0; int requiredSize = 0; //While nothing goes wrong while(result != 0) { //Starting with device 0... result = myUsb.CT_SetupDiEnumDeviceInterfaces(device_count); //Let me know how much room I'm going to need to get info about this device int resultb = myUsb.CT_SetupDiGetDeviceInterfaceDetail(ref requiredSize, 0); //Cool, store that size = requiredSize; //Gimme the info you've got resultb = myUsb.CT_SetupDiGetDeviceInterfaceDetailx(ref requiredSize, size); //Did we find the USB Receiver? Remember it's name from earlier? if(myUsb.DevicePathName.IndexOf("vid_04b4&pid_7417") > 0) { //Sweet, it's device # "device_count," let's store this! my_device_count = device_count; my_device_path = myUsb.DevicePathName; //Bail, we're done! break; } device_count++; } if(my_device_path == string.Empty) { Exception devNotFound = new Exception(@"Device could not be found."); throw(devNotFound); } return new UsbStickReceiver(my_device_count, my_device_path); }
这是一个具有代表性的抽象示例。其方法名称是“FindReceiver”,它不接受任何参数。但是,它会将一个 UsbStickReceiver 返回给调用方 KeyFobReceiver,并且隐藏了很多操作。在这个示例中,我们调用的方法都是 UsbSharp 类上的托管方法,而这个类又隐藏了所有非托管 Win32 函数。每个类都有自己的职责,并且只完成自己那部分职责。Bryan 完成了这些出色的工作,为此我们非常感谢他。
另一个有趣的事是,人机接口设备 (HID) 能够使用文件句柄为我们提供数据的访问权限,因此,UsbStickReceiver 接下来将接收上述代码中检索的 devicePath,并执行语句:
resultb = myUsb.CT_CreateFile(devicePath);
然后,接收结果文件句柄 HidHandle 并执行以下语句
fs = new FileStream(new Microsoft.Win32.SafeHandles.SafeFileHandle((IntPtr)myUsb.HidHandle, false), FileAccess.Read, 5, true);
以获取数据。确保阅读代码,并且能够随时参考类关系图。这真是有趣的事!
另一件有趣的小事情是:在 Bryan 和我(以及使用过该应用程序最初版本的人)测试这个应用程序时,我们发现在将 USB 接收器插入 USB 集线器时,某些用户的系统无法找到它。这些用户必须反复插拔接收器,直到系统找到它。于是,我们逐句审查了 UsbStickReceiver.cs 中的代码,并将每个方法调用 与 MSDN 文档进行了比较,这才发现我们向其中一个包装的 Win32 方法传递了一个不正确的参数。
//Wrong. We were passing in the previous device's resultb value, which caused random and unpredictable weirdness. int resultb = myUsb.CT_SetupDiGetDeviceInterfaceDetail(ref requiredSize, resultb); //Right. Odd as it may seem, the MSDN documentation explicitly says to pass in "0" for the second parameter.Whatever, dude. It makes the whole thing work! int resultb = myUsb.CT_SetupDiGetDeviceInterfaceDetail(ref requiredSize, 0);
这类 Bug 真的很难发现并修复,因为它确实可以运行。大多数情况下,它可以运行,因此我们通常会将它视为一个硬件问题。它很难调试,但是很有价值,因为现在我们能够可靠地找到 USB 接收器了。
到目前为止,我们完成了 PresenceManager。接下来,我们看一下 SettingsManager。由于一台计算机可能有不止一个人在使用,并且每个人都可能有自己的 key fob,因此我们希望每个用户都有自己的用户特定设置。我们希望获得特定于用户和应用程序的路径。我们还希望创建一个目录和合理的默认设置文件(如果需要)。
//This constructor is private because SettingsManager is accessed via a Factory private SettingsManager() { doc = new XmlDocument(); string appDirPath = Path.Combine( System.Environment.GetFolderPath(System.Environment.SpecialFolder.ApplicationData), "Usb Wireless Security"); configFilePath = Path.Combine(appDirPath, "SettingsV2.xml"); CreateIfNeeded(appDirPath, configFilePath); doc.Load(configFilePath); } protected void CreateIfNeeded(string directory, string file) { const string DEFAULT_SETTING_FILE = @"<?xml version=""1.0"" encoding=""utf-8"" ?> <Settings><KeyFobs/> <PresenceWindow>5</PresenceWindow> <OverridePassword /><DisabledPlugins/></Settings>"; DirectoryInfo di = new DirectoryInfo(directory); if(!di.Exists) { di.Create(); } FileInfo fi = new FileInfo(file); if(!fi.Exists) { FileStream fs = fi.Create(); StreamWriter sr = new StreamWriter(fs); sr.Write(DEFAULT_SETTING_FILE); sr.Close(); } }
NET 2.0 内置有很多新的设置功能,但就我们的需要而言,将一个简单的 XML 文件加载到 XmlDocument 中很简单,并且只需几行代码。每行代码对应于一个用户设置。避开使用这段代码的最重要的一件事是,您的应用程序必须遵循“最少反常原则”进行操作。这意味着,它不应该或不需要做使用户感到意外的事情。它应该只做该做的事,即使缺少一些资源,也应该完成。在我们的示例中,用户假定他们拥有自己的设置是合理的,因此我们将这些设置放在 C:\documents and settings\\Application Data 文件夹中,我们将通过以下语句检索该文件夹。 System.Environment.GetFolderPath(System.Environment.SpecialFolder.ApplicationData)
如果这个文件夹不存在,我们将创建一个文件夹;如果包含合理默认值的设置文件丢失,我们将创建一个新的设置文件。就是这样的小事情会使您的用户感到愉悦,并使您降低客户打来支持电话的几率。
创建应用程序很有趣,但是扩展应用程序才是最有趣的。本文的应用程序必须进行扩展。我们的应用程序通过 PresenceManager 侦听 key fob 的活动,当 PresenceManager 从 USB 接收器检测到活动时将发送一个事件。现在,我们希望应用程序向任何一个要进行侦听的人(也就是我们所有人)通告这些“存在事件”。
public void HandlePresenceNotification(PresenceNotificationEventArgs e) { foreach(Plugin plugin in Plugins) { if(plugin.Enabled) { plugin.Worker.HandlePresenceNotification(e); } } }
为创建插件,我们在 Visual Studio 内创建了一个新项目。这次,我们要使用 Visual Basic。我们创建这样一个插件,即,它在每次接收到一条 KeyFob 消息时,会将一条消息写入 Windows 事件日志。这对审核和调试都非常有用。它还记录了其他任何使用 KeyFob 的用户,这些人可能都用过我的机器。
每个插件都包含一个对 UsbSecurity.Core 程序集的引用。请注意,即使核心程序集是用 C# 编写的,它仍然能够为任何 .NET 语言(例如 VB)所使用。UsbSecurity.Core 程序集包含一个名为 PresencePluginBase 的基类,插件都必须从这个基类派生。如果看一下对象浏览器中的 PresencePluginBase,我们将发现它为我们提供了许多可利用的虚方法,例如 HandlePresenceNotification 和 WorkstationLocked。
我们先添加一个对 UsbWireless.Core 的引用,然后再从 PresensePluginBase 派生自己的类。我们还要导入 System.Diagnostics 命名空间,以便能够从 BCL 使用 EventLog 类。
Imports System Imports UsbWirelessSecurity Imports System.Text Imports System.Diagnostics Namespace DefaultPlugins 'When the host exe finds us, point them to our Configurator! <PRESENCEPLUGINCONFIGURATOR("Event Logging Plugin")> _ Class EventLoggerPlugin Inherits PresencePluginBase End Class End Namespace
除了用于生成插件的基类,我们还需要将一个自定义属性放到每个插件类中。由于每个插件都在应用程序 UI 的一个 ListBox 中出现,因此我们需要知道开发人员希望显示哪个插件。自定义属性是一种很容易的方法,插件开发人员可以使用它在代码中添加一个“通告备注”,以使我们了解更多信息。请注意上述 VB 代码中的 属性。
接下来,我们将在 PresencePluginBase 中重写每个虚方法,并将每条消息的细节记录在 EventLog 中。
Imports System Imports UsbWirelessSecurity Imports System.Text Imports System.Runtime.InteropServices Namespace DefaultPlugins 'When the host exe finds us, point them to our Configurator! <PRESENCEPLUGINCONFIGURATOR("Event Logging Plugin")> _ Class EventLoggerPlugin Inherits PresencePluginBase Dim logName As String = "USB Wireless Security" Public Overrides Sub HandleMessage(ByVal m As UsbWirelessSecurity.KeyFobMessage) MyBase.HandleMessage(m) If (Not m.MessageType = KeyFobMessageType.Heartbeat) Then Using aLog As New EventLog(logName) aLog.Source = logName aLog.WriteEntry( _ String.Format("Message Received: Device {0} reports {1}.", _ m.SerialNumber, m.MessageType.ToString())) End Using End If End Sub Public Overrides Sub WorkstationLocked() MyBase.WorkstationLocked() Using aLog As New EventLog(logName) aLog.Source = logName aLog.WriteEntry("Workstation Locked") End Using End Sub Public Overrides Sub WorkstationUnlocked() MyBase.WorkstationUnlocked() Using aLog As New EventLog(logName) aLog.Source = logName aLog.WriteEntry("Workstation Unlocked") End Using End Sub Public Overrides Sub HandlePresenceNotification(ByVal e As UsbWirelessSecurity.PresenceNotificationEventArgs) MyBase.HandlePresenceNotification(e) If (Not e.NotificationType = PresenceNotificationType.Heartbeat) Then Using aLog As New EventLog(logName) aLog.Source = logName aLog.WriteEntry( _ String.Format("Presence Received: Device {0} reports {1}.", _ e.KeyFob.SerialNumber, e.NotificationType.ToString())) End Using End If End Sub End Class End Namespace
为了阻止每过 250ms 就在 EventLog 中填入一条消息,我们将忽略这些消息。
通过使用简洁的硬件抽象层和插件体系结构,您能够让了解代码的用户以新功能来扩展您的应用程序。以下是我们在扩展 USB 无线安全应用程序时忽略的一些想法。我和 Bryan 希望大家能够应对挑战,并开始使用 Visual Studio 开发这种有用的小型设备。
• | 在设备插入或拔出时发送一封电子邮件或一条 SMS |
• | 启动默认的屏幕保护程序 |
• | 保存所有文件 |
• | 启动 Defragmenting 或 Drive Cleanup |
• | 当您离开时,将 Skype 设置为“Away” |
• | 停止播放所有音乐应用程序 |
• | 创建一个集中式 Web 服务,每个系统在发现 key fob 时都会调用这个 Web 服务来创建一个公司范围的“存在通知服务”。 |
尽量在现有项目中进行扩展,并记住不要畏惧“需要一些程序集!”这样的话。
非常感谢 Bryan Batchelder 提出的独到见解,为了使 USB key fob 可以在 .NET 中应用,他在硬件抽象层方面作出了令人赞叹的贡献!
Scott Hanselman 是 Corillian Corporation 的首席架构师,该公司是一个 eFinance 启动商。他在使用 C、C++、VB 和 COM 开发软件方面有 12 年的经验,最近他致力于使用 VB.NET 和 C# 进行开发。Scott 为身为一位 Microsoft RD 和 MVP 而感到骄傲。他与 Bill Evjen 等人合著了一本有关 ASP.NET 2.0 的新书,这本书已于 2005 年末出版。今年,他在 Orlando 举办的 TechEd 2005 大会上发表了有关代码生成和软件工厂的演讲。在他的网络日记 (http://www.computerzen.com) 中,您可以找到他对于 .NET 内涵、编程和 Web 服务的看法。
Bryan Batchelder 是 PatchAdvisor, Inc. 研发中心的主任,该公司是一个安全漏洞智能提供商,也是一家提供安全评估服务的公司。他还是 Greenline Systems 的架构师兼首席开发人员,该公司是一个供应链风险管理解决方案提供商,它与 DHS 合作,以共同保障进入美国的所有货物的安全。他在使用 C++、Java、ColdFusion 和 ASP 开发软件方面有 8 年的经验,最近他专门从事 .NET 和 C# 开发。在他的网络日记 (http://labs.patchadvisor.com/blogs/bryan) 中,您可以找到他对计算机安全、风险管理和软件开发的看法。