printer.ps1powershell
<#
.SYNOPSIS
Captures the printer configuration of one or more Windows PCs and writes, per PC,
a restore script named <COMPUTERNAME>.ps1 that sets the same configuration up again.
.DESCRIPTION
Captured per PC:
- Local printer queues: name, driver, port(s), comment, location, sharing,
print processor, schedule, separator page
- Printing defaults: the full PrintTicket, plus paper size, duplex, color and
collate as a fallback
- Installable options (Config:* device settings such as duplex unit or trays)
- Standard TCP/IP (RAW and LPR) and Local ports used by those queues
- Drivers, and with -IncludeDrivers the driver packages themselves
- Network printer connections (\\server\printer) and the default printer of
every user profile that is loaded at export time
The generated script embeds the configuration as JSON, needs nothing else apart
from the optional <COMPUTERNAME>_Drivers folder, and is safe to re-run: printers
and connections that already exist on the target PC are left as they are.
Run elevated to also capture other logged-on users. A non-elevated run captures
the current user's connections and default printer only. Printers redirected
through Remote Desktop are ignored.
.PARAMETER ComputerName
PC(s) to capture. Default: this PC. Remote PCs are captured in parallel over
PowerShell remoting (WinRM) with an account that is admin on them. Only users
logged on at that moment have their connections captured.
.PARAMETER OutputPath
Folder for the generated scripts and driver folders. Default: current folder.
.PARAMETER IncludeDrivers
Copy the packages of non-inbox drivers to <COMPUTERNAME>_Drivers next to the
script, so the restore also works on a PC where the drivers are missing.
.PARAMETER IncludeVirtual
Also capture software printers (Print to PDF, XPS, OneNote, Fax).
.PARAMETER Credential
Credential for remote PCs.
.EXAMPLE
.\Export-PrinterConfig.ps1 -IncludeDrivers
.EXAMPLE
.\Export-PrinterConfig.ps1 -ComputerName PC01, PC02 -OutputPath \\fs01\IT$\Printers -IncludeDrivers
.NOTES
Requires Windows 8 / Server 2012 or later (PrintManagement module) and
Windows PowerShell 5.1 or PowerShell 7.
#>
[CmdletBinding()]
param(
[string[]]$ComputerName = $env:COMPUTERNAME,
[string]$OutputPath = (Get-Location).Path,
[switch]$IncludeDrivers,
[switch]$IncludeVirtual,
[System.Management.Automation.PSCredential]$Credential
)
$ErrorActionPreference = 'Stop'
# -----------------------------------------------------------------------------
# Collector: runs on the source PC (locally or over remoting) and returns JSON.
# Kept self-contained so it can be sent to remote PCs unchanged.
# The PrintManagement cmdlets do not inherit the caller's ErrorActionPreference,
# so every call passes -ErrorAction itself.
# -----------------------------------------------------------------------------
$Collect = {
param([bool]$IncludeVirtual, [bool]$IncludeDrivers)
$ErrorActionPreference = 'Stop'
function Compress-Text([string]$Text) {
if (-not $Text) { return $null }
$bytes = [Text.Encoding]::UTF8.GetBytes($Text)
$ms = New-Object IO.MemoryStream
$gz = New-Object IO.Compression.GZipStream($ms, [IO.Compression.CompressionMode]::Compress)
$gz.Write($bytes, 0, $bytes.Length)
$gz.Dispose()
[Convert]::ToBase64String($ms.ToArray())
}
$virtual = '^(Microsoft (Print To PDF|XPS Document Writer|Shared Fax Driver|Software Printer Driver)|Send to Microsoft OneNote)'
$printers = @(Get-Printer -ErrorAction Stop | Where-Object {
[string]$_.Type -eq 'Local' -and
[string]$_.DriverName -ne 'Remote Desktop Easy Print' -and
[string]$_.PortName -notmatch '^TS\d+' -and
($IncludeVirtual -or [string]$_.DriverName -notmatch $virtual)
})
# One call each for ports and drivers, then look up only what the queues use
$portIndex = @{}
foreach ($pp in @(Get-PrinterPort -ErrorAction Stop)) { $portIndex[[string]$pp.Name] = $pp }
$driverIndex = @{}
foreach ($d in @(Get-PrinterDriver -ErrorAction Stop | Sort-Object { [string]$_.PrinterEnvironment -eq 'Windows NT x86' })) {
if (-not $driverIndex.ContainsKey([string]$d.Name)) { $driverIndex[[string]$d.Name] = $d }
}
$portNames = @($printers | ForEach-Object { ([string]$_.PortName).Split(',') } |
ForEach-Object { $_.Trim() } | Where-Object { $_ } | Sort-Object -Unique)
$ports = foreach ($name in $portNames) {
$pp = $portIndex[$name]
$cimClass = ''
$hostAddress = ''
if ($pp) {
$cimClass = [string]$pp.CimClass.CimClassName
$hostAddress = [string]$pp.PrinterHostAddress
if (-not $hostAddress) { $hostAddress = [string]$pp.PrinterHostIP }
}
if ($hostAddress) {
[pscustomobject]@{
Name = $name
Kind = $(if ([int]$pp.Protocol -eq 2) { 'Lpr' } else { 'Tcp' })
Host = $hostAddress
PortNumber = [int]$pp.PortNumber
LprQueueName = [string]$pp.LprQueueName
LprByteCounting = [bool]$pp.LprByteCounting
SnmpEnabled = [bool]$pp.SNMPEnabled
SnmpCommunity = [string]$pp.SNMPCommunity
SnmpIndex = [int]$pp.SNMPIndex
}
}
elseif ($pp -and ($cimClass -eq 'MSFT_LocalPrinterPort' -or [string]$pp.PortMonitor -eq 'Local Port')) {
[pscustomobject]@{ Name = $name; Kind = 'Local' }
}
else {
# WSD, USB, IPP and similar ports are created by Windows for the device itself
[pscustomobject]@{ Name = $name; Kind = 'Other'; Monitor = $(if ($pp) { ([string]$pp.PortMonitor).Trim() } else { 'not found' }) }
}
}
$drivers = foreach ($name in @($printers | ForEach-Object { [string]$_.DriverName } | Sort-Object -Unique)) {
$d = $driverIndex[$name]
$inf = ''
if ($d) { $inf = [string]$d.InfPath }
$infLeaf = ''
if ($inf) { $infLeaf = Split-Path -Path $inf -Leaf }
# Copy only third-party packages from the driver store, never inbox ones (ntprint, prnms)
$copy = $IncludeDrivers -and $inf -and ([string]$d.Manufacturer -ne 'Microsoft') -and
($inf -like '*\DriverStore\FileRepository\*') -and
($infLeaf -notlike 'ntprint*') -and ($infLeaf -notlike 'prnms*') -and
(Test-Path -LiteralPath $inf)
[pscustomobject]@{
Name = $name
Manufacturer = $(if ($d) { [string]$d.Manufacturer })
InfName = $infLeaf
Package = $(if ($copy) { Split-Path -Path (Split-Path -Path $inf -Parent) -Leaf })
SourcePath = $(if ($copy) { Split-Path -Path $inf -Parent })
}
}
$queues = foreach ($p in $printers) {
$cfg = $null
$deviceSettings = @()
try { $cfg = Get-PrintConfiguration -PrinterName $p.Name -ErrorAction Stop }
catch { Write-Warning "[$env:COMPUTERNAME] '$($p.Name)': printing defaults not readable: $($_.Exception.Message)" }
try {
$deviceSettings = @(Get-PrinterProperty -PrinterName $p.Name -ErrorAction Stop |
Where-Object { [string]$_.PropertyName -like 'Config:*' } |
ForEach-Object { [pscustomobject]@{ Name = [string]$_.PropertyName; Value = [string]$_.Value } })
}
catch { }
[pscustomobject]@{
Name = [string]$p.Name
DriverName = [string]$p.DriverName
PortName = [string]$p.PortName
Comment = [string]$p.Comment
Location = [string]$p.Location
Shared = [bool]$p.Shared
ShareName = [string]$p.ShareName
Published = [bool]$p.Published
Datatype = [string]$p.Datatype
PrintProcessor = [string]$p.PrintProcessor
KeepPrintedJobs = [bool]$p.KeepPrintedJobs
Priority = [int]$p.Priority
StartTime = [int]$p.StartTime
UntilTime = [int]$p.UntilTime
SeparatorPageFile = [string]$p.SeparatorPageFile
PaperSize = $(if ($cfg) { [string]$cfg.PaperSize })
Color = $(if ($cfg) { [bool]$cfg.Color })
DuplexingMode = $(if ($cfg) { [string]$cfg.DuplexingMode })
Collate = $(if ($cfg) { [bool]$cfg.Collate })
PrintTicket = $(if ($cfg) { Compress-Text $cfg.PrintTicketXML })
DeviceSettings = $deviceSettings
}
}
# Per-user connections and default printer, read from every loaded profile hive
$users = foreach ($hive in @(Get-ChildItem -Path 'Registry::HKEY_USERS' -ErrorAction SilentlyContinue)) {
$sid = $hive.PSChildName
if ($sid -notmatch '^S-1-(5-21|12-1)-[\d-]+$') { continue } # real accounts only, no *_Classes hives
$root = "Registry::HKEY_USERS\$sid"
$connections = @(Get-ChildItem -Path "$root\Printers\Connections" -ErrorAction SilentlyContinue |
ForEach-Object { $_.PSChildName.Replace(',', '\') } |
Where-Object { $_ -match '^\\\\[^\\]+\\[^\\]+$' })
$deviceValue = (Get-ItemProperty -Path "$root\Software\Microsoft\Windows NT\CurrentVersion\Windows" -Name Device -ErrorAction SilentlyContinue).Device
$defaultPrinter = $null
if ($deviceValue) { $defaultPrinter = ([string]$deviceValue).Split(',')[0] }
if (-not $connections.Count -and -not $defaultPrinter) { continue }
$account = $sid
try { $account = ([Security.Principal.SecurityIdentifier]$sid).Translate([Security.Principal.NTAccount]).Value } catch { }
[pscustomobject]@{ User = $account; Sid = $sid; DefaultPrinter = $defaultPrinter; Connections = $connections }
}
[pscustomobject]@{
ComputerName = $env:COMPUTERNAME
Json = ConvertTo-Json -Depth 6 -InputObject ([ordered]@{
SchemaVersion = 1
ComputerName = $env:COMPUTERNAME
ExportedAt = (Get-Date).ToString('yyyy-MM-dd HH:mm')
ExportedBy = "$env:USERDOMAIN\$env:USERNAME"
Drivers = @($drivers)
Ports = @($ports)
Printers = @($queues)
Users = @($users)
})
}
}
# -----------------------------------------------------------------------------
# Restore script template. Tokens: __PC__, __DATE__, __SUMMARY__, __CONFIG__
# -----------------------------------------------------------------------------
$RestoreTemplate = @'
<#
.SYNOPSIS
Restores the printer configuration captured from __PC__ on __DATE__.
.DESCRIPTION
Generated by Export-PrinterConfig.ps1. Captured:
__SUMMARY__
When started without -All or -Printer, it lists the captured printers and asks
which ones to restore: type numbers or ranges (1,3,5-7), A for all, Q to quit.
Unattended runs (no console to ask) restore everything.
Safe to re-run: printers and connections that already exist are left as they
are (see -Force). What gets restored depends on how it is run:
- Elevated (Run as administrator): drivers, ports, local printers, device
options and printing defaults. Also the connections and default printer
when the elevated account is the captured user.
- As the end user (not elevated): that user's network printer connections
and default printer.
Driver packages are installed from the __PC___Drivers folder next to this
script when it is present.
.PARAMETER All
Restore everything without asking.
.PARAMETER Printer
Restore only these printers, without asking. Local printer names or network
paths (\\server\printer); wildcards allowed, e.g. -Printer 'HP*', '\\print01\*'.
.PARAMETER GridView
Pick printers in a grid window (Out-GridView, Ctrl+A selects all) instead of
the numbered console list.
.PARAMETER User
Apply the connections and default printer captured for this user (DOMAIN\name,
name or SID) to the current account. Default: the captured user that matches
the current account (same SID, else same name).
.PARAMETER AllUsers
Add the captured network connections for every user of this PC (printui /ga,
needs elevation). They appear at the next logon.
.PARAMETER Force
Also update local printers that already exist, in place (driver, port, sharing,
comment and so on), and reapply their device options and printing defaults.
Printers are never deleted.
.PARAMETER SkipConnections
Do not add network printer connections.
.PARAMETER SkipDefault
Do not change the default printer.
.EXAMPLE
.\__PC__.ps1
Shows the list and asks which printers to restore.
.EXAMPLE
.\__PC__.ps1 -All -WhatIf
Shows what restoring everything would do, without changing anything.
.EXAMPLE
.\__PC__.ps1 -Printer 'HP*', '\\print01\Finance-A3'
.EXAMPLE
.\__PC__.ps1 -All -AllUsers
#>
[CmdletBinding(SupportsShouldProcess = $true)]
param(
[switch]$All,
[string[]]$Printer,
[switch]$GridView,
[string]$User,
[switch]$AllUsers,
[switch]$Force,
[switch]$SkipConnections,
[switch]$SkipDefault
)
__CONFIG__
# Note: the PrintManagement cmdlets do not inherit this script's ErrorActionPreference,
# so every call to them passes -ErrorAction itself.
$ErrorActionPreference = 'Stop'
if ($User -and $AllUsers) { throw 'Use either -User or -AllUsers, not both.' }
if ($All -and $Printer) { throw 'Use either -All or -Printer, not both.' }
$Cmdlet = $PSCmdlet
$Config = $ConfigJson | ConvertFrom-Json
$Results = New-Object System.Collections.Generic.List[object]
$Me = "$env:USERDOMAIN\$env:USERNAME"
$ScriptDir = if ($PSScriptRoot) { $PSScriptRoot } else { (Get-Location).Path }
$MySid = ''
$IsAdmin = $false
try {
$identity = [Security.Principal.WindowsIdentity]::GetCurrent()
$MySid = $identity.User.Value
$IsAdmin = ([Security.Principal.WindowsPrincipal]$identity).IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)
}
catch { }
if (-not (Get-Command -Name Add-Printer -ErrorAction SilentlyContinue)) {
throw 'PrintManagement module not found (Windows 8 / Server 2012 or later required).'
}
if ((Get-Service -Name Spooler).Status -ne 'Running') { throw 'The Print Spooler service is not running.' }
function Add-Result([string]$Area, [string]$Item, [string]$Status, [string]$Detail) {
$Results.Add([pscustomobject]@{ Area = $Area; Item = $Item; Status = $Status; Detail = $Detail })
}
# Runs one change with -WhatIf / -Confirm support and records the outcome.
# A step block returns an optional note; a note starting with WARNING: marks the step as Warning.
function Invoke-Step([string]$StepArea, [string]$StepItem, [string]$StepAction, [scriptblock]$StepBlock) {
if (-not $Cmdlet.ShouldProcess($StepItem, $StepAction)) {
Add-Result $StepArea $StepItem $(if ($WhatIfPreference) { 'WhatIf' } else { 'Skipped' }) $StepAction
return [bool]$WhatIfPreference
}
try {
$note = (@(& $StepBlock) | Where-Object { $null -ne $_ }) -join ' '
Add-Result $StepArea $StepItem $(if ($note -like 'WARNING:*') { 'Warning' } else { 'Done' }) (("$StepAction $note").Trim())
return $true
}
catch {
Add-Result $StepArea $StepItem 'Failed' $_.Exception.Message
return $false
}
}
function Get-NameSet([scriptblock]$Query) {
$set = @{}
try { foreach ($o in @(& $Query)) { $set[[string]$o.Name] = $true } } catch { }
$set
}
function Split-PortList([string]$PortName) {
$PortName.Split(',') | ForEach-Object { $_.Trim() } | Where-Object { $_ }
}
# Captured user record for an account: same SID first, then same full name, then same user name
function Find-User([object[]]$Users, [string]$Name, [string]$Sid) {
$hit = @($Users | Where-Object { $Sid -and $_.Sid -eq $Sid })
if (-not $hit.Count) { $hit = @($Users | Where-Object { $_.User -eq $Name }) }
if (-not $hit.Count) {
$leaf = ($Name -split '\\')[-1]
$hit = @($Users | Where-Object { ([string]$_.User -split '\\')[-1] -eq $leaf })
}
if ($hit.Count) { $hit[0] }
}
# Add-Printer / Set-Printer parameters for a captured queue (without Name and PortName)
function Get-QueueSettings($Queue, [switch]$Full) {
$s = @{ DriverName = [string]$Queue.DriverName }
if ($Queue.Comment) { $s.Comment = [string]$Queue.Comment }
if ($Queue.Location) { $s.Location = [string]$Queue.Location }
if ($Full) {
if ($Queue.Shared) {
$s.Shared = $true
if ($Queue.ShareName) { $s.ShareName = [string]$Queue.ShareName }
}
if ($Queue.Published) { $s.Published = $true }
if ($Queue.Datatype) { $s.Datatype = [string]$Queue.Datatype }
if ($Queue.PrintProcessor) { $s.PrintProcessor = [string]$Queue.PrintProcessor }
if ($Queue.KeepPrintedJobs) { $s.KeepPrintedJobs = $true }
if ($Queue.Priority -gt 1) { $s.Priority = [uint32]$Queue.Priority }
if ($Queue.StartTime -or $Queue.UntilTime) {
$s.StartTime = [uint32]$Queue.StartTime
$s.UntilTime = [uint32]$Queue.UntilTime
}
if ($Queue.SeparatorPageFile) { $s.SeparatorPageFile = [string]$Queue.SeparatorPageFile }
}
$s
}
function Expand-Text([string]$Base64) {
$ms = New-Object IO.MemoryStream -ArgumentList (, [Convert]::FromBase64String($Base64))
$gz = New-Object IO.Compression.GZipStream($ms, [IO.Compression.CompressionMode]::Decompress)
$reader = New-Object IO.StreamReader($gz, [Text.Encoding]::UTF8)
try { $reader.ReadToEnd() } finally { $reader.Dispose() }
}
function Test-NameMatch([string]$Name, [string]$Pattern) {
if ($Name -eq $Pattern) { return $true }
try { return ($Name -like $Pattern) } catch { return $false } # invalid wildcard, e.g. a lone [
}
# True when someone can answer a prompt (not a scheduled task, service or -NonInteractive run)
function Test-CanPrompt {
if (-not [Environment]::UserInteractive) { return $false }
if (@([Environment]::GetCommandLineArgs() | Where-Object { $_ -like '-noni*' }).Count) { return $false }
$true
}
# Shows the captured printers and returns the ones picked
function Select-Items($Items) {
$table = for ($i = 0; $i -lt $Items.Count; $i++) {
[pscustomobject]@{
'#' = $i + 1
Type = $Items[$i].Kind
Name = $Items[$i].Name
Default = $(if ($Items[$i].Name -eq $defaultName) { 'yes' } else { '' })
'On this PC' = $Items[$i].State
Details = $Items[$i].Details
}
}
if ($GridView) {
if (Get-Command -Name Out-GridView -ErrorAction SilentlyContinue) {
$title = "Printers from $($Config.ComputerName): select the ones to restore (Ctrl+A selects all)"
return @($table | Out-GridView -Title $title -OutputMode Multiple | ForEach-Object { $Items[$_.'#' - 1] })
}
Write-Host 'Out-GridView is not available here, using the console list instead.' -ForegroundColor Yellow
}
Write-Host ''
Write-Host "Printers captured from $($Config.ComputerName) on $($Config.ExportedAt):"
$table | Format-Table -AutoSize -Wrap | Out-Host
while ($true) {
$answer = ([string](Read-Host 'Printers to restore: numbers or ranges (e.g. 1,3,5-7), A = all, Q = quit')).Trim()
if ($answer -match '^(a|all|\*)$') { return @($Items) }
if ($answer -match '^(q|quit)$') { return @() }
$numbers = New-Object System.Collections.Generic.List[int]
$invalid = @()
foreach ($token in @($answer -split '[,;\s]+' | Where-Object { $_ })) {
if ($token -match '^(\d+)-(\d+)$') {
$from = [int]$Matches[1]
$to = [int]$Matches[2]
foreach ($n in $from..$to) { $numbers.Add($n) }
}
elseif ($token -match '^\d+$') { $numbers.Add([int]$token) }
else { $invalid += $token }
}
$invalid += @($numbers | Where-Object { $_ -lt 1 -or $_ -gt $Items.Count })
if ($invalid.Count -or -not $numbers.Count) {
Write-Host ("Not valid: {0}. Use numbers from 1 to {1}, A or Q." -f ($(if ($invalid.Count) { $invalid -join ', ' } else { '(empty)' })), $Items.Count) -ForegroundColor Yellow
continue
}
return @($numbers | Sort-Object -Unique | ForEach-Object { $Items[$_ - 1] })
}
}
$HavePrinters = Get-NameSet { Get-Printer -ErrorAction Stop }
# ============================== Selection =====================================
$printers = @($Config.Printers | Where-Object { $_ })
$users = @($Config.Users | Where-Object { $_ })
$current = Find-User $users $Me $MySid
$targets = @()
if ($User) {
$hit = Find-User $users $User $User
if ($hit) { $targets = @($hit) }
}
elseif ($AllUsers) { $targets = $users }
elseif ($current) { $targets = @($current) }
$defaultFrom = if ($AllUsers) { $current } else { $targets | Select-Object -First 1 }
$defaultName = ''
if (-not $SkipDefault -and $defaultFrom) { $defaultName = [string]$defaultFrom.DefaultPrinter }
$connections = @()
if (-not $SkipConnections) {
$connections = @($targets | ForEach-Object { @($_.Connections) } | Where-Object { $_ } | Sort-Object -Unique)
}
# One list of everything this run can restore
$items = New-Object System.Collections.Generic.List[object]
foreach ($p in $printers) {
$state = if ($HavePrinters.ContainsKey($p.Name)) { 'installed' } elseif ($IsAdmin) { 'missing' } else { 'missing, needs elevation' }
$items.Add([pscustomobject]@{ Kind = 'Local'; Name = [string]$p.Name; State = $state; Details = "$($p.PortName) | $($p.DriverName)" })
}
foreach ($cn in $connections) {
$state = if ($HavePrinters.ContainsKey($cn)) { 'installed' } else { 'missing' }
$items.Add([pscustomobject]@{ Kind = 'Network'; Name = [string]$cn; State = $state; Details = $(if ($AllUsers) { 'for all users' } else { "for $Me" }) })
}
if ($defaultName -and -not @($items | Where-Object { $_.Name -eq $defaultName }).Count) {
# Default printer that is neither a captured queue nor a connection (e.g. Microsoft Print to PDF)
$state = if ($HavePrinters.ContainsKey($defaultName)) { 'installed' } else { 'missing' }
$items.Add([pscustomobject]@{ Kind = 'Other'; Name = $defaultName; State = $state; Details = 'only set as default printer' })
}
if (-not $items.Count) { $selected = @() }
elseif ($All) { $selected = @($items) }
elseif ($Printer) {
$selected = @($items | Where-Object { $itemName = $_.Name; @($Printer | Where-Object { Test-NameMatch $itemName $_ }).Count })
foreach ($pattern in $Printer) {
if (-not @($items | Where-Object { Test-NameMatch $_.Name $pattern }).Count) {
Add-Result 'Selection' $pattern 'Failed' 'No captured printer matches this name'
}
}
}
elseif (Test-CanPrompt) {
$selected = @(Select-Items $items)
if (-not $selected.Count) {
Write-Host 'Nothing selected, no changes made.'
exit 0
}
}
else {
$selected = @($items)
Add-Result 'Selection' 'all' 'Info' 'Unattended run: everything selected (use -Printer to limit)'
}
$picked = @{}
foreach ($it in $selected) { $picked[[string]$it.Name] = $true }
$printers = @($printers | Where-Object { $picked.ContainsKey([string]$_.Name) })
$connections = @($connections | Where-Object { $picked.ContainsKey([string]$_) })
$setDefault = [bool]($defaultName -and $picked.ContainsKey($defaultName))
# ============================ Machine level ===================================
if ($printers.Count -and -not $IsAdmin) {
Add-Result 'Machine' "$($printers.Count) local printer(s)" 'Skipped' 'Run elevated to restore drivers, ports, local printers and their settings'
}
elseif ($printers.Count) {
$HaveDrivers = Get-NameSet { Get-PrinterDriver -ErrorAction Stop }
$HavePorts = Get-NameSet { Get-PrinterPort -ErrorAction Stop }
$portInfo = @{}
foreach ($pt in @($Config.Ports | Where-Object { $_ })) { $portInfo[[string]$pt.Name] = $pt }
$driverInfo = @{}
foreach ($dr in @($Config.Drivers | Where-Object { $_ })) { $driverInfo[[string]$dr.Name] = $dr }
$driverDir = Join-Path $ScriptDir "$($Config.ComputerName)_Drivers"
# Printers to create, plus (with -Force) existing ones to update in place
$toProcess = @($printers | Where-Object { $Force -or -not $HavePrinters.ContainsKey($_.Name) })
# 1. Drivers: from the exported package when present, otherwise from the driver store
$pnputil = Join-Path $env:SystemRoot 'Sysnative\pnputil.exe' # 32-bit host on 64-bit Windows
if (-not (Test-Path -LiteralPath $pnputil)) { $pnputil = Join-Path $env:SystemRoot 'System32\pnputil.exe' }
$pnpAdd = if ([Environment]::OSVersion.Version.Build -ge 14393) { '/add-driver' } else { '-a' } # -a before Windows 10 1607
foreach ($name in @($toProcess | ForEach-Object { [string]$_.DriverName } | Sort-Object -Unique)) {
if ($HaveDrivers.ContainsKey($name)) { Add-Result 'Driver' $name 'Exists' ''; continue }
$dr = $driverInfo[$name]
$inf = ''
if ($dr -and $dr.Package -and $dr.InfName) { $inf = Join-Path (Join-Path $driverDir $dr.Package) $dr.InfName }
$hasPackage = $inf -and (Test-Path -LiteralPath $inf)
$ok = Invoke-Step 'Driver' $name 'Install' {
if ($hasPackage) {
$ErrorActionPreference = 'Continue' # pnputil text on stderr is not a failure
$PSNativeCommandUseErrorActionPreference = $false # PowerShell 7.4+: the exit code is checked below
$out = & $pnputil $pnpAdd $inf 2>&1
if ($LASTEXITCODE -notin @(0, 3010)) {
throw ('pnputil returned {0}. {1}' -f $LASTEXITCODE, (($out | ForEach-Object { [string]$_ }) -join ' '))
}
}
try { Add-PrinterDriver -Name $name -ErrorAction Stop }
catch {
if ($hasPackage) { throw }
throw "$($_.Exception.Message) The driver is not in the driver store: export with -IncludeDrivers or install it first."
}
if ($hasPackage) { 'from package' } else { 'from driver store' }
}
if ($ok) { $HaveDrivers[$name] = $true }
}
# 2. Ports: Standard TCP/IP (RAW, LPR) and Local ports. WSD and USB ports are
# created by Windows when the device is connected.
foreach ($pn in @($toProcess | ForEach-Object { Split-PortList $_.PortName } | Sort-Object -Unique)) {
if ($HavePorts.ContainsKey($pn)) { Add-Result 'Port' $pn 'Exists' ''; continue }
$pt = $portInfo[$pn]
$kind = 'Unknown'
if ($pt) { $kind = [string]$pt.Kind }
if ($kind -notin @('Tcp', 'Lpr', 'Local')) {
Add-Result 'Port' $pn 'Skipped' "$kind port ($($pt.Monitor)) cannot be created by script: connect or reinstall the device"
continue
}
$ok = Invoke-Step 'Port' $pn 'Create' {
$a = @{ Name = $pn }
if ($kind -eq 'Tcp') {
$a.PrinterHostAddress = $pt.Host
if ($pt.PortNumber) { $a.PortNumber = [uint32]$pt.PortNumber }
}
elseif ($kind -eq 'Lpr') {
$a.LprHostAddress = $pt.Host
$a.LprQueueName = $pt.LprQueueName
if ($pt.LprByteCounting) { $a.LprByteCounting = $true }
}
if ($kind -ne 'Local' -and $pt.SnmpEnabled) {
$a.SNMP = [uint32][Math]::Max(1, [int]$pt.SnmpIndex)
if ($pt.SnmpCommunity) { $a.SNMPCommunity = $pt.SnmpCommunity }
}
Add-PrinterPort @a -ErrorAction Stop
if ($kind -ne 'Local') { "$kind $($pt.Host)" }
}
if ($ok) { $HavePorts[$pn] = $true }
}
# 3. Printers: create the missing ones; with -Force also update existing ones in
# place (never deleted, so a working printer is not lost if something fails).
# Device options go before printing defaults, so that e.g. duplex can be set.
foreach ($p in $printers) {
$exists = $HavePrinters.ContainsKey($p.Name)
if ($exists -and -not $Force) { Add-Result 'Printer' $p.Name 'Exists' 'left as is (-Force updates it)'; continue }
$portList = @(Split-PortList $p.PortName)
if (-not $HaveDrivers.ContainsKey($p.DriverName)) {
Add-Result 'Printer' $p.Name 'Skipped' "Driver not available: $($p.DriverName)"
continue
}
$missingPorts = @($portList | Where-Object { -not $HavePorts.ContainsKey($_) })
if ($missingPorts.Count) {
Add-Result 'Printer' $p.Name 'Skipped' "Port not available: $($missingPorts -join ', ')"
continue
}
$basic = Get-QueueSettings $p
$full = Get-QueueSettings $p -Full
if ($exists) {
$ok = Invoke-Step 'Printer' $p.Name 'Update' {
try { Set-Printer -Name $p.Name -PortName ($portList -join ',') @full -ErrorAction Stop }
catch {
$why = $_.Exception.Message
if ($full.Count -eq $basic.Count) { throw }
Set-Printer -Name $p.Name -PortName ($portList -join ',') @basic -ErrorAction Stop
"WARNING: basic settings only ($why)"
}
}
}
else {
$ok = Invoke-Step 'Printer' $p.Name 'Create' {
$note = ''
try { Add-Printer -Name $p.Name -PortName $portList[0] @full -ErrorAction Stop }
catch {
$why = $_.Exception.Message
if ($full.Count -eq $basic.Count) { throw }
$note = "WARNING: created with basic settings only ($why)"
if (-not (Get-Printer -Name $p.Name -ErrorAction SilentlyContinue)) {
Add-Printer -Name $p.Name -PortName $portList[0] @basic -ErrorAction Stop
}
}
if ($portList.Count -gt 1) { Set-Printer -Name $p.Name -PortName ($portList -join ',') -ErrorAction Stop } # printer pool
$note
}
}
if (-not $ok) { continue }
$HavePrinters[$p.Name] = $true
$deviceSettings = @($p.DeviceSettings | Where-Object { $_ })
if ($deviceSettings.Count) {
[void](Invoke-Step 'Settings' $p.Name 'Device options' {
$applied = 0
foreach ($s in $deviceSettings) {
try { Set-PrinterProperty -PrinterName $p.Name -PropertyName $s.Name -Value $s.Value -ErrorAction Stop; $applied++ } catch { }
}
"$applied of $($deviceSettings.Count) applied (read-only ones are skipped)"
})
}
if ($p.PrintTicket -or $p.PaperSize) {
[void](Invoke-Step 'Settings' $p.Name 'Printing defaults' {
try {
if (-not $p.PrintTicket) { throw 'no PrintTicket captured' }
Set-PrintConfiguration -PrinterName $p.Name -PrintTicketXml (Expand-Text $p.PrintTicket) -ErrorAction Stop
'full PrintTicket'
}
catch {
# Different driver version: fall back to the common settings
$c = @{ PrinterName = $p.Name }
if ($p.PaperSize) { $c.PaperSize = $p.PaperSize }
if ($p.DuplexingMode) { $c.DuplexingMode = $p.DuplexingMode }
if ($null -ne $p.Color) { $c.Color = [bool]$p.Color }
if ($null -ne $p.Collate) { $c.Collate = [bool]$p.Collate }
Set-PrintConfiguration @c -ErrorAction Stop
'paper, duplex, color and collate only'
}
})
}
}
}
# ============================== User level ====================================
if ($users.Count -and -not ($SkipConnections -and $SkipDefault)) {
if (-not $targets.Count) {
$captured = ($users | ForEach-Object { $_.User }) -join ', '
Add-Result 'User' $(if ($User) { $User } else { $Me }) 'Skipped' "No settings captured for this account. Captured: $captured. Use -User <name> or -AllUsers."
}
if ($connections.Count) {
if ($AllUsers -and -not $IsAdmin) {
Add-Result 'Connection' "$($connections.Count) connection(s)" 'Skipped' '-AllUsers needs an elevated session'
}
else {
$machineConnections = 'HKLM:\SYSTEM\CurrentControlSet\Control\Print\Connections' # where printui /ga keeps them
foreach ($cn in $connections) {
if ($AllUsers) {
$connKey = Join-Path $machineConnections $cn.Replace('\', ',')
if (Test-Path -LiteralPath $connKey) { Add-Result 'Connection' $cn 'Exists' 'already added for all users'; continue }
[void](Invoke-Step 'Connection' $cn 'Add for all users' {
Start-Process -FilePath rundll32.exe -ArgumentList "printui.dll,PrintUIEntry /ga /n`"$cn`" /q" -Wait -WindowStyle Hidden
if (Test-Path -LiteralPath $connKey) { 'appears at next logon' }
else { 'WARNING: printui gave no error but the connection is not listed, check with: rundll32 printui.dll,PrintUIEntry /ge' }
})
}
elseif ($HavePrinters.ContainsKey($cn)) {
Add-Result 'Connection' $cn 'Exists' ''
}
elseif (Invoke-Step 'Connection' $cn 'Add' {
try { Add-Printer -ConnectionName $cn -ErrorAction Stop }
catch { throw "$($_.Exception.Message) Check that the server is reachable and that Point and Print policy allows the driver install." }
}) {
$HavePrinters[$cn] = $true
}
}
}
}
if ($setDefault) {
$dp = $defaultName
$wp = @()
try { $wp = @(Get-CimInstance -ClassName Win32_Printer | Where-Object { $_.Name -eq $dp }) } catch { }
if (-not $wp.Count -and -not ($WhatIfPreference -and $HavePrinters.ContainsKey($dp))) {
Add-Result 'Default' $dp 'Skipped' 'Not on this PC yet: run again once it is installed (-AllUsers connections appear at the next logon)'
}
elseif ($wp.Count -and $wp[0].Default) {
Add-Result 'Default' $dp 'Exists' 'already the default printer'
}
else {
[void](Invoke-Step 'Default' $dp 'Set default printer' {
# Keep Windows from replacing the default with the last used printer
Set-ItemProperty -Path 'HKCU:\Software\Microsoft\Windows NT\CurrentVersion\Windows' -Name LegacyDefaultPrinterMode -Value 1 -Type DWord
$r = Invoke-CimMethod -InputObject $wp[0] -MethodName SetDefaultPrinter
if ($r.ReturnValue -ne 0) { throw "SetDefaultPrinter returned $($r.ReturnValue)" }
})
}
}
}
# ================================ Summary =====================================
if ($Results.Count) { $Results | Format-Table -AutoSize -Wrap | Out-Host }
else { Write-Host 'Nothing to restore.' }
$failed = @($Results | Where-Object { $_.Status -eq 'Failed' }).Count
if ($failed) {
Write-Warning "$failed step(s) failed, see the Detail column."
exit 1
}
'@
# -----------------------------------------------------------------------------
# Main
# -----------------------------------------------------------------------------
if (-not (Test-Path -LiteralPath $OutputPath)) { New-Item -ItemType Directory -Path $OutputPath -Force | Out-Null }
$OutputPath = (Resolve-Path -LiteralPath $OutputPath).ProviderPath
$collectArgs = @([bool]$IncludeVirtual, [bool]$IncludeDrivers)
$localNames = @('.', 'localhost', '127.0.0.1', $env:COMPUTERNAME)
$captures = New-Object System.Collections.Generic.List[object]
$sessions = @()
try {
if (@($ComputerName | Where-Object { $_ -in $localNames }).Count) {
try { $captures.Add([pscustomobject]@{ Result = (& $Collect @collectArgs); Session = $null }) }
catch { Write-Warning "[$env:COMPUTERNAME] capture failed: $($_.Exception.Message)" }
}
# Remote PCs: one session each, collected in parallel
$remote = @($ComputerName | Where-Object { $_ -notin $localNames } | Sort-Object -Unique)
if ($remote.Count) {
$sessionArgs = @{ ComputerName = $remote; ErrorAction = 'SilentlyContinue'; ErrorVariable = 'sessionErrors' }
if ($Credential) { $sessionArgs.Credential = $Credential }
$sessions = @(New-PSSession @sessionArgs)
foreach ($e in $sessionErrors) { Write-Warning "Connection failed: $($e.Exception.Message)" }
if ($sessions.Count) {
foreach ($r in @(Invoke-Command -Session $sessions -ScriptBlock $Collect -ArgumentList $collectArgs -ErrorAction Continue)) {
$s = $sessions | Where-Object { $_.ComputerName -eq $r.PSComputerName } | Select-Object -First 1
$captures.Add([pscustomobject]@{ Result = $r; Session = $s })
}
}
}
foreach ($capture in $captures) {
$data = $capture.Result.Json | ConvertFrom-Json
$pc = [string]$data.ComputerName
$scriptPath = Join-Path $OutputPath "$pc.ps1"
$driverDir = Join-Path $OutputPath "${pc}_Drivers"
# Driver packages: the DriverStore folder name includes a hash, so an existing copy is reused
foreach ($d in @($data.Drivers | Where-Object { $_.Package })) {
$target = Join-Path $driverDir $d.Package
if (Test-Path -LiteralPath (Join-Path $target $d.InfName)) { continue }
try {
New-Item -ItemType Directory -Path $driverDir -Force | Out-Null
$copyArgs = @{ Path = $d.SourcePath; Destination = $driverDir; Recurse = $true; Force = $true }
if ($capture.Session) { $copyArgs.FromSession = $capture.Session }
Copy-Item @copyArgs
if (-not (Test-Path -LiteralPath (Join-Path $target $d.InfName))) { throw "package not found in $target after copying" }
}
catch {
Write-Warning "[$pc] driver package for '$($d.Name)' not copied: $($_.Exception.Message)"
Remove-Item -LiteralPath $target -Recurse -Force -ErrorAction SilentlyContinue
$d.Package = $null
}
}
# Human-readable summary for the help block of the generated script
$printersData = @($data.Printers | Where-Object { $_ })
$usersData = @($data.Users | Where-Object { $_ })
$lines = New-Object System.Collections.Generic.List[string]
foreach ($p in $printersData) { $lines.Add((' Printer : {0} [{1} | {2}]' -f $p.Name, $p.DriverName, $p.PortName)) }
foreach ($u in $usersData) {
foreach ($c in @($u.Connections | Where-Object { $_ })) { $lines.Add((' Connection : {0} ({1})' -f $c, $u.User)) }
if ($u.DefaultPrinter) { $lines.Add((' Default : {0} ({1})' -f $u.DefaultPrinter, $u.User)) }
}
$packages = @($data.Drivers | Where-Object { $_.Package }).Count
if ($packages) { $lines.Add(" Driver packages in ${pc}_Drivers: $packages") }
if (-not $lines.Count) { $lines.Add(' (no printers, connections or default printer found)') }
$summary = ($lines -join "`n") -replace '#>', '# >'
# The JSON goes in last, at the last __CONFIG__ (the real token follows the help block)
$json = ConvertTo-Json -InputObject $data -Depth 6
$text = $RestoreTemplate.Replace('__PC__', $pc).Replace('__DATE__', [string]$data.ExportedAt).Replace('__SUMMARY__', $summary)
$at = $text.LastIndexOf('__CONFIG__')
$text = $text.Remove($at, '__CONFIG__'.Length).Insert($at, "`$ConfigJson = @'`n$json`n'@")
$text = $text -replace '\r?\n', "`r`n"
[IO.File]::WriteAllText($scriptPath, $text, (New-Object System.Text.UTF8Encoding($true))) # BOM, so 5.1 reads non-ASCII names correctly
[pscustomobject]@{
ComputerName = $pc
Printers = $printersData.Count
Connections = @($usersData | ForEach-Object { @($_.Connections | Where-Object { $_ }) }).Count
DriverPackages = $packages
RestoreScript = $scriptPath
}
}
}
finally {
if ($sessions.Count) { Remove-PSSession -Session $sessions }
}
0 comments