Rolls up everything from the CMM imaging test iteration tonight. No
single concern - several small, related polish items on the option-3
patched-MSI pipeline and the shopfloor-setup / sync_intune handoff.
- Rename all type-specific "01-Setup-<Type>.ps1" scripts to
"09-Setup-<Type>.ps1" across CMM, Display, Genspect, Keyence, Lab,
and WaxAndTrace. The "01-" prefix implied the script runs first in
the overall sequence when it actually runs between baseline (00, 04)
and finalization (06, 07). Logs now read "Running CMM setup:
09-Setup-CMM.ps1" which matches the real position. Standard/
01-eDNC.ps1 + 02-MachineNumberACLs.ps1 left alone - those digits
represent real within-type ordering.
- playbook/shopfloor-setup/site-config.json CMM profile updates:
- startupItems = [] (empty). Previously had WJ Shopfloor auto-launch
which the user does not want on CMM workstations. Now relies on
the Get-ProfileValue empty-array fix to not fall through to site
defaults.
- desktopApps + taskbarPins gain entries for PC-DMIS 2016, PC-DMIS
2019 R2, CLM Admin, and goCMM so 06-OrganizeDesktop Phase 2
materializes them into C:\\Users\\Public\\Desktop\\Shopfloor Tools\\
and 07-TaskbarLayout pins them. goCMM is under C:\\Program Files
(x86)\\General Electric\\goCMM\\ (GE product, not Hexagon).
- playbook/shopfloor-setup/Run-ShopfloorSetup.ps1: remove the blocking
"UNPLUG ethernet cable, press any key" prompt + the interactive
wired-NIC re-enable. The whole prompt block was a hard blocker on
the imaging chain that required a human to walk to each PC.
- playbook/shopfloor-setup/Shopfloor/lib/Monitor-IntuneProgress.ps1:
re-enable wired NICs unconditionally at the top of the transcript.
This is the new home for the re-enable that used to live behind the
prompt in Run-ShopfloorSetup. By the time sync_intune fires (after
PPKG reboot + auto-login + Stage-Dispatcher), the tech has had
minutes of wall-clock time to physically rewire from PXE to
production without us blocking on a keypress. Tower case is a
no-op because migrate-to-wifi.ps1 already left wired enabled.
- Internal comment updates in 09-Setup-CMM.ps1, cmm-manifest.json,
Install-FromManifest.ps1, and startnet.cmd (+ startnet-template)
to reflect the new filename.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
271 lines
9.7 KiB
PowerShell
271 lines
9.7 KiB
PowerShell
# Install-FromManifest.ps1 - Generic JSON-manifest installer for CMM apps.
|
|
#
|
|
# Duplicated (by design, for now) from Shopfloor\00-PreInstall-MachineApps.ps1
|
|
# with the Standard-PC-specific bits stripped out:
|
|
# - no PCTypes filter (every CMM manifest entry is CMM-only)
|
|
# - no site-name / machine-number placeholder replacement
|
|
# - no KillAfterDetection shortcut (Hexagon Burn bundles exit cleanly)
|
|
#
|
|
# A future pass will unify both runners behind one library; keeping them
|
|
# separate now avoids touching the Standard PC imaging path.
|
|
#
|
|
# Called from:
|
|
# - 09-Setup-CMM.ps1 at imaging time with InstallerRoot=C:\CMM-Install
|
|
# - CMM-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
|
|
|
|
# Synchronous write-through so each line hits disk immediately, mirroring
|
|
# the preinstall runner's approach - protects forensic trail if an installer
|
|
# triggers a reboot mid-loop.
|
|
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)"
|
|
|
|
# Detection helper - mirrors the preinstall runner's logic. Registry path +
|
|
# optional value name + optional exact value. The exact-value compare is how
|
|
# version-pinned drift detection works: bumping DetectionValue in the manifest
|
|
# makes the current install "fail" detection and reinstall.
|
|
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) {
|
|
# Cancel any pending reboot scheduled by a previous installer, same as the
|
|
# preinstall runner. Some Burn bundles schedule a reboot even with /norestart
|
|
# (chained bootstrapper ignores the flag for some internal prereqs).
|
|
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
|
|
|
|
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)"
|
|
}
|
|
}
|
|
else {
|
|
Write-InstallLog " Unsupported Type: $($app.Type) - skipping" "ERROR"
|
|
$failed++
|
|
continue
|
|
}
|
|
|
|
# Use Process.Start directly rather than Start-Process because PS 5.1's
|
|
# Start-Process -PassThru disposes the process handle when control returns,
|
|
# making ExitCode read as $null. Direct Process.Start gives us a live handle.
|
|
$proc = [System.Diagnostics.Process]::Start($psi)
|
|
|
|
# No per-app timeout here - PC-DMIS bundles can run 20+ minutes on slow
|
|
# disks and we don't want to kill them mid-chain. The calling script
|
|
# controls overall session timing.
|
|
$proc.WaitForExit()
|
|
$exitCode = $proc.ExitCode
|
|
|
|
# Burn and MSI exit codes:
|
|
# 0 success
|
|
# 1641 success, reboot initiated
|
|
# 3010 success, reboot pending
|
|
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" -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
|