Files
pxe-server/playbook/shopfloor-setup/Standard/lib/Install-FromManifest.ps1
cproudlock 3ef981f19e Add Standard-Machine logon enforcer for UDC/eDNC/NTLARS
Reason: Intune DSC's main-category YAML was pushing these to every main
device, including Timeclocks - DSC has no awareness of our pc-subtype
distinction. After UDC/eDNC/NTLARS are removed from the DSC YAML, ongoing
version drift would no longer be corrected. This enforcer replaces that,
scoped correctly by subtype.

Structure mirrors CMM (CMM-Enforce.ps1) and common (Acrobat-Enforce.ps1):
- Machine-Enforce.ps1: SYSTEM logon task; mounts SFLD share with HKLM-
  backed creds; hands off to Install-FromManifest.
- machineapps-manifest.template.json: repo reference; authoritative copy
  lives on the share at \\tsgwp00525.wjs.geaerospace.net\shared\dt\
  shopfloor\main\machineapps\machineapps-manifest.json.
- Register-MachineEnforce.ps1: idempotent setup; stages scripts to
  C:\Program Files\GE\MachineApps and registers the task.
- lib/Install-FromManifest.ps1: copy of the common/ version (already has
  Type=CMD support).

Sub-type gating belt-and-suspenders:
- Run-ShopfloorSetup.ps1 only calls Register-MachineEnforce when
  $pcType -eq "Standard" -and $pcSubType -eq "Machine".
- Machine-Enforce.ps1 itself re-reads pc-subtype.txt and exits early if
  not "Machine", so a mistakenly-deployed copy no-ops.

site-config.json:
- Added "machineappsSharePath" to Standard-Machine pcProfile.

Drive letter U: to stay clear of CMM (S:) and Acrobat (T:) enforcers
that may run concurrently at logon.

Update workflow:
  drop new UDC/eDNC/NTLARS installer on the SFLD share,
  bump DetectionValue in machineapps-manifest.json,
  every Machine PC catches up on next user logon.
2026-04-15 12:16:17 -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