# Install-ShopdbPrinterDrivers.ps1 # # Installs a SITE'S WHOLE DRIVER SET from a manifest, so a bay ends up with every # printer driver it might need in one converging run. Wraps # Install-ShopdbPrinterDriver.ps1, which does one driver. # # DESIGNED FOR DSC / Intune / GE-Enforce. It declares state rather than # performing an install: a driver already present is skipped, so this is safe to # run on a schedule and cheap when there is nothing to do. That is what lets a # DSC Script resource call it from TestScript as well as SetScript. # # THE MANIFEST, not arguments, is the contract. drivers.json lists each driver by # the name its INF declares - what Add-PrinterDriver matches on, verbatim - and # where its package lives. Paths are relative to this script, or absolute (a UNC # path on a site's share is normal). # # EXIT CODE: 0 when every driver in the manifest is present at the end, 1 when # one or more could not be installed. DSC needs a real answer here, unlike the # single-driver script which never fails an enforcement run. The per-driver log # says which and why. # # SHARE PATHS: on a GE-Enforce site the packages usually live on the SFLD share, # which is mounted ONLY during the enforcement cycle. Run this as a manifest # entry inside that cycle, not as its own scheduled task. # # WAVES, because a driver package is not a manifest check. GE-Enforce gives each # PC a start offset of SHA256(hostname) % 5 MINUTES and then repeats every 5 # minutes, which was sized for reading a few KB of JSON. A driver set is 100 MB # for the two universals and 226 MB for a full site, so the day an entry lands # the whole fleet pulls it inside one 5-minute window: roughly 30 GB across 300 # bays, on the same share every bay needs for everything else. The failure is not # slow drivers, it is a floor that stops converging. # # -WaveStart with -Waves spreads that out. Each PC derives its own wave from its # hostname and does nothing until its turn, so there is no central coordination, # no per-bay configuration and nothing to reconcile afterwards. The hash is the # same idiom Register-GEEnforce.ps1 uses for its offset, SHA-256 rather than MD5 # because FIPS-enforced bays disable MD5 outright. # # A bay switched off during its wave installs on its next cycle instead. Late is # a non-event here; the wave sets the earliest moment, not a deadline. # # Unparseable wave arguments FAIL CLOSED - nothing installs, and it says so every # cycle. Failing open would restore precisely the stampede this exists to avoid, # and 30 GB cannot be un-sent, whereas a typo that installs nothing is loud in # the log and fixed in a minute. param( # Defaults to drivers.json beside this script. [string]$Manifest = '', # Install only these driver names; everything else in the manifest is # ignored. For a bay that needs one driver out of a site-wide set. [string[]]$Only = @(), # Report what is missing and change nothing. This is what a DSC TestScript # calls: exit 0 means compliant. A bay whose wave has not arrived is # compliant BY DESIGN - not installed is its desired state today. [switch]$TestOnly, # Date the rollout opens, e.g. '2026-08-25' or '2026-08-25 22:00'. Empty # means no gating at all, which is right for imaging time: a bay being built # is one bay, and it should come off the line complete. [string]$WaveStart = '', # How many waves to spread the fleet across. 0 or 1 means everyone at once. # 300 bays over 10 daily waves is ~30 bays and ~3 GB a day. [int]$Waves = 0, # Wave spacing. Hours finishes a 10-wave rollout inside a day; Days is the # cautious setting when nobody is watching the share. [ValidateSet('Days', 'Hours')] [string]$WaveUnit = 'Days', # Pilot bays skip the gate. Use this to prove one bay before the fleet. [switch]$IgnoreWave ) $ErrorActionPreference = 'Continue' $here = Split-Path -Parent $MyInvocation.MyCommand.Path if (-not $Manifest) { $Manifest = Join-Path $here 'drivers.json' } $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 [set] $msg" | Tee-Object -FilePath $logFile -Append | Out-Null } # The gate runs BEFORE the manifest is read, because the manifest lives on the # share too. A bay that is not due this wave must not touch the share at all - # reading it is cheap, but 300 bays deciding to read it in the same 5 minutes is # how this whole problem starts. if ($WaveStart -and -not $IgnoreWave) { $start = [datetime]::MinValue if (-not [datetime]::TryParse($WaveStart, [ref]$start)) { Log "ERROR -WaveStart '$WaveStart' is not a date. Refusing to install." Log ' (failing closed on purpose: guessing here would release the whole fleet at once)' exit 1 } $waveCount = $Waves if ($waveCount -lt 1) { $waveCount = 1 } # Same hostname hash as the enforcer's own start offset, so a bay's wave is # stable for the life of its name: it cannot drift between cycles, and a # rerun never moves a PC into a different wave. $hostHash = [System.BitConverter]::ToUInt32( [System.Security.Cryptography.SHA256]::Create().ComputeHash( [System.Text.Encoding]::UTF8.GetBytes([System.Environment]::MachineName)), 0) $wave = [int]($hostHash % [uint32]$waveCount) if ($WaveUnit -eq 'Hours') { $due = $start.AddHours($wave) } else { $due = $start.AddDays($wave) } if ((Get-Date) -lt $due) { Log ("wave {0} of {1} for {2}; not due until {3}. Nothing to do." -f ` $wave, $waveCount, [System.Environment]::MachineName, $due.ToString('yyyy-MM-dd HH:mm')) exit 0 } Log ("wave {0} of {1} opened {2}; proceeding" -f ` $wave, $waveCount, $due.ToString('yyyy-MM-dd HH:mm')) } if (-not (Test-Path $Manifest)) { Log "ERROR manifest not found: $Manifest" exit 1 } try { $config = Get-Content -Raw -Path $Manifest | ConvertFrom-Json } catch { Log "ERROR manifest is not valid JSON: $($_.Exception.Message)" exit 1 } $wanted = @($config.drivers) if ($Only.Count -gt 0) { $wanted = @($wanted | Where-Object { $Only -contains $_.drivername }) } if ($wanted.Count -eq 0) { Log "nothing to do: the manifest selects no drivers" exit 0 } $single = Join-Path $here 'Install-ShopdbPrinterDriver.ps1' if (-not (Test-Path $single)) { Log "ERROR Install-ShopdbPrinterDriver.ps1 is not beside this script" exit 1 } $missing = @() foreach ($driver in $wanted) { $name = $driver.drivername if (-not $name) { continue } if (Get-PrinterDriver -Name $name -ErrorAction SilentlyContinue) { Log "present: $name" continue } if ($TestOnly) { Log "MISSING: $name" $missing += $name continue } # Relative paths are resolved against the package, so the whole thing can be # copied anywhere - a share, C:\ProgramData, an Intune staging folder - and # still find its own payloads. $path = $driver.path if ($path -and -not [System.IO.Path]::IsPathRooted($path)) { $path = Join-Path $here $path } if (-not $path -or -not (Test-Path $path)) { Log "ERROR package not found for '$name': $path" $missing += $name continue } Log "installing: $name" & $single -DriverName $name -Source $path | Out-Null if (Get-PrinterDriver -Name $name -ErrorAction SilentlyContinue) { Log "installed: $name" } else { Log "FAILED: $name (see the per-driver lines above)" $missing += $name } } if ($missing.Count -gt 0) { Log ("not present: {0}" -f ($missing -join ', ')) exit 1 } Log "all $($wanted.Count) driver(s) present" exit 0