MMC: Designing TView, a System Information View...

We were unable to locate this content in de-de.

Here is the same content in en-us.

This article may contain URLs that were valid when originally published, but now link to sites or pages that no longer exist. To maintain the flow of the article, we've left these URLs in the text, but disabled the links.
MSDN Magazine

MMC: Designing TView, a System Information Viewer MMC Snap-in

Tom Boldt
This article assumes you�re familiar with Visual C++ and ATL
Level of Difficulty     1   2   3 
Download the code for this article: TView.exe (237KB)
Browse the code for this article at Code Center: TView
SUMMARY Windows 2000 provides remote management tools running in the Microsoft Management Console (MMC), but if you're running Windows NT 4.0 you can create your own remote management tools by writing an MMC snap-in. This article walks through the development of a distributed process management utility, TView, which is similar to Process Viewer or Task Manager. The tool consists of an MMC snap-in, a COM+ component, and a kernel-mode driver. The DCOM interface, TView's access to system processes and information, and debugging of TView are discussed in detail.
W indows NT® Server 4.0 comes with many utilities for remote management, including the performance monitor, event log viewer, and server manager. In Windows® 2000, these remote management utilities have moved to one central location: the Microsoft® Management Console (MMC). Though MMC is not new to Windows 2000, in Windows 2000 you can manage many remote servers from a single application on a single computer. Another advantage of MMC is the ability to extend it with your own custom snap-ins. An MMC snap-in is the perfect way to ship your custom remote management tools because it integrates well with the Windows 2000 remote management utilities. MMC is not limited to Windows 2000. Microsoft Transaction Server (MTS), Internet Information Server (IIS), and Microsoft SQL Server™ use MMC on Windows NT 4.0, and so can you.
      In this article, I will walk you through the design, implementation, and debugging of TView, a utility similar to Process Viewer (sometimes called PView) or Task Manager. The article has two purposes: to help you write MMC snap-ins and to share the useful TView tool. The current version of MMC is 1.2, and version 2.0 is in the works. I wrote this code with MMC 1.0, so the project is offered on an unsupported, as-is basis. Why did I use 1.0? I needed to support Windows NT as well as Windows 2000 in this particular project. If you install the Windows NT 4.0 Option Pack, you get MMC 1.0. MMC 1.1 and 1.2 come with the Platform SDK and products such as SQL Server 7.0. You should use MMC 1.1 or 1.2 if your clients support it.

TView Overview

      TView helps identify and fix bugs during development, identifies configuration problems, monitors middle-tier servers, and terminates misbehaving processes. There are five folders (or views) that display process information: the Memory folder, the Modules folder, the Environment folder, the Handles folder, and the Processes folder.
      The Memory folder can be used to find out which virtual address ranges are in use or free. I have used this to find a new unique base address for a DLL, to find a virtual address for a memory-mapped file that had to be loaded at the same virtual address in every process, and to find out how much of a DLL is loaded into physical memory (the working set). I have also used this folder to identify a performance problem, in which an entire DLL was getting loaded into physical memory even though none of its functions had been called.
      The Modules folder can be used to determine the version of modules that are loaded and the directory they loaded from. I have used this to solve many configuration problems. For example, when a program works correctly on one computer, but not another, I have used TView to determine the versions of system DLLs and to identify that a DLL is being loaded from the wrong directory (for example, the debug directory instead of the release directory, or vice versa). The Modules folder can also be used to determine which dllhost.exe or mtx.exe process is the correct one by looking at which DLLs have been loaded into each process. I have used this to determine which process to debug or terminate.
      The Environment folder can be used to determine why DLLs are loaded from the wrong directory. I have used it to look at the path for a process. Even if you change the system path, the path of running processes does not change. The Environment folder can also be used to look at the command line of a process. I have used this to learn the command-line parameter for dllhost.exe (/ProcessID:{AppID}) and to determine which process to debug or terminate when there are multiple processes running with the same executable but different command lines.
      The Handles folder can be used to find handle leaks. For example, you might need to find a RegOpenKeyEx that was not matched with a RegCloseKey.
      The Processes view can be used to determine which processes are consuming resources (memory, handles, and CPU). I have used this to determine which processes are consuming the majority of my 64MB or 128MB of memory and causing my system to slow down due to swapping. As you can see, TView has many uses.
      TView consists of a client and a server. The client is an MMC snap-in (see Figure 1) and the server is a COM component that runs in COM+ DLL host (or MTS) on the remote server. The MMC snap-in communicates with the COM+ component using DCOM, as shown in Figure 2.

Figure 2 MMC Snap-in Architecture
Figure 2 MMC Snap-in Architecture

      First I'll explain the DCOM interface. Then I'll discuss the implementation of the COM+ component and explain how it accesses system processes, loads the PSAPI.DLL, and gets information about handles. Since only a kernel-mode driver can get the necessary information about handles, I'll explain how I implemented one. Following that, I'll discuss the MMC snap-in, explain how to enable security for the snap-in, and describe the debugging challenges that I faced.

The DCOM Interface

      The first step in designing TView was to define the DCOM interface, ITView. The Interface Definition Language (IDL) definition of ITView is shown in Figure 3. ITView was designed to require no more than one DCOM call for each user action. ITView uses ActiveX® Data Objects (ADO) disconnected recordsets to return a large amount of data in one network round-trip. If you think ADO is only used to access databases, you should take another look at it. ADO can be very useful by itself, without a database or an OLE DB provider. ADO parameters can be used in interfaces after importing the ADO type library into the IDL file using:
importlib("msado20.tlb");
      ITView was designed to be called from C++, Visual Basic®, and Active Scripting-compatible languages. TView is written entirely in C++, but developers can reuse the COM+ component from other programming languages.
      ITView has one method for each of the eight actions that can be performed using the MMC user interface. When the Processes folder is selected, expanded, or refreshed, the information for all processes is obtained using:
HRESULT GetProcesses([out, retval] _Recordset **ppRecordset);
When the Modules folder is selected, the information for all modules is obtained using:
HRESULT GetModules([in] long processID, 
                   [out, retval] _Recordset **ppRecordset);
Similarly, when the Memory, Handles, or Environment folders are selected, the GetMemory, GetHandles, or GetEnvironment methods are called with the same parameters as GetModules.
      A process can be terminated using:
HRESULT KillProcess([in] long processID);
Similarly, a process can be debugged using DebugProcess. Debugging a process is only useful when you are monitoring your local machine because calling DebugProcess on a remote machine starts the debugger on the remote machine, not on the client machine. You also need to have a debugger installed and the COM+ component must be configured to run as the same user name as the user who is logged in. It doesn't quite fit with the other features of TView because they can be used remotely and the debug feature cannot. Even though the debug feature is only useful in certain circumstances, it is really useful when you need it.
      Finally, the remote computer can be restarted using:
HRESULT ShutdownMachine([in] long nFlags);
      It may look like GetProcesses and the other Get methods are only returning the ADO _Recordset interface. Using the magic of ADO, the entire resultset is returned using marshal-by-value (MBV). Using the _Recordset interface after getting it will not result in any additional network trips.

Implementing the COM+ Component

      Using Visual C++® 6.0, I started the ATL COM AppWizard to create a COM+ component, called tviewmts, with the Support MTS option. I then inserted an MTS ATL object called TView into the project. Since this article assumes you are familiar with ATL, I won't go into any more detail on setting up the project.
      The first step in implementing the COM+ component is to get the skeleton to compile before adding any code. The IDL file referenced the ADO type library, so it must also be referenced in the source code. ADO can be included using compiler support for COM with this statement:
#import <msado20.tlb> no_namespace rename("EOF", "adoEOF")
      The first time I tried this I had a problem with EOF. EOF is #defined as (-1) in stdio.h. I had to use the rename modifier with the #import statement to change EOF to adoEOF. Using a namespace doesn't help because EOF is a #define in stdio.h, not a constant integer. Another way of handling this would be to #undef EOF as described in the article "Implementing ADO with Various Development Languages".
      When you use #import, the HRESULT return value gets turned into a _com_error exception. The [out, retval] parameter gets turned into the return value. As soon as you start to use C++ exceptions, you must make sure that your code is exception-safe by placing a try-catch block around the code that can throw exceptions and using classes with destructors to make sure your cleanup code gets called. For COM pointers, you can use the CComPtr ATL class or the _com_ptr_t compiler support for COM class (using the_COM_SMARTPTR_TYPEDEF macro). In my case, I wanted to make sure that the process handle gets closed after opening a process. This is handled by a class in TView.cpp (available from the link at the top of this article) that calls OpenProcess in its constructor and CloseHandle in its destructor.
      For allocations, I considered using the auto_ptr class from the standard C++ library. The problem with auto_ptr is that I wanted to allocate and delete an array, but auto_ptr calls delete without the array brackets. Even though it appears to work with BYTE arrays in Visual C++ 6.0, it is not guaranteed to work with other compilers or future versions of Visual C++. Therefore, I decided to be on the safe side and write my own small class to automatically allocate and delete arrays, as you can see in Figure 4.
      After setting up the try-catch block and creating cleanup classes, you can start using ADO through compiler support for COM. The code for GetProcesses and GetModules is very similar. They each create an ADO recordset using:
_RecordsetPtr pRecordset(__uuidof(Recordset));
The compiler support for a COM statement works similarly to this statement in Visual Basic:
Dim rs as new Recordset
These statements call CoCreateInstance to create the recordset.
      After creating the recordset, the next step is to set the cursor location to client-side cursors using:
pRecordset->CursorLocation = adUseClient;
It is important to specify client-side cursors because there is no data provider available for driver-supplied cursors.
      At this point, it is possible to add columns to the recordset. Columns are appended to the Fields property of the recordset. The Fields property can be obtained using:
FieldsPtr pFields = pRecordset->Fields;
A 32-bit integer column can be added using:
pFields->Append(L"id", adInteger, 0, adFldUnspecified);
And a string column can be added using:
pFields->Append(L"name", adBSTR, 0, adFldUnspecified);
Finally, a date/time column can be added using:
pFields->Append(L"start", adDate, 0, adFldUnspecified);
      For each Append statement, the name of the column is specified, followed by the type of data stored in that column. The third parameter is the size, which doesn't need to be specified for any of the three data types I have used. The last parameter is the attributes of the field, which also don't need to be specified.
      After the columns have been added, the disconnected recordset is opened using:
pRecordset->Open(vtMissing, vtMissing, adOpenStatic,
   adLockBatchOptimistic, adOptionUnspecified);
The first parameter of Open is the source, which could be an ADO command object or a string, such as a SQL statement. The second parameter is the connection, which could be an ADO Connection object or a connection string. Neither of these first two parameters is required for a disconnected recordset, so vtMissing is used. The predefined vtMissing constant is used for a VARIANT with vt=VT_ERROR and scode=DISP_E_PARAMNOTFOUND. This is a way to tell a Visual Basic-based program that an optional parameter is not being used. The last parameter can be left unspecified because it is used for source and connection options. So, only the third parameter (cursor type) and the fourth parameter (lock type) must be specified for disconnected recordsets. The adOpenStatic constant specifies a static copy of the records, and adLockBatchOptimistic specifies batch updates. These options, along with adUseClient, tell ADO to keep a copy of the data on the client, allow the client to scroll through the data, and allow the client to change the data without notifying the server. That is, these options tell ADO to marshal the entire recordset to the client by value.
      New rows can now be added using:
pRecordset->AddNew();
The data in each row can be set using statements such as:
pFields->Item[L"id"]->Value = (long) processID;
pFields->Item[L"name"]->Value = szExe;
pFields->Item[L"start"]->Value = vCreationTime;
      The five Get methods call various PSAPI and Win32® functions, and even some kernel-mode and undocumented functions to fill in the ADO recordset. The complete source code can be downloaded from the link at the top of this article.

TView's Access to System Processes

      If you use the Processes tab in the Windows NT or Windows 2000 task manager and you choose the End Process context menu option when you have a Windows NT service selected (such as inetinfo.exe), you will get an Unable to Terminate Process message that says "The operation could not be completed. Access is denied." The inetinfo.exe process will not be terminated. The end process option in TView would not be very useful if it had this same limitation, since the service processes are more important than desktop processes when monitoring remote servers. Using TView, the inetinfo.exe process can be terminated because TView enables the debug programs (SE_DEBUG_NAME) privilege in the AdjustPrivilege method. AdjustPrivilege is called with SE_DEBUG_NAME and SE_PRIVILEGE_ENABLED in the COpenProcess constructor, as you can see in TView.cpp (available in the code archive for this article). The debug programs privilege can only be enabled if the user specified for the TView COM+ application has been granted that privilege.
      Note that there is a limitation with this code. AdjustPrivilege adjusts the privileges of the process token, not the thread token, so it may not work all the time with multithreaded access. I originally tried to open the thread token, but it failed with an error code of ERROR_NO_TOKEN (an attempt was made to reference a token that does not exist.) A thread does not have a token unless it is impersonating a user. Since a computer will probably be monitored from only one location at a time anyway, I just used the process token. A future version of TView might at least use critical sections to ensure the privilege stays set until it is used.

Loading PSAPI.DLL

      PSAPI.DLL contains the process status helper functions for Windows NT and Windows 2000. It is an optional file on Windows NT 4.0, and does not work on Windows 95 or Windows 98. Windows 95 and Windows 98 have the toolhelp functions instead. Windows 2000 has both the process status helper functions and the toolhelp functions as part of the standard installation. Since PSAPI.DLL is an optional file, I used the Visual C++ 6.0 delayed loading option for it in the link options. This allows the COM+ component to be loaded and registered without having PSAPI.DLL installed.
      If PSAPI.DLL is not present when one of its functions is called, a Win32 exception is thrown. I catch these exceptions using the C++ catch statement. With Visual C++ 6.0, catching Win32 exceptions using C++ does not work by default. To get it to work, you must specify the /EHa compiler option. There is no checkbox for this, so it must be entered manually into the compiler options textbox. The /EHa option enables asynchronous exception handling. Visual C++ 5.0 had asynchronous exception handling as the default, with no compiler option to change it. Be careful when converting from Visual C++ 5.0 to 6.0 because the default has changed from enabled to disabled.

Getting Information about Handles

      To be a useful development tool, TView must provide as much information about a process as possible, including information that is difficult or impossible to obtain using the Win32 API. Knowing the number of handles used by a process can help you determine when there is a handle leak in a process. Once you have determined that there is a leak, you can use TView's handle view to learn which handle was not closed. There is no Win32 API to get the number of handles used by a process. In the January 1997 MSJ Under the Hood column, Matt Pietrek describes how to use NtQueryInformationProcess with the ProcessHandleCount enum to get this information. I had trouble linking with the NTDLL.LIB from the DDK as described in Matt's article, so I created my own instead. It was really quite simple. I created a DLL project called NTDLL.DLL and exported an empty function called NtQueryInformationProcess. That way I didn't have to complicate my source code with LoadLibrary and GetProcAddress.

The Kernel-mode Driver for Handles View

      If you are a fan of kernel-mode programming, you should read this section. If you prefer user-mode programming, feel free to skip to the next section, which discusses MMC.
      I wrote a kernel-mode driver to implement the Handles view. I have read a few articles (including old Nerditorium columns in MSJ) and books about kernel-mode drivers, but I had never tried writing one. A lot of useful debugging information is only available from kernel mode, and I wanted access to it. Unfortunately, the functions I wanted to call are not documented by Microsoft. Luckily, the books Windows NT/2000 Native API Reference by Gary Nebbett (New Rider Publishing, 2000) and Windows NT File System Internals by Rajeev Nagar (O'Reilly and Associates, 1997) document the functions I needed. Of course, the standard disclaimer for undocumented functions applies. Use at your own riskâ€"Microsoft may remove them in future versions of Windows 2000, or even in a service pack.
      To keep the kernel-mode driver simple, I implemented most of the logic in user mode. The kernel-mode driver has two device I/O control codes, one to get the name of a handle and one to get the type. Each one takes a process ID and a handle as input. While I could have returned the names or types of many handles in one call to kernel mode, that would have made the driver more complicated and prone to errors. As I quickly (and repeatedly) found out, errors in kernel-mode drivers cause the machine to hang or crash. Luckily, this hasn't happened using TView, except during development. It even works correctly on my dual-processor machine at work.
      I originally tried a simple driver running synchronously in the context of the calling process. All I did was call the kernel-mode routines I needed and then call IoCompleteRequest to complete the I/O Request Packet (IRP). This worked for most handle values, but for some handles it would hang the calling process and the driver. The machine had to be rebooted before the driver could be used again. This is when I reread the July 1999 NerditoriumE column in MSJ. That particular column discusses calling kernel-mode drivers asynchronously, which turns out to be just what I needed.
      I changed my code to process the IRP asynchronously by calling ExQueueWorkItem. ExQueueWorkItem is similar to CreateThread, except that it reuses a pool of kernel-mode threads that are not running in any user-mode process's address space. After a lot of trials, blue screens, and reboots, I finally got the code to work without hanging or crashing.
      My algorithm for looping through the possible handle values is imperfect because the current code just guesses possible handle values rather than calling a function that returns a list of all valid handle values. In a future version of TView, I will use ZwQuerySystemInformation (with the SystemHandleInformation enum, as described in Gary Nebbett's book) to get a list of all valid handle values. The book has an example for listing the open handles of a processâ€"exactly what TView does. The example is even implemented in user mode, but it comes with a note saying that the sample code hangs with some handle values. That sounds familiar. I guess I can't get rid of the kernel-mode code that easily. At least I can implement the call to ZwQuerySystemInformation in user mode so I won't have to debug through a long series of blue screens to upgrade the code. The kernel-mode code for getting the handle name and type won't have to change.

Creating the MMC Snap-in Project

      I used the ATL COM AppWizard to create a COM component, called tviewmmc (available from the link at the top of this article). I then inserted an MMC snap-in ATL object, called TViewSnapin, into the project. Then I compiled the project to try it out. (Note that the ATL Snap-in Wizard is an obsolete product and is not supported by Microsoft. If you use it to generate a snap-in, you're on your own!) I ran MMC, selected the Console | Add/Remove Snap-in menu option, and added the TView snap-in. When I tried saving the file, I got a message saying "Unable to open the selected file." MMC 1.0 will not let you save the console file using the wizard-generated implementation of IPersistStreamInit. I wanted to save the console file so I could debug my snap-in without having to add the snap-in every time I restarted. Later on, I noticed that this is not a problem with MMC 1.1 or MMC 1.2 (the version that comes with Windows 2000). Again, note that MMC 1.1 and 1.2 provide newer functionality than this codeâ€"check out http://msdn.microsoft.com/library/psdk/mmc/mmc12new01_1m42.htm for more details.
      I had a lot of trouble getting started with MMC snap-ins. Part of the problem was that I found the MMC COM interface names and ATL class names confusing. To clarify this for you I have prepared a table that you can refer to while reading the MMC documentation (see Figure 5).
      The other problem is that while MMC itself is well documented, the ATL support for MMC is not. The article "How Do I Add Custom Item Types to the Snap-In Object?" says: "By default, the ATL Object Wizard creates a single data class, derived from CSnapInItemImpl, for the snap-in object. In some cases, you need to implement one or more custom item types for a snap-in object." This statement confused me because it implies that it was normal to have only one node type, so having multiple node types must be out of the ordinary. This article goes on to say that there is no wizard for creating multiple node types; it must be done by hand. You may also notice that there is no wizard for adding in menu handlers, and the only documentation is the atlsnap.h header file.
      In the next sections I'll explain how I implemented persistence, multiple node types, the UI for each pane, and the standard and custom menu options for the MMC snap-in. I'll also cover sorting, saving the names of computers being monitored, retrying DCOM calls to keep the snap-in from displaying an error when the COM+ server application stops running temporarily, and implementing a My Computer node for TView.

Implementing Persistence and Node Types

      In order to be able to save a console file with MMC 1.0, I put a trivial implementation of IPersistStreamInit in CTViewSnapin. I set the class ID in GetClassID, set the size to zero in GetSizeMax, and returned S_OK for every function, as shown in Figure 6. I removed the implementation of IPersistStreamInit from CTViewSnapinComponent since I didn't anticipate the need for it.
      As I mentioned earlier in the MMC snap-in project section, the MMC Snap-in Wizard creates only one node type. One node type is probably insufficient. My snap-in uses 13 node types. I took the generated CTViewSnapinData class and moved it to separate header and source files. I then made 13 copies of these files, one for each node type. I named each file with a meaningful name and added them to my project. Thirteen identical source files are not much use, so each had to be modified to make it unique.
      Each item class has m_NODETYPE and m_SZNODETYPE static data members. It is important to initialize these data members with a unique GUID for each node type. When changing the GUIDs, make sure these two values stay in sync, since m_SZNODETYPE is supposed to be the string representation of m_NODETYPE. The new node types must be added to the registry (RGS) file, as you can see in the tviewsnapin.rgs file in the download for this article.
      The next obvious thing that needs to be changed is the class name. I decided to use class names ending with "Item," such as CRootFolderItem, instead of having them end with "Data," like the wizard-generated item classes (CTViewSnapinData). Besides, "Data" can be confused with IComponentData, the interface supported by the CTViewSnapin class.
      Each node type has different context menus, so I copied the default menu resource for each node type and gave each one a meaningful name. Each header file has a line with the SNAPINMENUID macro, which takes a menu resource ID as a parameter. I changed this parameter to match the new menu resource for each node type.
      In order to reduce the duplication between the item classes, I implemented a base item template class called CTViewSnapInItemImpl in the tviewitem.h file (found in the code download). One of the key features of this class is the override of CSnapInItemImpl::Notify. CTViewSnapInItemImpl::Notify calls virtual functions for each notification that I wanted to handle. Each derived class only needs to override the appropriate virtual function to handle a notification.

The Scope Pane

      Like Windows Explorer, MMC has two panes, and as mentioned in Figure 5, the left-hand side is a tree view called the scope pane. To implement this pane for TView, I needed to create parent and child links between the different node types. I used the Standard Template Library (STL) list class for child collections. To simplify the code, I used typedefs, such as:
typedef std::list<CComputerNodeItem *> COMPUTERLIST;
This typedef allows me to use the computer list without specifying the std namespace identifier or the template arguments. The data member is declared as:
COMPUTERLIST m_children;
      I added the necessary data members and modified the constructors to take all of the parameters necessary to initialize those variables. Each node deletes its children in its destructor using code like the following:
for (COMPUTERLIST::iterator iter = m_children.begin();
   iter != m_children.end(); iter++)
{
   delete (*iter);
}
      Once all the classes and data members were in place, the next step was to display them in MMC. Items can be added to the scope pane in response to the MMCN_EXPAND notification by calling IConsoleNameSpace::InsertItem. My CTViewSnapInItemImpl::Notify handles the MMCN_EXPAND notification by calling OnExpand. The class for each item type that has children in the scope pane overrides OnExpand to insert its child items (see Figure 7).
      I also noticed at this time that the code for getting the IConsole pointer was fairly repetitive between item classes, so I created a global function to return it (see Figure 8).
      You can choose to have each item in the scope pane expandable or not. I set m_scopeDataItem.cChildren = 1 in the constructor for expandable nodes and set m_scopeDataItem.cChildren = 0 in the constructor for nonexpandable nodes. The actual number of children will be determined in OnExpand by how many times InsertItem is called.
      The display name for an item is not set to a specific string in the constructor. It is just set to call back through GetScopePaneInfo using:
m_scopeDataItem.displayname = MMC_CALLBACK;
GetScopePaneInfo is called to get the display name for an item, so it can be implemented in the base class simply as:
STDMETHOD(GetScopePaneInfo)(SCOPEDATAITEM *pScopeDataItem)
{
   pScopeDataItem->displayname = m_bstrDisplayName;

   return S_OK;
}
The scope pane image list is set in CTViewSnapin::Initialize. I put the wizard-generated code for loading an image list into a function so it could be reused (see Figure 9). I set m_scopeDataItem.nImage and m_scopeDataItem.nOpenImage in the constructor of the item classes to select an image from the list.

The Result Pane Columns

      My next step was to implement the result pane for TView. By default, items that show up in the scope pane also show up in the result pane. However, if you want to display more information in the result pane than in the scope pane you must handle the MMCN_SHOW notification. I created the OnShow function to handle this notification from the Notify function. In OnShow, I inserted columns into the result pane, except when I only required the default "Name" column. When I added extra columns, I had to return the data in each column in GetResultPaneColInfo. The environment variables, handles, memory, and modules do not show up in the scope pane, so I had to do a little more work in OnShow (see Figure 10). I clear out the old listing and get the information from the server each time the folder is selected. For the complete code, see modulefolder.cpp in the download. The DeleteResultsQuickly function calls IResultData::DeleteAllRsltItems while preventing updates of the window. This noticeably speeds up the deletion of result items.
      The OnShow function gets the text to appear in the result pane, but I also needed images. The folder items handle the MMCN_ADD_IMAGES notification to add images for their child items. This is implemented in the base class in the OnAddImages virtual function. It calls the LoadImageList function that I created earlier for scope pane images. The child items must specify the image ordinal in either the constructor (for items that only display in the result pane) or in GetResultPaneInfo (for items that also display in the scope pane). I did not specify an image list in the parent node of the folder items in order to get the default folder image.
      At first, I found it a challenge to figure out what had to be handled by the parent items as opposed to what had to be handled by the child items. The parent inserts the columns and image lists for its result pane that contains its children. The children specify the strings and images to display for their row in the result pane.

Double-clicking in the Result Pane

      It would seem obvious that double-clicking on an icon in the result pane would have the same effect as double-clicking on the same item in the scope pane. However, this is not the default behavior because not all snap-ins want this behavior. Therefore, a little bit of code is required to get this to work. Luckily, all I had to do was handle the MMCN_DBLCLICK notification and call spConsole->SelectScopeItem. This notification was handled in the item base class as OnDblClick.
      Double-clicking does not always work for the TView (root) folder because the snap-in (with the double-click handling code) may not even be loaded. If the Console Root item is selected in the scope pane, the TView item will be displayed in the result pane. The TView item that is displayed comes from the snap-in registration, not from code executed by the snap-in. The snap-in only gets loaded when you initially select the TView item in the scope pane.

MMC Standard Menu Options

      MMC has many standard menu options that can be enabled using IConsoleVerb::SetVerbState. Standard options include Cut, Copy, Paste, Delete, Properties, Rename, Refresh, and Print. I only needed Delete for computers and Refresh for processes.
      These standard menu options should be enabled during the MMCN_SELECT notification. Using my base item class, this notification becomes OnSelect. This function must enable the standard menu option, like this:
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;
}
      When the user selects the standard Delete menu option, MMC sends the MMCN_DELETE notification. The base item class turns this into OnDelete. This method removes the computer from the scope pane, then deletes it from memory.
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;
}
The order here is important. If you delete an item from memory before letting MMC know it's gone, MMC may still try to refer to it, resulting in an access violation.
      The Refresh is enabled in the same way and results in the MMCN_REFRESH notification, which becomes OnRefresh. OnRefresh just calls OnExpand.

TView's Custom Menu Options

      In addition to the standard menu options, I also wanted five custom menu options: New Computer, Restart Computer, View System Processes, Kill Process, and Debug Process. The menu items are at four different levels in the scope pane hierarchy. I added the menu options to the menu resources that were created during the copy/search-and-replace phase.
      There is no wizard in MMC 1.0 for adding a handler for a menu option, and I couldn't find documentation on how this is done. After the menu option has been added to the resources, the handler function needs to be added to the header and source files. The function must be declared like this:
STDMETHOD(OnNewComputer)(bool& bHandled, CSnapInObjectRootBase* pObj);
The handler also needs to be added to the snap-in command map:
BEGIN_SNAPINCOMMAND_MAP(CComputerFolderItem, FALSE)
   SNAPINCOMMAND_ENTRY(ID_NEW_COMPUTER, OnNewComputer)
END_SNAPINCOMMAND_MAP()
      All of the menu handlers required an IConsole interface. I created another GetConsole function like the one I used for the Notify methods, as you can see in Figure 11.
      The five custom menu handlers are implemented in computerfolder.cpp, computernode.cpp, and processfolder.cpp (all included in the code download). Figure 12 shows the code for the OnRebootComputer function.
      The Restart Computer command can be used when the remote computer you are monitoring needs to be rebooted. The Restart Computer menu option asks the user to confirm the action, and to choose from a normal restart or a forced restart. If the user decides to go ahead, the end process handler calls the COM+ component to restart the computer, passing in the appropriate flags to restart or force a restart.
      The New Computer menu option required a dialog, so I created a dialog class by inserting an ATL dialog object. The dialog class is pretty simple, as you can see in the computerdialog.h and computerdialog.cpp files (also included in the code download). The handler is also simple because it only has to display the dialog, then add the computer to the list if the user pressed OK.
      The View System Processes menu option toggles between showing all processes and showing only interactive processes. This menu option needs a check mark beside it when showing system processes. The check mark can be displayed by overriding CSnapInItemImpl::UpdateMenuState, as you can see in Figure 13. The view system processes menu handler sets m_bShowSystem and calls OnExpand to refresh the list of processes.
      The End Process command can be used to terminate any process, including Windows NT service processes. I have used this feature on numerous occasions to kill unwanted processes. The End Process menu option asks the user to confirm the action. If the user decides to go ahead, the end process handler calls the COM+ component to kill the process, then removes the process from the scope pane. The process is removed from the scope pane whether or not the kill process succeeds. The processes that cannot be killed will reappear after the user refreshes the process list.
      For your own safety, avoid killing csrss.exe (client/server runtime subsystem), smss.exe (session manager subsystem), or winlogon.exe, as this will result in the dreaded blue screen. Actually, the ability to kill these processes is a feature. If you need to simulate a blue screen in the middle of processing to test your error recovery, killing winlogon.exe is a good way of doing this.
      The Debug Process command can be used to start up the debugger on a process after you have determined, using the different folders, which process you want to debug. The debug process menu handler is very similar to the end process handler, except for the specific COM+ method that it calls.

Implementing Sorting

      Before I implemented any sorting code, I noticed that clicking on a column heading for processes did nothing. The reason is that the items that appear in both the scope pane and the result pane show up in the same order in both panes. On the other hand, clicking on a column heading for modules sorted by the column you clicked on, but with a case-sensitive sort. I preferred to use a case-insensitive sort for the modules. I implemented this by adding support for the IResultDataCompare interface to CTViewSnapinComponent. I added IResultDataCompare to the class inheritance, added the COM_INTERFACE_ENTRY to support QueryInterface, and then implemented the Compare function, as shown in Figure 14.
      It is easier to find a process when they are sorted alphabetically. It is not easy to support dynamic process sorting based on column clicks because the processes also appear in the scope pane. MMC displays the processes in the same order in the result pane as in the scope pane. The only way to sort the processes in the result pane is to sort them in the scope pane (by removing and reinserting them). I decided not to bother with dynamic sorting and just sort the processes by name before adding them to the scope pane. I created the CSortProcessNodeItem class to support sorting by name (see Figure 15). I then changed the declaration of PROCESSLIST from
typedef std::list<CProcessNodeItem *> PROCESSLIST;
to:
typedef std::list<CSortProcessNodeItem> PROCESSLIST;
After that, I just had to call m_children.sort before adding the processes to the scope pane in OnExpand.
      Since sorting result pane items is not supported when those same items display in the scope pane, I did not want the column headers to look like they could be used for sorting. I called IResultData::ModifyViewStyle from OnShow to remove the sort headers, but this only works with MMC 1.2 (which comes with Windows 2000 and is available for Windows NT 4.0). It doesn't work with earlier MMC versions.
      To avoid having to retype the names of the computers I want to monitor every time I restarted, I decided to save them to file by implementing IPersistStreamInit. Earlier I had added a trivial implementation of this interface to allow saving a file referencing my snap-in. See the nontrivial implementation in tviewsnapin.cpp in the code download for guidance.

Retrying DCOM Calls

      If you have DCOM clients accessing a COM+ server and someone shuts down the COM+ application or terminates the dllhost.exe process, all of the DCOM clients will be disconnected. Clients that were connected will notice that the COM+ process is no longer running the next time they make a DCOM function call. With TView there are not likely to be many clients (probably one), but in another DCOM application many users could get upset that the client failed when the server was terminated. By retrying the DCOM calls on the client, the COM+ server application can be restarted without the clients knowing that there was a problem.
      If the COM+ server has failed, the DCOM function call will return an HRESULT error with an RPC facility code. When an error like this occurs, you can release the DCOM interface, reconnect to the COM+ component, and then you can repeat the function call. If you spread this logic throughout your code, it could get very messy.
      I have gotten around this problem by creating a template function for retrying, as shown in Figure 16. The template function is called with C++ function objectsâ€"classes that support operator. The function objects call the DCOM functions from their operator, such as:
class GetProcesses
{
public:
   _RecordsetPtr operator()(ITView *pTView) const
   {
      return pTView->GetProcesses();
   }
};
This template function and function object are called like this
_RecordsetPtr pRecordset = 
   CallTView<_RecordsetPtr, GetProcesses>(this, GetProcesses());
instead of calling the DCOM function directly:
_RecordsetPtr pRecordset = GetTViewComponent()->GetProcesses();
      Now, if the COM+ application is shut down for any reason, the user will never know. Adding retry logic is the best way to support Microsoft Cluster Server. When a clustered server running a COM+ component dies, the other server can take over the running of that COM+ component. After one DCOM call fails and is retried, the client will be connected to the alternate server without the user ever knowing there was a problem.

Implementing My Computer

      The Component Services snap-in lists My Computer in the computer's folder the first time you run it, so I wanted this feature for TView as well. I added the My Computer item to m_children in the computer folder item's constructor. This made sure that My Computer would be available after adding the TView snap-in to MMC. I deleted all computers before loading a file in CTViewSnapin::Load. This prevented My Computer from being displayed when loading a console file (unless the saved file contained My Computer). I checked for My Computer and changed it to the result of GetComputerName before using it in the call to CoCreateInstanceEx.

Final Steps

      The final implementation step was to remove all of the wizard-generated code that didn't do anything. The wizard generates a lot of skeleton code to allow you to implement every feature. If you do not need every feature, you do not need all that code. I think this cleanup phase makes it much easier to understand what is really happening in the TView MMC snap-in. It was not easy to tell which code was actually important before I eliminated all that clutter.

Setting Up Security

      Security is an important consideration with a tool that can crash any server on which it is installed. To enable security, run Component Services and enable authorization checking on the TView application. Add the Administrator role to the application's Roles and add the necessary users to the Administrator role. Finally, add the Administrator role to the Role Membership of the component.
      If you'd like to separate the monitoring security from the end process security, you can create a second role and apply that role to specific methods on Windows 2000. On Windows NT, you must modify the COM+ (MTS) component to call IObjectContext::IsCallerInRole.

Debugging Challenges

      The first step in debugging is to get the correct executable names and command-line arguments, since neither the client piece nor the server piece is an application on its own.
      The executable for debugging MTS on Windows NT 4.0 is MTX.EXE. The command line is /p:"Package Name" or /p:{Package ID GUID}. On Windows 2000, the COM+ executable is DLLHOST.EXE and the command line is /ProcessID:{Application ID GUID}. The executable for debugging MMC is MMC.EXE and the command line is the path to a saved .MSC file. The MTS, COM+, and MMC executables are all located in the WinNT\System32 directory.
      Testing efforts with MMC 1.0 resulted in an access violation, stating that the code at 0x00000000 tried to read memory at 0x00000000. I thought that unsafe casting may have caused the problem. I started looking at the disassembly on two computers at once, one with MMC 1.0 and one with MMC 1.1. I found out that MMC was trying to call the DestroyWindow virtual function for CMDIChildWnd. Now I knew that the pointer to CMDIChildWnd was not pointing to a valid one. Ah, but I still wasn't done.
      Now I had to figure out if the pointer was pointing to an invalid location or if the CMDIChildWnd object was getting corrupted, and if so, where. After a couple of more hours of debugging, I found that the call to IConsoleNameSpace::DeleteItem was causing the corruption. It looked like I was calling it correctly, so it may be an MMC bug that they fixed in MMC 1.1. Since I wanted my snap-in to work with the MMC version that comes with the Windows NT 4.0 Option Pack, I had to work around this bug. So I called IConsoleNameSpace::GetChildItem to determine whether there are any child items to delete before calling the function to delete them.
      As soon as I got that running, I noticed that the Environment, Handles, Memory, and Modules folders have a plus sign until you double-click on them. I left it alone since it appeared only with MMC 1.0.
      Finally, when I first tried testing on Windows 2000, TView didn't work. I didn't have a debugger installed at first, so I decided to test the COM+ component using a simple JScript®:
var tview = WScript.CreateObject("TView.TView");
var rs = tview.GetProcesses();
When I ran this script using Windows Script Host, it reported "ADODB.Field: Data value is too large to be represented by the field data type." I couldn't tell which field was too large, so I had to install the debugger under Windows 2000. The problem only occurred with the release build, so I had to add debug symbols to the release build to find the problem line of code. It turns out that I didn't initialize the DATE variables (which are double-precision floating point numbers) to 0.0, and the default value in release mode was NaN (not a number).

Conclusion

      In this article, I have described how I designed, implemented, and debugged TView. From this process, I hope you have learned how to start writing your own MMC snap-ins. Feel free to use the code I have provided to create your own remote management tools that integrate with Windows 2000.
For related articles see:
http://msdn.microsoft.com/library/periodic/period96/s402.htm
http://www.microsoft.com/msj/0197/hood/hood0197.htm
http://www.microsoft.com/msj/0799/nerd/nerd0799.htm

For background information see:
Windows NT/2000 Native API Reference by Gary Nebbett
Windows NT File System Internals by Rajeev Nagar
http://msdn.microsoft.com/library/psdk/mmc/mmcstart_1dph.htm

Tom Boldt is a senior software developer for DataMirror Corporation in Toronto, Canada where he works on iDeliver, a business-to-business integration solution. At home he works on his RPN Calculator ShareWare and various utilities. You can reach Tom at http://home.dsl.ca/~tpboldt.

From the December 2000 issue of MSDN Magazine

Page view tracker