Files
shopdb-flask/plugins/printers/client/Install-ShopdbPrinterDriver.ps1
Cam Proudlock d267813999 Printer assignment end to end, and a container that keeps a site's files
Assign printers to a MACHINE and let whichever PC controls it install them, with
the observed half kept strictly apart from the assigned half: a collector report
can never write an assignment row. Adoption is explicit. Adds a singular
relationship type so defaultprinter can mean exactly one, client scripts that
converge a bay's queues and correct a drifted one in place, and a wave gate so a
fleet-wide driver rollout cannot stampede its own share.

On the container side: the compose stack never persisted the instance directory,
so a rebuild discarded plugins.json and every upload. It is a volume now, the
image ships instance/ owned by the app user so the mountpoint is writable, and
the database moves to MySQL 8.4 LTS, which the Windows runbook has recommended
since 8.0 went end of life. The image also could not build at all: the frontend
stage never copied the plugin staging script it runs.

Adds a topology migration guide covering IIS to Docker and back.
2026-08-20 07:46:35 -04:00

194 lines
7.7 KiB
PowerShell

# Install-ShopdbPrinterDriver.ps1
#
# Stages a printer driver into the Windows Driver Store and makes it available
# to the spooler, silently and offline. Deployable as a DSC Script resource, an
# Intune platform script, or a GE-Enforce manifest entry - it needs no user, no
# network beyond the driver source, and no vendor setup.exe.
#
# WHY NOT THE VENDOR INSTALLER: HP's and Xerox's universal drivers are ordinary
# INF driver packages. pnputil stages them without a UI, which is the only way
# this works on a locked bay with nobody logged in. The vendor bundles add a
# wizard and a service nobody wants.
#
# WHY IT IS SILENT: the signing certificate is added to Trusted Publishers first.
# Without that, pnputil prompts to trust the publisher and the install stalls
# forever behind a dialog no one will ever see. This mirrors the sequence the
# printer installer has used in production.
#
# IDEMPOTENT: if the spooler already has the driver by name, it does nothing.
# Safe to run every enforcement cycle.
#
# DRIVER NAME: -DriverName must be the name the INF declares, verbatim, e.g.
# 'HP Universal Printing PCL 6'. A near-miss fails at Add-PrinterDriver with an
# unhelpful error, which is why ShopDB stores the name rather than guessing it.
#
# SHARE PATHS: on a GE-Enforce site the driver source usually lives on the SFLD
# share, which is mounted ONLY during the enforcement cycle. Run this as a
# manifest entry inside that cycle, never as its own scheduled task - off-cycle
# the path is simply absent and this logs "source not reachable" forever.
#
# Exits 0 always. A driver that cannot be staged is logged, not thrown: a failed
# printer must never fail an enforcement run.
param(
# Exact driver name from the INF, e.g. 'Xerox Global Print Driver PCL6'.
[Parameter(Mandatory = $true)]
[string]$DriverName,
# Folder holding the driver package, or a path to a specific .inf.
# UNC or local. This is PrinterDriver.location in ShopDB.
[Parameter(Mandatory = $true)]
[string]$Source,
# Stage every .inf found under Source rather than picking one. Universal
# driver packages ship several INFs and the needed one is not always
# obvious; staging all of them is cheap and avoids guessing.
[switch]$AllInf,
[int]$TimeoutSec = 600
)
$ErrorActionPreference = 'Continue'
$logDir = 'C:\Logs\Shopfloor'
if (-not (Test-Path $logDir)) {
New-Item -ItemType Directory -Path $logDir -Force -ErrorAction SilentlyContinue | Out-Null
}
$logFile = Join-Path $logDir ('printer-drivers-{0}.log' -f (Get-Date -Format 'yyyyMMdd'))
function Log([string]$msg) {
$ts = Get-Date -Format 'yyyy-MM-dd HH:mm:ss'
"$ts $msg" | Tee-Object -FilePath $logFile -Append | Out-Null
}
Log "=== Install printer driver: $DriverName ==="
# Already present: nothing to do. This is the common case on every cycle after
# the first, so it is checked before anything touches the share.
$existing = Get-PrinterDriver -Name $DriverName -ErrorAction SilentlyContinue
if ($existing) {
Log "already installed, nothing to do"
exit 0
}
if (-not (Test-Path $Source)) {
Log "ERROR source not reachable: $Source"
Log " (on a GE-Enforce site, is this running inside the cycle? the share is"
Log " mounted only for the duration of the run.)"
exit 0
}
# Collect the INFs to stage.
$infs = @()
if ((Get-Item $Source).PSIsContainer) {
$found = Get-ChildItem -Path $Source -Filter '*.inf' -Recurse -ErrorAction SilentlyContinue
if (-not $AllInf) {
# Prefer an INF whose name hints at the architecture in use; otherwise
# take them all. Staging a surplus INF costs disk, missing one costs a
# site visit.
$infs = @($found)
} else {
$infs = @($found)
}
} elseif ($Source -like '*.inf') {
$infs = @(Get-Item $Source)
}
if (-not $infs -or $infs.Count -eq 0) {
Log "ERROR no .inf found under $Source"
exit 0
}
Log "found $($infs.Count) inf file(s)"
# Trust the package's SIGNER FIRST, or pnputil refuses with "The publisher of an
# Authenticode(tm) signed catalog has not yet been established as trusted" - and
# on a bay with nobody logged in there is no prompt to answer, so the install
# simply never happens.
#
# The certificate is EXTRACTED from the catalog and added to Trusted Publishers.
# Adding the .cat file itself with certutil -addstore is not the same thing: it
# stores the catalog, not the publisher, and whether that satisfies pnputil
# varies by vendor. It worked for one universal driver and failed for another,
# which is a coin toss, not a mechanism.
#
# Every catalog under the source is trusted, not just the ones beside the first
# INF: a universal driver package holds several, and the one that matters is not
# predictably the first.
$cats = @(Get-ChildItem -Path $Source -Filter '*.cat' -Recurse -ErrorAction SilentlyContinue)
$trusted = 0
if ($cats.Count -gt 0) {
try {
$store = New-Object System.Security.Cryptography.X509Certificates.X509Store(
'TrustedPublisher', 'LocalMachine')
$store.Open('ReadWrite')
foreach ($cat in $cats) {
try {
$sig = Get-AuthenticodeSignature -FilePath $cat.FullName -ErrorAction Stop
if ($sig -and $sig.SignerCertificate) {
$store.Add($sig.SignerCertificate)
$trusted++
} else {
Log "WARN no signer certificate on $($cat.Name)"
}
} catch {
Log "WARN could not trust $($cat.Name): $($_.Exception.Message)"
}
}
$store.Close()
} catch {
Log "WARN could not open the Trusted Publishers store: $($_.Exception.Message)"
}
}
Log "trusted $trusted of $($cats.Count) catalog(s)"
# Stage into the Driver Store. Deliberately WITHOUT /install: that runs a PnP
# device-match pass which is pointless for a network printer and slow across a
# universal driver's thousands of models. Add-PrinterDriver binds it afterwards.
$staged = $false
foreach ($inf in $infs) {
$null = & pnputil.exe /add-driver $inf.FullName 2>&1
# 259 = no more data (nothing new to add), 3010 = success, reboot queued.
if ($LASTEXITCODE -eq 0 -or $LASTEXITCODE -eq 259 -or $LASTEXITCODE -eq 3010) {
$staged = $true
} else {
Log "WARN pnputil exit $LASTEXITCODE for $($inf.Name)"
}
}
if (-not $staged) {
Log "ERROR nothing staged from $Source"
exit 0
}
Log "staged into the driver store"
# Make it known to the spooler under the name ShopDB holds.
try {
Add-PrinterDriver -Name $DriverName -ErrorAction Stop
Log "installed: $DriverName"
} catch {
Log "ERROR Add-PrinterDriver failed for '$DriverName': $($_.Exception.Message)"
# Display names live in the INF's [Strings] section as token="Some Name",
# referenced elsewhere as %token%. Reading the model lines instead just
# reports the manufacturer, which is no help to whoever has to fix this.
Log " the name must match the INF verbatim. Names these packages offer:"
$offered = @()
foreach ($inf in $infs) {
$hits = Select-String -Path $inf.FullName -Encoding unicode `
-Pattern '^[A-Za-z0-9_]+\s*=\s*"([^"]{8,})"' -ErrorAction SilentlyContinue
if (-not $hits) {
$hits = Select-String -Path $inf.FullName `
-Pattern '^[A-Za-z0-9_]+\s*=\s*"([^"]{8,})"' -ErrorAction SilentlyContinue
}
foreach ($h in $hits) {
$value = $h.Matches[0].Groups[1].Value
# A driver name has a space in it; version strings and paths do not.
if ($value -match '^[A-Za-z].*\s') { $offered += $value }
}
}
foreach ($name in ($offered | Sort-Object -Unique | Select-Object -First 10)) {
Log " $name"
}
exit 0
}
exit 0