Tags

, , , ,


Coming up is an Asynchronous WMI Event query.  Read on…!

In the last post, I showed how VMware View has a ThinPrint component that brings printers from the physical PC into the virtual machine you connect to without your having to worry about installing the relevant drivers into your virtual infrastructure.  The printers appear in the VM with “#:” and an incremented number appended to their name.

We have an application that for convoluted reasons is set to print labels by looking for any printer installed on the local PC called “Label Printer”.  This works well and it will merrily print to USB-connected printers or networked printers without any worries.  No central management or printserver is required for them.

There’s always a complication though; in this case it’s the VDI environment where the printer name suddenly becomes something like “Label Printer#:1” thanks to the actions of VMware’s ThinPrint component.  Our application doesn’t support a wildcard in the name of the printer it is to look for…

I found that I could rename this printer back to “Label Printer” and it would still pass through to the print queue of the physical PC and on to the print device.  So I thought about a script to rename the printers.  VMware View has Group Policy settings (via ADM files) that let you run scripts when (re)connecting to the virtual machine.  It can take a while though for all the printers to add in if you have a lot of them but I didn’t want to code a wait into the script before renaming as I needed the printer usable as quickly as possible.  I decided to have a go at dynamically renaming them as they appeared in the VM.  I’ll say straight away that this ultimately didn’t end well – ThinPrint wouldn’t remove its printers on disconnect after I’d renamed them and if I didn’t remove them by a disconnect script, it would start re-adding them again on reconnect as extra printers.  Worse still, it would get randomly upset and stop the printer importing process mid-way and not restart it after a disconnect and reconnect.  The code I came up with did do what I intended and was quite cool to watch in action.  It also helped me get my head around some more unusual WMI techniques, so here it is for your enjoyment!

On Error Resume Next

Const wbemFlagReturnWhenComplete = 0

WScript.Sleep 2000

Set objWMISink = WScript.CreateObject("WbemScripting.SWbemSink","SINK_")
Set objWMIServices = GetObject("winmgmts:{impersonationLevel=impersonate}!\\.\root\cimv2")

strWQL = "SELECT * FROM __InstanceCreationEvent WITHIN 2 WHERE TargetInstance ISA 'Win32_Printer'"
objWMIServices.ExecNotificationQueryAsync objWMISink, strWQL

intAdded = 0
Do
	intLastTotal = intAdded
	WScript.Sleep 5000
Loop Until intAdded = intLastTotal

objWMISink.Cancel
Set objWMISink = Nothing
WScript.Quit

Sub SINK_OnObjectReady(objLatestEvent, objWbemAsyncContext)
	WScript.Sleep 750
	Set objPrinter = objLatestEvent.TargetInstance
	strName = objPrinter.Name
	intSuffix = InStr(strName, "#:")
	If intSuffix > 0 Then
		strNewName = Left(strName, intSuffix - 1)
		objPrinter.RenamePrinter strNewName
		intAdded = intAdded + 1
	End If
End Sub

When a new <WMI instance of something> is created in the OS, a WMI Event is fired called an “__InstanceCreationEvent“. The created Instance in question could be of a Win32_Process (eg the user opened Notepad.exe) or it could be a CIM_DataFile (eg a file was created in a folder) or it could be like in this case a Win32_Printer if a printer is added. There are also events for Modification and Deletion. The WQL query required is fairly self-explanatory once you’ve remembered the general syntax. My query was this: “SELECT * FROM __InstanceCreationEvent WITHIN 2 WHERE TargetInstance ISA ‘Win32_Printer’“. Really there are only two things to worry about editing there. The number 2 means look every two seconds to see if a new event has occurred, and the WMI Class name of what we’re looking for is also required – otherwise we’ll drown in all kinds of events! The more often we look, the more of a performance hit we’ll get from WMI doing its stuff, so use a higher value if you can. You can also add more things to the WHERE clause if appropriate to filter down to the events you want by looking at properties of the TargetInstance object.

This sort of query is executed using a different method call than for normal WMI queries.  You can do a synchronous query where the code will sit and wait for the next event or you can do an asynchronous one where your code carries on but will also jump off to a special chunk of code as soon as it is notified that an event is received.  I’m using the latter technique as I want to rename the printers as they arrive but be able to exit the entire script after a set maximum time.  This asynchronous variety requires the ExecNotificationQueryAsync() method and it takes another parameter before the WQL string.  This is called the Event Sink object and is to tell WMI what to do when it sees an event.  This is created with the line:

Set objWMISink = WScript.CreateObject("WbemScripting.SWbemSink","SINK_")

The supplied string “SINK_” is the subroutine name’s prefix that will be looked for when an event occurs. When the event occurs, the code will look for a subroutine called <Prefix>OnObjectReady so in this case, “SINK_OnObjectReady”. That subroutine has two parameters in its declaration:

Sub SINK_OnObjectReady(objLatestEvent, objWbemAsyncContext)

I don’t pretend to know much about WMI events, but the one thing to know is that inside this subroutine, the object passed in, objLatestEvent has a property called TargetInstance and that basically contains the WMI class you’ve filtered on – in my case a Win32_Printer so you can then do to that whatever you want.

You can now follow the flow of the full script.
There’s an initial pause to ensure the ThinPrint AutoConnect process has definitely started.  The Event Sink object is created, as is the usual WMI Services object.  The WQL query is then executed in the background (asynchronously).  If a printer appears in the system, the event sink calls the subroutine which after a little pause, renames the printer minus the “#:<num>” in its name.  It also increments a variable to track that it has added one.  Meanwhile the script has been sitting around for five seconds.  It then looks to see if any printers have been added during that time.  If any have, then it waits another five seconds before seeing if the count has gone up again and so on.  Once no more printers have been added in the previous five seconds, the event sink is got rid of and the script exits.

It didn’t end up solving my VDI printing problem but it was fascinating all the same!

Part 3 of this series can be found here.

Advertisements