Google

2013-08-13

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:
HKLM\SYSTEM\CurrentControlSet\Control\Print\Connections\,,PrintServer1,PrinterX
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
ScriptVersion="2013-08-13-02"

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."
 Wscript.Quit
end if

isFirst=vbTrue
Count=0
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
   isFirst=vbFalse
   Count=1
  Else
   PrinterGroups = PrinterGroups & ";" & objGroup.CN
   Count=Count+1
  End If   
  End if 
  'AllGroups = AllGroups & ";" & objGroup.CN
Next 

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
  oFSO.DeleteFile(sRegFile)
 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[
 
 arrPrinterGroups=Split(PrinterGroups,";") 
 for each PrinterGroup in arrPrinterGroups 
  arrGroup = Split(PrinterGroup,".")
  ' skipping first 2 grp and prn as groups are named grp.prn.ServerName.PrinterName
  Server=arrGroup(2)
  Printer=arrGroup(3)
  
  '' 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
   
 Next 
 
 oRegFile.close
 
 ' 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

2013-08-10

Printer Connection Issues on XP

I've been hitting my head against the wall with the inconsistencies I see when trying to use Microsoft printui.dll to connect network printers on XP machines. Here goes the story...

Problem:

There are a bunch of "floor" printers that are published in a Windows 2008 R2 print Queue. We want every user who logs on to the computers on that floor to automatically get these printers.

Solution:

There are several well known ways, I won't go into details. Here is a nice blog that goes over them: http://blog.powershell.no/category/print-management/

In my case, I came up with a naming schema for printer groups

  • grp.prn.PrintServerName.PrinterShareName 

ComputerNames are added into these groups, which I then parse through a startup script and use well-known printui.dll to connect the machine to these printers. There were several technical but of course also some political reasons to do it this way. Your mileage may vary.

Windows 7 & PowerShell

Anyway, I wrote a Computer Startup PowerShell script that is deployed via Group Policy, put the logic in to detect which printers the computer should be connecting to, and wrapped it around the command below:

  • printui.exe /ga /n\\PrintServer\Printer

and all went well.

The switch (ga) is causes "per-machine" connection. Underneath, it caused creation of a registry key entries under HKLM and once users are logged in it "pushes" these connections to user, so that user is able to see and print to those printers without requiring them do anything.

Windows XP, still around...

XP is another story. Well, for one, xp does not have printui.exe but its older cousin rundll32 printui.dll,PrintUIEntry, which is well documented on technet, is still around and unfortunately, there are several XP machines without PowerShell in the environment, so I had to go back to vbscript.

It's a similar story, I back-ported the logic and script to vbscript:

  • rundll32 printui.dll,PrintUIEntry /ga /n\\PrintServer\PrinterName 

Note that, technically, it's not the printer name we use but the Printer Share Name, which may be different than the printer name). Tested it, yep works.

A couple of days later, I hear from one of the techs that printers are not getting mapped for his computers. Script logging clearly showed that it ran the command. Problem is, command does not have a return code to tell us whether command was successful or not. I could not find any documentation that explains the details of what goes on at that point.

Perhaps a debug flag somewhere would be useful but spooler is a very magical and at the same time finicky service especially on Windows XP. One thing is certain, it is inconsistent and there is no way to know why (yet)...

I hit other weird issues during troubleshooting. Below is one of them and the workaround.

Issue:

I wanted to remove some of the printers I connected to. Because these printers are connected per-machine using /ga switch, they need to be removed using /gd switch.

So, I run the appropriate command

  • Locally: rundll32 printui.dll,PrintUIEntry /gd /n\\PrintServer\PrinterName
  • Remotely: rundll32 printui.dll,PrintUIEntry /gd /c\\TargetComputer /n\\PrintServer\PrinterName


Printer connection cannot be removed. Operation could not be completed.

Workaround:

The fastest workaround I found for this issue is delete the relevant registry keys that are under:

  • HKLM\System\CurrentControlSet\Control\Printer\Connections\


Below are some of the other useful resources on Printing


Sample VBScript:

On error resume next
ScriptVersion="2013-08-09-02"
Set oShell = CreateObject("WScript.Shell")
oShell.LogEvent 0, "WinXP_WSStartup.vbs: Script Version: " & ScriptVersion

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."
Wscript.Quit
end if

isFirst=vbTrue
Count=0
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
isFirst=vbFalse
Count=1
Else
PrinterGroups = PrinterGroups & ";" & objGroup.CN
Count=Count+1
End If
End if
Next

if (Count > 0) Then
arrPrinterGroups=Split(PrinterGroups,";")
for each PrinterGroup in arrPrinterGroups
arrGroup = Split(PrinterGroup,".")
' skipping first 2 grp and prn as groups are named grp.prn.ServerName.PrinterName
Server=arrGroup(2)
Printer=arrGroup(3)

cmdConnectPrinter="cmd /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

Next
End If

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

*******
In PowerShell, I wrote a couple of functions, thinking I might use the same logic to add user-side printer connections, hence the 'user' & 'computer' objectCategories below.

A single line calls the functions:


Connect-NetworkPrinter -objectCategory computer


Here are the functions:


Function Connect-NetworkPrinter {
[CMDLETBINDING()]
  Param(    
    [ValidateSet('user','computer')]
    [String]$objectCategory='computer',

[string]$PrinterGroupIdentifier='^cn=grp.prn.',
[string]$name )
if (!$name) { if ($objectCategory -eq 'computer') { $name=$($env:COMPUTERNAME) } if ($objectCategory -eq 'user') { $name=$($env:USERNAME) } } Write-Verbose "Connect-NetworkPrinter: PrinterGroupIdentifier set to $PrinterGroupIdentifier" Write-Verbose 'Connect-NetworkPrinter: Calling Function to Get Group Membership'
$groups = Get-ADGroupMembership -objectCategory $objectCategory -name $name
if (!$groups) { Write-Verbose "Connect-NetworkPrinter: No groups returned for current computer" logMsg "Connect-NetworkPrinter: No groups returned for current computer" return $false }
$printergroups=@() foreach ($group in $groups) {
Write-Verbose "Connect-NetworkPrinter: Checking group $group" if ($group -match $PrinterGroupIdentifier) { Write-Verbose "Connect-NetworkPrinter: Matches criteria $group"
$Server,$Printer,$null=($group -replace $PrinterGroupIdentifier -replace ',ou=.*$').split('.')
Write-Verbose "Connect-NetworkPrinter: Connecting to Printer: $Printer on Server: $Server" try { Invoke-Expression 'rundll32 printui.dll,PrintUIEntry /ga /n"\\${Server}\${Printer}"' ## rundll32 does not return anything so cannot check on error logMsg "Connecting to Printer: $Printer on Server: $Server" } catch { Write-Error "An error occured $_" logMsg "Error occured connecting to Printer: $Printer on Server: $Server" -entrytype 'Error' }
# [PSCustomObject]@{Server=$server;printer=$printer} ## return PSCustomObject } } Write-Verbose 'Connect-NYUPrinter: Exiting' logMsg "Printer connections complete"
}
function Get-ADGroupMembership { <# .SYNOPSIS Gets Active Directory Group membership for users or computers
.EXAMPLE Get-ADGroupMembership -name 'user1','user2' -Verbose Displays group membership for users user1 and user2
.Parameter name Name of the computers or users
.Parameter ObjectCategory Can be user or computer. By default it is 'computer'
.NOTES Output is string array of Distinguished Names. If nothing is found, it returns $false #>
[cmdletbinding()] param([Parameter(Mandatory=$true)] [string[]]$name, [ValidateSet('user','computer')] [String]$objectCategory='computer' ) Begin { Write-Verbose 'Get-ADGroupMembership: Creating Directory Search Object' $searcher=New-Object DirectoryServices.DirectorySearcher }
Process { $name | % { Write-Verbose "Get-NYUGroupMembership: Processing $_" $filter="(&(objectCategory=$objectCategory)(cn=$_))" ## this will only find user object $searcher.PageSize=1000 $searcher.Filter=$filter $result=$searcher.FindOne()
if ($result) { $result.Properties.Item("memberOf") } else { Write-Verbose '...is not member of any groups' } } }
END { Write-Verbose 'Get-ADGroupMembership: Disposing Directory Search Object' $searcher.Dispose() } }
I am not posting here the 'logmsg' function that the PowerShell functions are referring to, but it is basically just logging events into a Special Log I set up for all my scripts. It's so old, I did not even bother to name it PowerShell-way.

Enjoy!