Files
pxe-server/playbook/shopfloor-setup/common/lib/Install-FromManifest.ps1
cproudlock 8848fca88a Add Acrobat Reader logon enforcer (cross-PC-type), provtool.exe arg fix
Acrobat Reader enforcement:
- playbook/shopfloor-setup/common/ is the cross-PC-type staging dir. Mirrors
  CMM/ structure (enforce script + its Install-FromManifest copy + manifest
  template + register script).
- Acrobat-Enforce.ps1 runs as SYSTEM on every logon, reads
  acrobatSharePath from site-config.common, mounts the SFLD share with
  the same HKLM-backed credential lookup CMM-Enforce uses, hands the
  acrobat-manifest.json from the share to Install-FromManifest.
- Install-FromManifest extended with Type=CMD so it can invoke vendor-
  supplied .cmd wrappers (Install-AcroReader.cmd does a two-step MSI+MSP
  install that does not fit MSI/EXE types cleanly). cmd.exe /c wraps it
  because UseShellExecute=false cannot launch .cmd directly.
- Register-AcrobatEnforce.ps1 stages scripts to C:\Program Files\GE\Acrobat
  and registers "GE Acrobat Enforce" scheduled task. Called from
  Run-ShopfloorSetup.ps1 right before the enrollment (PPKG) step so it
  applies to every PC type, not just CMM.
- acrobat-manifest.template.json is the repo reference; the authoritative
  copy lives on the SFLD share at
  \\tsgwp00525.wjs.geaerospace.net\shared\dt\shopfloor\common\acrobat\
  Bumping Acrobat updates = drop new MSP on share, bump DetectionValue in
  manifest; enforcer catches every PC on next logon.
- site-config.json: add "common": { "acrobatSharePath": ... }. Uses a
  new top-level block rather than a PC-type-specific one since Acrobat
  applies everywhere.

Initial install still happens via the preinstall flow
(Install-AcroReader.cmd during WinPE). The enforcer is the ongoing-
updates side; on a freshly-imaged PC detection passes and it no-ops.

Also in this commit:
- run-enrollment.ps1: provtool.exe argument syntax fix. First test
  returned 0x80004005 E_FAIL in 1s because /ppkg: and /log: are not
  valid provtool flags; the cmdlet's internal call used positional
  path + /quiet + /source. Switched to that syntax.
2026-04-15 09:24:13 -04:00

259 lines
8.9 KiB
PowerShell

# Install-FromManifest.ps1 - Generic JSON-manifest installer for cross-PC-type
# apps enforced from the SFLD share (Acrobat Reader DC today; others later).
#
# Duplicated from CMM\lib\Install-FromManifest.ps1 with a few differences:
# - adds Type=CMD (cmd.exe /c wrapper, needed for Acrobat's two-step
# MSI + MSP install that the vendor ships as Install-AcroReader.cmd)
# - unchanged otherwise; a future pass will unify both libraries.
#
# Called from:
# - Acrobat-Enforce.ps1 on logon with InstallerRoot=<mounted tsgwp00525 share>
#
# Returns via exit code: 0 if every required app is either already installed
# or installed successfully; non-zero if any install failed.
param(
[Parameter(Mandatory=$true)]
[string]$ManifestPath,
[Parameter(Mandatory=$true)]
[string]$InstallerRoot,
[Parameter(Mandatory=$true)]
[string]$LogFile
)
$ErrorActionPreference = 'Continue'
$logDir = Split-Path -Parent $LogFile
if (-not (Test-Path $logDir)) {
New-Item -Path $logDir -ItemType Directory -Force | Out-Null
}
function Write-InstallLog {
param(
[Parameter(Mandatory=$true, Position=0)]
[string]$Message,
[Parameter(Position=1)]
[ValidateSet('INFO','WARN','ERROR')]
[string]$Level = 'INFO'
)
$stamp = Get-Date -Format "yyyy-MM-dd HH:mm:ss"
$line = "[$stamp] [$Level] $Message"
Write-Host $line
try {
$fs = New-Object System.IO.FileStream(
$LogFile,
[System.IO.FileMode]::Append,
[System.IO.FileAccess]::Write,
[System.IO.FileShare]::Read,
4096,
[System.IO.FileOptions]::WriteThrough
)
$bytes = [System.Text.Encoding]::UTF8.GetBytes($line + "`r`n")
$fs.Write($bytes, 0, $bytes.Length)
$fs.Flush()
$fs.Dispose()
} catch {
Add-Content -Path $LogFile -Value $line -ErrorAction SilentlyContinue
}
}
Write-InstallLog "================================================================"
Write-InstallLog "=== Install-FromManifest session start (PID $PID) ==="
Write-InstallLog "Manifest: $ManifestPath"
Write-InstallLog "InstallerRoot: $InstallerRoot"
Write-InstallLog "================================================================"
if (-not (Test-Path -LiteralPath $ManifestPath)) {
Write-InstallLog "Manifest not found: $ManifestPath" "ERROR"
exit 2
}
if (-not (Test-Path -LiteralPath $InstallerRoot)) {
Write-InstallLog "InstallerRoot not found: $InstallerRoot" "ERROR"
exit 2
}
try {
$config = Get-Content -LiteralPath $ManifestPath -Raw | ConvertFrom-Json
} catch {
Write-InstallLog "Failed to parse manifest: $_" "ERROR"
exit 2
}
if (-not $config.Applications) {
Write-InstallLog "No Applications in manifest - nothing to do"
exit 0
}
Write-InstallLog "Manifest lists $($config.Applications.Count) app(s)"
function Test-AppInstalled {
param($App)
if (-not $App.DetectionMethod) { return $false }
try {
switch ($App.DetectionMethod) {
"Registry" {
if (-not (Test-Path $App.DetectionPath)) { return $false }
if ($App.DetectionName) {
$value = Get-ItemProperty -Path $App.DetectionPath -Name $App.DetectionName -ErrorAction SilentlyContinue
if (-not $value) { return $false }
if ($App.DetectionValue) {
return ($value.$($App.DetectionName) -eq $App.DetectionValue)
}
return $true
}
return $true
}
"File" {
return Test-Path $App.DetectionPath
}
default {
Write-InstallLog " Unknown detection method: $($App.DetectionMethod)" "WARN"
return $false
}
}
} catch {
Write-InstallLog " Detection check threw: $_" "WARN"
return $false
}
}
$installed = 0
$skipped = 0
$failed = 0
foreach ($app in $config.Applications) {
cmd /c "shutdown /a 2>nul" *>$null
Write-InstallLog "==> $($app.Name)"
if (Test-AppInstalled -App $app) {
Write-InstallLog " Already installed at expected version - skipping"
$skipped++
continue
}
$installerPath = Join-Path $InstallerRoot $app.Installer
if (-not (Test-Path -LiteralPath $installerPath)) {
Write-InstallLog " Installer file not found: $installerPath" "ERROR"
$failed++
continue
}
Write-InstallLog " Installing from $installerPath"
if ($app.InstallArgs) {
Write-InstallLog " InstallArgs: $($app.InstallArgs)"
}
try {
$psi = New-Object System.Diagnostics.ProcessStartInfo
$psi.UseShellExecute = $false
$psi.CreateNoWindow = $true
$psi.WindowStyle = [System.Diagnostics.ProcessWindowStyle]::Hidden
$msiLog = $null
if ($app.Type -eq "MSI") {
$safeName = $app.Name -replace '[^a-zA-Z0-9]','_'
$msiLog = Join-Path $logDir "msi-$safeName.log"
if (Test-Path $msiLog) { Remove-Item $msiLog -Force -ErrorAction SilentlyContinue }
$psi.FileName = "msiexec.exe"
$psi.Arguments = "/i `"$installerPath`""
if ($app.InstallArgs) { $psi.Arguments += " " + $app.InstallArgs }
$psi.Arguments += " /L*v `"$msiLog`""
Write-InstallLog " msiexec verbose log: $msiLog"
}
elseif ($app.Type -eq "EXE") {
$psi.FileName = $installerPath
if ($app.InstallArgs) { $psi.Arguments = $app.InstallArgs }
if ($app.LogFile) {
Write-InstallLog " Installer log: $($app.LogFile)"
}
}
elseif ($app.Type -eq "CMD") {
# .cmd/.bat scripts cannot be executed directly via
# ProcessStartInfo with UseShellExecute=false; route through
# cmd.exe /c. Vendor-provided two-step install wrappers
# (Install-AcroReader.cmd) fit here naturally.
$psi.FileName = "cmd.exe"
$psi.Arguments = "/c `"$installerPath`""
if ($app.InstallArgs) { $psi.Arguments += " " + $app.InstallArgs }
if ($app.LogFile) {
Write-InstallLog " Installer log: $($app.LogFile)"
}
}
else {
Write-InstallLog " Unsupported Type: $($app.Type) - skipping" "ERROR"
$failed++
continue
}
$proc = [System.Diagnostics.Process]::Start($psi)
$proc.WaitForExit()
$exitCode = $proc.ExitCode
if ($exitCode -eq 0 -or $exitCode -eq 1641 -or $exitCode -eq 3010) {
Write-InstallLog " Exit code $exitCode - SUCCESS"
if ($exitCode -eq 3010) { Write-InstallLog " (Reboot pending for $($app.Name))" }
if ($exitCode -eq 1641) { Write-InstallLog " (Installer initiated a reboot for $($app.Name))" }
$installed++
}
else {
Write-InstallLog " Exit code $exitCode - FAILED" "ERROR"
if (($app.Type -eq "EXE" -or $app.Type -eq "CMD") -and $app.LogFile -and (Test-Path $app.LogFile)) {
Write-InstallLog " --- last 30 lines of $($app.LogFile) ---"
Get-Content $app.LogFile -Tail 30 -ErrorAction SilentlyContinue | ForEach-Object {
Write-InstallLog " $_"
}
Write-InstallLog " --- end installer log tail ---"
}
if ($app.Type -eq "MSI" -and $msiLog -and (Test-Path $msiLog)) {
Write-InstallLog " --- meaningful lines from $msiLog ---"
$patterns = @(
'Note: 1: ',
'return value 3',
'Error \d+\.',
'CustomAction .* returned actual error',
'Failed to ',
'Installation failed',
'1: 2262',
'1: 2203',
'1: 2330'
)
$regex = ($patterns -join '|')
$matches = Select-String -Path $msiLog -Pattern $regex -ErrorAction SilentlyContinue |
Select-Object -First 30
if ($matches) {
foreach ($m in $matches) { Write-InstallLog " $($m.Line.Trim())" }
} else {
Get-Content $msiLog -Tail 25 -ErrorAction SilentlyContinue | ForEach-Object {
Write-InstallLog " $_"
}
}
Write-InstallLog " --- end MSI log scan ---"
}
$failed++
}
} catch {
Write-InstallLog " Install threw: $_" "ERROR"
$failed++
}
}
Write-InstallLog "============================================"
Write-InstallLog "Install-FromManifest complete: $installed installed, $skipped skipped, $failed failed"
Write-InstallLog "============================================"
cmd /c "shutdown /a 2>nul" *>$null
if ($failed -gt 0) { exit 1 }
exit 0