Private/Update-VBUserPrinterRegistry.ps1
|
# ============================================================ # FUNCTION : Update-VBUserPrinterRegistry # MODULE : VB.WorkstationReport # VERSION : 1.0.0 # CHANGED : 23-04-2026 -- Initial release # AUTHOR : Vibhu Bhatnagar # PURPOSE : Applies printer path migrations to a single mounted user hive # ENCODING : UTF-8 with BOM # NOTE : Private -- caller must mount hive via Mount-VBUserHive before calling # ============================================================ function Update-VBUserPrinterRegistry { <# .SYNOPSIS Applies printer path migrations to a single mounted user registry hive. .DESCRIPTION Update-VBUserPrinterRegistry is an internal helper called by Set-VBUserPrinterMigration. It accepts a SID whose hive is already mounted under HKEY_USERS and applies a set of printer mapping rules, handling all four migration scenarios: UNC -> UNC Rename the Connections subkey and update Devices/PrinterPorts entries. UNC -> IP Remove the Connections subkey; add Devices/PrinterPorts pointing to the new TCP/IP port name (IP_x.x.x.x). Machine-level port must already exist. IP -> UNC Add a Connections subkey for the new UNC path; update Devices/PrinterPorts. IP -> IP Update Devices/PrinterPorts to point to the new TCP/IP port name. Registry keys written per user: HKU\{SID}\Printers\Connections\,,server,share -- UNC connection marker HKU\{SID}\Software\...\Devices -- printer device list HKU\{SID}\Software\...\PrinterPorts -- printer port list (if present) HKU\{SID}\Software\...\Windows (Device value) -- default printer Idempotent: if the new printer already exists and the old printer is already gone the function returns Action = 'AlreadyMigrated' without making any changes. NOTE: This is a private function. Do not call it directly. Use Set-VBUserPrinterMigration. The caller is responsible for mounting and dismounting the hive via Mount-VBUserHive and Dismount-VBUserHive. .PARAMETER SID SID of the user whose hive is currently mounted under HKEY_USERS. .PARAMETER Username Display name used in output objects. Defaults to 'Unknown'. .PARAMETER ComputerName Computer name used in output objects. Defaults to local machine. .PARAMETER Mappings Array of PSCustomObjects, each with OldPath, NewPath, and DriverName properties. Produced by Set-VBUserPrinterMigration from CSV or hashtable input. .EXAMPLE # Called internally by Set-VBUserPrinterMigration after Mount-VBUserHive $mappings = @( [PSCustomObject]@{ OldPath = '\\OldServer\HP01'; NewPath = '10.30.1.50'; DriverName = 'HP LaserJet' } ) Update-VBUserPrinterRegistry -SID $mountResult.SID -Username 'jdoe' -Mappings $mappings .OUTPUTS PSCustomObject Returns one object per mapping rule processed: - ComputerName : Target computer - Username : User profile name - SID : User SID - OldPath : Old printer path - NewPath : New printer path - Action : 'Migrated', 'Skipped', 'AlreadyMigrated', or 'Failed' - Details : Semicolon-separated list of registry actions taken - Status : 'Success' or 'Failed' - Error : Error message (only present on failure) - Timestamp : Time of action (dd-MM-yyyy HH:mm:ss) .NOTES Version : 1.0.0 Author : Vibhu Bhatnagar Category : Printer Management (Private) UNC path to Connections key name conversion: \\server\printer -> ,,server,printer Leading \\ becomes ,, then remaining \ become , Devices key value format: UNC printer : winspool,Ne00: (port reassigned by Windows at logon) IP printer : winspool,IP_x.x.x.x (TCP/IP port name) PrinterPorts key value format: UNC printer : winspool,Ne00:,15,45 (adds read/transmit timeouts) IP printer : winspool,IP_x.x.x.x,15,45 A user logoff/logon may be required for printer changes to fully apply in active sessions. #> [CmdletBinding(SupportsShouldProcess)] param( [Parameter(Mandatory)] [string]$SID, [string]$Username = 'Unknown', [string]$ComputerName = $env:COMPUTERNAME, [Parameter(Mandatory)] [object[]]$Mappings # PSCustomObjects: OldPath, NewPath, DriverName ) # --- Registry paths --- $regBase = "Registry::HKEY_USERS\$SID" $connectPath = "$regBase\Printers\Connections" $devicesPath = "$regBase\Software\Microsoft\Windows NT\CurrentVersion\Devices" $portsPath = "$regBase\Software\Microsoft\Windows NT\CurrentVersion\PrinterPorts" $windowsPath = "$regBase\Software\Microsoft\Windows NT\CurrentVersion\Windows" foreach ($map in $Mappings) { # Step 1 -- Normalize paths $oldPath = ($map.OldPath -replace '/', '\').TrimEnd('\').Trim() $newPath = ($map.NewPath -replace '/', '\').TrimEnd('\').Trim() $isOldUNC = $oldPath -match '^\\\\' $isNewUNC = $newPath -match '^\\\\' # Step 2 -- Pre-compute key and port names $oldKeyName = $null $newKeyName = $null $newPortName = $null if ($isOldUNC) { # \\server\printer -> ,,server,printer $oldKeyName = ',,' + ($oldPath.TrimStart('\') -replace '\\', ',') } if ($isNewUNC) { $newKeyName = ',,' + ($newPath.TrimStart('\') -replace '\\', ',') } else { $newPortName = "IP_$newPath" } # Step 3 -- Find old printer in user registry $oldFound = $false $oldDeviceName = $null if ($isOldUNC) { $oldFound = Test-Path -Path "$connectPath\$oldKeyName" $oldDeviceName = $oldPath # in Devices key, UNC printer name IS the UNC path } else { # IP -- find by matching port value in Devices key (value contains 'IP_x.x.x.x') $portToFind = "IP_$($oldPath.Trim(','))" $deviceProps = Get-ItemProperty -Path $devicesPath -ErrorAction SilentlyContinue if ($deviceProps) { $members = $deviceProps | Get-Member -MemberType NoteProperty | Where-Object { $_.Name -notlike 'PS*' } foreach ($member in $members) { if ($deviceProps.($member.Name) -like "*$portToFind*") { $oldDeviceName = $member.Name $oldFound = $true break } } } } if (-not $oldFound) { [PSCustomObject]@{ ComputerName = $ComputerName Username = $Username SID = $SID OldPath = $map.OldPath NewPath = $map.NewPath Action = 'Skipped' Details = 'Old printer not found in user profile' Status = 'Success' Timestamp = (Get-Date).ToString('dd-MM-yyyy HH:mm:ss') } continue } # Step 4 -- Compute new printer display name (needed for Devices key and default printer) $newDisplayName = $null if ($isNewUNC) { $newDisplayName = $newPath } elseif ($isOldUNC) { # UNC -> IP: use share name as display name (e.g. \\server\HP01 -> HP01) $newDisplayName = $oldPath.TrimEnd('\').Split('\')[-1] } else { # IP -> IP: keep existing display name $newDisplayName = $oldDeviceName } # Step 5 -- Idempotency: check if new printer already mapped $newAlreadyExists = $false if ($isNewUNC) { $newAlreadyExists = Test-Path -Path "$connectPath\$newKeyName" } else { $deviceProps = Get-ItemProperty -Path $devicesPath -ErrorAction SilentlyContinue if ($deviceProps) { $members = $deviceProps | Get-Member -MemberType NoteProperty | Where-Object { $_.Name -notlike 'PS*' } foreach ($member in $members) { if ($deviceProps.($member.Name) -like "*$newPortName*") { $newAlreadyExists = $true break } } } } # If new already mapped AND old already gone -- fully migrated, nothing to do $oldStillPresent = $false if ($isOldUNC) { $oldStillPresent = Test-Path -Path "$connectPath\$oldKeyName" } else { $oldStillPresent = $null -ne $oldDeviceName } if ($newAlreadyExists -and (-not $oldStillPresent)) { [PSCustomObject]@{ ComputerName = $ComputerName Username = $Username SID = $SID OldPath = $map.OldPath NewPath = $map.NewPath Action = 'AlreadyMigrated' Details = 'New printer already present, old printer already removed' Status = 'Success' Timestamp = (Get-Date).ToString('dd-MM-yyyy HH:mm:ss') } continue } if (-not $PSCmdlet.ShouldProcess("$Username on $ComputerName", "Migrate $($map.OldPath) -> $($map.NewPath)")) { continue } try { $actionsTaken = [System.Collections.Generic.List[string]]::new() # Step 6 -- Add new printer entries (if not already present) if (-not $newAlreadyExists) { # Read old Devices value to carry forward when migrating UNC -> UNC $oldDeviceValue = $null if ($oldDeviceName -and (Test-Path -Path $devicesPath)) { $oldDeviceValue = (Get-ItemProperty -Path $devicesPath -Name $oldDeviceName -ErrorAction SilentlyContinue).$oldDeviceName } # Build new Devices value $newDeviceValue = $null if ($isNewUNC) { # Carry forward port value for UNC -> UNC; default for other types if ($isOldUNC -and $oldDeviceValue) { $newDeviceValue = $oldDeviceValue } else { $newDeviceValue = 'winspool,Ne00:' } } else { $newDeviceValue = "winspool,$newPortName" } # Add UNC Connections subkey if ($isNewUNC) { if (-not (Test-Path -Path $connectPath)) { New-Item -Path $connectPath -Force | Out-Null } New-Item -Path $connectPath -Name $newKeyName -Force | Out-Null $actionsTaken.Add("Added Connections key: $newKeyName") } # Add Devices entry if (Test-Path -Path $devicesPath) { Set-ItemProperty -Path $devicesPath -Name $newDisplayName -Value $newDeviceValue -Type String -Force $actionsTaken.Add("Added Devices entry: $newDisplayName = $newDeviceValue") } # Add PrinterPorts entry (optional key -- only write if it exists) if (Test-Path -Path $portsPath) { # PrinterPorts value appends timeout values: winspool,Ne00:,15,45 $newPortsValue = $newDeviceValue if ($newPortsValue -notlike '*,15,45') { $newPortsValue = "$newPortsValue,15,45" } Set-ItemProperty -Path $portsPath -Name $newDisplayName -Value $newPortsValue -Type String -Force $actionsTaken.Add("Added PrinterPorts entry: $newDisplayName") } } # Step 7 -- Remove old printer entries if ($isOldUNC) { $oldConnKeyPath = "$connectPath\$oldKeyName" if (Test-Path -Path $oldConnKeyPath) { Remove-Item -Path $oldConnKeyPath -Recurse -Force $actionsTaken.Add("Removed Connections key: $oldKeyName") } if (Test-Path -Path $devicesPath) { Remove-ItemProperty -Path $devicesPath -Name $oldPath -ErrorAction SilentlyContinue $actionsTaken.Add("Removed Devices entry: $oldPath") } if (Test-Path -Path $portsPath) { Remove-ItemProperty -Path $portsPath -Name $oldPath -ErrorAction SilentlyContinue } } else { if ($oldDeviceName) { if (Test-Path -Path $devicesPath) { Remove-ItemProperty -Path $devicesPath -Name $oldDeviceName -ErrorAction SilentlyContinue $actionsTaken.Add("Removed Devices entry: $oldDeviceName") } if (Test-Path -Path $portsPath) { Remove-ItemProperty -Path $portsPath -Name $oldDeviceName -ErrorAction SilentlyContinue } } } # Step 8 -- Update default printer if it was the one being migrated $defaultReg = Get-ItemProperty -Path $windowsPath -Name 'Device' -ErrorAction SilentlyContinue if ($defaultReg) { $defaultName = ($defaultReg.Device -split ',')[0] $isDefault = $false if ($isOldUNC) { $isDefault = $defaultName -eq $oldPath } else { $isDefault = $defaultName -eq $oldDeviceName } if ($isDefault) { $newDefaultValue = $null if ($isNewUNC) { $newDefaultValue = "$newPath,winspool,Ne00:" } else { $newDefaultValue = "$newDisplayName,winspool,$newPortName" } Set-ItemProperty -Path $windowsPath -Name 'Device' -Value $newDefaultValue -Type String -Force $actionsTaken.Add("Updated default printer: $newDisplayName") } } [PSCustomObject]@{ ComputerName = $ComputerName Username = $Username SID = $SID OldPath = $map.OldPath NewPath = $map.NewPath Action = 'Migrated' Details = ($actionsTaken -join '; ') Status = 'Success' Timestamp = (Get-Date).ToString('dd-MM-yyyy HH:mm:ss') } } catch { [PSCustomObject]@{ ComputerName = $ComputerName Username = $Username SID = $SID OldPath = $map.OldPath NewPath = $map.NewPath Action = 'Failed' Details = $_.Exception.Message Error = $_.Exception.Message Status = 'Failed' Timestamp = (Get-Date).ToString('dd-MM-yyyy HH:mm:ss') } } } } |