Powershell Printer Script

Over the last 15 years I’ve tried pretty much every method of adding printers at logon there is – KIXTART script, VBS, Group Policy Preferences and Powershell. As part of speeding up logon, and investigating a weird issue with Windows 10 printers, I moved away from GPP and to Powershell shortly after we upgraded from Windows 8.1 to Windows 10.

Some detail is show to the user, this form isn’t modal so won’t take over the screen, and if they close it the script carries on in the background.

The issue being – roughly 5% of the time, on random user/computer combinations, printers would take a long time adding and then fail to add, with a non-specific error message. My first go at this was a basic powershell script which had a hard coded list of location/printer mapping, and it would run the “add printer” command repeatedly until the error went away. (It always added fine on the 2nd go). The problem with this is that it’s a complicated script for technicians to update, and being a single threaded script the nice form it displays showing people what’s happening would freeze while it was working in the background.

My new script does the bulk of the work in background jobs – so printers add quicker (as it can do more than one at once), and the UI doesn’t lock up and freeze. More importantly, it uses Group Policy Preferences by reading the XML file generated and applies that – so technicians have the familiar interface for adding/removing printers from the script. 

First step for this is to create a group policy object – call it anything you like, mine is “Printers v3”. Make sure that you DO NOT link it to any OU (or if you do, the link is disabled) as we do not want this processed by the group policy client, this is purely to generate the XML file for the powershell script.

I’ll leave a detailed howto on group policy preferences but you should be able to find what you need with a quick search on the Internet.

The script uses the User Configuration set of preferences – as you can’t add shared printers to the Computer section, and currently only supports shared (i.e. networked and shared off a Windows server) printers.

Add your printer connections and re-order, if you have a “Delete All” it MUST come first.

Set up your list of printers – it doesn’t matter whether you select Create, Replace or Update as they all do the same thing in this instance. If you set multiple printers to be default, whichever runs last will win.

You can use item level targeting to determine which groups/computers/users/OUs get which printers – it supports collections, which basically apply like brackets around the logic, so you can be pretty targeted if required.

Now on to the script. You will need the GUID of the policy, this can be found in Group Policy Management Console by selecting the GPO, then going onto the “Details” tab, copy the Unique ID and insert that where prompted near the start of the script, along with your fully qualified domain name (on the same screen, this is “Domain” at the top).

As the preferences file is XML it’s easy to read into powershell.

When displaying the printer details to the end user, it will show whatever is down as the printer name in the GPP section. By default this is the share name, however you can rename this by selecting the item and hitting F2.

Set this as the logon script in Group Policy – you will need to make sure you are allowing all scripts to run (Computer Configuration->Policies->Windows Components->Windows Powershell->Turn on Script Execution) or set it as a non-Powershell logon script with the command:

powershell.exe -executionpolicy bypass -file \\path\to\printers.ps1"

Download Script

# Printers Logon Script
#
# Uses a Group Policy Object's User Preference items to add printers.
# Create the GPO but do not link it, it should be used by this script but NOT processed by group policy at logon.
#
# Katy Nicholson 04/08/2020
# katynicholson.uk

# Set your domain and the GUID of the policy object here.
$domain = "fully.qualified.domain.name"
$policyGUID = "{C96580A0-00DC-4E63-899A-3D82CF5EE141}"

$GPOxmlFile = "\\$domain\sysvol\$domain\Policies\$policyGUID\User\Preferences\Printers\Printers.xml"

#Set up the form to show progress to the user
Add-Type -AssemblyName System.Windows.Forms
#Force the form to show
$t = '[DllImport("user32.dll")] public static extern bool ShowWindow(int handle, int state);'
try{
    Add-Type -Name win -Member $t -Namespace native
    [native.win]::ShowWindow(([System.Diagnostics.Process]::GetCurrentProcess() | Get-Process).MainWindowHandle, 0)
} catch {
    $Null
}
$global:PrinterForm = New-Object System.Windows.Forms.Form
$global:Font = New-Object System.Drawing.Font("Calibri", 12)
$global:LabelTop = 25

$PrinterForm.ClientSize = '500,300'
$PrinterForm.text = "Printers"
$PrinterForm.BackColor = "#ffffff"

$Label1 = New-Object System.Windows.Forms.Label
$Label1.Text = "Please wait while your printer connections are configured..."
$Label1.Font = $Font
$Label1.Width = 500
$Label1.Height = 30
$Label1.Top = 10
$Label1.Left = 10

$PrinterForm.Controls.Add($Label1)
[void]$PrinterForm.Refresh()
[void]$PrinterForm.Show()
[void]$PrinterForm.Focus()

#Supporting functions

#Removes domain when in the format DOMAIN\username
Function stripDomain($item) {
    if ($item.IndexOf('\') -gt 0) {
        return $item.Substring(($item.IndexOf('\')+1))
    } else {
        return $item
    }
}

#Searches AD to check if the specified username is in the specified group
Function isInGroup([String]$groupname, [String]$username) {
    #Strip domain from group and user names, if present
    $groupname = stripDomain($groupname)
    $username = stripDomain($username)

    if ($group = ([ADSISearcher]"sAMAccountName=$groupname").FindOne()) {
        $groupDN = $group.Properties['distinguishedName']
        if ($isMember = ([ADSISearcher]"(&(|(objectClass=computer)(objectClass=person))(sAMAccountName=$username)(memberOf:1.2.840.113556.1.4.1941:=$groupDN))").FindOne()) {
            return $true
        }
    }
    return $false
}

#Evaluate the item targeting filters
Function evaluateFilters($Filters) {

    $FilterResults = @()
    foreach ($Filter in $Filters.ChildNodes) {
        #Process filters on this item
        [Bool]$return = $false
        switch ($Filter.LocalName) {
            "FilterCollection" {
                #Recurse through collections
                return evaluateFilters $Filter
            }
            "FilterGroup" {
                #Filter to whether the user (or computer) is a member of the specified group
                if ($Filter.userContext -eq "1") {
                    $username = $env:USERNAME
                } else {
                    $username = "$env:COMPUTERNAME$"
                }
                if (isInGroup $Filter.name $username) {
                    $return = $true
                }
                break
            }
            "FilterComputer" {
                #Filter if the computer is the specified name
                if ($env:COMPUTERNAME -ieq (stripDomain $Filter.name)) {
                    $return = $true
                }
                break
            }
            "FilterUser" {
                #Filter if the user is the specified name
                if ($env:USERNAME -ieq (stripDomain $Filter.name)) {
                    $return = $true
                }
                break
            }
            "FilterOrgUnit" {
                #Filter if the user or computer is a member of the specified OU (either directly or in a child OU)
                if ($Filter.userContext -eq "1") {
                    $itemDN = ([ADSISearcher]"sAMAccountName=$env:USERNAME").FindOne().Properties.distinguishedname[0]
                } else {
                    $itemDN = ([ADSISearcher]"sAMAccountName=$env:COMPUTERNAME$").FindOne().Properties.distinguishedname[0]
                }

                if ($itemDN.IndexOf($Filter.name, [System.StringComparison]::OrdinalIgnoreCase) -gt -1) {
                    #Item is in the OU
                    $OUPath = $itemDN.SubString($itemDN.IndexOf("OU="))
                    if ($Filter.directmember -eq "1") {
                        $return = ($OUPath -ieq $Filter.name)
                    } else {
                        $return = $true
                    }
                }
                break
            }
        }
        # If filter is set to NOT then negate return value
        if ($Filter.not -eq "1") {
            $FilterResults += ,(@($Filter.bool.ToString(),(!$return)))
        } else {
            $FilterResults += ,(@($Filter.bool.ToString(),($return)))
        }
    }

    #Evaluate the filters = AND or OR the first two results, depending on what is configured, then AND/OR this with the next value, repeat until done.
    $previousItem = $true
    foreach ($item in $FilterResults) {

        if ($item[0] -eq "AND") {
            if ($previousItem -and $item[1]) {
                $previousItem = $true
            } else {
                $previousItem = $false
            }
        } else {
            if ($previousItem -or $item[1]) {
                $previousItem = $true
            } else {
                $previousItem = $false
            }
        }
    }
    return $previousItem
}

#Update the status labels on the form
Function updateStatus($printerName, $status) {

    foreach ($Label in $PrinterForm.Controls) {
        if ($Label.Tag -and $printerName -eq $Label.Tag.Printer.ToString()) {
            $Label.Text = $Label.Tag.Display.ToString() + " - " + $status
            return
        }
    }
    $PrinterForm.Refresh()
}

#Add an item status label to the form
Function createStatus($printerName, $displayName, $status) {
    $LabelTop += 25
    $NewLabel = New-Object System.Windows.Forms.Label
    $NewLabel.Text = $displayName + " - " + $status
    $NewLabel.Tag = [PSCustomObject]@{
        "Printer"=$printerName
        "Display"=$displayName
    }
    $NewLabel.Font = $Font
    $NewLabel.Width = 500
    $NewLabel.Height = 20
    $NewLabel.Top = $LabelTop
    $NewLabel.Left = 20
    $PrinterForm.Controls.Add($NewLabel)
    $PrinterForm.Refresh()
    Set-Variable -Name "LabelTop" -Value $LabelTop -Scope Global
}

[xml]$Printers = Get-Content -Path $GPOxmlFile
#Logic for adding printers, to run in background job
$AddPrinterScriptBlock = {

    Function AddPrinter([String]$printer, $default) {

        # When Windows 10 decides it doesn't want to add on the first go at random intervals - retry forever until it adds
        try {
            Add-Printer -ConnectionName $printer -ErrorAction Stop
        } catch {
            AddPrinter $printer $default
        }
        #Set default printer if specified
        if ($default -eq "1") {
            (New-Object -ComObject WScript.Network).SetDefaultPrinter($printer)
        }
    }
    AddPrinter $args[0] $args[1]
}

#Logic for removing printers, to run in background job
$RemovePrinterScriptBlock = {

    Function RemovePrinter([String]$printer) {
        if ($printer -eq "All") {
            #Remove all printer connections
            Get-Printer | Where {$_.Type -eq "Connection"} | Remove-Printer
        } else {
            #Remove specified printer connections
            (New-Object -ComObject WScript.Network).RemovePrinterConnection($printer)
        }
    }
    RemovePrinter $args[0]
}

#Loop through each printer connection in the GPP list, if it has item level targeting (filters) then evaluate these. If matched, perform the relevant action.
foreach ($Printer in $Printers.Printers.SharedPrinter) {
    [Bool]$matched = $true

    if ($Printer.Filters.HasChildNodes) {
        $matched = evaluateFilters $Printer.Filters
    }
    if ($matched -eq $true) {
        if ($Printer.Properties.action -eq "D") {
            if ($Printer.Properties.deleteAll -eq "1") {
                #Delete All network printers - we Wait-Job on this one as we don't want it running at the same time as we are creating printer connections.
                #Delete All needs to be priority 1 in the GPP list.
                createStatus "DeleteAll" "Clean up old printers" "Running..."
                Start-Job $RemovePrinterScriptBlock -Name "DeleteAll" -ArgumentList "All" | Out-Null
                Wait-Job -Name "DeleteAll"
                updateStatus "DeleteAll" "Complete"
            } else {
                createStatus $Printer.name ("Remove " + $printer.name.ToString()) "Not started"
                Start-Job $RemovePrinterScriptBlock -Name $printer.name.ToString() -ArgumentList $Printer.Properties.path | Out-Null
            }
        } else {
            createStatus $Printer.name ("Add " + $Printer.name.ToString()) "Not started"
            Start-Job $AddPrinterScriptBlock -Name $Printer.name.ToString() -ArgumentList @($Printer.Properties.path, $Printer.Properties.default) | Out-Null
        }
    }
}

#Check on the background jobs, updating the UI.
While ($jobs = Get-Job -State "Running")
{
    ForEach ($job in $jobs) {
        switch ($job.State) {
            "Running" {
                updateStatus $job.Name "Running..."
                break
            }
            "Completed" {
                updateStatus $job.Name "Complete"
                break
            }
            "Failed" {
                updateStatus $job.Name "Failed"
                break
            }
        }
    }
    Start-Sleep -Milliseconds 100 
    [System.Windows.Forms.Application]::DoEvents()
}
Start-Sleep 1
$PrinterForm.Hide()
#Clean up
Get-Job | Remove-Job

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.