Ein Fernwartungssystem im Eigenbau

Veröffentlicht: 15. Dez 2001 | Aktualisiert: 17. Jun 2004

Von Tom Boldt

Die Erstellung von Erweiterungen der Management-Konsole ist eine reizvolle, wenn auch nicht ganz problemlose Angelegenheit. Anhand eines sehr nützlichen Beispielprojekts zeigen wir Ihnen, wie der Ablauf der Projekterstellung sein kann.

Auf dieser Seite

 TView im Überblick
 Die DCOM-Schnittstelle
 Implementierung der COM+-Komponente
 So greift TView auf Systemprozesse zu
 Das Laden der PSAPI.DLL
 Beschaffung von Informationen über Handles
 Der Kernmodustreiber für die Anzeige der Handles
 Das MMC-Snap-in-Projekt
 Implementierung der Persistenz und der Knotentypen
 Die Bereichsdarstellung
 Die Ergebnisdarstellung
 Ein Doppelklick auf die Ergebnisdarstellung
 MMC-Standardmenüs
 Die anwenderdefinierten Menübefehle von TView
 Implementierung der Sortierung
 Wiederholung der DCOM-Aufrufe, bis es klappt...
 Die Implementierung von "My Computer"
 Die letzten Schritte vor dem Ziel
 Die Sache mit der Sicherheit
 Die übliche Fehlersuche
 Ein Wort zum Schluss

Diesen Artikel können Sie hier lesen dank freundlicher Unterstützung der Zeitschrift:

Bild03

Windows NT Server 4.0 wird mit vielen Hilfsprogrammen für die Fernverwaltung des Systems geliefert, zum Beispiel mit einem Leistungsmonitor, einer Ereignisanzeige und einem Servermanager. Unter Windows 2000 werden diese Verwaltungsprogramme an einem zentralen Ort zusammengefasst, nämlich in der Microsoft Management Console (MMC). Mit Hilfe dieser Verwaltungskonsole kann man von einer einzelnen Anwendung auf einem einzelnen Computer aus viele ferne Server im Netz verwalten. Ein weiterer Vorteil der MMC ist, dass sie sich durch kleine Zusätze erweitern lässt, durch eine Programmart, die "Snap-In" genannt wird. Solch ein Snap-In ist die perfekte Form für den Vertrieb der eigenen Werkzeuge zur Fernverwaltung irgendwelcher Anwendungen oder Server, weil es sich nahtlos in die MMC einfügt. Die MMC ist nicht allein auf Windows 2000 beschränkt. Der Microsoft Transaction Server (MTS), der Internet Information Server (IIS) und der Microsoft SQL Server benutzen die MMC auch unter Windows NT 4.0. Und das können Sie auch.

In diesem Artikel möchte ich Ihnen vom Entwurf, der Implementierung und der Fehlersuche in TView erzählen, einem kleinen Hilfsprogramm, das dem Taskmanager oder dem Process Viewer ähnelt, der gelegentlich auch PView genannt wird. Mit dem Artikel möchte ich Ihnen bei der Entwicklung eigener MMC-Programme helfen. Außerdem ist das fertige TView ein nützliches kleines Programm. Zu dem Zeitpunkt, an dem ich diesen Artikel schreibe, ist die MMC-Version 1.2 aktuell und die Version 2.0 ist in Arbeit. Ich habe den Code mit der MMC 1.0 entwickelt und kann Ihnen das Objekt daher nur so anbieten, wie es ist - ohne weiteren Support. Warum ich 1.0 benutzt habe? Nun, in diesem speziellen Projekt musste ich Windows NT ebenso berücksichtigen wie Windows 2000. Wenn Sie das Windows NT 4.0 Option Pack installieren, erhalten Sie die MMC 1.0. Die MMC 1.1 und 1.2 sind im Plattform-SDK und in Produkten wie dem SQL Server 7.0 zu finden. Wenn Ihre Clients entsprechend ausgerüstet sind, sollten Sie die MMC 1.1 oder 1.2 benutzen.

TView im Überblick

TView hilft während der Entwicklung bei der Erkennung und Beseitigung von Fehlern, macht Konfigurationsprobleme deutlich, überwacht die Server in der mittleren Schicht und terminiert rüpelhafte Prozesse. Es gibt fünf Ansichten, in denen die Prozessinformationen angezeigt werden, nämlich die Ordner Memory, Modules, Environment, Handles und Processes.

Im Memory-Ordner kann man zum Beispiel herausfinden, welche virtuellen Adressbereiche frei sind und welche nicht. Ich habe damit schon eine neue Basisadresse für eine DLL gesucht. Ebenso eine virtuelle Adresse für eine auf den Speicher abgebildete Datei (memory-mapped file), die in jedem Prozess an derselben virtuellen Adresse liegen sollte. Außerdem habe ich in diesem Ordner schon untersucht, wieviel von einer DLL tatsächlich im realen Speicher liegt (der "Arbeitssatz" oder working set). Und ich habe diesen Ordner zur Suche nach den Ursachen eines Leistungsproblems benutzt, bei dem die gesamte DLL in den realen Speicher geladen wurde, obwohl keine ihrer Funktionen aufgerufen wurde.

Im Modules-Ordner lassen sich die Versionen der geladenen Module erkennen und die Verzeichnisse, aus denen sie geladen werden. Damit konnte ich schon so manches Konfigurationsproblem lösen. Wenn ein Programm zum Beispiel auf einem Computer korrekt läuft, auf einem anderen aber nicht, überprüfe ich mit TView die Versionen der System-DLLs und überprüfe, ob eine DLL vielleicht aus dem falschen Verzeichnis geladen wird (zum Beispiel aus dem Debug-Verzeichnis statt aus dem Release-Verzeichnis oder umgekehrt). Im Modules-Ordner kann man auch überprüfen, ob der dllhost.exe- oder mtx.exe-Prozess der richtige ist, indem man sich die DLLs ansieht, die in jeden Prozess geladen wurden. Damit konnte ich schon entscheiden, welcher Prozess zu terminieren und welcher zu debuggen ist.

Im Environment-Ordner lässt sich ermitteln, warum DLLs aus dem falschen Verzeichnis geladen werden. Ich habe mir damit die Pfade der Prozesse angesehen. Selbst wenn Sie den Systempfad ändern, ändern sich nicht die Pfade der laufenden Prozesse. Im Environment-Ordner kann man sich auch die Kommandozeile eines Prozesses anschauen. Damit konnte ich zum Beispiel den Kommandozeilenparameter von dllhost.exe ermitteln (/ProcessID:{AppID}) und anhand der Kommandozeile entscheiden, welcher Prozess zu debuggen oder zu beenden ist, wenn sich die Prozesse nur in Kommandozeile unterscheiden.

Im Handles-Ordner erkennt man Handle-Lecks. Vielleicht brauchen Sie die Informationen, um einem RegOpenKeyEx auf die Spur zu kommen, dem noch ein passendes RegCloseKey fehlt.

Die Processes-Darstellung lässt sich zum Beispiel zur Überprüfung des Ressourcenverbrauchs eines Prozesses verwenden (Speicher, Handles und CPU). Mich interessiert gelegentlich, welche Prozesse den Hauptteil meiner 64 MByte oder 128 MByte Speicher belegen und meine Maschine wegen der zunehmenden Auslagerung von Speicherseiten in die Knie zwingen.

Wie Sie sehen, ist TView durchaus ein nützlicher kleiner dienstbarer Geist. TView besteht aus einem Client und einem Server. Der Client ist ein MMC-Snap-In (Bild B1) und beim Server handelt es sich um eine COM-Komponente, die auf der fernen Maschine in einem COM+-DLL-Host (oder MTS) läuft. Das MMC-Programm kommuniziert via DCOM mit der COM+-Komponente, wie in Bild B2 gezeigt.

Bild01

B1 Die Moduldarstellung in der MMC-Erweiterung TView.

Bild02

B2 Die Architektur eines MMC-Programms (Snap-In)

Zuerst möchte ich auf die DCOM-Schnittstelle eingehen. Anschließend werde ich die Implementierung der COM+-Komponente beschreiben und erläutern, wie sie Zugriff auf die Systemprozesse erhält, die PSAPI.DLL lädt und sich Informationen über die Handles besorgt. Da nur ein Kernmodustreiber die erforderlichen Angaben über Handles erhält, möchte ich natürlich auch noch erzählen, wie ich einen solchen implementiert habe. Im Anschluss daran möchte ich das MMC-Programm und den dazugehörigen Sicherheitsmechanismus beschreiben und Ihnen auch noch etwas über die Fehlersuche erzählen, die sich unvermeidlich an die Implementierung anschloss.

 

Die DCOM-Schnittstelle

Der erste Schritt beim Entwurf von TView war die Definition der DCOM-Schnittstelle. Listing L1 zeigt ihre IDL-Definition (Interface Definition Language). ITView wurde so ausgelegt, dass für jede Aktion des Anwenders nur ein DCOM-Aufruf erforderlich ist. ITView benutzt unverbundene ADO-Recordsets (ActiveX Data Objects), um in einem einzigen Rundgang durch das Netz große Datenmengen liefern zu können. Falls Sie bisher immer der Meinung waren, ADO werde nur für Zugriffe auf Datenbanken benutzt, sollten Sie unbedingt einen zweiten Blick riskieren. ADO selbst kann schon höchst nützlich sein, auch ohne angeschlossene Datenbank oder OLE DB-Anbieter. ADO-Parameter können in Schnittstellen eingesetzt werden, nachdem die ADO-Typbibliothek mit der folgenden Zeile in die IDL-Datei aufgenommen wurde:
importlib("msado20.tlb");
ITView wurde für den Aufruf von C++ und Visual Basic aus konzipiert, und von Sprachen, die zum Active Scripting kompatibel sind. TView selbst wurde zwar vollständig in C++ geschrieben, aber wer gerne mit anderen Programmiersprachen arbeitet, kann die COM+-Komponente in einer geeigneten Sprache seiner Wahl verwenden.

Für jede der acht Aktionen, die sich in der MMC-Anwenderschnittstelle ausführen lassen, hat ITView eine Methode. Wenn der Processes-Ordner angewählt, erweitert oder aktualisiert wird, liefert GetProcesses die Informationen über alle Prozesse:

HRESULT GetProcesses([out, retval] _Recordset **ppRecordset);

Bei der Anwahl des Ordners Modules liefert GetModules die Informationen über alle Module:

HRESULT GetModules([in] long processID,  
                   [out, retval] _Recordset **ppRecordset);

In ähnlicher Weise werden nach der Anwahl des Ordners Memory, Handles oder Environment die Methoden GetMemory, GetHandles oder GetEnvironment aufgerufen, und zwar mit denselben Parametern wie GetModules.

Mit folgender Funktion lässt sich ein Prozess terminieren:

HRESULT KillProcess([in] long processID);

In ähnlicher Weise lässt sich ein Prozess mit DebugProcess debuggen. Sinnvoll ist das allerdings nur, wenn Sie gerade Ihre lokale Maschine überwachen, denn auf einer fernen Maschine startet DebugProcess den Debugger auf der fernen Maschine und nicht auf der Client-Maschine. Außerdem müssen Sie einen Debugger installiert haben und die COM+Komponente muss so konfiguriert sein, dass sie mit demselben Anwendernamen läuft, den auch der aktuell angemeldete Anwender benutzt. Das passt nicht so ganz zu den anderen Leistungen vom TView, weil diese auch für ferne Maschinen verfügbar sind, während das Debuggen sinnvollerweise nur auf einer lokalen Maschine stattfinden kann. Auch wenn der Debug-Aufruf nur unter bestimmten Umständen von Nutzen ist, braucht man ihn dann meistens umso dringender.

Und schließlich lässt sich die ferne Maschine noch zum Neustart veranlassen:

HRESULT ShutdownMachine([in] long nFlags);

Es sieht so aus, als lieferten GetProcesses und die anderen Get-Methoden nur die ADO-Schnittstelle _Recordset. Durch die Magie von ADO wird aber die gesamte Ergebnistabelle als Wert geliefert. Wenn Sie anschließend die _Recordset-Schnittstelle benutzen, werden dadurch keine weiteren Rundgänge durch's Netz ausgelöst.

L1 So wird die ITView-Schnittstelle in IDL definiert

import "oaidl.idl"; 
import "ocidl.idl"; 
[ 
   uuid(48BBFB46-B3C3-11D1-860C-204C4F4F5020), 
   version(1.0), 
   helpstring("tviewmts 1.0 Type Library") 
] 
library TVIEWMTSLib 
{ 
   importlib("stdole32.tlb"); 
   importlib("stdole2.tlb"); 
   importlib("msado20.tlb"); 
   [ 
      object, 
      uuid(A56485E2-1DDA-4B2B-ACDC-94E51212873B), 
      dual, 
      helpstring("ITView Interface"), 
      pointer_default(unique) 
   ] 
   interface ITView : IDispatch 
   { 
      // Maschinenaktionen 
      [id(1)] HRESULT ShutdownMachine([in] long nFlags); 
      // Maschineninfo 
      [id(2)] HRESULT GetProcesses( 
         [out, retval] _Recordset **ppRecordset); 
      // Prozessaktionen 
      [id(3)] HRESULT KillProcess([in] long processID); 
      [id(4)] HRESULT DebugProcess([in] long processID); 
      // Prozessinfo 
      [id(5)] HRESULT GetModules([in] long processID, 
         [out, retval] _Recordset **ppRecordset); 
      [id(6)] HRESULT GetMemory([in] long processID, 
         [out, retval] _Recordset **ppRecordset); 
      [id(7)] HRESULT GetHandles([in] long processID, 
         [out, retval] _Recordset **ppRecordset); 
      [id(8)] HRESULT GetEnvironment([in] long processID, 
         [out, retval] _Recordset **ppRecordset); 
   }; 
   [ 
      uuid(48BBFB56-B3C3-11D1-860C-204C4F4F5020), 
      helpstring("TView Class") 
   ] 
   coclass TView 
   { 
      [default] interface ITView; 
   }; 
};

 

Implementierung der COM+-Komponente

Zur Erstellung der COM+-Komponente namens tviewmts startete ich im Visual Studio C++ 6.0 den ATL COM AppWizard und wählte die Option "Support MTS". Später nahm ich ein ATL-Objekt namens TView ins Projekt auf. Da ich in diesem Artikel wohl davon ausgehen darf, dass Sie sich mit ATL auskennen, möchte ich Sie nicht mit Einzelheiten über die Vorbereitungen langweilen.

Der erste Schritt bei der Implementierung der COM+-Komponente besteht darin, das Grundgerüst kompilierbar zu machen, bevor man irgendwelchen Code hinzufügt. Die IDL-Datei bezieht sich auf die ADO-Typbibliothek. Folglich muss diese auch in den Quelltext eingebunden werden. ADO lässt sich mit folgender Zeile einbinden:

#import <msado20.tlb> no_namespace rename("EOF", "adoEOF")

Bei meinem ersten Versuch hatte ich ein Problem mit EOF. In der stdio.h wird EOF mit -1 definiert. Ich musste die Importanweisung um die rename-Angabe erweitern, die EOF in adoEOF ändert. Ein Namensraum hilft da nicht viel weiter, weil es sich bei EOF nicht um ein konstantes Objekt handelt, das in irgendeinem Namensraum liegt, sondern um eine symbolische Konstante, die in der stdio.h definiert wird. Man könnte EOF natürlich auch per #undef loswerden, wie es der Artikel "Implementing ADO with Various Development Languages" beschreibt (https://msdn.microsoft.com/library/techart/msdn\_adorosest.htm).

Wenn Sie mit dem obigen #import arbeiten, wird der HRESULT-Rückgabewert in eine _com_error-Ausnahme umgewandelt. Der [out, retval]-Parameter wird in den Rückgabewert umgewandelt. Sobald Sie mit C++-Ausnahmen arbeiten, müssen Sie dafür sorgen, dass Ihr Code tatsächlich dafür vorbereitet ist. Stellen Sie den Code, der zur Meldung von Ausnahmen führen kann, in einen entsprechenden try-catch-Block und benutzen Sie Klassen mit Destruktoren, damit der entsprechende Aufräumcode automatisch aufgerufen wird. Für COM-Zeiger können Sie die ATL-Klasse CComPtr benutzen oder auf die Hilfe zurückgreifen, die der Compiler Ihnen mit _com_ptr_t für COM-Klassen gibt (schauen Sie sich die Dokumentation des Makros _COM_SMARTPTR_TYPEDEF an). In meinem Fall wollte ich sicherstellen, dass das Prozesshandle nach der Öffnung eines Prozesses auch irgendwann wieder geschlossen wird. Darum kümmert sich nun eine Klasse in der TView.cpp, die in ihrem Konstruktor OpenProcess aufruft und im Destruktor CloseHandle.

Für die Speicheranforderungen wollte ich eigentlich die auto_ptr-Klasse aus der C++-Standardbibliothek verwenden. Das Problem mit auto_ptr ist nur, dass ich ein Array anlegen und wieder entsorgen wollte, wogegen auto_ptr delete ohne die eckigen Array-Klammern aufruft. Obwohl die Klasse in Visual C++ 6.0 mit BYTE-Arrays zu funktionieren scheint, ist damit nicht gesagt, dass dies auch bei anderen Compilern so ist oder in zukünftigen Visual C++-Versionen so bleibt. Daher habe ich mich für die sichere Seite entschieden und meine eigene kleine Klasse geschrieben, die Arrays automatisch anlegt und entsorgt. Und wie aus Listing L2 hervorgeht, ist das eigentlich auch keine große Sache.

L2 Anlage und Entsorgung von Arrays

template <class T> 
class CBuffer 
{ 
public: 
   explicit CBuffer(DWORD nSize) 
   { 
      m_p = new T[nSize]; 
   } 
   ~CBuffer() 
   { 
      delete [] m_p; 
   } 
   operator T*() 
   { 
      return m_p; 
   } 
private: 
   T *m_p; 
};

Nach der Vorbereitung des try-catch-Blocks und der Erstellung der Aufräumklassen können Sie ADO benutzen. Der Code für GetProcesses und GetModules unterscheidet sich kaum. Beide Funktionen legen einen ADO-Recordset an:

_RecordsetPtr pRecordset(__uuidof(Recordset));

Der Umgang mit COM ist in Visual C++ eigentlich nicht wesentlich schwerer als in Visual Basic, wo die obige Zeile ungefähr so lautet:

Dim rs As New Recordset

Beide Zeilen greifen hinter der Bühne auf CoCreateInstance zurück, um den Recordset zu bauen.

Sobald der Recordset fertig ist, braucht man zum Navigieren im Recordset einen passenden Cursor. In diesem Fall muss der Client für den Cursor sorgen, weil es keinen Datenanbieter gibt und somit auch kein Cursor aus irgendeinem Treiber verfügbar ist.

pRecordset->CursorLocation = adUseClient;

An diesem Punkt ist es nun möglich, die gewünschten Spalten in den Recordset einzubauen. Spalten werden an das Fields-Property des Recordsets angehängt. Das Fields-Property erhalten Sie mit folgender Zeile:

FieldsPtr pFields = pRecordset->Fields;

Wir brauchen eine Spalte für ganze 32-Bit-Zahlen:

pFields->Append(L"id", adInteger, 0, adFldUnspecified);

Eine Textspalte wäre auch nicht schlecht:

pFields->Append(L"name", adBSTR, 0, adFldUnspecified);

Und die folgende Zeile sorgt für eine Spalte, in der Datums- und Zeitangaben Platz finden:

pFields->Append(L"start", adDate, 0, adFldUnspecified);

In jeder Append-Anweisung wird der Name der Spalte angegeben, gefolgt von der Art der Daten, die in der Spalte unterzubringen sind. Das dritte Argument ist die Größe, die bei den drei Datentypen, die ich verwende, aber nicht angegeben werden muss. Das letzte Argument schließlich ist das Feldattribut. Auch hier sind in diesem Fall keine speziellen Angaben erforderlich.
Sobald die Spalten eingebaut sind, wird der unverbundene Recordset (die Tabelle) geöffnet:

pRecordset->Open(vtMissing, vtMissing, adOpenStatic, 
   adLockBatchOptimistic, adOptionUnspecified);

Der erste Parameter von Open nennt die Quelle, wobei es sich um ein ADO-Befehlsobjekt handeln kann oder um einen String, der zum Beispiel einen SQL-Befehl enthält. Der zweite Parameter ist die Verbindung, wobei es sich um ein Connection-Objekt von ADO oder um einen Verbindungsstring handeln kann. In unverbundenen Recordsets wird keiner der ersten beiden Angaben gebraucht, so dass hier nur vtMissing angegeben wird. Die vordefinierte vtMissing-Konstante wird für ein VARIANT mit vt=VT_ERROR und scode=DISP_E_PARAMNOTFOUND benutzt. Auf diese Weise teilt man Visual Basic mit, dass ein optionaler Parameter unbenutzt bleibt. Der letzte Parameter dient zur Angabe von Optionen für die Quelle und die Verbindung. Auch für ihn sind keine besonderen Werte erforderlich. Für unverbundene Recordsets brauchen also nur der dritte (Cursorart) und der vierte Parameter (Verriegelungsart) angegeben zu werden. Die Konstante adOpenStatic sorgt für eine statische Kopie der Datensätze und adLockBatchOptimistic fordert die Aktualisierung der Daten in einem Rutsch. Diese Optionen ermöglichen es ADO zusammen mit adUseClient, im Client eine Kopie der Daten zu halten, dem Client das Blättern in den Daten zu erlauben und ihm auch die Änderung der Daten zu gestatten, ohne den Server darüber zu informieren. Anders gesagt, diese Optionen weisen ADO an, den gesamten Recordset als Wert an den Client zu übermitteln.

Nun lassen sich mit AddNew neue Datensätze oder Zeilen in den Recordset eintragen:
pRecordset->AddNew();
Die einzelnen Daten in den Zeilen lassen sich mit Anweisungen wie den folgenden setzen:

pFields->Item[L"id"]->Value = (long) processID; 
pFields->Item[L"name"]->Value = szExe; 
pFields->Item[L"start"]->Value = vCreationTime;

Die fünf Get-Methoden rufen verschiedene PSAPI- und Win32-Funktionen auf, um den ADO-Recordset mit der gewünschten Nutzlast zu füllen. Auch einige Kernmodusfunktionen sind darunter, sogar undokumentierte Funktionen. Den vollständigen Quelltext finden Sie auf der Begleit-CD dieses Hefts.

 

So greift TView auf Systemprozesse zu

Wenn Sie im Taskmanager von Windows NT oder 2000 die Seite mit der Prozessanzeige aufschlagen und im Kontextmenü den Befehl Prozess beenden geben, wenn ein Windows NT-Dienst angewählt ist (zum Beispiel inetinfo.exe), erhalten Sie eine "Abbrechen des Prozesses nicht möglich"-Meldung mit dem Text "Der Taskmanager konnte diesen unverzichtbaren Systemprozess nicht beenden". Der inetinfo.exe-Prozess läuft dann einfach weiter. Ein Befehl zum Beenden eines Prozesses wäre im TView wohl kaum sinnvoll, wenn er dieselbe Zurückhaltung aufwiese, denn Dienstprozesse sind bei der Überwachung von fernen Servern wichtiger als Desktop-Prozesse. Mit TView lässt sich der inetinfo.exe-Prozess terminieren, weil sich TView in der Methode AdjustPrivilege das Debug-Privileg sichert (SE_DEBUG_NAME). Wie Sie in der Datei TView.cpp sehen (zu finden auf der Begleit-CD), wird AdjustPrivilege im Konstruktor von COpenProcess mit den Schaltern SE_DEBUG_NAME und SE_PRIVILEGE_ENABLED aufgerufen. Das Recht zum Debuggen von Programmen lässt sich nur freischalten, wenn schon der Anwender dieses Recht hat, der für die TView-COM+-Anwendung angegeben wurde.

Dieser Code hat aber seine Grenzen. AdjustPrivilege ändert nur die Rechte im Prozesstoken, also nicht im Threadtoken. Beim Multithread-Zugriff wird er wohl nicht immer funktionieren. Eigentlich wollte ich anfangs das Threadtoken öffnen, aber das ging mit dem Fehlercode ERROR_NO_TOKEN schief (es wurde der Versuch gemacht, auf ein nicht existierendes Token zuzugreifen). Ein Thread hat kein Token, solange er nicht als Anwender auftritt. Da ein Computer vermutlich sowieso nur von einer Stelle aus überwacht wird, beschränke ich mich einfach auf das Prozesstoken. In einer zukünftigen TView-Version werden vielleicht kritische Abschnitte eingesetzt (critical sections), um zumindest sicherzustellen, dass das gesetzte Recht auch gesetzt bleibt.

 

Das Laden der PSAPI.DLL

Die PSAPI.DLL enthält in Windows NT und 2000 die Hilfsfunktionen für den Prozesszustand. Unter Windows NT 4.0 ist es eine optionale Datei, die übrigens nicht unter Windows 95 oder 98 läuft. Dort gibt es statt dessen die Toolhelp-Funktionen. Schon in der Standardinstallation von Windows 2000 gibt es dagegen sowohl die Hilfsfunktionen für den Prozesszustand als auch die Toolhelp-Funktionen. Da es sich bei der PSAPI.DLL um eine optionale Datei handelt, gebe ich in den Linkeroptionen vom Visual C++ 6.0 für diese Datei das verzögerte Laden an. Dadurch lässt sich die COM+-Komponente auch ohne installierte PSAPI.DLL laden und registrieren.

Sollte die PSAPI.DLL beim Aufruf einer ihrer Funktionen nicht verfügbar sein, wird eine Win32-Ausnahme gemeldet. Ich fange diese Meldungen mit der catch-Anweisung von C++ ab. Wenn Sie mit Visual C++ 6.0 arbeiten, lassen sich Win32-Ausnahmen nicht ohne Vorbereitungen abfangen. Damit das nämlich funktioniert, müssen Sie den Code mit dem Compilerschalter /EHa kompilieren. Allerdings gibt es für diesen Schalter keine Checkbox, so dass er von Hand ins Eingabefeld mit den Compilerschaltern eingegeben werden muss. Durch /EHa wird die asynchrone Bearbeitung von Ausnahmen eingeschaltet. Visual C++ 5.0 war von Haus aus in der Lage, mit asynchronen Ausnahmen umzugehen. Es gab auch keinen Compilerschalter, um das zu ändern. Seien Sie also vorsichtig, wenn Sie von Visual C++ 5.0 auf 6.0 umsteigen, denn der Standardmodus hat sich von freigeschaltet auf gesperrt geändert.

 

Beschaffung von Informationen über Handles

Um ein nützliches Werkzeug zu werden, muss TView so viele Informationen wie möglich über einen Prozess liefern. Auch und gerade solche Informationen, die mit den üblichen Win32-Funktionen nur schwer oder gar nicht zu beschaffen sind. Das Wissen um die Zahl der Handles, die von einem Prozess benutzt werden, hilft bei der Überprüfung des Prozesses auf Handle-Lecks. Sobald Sie zu dem Schluss gekommen sind, dass es ein Leck geben muss, können Sie sich in der Handle-Anzeige von TView darüber informieren, welche Handles nicht ans System zurückgegeben wurden.

Nun gibt es aber keine Win32-Funktion, mit der sich die Zahl der Handles ermitteln ließe, die ein Prozess benutzt. Allerdings hat Matt Pietrek schon in seinem Artikel "Unter der Haube" im System Journal 03/1997 beschrieben, wie man sich diese Informationen mit NtQueryInformationProcess und ProcessHandleCount beschafft. Allerdings hatte ich so meine Probleme damit, die NTDLL.LIB aus dem DDK zu linken, wie Matt es in seinem Artikel beschreibt. Also legte ich meine eigene an. Das war sogar ziemlich einfach. Ich legte ein DLL-Projekt namens NTDLL.DLL an und exportierte eine leere Funktion namens NtQueryInformationProcess. Auf diese Weise brauchte ich meinen Quelltext nicht mit LoadLibrary und GetProcAddress komplizierter als nötig zu gestalten.

 

Der Kernmodustreiber für die Anzeige der Handles

Wenn Sie ein Fan der Kernmodusprogrammierung sind, sollten Sie diesen Abschnitt lesen. Falls Sie sich aber in der üblichen Programmierung im Anwendungsmodus wohler fühlen, können Sie diesen Abschnitt ruhig überspringen und zum nächsten übergehen. Dort geht es mit der MMC weiter.

Um die Handles-Darstellung implementieren zu können, musste ich einen Kernmodustreiber schreiben. Ich hatte zwar schon einige Artikel (darunter auch alte Nerditorium-Kolumnen aus dem System Journal) und Bücher über Kernmodustreiber gelesen, aber noch nie versucht, selbst einen zu schreiben. Im Kernmodus stehen sehr viele Informationen zur Verfügung, die bei der Fehlersuche nützlich sind. Und an diese Informationen wollte ich herankommen. Leider hat Microsoft die Funktionen, die ich aufrufen wollte, nie offiziell dokumentiert. Zum Glück fand sich in den Büchern Windows NT/2000 Native API Reference von Gary Nebbett (New Rider Publishing, 2000) und Windows NT File System Internals von Rajeev Nagar (O'Reilly and Associates, 1997) alles, was ich über diese Funktionen wissen musste. Und selbstverständlich muss ich jetzt die Verteidigungslinien aufbauen, die beim Einsatz von undokumentierten Funktionen üblich sind: wenn Sie diese Funktionen benutzen, tun Sie dies auf Ihre eigene Gefahr. Microsoft könnte diese Funktionen in einer zukünftigen Version von Windows 2000 entfernen, vielleicht sogar schon im nächsten Service Pack.

Damit der Kernmodustreiber möglichst einfach bleibt, habe ich den Hauptteil der Logik im Anwendungsmodus implementiert. Der Kernmodustreiber hat zwei Gerätesteuercodes, nämlich einen zur Ermittlung des Namens von einem Handle und einen zweiten zur Ermittlung des Typs. In beiden Fällen ist die Angabe einer Prozesskennung und eines Handles erforderlich. Nun hätte ich zwar in einem einzigen Aufruf die Namen oder Typen von vielen Handles liefern können, aber dadurch wäre der Treiber komplizierter und fehleranfälliger geworden. Wie ich sehr schnell (und wiederholt) herausfand, führen Fehler im Kernmodustreiber dazu, dass die Maschine abstürzt oder einfach festhängt. Zum Glück ist das beim normalen Einsatz von TView noch nicht vorgekommen, sondern nur während der Entwicklung. Das TView läuft sogar auf der Zwei-Prozessor-Maschine ganz ordentlich, die ich an meinem Arbeitsplatz zur Verfügung habe.

Anfangs hatte ich vor, einen einfachen Gerätetreiber synchron im Kontext des aufrufenden Prozesses laufen zu lassen. Ich wollte nur die Kernmodusfunktionen aufrufen, die ich brauchte, und dann das IRP (I/O Request Packet) mit IoCompleteRequest abschließen. Mit den meisten Handlewerten funktionierte das recht ordentlich, aber manche Handles brachten den aufrufenden Prozess und den Treiber dazu, sich gemeinsam aufzuhängen. Ich musste die Maschine neu starten, um den Treiber wieder einsatzbereit zu machen. Dann las ich irgendwann den Artikel "Techniken für die Benachrichtigung von Anwendungen" (System Journal 06/1999, S. 74) noch einmal. In diesem Beitrag ging es um den asynchronen Aufruf von Kernmodustreibern. Es war also genau das, was ich brauchte.

Ich änderte meinen Code so ab, das er das IRP asynchron bearbeitet und ExQueueWorkItem aufrief. ExQueueWorkItem lässt sich mit CreateThread vergleichen, greift aber auf einen Vorrat an Kernmodus-Threads zurück, die nicht in irgendwelchen Anwendungsmodus-Adressräumen laufen. Nach vielen Versuchen, blauen Bildschirmen und Neustarts brachte ich den Code schließlich dazu, ohne Hänger und Abstürze zu laufen.

Mein Algorithmus zum Aufzählen der möglichen Handlewerte ist allerdings noch verbesserungsbedürftig, denn ich rate die Werte einfach nur, statt eine Funktion aufzurufen, die eine Liste mit den gültigen Handlewerten liefert. In einer zukünftigen Version von TView werde ich mir wohl mit ZwQuerySystemInformation (zusammen mit SystemHandleInformation, wie es Gary Nebbett in seinem Buch beschreibt) eine Liste der gültigen Handlewerte beschaffen. In diesem Buch gibt es ein Beispiel, in dem alle gültigen Handlewerte aus einem Prozess aufgezählt werden. Genau das, was TView macht. Das Beispiel läuft sogar im Anwendungsmodus. Allerdings heißt es an einer Stelle, das Beispiel würde bei bestimmten Handlewerten hängen bleiben. Das kam mir irgendwie bekannt vor. Ich vermute, so einfach werde ich den Kernmoduscode wohl nicht loswerden. Zumindest kann ich den ZwQuerySystemInformation-Aufruf im Anwendungsmodus implementieren, so dass ich mir bei der Fehlersuche während der zukünftigen Umstellung die langen Reihen blauer Bildschirme erspare (hoffe ich zumindest). Im Kernmoduscode, der den Namen und den Typ des Handles ermittelt, dürften keine Änderungen erforderlich werden.

 

Das MMC-Snap-in-Projekt

Die Entwicklung der COM-Komponente tviewmmc, die Sie auf der Begleit-CD finden, begann ich mit dem ATL COM AppWizard. Anschließend nahm ich ein ATL-Objekt namens TViewSnapin ins Projekt auf, das ich mit dem MMC Snap-in Wizard erstellte (allerdings handelt es sich bei diesem Assistenten um ein veraltetes Produkt, das nicht von Microsoft unterstützt wird - wenn Sie damit arbeiten, sind Sie auf sich selbst gestellt). Ich startete die MMC, gab den Menübefehl Konsole / Snap-in hinzufügen/entfernen und nahm mein TView-Programm dort auf. Beim Versuch, die Datei zu speichern, erhielt ich die Meldung "Die gewählte Datei konnte nicht geöffnet werden." Der MMC 1.0 lässt es nicht zu, die Konsolendatei mit der vom Wizard generierten Implementierung von IPersistStreamInit abzuspeichern. Ich wollte die Konsolendatei speichern, damit ich mein Snap-in debuggen konnte, ohne es nach jedem Neustart wieder als MMC-Erweiterung eintragen zu müssen. Später fiel mir auf, dass dies bei den MMC-Versionen 1.1 und 1.2 (das ist die Version im Windows 2000) anscheinend kein Problem mehr ist. Daher möchte ich an dieser Stelle noch einmal darauf hinweisen, dass die MMC 1.1 und 1.2 eine weiterentwickelte Funktionalität bieten als dieser Code (Weitere Einzelheiten finden Sie unter https://msdn.microsoft.com/library/psdk/mmc/mmc12new01\_1m42.htm).

Ich hatte so meine Schwierigkeiten, als ich mit der Entwicklung der MMC-Snap-Ins begann. Zum Teil lag es wohl daran, dass mir die Namen der MMC COM-Schnittstelle und der ATL-Klassennamen nicht gerade "intuitiv" einleuchteten. Um Ihnen diesen Ärger zu ersparen, habe ich keine kleine Tabelle aufgestellt, die Sie zu Rate ziehen können, wenn Sie die MMC-Dokumentation lesen (Tabelle T1).

T1 Die Namen der COM- und ATL-Schnittstellen

Beschreibung

linke Seite

rechte Seite

einzelner Eintrag

Name in der MMC-Anwenderschnittstelle

Console Tree

Details Pane

Item

Name in der Entwickler-Dokumentation

Scope Pane oder Scope View

Result Pane oder Result View

Item

Schnittstellen, die von der MMC implementiert werden

IConsoleIconsoleNameSpace

IConsoleIResultData

(keine)

Schnittstellen, die vom MMC-Programm implementiert werden

IcomponentData

IComponent

IDataObject

ATL-Basisklasse

CSnapInObjectRoot<1>

CSnapInObjectRoot<2>

CSnapInItemImpl

Vom ATL-Wizard generierte Klasse

CXXX

CXXXComponent

CXXXData

Struktur

SCOPEDATAITEM

RESULTDATAITEM

SCOPEDATAITEM und RESULTDATAITEM

Das andere Problem besteht darin, dass die MMC selbst zwar recht gut dokumentiert ist, die ATL-Unterstützung für die MMC aber nicht. In dem Artikel "How Do I Add Custom Item Types to the Snap-In Object?" unter https://msdn.microsoft.com/library/devprods/vs6/visualc/vcmfc/vcrefhowdoiaddcustomitemtypestosnapinobject.htm heißt es: "Im Normalfall erzeugt der ATL Object Wizard für das Snap-in-Objekt eine einzelne Datenklasse, die er von CSnapInItemImpl ableitet. In manchen Fällen müssen Sie eine oder mehrere anwenderdefinierte Item-Typen für das Snap-in-Objekt implementieren." Nun, auch diese Aussage brachte mich etwas durcheinander, weil sie implizierte, dass es normal war, mit einem Knotentyp auszukommen. Mehrere verschiedene Knotentypen war also nicht normal. Im besagten Artikel heißt es weiter, dass es keinen Wizard gäbe, der mehrere Knotentypen generieren könnte. Das muss von Hand erledigt werden. Vielleicht ist Ihnen auch schon aufgefallen, dass es keinen Wizard für den Einbau von Menü-Handlern gibt und die Kopfdatei atlsnap.h praktisch die einzige Dokumentation darstellt.

In den nächsten Abschnitten möchte ich Ihnen schildern, wie ich die Persistenz, mehrere Knotentypen, die UI für jede Anzeigefläche und die Menüoptionen für das MMC-Programm implementierte. Außerdem gibt es noch einiges über die Sortierung zu sagen, über die Speicherung der Namen von den überwachten Computern, über die Wiederholung von DCOM-Aufrufen, um das Snap-In bei zeitweiligen Aussetzern der COM+-Serveranwendung von der Anzeige eines Fehlers abzuhalten, und über die Implementierung eines "My Computer"-Knotens für TView.

 

Implementierung der Persistenz und der Knotentypen

Um mit der MMC 1.0 eine Konsolendatei abspeichern zu können, baute ich versuchsweise eine IPersistStreamInit-Schnittstelle in CTViewSnapin ein. Die Klassenkennung setzte ich in GetClassID, die Größe gab ich in GetSizeMax mit null an und ließ jede Funktion S_OK zurückgeben, wie in Listing L3. Da ich aber keine Verwendung für IPersistStreamInit sah, nahm ich diese Schnittstelle aus CTViewSnapinComponent heraus.

L3 Die Funktionen in der Datei TViewSnapin.cpp

HRESULT CTViewSnapin::GetClassID(CLSID *pClassID) 
{ 
   *pClassID = CLSID_TViewSnapin; 
   return S_OK; 
} 
HRESULT CTViewSnapin::IsDirty() 
{ 
   return S_OK; 
} 
HRESULT CTViewSnapin::Load(IStream * /*pStm*/) 
{ 
   return S_OK; 
} 
HRESULT CTViewSnapin::Save(IStream * /*pStm*/, BOOL 
/*fClearDirty*/) 
{ 
   return S_OK; 
} 
HRESULT CTViewSnapin::GetSizeMax(ULARGE_INTEGER *pcbSize) 
{ 
   pcbSize->QuadPart = 0; 
   return S_OK; 
} 
HRESULT CTViewSnapin::InitNew() 
{ 
   return S_OK; 
}

Wie schon im Abschnitt über das MMC-Snap-In-Projekt erwähnt, generiert der MMC Snap-In Wizard nur einen Knotentyp. Und das reicht wohl nicht aus. Meine MMC-Erweiterung benutzt immerhin 13 Knotentypen. Also nahm ich mir die generierte CTViewSnapinData-Klasse vor und teilte sie in separate Kopf- und Rumpfdateien auf. Anschließend fertigte ich 13 Kopien dieser Dateien an, für jeden Knotentyp eine. Ich gab jeder Datei einen halbwegs sinnvollen Namen und nahm die Dateien in mein Projekt auf. Nun sind dreizehn identische Quelltextdateien nicht sonderlich nützlich. Also musste jede überarbeitet werden und ihren eigenen Charakter erhalten.

In jeder Knotenklasse gibt es die statischen Datenelemente m_NODETYPE und m_SZNODETYPE. Nun ist es sehr wichtig, diese Datenelemente für jede Knotenart mit einer unverwechselbaren GUID zu initialisieren. Achten Sie bei der Änderung der GUIDs aber unbedingt darauf, dass diese beiden Werte synchron bleiben, denn m_SZNODETYPE ist die Stringdarstellung von m_NODETYPE. Die neuen Knotentypen müssen in die Registrierdatei (RGS) eingetragen werden (Werfen Sie hierzu einen Blick in die Datei tviewsnapin.rgs auf der Begleit-CD).

Die nächste offensichtliche Änderung betraf den Klassennamen. Ich entschied mich für Klassennamen mit der Endung "Item", wie zum Beispiel CRootFolderItem, statt die Klassen wie die vom Wizard generierten Item-Klassen auf "Data" enden zu lassen (CTViewSnapinData). Übrigens könnte man "Data" auch leicht mit IComponentData verwechseln, der Schnittstelle, die in der Klasse CTViewSnapin implementiert wird.

Jeder Knoten hat ein anderes Kontextmenü. Also kopierte ich die Standard-Menüressource für jeden Knotentyp und gab ihr jeweils einen anderen Namen. In jeder Kopfdatei gibt es eine Zeile mit dem Makro SNAPINMENUID, das eine Menüressourcenkennung als Argument erwartet. Dieses Argument änderte ich natürlich für jeden Knotentyp so ab, dass es zur neuen Menüressource passt.

Damit die verschiedenen Knotenklassen nicht unnötige Codeduplikate mit sich herumschleppen, implementierte ich in der Datei tviewitem.h eine passende Templateklasse namens CTViewSnapInItemImpl, die Sie auf der Begleit-CD finden. Eine der wichtigsten Aufgaben dieser Klasse ist die Überschreibung von CSnapInItemImpl::Notify. CTViewSnapInItemImpl::Notify ruft für alle Hinweisnachrichten, die ich bearbeiten möchte, virtuelle Funktionen auf. In der abgeleiteten Klasse braucht man nur die entsprechende virtuelle Funktion zu überschreiben, um eine bestimmte Hinweisnachricht bearbeiten zu können.

L4 Die Funktion OnExpand

HRESULT CComputerFolderItem::OnExpand(BOOL bExpand, 
   HSCOPEITEM hScopeItem, IConsole* pConsole) 
{ 
   CComQIPtr<IConsoleNameSpace, &IID_IConsoleNameSpace> 
      spConsoleNameSpace(pConsole); 
   HSCOPEITEM hChild = NULL; 
   long cookie = 0; 
   spConsoleNameSpace->GetChildItem(m_scopeDataItem.ID, &hChild, 
                                    &cookie); 
   if (hChild != NULL) 
   { 
      // (wegen eines Fehlers in der MMC 1.0): Prüfe, ob es das 
      // Kind gibt, bevor die Löschung erfolgt 
      spConsoleNameSpace->DeleteItem(m_scopeDataItem.ID, FALSE); 
   } 
   if (!bExpand) 
   { 
      return S_OK; 
   } 
   for (COMPUTERLIST::iterator iter = m_children.begin(); 
      iter != m_children.end(); iter++) 
   { 
      CComputerNodeItem *pComputerNodeItem = *iter; 
      pComputerNodeItem->m_scopeDataItem.relativeID = 
                                         m_scopeDataItem.ID; 
      spConsoleNameSpace->InsertItem( 
                          &pComputerNodeItem->m_scopeDataItem); 
   } 
   return S_OK; 
}

 

Die Bereichsdarstellung

Wie der Windows-Explorer hat die MMC in ihrem Fenster zwei Darstellungsbereiche (Panes), um im Bild zu bleiben. Und wie aus Tabelle T1 hervorgeht, zeigt der linke Bereich eine Baumdarstellung. Um diese Anzeigefläche für TView implementieren zu können, musste ich ein Elternobjekt und Kind-Verknüpfungen zwischen den verschiedenen Knotentypen herstellen. Für die Sammlung mit den Kindern wählte ich die Listenklasse aus der STL (Standard Template Library). Zur Vereinfachung des Codes definierte ich einige Typen mit typedefs, wie in der folgenden Zeile:

typedef std::list<CComputerNodeItem *> COMPUTERLIST;

Dank dieser Typdefinition konnte ich die Computerliste ohne Angabe einer std-Namensraumkennung oder Templateargumenten benutzen. Das Datenelement wird folgendermaßen definiert:

COMPUTERLIST m_children;

Ich setzte die erforderlichen Datenelemente ein und gab den Konstruktoren die Parameter, die zur Initialisierung dieser Variablen erforderlich sind. Jeder Knoten löscht seine Kinder in seinem Konstruktor. Der entsprechende Code sieht ungefähr so aus:

for (COMPUTERLIST::iterator iter = m_children.begin(); 
   iter != m_children.end(); iter++) 
{ 
   delete (*iter); 
}

Nachdem alle Klassen und Datenelemente soweit fertig waren, bestand der nächste Schritt darin, sie in der MMC anzuzeigen. In der Bereichsanzeige lassen sich die Einträge durch die entsprechende Reaktion auf die Hinweisnachricht MMCN_EXPAND vornehmen, und zwar durch den Aufruf von IConsoleNameSpace::InsertItem. Mein CTViewSnapInItemImpl::Notify reagiert auf die MMCN_EXPAND-Nachricht mit dem Aufruf von OnExpand. Die Klasse für einen Knotentyp, der in der Bereichsanzeige Kinder hat, überschreibt OnExpand und trägt darin ihre Kinder ein (Listing L4).

Bei dieser Gelegenheit fiel mir auf, dass sich der Code für die Beschaffung des IConsole-Zeigers in den Klassen immer wiederholte. Also schrieb ich eine globale Funktion, die diesen Zeiger liefert (Listing L5).
L5 GetConsole für die Notify-Methoden

CComPtr<IConsole> GetConsole(IComponentData *pComponentData, 
   IComponent *pComponent) 
{ 
   _ASSERTE(pComponentData != NULL || pComponent != NULL); 
   if (pComponentData != NULL) 
   { 
      return ((CTViewSnapin *) pComponentData)->m_spConsole; 
   } 
   else 
   { 
      return ((CTViewSnapinComponent *) pComponent)->m_spConsole; 
   } 
}

Sie können selbst entscheiden, ob die Einträge in der Bereichsanzeige expandierbar sein sollen oder nicht. Für expandierbare Knoten setze ich m_scopeDataItem.cChildren im Konstruktor auf eins und für nicht expandierbare Knoten auf null. Die tatsächliche Zahl der Kinder wird in OnExpand durch die Zahl der InsertItem-Aufrufe bestimmt.

Der Anzeigename für die Einträge wird nicht im Konstruktor festgelegt. Er wird mit einem Rückruf über GetScopePaneInfo ermittelt, der folgendermaßen vorbereitet wird:

m_scopeDataItem.displayname = MMC_CALLBACK;

GetScopePaneInfo wird aufgerufen, sobald der Anzeigename gebraucht wird. Die Methode lässt sich in der Basisklasse implementieren und kann so einfach sein wie das folgende Beispiel:

STDMETHOD(GetScopePaneInfo)(SCOPEDATAITEM *pScopeDataItem) 
{ 
   pScopeDataItem->displayname = m_bstrDisplayName; 
   return S_OK; 
}

Die Bilderliste für die Bereichsanzeige wird in CTViewSnapin::Initialize festgelegt. Ich habe den Code, den der Wizard zum Laden der Bilderliste generiert, in einer Funktion untergebracht, damit er sich leichter wiederverwenden lässt (Listing L6). Die Auswahl eines bestimmten Bildes aus der Liste erfolgt im Konstruktor der Item-Klassen durch die entsprechende Initialisierung von m_scopeDataItem.nImage und m_scopeDataItem.nOpenImage.

L6 Die Funktion LoadImage

HRESULT LoadImageList(IImageList *pImageList) 
{ 
   HRESULT hr = E_FAIL; 
   HBITMAP hBitmap16 = LoadBitmap(_Module.GetResourceInstance(), 
      MAKEINTRESOURCE(IDB_NODE_16)); 
   if (hBitmap16 != NULL) 
   { 
      HBITMAP hBitmap32 = LoadBitmap(_Module.GetResourceInstance(), 
         MAKEINTRESOURCE(IDB_NODE_32)); 
      if (hBitmap32 != NULL) 
      { 
         hr = pImageList->ImageListSetStrip((long*) hBitmap16, 
              (long*) hBitmap32, 0, RGB(0, 128, 128)); 
         if (FAILED(hr)) 
         { 
            ATLTRACE(_T("IImageList::ImageListSetStrip failed\n")); 
         } 
      } 
   } 
   return hr; 
}

 

Die Ergebnisdarstellung

Mein nächster Schritt war die Implementierung der Ergebnisdarstellung für TView. Normalerweise erscheinen Einträge, die sich in der Bereichsdarstellung zeigen, auch in der Ergebnisdarstellung. Wenn Sie in der Ergebnisdarstellung aber mehr zeigen wollen, als in der Bereichsdarstellung sichtbar wird, müssen Sie die Hinweisnachricht MMCN_SHOW auswerten. Ich habe die Funktion OnShow geschrieben, um auf diesen Hinweis von der Notify-Funktion reagieren zu können. In OnShow trage ich die zusätzlichen Spalten in die Ergebnisdarstellung ein, es sei denn, ich komme mit der Standardspalte "Name" aus. Wenn ich zusätzliche Spalten einfüge, muss ich in GetResultPaneColInfo die Daten über jede Spalte liefern. Die Umgebungsvariablen, Handles, Speicher und Module tauchen nicht in der Ergebnisdarstellung auf, was mir in OnShow etwas zusätzliche Arbeit einbrachte (Listing L7). Zuerst lösche ich in OnShow die alte Liste, sobald der Ordner angewählt wird, und besorge mir dann die Informationen vom Server. Den vollständigen Quelltext finden Sie auf der Begleit-CD in der Datei modulefolder.cpp. Die Funktion DeleteResultsQuickly ruft IResultData::DeleteAllRsltItems auf, wobei sie eine ständige Aktualisierung des Fensters verhindert. Dadurch werden die Einträge in der Ergebnisdarstellung wesentlich schneller gelöscht.

L7 Die Funktion OnShow ist für den rechten Darstellungsbereich zuständig

HRESULT CModuleFolderItem::OnShow(BOOL bShow,  
   HSCOPEITEM /*hScopeItem*/, IConsole* pConsole) 
{ 
   CComQIPtr<IHeaderCtrl, &IID_IHeaderCtrl> spHeader(pConsole); 
   CComQIPtr<IResultData, &IID_IResultData> spResultData(pConsole); 
   spResultData->ModifyViewStyle((MMC_RESULT_VIEW_STYLE) 0, 
      MMC_NOSORTHEADER); 
   DeleteResultsQuickly(pConsole); 
   IDataObjectPtr spDataObject; 
   GetDataObject(&spDataObject, CCT_SCOPE); 
   pConsole->UpdateAllViews(spDataObject, 0, 0); 
   for (MODULELIST::iterator iter = m_children.begin(); 
      iter != m_children.end(); iter++) 
   { 
      delete (*iter); 
   } 
   m_children.clear(); 
   if (!bShow) 
   { 
      return S_OK; 
   } 
   spHeader->InsertColumn(0, L"Name", LVCFMT_LEFT, 100); 
   spHeader->InsertColumn(1, L"Address", LVCFMT_RIGHT, 100); 
   spHeader->InsertColumn(2, L"Path", LVCFMT_LEFT, 200); 
   spHeader->InsertColumn(3, L"Version", LVCFMT_LEFT, 100); 
   spHeader->InsertColumn(4, L"Description", LVCFMT_LEFT, 200); 
   RecordsetPtr pRecordset = CallTView<_RecordsetPtr, GetModules> 
      (m_pParent->m_pParent, GetModules(m_pParent->m_processID)); 
   FieldsPtr pFields = pRecordset->Fields; 
   pRecordset->MoveFirst(); 
   while (pRecordset->adoEOF == VARIANT_FALSE) 
   { 
      DWORD hModule = (long) pFields->Item[L"id"]->Value; 
     _bstr_t bstrModuleName = pFields->Item[L"name"]->Value; 
     _bstr_t bstrModulePath = pFields->Item[L"path"]->Value; 
     _bstr_t bstrVersion = pFields->Item[L"version"]->Value; 
     _bstr_t bstrDescription = pFields->Item[L"description"]->Value; 
      m_children.push_back( 
         new CModuleNodeItem(this, hModule, bstrModuleName, 
         bstrModulePath, bstrVersion, bstrDescription)); 
      pRecordset->MoveNext(); 
   } 
   for (iter = m_children.begin(); iter != m_children.end(); iter++) 
   { 
      CModuleNodeItem *pModuleNodeItem = *iter; 
      spResultData->InsertItem(&pModuleNodeItem->m_resultDataItem); 
   } 
   return S_OK; 
}

Die OnShow-Funktion sorgt dafür, dass der gewünschte Text in der Ergebnisdarstellung erscheint. Aber sie braucht auch Bilder. Die Ordner bearbeiten die Hinweisnachricht MMCN_ADD_IMAGES, um ihre Kinder mit Bildern zu versorgen. Implementiert wird dies in der Basisklasse, und zwar in der virtuellen Funktion OnAddImages. Sie ruft für die Ergebnisbilder die LoadImageList-Funktion auf. Die Kinder müssen die Ordnungszahl des gewünschten Bildes angeben, und zwar entweder im Konstruktor (bei Einträgen, die nur in der Ergebnisdarstellung sichtbar werden) oder in GetResultPaneInfo (wenn die Einträge auch in der Bereichsdarstellung erscheinen). Im Elternknoten der Ordnereinträge habe ich keine Bilderliste festgelegt, damit er mit dem üblichen Ordnerbild dargestellt wird.

Es war nicht so leicht, überhaupt herauszufinden, was die Elterneinträge erledigen und was von den Kindern getan werden muss. Die Eltern bauen die Spalten in die Ergebnisdarstellung ein und besorgen die Bilderlisten für ihre Kinder. Die Kinder entscheiden, welche Strings und welche Bilder in der Ergebnisdarstellung auf ihren Zeilen erscheinen.

 

Ein Doppelklick auf die Ergebnisdarstellung

Es scheint offensichtlich zu sein, dass ein Doppelklick auf ein Bildchen in der Ergebnisdarstellung dieselbe Wirkung haben sollte wie ein Doppelklick auf den gleichen Eintrag in der Bereichsdarstellung. Allerdings ist das nicht das Standardverhalten der MMC, weil nicht alle MMC-Snap-Ins es so wollen. Damit es tatsächlich so geschieht, ist also etwas zusätzlicher Code erforderlich. Zum Glück brauchte ich mich nur um die Hinweisnachricht MMCN_DBLCLICK zu kümmern und spConsole->SelectScopeItem aufzurufen. Diese Hinweisnachricht wird in der Basisklasse der Einträge als OnDblClick verarbeitet.

Der Doppelklick funktioniert beim Wurzelordner von TView nicht immer, weil das entsprechende Snap-In mit dem Code, der für die Bearbeitung des Doppelklicks erforderlich ist, vielleicht gar nicht geladen ist. Wenn in der Bereichsdarstellung also der Eintrag Console Root angewählt ist, wird in der Ergebnisdarstellung der TView-Eintrag sichtbar. Der angezeigte TView-Eintrag stammt aus der Anmeldung des Snap-Ins und nicht von dem Code, den es ausführt. Das Snap-In wird nur geladen, wenn Sie anfangs in der Bereichsdarstellung denTView-Eintrag wählen.

L8 Die GetConsole-Funktion für anwenderdefinierte Menüs

CComPtr<IConsole> GetConsole(CSnapInObjectRootBase *pObj) 
{ 
   _ASSERTE(pObj != NULL  
            && (pObj->m_nType == 1 || pObj->m_nType == 2)); 
   if (pObj->m_nType == 1) 
   { 
      return ((CTViewSnapin*) pObj)->m_spConsole; 
   } 
   else 
   { 
      return ((CTViewSnapinComponent*) pObj)->m_spConsole; 
   } 
}

 

MMC-Standardmenüs

Die MMC bietet eine ganze Reihe von Menüpunkten an, die sich mit IConsoleVerb::SetVerbState freischalten lassen. Zu den Standardbefehlen zählen Ausschneiden, Kopieren, Einfügen, Löschen, Eigenschaften, Umbenennen, Aktualisieren und Drucken. Ich brauche aber nur Löschen für Computer und Aktualisieren für Prozesse.

Diese Standardmenübefehle lassen sich bei der Bearbeitung der Hinweisnachricht MMCN_SELECT freischalten. Durch meine Basisklasse wird aus dieser Nachricht ein OnSelect-Aufruf. Diese Funktion muss nun die gewünschten Standardmenübefehle freischalten. Etwa so:

HRESULT CComputerNodeItem::OnSelect(BOOL /*bScope*/, BOOL bSelect, 
   IConsole* pConsole) 
{ 
   if (bSelect) 
   { 
      CComPtr<IConsoleVerb> spConsoleVerb; 
      pConsole->QueryConsoleVerb(&spConsoleVerb); 
      spConsoleVerb->SetVerbState(MMC_VERB_DELETE, ENABLED, TRUE); 
   } 
   return S_OK; 
}

Wenn der Anwender nun den Standardbefehl Löschen (Delete) gibt, schickt die MMC die Hinweisnachricht MMCN_DELETE. Die Basisklasse wandelt sie in einen OnDelete-Aufruf um. Diese Funktion löscht den Computer aus der Bereichsanzeige und anschließend auch aus dem Speicher.

HRESULT CComputerNodeItem::OnDelete(IConsole* pConsole) 
{ 
   CComQIPtr<IConsoleNameSpace, &IID_IConsoleNameSpace> 
      spConsoleNameSpace(pConsole); 
   spConsoleNameSpace->DeleteItem(m_scopeDataItem.ID, TRUE); 
   m_pParent->m_children.remove(this); 
   delete this; 
   return S_OK; 
}

Die Reihenfolge ist sehr wichtig. Wenn Sie einen Eintrag erst aus dem Speicher löschen, bevor Sie die MMC über dessen Verschwinden informieren, könnte die MMC vielleicht noch einen Zugriff auf den verschwundenen Eintrag versuchen. Und das wird einen schönen Zugriffsfehler geben.

Der Befehl zum Aktualisieren (Refresh) wird in derselben Weise freigeschaltet und führt zur Hinweisnachricht MMCN_REFRESH, die in einen OnRefresh-Aufruf umgewandelt wird. OnRefresh ruft einfach nur OnExpand auf.

 

Die anwenderdefinierten Menübefehle von TView

Außer den Standardbefehlen wollte ich auch noch fünf weitere Menüpunkte haben, nämlich New Computer, Restart Computer, View System Processes, Kill Process und Debug Process. Diese Menüpunkte stehen in der Hierarchie der Bereichsdarstellung an vier verschiedenen Stellen. Ich habe die Menüpunkte in die Menüressourcen eingefügt, die in der "Kopieren/Suchen-und-ersetzen"-Phase entstanden sind.

Es gibt für die MMC 1.0 keinen Wizard, der für einen selbstdefinierten Menübefehl die entsprechende Funktion hinzufügen könnte. Ich konnte auch keine Dokumentation darüber finden, wie das überhaupt gemacht wird. Nachdem man den neuen Menüpunkt in den Ressourcen untergebracht hat, muss man die Kopf- und Implementierungsdatei um die entsprechende Funktion erweitern, die den Menübefehl ausführt. Die Funktion wird folgendermaßen deklariert:

STDMETHOD(OnNewComputer)(bool& bHandled, CSnapInObjectRootBase* pObj);

Außerdem muss die Funktion in die Befehlstabelle des Snap-Ins eingetragen werden:

BEGIN_SNAPINCOMMAND_MAP(CComputerFolderItem, FALSE) 
   SNAPINCOMMAND_ENTRY(ID_NEW_COMPUTER, OnNewComputer) 
END_SNAPINCOMMAND_MAP()

Diese ausführenden Funktionen sind auf eine IConsole-Schnittstelle angewiesen. Also schrieb ich eine weitere GetConsole-Funktion, die derjenigen ähnelt, die ich in den Notify-Methoden einsetze (Listing L8).

Die fünf Funktionen für die fünf selbstdefinierten Menübefehle liegen in den Dateien computerfolder.cpp, computernode.cpp und processfolder.cpp, die Sie alle auf der Begleit-CD finden. Listing L9 zeigt den Code für die Funktion OnRebootComputer.

L9 OnRebootComputer kann einen Computer zum Neustart veranlassen

HRESULT CComputerNodeItem::OnRebootComputer(bool& bHandled, 
   CSnapInObjectRootBase* pObj) 
{ 
   bHandled = true; 
   CComPtr<IConsole> spConsole = GetConsole(pObj); 
   try 
   { 
      CComBSTR bstrText; 
      bstrText.LoadString(IDS_CONFIRM_REBOOT); 
      CComBSTR bstrCaption; 
      bstrCaption.LoadString(IDS_CAPTION); 
      int retval = IDCANCEL; 
      spConsole->MessageBox(bstrText, bstrCaption, 
         MB_YESNOCANCEL | MB_DEFBUTTON2 | MB_ICONQUESTION, &retval); 
      if (retval == IDCANCEL) 
      { 
         return S_OK; 
      } 
      long nFlags = EWX_REBOOT; 
      if (retval == IDYES) 
      { 
         nFlags |= EWX_FORCE; 
      } 
      // Einzelner Aufruf 
      //m_pChild->GetTViewComponent()->ShutdownMachine(nFlags); 
      // mit Wiederholungsschleife aufrufen 
      CallTView<HRESULT, ShutdownMachine> 
         (m_pChild, ShutdownMachine(nFlags)); 
   } 
   catch (_com_error & error) 
   { 
      int retval = IDOK; 
      spConsole->MessageBox(CComBSTR(error.ErrorMessage()), NULL, 
         MB_OK | MB_ICONERROR, &retval); 
   } 
   return S_OK; 
}

Der Befehl Restart Computer lässt sich anwenden, wenn der ferne Computer, den Sie überwachen, neu gestartet werden soll. Nachdem er den Befehl gegeben hat, wird der Anwender aufgefordert, den Befehl zu bestätigen und zu wählen, ob es ein normaler Neustart oder ein erzwungener Neustart werden soll. Wenn sich der Anwender zum Fortfahren entschließt, wird der Befehl mit den entsprechenden Flags für den normalen Neustart oder den erzwungenen Neustart an die COM+-Komponente weitergegeben, die den Computer neu startet.

Für den Menübefehl New Computer ist ein Dialog erforderlich. Also entwickelte ich mit einem ATL-Dialogobjekt eine passende Dialogklasse. Die Dialogklasse ist ziemlich einfach, wie Sie an den Dateien computerdialog.h und computerdialog.cpp sehen (auf der Begleit-CD). Auch die Funktion, die den Menübefehl ausführt, ist ziemlich einfach, weil sie nur den Dialog anzuzeigen braucht und den Computer anschließend in die Liste aufnimmt, sobald der Anwender sein OK gibt.

Mit dem Menübefehl View System Processes lässt sich die Darstellung zwischen der Anzeige von allen Prozessen und der Anzeige der interaktiven Prozesse umschalten. Dieser Menüpunkt wird mit einem kleinen Haken dargestellt, wenn die Systemprozesse angezeigt werden. Dieser Haken lässt sich durch die Überschreibung von CSnapInItemImpl::UpdateMenuState einbauen (Listing L10). Die zuständige ausführende Funktion setzt m_bShowSystem entsprechend und sorgt mit dem Aufruf von OnExpand für die Aktualisierung der Prozessliste.

L10 UpdateMenuState hakt den Menüpunkt ab - oder auch nicht

void CProcessFolderItem::UpdateMenuState(UINT id, LPTSTR pBuf, 
   UINT *flags) 
{ 
   if (id == ID_VIEW_SYSTEM) 
   { 
      if (m_bShowSystem) 
      { 
         *flags |= MF_CHECKED; 
      } 
      else 
      { 
         *flags &= ~MF_CHECKED; 
      } 
   } 
}

Mit dem Befehl End Process lässt sich jeder Prozess beenden, einschließlich der Dienstprozesse von Windows NT. Ich habe das schon bei zahllosen Gelegenheiten tun müssen, um unerwünschte Prozesse loszuwerden. Nach dem Befehl End Process wird der Anwender zur Bestätigung aufgefordert. Wenn er sich zum Fortfahren entschließt, ruft die zuständige Funktion die COM+-Komponente auf und gibt ihr den Auftrag, den Prozess zu beseitigen. Anschließend löscht sie den Prozess aus der Bereichsdarstellung. Übrigens verschwindet der Prozess auf jeden Fall aus der Darstellung, ob die versuchte Terminierung nun erfolgreich verläuft oder nicht. Ein Prozess, der sich nicht beseitigen lässt, erscheint automatisch wieder in der Bereichsdarstellung, sobald der Anwender die Prozessliste aktualisiert.

Zu Ihrer eigenen Sicherheit sollten Sie gar nicht erst versuchen, die Prozesse csrss.exe (client/server runtime subsystem), smss.exe (session manager subsystem) oder winlogon.exe zu beseitigen, da Sie sonst wieder den hübschen blauen Bildschirm bewundern dürfen. Allerdings betrachte ich es nicht als Bug, sondern als Feature, dass sich diese Prozesse überhaupt löschen lassen. Wenn Sie mitten im laufenden Betrieb einen blauen Bildschirm simulieren müssen, um zu überprüfen, ob der Prüfling wie erwartet reagiert, ist das Löschen von winlogon.exe nicht die schlechteste Lösung.

Mit dem Befehl Debug Process können Sie den Debugger auf den gewünschten Prozess ansetzen, sobald Sie anhand der verschiedenen Ordner herausgefunden haben, welchen Prozess Sie überhaupt bearbeiten müssen. Die zuständige ausführende Funktion ähnelt derjenigen vom Menüpunkt End Process. Allerdings ruft sie eine andere COM+-Methode auf.

 

Implementierung der Sortierung

Bevor ich auf die Idee kam, eine Sortierung vorzusehen, fiel mir auf, das nach dem Klick auf die Spaltenüberschrift für Prozesse rein gar nichts geschah. Die Ursache dafür ist, dass die Einträge, die auf beiden Darstellungsbereichen erscheinen, auf beiden Seiten in derselben Reihenfolge auftauchen. Ein Klick auf die Spaltenüberschrift für Module führte allerdings zur Sortierung der Einträge nach der entsprechenden Spalte. Dabei wurde aber zwischen Groß- und Kleinbuchstaben unterschieden. Ich ziehe für die Module eine Sortierung vor, bei der nicht zwischen Groß- und Kleinbuchstaben unterschieden wird. Implementiert habe ich diese Sortierung mit Hilfe der Schnittstelle IResultDataCompare in CTViewSnapinComponent. Ich nahm IResultDataCompare in die Erbliste auf, nahm für QueryInterface den COM_INTERFACE_ENTRY vor und implementierte dann die Compare-Funktion, wie in Listing L11 gezeigt.

L11 Die Vergleichsfunktion Compare

HRESULT CTViewSnapinComponent::Compare(long /*lUserParam*/, 
   long cookieA, long cookieB, int * pnResult) 
{ 
   CSnapInItem *pA = (CSnapInItem *) cookieA; 
   CSnapInItem *pB = (CSnapInItem *) cookieB; 
   int nColumn = *pnResu< 
   RESULTDATAITEM rdiA; 
   rdiA.mask = RDI_STR; 
   rdiA.nCol = nColumn; 
   pA->GetResultPaneInfo(&rdiA); 
   RESULTDATAITEM rdiB; 
   rdiB.mask = RDI_STR; 
   rdiB.nCol = nColumn; 
   pB->GetResultPaneInfo(&rdiB); 
   *pnResult = wcsicmp(rdiA.str, rdiB.str); 
   return S_OK; 
}

Prozesse lassen sich leichter finden, wenn die Liste alphabetisch sortiert ist. Allerdings ist es gar nicht so einfach, eine dynamische Sortierung der Prozesse anhand von Klicks auf die Spaltenüberschriften zu implementieren, denn die Prozesse sind ja auch in der Bereichsanzeige sichtbar. Die MMC zeigt die Prozesse in der Ergebnisdarstellung in derselben Reihenfolge an wie in der Bereichsdarstellung. Also ist die Sortierung der Prozesse in der Bereichsdarstellung (durch entfernen und wieder einfügen) auch die einzige Möglichkeit, sie in der Ergebnisdarstellung zu sortieren. Ich entschloss mich, die dynamische Sortierung dynamische Sortierung sein zu lassen und die Prozesse einfach nur nach den Namen zu sortieren, und zwar noch vor dem Eintrag in die Bereichsdarstellung. Zu diesem Zweck schrieb ich die Klasse CSortProcessNodeItem (Listing L12). Außerdem änderte ich die Deklaration der PROCESSLIST von

typedef std::list<CProcessNodeItem *> PROCESSLIST;

auf

typedef std::list<CSortProcessNodeItem> PROCESSLIST;

Anschließend brauchte ich vor dem Einfügen der Prozesse in die Bereichsdarstellung in OnExpand nur m_children.sort aufzurufen.

Da sich die Ergebnisse nicht sortieren lassen, wenn dieselben Einträge auch in der Bereichsdarstellung zu sehen sind, wollte ich die Spaltenüberschriften auch nicht so darstellen, als eigneten sie sich zur Sortierung. Also rufe ich in OnShow die Funktion IResultData::ModifyViewStyle auf, um die Sortierüberschriften zu entfernen. Das funktioniert aber nur mit der MMC 1.2 (sie gehört zum Lieferumfang von Windows 2000 und ist für Windows NT 4.0 verfügbar). Mit den älteren MMC-Versionen geht das leider nicht.

Da ich nicht nach jedem Start die Namen der zu überwachenden Computer neu eingeben wollte, entschloss ich mich, sie in einer Datei zu speichern. Also implementierte ich IPersistStreamInit. Ich hatte früher schon eine Versuchsimplementierung dieser Schnittstelle vorgenommen, um die Speicherung einer Datei zu ermöglichen, die mit meinem Snap-In arbeitet. Auf der Begleit-CD finden Sie die nicht mehr ganz so triviale Implementierung in tviewsnapin.cpp.

L12 Die Sortierklasse CSortProcessNodeItem

class CSortProcessNodeItem 
{ 
public: 
   CSortProcessNodeItem(); 
   CSortProcessNodeItem(CProcessNodeItem *pProcessNodeItem); 
   bool operator<(const CSortProcessNodeItem & other) const; 
   bool operator==(const CSortProcessNodeItem & other) const; 
   CProcessNodeItem *m_pProcessNodeItem; 
}; 
CSortProcessNodeItem::CSortProcessNodeItem() 
{ 
} 
CSortProcessNodeItem::CSortProcessNodeItem( 
   CProcessNodeItem *pProcessNodeItem) 
{ 
   m_pProcessNodeItem = pProcessNodeItem; 
} 
// für list::sort 
bool CSortProcessNodeItem::operator<( 
   const CSortProcessNodeItem & other) const 
{ 
   return (wcsicmp(m_pProcessNodeItem->m_bstrDisplayName, 
      other.m_pProcessNodeItem->m_bstrDisplayName) < 0); 
} 
// für list::remove 
bool CSortProcessNodeItem::operator==( 
   const CSortProcessNodeItem & other) const 
{ 
   return (m_pProcessNodeItem == other.m_pProcessNodeItem); 
}

 

Wiederholung der DCOM-Aufrufe, bis es klappt...

Wenn Sie mit DCOM-Clients arbeiten, die auf einen COM+-Server zugreifen, und irgendjemand die COM+-Anwendung herunterfährt oder den Prozess dllhost.exe beendet, bricht die Verbindung zu allen DCOM-Clients ab. Die Clients bemerken spätestens beim nächsten DCOM-Funktionsaufruf, dass der COM+-Prozess nicht mehr läuft. Nun ist es im Falle von TView zwar recht unwahrscheinlich, dass es viele Clients gibt (normalerweise wohl nur einen), aber in anderen DCOM-Anwendungen könnten viele Anwender mit den entsprechenden Servern arbeiten und sich dann darüber aufregen, dass ihr Client versagt, wenn jemand den Server terminiert. Durch die Wiederholung der DCOM-Aufrufe an den Client lässt sich der COM+-Server neu starten, ohne dass die Anwender überhaupt bemerken, dass es ein Problem gab.

Wenn der COM+-Server versagt, kehrt die aufgerufene DCOM-Funktion mit einem HRESULT-Fehlercode zurück, der den Bereichscode FACILITY_RPC enthält. Sobald solch ein Fehler auftritt, können Sie die DCOM-Schnittstelle abgeben, erneut die Verbindung zur COM+-Komponente herstellen und den Funktionsaufruf dann wiederholen. Falls Sie aber Ihrem Code diesen Trick beibringen möchten, kann der Code sehr unübersichtlich werden.

Zur Lösung dieses Problems habe ich eine Template-Funktion für den erneuten Versuch geschrieben (Listing L13). Die Template-Funktion wird mit C++-Funktionsobjekten aufgerufen - also mit Klassen, die das Wörtchen operator () enthalten. Die Funktionsobjekte rufen die DCOM-Funktionen von ihrem operator () aus auf, wie in den folgenden Zeilen:

class GetProcesses 
{ 
public: 
   _RecordsetPtr operator()(ITView *pTView) const 
   { 
      return pTView->GetProcesses(); 
   } 
};

Diese Template-Funktion und das Funktionsobjekt werden wie folgt aufgerufen:

_RecordsetPtr pRecordset =  
   CallTView<_RecordsetPtr, GetProcesses>(this, GetProcesses());

Die DCOM-Funktion wird also nicht mehr direkt aufgerufen, wie in der folgenden Zeile:

_RecordsetPtr pRecordset = GetTViewComponent()->GetProcesses();

Sollte die COM+-Anwendung jetzt aus irgendeinem Grund heruntergefahren werden, merkt der Anwender es noch nicht einmal (nun, vielleicht durch eine etwas längere Wartezeit oder falls ein ernsterer Fehler aufgetreten ist). Der Einsatz solch einer Wiederholungslogik ist die beste Unterstützung für den Microsoft Cluster Server. Wenn ein Clusterserver stirbt, auf dem eine COM+-Komponente läuft, kann ein anderer Server aus dem Cluster die COM+-Komponente übernehmen. Schlägt dann ein DCOM-Aufruf fehl und wird wiederholt, so wird der Client automatisch mit dem neuen Server verbunden, ohne dass der Anwender von diesem Problem erfährt.

L13 Die Template-Klasse CallView

template <class TReturn, class TFunction> 
TReturn CallTView(CProcessFolderItem *pProcessFolderItem, 
   const TFunction & function) 
{ 
   CHourglass hg; 
   HRESULT hr = S_OK; 
   for (int nCount = 0; nCount < 2; nCount++) 
   { 
      ITViewPtr spTView = pProcessFolderItem->GetTViewComponent(); 
      try 
      { 
         return function(spTView); 
      } 
      catch (_com_error & error) 
      { 
         // Fehler 
         pProcessFolderItem->ReleaseTViewComponent(); 
         hr = error.Error(); 
      } 
   } 
   _com_issue_error(hr); 
   return TReturn(); 
} 
ITViewPtr CProcessFolderItem::GetTViewComponent() 
{ 
   if (m_spTView != NULL) 
   { 
      return m_spTView; 
   } 
   COSERVERINFO csi = { 0, NULL, NULL, 0 }; 
   csi.pwszName = m_pParent->m_bstrDisplayName; 
   OLECHAR szComputer[MAX_COMPUTERNAME_LENGTH + 1] = L++; 
   CComBSTR bstrMyComputer; 
   bstrMyComputer.LoadString(IDS_MYCOMPUTER); 
   if (wcsicmp(m_pParent->m_bstrDisplayName, bstrMyComputer) == 0) 
   { 
      DWORD nSize = sizeof(szComputer) / sizeof(OLECHAR); 
      GetComputerNameW(szComputer, &nSize); 
      csi.pwszName = szComputer; 
   } 
   MULTI_QI mqi; 
   mqi.hr = S_OK; 
   mqi.pIID = &__uuidof(ITView); 
   mqi.pItf = NULL; 
   HRESULT hr = CoCreateInstanceEx(__uuidof(TView), NULL, 
      CLSCTX_LOCAL_SERVER | CLSCTX_REMOTE_SERVER, &csi, 1, &mqi); 
   if (SUCCEEDED(hr)) 
   { 
      m_spTView.Attach((ITView *) mqi.pItf, false); 
   } 
   else 
   { 
      _com_issue_error(hr); 
   } 
   return m_spTView; 
} 
void CProcessFolderItem::ReleaseTViewComponent() 
{ 
   m_spTView = NULL; 
}

 

Die Implementierung von "My Computer"

Das Komponentendienst-Snap-In listet im Computerordner My Computer (den Arbeitsplatz) auf, wenn es zum ersten Mal gestartet wird. Und so sollte sich auch TView verhalten. Also nehme ich im Konstruktor des Computerordners den Eintrag "My Computer" vor. Dadurch ist sichergestellt, dass My Computer direkt nach dem Einbau des TView-Snap-Ins in die MMC verfügbar ist. Bevor ich in CTViewSnapin::Load eine Datei lade, lösche ich alle Computer. Das verhindert die Anzeige von My Computer, wenn ich eine Konsolendatei lade (es sei denn, My Computer ist auch in der gespeicherten Datei enthalten). Bevor ich My Computer im Aufruf von CoCreateInstanceEx einsetze, ändere ich es in den aktuellen Computernamen, das Ergebnis von GetComputerName, ab.

 

Die letzten Schritte vor dem Ziel

Im letzten Implementierungsschritt entfernte ich sämtlichen vom Wizard generierten Code, für den ich keine Verwendung hatte. Der Wizard generiert ein umfangreiches Codegerüst, das Sie zum vollen Leistungsumfang ausbauen können. Wenn Sie nicht den gesamten Leistungsumfang brauchen, so brauchen Sie auch nicht das ganze Grundgerüst. Ich denke, dass sich nach dieser Aufräumaktion wesentlich besser überschauen lässt, was im TView-Snap-In geschieht. Ich hatte selbst so meine Probleme bei der Frage, welcher Code wichtig ist und welcher nicht, bis ich die überflüssigen Teile rausgeworfen hatte.

 

Die Sache mit der Sicherheit

Sicherheit ist in einem Werkzeug, das jeden Server in die Knie zwingen kann, auf dem es läuft, ein wichtiger Aspekt.

Um TView in den Sicherheitsmechanismus einzubinden, starten sie die Komponentendienste und schalten die Autorisationsüberprüfung für die TView-Anwendung frei. Tragen Sie in den Rollen der Anwendung den Administrator ein und sorgen Sie dafür, dass unter der Administratorrolle die vorgesehenen Anwender verzeichnet sind. Zum Schluss tragen Sie die Administratorrolle in die Rollenliste der Komponente ein.

Wenn Sie die Sicherheitsaspekte der Überwachung von der Sicherheit des Endprozesses trennen möchten, können Sie eine zweite Rolle anlegen und sie unter Windows 2000 auf bestimmte Methoden anwenden. Unter Windows NT müssen Sie die COM+ (MTS)-Komponente so ändern, dass sie IObjectContext::IsCallerInRole aufruft.

 

Die übliche Fehlersuche

Der erste Schritt bei der Fehlersuche besteht darin, die korrekten Namen von den ausführbaren Dateien und die Kommandozeilenargumente zu ermitteln, denn weder der Client noch der Server stellen in diesem Fall eigenständige Anwendungen dar.

Die ausführbare Datei für die MTS-bezogene Fehlersuche unter Windows NT 4.0 ist MTX.EXE. Die Kommandozeile lautet /p:"Paketname" oder /p:{Paket-ID GUID}. Unter Windows 2000 heißt die ausführbare Datei DLLHOST.EXE und die Kommandozeile ist /Prozess-ID:{Anwendungs-ID GUID}. Die ausführbare Datei zur Fehlersuche in der MMC lautet MMC.EXE und die Kommandozeile ist der Pfad einer gespeicherten .MSC-Datei. Die ausführbaren Dateien für MTS, COM+ und MMC liegen alle im Verzeichnis WinNT\System32.

Die Tests mit der MMC 1.0 ergaben schnell einen Zugriffsfehler, bei dem es hieß, der Code an 0x00000000 habe versucht, den Speicher an 0x00000000 zu lesen. Zuerst vermutete ich, das Problem sei wohl durch eine unbedachte Typumwandlung verursacht worden. Ich sah mir auf zwei Maschinen gleichzeitig den disassemblierten Code an. Eine Maschine fuhr die MMC 1.0, die andere MMC 1.1. Irgendwann fand ich heraus, dass die MMC versuchte, die virtuelle Funktion DestroyWindow für ein CMDIChildWindow aufzurufen. Nun wusste ich also, dass der Zeiger auf das CMDIChildWnd nicht auf ein gültiges Kindfenster verwies. Aha. Nun gut. Aber ich war noch nicht fertig.

Nun musste ich noch herausfinden, ob der Zeiger auf eine ungültige Speicherstelle verwies oder ob das CMDIChildWnd-Objekt zerstört wurde. Und falls ja, wo. Einige Stunden später fand ich heraus, dass der Aufruf von IConsoleNameSpace::DeleteItem zur Zerstörung des Objekts führte. Es sah allerdings so aus, als würde ich diese Funktion korrekt aufrufen. Es könnte sich also um einen MMC-Fehler handeln, der in der MMC 1.1 behoben wurde. Da mein Snap-In aber mit der MMC-Version aus dem Windows NT 4.0 Option Pack laufen sollte, musste ich diesen Fehler anderweitig umgehen. Also überprüfe ich mit IConsoleNameSpace::GetChildItem, ob es überhaupt ein Kindfenster gibt, das gelöscht werden muss, bevor ich es lösche.

Als das endlich lief, fiel mir auf, dass die Ordner Environment, Handles, Memory und Modules ein Plus-Zeichen haben, bis man sie mit einem Doppelklick anklickt. Damit war ich allerdings überfragt, denn dieses Phänomen trat nur mit der MMC 1.0 auf.

Als ich schließlich meine ersten Versuche unter Windows 2000 unternahm, wollte TView nicht laufen. Anfangs hatte ich noch keinen Debugger installiert, so dass ich mich dazu entschloss, die COM+-Komponente mit einem einfachen JScript zu testen:

var tview = WScript.CreateObject("TView.TView"); 
var rs = tview.GetProcesses();

Als ich dieses Skript im Windows Script Host ausprobierte, erhielt ich die Meldung: "ADODB.Field: Datenwert zu groß, um mit dem Datentyp des Felds dargestellt zu werden." Ich konnte aber nicht sagen, welches Feld zu groß war. Also musste ich auf der Windows 2000-Maschine den Debugger installieren. Das Problem trat nur in der Lieferversion auf (release build). Also musste ich noch die Debug-Symbole für die Lieferversion installieren, um die boshafte Codezeile zu finden. Wie sich herausstellte, hatte ich die DATE-Variablen (dabei handelt es sich um Gleitkommazahlen mit doppelter Genauigkeit) nicht mit 0.0 initialisiert. Und der Standardwert in der Lieferversion war NaN (not a number).

 

Ein Wort zum Schluss

In diesem Artikel habe ich beschrieben, wie ich TView entworfen, implementiert und entwanzt habe. Ich hoffe, dass Ihnen diese Schilderung bei der Entwicklung eigener MMC-Snap-Ins von Nutzen sein wird. Wenn Sie möchten, können Sie den Code, den ich Ihnen hier vorgestellt habe, für die Entwicklung eigener Verwaltungsprogramme benutzen, die sich nahtlos ins Windows 2000 einfügen.