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.
For more in this issue on WMI: |
|||||||||||||
Say Goodbye to Quirky APIs: |
|||||||||||||
Kevin Hughes and David Wohlferd | |||||||||||||
|
|||||||||||||
indows® Management Instrumentation (WMI) is a Microsoft® technology that simplifies system and hardware management by providing one consistent, standardized interface to application and device management data. WMI is built on standards set by the Desktop Management Task Force (DMTF) to make it useable across systems and platforms. For a broader view of WMI, see Jeffrey Cooperstein�s article, "Windows Management Instrumentation: Administering Windows and Applications across Your Enterprise," in this issue.
WMI also supports events. Using WMI, it is possible for a client application to be notified when certain events occur. The runtime services supported by WMI operate through the WinMgmt service (WINMGMT.EXE), also referred to as the Common Information Model Object Manager (CIMOM). These services provide certain built-in event types, but custom types can also be defined and fired by event providers. This capability makes it possible, for example, for devices to report when they are overheating, or applications to report when they are running out of resources.
The complexity of your provider will primarily depend on how much information you have available. Information for a single device will probably require just a few classes and perhaps an association or two. The total management of an e-mail or database server could require well over a hundred classes. Once the schema is defined, writing a provider for a single class is generally easy. It�s just a matter of calling the API for your component, taking the data from your structures, and passing it to WinMgmt. Why Use WMI?You may well be asking yourself why you should use WMI. After all, you already have an API that communicates with your component. Why should you add another layer on top of it? You may be surprised to learn that there are compelling reasons to do so.First, consider that every operating system component requires learning some new paradigm. As a simple example, consider the differences between enumerating files and enumerating users. Who is responsible for allocating memory? Who is responsible for freeing it? Do you use CloseHandle or CloseFile? There are just about as many answers to these questions as there are APIs. With WMI, client applications interact with these objects indirectly via consistent, well-defined, and immutable COM interfaces. Second, consider the fact that enumeration, getting and setting data, deleting instances, and creating instances are all functions common to essentially every API set, yet every API set has some unique way to handle them. For instance, there are four different ways to represent dates and times within a single API grouping. Not only is there an overlap in functionality, but there is often precious little documentation on the relationships between API sets. Can the third element in this network API structure be passed as the second parameter to that RAS function? Well, maybe, sometimes; other times you just have to try it and find out. With WMI, client applications use the standard WMI calls, and leave it up to the provider to figure out the details. WMI provides a single, consistent interface that is well-documented. This interface allows for scripting, remoting, describing the relationships between classes, and inheritance to extend existing classes with more specific data. Combine this with the fact that WMI supports events, and you have a very powerful tool. Indeed, Microsoft has chosen WMI as the standard for the management of its operating systems and applications, and I�m sure you will soon see many popular programs employing this technology. So if you want to use the WMI interface, does that mean you have to publish your proprietary interface in addition to the WMI interface? Not at all. Just publish one interface, and make it WMI. Getting StartedLet�s begin developing WMI framework providers. In particular, we will write providers using the WMI provider framework in conjunction with certain tools available through the WMI SDK (https://msdn.microsoft.com/downloads/default.asp?URL=/code/topic.asp?URL=/MSDN-FILES/028/000/015/topic.xml) that allow you to write providers quickly and easily using C++ classes instead of the COM interfaces for providers. We will focus on which methods your provider needs to support, and how to implement those methods to achieve the greatest possible performance.Prior to writing your provider, you must make a number of design decisions, most of which are dictated by what you want your provider to do, and by the role the provider will play relative to the overall schema. When defining your schema, you are really defining the specification of your provider. The schema defines what information is to be made available, what operations can be performed on that data, and relationships between different groups of data. Not only is this the first step you will take when defining your provider, it is probably the most important. When designing your provider, you should decide which class (the schema class, not any C++ class you�re using) if any to derive it from. If your schema is not related to any other schema on the machine, you should consider creating your own namespace rather than deriving your schema classes from an existing class. However, if you have an existing namespace in which other providers and classes are defined (such as the CIMV2 namespace, in which all the Win32 system classes reside), you can add your provider and classes to that namespace. You can also derive your classes from existing classes. This establishes a schema relationship between your class and others defined in the schema. The SDK contains a file named SCHEMA.TXT that will help with these decisions. Implementing a ProviderWe have chosen to illustrate provider development using classes that represent Windows NT® user accounts (MSJ_User) and groups (MSJ_Group) and the relationship between them (MSJ_GroupMembership). You should note that, because users and groups are only defined on Windows NT and Windows 2000, our provider will not operate on Windows 95 or Windows 98. In general, however, a single WMI provider can support multiple platforms.We used the WMI CIM Studio to create the namespace and schema classes to be used by our provider. To export the schema classes to a Managed Object Format (MOF) file (a standardized means of representing a schema, defined by the DMTF), we used the WMI MOF Generation Wizard. We then used the WMI Code Generator Wizard (also available through the WMI CIM Studio) to create all the files necessary to get the provider up and running. For details on how to use these tools, please see the Framework Tutorial in the WMI SDK. The files produced by the code generator include the provider MOF file, a MAINDLL.CPP file (containing provider self-registration code and the standard COM in-proc server exported functions), a makefile, a .def file, and default implementation header and .cpp files. The skeletons of the functions produced by the code generator must be filled in, but this is a good start. At this point we have created two separate MOFs: one from the WMI MOF Generation Wizard to define the classes, and a second from the WMI Code Generator Wizard that registers your provider and describes what type of provider it is. These MOFs should be combined into a single MOF for ease in distribution. Figure 4 shows these sections merged into a single MOF. You will also see some code at the top of the MOF that creates the namespace. Throughout the remainder of the article we�ll be examining the code for the sample provider. Due to the size of the listings, we recommend downloading the code from the link at the top of this article. The code is found in three flavors: raw from the code generator, basic implementation, and advanced implementation. The class MSJ_User is located in MSJ_User.cpp. As you can see from the heavily commented code produced, we now have function placeholders for GetObject, EnumerateInstances, ExecQuery, PutInstance, DeleteInstance, and ExecMethod. These six functions are the guts of your provider. They are the only entry points to your provider that the framework will call. However, you need not support them all. For an instance provider (a provider that dynamically supplies instances of a class), it is sufficient to support only GetObject and EnumerateInstances; for a method provider (a type of provider that supports the use of provider-declared functions), you need only implement ExecMethod. For our sample combination provider, however, we have included them all to illustrate how each is implemented. Before discussing these functions in further detail, a couple of additional comments about the files generated by the code generator are in order. First, look at this line from MSJ_User:
This declares a static instance of your class. This instance, which is created when your provider loads, allows the framework to map the class internally as one that it interacts with. All calls from WinMgmt involving MSJ_User will be routed by the framework through this instance. This is important when you consider that WinMgmt is a multithreaded application. It is possible that multiple calls will be made into your class concurrently, and each will be routed through this instance. Implementing GetObjectThe purpose of GetObject is to locate a specific instance of a class. When GetObject is called, the keys to a specific instance are passed in. If our schema was designed correctly, the keys uniquely identify an instance of the actual object modeled by the corresponding schema class. It is the provider�s job to assess whether an actual corresponding instance of the modeled object exists and, if it does, to fully populate all of the instance�s properties. By the time your provider�s implementation of GetObject is called, the framework will have created a new instance of the class and filled in its key properties based on the object path specified by the client application making the call to GetObjectAsync.The dynamic provider�s job is to attempt to find the actual object represented by your class. If the object is found, the function should populate the nonkey properties and return WBEM_S_ NO_ERROR; if the object cannot be found, the function should return WBEM_E_NOT_FOUND; and if some other sort of error occurs, the function should return WBEM_E_PROVIDER_FAILURE (for a generic error), or any valid Win32 HRESULT. This code illustrates a basic instrumentation of GetObject.
In this example, and in those that follow for the other methods that our provider instruments, we will attempt to separate the structure of the provider from the implementation details specific to the particular class and/or properties used in the examples. We do this to more clearly highlight those aspects of provider writing that will be common to most of the providers you write. You can see that the work of GetObject itself is straightforward. It attempts to find the instance requested. If the instance is found, it sets the nonkey properties for that instance. GetObject for Association ClassesWhen instrumenting GetObject for association classes (such as MSJ_GroupMembership), your goal is the same, but the steps are different. To say that the association exists, you need to verify two things. First, do the association class�s endpoints exist? In this case, do instances of MSJ_User and MSJ_Group really exist as specified in the association instance path? Second, is there a real association between the elements your association class models? In this example, is the specified user really a member of the specified group? Figure 5 contains the code used to make these tests. Only if both tests are true should our association class report WBEM_S_NO_ ERROR from GetObject.In implementing GetObject for MSJ_GroupMembership, we have employed the function GetInstanceKeysByPath, which is supplied to us by the framework. This function performs a GetObject on the specified class, only retrieving its key properties, thereby avoiding the overhead of obtaining unnecessary, and possibly expensive, properties. This function is one of a number of framework functions that call back into WinMgmt (see Figure 6). It is convenient to use WMI itself to instrument your classes in this manner, and it keeps our sample code clean, but you should be aware that extra overhead is generated as a result of calls back into WinMgmt. Given that the code to obtain group and user information is available, we could have cut and pasted that code directly into MSJ_GroupMembership, thereby improving our performance. This trade-off between performance and tidiness in your providers is one you will have to weigh when using GetInstanceKeysByPath or any other WinMgmt callback function. A Better GetObjectAlthough the work of GetObject seems simple enough, it is still a very important function to code efficiently. You might be lulled into thinking that this function, which returns only a single instance, is not the best candidate to spend your time optimizing. However, the class may contain certain expensive properties that may not be required by the caller. If you can avoid filling in these properties, you should.Second, WinMgmt will call this function in response to a number of different client requests such as ExecMethod or Associator queries (which relate instances of the user class to instances of other classes). In such cases, WinMgmt only needs to confirm that the object exists. Fortunately, there are simple techniques for getting the best performance from your GetObject routine. Figure 7 contains a revised version of GetObject that has been optimized to take advantage of these two situations. The first difference you will note is that the function signature for GetObject is different from the basic implementation. A new parameter, CFrameworkQuery& Query, has been included. The framework calls this overload of GetObject if the provider has implemented it; otherwise, the original GetObject is called. The CFrameworkQuery reference (which we will discuss in more detail later) can be used to obtain information regarding what the client caller really is interested in obtaining from the GetObject function call. We have also defined a DWORD, dwRequestedProperties, which will contain a bitmask of the properties requested by the client. Values for the bitmask are defined in each class�s header. We set this value in a new member function, GetRequestedProps:
GetRequestedProps is a helper function we use to encapsulate calls to the CFrameworkQuery function IsPropertyRequired, which returns true if the caller requested the property supplied as its argument. If the caller did not explicitly request the property, the function returns false. We use this function to modify a DWORD bitmask of the requested properties. Implementing EnumerateInstancesAs you might have guessed from the name, the job of the EnumerateInstances function is to return all the properties of all instances of the specified class. The implementation of the EnumerateInstances function is a whopping three lines:
We construct the calls this way to take advantage of the fact that certain variations of ExecQuery (which we will discuss later) can call the same Enumerate function. Improving EnumerateInstancesFrom a performance perspective, not much can be done to improve the basic implementation of the Enumerate function shown in Figure 8. We must return all the instances of the class, and we must fill in all the properties of each instance. However, we would still recommend two changes to this routine. They are enhancements that can be employed in the other functions we will be discussing later. Both of these changes have to do with error recovery. As the code generator-supplied comment says, the function CreateNewInstance just might throw an exception. If this happened in the basic implementation, two problems arise: we would leak the buffer pInfo, and we might fail to release pInstance.To prevent this, we have revised the code as shown in Figure 9 in the advanced implementation of MSJ_User.cpp. There you�ll see two changes: a try/catch block, and the use of a smartpointer version of CInstance, CInstancePtr. CInstancePtr is defined in the header via the standard macro _COM_SMARTPTR_TYPEDEF, which results in Release being called automatically on the encapsulated interface whenever the object goes out of scope. This includes going out of scope if an exception is thrown. Also note that the memory allocated via the call to NetUserEnum is freed in the catch block, before we throw the exception again. Advanced Implementation of ExecQueryThe best way to get the most out of your provider is to implement query support by instrumenting the framework provider function ExecQuery. Improved performance is the sole reason for the use of ExecQuery. By specifying a query, clients (and WinMgmt itself) hope to evoke optimized instance retrieval from your provider by specifying which instances they want and what properties they are interested in. Because ExecQuery is all about performance, we will not describe a basic and an improved flavor of this function; the version we will describe is representative of any ExecQuery you should write.If you do not implement this function and a client issues a query against an object supported by your provider, WinMgmt will call your EnumerateInstances function, thereby obtaining every property of every instance of the class in question. WinMgmt will then post-filter those instances and properties that do not fulfill the request. Take the example of a file system provider. You can imagine just how intolerable the performance of this approach would be. If a client were interested in obtaining a resultset containing just the names of the files on the root of the C: drive, for example, the provider would enumerate all files on all drives, including mapped network shares, and attempt to obtain all possible properties for every instance. Fortunately, supporting queries is easy using the framework. You do not need to write extensive WMI Query Language (WQL) query parsers. Easy-to-use functions are available to help you process the WHERE clause (which specifies which instances are returned) and the set of properties requested of each instance. In our sample implementation of ExecQuery we will first consider how this routine optimizes on WHERE clause expressions. The framework function GetValuesForProp is used to determine which instances of the specified class are explicitly requested by the query.
Hence, the WHERE clause
would result in GetValuesForProp returning an array of two strings: LocalComputer and Redmond. GetValuesForProp operators other than the equal sign will not result in GetValuesForProp returning any elements. ExecQuery for Association ClassesAs was the case in the implementation of GetObject for our sample association class, certain aspects of ExecQuery routines are unique to association class instance providers and bear further discussion. Note that implementing ExecQuery for association classes is a relatively advanced exercise, so we will include a brief discussion of our implementation for completeness. We recommend that you become comfortable with the other concepts discussed here before you ponder the intricacies of this code.MSJ_GroupMembership.cpp illustrates how to instrument ExecQuery for the association class MSJ_GroupMembership. The MSJ_GroupMembership association class lists the groups to which various users belongâ€"and, by implication, which users belong to various groups. The MSJ_GroupMembership association class consists of only the object paths (a string consisting of the key properties that uniquely identify that instance) to the user and the group. We chose to instrument ExecQuery for this class rather than relying on WinMgmt to postprocess the results of our EnumerateInstances function. This way we can optimize this class�s provider�s performance for certain types of queries. Specifically, if the client issues WQL queries requesting the members of a specific group or the group membership of a specific user, we can reduce the work our provider has to do to satisfy the request. In our simple example schema in which we only provide instances for local groups, the optimization is not overly significant. However, if the classes in our schema provided instances for all users and groups on all domains visible to the machine executing the query, optimization on queries such as these would be critical to our performance. Without such optimization, every query would result in all group membership instances being enumerated, potentially having all but one instance thrown away. In our instrumentation of ExecQuery, we determine what was specified in the query�s WHERE clause and decide what approach will optimally resolve it. If both users and groups have been specified, we get the users indicated by the query and check to see if each user belongs to any of the specified groups. If it does, we create a new instance of the association class, fill in its properties, and commit the instance (returning it to WinMgmt). Another type of query is where one or more groups were specified. For each such group we get its members, create instances of the association class, and commit each new instance. If one or more users were specified, we get the groups each user belongs to, create new association class instances, and commit each instance. If the query was of a form other than these three, we simply do an enumeration of the association class and allow WinMgmt to post-filter the resultset such that the query is satisfied. Advanced Implementation of ExecMethodIn representing software or hardware using a WMI schema you define, you are not limited to reporting instances of the objects you are modelingâ€"WMI also allows you to execute methods against the objects you define. For instance, a class that modeled CD-ROM drives might have an Eject method defined.Methods come in two flavors: those that operate on instances of a class (instance methods), and those that operate against the class itself (static methods). Unlike instance methods, static methods contain the "static" qualifier in the class�s MOF. An example of a static method would be a method called CountDirectories defined on a class that models file system directories (such as the class Win32_Directory) that could return a count of all the directories on the machine. This involves the class in general, not a particular instance of the class. In our sample MSJ_User class we have included an instance method called Rename, which allows you to change the name of a user account. MSJ_User.cpp contains the code for our implementation of MSJ_User�s ExecMethod function. The first thing to note about the implementation of ExecMethod is that it is the single entry point called by the framework for all methods your class supports. Therefore, your first task in implementing ExecMethod is to determine exactly which method the client called. Also potentially confusing to first-time provider writers are the return values from methods. The MOF description of the Rename method is as follows:
The uint32 return code is used to indicate whether the method performed as expected. What doesn�t appear in the MOF, however, is the HRESULT value returned to the client for the ExecMethod call. This value is used to indicate whether the provider successfully managed to call our method. PutInstance and DeleteInstanceFor instance providers, the methods GetObject and EnumerateInstances are the only functions that your provider is required to support. For method providers, ExecMethod is the only function the provider must support. As you have seen, ExecQuery, although not absolutely required, offers the greatest potential for improving the performance of your provider.There are still two functions to discuss: PutInstance and DeleteInstance. These functions are not required for any provider, although they do add functionality that your clients may expect. PutInstance actually serves two purposes: creating new instances of a class and modifying the (nonkey) properties of an existing instance. The action taken is based on the lFlags parameter of the PutInstance call:
The lFlags parameter can be one of several values. WBEM_ FLAG_CREATE_OR_UPDATE signals the provider that the caller would like to create a new object (as specified by the key properties of the Instance parameter), if such an object does not already exist. If an object already exists as specified by the key properties, this signals that the caller would like to modify the properties of that object. WBEM_FLAG_UPDATE_ONLY specifies that the caller wants to update the properties of an existing instance. It returns an error if the specified instance does not already exist. WBEM_ FLAG_CREATE_ONLY requests that a new instance should be created as specified. An error should be returned if such an instance already exists. Testing the ProviderAfter implementing the methods you have decided to support in your provider, you are ready to test it. To do so, be sure you have first done the following:
If you note any missing instances, extra instances, invalid or missing data, assertions, or other problems, you will need to debug your provider. As an in-process provider (which is what the code generator creates), you are in-proc to WinMgmt. Therefore, WINMGMT.EXE is the image associated with the process you should be debugging. It is a good idea to be logged in as an administrator on the machine you will be debugging on because WinMgmt runs as a system service. As an administrator, you will have the permissions to debug that process. By default, only administrators are granted full access to WMI. If you are debugging using Visual Studio, fire it up, then attach to WINMGMT.EXE. In the Project/Settings dialog, add your provider as well as FRAMEDYD.DLL to the list of additional DLLs you will be debugging. Attaching to WINMGMT.EXE this way attaches you to the process as it runs under the local system account. When you halt your debug session, you will stop the WinMgmt service. To start debugging again, you should first restart the service (you can issue the command "net start Winmgmt" from a command prompt), then reattach. You can also debug your provider by starting WinMgmt under the debugger (with the F5 key in Visual Studio). This will start WinMgmt under the context of the user currently logged on rather than as a system service. This may be fine for you as the debugger, but be aware that this is not how WinMgmt will be running on most machines that will be using your provider. As another variation on debugging, if you elect to start WinMgmt from the debugger, you can specify the command-line switch /EXE, which will cause WinMgmt to run as an executable rather than as a service. You can now set breakpoints where appropriate in your modules, most likely at the entry points to GetObject, EnumerateInstances, ExecQuery, and the other functions we have discussed. Your first task in testing your provider should probably be to run an enumeration of your class�s instances, which you would request by clicking the Enumerate Instances button on CIM Studio. When you do this, you should break into the debugger at your EnumerateInstances function. If this does not happen, it is likely due to one of the following:
If you successfully see all the instances of your class, you should attempt to do a GetObject on each instance. You can do this using your own client code or a tool such as WBEMDUMP.EXE, which ships with the WMI SDK. With WBEMDUMP.EXE, using the /G option when requesting an enumeration will perform a GetObject on each enumerated instance. You should make sure that you cannot retrieve bogus instances from GetObject by specifying the path of an object that does not really exist. You should also test case sensitivity in your GetObject code. In general, object paths should be case-insensitive, so you should be able to specify any combination of upper or lower case in your object path and still retrieve the proper instance. In the case of classes with complex keys (that is, more than one key, such as our MSJ_Group and MSJ_User classes, which each have two key properties, Domain and Name), you should test that the order in which the keys are specified does not affect the provider�s behavior. To test ExecQuery, you should specify as many combinations of WHERE clauses and properties as you have provided optimized support for in your provider, and confirm that your code follows the optimized code path when it should rather than falling into an enumeration. Testing PutInstance, DeleteInstance, and ExecMethod are all very straightforward, and follow the same pattern. Test that they work for valid data, and that they fail for invalid data, returning the proper error codes in each case. For instance, attempt to execute a method that doesn�t existâ€"you can�t use CIM Studio to do this, but you could use a Visual Basic® script launched from the command line, for instanceâ€"and make sure that your provider fails gracefully with the proper error. Attempt to execute a valid method with invalid method parameters, and check for the same behavior. Finally, prior to shipping your provider, be sure to recompile it as a release build, and link to FRAMEDYN.DLL (the release version of the framework). You should then install everything, including your MOF, on a clean machine and retest. ConclusionWMI provides a means for client applications, whether they are scripts written in VBScript or applications written in C++, to interact with software and hardware elements using a published, formalized schema. WMI providers, in combination with the WinMgmt service, make this interaction possible.It is easy to write a WMI provider using the tools available through the WMI SDKâ€"namely, the CIM Studio (to define your classes), the CIM Studio MOF Generator (to produce the document that allows you to install your schema on other machines), and the WMI Code Generator. With the information and samples provided in this article, in combination with the WMI SDK, you are well equipped to use this exciting new tool in a wide array of applications. |
|||||||||||||
For related articles see: From the May 2000 issue of MSDN Magazine. |