Stage printer drivers as a deployable set, for the common scope

Assigning a printer to a bay is useless if the bay cannot install it, and the
fleet data says why that mattered: 42 of 44 printers could not resolve a driver.
This is the delivery half - the drivers themselves, staged once per bay, so that
creating a queue never waits on a download.

Install-ShopdbPrinterDriver.ps1 does one driver: trust the package's signer, then
pnputil /add-driver, then Add-PrinterDriver. Install-ShopdbPrinterDrivers.ps1
does a site's whole set from drivers.json, and answers a compliance question with
-TestOnly, which is what makes it a clean DSC Script resource rather than a
fire-and-forget install.

Deliberately SEPARATE from assignment. Drivers are large, near-identical across a
fleet and change rarely; assignments are small, per-bay and change often. Staging
the set in the GE-Enforce common scope means the assignment client only ever
creates a queue - it never fetches a 48 MB package while somebody is waiting to
print, or discovers the share is unmounted at the worst moment.

THE SIGNER TRUST STEP IS THE WHOLE TRICK, and it took a real driver to find it.
certutil -addstore on the .cat file satisfied the Xerox package and failed every
HP INF with "The publisher of an Authenticode(tm) signed catalog has not yet been
established as trusted" - a coin toss, not a mechanism. The certificate is now
extracted with Get-AuthenticodeSignature and added to Trusted Publishers, for
every catalog under the package rather than the first INF's neighbours. On a
locked bay there is no prompt to answer, so the old failure was silent.

Verified on Windows against real packages, not by reading: all six drivers this
site needs install through the script, a second run is a no-op, a wrong driver
name fails with the names the package actually offers, and the DSC cycle behaves
- TestOnly exits 1 on a clean box, install exits 0, TestOnly then exits 0.

The packages themselves stay out of git: they are licensed vendor binaries, and
they belong on the share beside the other imaging payloads.

DEPLOYING-DRIVERS.md carries the GE-Enforce entry, the DSC configuration and the
Intune shape, plus the constraint that has cost a session before: the SFLD share
is mounted only during the enforcement cycle, so this runs as a manifest entry
and never as its own scheduled task.
This commit is contained in:
cproudlock
2026-08-19 09:33:05 -04:00
parent 2083029ff2
commit 03d0754fdc
4 changed files with 468 additions and 0 deletions

View File

@@ -0,0 +1,193 @@
# 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