In questo post parleremo in modo particolare di come
fare delle query verso il mondo Windows Management Instrumentation (WMI). Tramite
WMI, possiamo infatti accedere dal mondo managed di .NET a tutta una serie di
informazioni gestite da Win32, alcune delle quali particolarmente interessanti,
specialmente se si lavora un po' più a basso livello rispetto a quanto facciamo
di solito. Tanto per citarne qualcosa, è possibile eseguire delle query
tipo:
SELECT * FROM Win32_NTLogEvent -- Log degli eventi
SELECT * FROM Win32_BIOS -- Info sul BIOS
SELECT * FROM Win32_NetworkAdapter -- Info sulle schede di rete
SELECT * FROM Win32_LogicalDisk -- Info sui dischi
SELECT * FROM Win32_Product -- Info sui software installati
-- con Windows Installer
Il linguaggio utilizzato è un SQL-style, utilizza la clausola SELECT * FROM
identifer, ed eventualmente
possiamo aggiungere una clausola WHERE per filtrare determinati contenuti. MSDN riporta la struttura delle classi esposte
da Win32, giusto per capire, per esempio, a cosa possiamo
accedere (share di rete, scheda madre, servizi di sistema, etc. etc.). Per esempio,
la classe Win32_Processor espone molte proprietà relative alla CPU installata
nel sistema. Questa pagina riporta maggiori dettagli.
.NET permette di accedere a queste informazioni con delle classi contenute
nel namespace System.Management. Nel codice che metterò a disposizione
ho fatto uso delle classi ManagementObjectSearcher, ObjectQuery, ManagementObject e così via. Lo vedremo fra poco.
Siccome il reperimento di alcune informazioni potrebbe essere un po' lento, ho
fatto uso del componente BackgroundWorker per rendere asincrona tale operazione.
E mi sono divertito parecchio!
Come interrogare WMI ed ottenere quello che
vogliamo
Interrogare WMI significa semplicemente istanziare un
oggetto ManagementObjectSearcher, passando come parametro un ObjectQuery che
rappresenta ciò che vogliamo ottenere:
ObjectQuery qry = new ObjectQuery(query);
ManagementObjectSearcher src = new ManagementObjectSearcher(qry);
objects = src.Get();
foreach (ManagementObject obj in objects)
// gestisto ogni obj per popolare
// una ListBox
Nel codice qui sopra, la variabile query è una semplice
string come "SELECT * FROM Win32_DesktopMonitor", ad
esempio, o una qualsiasi riportate nel primo riquadro. Il metodo
Get() della classe ritorna un oggetto ManagementObjectCollection che contiene tutti gli
oggetti ritornati. Tale collection può essere ovviamente ciclata con un banale
foreach. Ogni elemento della collection è in realtà
un'istanza di ManagementObject che, per definizione, può contenere qualsiasi
cosa, dipendentemente dalla query che abbiamo eseguito (dal log degli eventi,
alla struttura delle partizioni di avvio del sistema, dal BIOS alla porta
seriale del PC): per questo motivo, la classe ManagementObject espone tutta
una serie di metodi (primo fra tutti GetPropertyValue) che ci permette di
ottenere ciò che vogliamo. GetPropertyValue ritorna in ogni caso un object, che
dobbiamo castare al tipo corretto.
Qui sotto riporto uno screenshot della piccola applicazione che ho
scritto:
Ho creato una ComboBox che contiene alcune query WMI carine, una prima
ListBox che elenca gli oggetti recuperati dalla query, ed una seconda ListBox
che contiene le proprietà di ogni singolo oggetto che selezioniamo dal primo
elenco. Per ottenere l'elenco delle proprietà, ho utilizzato la proprietà
Properties di ogni
singolo ManagementObject, che ritorna un'istanza di PropertyDataCollection
.
Siccome l'operazione può essere un po' lunga...lo facciamo
asincrono?
A seconda della query, il click su Button Refresh della
mia WF può impiegare un po' di tempo, bloccando la UI e di certo questa non è
una cosa bella. Provate a fare una query con "SELECT * FROM Win32_Product" e
capirete subito cosa intendo.
Ho quindi utilizzato il componente BackgroundWorker per rendere
asincrona l'esecuzione della query ed il popolamento della prima ListBox. Non
voglio scendere troppo nel dettaglio, però voglio comunque fare il punto su cosa
ho fatto per implementare questa cosa:
- ovviamente, ho aggiunto un BackgroundWorker sulla mia WF
- nel costruttore della WF, chiamo un mio metodo privato
InitializeBackgroundWorker che associa 3 handler a 3 eventi del componente (DoWork, ProgressChanged e RunWorkerCompleted)
- Imposto la proprietà WorkerReportsProgress = true
- i tre handler sono, rispettivamente, i metodi Comincia,
Durante e AllaFine della mia WF (ho dato
nomi esplicativi perchè stavo impazzendo )
- sul click del Button, chiamo il metodo RunWorkerAsync()
Quando clicco sul Button, il BackgroundWorker comincia a fare il suo lavoro.
Notare che all'interno della function Comincia, faccio una cosa
particolare: imposto in e.Result il risultato dell'esecuzione di
executeQuery, che è la function che interroga WMI
e con un loop foreach gestisce i ManagementObject
trovati in base alla query. All'interno di executeQuery chiamo il metodo
ReportProgress che non fa nient'altro che aggiornare la UI dal suo stesso
thread, evitando tutte le complicazioni che ben conosciamo. Attenzione: questa operazione fallisce
sistematicamente se non impostiamo WorkerReportsProgress =
true, come indicato nel punto
(3).
C'è una cosa che voglio sottolineare, perchè riveste
molta importanza: il thread secondario in cui gira il BackgroundWorker comunica
al thread della UI cosa mostrare sulla WF stessa. Notate infatti la chiamata a
ReportProgress all'interno di executeQuery: questa
funzione gira nel thread secondario, non può di conseguenza aggiornare la UI (provate a
scrivere qualcosa sulla WF in questo punto e vedrete). Quindi, come
facciamo a fare un refresh e ad informare l'utente di quello che sta
accadendo? Basta fare una cosa molto semplice: utilizzare il secondo parametro (che è un
semplice object) per comunicare al thread della UI cosa
mostrare. Nel mio esempio, passo semplicemente:
worker.ReportProgress(100, obj.GetText(TextFormat.Mof));
Sinceramente, non sono stato a chiedermi cosa sia quell'enum TextFormat: mi
basta sapere che dal thread secondario al thread della UI passo una string che
non è nient'altro che una rappresentazione dell'oggetto che ho reperito. Tale
chiamata scatena l'esecuzione del mio metodo Durante che,
banalmente, reperisce il valore che gli è arrivato e lo aggiunge alla
ListBox:
void Durante(object sender, ProgressChangedEventArgs e)
{
this.Text = "In esecuzione...";
lstItems.Items.Add(e.UserState as string);
// altre operazioni
}
Ovviamente, l'oggetto passato da un thread all'altro
può essere qualsiasi cosa.
Il codice, se vi interessa, è scaricabile
da qui. Contiene un po' tutto quello che ho scritto sinora: le classi su
Storage-Shelf-Book, la serializzazione, il custom visualizer, il process viewer
e, oggi, tutto questo discorso delle query WMI. Non è molto user-friendly,
nel senso che per vedere il funzionamento di una cosa piuttosto che di un'altra,
è necessario andare manualmente nel Main() e decidere quale WF aprire. Non ho badato molto a questo aspetto,
tanto...voglio dire...è tutto a scopo didattico. La cosa interessante è aprire
il codice di ogni WF e tirar fuori quello che serve.
Forse, se troverò qualche spunto interessante, farò un altro post su
questo tema, perchè le classi offerte da System.Management sono parecchie ed alcune hanno
attirato la mia attenzione.