Work around quirky Printui.dll on XP

In the previous post, I talked about issues I was seeing when using printui.dll to push per-machine connections on Windows XP. I did tons of testing and in the realized that XP's spooler service was too flaky for my purpose.

I spent some time diving deeper into what the commands are doing and if there was anything that I could do in the startup script that would make it more consistent...

Let's cut to the chase:

I concluded that the most reliable way to make sure printers will be pushed when users logged in was to make sure relevant connection entries are created by my script under HKLM during startup, instead of relying printui.dll /ga switch.

How did I arrive at this conclusion?

Well,  I used Process Monitor (aka Procmon) from SysInternals,
watched what happens if I issue commands like the ones below manually

  • rundll32 printui.dll,PrintUIEntry /ga /n\\PrintServer\Printer (add per-machine printer connection)
  • rundll32 printui.dll,PrintUIEntry /gd /n\\PrintServer\Printer (remove per-machine printer connection)

A Watched sequence of events during boot (Procmon > Options > Enable Boot-Logging)

Here are a few things to note that will help us understand what is going on :

  • Our Print Server is named PrintServer1 
  • Printers shared on it has sharenames Printer1.. PrinterN 
  • HKLM Connections key: HKLM\SYSTEM\CurrentControlSet\Control\Print\Connections
  • HKCU Connections key: HKCU\Printers\Connections
  • If there is no ",,PrintServer1,PrinterX" entry under HKLM Connections key, user does not get the PrinterX connection.

Q. What happens if we "add per-machine printer connection" in a user session?
A. Seemingly nothing, user does not see the new printer connections until Spooler service is started. In reality, a new registry key under HKLM Connections key is created.

Q. So, what happens if we re-start the spooler services?
A. Normally, user gets the printer connection. It does not show up immediately, spooler service takes its sweet time but unless something goes terribly wrong, new printers shows up in a minute or two. Here is how (shortened for relevancy):

Spooler Service checks the entries under HKLM Connections key and detects a new printer connection, which will be in the following form:
Server=\\PrintServer1 (RegSZ)
Provider=Win32spl.dll (RegSZ)
LocalConnection=0x0000001 (DWORD)

You probably noticed that \\PrintServer1\PrinterX has been transformed to ,,PrintServer1,PrinterX and became a key. Provider and LocalConnection always had the same value, "Server" was set to the name of the Print Server with '\\' prefix.

So far, so good. The rest is the tiring loop:

  • Spooler starts going through SIDS
  • Creates a new connection under HKCU\Printers\Connections,,PrinterServer1.PrinterX
  • Clones the 'Server', 'Provider', 'LocalConnection' settings from HKLM Connections key
  • Queries and creates a new port setting:

    HKLM\Software\Microsoft\Windows NT\CurrentVersion\Ports\NeXX: (Reg_SZ ),
    where Ne in my case went from Ne00 to Ne08
  • It then created an entry under devices that linked the port to printer:

    HKCU\Software\Microsoft\Windows NT\CurrentVersion\Devices\
    \\PrintServer1\PrinterX = Winspool,NeXX: (Reg_SZ)
  • In the next step, spooler creater a PrinterPort entry in the following form:

    HKCU\Software\Microsoft\Windows NT\CurrentVersion\PrinterPorts\
    \\PrintServer1\PrinterX = Winspool,NeXX:15,45 (Reg_SZ)
  • In the actual flow, HKCU was the last. Spooler actually started with SID S-1-5-19 (HKU\S-1-5-19\)and cycled through all SIDS under HKU but you get the point.
  • THEN, an important step comes: querying printer, getting a connection to it (I see a %windir%\System32\spool\PIPE file created), creating a registry key for printer and writing everything about that printer

    HKLM\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Print\Providers\LanMan Print Services\Servers\PrinterServer1\Printers\PrinterX
So, that's gist of what spooler seems to be doing. 

What's the problem?

Well, the problem I identified was that during the startup Spooler service would something be slacking. The command I ran to add a per-machine printer connection would return 258. 
Whenever rundll32 returned 0, that process ended up creating an HKLM connection entry and printer showed up just fine for user.
If, however, rundll32 returned 258, no connection was created.

So, what does return code 258 mean? Answer comes from Alan Morris of Microsoft Windows Printing team:

"this is the error
C:\>winerror 258
   258 WAIT_TIMEOUT <--> No NTSTATUS matched
printui does not validate the data that you are adding to the registry."
He goes on to explain:

"I'd check the number of printers and drivers as well as the drivers installed on a machine where this is failing.  The spooler may not be fully online and printui does call into spooler for these methods." 

Well, all good, but I had seen services.exe kicking off spooler service a full minute before any commands were issued to it. Additionally, spooler service would almost always map the first printer but the number of printers it connected was varying each time.

I was using the same XP VM and got considerably different results (sometimes tons of return code 258 errors, sometimes just 1 or two).

Techs in the field were reporting a similar pattern and a quick  Google search also got many hits in terms of people complaining about success rate of using these commands in the startup scripts.

What do we do now?

Well, as I said in the beginning, my approach was to help the spooler service a bit by creating the HKLM connections registry keys for it. In my tests, it worked like a charm (YMMV) every single time.

Below is modified sample script

on error resume next
Const ForReading = 1, ForWriting = 2, ForAppending = 8, CanCreate=True

Set oShell = CreateObject("WScript.Shell")
oShell.LogEvent 0, "WinXP_WSStartup.vbs: Script Version: " & ScriptVersion

'Get group membership for the machine
Set objSysInfo = CreateObject("ADSystemInfo")
Set objComputer = GetObject("LDAP://" & objSysInfo.ComputerName)

Groups = objComputer.GetEx("MemberOf")
' If Above Command does not find any groups for computer, it will error
if  Err.Number <> 0 Then  
 oShell.LogEvent 0, "WinXP_WSStartup.vbs: Computer is not member of any AD groups. Exiting."
end if

For Each group In Groups
  group=Replace(group,"/","\/")   ' escape groups with a slash in the object name   
  Set objGroup = GetObject("LDAP://" & group)
  If ( Instr(LCase(objGroup.CN),"grp.prn." ) > 0 ) then          
  If isFirst Then
   PrinterGroups = objGroup.CN
   PrinterGroups = PrinterGroups & ";" & objGroup.CN
  End If   
  End if 
  'AllGroups = AllGroups & ";" & objGroup.CN

if (Count > 0) Then
 'Take care of setting up Reg File
 Set oFSO = CreateObject("Scripting.FileSystemObject") 
 sRegFile = oShell.ExpandEnvironmentStrings("%TEMP%")  & "\WinXP_WSStartup_PrinterConnections.reg" 
 If oFSO.FileExists(sRegFile) Then
 End If 
 Set oRegFile = oFSO.OpenTextFile(sRegFile, ForAppending, CanCreate)
 oRegFile.WriteLine "Windows Registry Editor Version 5.00" & vbCrLF & vbCrLF
 ' Now let's process each printer grou[
 for each PrinterGroup in arrPrinterGroups 
  arrGroup = Split(PrinterGroup,".")
  ' skipping first 2 grp and prn as groups are named grp.prn.ServerName.PrinterName
  '' Update registry file
   oRegFile.WriteLine("[HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Print\Connections\,," & Server & "," & Printer & "]" )
   oRegFile.WriteLine(chr(34) & "Server" & chr(34) & "=" & chr(34) & "\\\\" & Server & chr(34))
   oRegFile.WriteLine(chr(34) & "Provider" & chr(34) & "=" & chr(34) & "win32spl.dll" & chr(34))
   oRegFile.WriteLine(chr(34) & "LocalConnection" & chr(34) & "=dword:0000001" & vbCrLf)

'  cmdConnectPrinter="%comspec% /c rundll32 printui.dll,PrintUIEntry /ga /n\\" & Server & "\" & Printer
  oShell.LogEvent 0, "WinXP_WSStartup.vbs: Connecting to Printer: " & Printer & " on Server: " & Server
'  oShell.LogEvent 0, "WinXP_WSStartup.vbs: Running " & cmdConnectPrinter
  ' Hide Window and return immediately without waiting
'  oShell.Run cmdConnectPrinter,0,false
 ' Import Printer Connection file silently
 oShell.Run "regedit /s " & Chr(34) & sRegFile & Chr(34), 0, True 
End If

oShell.LogEvent 0, "WinXP_WSStartup.vbs: Nothing else to do. Exiting" 
Set oShell=Nothing

No comments: