diff --git a/test/Samy.Printers.ps1 b/test/Samy.Printers.ps1 new file mode 100644 index 0000000..f5a9d65 --- /dev/null +++ b/test/Samy.Printers.ps1 @@ -0,0 +1,649 @@ +# Samy.Printers.ps1 +# Printer config, driver, and HTTP handlers + +function Get-SamyDriverRootFolder { + [CmdletBinding()] + param() + + $root = Join-Path $env:ProgramData 'SVS\Samy\Drivers' + + if (-not (Test-Path $root)) { + try { + New-Item -Path $root -ItemType Directory -Force | Out-Null + Write-LogHybrid "Created driver root folder '$root'." Info Printers -LogToEvent + } + catch { + Write-LogHybrid "Failed to create driver root folder '$root': $($_.Exception.Message)" Warning Printers -LogToEvent + } + } + + return $root +} + +function Get-SamyDriverFolderForProfile { + [CmdletBinding()] + param( + [Parameter(Mandatory)][pscustomobject]$Profile + ) + + $root = Get-SamyDriverRootFolder + + if ($Profile.PSObject.Properties.Name -contains 'DriverFolderName' -and $Profile.DriverFolderName) { + $folderName = $Profile.DriverFolderName + } + else { + $folderName = "$($Profile.ClientCode)_$($Profile.ProfileName)" + } + + $dest = Join-Path $root $folderName + + if (-not (Test-Path $dest)) { + try { + New-Item -Path $dest -ItemType Directory -Force | Out-Null + Write-LogHybrid "Created driver folder '$dest'." Info Printers -LogToEvent + } + catch { + Write-LogHybrid "Failed to create driver folder '$dest': $($_.Exception.Message)" Warning Printers -LogToEvent + } + } + + return $dest +} + +function Get-SamyDriverPackageUrl { + [CmdletBinding()] + param( + [Parameter(Mandatory)][pscustomobject]$Profile + ) + + if ($Profile.PSObject.Properties.Name -contains 'DriverPackageUrl' -and $Profile.DriverPackageUrl) { + return $Profile.DriverPackageUrl + } + + if ($Profile.PSObject.Properties.Name -contains 'DriverPackagePath' -and $Profile.DriverPackagePath) { + return "$Script:SamyRepoBase/$Script:SamyBranch/$($Profile.DriverPackagePath)?raw=1" + } + + return $null +} + +function Get-SamyClientListFromServer { + [CmdletBinding()] + param( + [Parameter(Mandatory)] + [string]$Uri, + + [Parameter(Mandatory)] + [string]$Password + ) + + try { + Write-LogHybrid "Calling client list service at $Uri" Info Printers -LogToEvent + + $headers = @{ + SAMYPW = $Password + } + + $resp = Invoke-RestMethod -Uri $Uri ` + -Method Post ` + -Headers $headers ` + -ContentType 'application/json' ` + -ErrorAction Stop + + if (-not $resp) { + Write-LogHybrid "Client list service returned no data." Warning Printers -LogToEvent + return @() + } + + if ($resp -is [System.Collections.IEnumerable] -and -not ($resp -is [string])) { + return @($resp) + } + else { + return ,$resp + } + } + catch { + Write-LogHybrid ("Get-SamyClientListFromServer failed for {0}: {1}" -f $Uri, $_.Exception.Message) Error Printers -LogToEvent + return @() + } +} + +function Invoke-GetPrinters { + param($Context) + + try { + if ($Context.Request.HttpMethod -ne 'POST') { + $Context.Response.StatusCode = 405 + Send-Text $Context 'Use POST' + return + } + + $rawBody = (New-Object IO.StreamReader $Context.Request.InputStream).ReadToEnd() + if (-not $rawBody) { + $Context.Response.StatusCode = 400 + Send-Text $Context 'Missing request body.' + return + } + + try { + $body = $rawBody | ConvertFrom-Json + } + catch { + $Context.Response.StatusCode = 400 + Send-Text $Context 'Invalid JSON body.' + return + } + + $password = $body.password + if (-not $password) { + $Context.Response.StatusCode = 400 + Send-Text $Context 'Password is required.' + return + } + + $uri = 'https://bananas.svstools.ca/getprinters' + Write-LogHybrid "Fetching printers from $uri" Info Printers -LogToEvent + + $printers = Get-SamyClientListFromServer -Uri $uri -Password $password + + if ($null -eq $printers) { + Write-LogHybrid "Get-SamyClientListFromServer returned `$null; sending empty JSON array." Warning Printers -LogToEvent + $printers = @() + } + + try { + Update-SamyPrinterConfig -PrinterProfiles $printers -SkipIfEmpty + } + catch { + Write-LogHybrid "Update-SamyPrinterConfig failed: $($_.Exception.Message)" Warning Printers -LogToEvent + } + + Send-JSON $Context $printers + } + catch { + Write-LogHybrid "Invoke-GetPrinters error: $($_.Exception.Message)" Error Printers -LogToEvent + $Context.Response.StatusCode = 500 + Send-Text $Context "Internal server error fetching printers." + } +} + +function Invoke-InstallPrinters { + param($Context) + + try { + if ($Context.Request.HttpMethod -ne 'POST') { + $Context.Response.StatusCode = 405 + Send-Text $Context 'Use POST' + return + } + + $rawBody = (New-Object IO.StreamReader $Context.Request.InputStream).ReadToEnd() + if (-not $rawBody) { + $Context.Response.StatusCode = 400 + Send-Text $Context 'Missing request body.' + return + } + + try { + $body = $rawBody | ConvertFrom-Json + } + catch { + $Context.Response.StatusCode = 400 + Send-Text $Context 'Invalid JSON body.' + return + } + + $printers = $body.printers + if (-not $printers -or $printers.Count -eq 0) { + $Context.Response.StatusCode = 400 + Send-Text $Context 'No printers specified.' + return + } + + Write-LogHybrid "Received request to install $($printers.Count) printers." Info Printers -LogToEvent + + $successCount = 0 + $failures = @() + + foreach ($p in $printers) { + $clientCode = $p.ClientCode + $profileName = $p.ProfileName + $setDefault = $false + + if ($p.PSObject.Properties.Name -contains 'SetAsDefault' -and $p.SetAsDefault) { + $setDefault = $true + } + + if (-not $clientCode -or -not $profileName) { + $msg = "Skipping printer entry because ClientCode or ProfileName is missing." + Write-LogHybrid $msg Warning Printers -LogToEvent + $failures += $msg + continue + } + + $summary = "ClientCode=$clientCode ProfileName=$profileName DisplayName=$($p.DisplayName) Location=$($p.Location) SetAsDefault=$setDefault" + Write-LogHybrid "Installing printer ($summary)" Info Printers -LogToEvent + + try { + Invoke-SamyPrinterInstall ` + -ClientCode $clientCode ` + -ProfileName $profileName ` + -SetAsDefault:$setDefault ` + #-WhatIf + + $successCount++ + } + catch { + $errMsg = "Failed to install printer ($summary): $($_.Exception.Message)" + Write-LogHybrid $errMsg Error Printers -LogToEvent + $failures += $errMsg + } + } + + $result = @{ + SuccessCount = $successCount + FailureCount = $failures.Count + Failures = $failures + Message = "Printer install (WHATIF) processed. Check SAMY logs for detail." + } + + Send-JSON $Context $result + } + catch { + Write-LogHybrid "Invoke-InstallPrinters error: $($_.Exception.Message)" Error Printers -LogToEvent + $Context.Response.StatusCode = 500 + Send-Text $Context "Internal server error installing printers." + } +} + +function Get-SamyPrinterLocalConfigPath { + [CmdletBinding()] + param() + + $configDir = Join-Path $env:ProgramData 'SVS\Samy\Printers' + + if (-not (Test-Path $configDir)) { + try { + New-Item -Path $configDir -ItemType Directory -Force | Out-Null + Write-LogHybrid "Created printer config folder at '$configDir'." Info Printers -LogToEvent + } + catch { + Write-LogHybrid "Failed to create printer config folder '$configDir': $($_.Exception.Message)" Warning Printers -LogToEvent + } + } + + return (Join-Path $configDir 'printers.json') +} + +function Get-SamyPrinterConfigFromFile { + [CmdletBinding()] + param() + + $path = Get-SamyPrinterLocalConfigPath + + if (-not (Test-Path $path)) { + throw "Local printer config file not found at '$path'. Create or update printers.json first." + } + + $json = Get-Content -Path $path -Raw -ErrorAction Stop + $profiles = $json | ConvertFrom-Json + + if (-not $profiles) { + throw "Printer config file '$path' is empty or invalid JSON." + } + + return $profiles +} + +$Script:Samy_PrinterProfiles = $null + +function Get-SamyPrinterProfiles { + [CmdletBinding()] + param( + [string]$ClientCode + ) + + if (-not $Script:Samy_PrinterProfiles) { + $Script:Samy_PrinterProfiles = Get-SamyPrinterConfigFromFile + } + + $result = $Script:Samy_PrinterProfiles + + if ($PSBoundParameters.ContainsKey('ClientCode') -and $ClientCode) { + $result = $result | Where-Object { $_.ClientCode -eq $ClientCode } + } + + return $result +} + +function Get-SamyPrinterProfile { + [CmdletBinding()] + param( + [Parameter(Mandatory)][string]$ClientCode, + [Parameter(Mandatory)][string]$ProfileName + ) + + $profiles = Get-SamyPrinterProfiles -ClientCode $ClientCode + $match = $profiles | Where-Object { $_.ProfileName -eq $ProfileName } + + if (-not $match) { + throw "No printer profile found for ClientCode '$ClientCode' and ProfileName '$ProfileName'." + } + + if ($match.Count -gt 1) { + throw "Multiple printer profiles found for ClientCode '$ClientCode' and ProfileName '$ProfileName'. De-duplicate in printers.json." + } + + return $match +} + +function Ensure-SamyPrinterDriver { + [CmdletBinding()] + param( + [Parameter(Mandatory)] + [pscustomobject]$Profile + ) + + $driverName = $Profile.DriverName + if (-not $driverName) { + throw "Profile '$($Profile.ProfileName)' has no DriverName defined in printer config." + } + + $existingDriver = Get-PrinterDriver -Name $driverName -ErrorAction SilentlyContinue + if ($existingDriver) { + Write-LogHybrid "Printer driver '$driverName' already installed." Info Printers -LogToEvent + return + } + + Write-LogHybrid "Printer driver '$driverName' not found. Preparing to install." Info Printers -LogToEvent + + $localDriverRoot = Get-SamyDriverFolderForProfile -Profile $Profile + + $infPath = $null + if ($Profile.PSObject.Properties.Name -contains 'DriverInfPath' -and $Profile.DriverInfPath) { + if (Test-Path $Profile.DriverInfPath) { + $infPath = $Profile.DriverInfPath + Write-LogHybrid "Using existing INF path '$infPath' for driver '$driverName'." Info Printers -LogToEvent + } + else { + Write-LogHybrid "Configured DriverInfPath '$($Profile.DriverInfPath)' does not exist, will try repo download." Warning Printers -LogToEvent + } + } + + $packageDownloaded = $false + + if ($Profile.PSObject.Properties.Name -contains 'DriverPackagePath' -and $Profile.DriverPackagePath) { + $driverPackageUrl = "$Script:SamyRepoBase/$Script:SamyBranch/$($Profile.DriverPackagePath)?raw=1" + $localZip = Join-Path $localDriverRoot "package.zip" + + Write-LogHybrid "Attempting to download driver package from $driverPackageUrl." Info Printers -LogToEvent + + try { + Invoke-WebRequest -Uri $driverPackageUrl -OutFile $localZip -UseBasicParsing -ErrorAction Stop + Write-LogHybrid "Downloaded driver package from $driverPackageUrl to $localZip." Success Printers -LogToEvent + $packageDownloaded = $true + } + catch [System.Net.WebException] { + $response = $_.Exception.Response + $statusCode = $null + if ($response -and $response.StatusCode) { + $statusCode = [int]$response.StatusCode + } + + if ($statusCode -eq 404) { + Write-LogHybrid "Driver package not found at $driverPackageUrl (404). Falling back to INF-only install for '$($Profile.DisplayName)'." Warning Printers -LogToEvent + } + else { + Write-LogHybrid "Driver package download failed ($statusCode) from $($driverPackageUrl): $($_.Exception.Message)" Error Printers -LogToEvent + throw "Failed to download driver package from $($driverPackageUrl): $($_.Exception.Message)" + } + } + catch { + Write-LogHybrid "Driver package download failed from $($driverPackageUrl): $($_.Exception.Message)" Error Printers -LogToEvent + throw "Failed to download driver package from $($driverPackageUrl): $($_.Exception.Message)" + } + } + else { + Write-LogHybrid "No DriverPackagePath defined for '$($Profile.DisplayName)'; will rely on local INF." Info Printers -LogToEvent + } + + if ($packageDownloaded) { + try { + Expand-Archive -Path $localZip -DestinationPath $localDriverRoot -Force + Write-LogHybrid "Expanded driver package to '$localDriverRoot'." Info Printers -LogToEvent + } + catch { + Write-LogHybrid "Failed to expand driver package '$localZip': $($_.Exception.Message)" Error Printers -LogToEvent + throw "Failed to expand driver package '$localZip': $($_.Exception.Message)" + } + + if (-not $infPath) { + if ($Profile.PSObject.Properties.Name -contains 'DriverInfName' -and $Profile.DriverInfName) { + + $candidateInf = Join-Path $localDriverRoot $Profile.DriverInfName + if (Test-Path $candidateInf) { + $infPath = $candidateInf + Write-LogHybrid "Resolved INF from package as '$infPath' using DriverInfName '$($Profile.DriverInfName)' at root." Info Printers -LogToEvent + } + else { + Write-LogHybrid "Expected INF '$candidateInf' not found at root; searching recursively..." Warning Printers -LogToEvent + + $found = Get-ChildItem -Path $localDriverRoot -Recurse -Filter $Profile.DriverInfName -File -ErrorAction SilentlyContinue | + Select-Object -First 1 + + if ($found) { + $infPath = $found.FullName + Write-LogHybrid "Resolved INF from package as '$infPath' (found by recursive search for '$($Profile.DriverInfName)')." Info Printers -LogToEvent + } + else { + Write-LogHybrid "Could not find any '$($Profile.DriverInfName)' under '$localDriverRoot' after expanding package." Error Printers -LogToEvent + } + } + } + else { + Write-LogHybrid "DriverInfName not defined for profile '$($Profile.ProfileName)'; cannot auto-resolve INF from expanded package." Warning Printers -LogToEvent + } + } + } + + if (-not $infPath -or -not (Test-Path $infPath)) { + throw "Driver '$driverName' is not installed and no valid DriverInfPath or usable driver package is available for profile '$($Profile.ProfileName)'." + } + + Write-LogHybrid "Installing printer driver '$driverName' from '$infPath'." Info Printers -LogToEvent + + $pnputilCmd = "pnputil.exe /add-driver `"$infPath`" /install" + Write-LogHybrid "Running: $pnputilCmd" Info Printers -LogToEvent + + $pnputilOutput = & pnputil.exe /add-driver "`"$infPath`"" /install 2>&1 + $exitCode = $LASTEXITCODE + + Write-LogHybrid "pnputil exit code: $exitCode. Output:`n$pnputilOutput" Info Printers -LogToEvent + + if ($exitCode -ne 0) { + throw "pnputil failed with exit code $exitCode installing '$driverName' from '$infPath'." + } + + $existingDriver = Get-PrinterDriver -ErrorAction SilentlyContinue | Where-Object { + $_.Name -eq $driverName -or + $_.Name -like "*$driverName*" -or + $driverName -like "*$($_.Name)*" + } + + if (-not $existingDriver) { + $sharpDrivers = Get-PrinterDriver -ErrorAction SilentlyContinue | + Where-Object { $_.Name -like "SHARP*" } + + $sharpList = if ($sharpDrivers) { + ($sharpDrivers | Select-Object -ExpandProperty Name) -join ', ' + } + else { + '(none)' + } + + Write-LogHybrid "After pnputil, driver '$driverName' not found. Existing SHARP drivers: $sharpList" Warning Printers -LogToEvent + } + else { + Write-LogHybrid "Printer driver '$($existingDriver.Name)' is present after pnputil (requested '$driverName')." Success Printers -LogToEvent + } +} + +function Install-SamyTcpIpPrinter { + [CmdletBinding()] + param( + [Parameter(Mandatory)] + [pscustomobject]$Profile, + + [switch]$SetAsDefault + ) + + $portName = $Profile.Address + $printerName = $Profile.DisplayName + + if (-not $portName) { + throw "TCP/IP printer profile '$($Profile.ProfileName)' is missing Address in printer config." + } + + if (-not (Get-PrinterPort -Name $portName -ErrorAction SilentlyContinue)) { + Write-Verbose "Creating TCP/IP port '$portName'." + Add-PrinterPort -Name $portName -PrinterHostAddress $Profile.Address + } + else { + Write-Verbose "TCP/IP port '$portName' already exists." + } + + $existingPrinter = Get-Printer -Name $printerName -ErrorAction SilentlyContinue + if ($existingPrinter) { + Write-Verbose "Printer '$printerName' already exists. Skipping creation." + } + else { + Write-Verbose "Creating printer '$printerName' on port '$portName' using driver '$($Profile.DriverName)'." + Add-Printer -Name $printerName -PortName $portName -DriverName $Profile.DriverName + } + + if ($SetAsDefault -or $Profile.IsDefault) { + Write-Verbose "Setting '$printerName' as default printer." + (New-Object -ComObject WScript.Network).SetDefaultPrinter($printerName) + } +} + +function Install-SamySharedPrinter { + [CmdletBinding()] + param( + [Parameter(Mandatory)] + [pscustomobject]$Profile, + + [switch]$SetAsDefault + ) + + if (-not $Profile.PrintServer -or -not $Profile.ShareName) { + throw "Shared printer profile '$($Profile.ProfileName)' is missing PrintServer or ShareName in printer config." + } + + $connectionName = "\\$($Profile.PrintServer)\$($Profile.ShareName)" + + $existing = Get-Printer -ErrorAction SilentlyContinue | + Where-Object { + $_.Name -eq $Profile.DisplayName -or + $_.ShareName -eq $Profile.ShareName + } + + $printerName = $null + + if ($existing) { + Write-Verbose "Shared printer '$($Profile.DisplayName)' already connected as '$($existing.Name)'." + $printerName = $existing.Name + } + else { + Write-Verbose "Adding shared printer connection '$connectionName'." + Add-Printer -ConnectionName $connectionName + + $printerName = (Get-Printer | + Where-Object { $_.Name -like "*$($Profile.ShareName)*" } | + Select-Object -First 1 + ).Name + } + + if ($SetAsDefault -or $Profile.IsDefault) { + Write-Verbose "Setting '$printerName' as default printer." + (New-Object -ComObject WScript.Network).SetDefaultPrinter($printerName) + } +} + +function Invoke-SamyPrinterInstall { + [CmdletBinding(SupportsShouldProcess = $true)] + param( + [Parameter(Mandatory)] + [string]$ClientCode, + + [Parameter(Mandatory)] + [string]$ProfileName, + + [switch]$SetAsDefault + ) + + try { + $profile = Get-SamyPrinterProfile -ClientCode $ClientCode -ProfileName $ProfileName + $targetName = $profile.DisplayName + + if ($PSCmdlet.ShouldProcess($targetName, "Install printer")) { + + Write-LogHybrid "Installing printer profile '$($profile.ProfileName)' for client '$ClientCode' (WhatIf=$WhatIfPreference)." Info Printers -LogToEvent + + Ensure-SamyPrinterDriver -Profile $profile + + switch ($profile.Type) { + 'TcpIp' { + Install-SamyTcpIpPrinter -Profile $profile -SetAsDefault:$SetAsDefault + } + 'Shared' { + Install-SamySharedPrinter -Profile $profile -SetAsDefault:$SetAsDefault + } + default { + throw "Unsupported printer Type '$($profile.Type)' in profile '$($profile.ProfileName)'. Use 'TcpIp' or 'Shared'." + } + } + + Write-LogHybrid "Installed printer '$($profile.DisplayName)' (Client=$ClientCode Profile=$ProfileName)." Info Printers -LogToEvent + } + } + catch { + Write-LogHybrid ( + "Printer install failed for Client={0} Profile={1}: {2}" -f $ClientCode, $ProfileName, $_.Exception.Message + ) Error Printers -LogToEvent + throw + } +} + +function Update-SamyPrinterConfig { + [CmdletBinding()] + param( + [Parameter(Mandatory)] + [object]$PrinterProfiles, + + [switch]$SkipIfEmpty + ) + + $path = Get-SamyPrinterLocalConfigPath + + $profilesArray = @($PrinterProfiles) + + if ($SkipIfEmpty -and ($null -eq $PrinterProfiles -or $profilesArray.Count -eq 0)) { + Write-LogHybrid "Update-SamyPrinterConfig: no printer profiles returned; keeping existing printers.json." Warning Printers -LogToEvent + return + } + + if ($profilesArray.Count -eq 0) { + Write-LogHybrid "Update-SamyPrinterConfig: zero profiles; writing empty printers.json." Warning Printers -LogToEvent + } + + try { + $profilesArray | ConvertTo-Json -Depth 5 | Set-Content -Path $path -Encoding UTF8 + Write-LogHybrid "Saved $($profilesArray.Count) printer profiles to '$path'." Success Printers -LogToEvent + + $Script:Samy_PrinterProfiles = $null + } + catch { + Write-LogHybrid "Failed to write printers.json to '$path': $($_.Exception.Message)" Error Printers -LogToEvent + } +}