# 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