Needed for eMxInfo.txt (site-specific eDNC config). The file has no DisplayVersion in the registry and no canonical MSI; we ship it as a standalone secret on the SFLD share and key drift correction off its SHA256. When the yearly replacement drops, bump the hash in machineapps-manifest.json and every Standard-Machine PC catches up on next logon. Patched Install-FromManifest in all three copies (CMM, common, Standard) for consistency. Also adds the eMxInfo.txt entry to the Standard machineapps-manifest template and an Install-eMxInfo.cmd template that copies the file into both 32/64-bit eDNC Program Files paths.
273 lines
9.8 KiB
PowerShell
273 lines
9.8 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
|
|
}
|
|
"Hash" {
|
|
# Compare SHA256 of the on-disk file against the manifest's
|
|
# expected value. Used for content-versioned files that do not
|
|
# expose a DisplayVersion (secrets like eMxInfo.txt). Bumping
|
|
# DetectionValue in the manifest and replacing the file on the
|
|
# share is the entire update workflow.
|
|
if (-not (Test-Path $App.DetectionPath)) { return $false }
|
|
if (-not $App.DetectionValue) {
|
|
Write-InstallLog " Hash detection requires DetectionValue - treating as not installed" "WARN"
|
|
return $false
|
|
}
|
|
$actual = (Get-FileHash -Path $App.DetectionPath -Algorithm SHA256 -ErrorAction Stop).Hash
|
|
return ($actual -ieq $App.DetectionValue)
|
|
}
|
|
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
|