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 ManagementEventWatcher

La 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 Class

Questa 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 Sub

Il 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 Sub

Si 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 Function

Prima 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 Sub

In 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 Region

Nel 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 Sub

Nel 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 Sub

In 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 Sub

Conclusione
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.