shopfloor: CMM PC-DMIS version gate, ShopDB reporter fixes, staging self-heal

- lib Install-FromManifest 2.5->2.6: add _CmmVersion per-entry filter (reads
  C:\Enrollment\cmm\version.txt). Lifted the version gate out of 09-Setup-CMM
  into the shared lib so imaging and GE-Enforce apply it identically and cannot
  drift (root cause of PC-DMIS 2016 installing on every CMM).
- Install-goCMMSettings: canonicalize the part-group share host to the FQDN in
  both the registry and ApplicationSettings.xml. Handles bare \\tsgwp00525\ and
  the legacy rd.ds.ge.com domain; idempotent. VM-tested.
- Report-AssetToShopDB: resolve the machine number eDNC registry first, then fall
  back to C:\Enrollment\machine-number.txt (matches the lib resolution order) so
  a freshly imaged PC still reports its number for the PC-machine relationship.
- Add Update-CMMEnforcer.ps1/.bat: update one CMM's local lib to the gated
  version and self-heal its PC-DMIS version.
- Add Debug-ShopDBReporting.ps1/.bat: one-shot reporter triage (preconditions,
  client log, live test POST, verdict).
- Add Verify-And-Heal-Staging.ps1/.bat: post-boot check that every imaging
  payload arrived and re-pull anything missing from the share, including the CMM
  bundle and the selected bay's backup (the payload that times out in WinPE).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
cproudlock
2026-06-14 09:14:54 -04:00
parent c2538a05c5
commit e97e5bd049
10 changed files with 735 additions and 45 deletions

View File

@@ -38,6 +38,25 @@ param(
$DefaultRewrites = @(
@{ From = 'rd.ds.ge.com'; To = 'wjs.geaerospace.net' } # WJ legacy domain -> new domain
)
# ----------------------------------------------------------------------------
# Part-group share host canonicalization. Bays were captured with three
# inconsistent forms of the share host in the goCMM 'Selected Part Group' /
# ApplicationSettings.xml <PartGroup FullName>:
# \\tsgwp00525\... (bare hostname - DNS-suffix dependent)
# \\tsgwp00525.rd.ds.ge.com\... (legacy GE corp domain - dead on the
# air-gapped shopfloor net)
# \\tsgwp00525.wjs.geaerospace.net\... (correct)
# The DefaultRewrites above only fix the middle form. goCMM DISPLAYS the part
# group from ApplicationSettings.xml, so the bare form survived into the UI.
# Pin every form to the FQDN in BOTH the registry and the XML. The regex
# matches the UNC host with an optional domain suffix and is idempotent (an
# already-correct FQDN maps to itself). Disabled by -NoDefaultRewrite.
$PartGroupHostShort = 'tsgwp00525'
$PartGroupHostFqdn = 'tsgwp00525.wjs.geaerospace.net'
# \\HOST or \\HOST.any.domain (followed by the next path backslash) -> \\FQDN
$PartGroupHostRx = '(?i)\\\\' + [regex]::Escape($PartGroupHostShort) + '(?:\.[A-Za-z0-9.\-]+)?(?=\\)'
$PartGroupHostTo = '\\' + $PartGroupHostFqdn
$ErrorActionPreference = 'Continue'
$ts = Get-Date -Format 'yyyyMMdd-HHmmss'
$logDir = 'C:\Logs\CMM'
@@ -148,6 +167,45 @@ if ($rewrites.Count -gt 0) {
}
}
# --- Canonicalize the part-group share host to the FQDN in reg + XML ---
# Runs AFTER the geaofi robocopy (so ApplicationSettings.xml is in place)
# and AFTER the literal rewrites above. Idempotent. This is what makes
# goCMM show the FQDN regardless of which form the bay was captured with.
if (-not $NoDefaultRewrite) {
# registry: every string value under the goCMM key
if (Test-Path $goCmmKey) {
try {
$props = Get-ItemProperty -Path $goCmmKey
foreach ($p in $props.PSObject.Properties) {
if ($p.Name -like 'PS*') { continue }
if (($p.Value -is [string]) -and ([regex]::IsMatch($p.Value, $PartGroupHostRx))) {
$new = [regex]::Replace($p.Value, $PartGroupHostRx, $PartGroupHostTo)
if ($new -ne $p.Value) {
Set-ItemProperty -Path $goCmmKey -Name $p.Name -Value $new
Log " host-canon reg [$($p.Name)] -> $new"
}
}
}
} catch { Log " WARN: host canonicalize (reg) failed: $($_.Exception.Message)" }
}
# files: every text file under the Shared Data Directory (incl ApplicationSettings.xml)
if (Test-Path $sharedDir) {
$utf8NoBomHost = New-Object System.Text.UTF8Encoding($false)
Get-ChildItem -Path $sharedDir -Recurse -File -Include *.xml,*.txt,*.csv,*.config,*.ini,*.bas -ErrorAction SilentlyContinue | ForEach-Object {
try {
$c = [System.IO.File]::ReadAllText($_.FullName)
if ([regex]::IsMatch($c, $PartGroupHostRx)) {
$nc = [regex]::Replace($c, $PartGroupHostRx, $PartGroupHostTo)
if ($nc -ne $c) {
[System.IO.File]::WriteAllText($_.FullName, $nc, $utf8NoBomHost)
Log " host-canon file [$($_.Name)] rewritten"
}
}
} catch { Log " WARN: host canonicalize $($_.FullName): $($_.Exception.Message)" }
}
}
}
# --- Grant BUILTIN\Users ReadKey+WriteKey on the reg key (goCMM opens it writable:true to read) ---
if (Test-Path $goCmmKey) {
try {

View File

@@ -0,0 +1,41 @@
@echo off
REM ==========================================================================
REM Update-CMMEnforcer.bat - update ONE CMM's local GE-Enforce lib to the
REM version that honors the PC-DMIS _CmmVersion gate, then self-heal.
REM
REM Usage (run on the CMM PC):
REM Update-CMMEnforcer.bat update lib + report wrong versions + self-heal
REM Update-CMMEnforcer.bat /remove also uninstall the wrong PC-DMIS version(s)
REM
REM Pulls the lib from the tsgwp00525 share by default. To use a USB copy:
REM Update-CMMEnforcer.bat /src "D:\Install-FromManifest.ps1"
REM Update-CMMEnforcer.bat /remove /src "D:\Install-FromManifest.ps1"
REM ==========================================================================
setlocal EnableDelayedExpansion
REM --- self-elevate to administrator ---
net session >nul 2>&1
if %errorlevel% neq 0 (
echo Requesting administrator elevation...
powershell -NoProfile -Command "Start-Process -Verb RunAs -FilePath '%~f0' -ArgumentList '%*'"
exit /b
)
set "PS=%~dp0Update-CMMEnforcer.ps1"
set "ARGS="
:parse
if "%~1"=="" goto run
if /I "%~1"=="/remove" set "ARGS=!ARGS! -RemoveWrongVersions" & shift & goto parse
if /I "%~1"=="/src" set "ARGS=!ARGS! -SourceLib '%~2'" & shift & shift & goto parse
shift
goto parse
:run
echo Running: powershell -File "%PS%" !ARGS!
powershell.exe -NoProfile -ExecutionPolicy Bypass -File "%PS%" !ARGS!
set "RC=%errorlevel%"
echo.
echo Exit code: %RC%
pause
endlocal

View File

@@ -0,0 +1,131 @@
<#
Update-CMMEnforcer.ps1
Bring ONE CMM shopfloor PC's local GE-Enforce engine up to the lib version
that understands the PC-DMIS _CmmVersion gate (lib MINOR >= 6), then run a
single enforce cycle so it self-heals to this bay's correct PC-DMIS version.
Why this exists: GE-Enforce runs its lib from LOCAL
C:\Program Files\GE\Shopfloor\lib\ and does NOT auto-update from the share.
A bay running an older lib ignores the _CmmVersion tags in the synced CMM
manifest and would install every version it does not detect. This script
pushes the new lib locally so the gate takes effect.
Run as administrator on the CMM PC.
Params:
-SourceLib Explicit path to the 2.6+ lib (e.g. a USB copy). If omitted,
pulls from the tsgwp00525 share (-ShareRoot).
-ShareRoot Share root holding common\lib\Install-FromManifest.ps1.
-RemoveWrongVersions Uninstall any installed PC-DMIS version that does not
match this bay's C:\Enrollment\cmm\version.txt.
-NoEnforceRun Skip the post-update enforce cycle (just update + report).
#>
param(
[string]$SourceLib,
[string]$ShareRoot = '\\tsgwp00525.wjs.geaerospace.net\shared\dt\shopfloor',
[string]$InstallRoot = 'C:\Program Files\GE\Shopfloor',
[int]$MinLibMinor = 6,
[switch]$RemoveWrongVersions,
[switch]$NoEnforceRun
)
$ErrorActionPreference = 'Stop'
function Log($m){ Write-Host ("[Update-CMMEnforcer] {0}" -f $m) }
function Get-LibMinor($path){
if (-not (Test-Path -LiteralPath $path)) { return -1 }
$m = Select-String -Path $path -Pattern 'LIB_MANIFEST_MINOR\s*=\s*(\d+)' -ErrorAction SilentlyContinue | Select-Object -First 1
if ($m) { return [int]$m.Matches[0].Groups[1].Value } else { return -1 }
}
# ---------------------------------------------------------------------------
# 1. Resolve the source lib (USB override or share) and validate its version
# ---------------------------------------------------------------------------
if (-not $SourceLib) { $SourceLib = Join-Path $ShareRoot 'common\lib\Install-FromManifest.ps1' }
if (-not (Test-Path -LiteralPath $SourceLib)) {
throw "Source lib not found: $SourceLib (if using the share, confirm you are authenticated to it; or pass -SourceLib <path to a USB copy>)"
}
$srcMinor = Get-LibMinor $SourceLib
if ($srcMinor -lt $MinLibMinor) {
throw "Source lib is MINOR=$srcMinor but >= $MinLibMinor is required. Point -SourceLib at the updated lib (or sync the share first)."
}
$dstLib = Join-Path $InstallRoot 'lib\Install-FromManifest.ps1'
$oldMinor = Get-LibMinor $dstLib
Log "Source lib MINOR=$srcMinor ($SourceLib)"
Log "Local lib MINOR=$oldMinor ($dstLib)"
# ---------------------------------------------------------------------------
# 2. Back up + copy the lib into place, verify
# ---------------------------------------------------------------------------
$libDir = Split-Path -Parent $dstLib
New-Item -ItemType Directory -Path $libDir -Force | Out-Null
if (Test-Path -LiteralPath $dstLib) {
$bak = "$dstLib.bak-{0}" -f (Get-Date -Format 'yyyyMMdd-HHmmss')
Copy-Item -LiteralPath $dstLib -Destination $bak -Force
Log "Backed up local lib -> $bak"
}
Copy-Item -LiteralPath $SourceLib -Destination $dstLib -Force
$newMinor = Get-LibMinor $dstLib
if ($newMinor -lt $MinLibMinor) { throw "Copy did not take - local lib still MINOR=$newMinor" }
Log "Local lib updated: MINOR $oldMinor -> $newMinor (OK - gate is now active)"
# ---------------------------------------------------------------------------
# 3. Report bay version + which PC-DMIS versions are installed
# ---------------------------------------------------------------------------
$verFile = 'C:\Enrollment\cmm\version.txt'
$bayVer = if (Test-Path -LiteralPath $verFile) { (Get-Content -LiteralPath $verFile -First 1).Trim() } else { '' }
Log "Bay PC-DMIS version (version.txt): $(if($bayVer){$bayVer}else{'(none - bay not resolved; gate will install-all)'})"
$pcd = @(
[pscustomobject]@{ Ver='2016'; Guid='{5389B196-81F0-44A9-A073-4C1D72041F09}'; Name='PC-DMIS 2016.0' },
[pscustomobject]@{ Ver='2019'; Guid='{49DBE7F9-228A-4E66-8BB5-DB5A446DCAE7}'; Name='PC-DMIS 2019 R2' },
[pscustomobject]@{ Ver='2026'; Guid='{81BACE1B-FB08-4DCF-8100-79911AD3EC1E}'; Name='PC-DMIS 2026.1' }
)
function Test-MsiInstalled($guid){
foreach($r in @(
"HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\$guid",
"HKLM:\SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall\$guid")){
if (Test-Path -LiteralPath $r) { return $true }
}
return $false
}
$installed = @($pcd | Where-Object { Test-MsiInstalled $_.Guid } | ForEach-Object { $_.Ver })
Log "PC-DMIS installed: $(if($installed){$installed -join ', '}else{'none'})"
$wrong = @($installed | Where-Object { $bayVer -and $_ -ne $bayVer })
if ($wrong.Count) { Log "WRONG for this bay (expected $bayVer): $($wrong -join ', ')" }
# ---------------------------------------------------------------------------
# 4. Optionally uninstall wrong versions
# ---------------------------------------------------------------------------
if ($RemoveWrongVersions -and $bayVer -and $wrong.Count) {
foreach($p in ($pcd | Where-Object { $wrong -contains $_.Ver })){
Log "Uninstalling $($p.Name) $($p.Guid) ..."
$proc = Start-Process msiexec.exe -ArgumentList "/x $($p.Guid) /qn /norestart REBOOT=ReallySuppress" -Wait -PassThru -WindowStyle Hidden
Log " msiexec exit $($proc.ExitCode) $(if($proc.ExitCode -in 0,1605,3010){'(OK)'}else{'(check)'})"
}
} elseif ($wrong.Count) {
Log "Re-run with -RemoveWrongVersions to uninstall the wrong version(s)."
}
# ---------------------------------------------------------------------------
# 5. Kick one enforce cycle so it self-heals against the gated manifest
# ---------------------------------------------------------------------------
if (-not $NoEnforceRun) {
$task = Get-ScheduledTask -ErrorAction SilentlyContinue | Where-Object { $_.TaskName -match 'Enforce' } | Select-Object -First 1
$enf = Join-Path $InstallRoot 'GE-Enforce.ps1'
if ($task) {
Log "Triggering scheduled task '$($task.TaskName)' ..."
Start-ScheduledTask -TaskName $task.TaskName -TaskPath $task.TaskPath
Log "Triggered. Watch C:\Logs\Shopfloor\enforce-*.log"
} elseif (Test-Path -LiteralPath $enf) {
Log "No enforce task found - running GE-Enforce.ps1 directly ..."
& powershell.exe -NoProfile -ExecutionPolicy Bypass -File $enf | Out-Null
Log "Enforce cycle done. See C:\Logs\Shopfloor\enforce-*.log"
} else {
Log "Could not find an enforce task or $enf - trigger enforcement manually."
}
}
Log "DONE. lib MINOR=$newMinor | bay=$(if($bayVer){$bayVer}else{'?'}) | installed=$($installed -join ',')"
Log "Verify next enforce log shows the 2016/2026 entries skipped with '_CmmVersion filter'."