Google

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!

No comments: