Standardisierter Zugriff auf Systeminformationen und Managementmethoden, Teil 2
Im ersten Teil wurde WMI als eine standardisierte Möglichkeit vorgestellt, auf unterschiedlichste Informationen unter Windows zuzugreifen. Als Programmierer müssen Sie sich nicht mehr mit unterschiedlichen APIs und Klassenbibliotheken herumschlagen. Stattdessen verwenden Sie zum Auslesen immer denselben Weg. Nun kann man über WMI nicht nur Informationen abfragen, sondern auch auf Systemereignisse reagieren, andere Anwendungen starten oder mit den Methoden und Daten eines Objekts arbeiten. Genau dies soll Gegenstand dieses Teils sein.
Rückblick
WMI (Windows Management Instrumentation) ist ein API, das eine standardisierte Schnittstelle zu unterschiedlichsten Windows-Funktionen und Datenlieferanten herstellt. WMI steht standardmäßig unter Windows XP, 2000 und Me zur Verfügung. Für Windows NT oder Windows 9x kann es nachinstalliert werden. Sie können WMI in Delphi verfügbar machen, indem Sie die Typbibliothek Microsoft WMI Scripting V1.2 Library (Version 1.2) einbinden. Markieren Sie dazu die Option Komponenten-Wrapper generieren
und danach den Button Unit anlegen. Die Versionsnummer der Bibliothek kann sich bei Ihnen unterscheiden. Die Beispiele im Artikel wurden mit Delphi 7 unter Windows XP erzeugt. Sie liegen aber auf CD ebenfalls für Delphi 5 vor. Die Verwendung mit anderen Delphi-Versionen ist meist durch einfache Änderungen möglich. So wird z.B. in Delphi 5 die Unit
Variants.pas nicht benötigt.
Namespaces, Klassen und Instanzen auflisten
Beim letzten Mal wurden in einem Beispiel Informationen zum Bios über die WMI-Klasse
Win32_Bios ausgegeben. Das folgende Beispiel soll noch einmal den Zugriff auf Namespaces, deren Klassen und wiederum den Instanzen einer Klasse beleuchten. Für eine solche Anwendung wird schon etwas mehr Sourcecode benötigt. Letzten Endes erhalten Sie aber eine Anwendung, die bereits so gut wie alle Informationen, die über WMI bereitgestellt werden, anzeigt (Abb. 1). Im linken Bereich werden nach dem Klicken auf den Button Start alle Namespaces aufgelistet. Rechts oben werden nach einem Doppelklick auf einen Namespace dessen Klassen und beim Doppelklick auf eine Klasse deren Instanzen rechts unten dargestellt.
Um alle Namespaces zu ermitteln, verbinden Sie sich jeweils mit einem Namespace und erhalten über den Objektpfad __Namespace (2 Unterstriche) alle seine untergeordneten Namespaces. Sie beginnen mit dem Namespace root und iterieren dann immer über die gefundenen untergeordneten Namespaces. In der Methode
InstancesOf im Listing 1 wird deshalb als Pfad __Namespace angegeben. Als einzige Eigenschaft wird der Name des Namespaces ausgewertet. Das Durchlaufen durch die ermittelten Instanzen gleicht dem gezeigten Beispiel zur Ermittlung der Bios-Eigenschaften. Die gefundenen Namespaces werden zuerst in eine String List
SortedNameList eingefügt, sortiert und dann in den Treeview links eingegliedert.

Abb. 1: WMI-Browser
Listing 1 FServices := FLocator.ConnectServer('.', NamespaceName, '',
'', '', '', wbemConnectFlagUseMaxWait, nil);
FObjectSet := FServices.InstancesOf('__Namespace', 0, nil);
FObjEnum := (FObjectSet._NewEnum) as IEnumVariant;
while(FObjEnum.Next(1, FWMITmpObj, Cnt) = S_OK) do
begin
FWMIObj := IUnknown(FWMITmpObj) as SWbemObject;
SortedNameList.Add(FWMIObj.Properties_.Item(
'Name', 0).Get_Value);
end;
Im nächsten Schritt wühlen wir uns durch die Klassen eines Namespaces. Dazu verbindet man sich wieder mit einem Namespace und ruft die Klassen eines Namespaces über die Methode
SubClassesOf des Interfaces
ISWbemServices ab. Alternativ kann man auch die Abfrage über WQL definieren und mit
ExecQuery ausführen. Um die Klassen zu ermitteln ist SELECT * FROM meta_class anzugeben. Über eine WHERE-Klausel kann auch nur eine bestimmte Klasse zurückgegeben werden, z.B. SELECT * FROM meta_class WHERE __Class = "__Win32Provider" (Listing 2).
Listing 2 FServices := FLocator.ConnectServer('.',
GetNamespaceName(tvNamespaces.Selected), '', '', '', '',
wbemConnectFlagUseMaxWait, nil);
FObjectSet := FServices.SubclassesOf('', 0, nil);
// FObjectSet := FServices.ExecQuery(
// 'SELECT * FROM meta_class', 'WQL', 0, nil);
FObjEnum := (FObjectSet._NewEnum) as IEnumVariant;
while(FObjEnum.Next(1, FWMITmpObj, Cnt) = S_OK) do
begin
FWMIObj := IUnknown(FWMITmpObj) as SWbemObject;
lbClasses.Items.Add(FWMIObj.Path_.Class_);
end;
Zum Abschluss werden beim Doppelklick auf eine Klasse deren Instanzen ermittelt. Damit der verwendete List View auch korrekte Spaltenüberschriften verwendet, müssen wir zusätzlich zur ausgewählten Klasse deren Eigenschaftsnamen herausbekommen. Die Instanzen der Klasse werden wieder über die Methode
InstancesOf ermittelt. Danach werden von
FWMIObj, das eine Instanz der Klasse
ISWbemObject enthält, dessen Eigenschaften über die Eigenschaft
Properties_, die vom Typ des Interfaces
ISWbemPropertySet ist, zurückgegeben. Diese werden nun über eine Schleife durchlaufen. Der Name einer Eigenschaft wird über das Interface
ISWbemProperty und der Eigenschaft
Name geholt. Die Eigenschaftsnamen werden im List View
lvInstances als Spaltenüberschriften hinzugefügt (Listing 3).
Listing 3 FPropSet := FWMIObj.Properties_;
FPropEnum := (FPropSet._NewEnum) as IEnumVariant;
lvInstances.Columns.BeginUpdate;
lvInstances.Columns.Clear;
while(FPropEnum.Next(1, FWMITmpProp, Cnt) = S_OK) do
begin
FProp := IUnknown(FWMITmpProp) as ISWbemProperty;
lc := lvInstances.Columns.Add;
lc.Caption := FProp.Name;
lc.Width := 100;
end;
lvInstances.Columns.EndUpdate;
Zu guter Letzt müssen wir noch die Eigenschaftswerte bestimmen. Doch hier gibt es einiges zu beachten. Eigenschaften können in WMI einfache Typen wie Zahlen oder Strings sein, aber auch Arrays oder Objekte. Dies muss beim Auslesen der Werte beachtet werden. Weiterhin können die Werte
null oder
leer sein. Delphi liefert aber schon die benötigten Funktionen mit, über die Sie den Typ und den Inhalt einer Eigenschaft bestimmen können (Tabelle 1). Sämtliche Funktionen finden Sie unter Delphi 7 in der Unit
Variants.pas. Als Parameter erwarten sie einen
Variant- bzw.
OLEVariant-Wert.
Im Beispiel wird nur zwischen Arrays und allen anderen Typen unterschieden. Die Werte eines Arrays werden über eine separate Methode bestimmt, alle anderen als String zurückgegeben. Dies führt dazu, dass es bei einigen Instanzen noch zu Fehlern bei der Ergebnisausgabe kommen kann.
Auf Ereignisse reagieren
Ändern sich Daten oder Zustände innerhalb von WMI, werden verschiedene Ereignisse ausgelöst, auf die eine Anwendung reagieren kann. So wird beispielsweise beim Erzeugen eines neuen Prozesses ein __InstanceCreationEvent vom entsprechenden WMI-Provider ausgelöst und es wird eine neue Instanz für die Klasse Win32_Process erzeugt. Der Eventprovider (Produzent) informiert intern den CIMOM und dieser leitet wiederum das Ereignis an die Konsumenten, z.B. eine Anwendung, weiter. Auf Ereignisse kann synchron oder asynchron gewartet werden. Während im ersten Fall eine Anwendung blockiert (man kann natürlich mit Threads arbeiten) arbeitet die zweite Variante wie die Ereignisbehandlung Delphis. Um sich als Konsument anzumelden, wird über WQL ein Eventfilter definiert. Dieser legt die Bedingungen fest, unter denen er an einer Information zu einem Ereignis interessiert ist. Der Aufbau eines Eventfilters besitzt einige zusätzliche WQL-Klauseln, über die ein Intervall festgelegt werden kann und eventuell noch eine Prüfung des Instanztyps durchgeführt wird.
Es gibt verschiedene Ereignistypen. Für Klassen, Instanzen und Namespaces existieren beispielsweise Ereignisse, die beim Erstellen, Löschen oder Ändern von Objekten ausgelöst werden. Die Namen sind entsprechend __ClassCreationEvent, __InstanceModificationEvent oder __NamespaceDeletionEvent (immer mit zwei Unterstrichen beginnend). Über die Klausel WITHIN wird ein Intervall in Sekunden angegeben, in dem man auf ein Ereignis reagieren möchte. Das Ereignis __InstanceCreationEvent, das beim Erzeugen eines Objekts einer beliebigen Klasse ausgelöst wird, besitzt drei Eigenschaften, wobei die Eigenschaft TargetInstance das erzeugte Objekt kennzeichnet. Die WQL-Abfrage wird in der Beispielanwendung über die Methode ExecNotificationQueryAsync erzeugt (Listing 4). Damit wartet man nicht, sondern der Aufruf kehrt sofort zurück. Wie erhält man nun eine Nachricht, wenn das betreffende Ereignis eingetreten ist? Dazu erzeugt man ein TSWbemSink-Objekt. Der Eigenschaft OnObjectReady wird dann eine Methode zugewiesen die aufgerufen wird, wenn das betreffende Ereignis eingetreten ist.
Listing 4 FServices := FLocator.ConnectServer('.', 'root\cimv2', '',
'', '', '', wbemConnectFlagUseMaxWait, nil);
FSink := TSWbemSink.Create(self);
FSink.OnObjectReady := ProcessCreated;
FServices.ExecNotificationQueryAsync(
FSink.DefaultInterface,
'SELECT * FROM __InstanceCreationEvent
WITHIN 0.1 WHERE TargetInstance ISA
"Win32_Process"', 'WQL', 0, nil, nil);
Der Methode, die beim Auftreten des Ereignisses aufgerufen wird, werden zwei Parameter übergeben, die in Zusammenhang mit dem Ereignis ausgewertet werden können (Listing 5). Dabei ist der Parameter vom Typ ISWbemObject hier interessant, da er die neu erzeugte Instanz der Klasse Win32_Process kennzeichnet. Auf die Eigenschaften der Instanz kann über den Eigenschaftsnamen zugegriffen werden. In diesem Fall sind dies die Eigenschaften eines Win32_Process-Objekts. Über die Methode GetObjectText können Sie einfach alle Informationen in Textform ausgeben. Dies ist zur Übersicht hilfreich, sollte aber natürlich nicht für Benutzerausgaben genutzt werden. Starten Sie zum Test die Anwendung und klicken Sie auf Start
. Starten Sie nun eine beliebige andere Anwendung. Dieser Start sollte ein Ereignis auslösen, da ein neuer Prozess und damit ein neues Objekt der Klasse Win32_Process erzeugt wird.
Listing 5 procedure TfrmMain.ProcessCreated(ASender: TObject; const objWbemObject: ISWbemObject; const objWbemAsyncContext: ISWbemNamedValueSet);
var
PropVal: OLEVariant;
begin
PropVal := objWbemObject;
mmEvents.Lines.Add('CommandLine: ' +
PropVal.TargetInstance.CommandLine);
mmEvents.Lines.Add('Description: ' +
PropVal.TargetInstance.Description);
mmEvents.Lines.Add('Name: ' +
PropVal.TargetInstance.Name);
mmEvents.Lines.Add('ProcessID: ' +
IntToStr(PropVal.TargetInstance.ProcessID));
ShowMessage(objWbemObject.GetObjectText_(1));
end;
Anwendungen starten
Einige WMI-Klassen besitzen Methoden, die von Ihnen aufgerufen werden können. Welche Klassen Methoden besitzen, erkennt man am besten daran, dass sich vor den Klassennamen in der Referenz ein Pluszeichen befindet (Abb. 2). Am Beispiel des Startens einer Anwendung mithilfe der Klasse Win32_Process soll der Aufruf einer Methode gezeigt werden. Das Starten einer Anwendung bedeutet, einen neuen Prozess zu erzeugen. Dazu wird ein Klassenobjekt der Klasse Win32_Process über die Methode Get des Interfaces ISWbemServices geholt. Danach wird für die Methode Create über die Auflistung Methods_ vom Typ ISWbemMethodSet ein ISWbemMethod-Objekt ermittelt. Als nächstes muss ein Eingabeparameterobjekt erzeugt werden, welches den aufzurufenden Prozess spezifiziert. Dazu wird über das Objekt InParameters und dem Aufruf der Methode SpawnInstance_ eine neue Instanz für ein Parameterobjekt erzeugt. Dem Parameterobjekt wird die Eigenschaft CommandLine hinzugefügt, die den Namen der zu startenden Anwendung zugewiesen bekommt. Zum Abschluss wird die Methode Create des WMI-Objekts über ExecMethod_ aufgerufen und die Eingabeparameter werden übergeben (Listing 6).

Abb. 2: WMI-Klassen-Referenz
Das ganze sieht wie üblich sehr kompliziert aus, und das ist es vielleicht auch das erste Mal. Die Aufrufe lassen sich aber auch gut in Prozeduren und Funktionen verpacken, da sie sich für andere Anwendungsgebiete nur in ein paar Parametern unterscheiden.
Listing 6 var
FProcess: ISWbemObject;
FMethod: ISWbemMethod;
FInParam: ISWbemObject;
FOutParam: ISWbemObject;
FProperty: ISWbemProperty;
FPropValue: OleVariant;
...
FServices.Security_.Set_ImpersonationLevel(
wbemImpersonationLevelImpersonate);
FProcess := FServices.Get('Win32_Process', 0, nil);
FMethod := FProcess.Methods_.Item('Create', 0);
FInParam := FMethod.InParameters.SpawnInstance_(0);
FProperty := FInParam.Properties_.Add(
'CommandLine', wbemCimtypeString, False, 0);
FPropValue := edtAppName.Text;
FProperty.Set_Value(FPropValue);
FOutParam := FProcess.ExecMethod_(
'Create', FInParam, 0, nil);
Daten eines Objekts bearbeiten
In der Klassenbeschreibung der WMI-Klassen finden Sie bei einigen Eigenschaften den Zugriffstyp (
AccessType)
Read/Write. Sie können also den Wert nicht nur lesen, sondern auch ändern. Da das Ändern von Werten ein Sicherheitsrisiko darstellt (wie übrigens auch das Ausführen von Methoden), müssen Sie den Impersonation Level setzen. Dieser legt fest, unter welchem Benutzer Sie sich bei WMI identifizieren (vgl. auch Tabelle 2). Da WMI als Service läuft, hat es die volle Zugriffskontrolle auf das System. Bei jedem Verbindungsaufbau zu WMI muss daher eine gültige Benutzerkennung verwendet werden.
Nach dem Setzen des ImpersonationLevels holen Sie ein Klassenobjekt der gewünschten Klasse über die Methode Get und erzeugen eine neue Instanz FWMIObj. Dieser Instanz werden dann beispielsweise neue Eigenschaften hinzugefügt. In diesem Beispiel soll eine neue Umgebungsvariable definiert werden. Dazu wurde auf dem beschriebenen Weg ein Win32_Environment-Objekt erzeugt. Um eine Systemvariable zu setzen, wird der Eigenschaft UserName der Wert
zugewiesen. Eine Benutzervariable wird durch den Host- und Benutzernamen, z.B. MeinPC\Frischa, definiert. Da es bei anderen Eigenschaften durchaus auch nicht ganz trivial ist, wie der Wert gesetzt wird, sollte man zuerst die bereits vorhandenen Instanzen durchstöbern. Deren Werte sind dann für eigene Kreationen sehr aufschlussreich. Dazu können Sie z.B. den WMI-Browser aus dem ersten Beispiel nutzen. Nach dem Ablegen des Wertes in einer Variablen vom Typ OLEVariant, kann sie über die Methode Set_Value gesetzt werden. Zum Abschluss verwenden Sie die Methode put_ des neuen Objekts, um den Wert festzuschreiben (Listing 7).
Die Fehlerbehandlung erfolgt in Delphi wie üblich über Exceptionhandling. Da Sie mit COM/DCOM arbeiten, werden im Fehlerfall OLEExceptions erzeugt. Deren Fehlercode korrespondiert mit den Fehlercodes, die in der WMI-Referenz verwendet werden. Dazu wird im Beispiel der Hex-Wert ausgegeben, der auch in der WMI-Referenz angegeben wird.
Listing 7 FServices.Security_.Set_ImpersonationLevel(
wbemImpersonationLevelImpersonate);
FWMIObj := FServices.Get('Win32_Environment', 0, nil);
FWMINewObj := FWMIObj.SpawnInstance_(0);
// varValue := '<SYSTEM>';
varValue := 'HostName\UserName';
FWMINewObj.Properties_.Item(
'UserName', 0).Set_Value(varValue);
varValue := 'KaffeeStatus';
FWMINewObj.Properties_.Item('Name', 0).Set_Value(varValue);
varValue := 'Fertig';
FWMINewObj.Properties_.Item(
'VariableValue', 0).Set_Value(varValue);
try
FWMINewObj.Put_(wbemChangeFlagCreateOnly, nil);
except
on E: EOLEException do
ShowMessage(Format('%x', [E.Errorcode]));
end;
Remote-Zugriff
Richtig spannend wird WMI bei der Administration remoter Rechnersysteme. Sie benötigen dazu aber auf dem anderen System einen Account. Beide Accounts, der auf dem aktuellen und dem remoten System, müssen Administrator-Accounts sein. Weiterhin darf das Passwort auf dem Client-PC nicht leer sein. Dies sollte in Firmennetzen kein Problem darstellen, da ein Systemadministrator immer bestimmte Arbeiten an den PCs durchführen muss und auch standardmäßig Passwörter verwendet werden. Um die Verbindung zum anderen Rechner herzustellen, wird die Methode ConnectServer diesmal mit zusätzlichen Parametern ausgeführt, die den Rechnernamen, den Benutzernamen sowie das Passwort enthalten: FServices := FLocator.ConnectServer('Rechnername',
'root/cimv2', 'Benutzername', 'Passwort', '', '',
wbemConnectFlagUseMaxWait, nil);
Wird der Benutzername und das Passwort frei gelassen, wird der aktuelle Benutzer verwendet. Der Parameter wbemConnectFlagUseMaxWait bedeutet übrigens, dass zur Verbindung zum Namespace höchstens zwei Minuten gewartet wird bis die Methode zurückkehrt. Dies verhindert ein einfrieren, wenn WMI nicht zur Verfügung steht. Alternativ kann noch der Wert 0 verwendet werden, der dann unendlich lange auf eine erfolgreiche Verbindung wartet (wie vielleicht der Anwender des Programms). Die weitere Vorgehensweise gleicht dem Zugriff auf die lokalen Daten.
Natürlich ist der Remote-Zugriff auf einen Rechner über WMI nicht in jedem Fall erwünscht. Da z.B. unter Windows XP der WMI-Service standardmäßig gestartet wird, müssen Maßnahmen ergriffen werden, die den Zugriff auf den Rechner unterbinden. Allein die Hoffnung, dass der Rechner- und Benutzername sowie das Passwort nicht bekannt sind, reicht zum Schutz vor Hackern nicht unbedingt aus. Unter den Systemeigenschaften finden Sie unter dem Register Remote die Einstellungen zur Remoteunterstützung und dem Remotedesktop. Beide dienen dazu, den remoten Zugriff auf einen Rechner zu erlauben. Wenn Sie die Dienste deaktivieren, ist damit auch der Zugriff über WMI nicht mehr möglich.
Der Remote-Zugriff hat leider noch ein paar Einschränkungen. So werden bestimmte Konstellationen bei der Herstellung einer Verbindung nicht unterstützt, z.B. der Zugriff auf Windows XP Home-PCs. In anderen Fällen werden zusätzliche Anforderungen an die betreffenden Betriebssysteme gestellt (installierte Service Packs etc.). Weiter Informationen finden Sie unter [1]. Zusammenfassung
Wie man sieht ist WMI mehr als nur ein Informationslieferant. Sie können Methoden von WMI-Objekten aufrufen, selbst neue Objekte erzeugen und dies auch auf remoten Rechnern durchführen. Damit tut sich ein unglaublich breit gefächertes Anwendungsgebiet auf. Als Delphi-Programmierer findet man im Internet oder im Buchhandel wenig Unterstützung bei der Arbeit mit WMI. Die Vorgehensweisen über Scripting oder .NET lassen sich aber weitestgehend adaptieren, sodass die dafür existierende Literatur auch genutzt werden kann. Beachten Sie, dass beide hier vorgestellte Artikel eher praxisorientiert an die Vermittlung des Themas gegangen sind, sodass Sie schon einmal lauffähige Anwendungen erstellen können. Das Studium der Klassen, deren Methoden, Parametern und Rückgabewerten kann und sollte jeder selbst in der WMI-Dokumentation nachlesen [2]. Links und Literatur
[1] Herstellen der Verbindung zu einem remoten PC:
msdn.microsoft.com/library/en-us/wmisdk/wmi/connecting_to_wmi_on_a_remote_computer.asp
[2] Startseite der WMI-Dokumentation:
msdn.microsoft.com/library/en-us/dnanchor/html/anch_wmi.asp