Handling WMI events using TWAPI

Some examples of handling WMI events through TWAPI COM support. See TWAPI and WMI for examples related to retrieving WMI data.

WMI can be configured to monitor system events and send notifications to an event sink when the events occur. Note that asynchronous notifications in WMI have security issues depending on what you do with the data. Refer to the WMI documentation at [1 ] for details and steps to secure the notifications.

The examples below illustrate how to set up the event sink and associated handlers using TWAPI COM support.

Remember to do a

  $wmi -destroy

at the end when you no longer need WMI.

Also, the Tcl event loop must be running, either through Tk or through a vwait command.

Maiki - 2011-05-06 09:08:40

Assumes twapi 3.0 - will need to be modified for earlier versions

proc start_process_tracker {} {
        # Get local WMI root provider
        set ::wmi [twapi::_wmi]

        # Create an WMI event sink
        set ::event_sink [twapi::comobj wbemscripting.swbemsink]

        # Attach our handler to it
        set ::event_sink_id [$::event_sink -bind process_start_handler]

        # Associate the sink with a query that polls every 1 sec for process
        # starts.
        $::wmi ExecNotificationQueryAsync [$::event_sink -interface] "select * from __InstanceCreationEvent within 1 where TargetInstance ISA 'Win32_Process'"
}

proc process_start_handler {wmi_event args} {
        if {$wmi_event eq "OnObjectReady"} {
                # First arg is a IDispatch interface of the event object
                # Create a TWAPI COM object out of it
                set ifc [lindex $args 0]
                IUnknown_AddRef $ifc;   # Must hold ref before creating comobj
                set event_obj [twapi::comobj_idispatch $ifc]

                # Get and print the Name property
                puts "Process [$event_obj ProcessID] [$event_obj ProcessName] started at [clock format [large_system_time_to_secs [$event_obj TIME_CREATED]] -format {%x %X}]"

                # Get rid of the event object
                $event_obj -destroy
        }
}

Avoiding Hangs in Queries neb 2011-07-19 I've been using a set of wmi queries on a large number of machines. Occasionally, they will hang for no apparent reason. Everything works, connects, etc, but the query just never returns. This is an oft-asked-about issue, online; and Microsoft never saw fit to offer any way to cancel or timeout a query. You can, however, use an Asynchronous (as opposed to a 'Semisynchronous') query using close to the same process as above.

The following code is a knock-off of this MSDN example: http://msdn.microsoft.com/en-us/library/aa392295%28v=vs.85%29.aspx

package require twapi

set ::bdone 0
set count 0

proc change_handler {wmi_event args} {
        switch $wmi_event {
                OnObjectReady {
                        # Create a TWAPI COM object out of the IDispatch interface (first arg)
                        set ifc [lindex $args 0]
                        ::twapi::IUnknown_AddRef $ifc;   # Must hold ref before creating comobj
                        set event_obj [twapi::comobj_idispatch $ifc]
                        puts "Name: [$event_obj Name]"
                        incr ::count
                        $event_obj destroy
                    }
                OnCompleted {
                        set ::bdone 1
                }
        }
}

set eventsink [twapi::comobj wbemscripting.swbemsink]
set eventsink_id [$eventsink -bind change_handler]

set wmi [twapi::_wmi]

after 12 {if {!$::bdone} {set ::bdone 2}}

set wql {SELECT Name FROM Win32_Process}
$wmi ExecQueryAsync [$eventsink -interface] $wql

vwait bdone

$eventsink -unbind $eventsink_id
$eventsink -destroy
$wmi -destroy

after 3000

puts "$count objects"
if {$::bdone==2} {puts timedout}

'after 12' corresponds to roughly how long it takes the machine I am on to return the first record. Adding one ms results in the rest of the list all burped out at once.

A more annotated version follows. Note it also uses '$eventsink Cancel' for the timeout action, rather than relying on the variable. Canceling triggers the 'OnCompleted' event, with a result value of 0x80041032 (user cancel). It also seems to work faster: Cancel will interrupt the list in around 6ms on my machine, vs 12 for the version above.

There should be a way to turn the objErrorObject parameter into a COM or SWbemLastError object (which would have a GetObjectText_ method), but I haven't worked it out.

package require twapi

set ::bdone 0
set count 0

proc change_handler {wmi_event args} {
        switch $wmi_event { # http://msdn.microsoft.com/en-us/library/gg196623%28v=VS.85%29.aspx
                OnObjectReady {        # objObject, objAsyncContext
                        # Create a TWAPI COM object out of the IDispatch interface (first arg)
                        set ifc [lindex $args 0]
                        ::twapi::IUnknown_AddRef $ifc;   # Must hold ref before creating comobj
                        set event_obj [twapi::comobj_idispatch $ifc]
                        puts "Name: [$event_obj Name]"
                        incr ::count
                        $event_obj -destroy
                    }
                OnCompleted { # iHResult, objErrorObject, objAsyncContext
                        set ::bdone 1
                        if {[lindex $args 0]} {
                                puts "Completed with error of 0x[format %08X [lindex $args 0]] (0x80041032 = user cancel)"
                        }
                }
                OnProgress { # iUpperBound, iCurrent, strMessage, objWbemAsyncContext
                        # Needs the '128' in the ExecQueryAsync call (4th param). Not all queries supply progress info.
                        puts Progress
                }
                OnObjectPut { # objWbemObjectPath, objWbemAsyncContext
                        puts Put
                }
                default {
                        puts "Unhandled event: $wmi_event"
                }
        }
}

set eventsink [twapi::comobj wbemscripting.swbemsink]
set eventsink_id [$eventsink -bind change_handler]

set wmi [twapi::_wmi]

after 6 {$eventsink Cancel; puts "query timed out."}
set wql {SELECT Name FROM Win32_Process}

# params: eventsink, query string, must be 'WQL', 128 (0x80) = wbemFlagSendStatus
$wmi ExecQueryAsync [$eventsink -interface] $wql WQL 128

vwait bdone

puts "unmaking objects"
$eventsink -unbind $eventsink_id
$eventsink -destroy
$wmi -destroy

puts "Pausing to make sure everything is canceled."
after 3000
puts "$count objects"

One thing I am worried about with these is that we set event_obj on each record returned, but I can't destroy it. Uncommenting the destroy line stops the query results at that point. In VBS examples online, they don't worry about destroying the objects--but they also don't have to create them. I'm not certain if the result of comobj_idispatch gets re-used, and then dies when it goes out of scope, or if it's creating a new object at every iteration (and how to delete it), and therefore these scripts have a memory leak.

APN neb, I've modified your examples above to do an IUnknown_AddRef on the event object interface before converting it to a comobj. This is required in the general case. You can then call $event_obj destroy. and I believe you will not see the query results stop. Otherwise, yes, you will have a memory leak of comobj objects. Let me know if that does not work for you.

IMHO, WMI is a completely unreliable piece of crap. I spent long hours debugging what I thought were problems in TWAPI's event handling when it turned out VB code and MS own SDK tools exhibited the same behaviours with hangs and timeouts. I don't know if the WMI implementations have improved in the last few years.


neb: Thank you, APN. I had tried putting in an addref, but mucked it up somewhere. I've made the references you added explicit, so they run as-is on my machine.

And no, WMI has not improved, in my experience. Aside from the intermittent and inexplicable hangs, I find it easy to corrupt the repository. Eg, we had a range of subnets that had originally been on Novell. When it was uninstalled, WMI corrupted on approximately 50% of the machines, requiring a stop/delete/restart type of refresh. It's not immediately obvious *why* things don't work, in that situation. WMI just sort of stops giving back results.