Intercettare inserimento o rimozione di una unità USB
a cura di Diego Cattaruzza (requisiti: nessuno)Premessa
Nei giorni scorsi sono venuto a conoscenza di una notevole possibilità offerta da .Net: si può monitorare l'inserimento e la rimozione di una chiavetta USB.Credo che, se ne avessi avuto bisogno, l'avrei saputo scoprire da solo, ma stavolta mi è stata risparmiata un po' di fatica, perché ho avuto l'indicazione precisa di dove andare a cercare la soluzione: le classi di sistema di WMI.
Partendo da quella pagina, ho scoperto che .Net offre la possibilità di usare queste classi attraverso le sue classi esposte nel namespace System.Management. Indagando ulteriormente 'nei dintorni', ho acquisito le informazioni necessarie a sviluppare la classe che vi presento in questo articolo.
L'esplorazione di MSDN intorno a WMI
WMI (Windows Management Instrumentation) espone molte classi ed eventi utili per ottenere dati o compiere particolari operazioni che convolgano aspetti di gestione di Windows.In particolare, espone, tra le classi di sistema (WMI System Classes) gli eventi __InstanceCreationEvent e __InstanceDeletionEvent - prima indicazione ricevuta e punto di partenza dell'esplorazione.
Dalla pagina Using WMI leggo l'informazione che per ottenere informazioni attraverso WMI posso usare classi esposte nel namespace System.Management. Dalla pagina WMI .NET Overview vengo indirizzato alla pagina WMI .NET Code Directory. Da qui, attraverso la consultazione di How To: Receive an Event, sono arrivato all'esame della classe ManagementEventWatcher.
Uno dei costruttori esposti da questa classe è quello che serve a me:
Dim instance As New ManagementEventWatcher(eventQuery).In particolare, scopro che l'argomento eventQuery può essere un generico oggetto EventQuery, ma anche un più specifico oggetto WqlEventQuery, che però richiede la conoscenza del particolare linguaggio WQL per WMI, che è un dialetto di SQL.
Non è che si debba per forza scrivere una espressione di query (anche se si potrebbe), ma bisogna sapere 'cosa' cercare, e 'dove' e 'come'. Tenendo conto dell'esempio mostrato nella pagina relativa alla proprietà QueryString, infatti, ho cercato di individuare gli elementi esposti da WMI che mi servivano.
L'esplorazione è così continuata, attraverso le WMI Reference, le WMI Classes, le Win32 classes, fino a trovare il nome Win32_LogicalDisk per il tipo di oggetti da indagare (campo TargetInstance della classe di evento).
Sempre dalla pagina WMI .NET Code Directory, ho consultato, via via, How To: Retrieve Collections of Managed Objects, How To: Retrieve Information About Managed Objects by Queries, ManagementObjectSearcher, ManagementObjectCollection e ovviamente ManagementObject.
Ho così pian piano acquisito tutte le conoscenze necessarie per provare a implementare una classe che intercetti inserimento e rimozione di una unità USB.
La classe LogicalDiskEventWatcher
Questa classe deve istanziare due osservatori per monitorare gli eventi di inserimento e rimozione di una unità; deve restare in attesa della notifica di questi eventi; deve esporre un evento specifico per notificare all'esterno l'evento avvenuto, con informazioni su di esso; deve essere in grado di liberare le risorse impegnate (le WMI sono una tecnologia COM, anche se sono gestite attraverso classi .Net; è quindi opportuno forzarne la finalizzazione).
Ecco quindi le prime righe di codice:Imports System.Management Public Class LogicalDiskEventWatcher Implements IDisposable Public Event LogicalDiskEventArrived(ByVal sender As Object, _ ByVal e As LogicalDiskEventArrivedEventArgs) Private WithEvents mCreationWatcher As ManagementEventWatcher Private WithEvents mDeletionWatcher As ManagementEventWatcherLa direttiva Imports permette di abbreviare il codice relativo alle classi esposte nel namespace System.Management, debitamente incluso nei riferimenti del progetto.
La classe implementa l'interfaccia IDisposable per i compiti di finalizzazione ed espone l'evento di notifica, per il quale conviene preparare una apposita classe di tipo EventArgs:Public Class LogicalDiskEventArrivedEventArgs Inherits EventArgs Private mUnit As String Public Property LogicalUnit() As String Get Return mUnit End Get Set(ByVal value As String) mUnit = value End Set End Property Private mInserted As Boolean Public Property Inserted() As Boolean Get Return mInserted End Get Set(ByVal value As Boolean) mInserted = value End Set End Property Public Sub New(ByVal unit As String, ByVal inserted As Boolean) Me.LogicalUnit = unit Me.Inserted = inserted End Sub End ClassQuesta classe espone l'unità logica interessata dall'evento, il tipo di evento (inserimento o rimozione) e un costruttore per impostare agevolmente queste proprietà.
Gli oggetti destinati al monitoraggio degli eventi sono dichiarati WithEvents, per avere notifica dell'evento EventArrived della classe ManagementEventWatcher.
Nel costruttore della classe vengono istanziati e avviati i due osservatori:
Public Sub New() Try mCreationWatcher = New ManagementEventWatcher(New WqlEventQuery( _ "__InstanceCreationEvent", _ New TimeSpan(0, 0, 1), _ "TargetInstance isa ""Win32_LogicalDisk""")) mCreationWatcher.Start() mDeletionWatcher = New ManagementEventWatcher(New WqlEventQuery( _ "__InstanceDeletionEvent", _ New TimeSpan(0, 0, 1), _ "TargetInstance isa ""Win32_LogicalDisk""")) mDeletionWatcher.Start()Agli osservatori vengono passati due oggetti WqlEventQuery, istanziati passando il nome della classe di evento, l'intervallo di tempo tra una indagine e l'altra (un secondo), la condizione che filtra gli eventi della classe indicata. La QueryString, tradotta, suonerebbe all'incirca come "vedi se è avvenuto un evento di creazione di istanza (inserimento) o di eliminazione di istanza (rimozione) che riguardi una istanza di unità logica di disco di Windows e fai questa verifica ogni secondo".
Finally CompareDriveList() End Try End SubIl costruttore termina con la preparazione della lista delle unità logiche, per confrontarla in seguito agli eventi monitorati e dedurre qual è l'unità interessata. Questa lista costituisce una variabile statica nel metodo chiamato:
Private Sub CompareDriveList() Static oldDriveList As String Dim moSearcher As Management.ManagementObjectSearcher = _ New Management.ManagementObjectSearcher("Select Name from Win32_LogicalDisk") Dim moCollection As Management.ManagementObjectCollection = moSearcher.Get Dim newDriveList As String = String.Empty For Each mo As Management.ManagementObject In moCollection newDriveList += mo("Name").ToString Next If oldDriveList Is Nothing Then ' do nothing Else RaiseEvent LogicalDiskEventArrived(Me, New LogicalDiskEventArrivedEventArgs( _ GetMissingLetter(oldDriveList, newDriveList), _ oldDriveList.Length < newDriveList.Length)) End If oldDriveList = newDriveList End SubSi istanzia un oggetto ManagementObjectSearcher, che cerchi i nomi delle unità logiche (le lettere). Da esso si ottiene (Get) l'insieme ManagementObjectCollection degli oggetti trovati, che viene poi scandito per formare la 'nuova' lista (newDriveList) delle unità (una stringa simile a "A:B:C:D:...eccetera:").
La prima volta che viene eseguito CompareDriveList, la variabile statica oldDriveList non è inizializzata e si sfrutta questa circostanza per evitare di scatenare l'evento e saltare direttamente alla sua valorizzazione con la newDriveList appena costruita.
Le volte successive, cioè in occasione degli eventi di inserimento o rimozione, viene scatenato l'evento LogicalDiskEventArrived, passando una nuova istanza di argomenti LogicalDiskEventArrivedEventArgs: l'unità logica coinvolta e il tipo di evento. Quest'ultimo è indicato con il risultato di un confronto tra le lunghezze delle due stringhe-liste-di-unità: se quella nuova è più lunga, è stata inserita un'unità, quindi IsInserted è True (ovviamente è False altrimenti).
La lettera dell'unità coinvolta viene estratta tramite il metodo GetMissingLetter:Private Function GetMissingLetter(ByVal oldLetters As String, _ ByVal newLetters As String) As String Dim shorter, longer As String If oldLetters.Length > newLetters.Length Then shorter = newLetters longer = oldLetters Else shorter = oldLetters longer = newLetters End If Dim units As String() = longer.Split(":"c) Dim u As String = String.Empty For s As Integer = 0 To units.Count - 2 u = units(s) If u <> String.Empty AndAlso Not shorter.Contains(u) Then Exit For Next Return u End FunctionPrima si deifniscono la stringa più corta e quella più lunga, quindi si ottiene il vettore delle unità descritte nella stringa più lunga, infine si scandisce questo vettore verificando quale di queste manca nella stringa più corta. Il limite superiore del ciclo è ridotto perché l'ultimo elemento è una stringa vuota.
Come s'era detto, si sono istanziati WithEvents gli osservatori perché la classe ManagementEventWatcher espone l'evento EventArrived, per il quale ho implementato il gestore:
Private Sub w_EventArrived(ByVal sender As Object, _ ByVal e As System.Management.EventArrivedEventArgs) _ Handles mCreationWatcher.EventArrived, mDeletionWatcher.EventArrived CompareDriveList() End SubIn esso ci si limita a richiamare il metodo CompareDriveList, già illustrato, dal quale si scatena l'evento di notifica all'esterno.
Come s'è prefigurato all'inizio della descrizione di questa classe, essa implementa l'interfaccia IDisposable, in ragione della quale l'IDE ha preparato lo scheletro per due metodi Dispose e il campo disposedValue:
#Region " IDisposable Support " Private disposedValue As Boolean = False ' To detect redundant calls ' IDisposable Protected Overridable Sub Dispose(ByVal disposing As Boolean) If Not Me.disposedValue Then If disposing Then Try mCreationWatcher.Stop() mDeletionWatcher.Stop() mCreationWatcher.Dispose() mDeletionWatcher.Dispose() Catch ex As Exception Throw End Try End If End If Me.disposedValue = True End Sub ' This code added by Visual Basic to correctly implement the disposable pattern. Public Sub Dispose() Implements IDisposable.Dispose ' Do not change this code. Put cleanup code in Dispose(ByVal disposing As Boolean) above. Dispose(True) GC.SuppressFinalize(Me) End Sub #End RegionNel metodo Dispose con parametro si è scritto il codice di arresto e di finalizzazione dei due ManagementEventWatcher.
Il codice di test
Per collaudare la classe, ho disegnato una semplice form con due pulsanti, GO e STOP.Private watcher As LogicalDiskEventWatcher Private Sub GObtn_Click(ByVal sender As System.Object, _ ByVal e As System.EventArgs) Handles GObtn.Click watcher = New LogicalDiskEventWatcher AddHandler watcher.LogicalDiskEventArrived, AddressOf LogicalDiskEventHandler End SubNel primo istanzio nel campo watcher (dichiarato a livello di classe) un oggetto LogicalDiskEventWatcher e aggiungo alla gestione del suo evento LogicalDiskEventArrived il gestore LogicalDiskEventHandler appositamente implementato:
Private Sub LogicalDiskEventHandler(ByVal sender As Object, _ ByVal e As LogicalDiskEventArrivedEventArgs) MessageBox.Show(String.Format("Logical Disk {0}: {1}", _ e.LogicalUnit, _ If(e.Inserted, "inserted", "removed")), _ Me.Text) End SubIn esso si espongono tramite una MessageBox le informazioni fornite dai LogicalDiskEventArrivedEventArgs dell'evento gestito: l'unità logica e il tipo di evento. Chiaramente, questo è solo un esempio. In una applicazione reale, si potrebbe dover attendere che l'utente inserisca una chiavetta USB quando invitato dal programma oppure si vorrebbe essere avvisati nel caso venga rimossa prima del tempo.
Nel gestore dell'evento Click del pulsante di stop, nonché in quello di chiusura della form, caso mai l'utente si dimenticasse di usarlo, anniento l'osservatore:
Private Sub StopBtn_Click(ByVal sender As System.Object, _ ByVal e As System.EventArgs) Handles StopBtn.Click watcher.Dispose() End Sub Private Sub Form1_FormClosing(ByVal sender As Object, _ ByVal e As System.Windows.Forms.FormClosingEventArgs) _ Handles Me.FormClosing watcher.Dispose() End SubConclusione
In questo articolo è stato illustrato un esempio di uso di classi di sistema offerte da WMI (Windows Management Instrumentation) attraverso oggetti forniti dal namespace System.Management, per ricevere notifica dell'inserimento o della rimozione di una chiavetta USB.
Per l'occasione, si è data nota della lunga esplorazione in MSDN per acquisire le conoscenze necessarie.Come al solito, il codice correlato a questo articolo è esposto in area download.