Welcome to Foxite.COM Community Weblog Sign in | Join | Help

WMI part 4 - Implementing a temporary WMI Event consumer in VFP

All the WMI queries so far in this series have been looking at static data, but WMI has a mechanism for checking for changes to local or remote computers and reacting to them: WMI events. Applications can register themselves as event consumers, and receive notifications of these changes. This article shows how, in VFP6 or later.

Extrinsic Events and Semi-Synchronous Queries

There are two main types of WMI events. The simpler ones are the extrinsic event classes. These report on events outside WMI's domain, and consequently have to have an event provider, which alerts WMI when the event takes place. Windows XP and 2003 introduce lots of fun new classes and event providers but only those available in Windows 2000 are used in this article (many are also available in Win 9x and/or NT). The power management event provider (2000+) and the registry event provider are the two main extrinsic event classes available across the spectrum of Windows versions.

Using a method of the WMI service object - ExecNotificationQuery - WMI is instructed to go away and wait for something to happen with our selected event class, and to come back and tell us when it does.

WMI event sample code usually uses this semi-synchronous method for event queries: the obvious attraction is that the syntax is compellingly simple. The downside (in a single-threaded environment like Foxpro at least) is that it blocks the calling application until the event is received, the call reaches it's (optional) timeout, or the application is forcibly terminated: whichever is the earliest. Running the following code sample completely blocks Foxpro for as many milliseconds are specified as the timeout, unless a power management event occurs in that time (you will need a laptop running Windows 2000 or later to generate an actual power management event.)
** Example of semi-synchronously querying an extrinsic event
** If you can't generate a power event, you need to wait for the timeout.

oWMILocator = createobject("WbemScripting.SWbemLocator")
oWMI = oWMILocator.ConnectServer(".", "root\cimv2")

cQuery = "select * from WIN32_PowerManagementEvent"
oResults = oWMI.ExecNotificationQuery(cQuery)
* Timeout in milliseconds...
nTimeout = 10000
oEvent = oResults.NextEvent(nTimeout)
* ... 12289 Error results if no event occurs within the timeout.
do case
	case oEvent.EventType = 4
		? "Entering Suspend"
	case oEvent.EventType = 7
		? "Resume from Suspend"
	case oEvent.EventType = 10
		? "Power Status Change" && mains <-> battery
	case oEvent.EventType = 11
		?  "OEM Event"
	case oEvent.EventType = 18
		? "Resume Automatic"
endcase

Asynchronous Queries and Sink Objects

Blocking behaviour like this effectively useless, so it's just as well that there is another way - as long as we are querying our local computer, we can use asynchronous queries instead. Asynchronous calls to other computers are strongly discouraged due to security considerations and may fail because of them. There is a workaround for semi-synchronous queries, unsurprisingly involving worker threads, which I will come to later.

The ExecNotificationQueryAsync method is used for asynchronous event queries. There are two required parameters: a notification query to execute, and a WMI sink object to receive the event notification from WMI. WMI provides the sink class, all we need to do from VFP is create a VFP sink object and bind it to the WMI sink's events. In VFP7 and later, the intrinsic VFP EventHandler function can be used, but for VFP6 the VFPCom utility is required. The latest version of VFPCom works with VFP6: you'll see in the code later that the sink class syntax differs slightly when using VFPCOM to bind the two objects together.

The WMI sink's OnObjectReady event fires when the notification query finds a hit, and receives the class we are monitoring. OnComplete fires when the query ends, or is cancelled (the WMI sink object has a Cancel method which is used to cancel all asynchronous queries bound to it).

Intrinsic Events

The second type of WMI events, intrinsic events, allow monitoring any of the WMI classes (representing hardware, Windows components, programs, etc) and raise an event as required when an instance of the target class is 'created', 'modified', or 'deleted' - the precise nature of the actual event that generates this instance depends on the class context. The query syntax is a bit more complex for this type of events.

As an example, a simple query would monitor for a new process (i.e. program) being started on the computer. In the query, we need to request instances of a WMI system class, in this case __InstanceCreationEvent, to generate the event when a new instance of the process class is created by a program being started. We also need to specify a polling interval for the WITHIN clause, which is mandatory for event queries which don't have an event provider (an error is raised if it's omitted). The interval is the maximum amount of time in seconds that can elapse before notification of an event must be delivered: the right value to use depends on what you are monitoring. Higher values will receive notifications less frequently, low values (less than 1) affect system and application performance and may eventually generate an error.

Intrinsic event queries must have a WHERE clause: at the least, the event class's TargetInstance property must be the target class name as specified by the ISA operator (instances of all subclasses of the specified class are monitored). A more specific where clause will result in better performance: it must follow the "property operator value" syntax throughout though. Note that the where clause cannot reference any array properties the class may have.

Put these pieces together and this valid query is the result: it checks every three seconds for a new process being started.
select * from __InstanceCreationEvent within 3 where TargetInstance ISA 'Win32_process'
Now we can call the ExecNotificationQueryAsync with the WMI sink object and query. Whan a new process is created, the sink object's OnObjectReady event fires, the instance creation object is received, and has a reference to the new object in it's TargetInstance property. Note that WMI will create a process of it's own - unsecapp.exe - to handle the callback from the asynchronous method call.

Event catching class

If either sink variable goes out of scope, they are automatically unbound, so it's useful to encapsulate them in a class. This sample code uses a general event catching class to watch for new processes being started. There are two versions, one suitable for use in VFP7 or later and one for use in VFP6 or (possibly) earlier which requires the VFPCom utility from Microsoft. Subsequent examples will only show the newer sink syntax.
** Demo query: when a new process is started, echo it's WMI path to the screen.
* RELEASE oEventCatcher in the command window when you tire of receiving events.

public oEventCatcher
* Change class to eventcatcher6 for VFP6 version
oEventCatcher = createobject("eventcatcher")

cQuery = [select * from __InstanceCreationEvent within 3 where TargetInstance ISA 'Win32_process']
oEventCatcher.CatchEvents(cQuery)

return

**
** Event catching class
**

* VFP7+ version

define class eventcatcher as relation

	oSink = .null.
	oWbemSink = .null.
	oWMI = .null.

	procedure init

		this.oWMI = getobject("winmgmts:")
		* Create the sink objects and bind them
		this.oWbemSink = createobject("wbemscripting.swbemsink")
		this.oSink = createobject("vfpsink")
		eventhandler(this.oWbemSink, this.oSink)

	endproc

	procedure destroy

		if vartype(this.oWbemSink) = "O"
			this.oWbemSink.Cancel()
		endif

	endproc

	procedure CatchEvents
		lparameters cQuery
		* Send the WMI sink not the VFP one.
		this.oWMI.ExecNotificationQueryAsync(this.oWbemSink, cQuery)
	endproc

enddefine

** The sink class is easier to manage when decoupled from the eventcatcher class

define class vfpsink as relation

	implements ISWbemSinkEvents in "WbemScripting.SWbemSink"

	procedure ISWbemSinkEvents_OnObjectReady(oObject, oAsyncContext)
		* Assign the TargetInstance object reference to a property at this point.
		? oObject.TargetInstance.Path_.Path
	endproc

	procedure ISWbemSinkEvents_OnCompleted(nResult, oErrorObject, oAsyncContext)
	procedure ISWbemSinkEvents_OnProgress(nUpperBound, nCurrent, cMessage, oAsyncContext)
	procedure ISWbemSinkEvents_OnObjectPut(oObjectPath, oAsyncContext)

enddefine

** VFP6- version

define class eventcatcher6 as relation

	oSink = null
	oWbemSink = null
	oWMI = null
	oVFPCom = null

	procedure init

		oWMILocator = createobject("WbemScripting.SWbemLocator")
		this.oWMI = oWMILocator.ConnectServer(".", "root\cimv2")
		* Create the sink objects and bind them
		this.oWbemSink = createobject("wbemscripting.swbemsink")
		this.oSink = createobject("vfpsink6")
		this.oVFPCom = createobject("vfpcom.comutil")
		this.oVFPCom.BindEvents(this.oWbemSink, this.oSink)

	endproc

	procedure destroy

		if vartype(this.oWbemSink) = "O"
			this.oWbemSink.Cancel()
		endif

	endproc

	procedure CatchEvents
		lparameters cQuery
		this.oWMI.ExecNotificationQueryAsync(this.oWbemSink, cQuery)
	endproc
enddefine


define class vfpsink6 as relation

	procedure OnObjectReady
		lparameters oObject, oAsyncContext
		? oObject.TargetInstance.Path_.Path
	endproc

	procedure OnCompleted
		lparameters nResult, oErrorObject, oAsyncContext
	procedure OnProgress
		lparameters nUpperBound, nCurrent, cMessage, oAsyncContext
	procedure OnObjectPut
		lparameters oObjectPath, oAsyncContext

enddefine

Extending the event catcher

One scenario an application might have to deal with is starting a process, and waiting for it to terminate. We can do this using the __InstanceDeletionEvent class. This still receives a TargetInstance reference (it is to a copy of the deleted object.)
public oEventCatcher
oWMILocator = createobject("WbemScripting.SWbemLocator")
oWMI = oWMILocator.ConnectServer(".", "root\cimv2")
oEventCatcher = createobject("eventcatcher")
oProcessClass = oWMI.Get("Win32_Process")

* Can't just call the static Create method as we won't get a reference 
* to the new process: another approach is needed:
* use the OutParameters object's Process ID property 

* First create the InParameters object:
oInParameters = oProcessClass.Methods_("Create").InParameters.SpawnInstance_

* Set the required command line property for the process to be monitored:
oInParameters.CommandLine = "notepad.exe"

* Now call the method using ExecMethod which returns the OutParameters object:
oOutParameters = oWMI.ExecMethod("WIN32_process", "Create", oInParameters)

* Now we can construct the event query which will fire when this
* specific process ends... no idea what's with Windows 9x here,
* but the (-ve) process ID needs tweaking, at least under Virtual PC.

cQuery = "select * from __InstanceDeletionEvent within 1 "
cQuery = cQuery + " where TargetInstance ISA 'Win32_Process' and TargetInstance.ProcessID="
if os() = "Windows 4"
	nProcessID = 4294967296 + oOutParameters.processID
else
	nProcessID = oOutParameters.processID 
endif

cQuery = cQuery + str(nProcessID)
? cQuery

* Execute the query.
oEventCatcher.CatchEvents(cQuery)
There is one more intrinsic event class, __InstanceModificationEvent: this adds a PreviousInstance property, which has a reference to the object before it was modified. This is very useful for detecting specific changes: the where clause must still follows the property-operator-value syntax though: a query containing "where TargetInstance.Property <> PreviousInstance.Property" will result in an Unparsable Query error.

This query watches for a disk being inserted into a CD drive:
select * from __InstanceModificationEvent within 1 where TargetInstance ISA 'Win32_CDROMDrive' 
and TargetInstance.MediaLoaded = True and PreviousInstance.MediaLoaded = False 
Another example watches installed printers for a change of orientation:
select * from __InstanceModificationEvent within 1 where TargetInstance ISA 'Win32_PrinterConfiguration' 
and (TargetInstance.Orientation = 1 and PreviousInstance.Orientation = 2
or TargetInstance.Orientation = 2 and PreviousInstance.Orientation = 1)
What you look for with a WMI event query depends on your application's needs: the extrinsic power management query we used at the beginning of the article will work asynchronously (the receiving sink class would have to change as it will receive the Win32_PowerManagementEvent class instead of one of the intrinsic event classes, so would not then need to reference TargetInstance.) An application could augment that by directly monitoring a laptop's power status by looking at the BatteryStatus property of the Win32_PortableBattery class.. This is probably achievable using BindEvent in VFP9, but not at all easy to achieve otherwise in earlier versions of VFP.

All the notification classes derive from __InstanceOperationEvent - in order to query more than one type of operation we can request instances of this class. The query will raise events for all three subclasses, and the sink can determine which is which by the object's Path_.Class property. There's an example of this later on.

Using the event catcher to catch Event Log events

It's easy to use this technique to monitor for specific events in the Windows Event Log:
* See all events:
select * from __InstanceCreationEvent where TargetInstance ISA 'Win32_NTLogEvent'

* Catch only specific events: 4202 is a network transport failure
select * from __InstanceCreationEvent where TargetInstance ISA 'Win32_NTLogEvent'
and TargetInstance.EventCode=4202

* Catch only events from a specific source: in this case WMI itself.
select * from __InstanceCreationEvent where TargetInstance ISA 'Win32_NTLogEvent'
and TargetInstance.SourceName='WinMgmt'
This will raise an error on a local query if the query results include a log event from the Security Event Log. Access to the Security log requires a WMI privilege to be added to the connection: this should be done at the time of the call in Windows 2000+ but must be requested as part of the original connection in Windows 95/NT. See my WMI connection class for how to do that.
define class logeventcatcher as eventcatcher

	procedure CatchEvents
		lparameters cQuery
		* Required to access the Security log
		this.oWMI.Security_.Privileges.AddAsString("SeSecurityPrivilege")
		dodefault(cQuery)
	endproc

enddefine
If you look at the properties of a Log Event, you'll see that several use the WMI time format - e.g. 20050905151317.000000+060. In order to easily transform these into VFP datetime values you can use this format code:
cWMIDate = oEvent.TimeGenerated
tDateTime = ctot(transform(cWMIDate, '@R 9999-99-99T99:99:9999'))
The final component of the WMI datetime (+060 in the example above) indicates the offset in minutes from GMT. (There's a scripting object which works with WMI dates but it's only available in Windows XP/2003.)

Incidentally, the upper case "T" in the format mask is used to tell CTOT() that it should internally switch to YMD format for the date conversion regardless of what the SET DATE settings are currently.

Querying Association Class Events

Association classes are the glue classes that tie the WMI class hierarchy together. An association class represents a relationship between objects - Win32_UserDesktop relates users and their desktop settings: if you have one, you can determine the other. Other association classes represent the relationship between a single entity and a group - an instance of one of these classes has a property for the "owner" object and a collection representing the "owned" objects. CIM_ProcessExecutable links processes and the files they have open, for example.

This sample association class query watches a specific directory - in this case c:\temp - for files being created, changed, or deleted. The query includes both single and double quotes, and note the amount of escaping required on the backslashes. This will work for mapped network drives but not UNC paths.
select * from __InstanceOperationEvent within 3 where 
TargetInstance ISA 'CIM_DirectoryContainsFile' and
TargetInstance.GroupComponent='WIN32_Directory.name="c:\\\\temp"'
We need to change the sink class for this query as the TargetInstance is an association class: association classes have a number of ways to refer to their components: in this case, GroupComponent and PartComponent are the association properties, but other property pairs used include Antecedent/Dependent, and Element/Setting. The difference from our point of view here is that we don't receive a reference to an instance of the class in the association class, we instead get the full WMI path. If the object still exists, then the WMI Get() method can be used to get an object reference, but that's not the case for __InstanceDeletion as is shown here:
procedure ISWbemSinkEvents_OnObjectReady(oObject, oAsyncContext)

	* Scratch WMI object
	oWMILocator = createobject("WbemScripting.SWbemLocator")
	oWMI = oWMILocator.ConnectServer(".", "root\cimv2")

	do case
		case oObject.Path_.class = "__InstanceCreationEvent"
			oFile = oWMI.get(oObject.TargetInstance.PartComponent)
			? "File " + oFile.name + " was created with filesize " + oFile.FileSize

		case oObject.Path_.class = "__InstanceModificationEvent"
			* This never fires for file modifications. Renaming a file produces a
			* deletion event and a creation event instead.
			? "This never fires for files"

		case oObject.Path_.class = "__InstanceDeletionEvent"
			* Can't "get" an object that's been deleted.
			? oObject.TargetInstance.PartComponent + " was deleted"
	endcase

endproc

A solution for remote semi-synchronous queries

We started this by looking at the semi-synchronous event query ExecNotificationQuery, which blocked Foxpro while waiting for the event. Asynchronous queries are a solution for local queries, but are strongly discouraged for use with remote queries. We can use semi-synchronous remote queries if we can create a worker thread for each, though: fortunately, there is a 3rd party COM component that enables VFP applications to launch background tasks in separate threads, and is free to use and distribute according to the accompanying license (it requires MSVCP70.DLL too).

To see it in action, create a project and compile a MTDLL with this program as the main: save it as wmimtdll.dll (if you prefer to use another name the name will have to be changed in the call below)
define class eventcatcher as session olepublic

	procedure init
	procedure destroy

	procedure CatchEvents
		lparameters cQuery, cServer, cNamespace && cUsername, cPassword etc
		oLocator = createobject("wbemscripting.swbemlocator")
		* See http://weblogs.foxite.com/stuartdunkeld/archive/2005/09/14/910.aspx
		* for username syntax or use the WMI Connection class.
		oWMI = oLocator.ConnectServer(cServer, cNamespace, "", "", "", "", 128)
		oResults = oWMI.ExecNotificationQuery(cQuery)
		oEvent = oResults.NextEvent()
		return oEvent
	endproc

	procedure error(nError, cMethod, nLine)
		comreturnerror("MTEventCatcher", "Error " + transform(nError) + " at line " + transform(nLine) + " of " + cMethod)
	endproc

enddefine
We still need a sink to receive the event: we make our own this time. There's a version for VFP6 too.
define class mtsink as relation

	implements _IWorkerEvents in "vfpmtapp.worker"

	procedure _IWorkerEvents_OnTerminate
		lparameters vResult
		? vResult.TargetInstance.Name
	endproc

enddefine

define class mtsink6 as relation

	procedure OnTerminate
		lparameters vResult
		? vResult.TargetInstance.Name
	endproc

enddefine
It won't come as a blinding surprise that now we create the various objects and bind them together. Note we pass the thread factory an array containing the parameters to be passed to the VFP object method call. This will only work in the command window unless the variables are kept in scope.
* This is the 3rd party component
oThreadFactory = createobject("vfpmtapp.worker.1")

* VFP7: create the sink and bind it.
oSink = createobject("mtsink")
eventhandler(oThreadFactory, oSink)

* VFP6
* oSink = createobject("mtsink6")
* oVFPCom = createobject("vfpcom.comutil")
* oVFPCom.BindEvents(oThreadFactory, oSink)

cQuery = "select * from __InstanceCreationEvent within 1 where TargetInstance ISA 'WIN32_Process'" 
cServer = "remote_computer" && this will still work locally.

dimension aParameters[3]
aParameters[1] = cQuery
aParameters[2] = cServer
aParameters[3] = "root\CIMV2"
* Change this call if you chose another name for the DLL:
oThreadFactory.Run("wmimtdll.eventcatcher", "CatchEvents", @aParameters)
There are limitations to using this control: as the author states "If you return an object ... do not cache a reference to it, the object will be destroyed after the the last notification is executed, because the working thread that hosts this object will end". This means that when the sink's OnTerminate method finishes, the object reference disappears so all the object processing needs to take place in the sink (which can call other methods). Another issue (with WMI more than with the control) is that after NextEvent has returned an event, the thread ends and the query has to be reinvoked.

Providing worker threads sounds like something Sedna could do: until then, this will do, especially in a scenario where one computer is querying many remote computers and there are no distribution issues to worry about.

Temporary and Permanent event consumers

Thus far we have looked at the events that applications or their users might be interested in: the techniques used only allow events to be caught while an application or script is running. There are numerous events that it would be very useful to monitor all the time, whether an application is running or not, and especially on a server: checking for free diskspace of less than 10% on any disk, or CPU usage stuck at 100%, memory usage going through the roof, unknown processes or services, UPS status - WMI has an inbuilt mechanism for this kind of event query. If you create a Permanent Event Consumer (a COM object implementing the IWbemUnboundObjectSink interface) linked to an EventFilter query, then when the event fires, WMI will instantiate the object and pass it the event data.

Edit: this isn't possible in VFP, whose implementation of IMPLEMENTS is lacking the capability of returning the interface when requested via  CoGetClassObject. The error hresult is 0x80040154, CLASS_NOT_REGISTERED.

Published Friday, September 16, 2005 7:21 AM by stuartd
Filed Under:

Comments

# re: WMI part 4 - WMI Events part 1

Do you know any command-line utilities or one liner script commands to remove permanent event consumers? The mofcomp.exe lets you put those register those even consumers but not unregistered them. You know what I mean?
Thursday, October 27, 2005 3:50 AM by Gary

# re: WMI part 4 - WMI Events part 1

Hi Gary

Yes, as long as you know which one you want to delete. The easiest way is to call the Delete method of the WMI services object:
oWMI.Delete("MyEventConsumer")
This works even if there are registered instances of the event consumer.

Alternatively, query oWMI.SubclassesOf("__EventConsumer") to see all registered event consumers and call the target object's Delete_() method.


Thursday, October 27, 2005 8:20 PM by stuartdunkeld
New Comments to this post are disabled