PrintToVeraeTimeStamp/platform/windows/scripts/install-printer.ps1

214 lines
14 KiB
PowerShell

# Verae TimeStamp PDF — printer installer. Must run elevated.
# Every Add-* is followed by a Get-* verification. Failed steps (only) are
# appended to %ProgramData%\Verae\logs\install-errors-<time>.log. With
# VERAE_LOG_LEVEL=debug (or -DebugLog) a full transcript + JSON summary are kept too.
# Exit code 0 = at least one printer verified.
param(
[string]$InstallDir = (Split-Path -Parent (Split-Path -Parent $MyInvocation.MyCommand.Path)),
[switch]$SkipIpp,
[switch]$SkipFallback,
[string]$IppAddr = "127.0.0.1:8631",
[string]$ResultIni = "",
[switch]$DebugLog
)
$ErrorActionPreference = "Continue"
$PrinterName = "Verae TimeStamp PDF"
$FallbackName = "Verae TimeStamp PDF (Local Port)"
$LogDir = Join-Path $env:ProgramData "Verae\logs"
$DropDir = Join-Path $env:ProgramData "Verae\spool\fallback"
$PortPath = Join-Path $DropDir "verae-job.pdf"
$Bin = Join-Path $InstallDir "bin"
$Helper = Join-Path $Bin "verae-printhelper.exe"
$Cli = Join-Path $Bin "verae-print.exe"
$IppUrl = "http://$IppAddr/ipp/print"
$ts = Get-Date -Format "yyyyMMdd-HHmmss"
. (Join-Path $PSScriptRoot "common.ps1")
$TaskName = "Verae Print Helper"
New-Item -ItemType Directory -Force -Path $LogDir, $DropDir | Out-Null
$DebugOn = $DebugLog -or ($env:VERAE_LOG_LEVEL -eq "debug")
$ErrLog = Join-Path $LogDir "install-errors-$ts.log"
$Transcript = ""
if ($DebugOn) {
$Transcript = Join-Path $LogDir "install-$ts.log"
Start-Transcript -Path $Transcript -Force | Out-Null
}
$summary = [ordered]@{ started = (Get-Date).ToString("o"); steps = @(); ipp = "not-run"; fallback = "not-run" }
function Step([string]$Name, [scriptblock]$Do, [scriptblock]$Verify) {
Write-Host "`n=== STEP: $Name"
$err = $null; $ok = $false
try { & $Do | Out-Host } catch { $err = $_; Write-Host " EXCEPTION: $($_.Exception.GetType().FullName): $($_.Exception.Message) HResult=0x$('{0:X8}' -f $_.Exception.HResult)" }
if ($Verify) {
try { $ok = [bool](& $Verify) } catch { Write-Host " VERIFY EXCEPTION: $($_.Exception.Message)" }
} else { $ok = ($null -eq $err) }
Write-Host (" RESULT: " + $(if ($ok) { "OK" } else { "FAIL" }))
if (-not $ok) {
$msg = if ($err) { "$($err.Exception.Message) HResult=0x$('{0:X8}' -f $err.Exception.HResult)" } else { "verification failed" }
Add-Content -Path $ErrLog -Value ("{0} FAIL {1}: {2}" -f (Get-Date -Format "yyyy-MM-dd HH:mm:ss"), $Name, $msg)
}
$script:summary.steps += [ordered]@{ step = $Name; ok = $ok; error = $(if ($err) { "$($err.Exception.Message)" } else { $null }) }
return $ok
}
Write-Host "Verae printer install $ts (debug log: $DebugOn)"
Write-Host "InstallDir=$InstallDir User=$(whoami) PS=$($PSVersionTable.PSVersion) 64bitProcess=$([Environment]::Is64BitProcess)"
$os = Get-CimInstance Win32_OperatingSystem
Write-Host "OS: $($os.Caption) $($os.Version) build $($os.BuildNumber) $($os.OSArchitecture)"
$Target = Get-VeraeTargetUser
Write-Host "Target (logged-in) user: $($Target.Account) sid=$($Target.Sid) profile=$($Target.Profile) via $($Target.Source)"
$isAdmin = ([Security.Principal.WindowsPrincipal][Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)
Write-Host "Elevated: $isAdmin"
if (-not $isAdmin) { Write-Host "ERROR: not elevated — printer/port creation will fail (Access denied)." }
if (-not [Environment]::Is64BitProcess -and [Environment]::Is64BitOperatingSystem) { Write-Host "WARNING: 32-bit PowerShell on 64-bit Windows (WOW64) — should be launched via Sysnative." }
Write-Host "`n--- WPP / policy registry"
foreach ($k in 'HKLM:\SOFTWARE\Policies\Microsoft\Windows NT\Printers\WPP','HKLM:\SOFTWARE\Policies\Microsoft\Windows NT\Printers','HKLM:\SOFTWARE\Policies\Microsoft\Windows NT\Printers\PointAndPrint') {
if (Test-Path $k) { Write-Host "[$k]"; Get-ItemProperty $k | Format-List | Out-String | Write-Host } else { Write-Host "(absent) $k" }
}
Write-Host "--- BEFORE: printers / drivers / ports"
Get-Printer | Format-Table Name,DriverName,PortName,Type -AutoSize | Out-String -Width 250 | Write-Host
Get-PrinterDriver | Format-Table Name,MajorVersion,PrinterEnvironment -AutoSize | Out-String -Width 250 | Write-Host
Get-PrinterPort | Format-Table Name,Description,PortMonitor -AutoSize | Out-String -Width 250 | Write-Host
Step "Spooler running" { $s = Get-Service Spooler; if ($s.Status -ne 'Running') { Start-Service Spooler } } { (Get-Service Spooler).Status -eq 'Running' } | Out-Null
Step "Enable PrintService/Operational log" { wevtutil sl Microsoft-Windows-PrintService/Operational /e:true | Out-Host } { (Get-WinEvent -ListLog Microsoft-Windows-PrintService/Operational).IsEnabled } | Out-Null
Step "ACL logs+spool dirs (Users: Modify)" {
icacls (Join-Path $env:ProgramData "Verae") /grant "*S-1-5-32-545:(OI)(CI)M" /T /C | Out-Host
} { Test-Path $LogDir } | Out-Null
Step "Registry HKLM\Software\Verae\PrintStamp (InstallDir; debug logging off)" {
New-Item -Path 'HKLM:\Software\Verae\PrintStamp' -Force | Out-Null
Set-ItemProperty 'HKLM:\Software\Verae\PrintStamp' -Name InstallDir -Value $InstallDir
Remove-ItemProperty 'HKLM:\Software\Verae\PrintStamp' -Name Debug -ErrorAction SilentlyContinue
} { (Get-ItemProperty 'HKLM:\Software\Verae\PrintStamp').InstallDir -eq $InstallDir } | Out-Null
$script:taskOk = Step "Helper autostart: scheduled task '$TaskName' for $($Target.Account) (at logon, non-elevated)" {
Remove-ItemProperty 'HKLM:\Software\Microsoft\Windows\CurrentVersion\Run' -Name VeraePrintHelper -ErrorAction SilentlyContinue
$action = New-ScheduledTaskAction -Execute $Helper -WorkingDirectory $Bin
$trigger = New-ScheduledTaskTrigger -AtLogOn -User $Target.Account
$principal = New-ScheduledTaskPrincipal -UserId $Target.Account -LogonType Interactive -RunLevel Limited
$settings = New-ScheduledTaskSettingsSet -ExecutionTimeLimit ([TimeSpan]::Zero) -AllowStartIfOnBatteries -DontStopIfGoingOnBatteries -MultipleInstances IgnoreNew -StartWhenAvailable -RestartCount 3 -RestartInterval (New-TimeSpan -Minutes 1)
Register-ScheduledTask -TaskName $TaskName -Action $action -Trigger $trigger -Principal $principal -Settings $settings -Description "Verae TimeStamp PDF print helper (IPP loopback 127.0.0.1:8631)" -Force | Out-Null
} { $t = Get-ScheduledTask -TaskName $TaskName -ErrorAction SilentlyContinue; if ($t) { Write-Host " task principal=$($t.Principal.UserId) runlevel=$($t.Principal.RunLevel)" }; [bool]$t }
Step "Remove old per-user 0.1.2 install for $($Target.Account)" {
$old = Join-Path $Target.Profile "AppData\Local\Verae\PrintStamp\bin\verae-print.exe"
if (Test-Path $old) { Remove-Item -Force $old; Write-Host " removed $old" } else { Write-Host " none" }
Remove-Item "Registry::HKEY_USERS\$($Target.Sid)\Software\Microsoft\Windows\CurrentVersion\Uninstall\VeraePrintStamp" -Recurse -ErrorAction SilentlyContinue
} $null | Out-Null
Step "Add bin to machine PATH" {
$p = [Environment]::GetEnvironmentVariable("Path","Machine")
if ($p -notlike "*$Bin*") { [Environment]::SetEnvironmentVariable("Path", ($p.TrimEnd(';') + ";" + $Bin), "Machine") }
} { [Environment]::GetEnvironmentVariable("Path","Machine") -like "*$Bin*" } | Out-Null
function Test-Ipp { try { $r = Invoke-WebRequest -UseBasicParsing -TimeoutSec 3 "http://$IppAddr/"; return ($r.StatusCode -eq 200) } catch { return $false } }
# ---------------- Primary: IPP class driver → loopback helper ----------------
if (-not $SkipIpp) {
$script:tempHelper = $null
$helperUp = Step "Start helper as $($Target.Account) and wait for $IppAddr" {
if (-not (Test-Ipp)) {
if ($script:taskOk) { Write-Host " Start-ScheduledTask '$TaskName'"; Start-ScheduledTask -TaskName $TaskName }
for ($i = 0; $i -lt 30 -and -not (Test-Ipp); $i++) { Start-Sleep -Milliseconds 500 }
}
if (-not (Test-Ipp)) {
Write-Host " task did not bring IPP up; trying explorer.exe (shell token) launch"
Start-Process explorer.exe -ArgumentList "`"$Helper`""
for ($i = 0; $i -lt 20 -and -not (Test-Ipp); $i++) { Start-Sleep -Milliseconds 500 }
}
if (-not (Test-Ipp)) {
Write-Host " still down; starting a TEMPORARY elevated helper only for printer discovery (stopped afterwards)"
$script:tempHelper = Start-Process -FilePath $Cli -ArgumentList "serve --no-watch" -WindowStyle Hidden -PassThru
for ($i = 0; $i -lt 20 -and -not (Test-Ipp); $i++) { Start-Sleep -Milliseconds 500 }
}
& $Cli ipp-selftest --addr $IppAddr | Out-Host
} { Test-Ipp }
if ($helperUp) {
Get-Printer -Name $PrinterName -ErrorAction SilentlyContinue | ForEach-Object { Write-Host " removing stale '$($_.Name)'"; Remove-Printer -Name $_.Name }
$before = @(Get-Printer | Select-Object -ExpandProperty Name)
$hasIppUrl = (Get-Command Add-Printer).Parameters.ContainsKey('IppURL')
Write-Host "Add-Printer supports -IppURL: $hasIppUrl"
$added = Step "Add IPP printer ($IppUrl)" {
if ($hasIppUrl) {
Add-Printer -IppURL $IppUrl -ErrorAction Stop
} else {
Add-Printer -Name $PrinterName -DriverName "Microsoft IPP Class Driver" -PortName $IppUrl -ErrorAction Stop
}
} {
Start-Sleep 2
$new = @(Get-Printer | Where-Object { $before -notcontains $_.Name })
Write-Host " new printers: $(( $new | ForEach-Object { "$($_.Name) [$($_.DriverName) / $($_.PortName)]" }) -join '; ')"
$script:newIpp = $new | Select-Object -First 1
($null -ne $script:newIpp) -or (Get-Printer -Name $PrinterName -ErrorAction SilentlyContinue)
}
if ($added -and $script:newIpp -and $script:newIpp.Name -ne $PrinterName) {
Step "Rename '$($script:newIpp.Name)' → '$PrinterName'" { Rename-Printer -Name $script:newIpp.Name -NewName $PrinterName -ErrorAction Stop } { Get-Printer -Name $PrinterName -ErrorAction SilentlyContinue } | Out-Null
}
$p = Get-Printer -Name $PrinterName -ErrorAction SilentlyContinue
if ($p) { $p | Format-List * | Out-String | Write-Host; $summary.ipp = "OK: driver=$($p.DriverName) port=$($p.PortName)" } else { $summary.ipp = "FAIL" }
} else { $summary.ipp = "FAIL: helper not reachable" }
}
if ($script:tempHelper) { Write-Host "Stopping temporary elevated helper pid=$($script:tempHelper.Id)"; Stop-Process -Id $script:tempHelper.Id -Force -ErrorAction SilentlyContinue }
$script:helperOwner = ""
$helperRunning = Step "Helper running as logged-in user $($Target.Account)" { } {
$p = Get-CimInstance Win32_Process -Filter "Name='verae-printhelper.exe'" -ErrorAction SilentlyContinue | Select-Object -First 1
if (-not $p) { Write-Host " helper process not found"; return $false }
$o = Invoke-CimMethod -InputObject $p -MethodName GetOwner
$script:helperOwner = "$($o.Domain)\$($o.User)"
Write-Host " helper pid=$($p.ProcessId) owner=$script:helperOwner session=$($p.SessionId)"
($script:helperOwner -eq $Target.Account) -and (Test-Ipp)
}
# ---------------- Fallback: Microsoft Print To PDF → Local file port ----------------
if (-not $SkipFallback) {
$drv = Step "Driver 'Microsoft Print To PDF' present" {
if (-not (Get-PrinterDriver -Name "Microsoft Print To PDF" -ErrorAction SilentlyContinue)) { Add-PrinterDriver -Name "Microsoft Print To PDF" -ErrorAction Stop }
} { Get-PrinterDriver -Name "Microsoft Print To PDF" -ErrorAction SilentlyContinue }
$port = Step "Local port '$PortPath'" {
if (-not (Get-PrinterPort -Name $PortPath -ErrorAction SilentlyContinue)) { Add-PrinterPort -Name $PortPath -ErrorAction Stop }
} { Get-PrinterPort -Name $PortPath -ErrorAction SilentlyContinue }
if ($drv -and $port) {
Get-Printer -Name $FallbackName -ErrorAction SilentlyContinue | ForEach-Object { Remove-Printer -Name $_.Name }
$fb = Step "Add printer '$FallbackName'" {
Add-Printer -Name $FallbackName -DriverName "Microsoft Print To PDF" -PortName $PortPath -ErrorAction Stop
} { Get-Printer -Name $FallbackName -ErrorAction SilentlyContinue }
$summary.fallback = $(if ($fb) { "OK" } else { "FAIL" })
} else { $summary.fallback = "FAIL: driver=$drv port=$port" }
}
Write-Host "`n--- AFTER: printers / ports"
Get-Printer | Format-Table Name,DriverName,PortName,Type,PrinterStatus -AutoSize | Out-String -Width 250 | Write-Host
Get-PrinterPort | Format-Table Name,Description,PortMonitor -AutoSize | Out-String -Width 250 | Write-Host
Write-Host "--- PrintService events (last 15 min)"
foreach ($ln in 'Microsoft-Windows-PrintService/Admin','Microsoft-Windows-PrintService/Operational') {
Get-WinEvent -FilterHashtable @{LogName=$ln; StartTime=(Get-Date).AddMinutes(-15)} -ErrorAction SilentlyContinue |
Format-Table TimeCreated,Id,LevelDisplayName,Message -Wrap | Out-String -Width 250 | Write-Host
}
$summary.target = $Target.Account
$summary.helperOwner = $script:helperOwner
$summary.finished = (Get-Date).ToString("o")
$summary.transcript = $Transcript
$summaryPath = ""
if ($DebugOn) {
$summaryPath = Join-Path $LogDir "install-summary-$ts.json"
$summary | ConvertTo-Json -Depth 5 | Set-Content -Encoding UTF8 $summaryPath
}
$okAny = ($summary.ipp -like "OK*") -or ($summary.fallback -eq "OK")
Write-Host "`nVerae printer install: $(if ($okAny) {'OK'} else {'FAIL'}) ipp=[$($summary.ipp)] fallback=[$($summary.fallback)]"
Write-Host "Log: $Transcript"
Write-Host "Summary: $summaryPath"
if ($ResultIni) {
$stepOk = { param($pat) $x = $summary.steps | Where-Object { $_.step -like $pat } | Select-Object -Last 1; if ($null -eq $x) { "SKIP" } elseif ($x.ok) { "OK" } else { "FAIL" } }
Write-VeraeIni $ResultIni "printer" ([ordered]@{
target = $Target.Account
spooler = (& $stepOk "Spooler running")
oplog = (& $stepOk "Enable PrintService*")
task = (& $stepOk "Helper autostart*")
helper = $(if ($helperRunning) { "OK ($script:helperOwner)" } else { "FAIL ($script:helperOwner)" })
ipp = $summary.ipp
fallback = $summary.fallback
overall = $(if ($okAny) { "OK" } else { "FAIL" })
transcript = $Transcript
errors = $(if (Test-Path $ErrLog) { $ErrLog } else { "" })
summary = $summaryPath })
}
if ($DebugOn) { Stop-Transcript | Out-Null }
if ($okAny) { exit 0 } else { exit 1 }