# 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= # # 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