Skip to content

MSFT: Printer configuration backup and restore

● cpx October 26, 2026 23 min read MSFT Troubleshoot MSWindows 11 Powershell/PWSH
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 }
}

Be the first to comment

protected · no tracking

0 comments