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 進行的互動和一些抽象
在淺綠色 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 devicesint 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 GUIDmyUsb.CT_HidGuid();//Let the system know we're looking for active devicesmyUsb.CT_SetupDiGetClassDevs();//Get ready...int result = -1;int device_count = 0;int size = 0;int requiredSize = 0;//While nothing goes wrongwhile(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 deviceint resultb = myUsb.CT_SetupDiGetDeviceInterfaceDetail(ref requiredSize, 0);//Cool, store thatsize = requiredSize;//Gimme the info you've gotresultb = 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);
以擷取資料。確保閱讀代碼,並且能夠隨時參考類別關係圖。這真是有趣的事!
返回頁首
消滅 Bug、閱讀手冊
另一件有趣的小事情是:在 Bryan 和我(以及使用過該應用程式最初版本的人)測試這個應用程式時,我們發現在將 USB 接收器插入 USB 集線器時,某些使用者的系統無法找到它。這些使用者必須反覆插拔接收器,直到系統找到它。於是,我們逐句審查了 UsbStickReceiver.cs 中的代碼,並將每個方法調用 與 MSDN 文檔進行了比較,這才發現我們向其中一個封裝的 Win32 方法傳遞了一個不正確的參數。
//Wrong. We were passing in the previous device's resultb value, which caused random andunpredictable 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 thesecond 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 Factoryprivate 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 SystemImports UsbWirelessSecurityImports System.TextImports System.DiagnosticsNamespace DefaultPlugins'When the host exe finds us, point them to our Configurator!<PRESENCEPLUGINCONFIGURATOR("Event Logging Plugin")> _Class EventLoggerPluginInherits PresencePluginBaseEnd ClassEnd Namespace
除了用於產生外掛程式的基類,我們還需要將一個自訂屬性放到每個外掛程式類中。由於每個外掛程式都在應用程式 UI 的一個 ListBox 中出現,因此我們需要知道開發人員希望顯示哪個外掛程式。自訂屬性是一種很容易的方法,外掛程式開發人員可以使用它在代碼中添加一個“通告備忘”,以使我們瞭解更多資訊。請注意上述 VB 代碼中的 屬性。
接下來,我們將在 PresencePluginBase 中重寫每個虛方法,並將每條訊息的細節記錄在 EventLog 中。
Imports SystemImports UsbWirelessSecurityImports System.TextImports System.Runtime.InteropServicesNamespace DefaultPlugins'When the host exe finds us, point them to our Configurator!<PRESENCEPLUGINCONFIGURATOR("Event Logging Plugin")> _Class EventLoggerPluginInherits PresencePluginBaseDim logName As String = "USB Wireless Security"Public Overrides Sub HandleMessage(ByVal m As UsbWirelessSecurity.KeyFobMessage)MyBase.HandleMessage(m)If (Not m.MessageType = KeyFobMessageType.Heartbeat) ThenUsing aLog As New EventLog(logName)aLog.Source = logNameaLog.WriteEntry( _String.Format("Message Received: Device {0} reports {1}.", _m.SerialNumber, m.MessageType.ToString()))End UsingEnd IfEnd SubPublic Overrides Sub WorkstationLocked()MyBase.WorkstationLocked()Using aLog As New EventLog(logName)aLog.Source = logNameaLog.WriteEntry("Workstation Locked")End UsingEnd SubPublic Overrides Sub WorkstationUnlocked()MyBase.WorkstationUnlocked()Using aLog As New EventLog(logName)aLog.Source = logNameaLog.WriteEntry("Workstation Unlocked")End UsingEnd SubPublic Overrides Sub HandlePresenceNotification(ByVal eAs UsbWirelessSecurity.PresenceNotificationEventArgs)MyBase.HandlePresenceNotification(e)If (Not e.NotificationType = PresenceNotificationType.Heartbeat) ThenUsing aLog As New EventLog(logName)aLog.Source = logNameaLog.WriteEntry( _String.Format("Presence Received: Device {0} reports {1}.", _e.KeyFob.SerialNumber, e.NotificationType.ToString()))End UsingEnd IfEnd SubEnd ClassEnd 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) 中,您可以找到他對電腦安全、風險管理和軟體開發的看法。
轉到原英文頁面