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

@@ -0,0 +1,27 @@
@echo off
REM ==========================================================================
REM Verify-And-Heal-Staging.bat - check every imaging payload arrived on this PC
REM and re-pull whatever is missing from the enrollment share.
REM
REM Usage (run on the PC):
REM Verify-And-Heal-Staging.bat verify + heal anything missing
REM Verify-And-Heal-Staging.bat /verifyonly report only, do not pull
REM ==========================================================================
setlocal EnableDelayedExpansion
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=%~dp0Verify-And-Heal-Staging.ps1"
set "ARGS="
if /I "%~1"=="/verifyonly" set "ARGS=-VerifyOnly"
powershell.exe -NoProfile -ExecutionPolicy Bypass -File "%PS%" %ARGS%
echo.
echo Exit code: %errorlevel% (0=all present/healed, 1=still missing)
pause
endlocal

View File

@@ -0,0 +1,155 @@
<#
Verify-And-Heal-Staging.ps1
Post-boot check that every payload the imaging flow is supposed to stage onto a
shopfloor PC actually arrived - and re-pull (heal) anything missing from the
enrollment share. Runs in full Windows (reliable network), so it is immune to the
WinPE samba-idle-drop that loses copies during the WIM apply.
Covers the generic Fetch payload (shopfloor-setup tree + preinstall bundle) AND
the heavy per-type payload that Fetch-StagingPayload does NOT pull today: the CMM
bundle (C:\CMM-Install) and the selected bay's backup set
(C:\CMM-Install\backups\<cmmid>). That is the one that silently goes missing when
WinPE staging runs out of time before reboot.
Designed to be:
- run manually on a problem PC (Verify-And-Heal-Staging.bat), or
- called from the pre-install phase before 00-PreInstall-MachineApps so a bay is
never left under-provisioned.
Idempotent. Verifies first; only heals what is actually missing/empty. Heals use
/R:3 /W:5 (resilient), not the WinPE fail-fast /R:1 /W:1.
Share + creds: read from C:\Enrollment\fetch-source.txt (line1=UNC, line2=user,
line3=pass) - same file Fetch-StagingPayload uses - else the defaults below.
Run as administrator. Exit 0 = everything present or healed; 1 = something still
missing after heal attempts (read the table).
#>
param(
[string]$ShareUnc,
[string]$ShareUser,
[string]$SharePass,
[switch]$VerifyOnly # report only, do not heal
)
$ErrorActionPreference = 'Continue'
$ts = Get-Date -Format 'yyyyMMdd-HHmmss'
$logDir = 'C:\Logs\Fetch'
New-Item -ItemType Directory -Path $logDir -Force -ErrorAction SilentlyContinue | Out-Null
$log = Join-Path $logDir "verify-heal-$ts.log"
function Log($m,$lvl='INFO'){ $line="[$(Get-Date -Format 'HH:mm:ss')] [$lvl] $m"; Write-Host $line; Add-Content -Path $log -Value $line -EA SilentlyContinue }
# --- share + creds (mirror Fetch-StagingPayload) ---
$defUnc='\\172.16.9.1\enrollment'; $defUser='pxe-upload'; $defPass='pxe'
$srcFile='C:\Enrollment\fetch-source.txt'
if ((-not $ShareUnc) -and (Test-Path -LiteralPath $srcFile)) {
$l=@(Get-Content -LiteralPath $srcFile -EA SilentlyContinue)
if ($l.Count -ge 1 -and $l[0].Trim()) { $ShareUnc=$l[0].Trim() }
if ($l.Count -ge 2 -and $l[1].Trim()) { $ShareUser=$l[1].Trim() }
if ($l.Count -ge 3 -and $l[2].Trim()) { $SharePass=$l[2].Trim() }
}
if (-not $ShareUnc) { $ShareUnc=$defUnc }
if (-not $ShareUser) { $ShareUser=$defUser }
if (-not $SharePass) { $SharePass=$defPass }
# --- identity ---
function ReadTxt($p){ if (Test-Path -LiteralPath $p) { (Get-Content -LiteralPath $p -First 1 -EA 0).Trim() } else { '' } }
$pcType = ReadTxt 'C:\Enrollment\pc-type.txt'
$cmmid = ReadTxt 'C:\Enrollment\cmm\cmmid.txt'
Log "=== Verify-And-Heal-Staging ==="
Log "share=$ShareUnc user=$ShareUser pcType=$(if($pcType){$pcType}else{'(none)'}) cmmid=$(if($cmmid){$cmmid}else{'(none)'}) verifyOnly=$VerifyOnly"
# --- expected payload manifest -------------------------------------------------
# Each: Label, Src (under share), Dst, Mode (File|Dir), Verify (path that must
# exist to count as present), Optional (missing-and-no-source is not a failure),
# Files (for Mode=File), Xd (robocopy /XD dirs to exclude on heal).
$items = New-Object System.Collections.Generic.List[object]
function Add-Item($Label,$Src,$Dst,$Mode,$Verify,$Files=$null,$Optional=$false,$Xd=$null){
$items.Add([pscustomobject]@{Label=$Label;Src=$Src;Dst=$Dst;Mode=$Mode;Verify=$Verify;Files=$Files;Optional=$Optional;Xd=$Xd})
}
$ENR='C:\Enrollment'; $SFD='C:\Enrollment\shopfloor-setup'; $PIN='C:\PreInstall'
Add-Item 'Run-ShopfloorSetup.ps1' 'shopfloor-setup' $ENR 'File' (Join-Path $ENR 'Run-ShopfloorSetup.ps1') @('Run-ShopfloorSetup.ps1')
Add-Item 'Shopfloor baseline' 'shopfloor-setup\Shopfloor' (Join-Path $SFD 'Shopfloor') 'Dir' (Join-Path $SFD 'Shopfloor')
Add-Item 'common' 'shopfloor-setup\common' (Join-Path $SFD 'common') 'Dir' (Join-Path $SFD 'common')
Add-Item '_ntlars-backups' 'shopfloor-setup\_ntlars-backups' (Join-Path $SFD '_ntlars-backups') 'Dir' (Join-Path $SFD '_ntlars-backups') $null $true
if ($pcType) {
Add-Item "type:$pcType" "shopfloor-setup\$pcType" (Join-Path $SFD $pcType) 'Dir' (Join-Path $SFD $pcType)
}
Add-Item 'preinstall.json' 'pre-install' $PIN 'File' (Join-Path $PIN 'preinstall.json') @('preinstall.json')
Add-Item 'preinstall installers' 'pre-install\installers' (Join-Path $PIN 'installers') 'Dir' (Join-Path $PIN 'installers')
Add-Item 'udc-backups' 'pre-install\udc-backups' (Join-Path $PIN 'udc-backups') 'Dir' (Join-Path $PIN 'udc-backups') $null $true
# --- heavy CMM payload (the gap) ---
if ($pcType -eq 'gea-shopfloor-cmm') {
Add-Item 'CMM bundle' 'installers-post\cmm' 'C:\CMM-Install' 'Dir' 'C:\CMM-Install\cmm-manifest.json' $null $false 'backups'
if ($cmmid) {
Add-Item "CMM backup ($cmmid)" "installers-post\cmm\backups\$cmmid" "C:\CMM-Install\backups\$cmmid" 'Dir' "C:\CMM-Install\backups\$cmmid" $null $true
}
}
# --- present check -------------------------------------------------------------
function Test-Present($it){
if (-not (Test-Path -LiteralPath $it.Verify)) { return $false }
if ($it.Mode -eq 'Dir') {
$n = @(Get-ChildItem -LiteralPath $it.Verify -Recurse -File -EA SilentlyContinue).Count
return ($n -gt 0)
}
return $true
}
# --- mount share fresh (only if we will heal) ---------------------------------
$drive='Z:'; $mounted=$false
function Mount-Share { & net use $drive /delete /y 2>$null | Out-Null; & net use $drive $ShareUnc /user:$ShareUser $SharePass /persistent:no 2>&1 | Out-Null; return ($LASTEXITCODE -eq 0) }
# --- first pass: verify --------------------------------------------------------
$report = New-Object System.Collections.Generic.List[object]
$missing = New-Object System.Collections.Generic.List[object]
foreach ($it in $items) {
if (Test-Present $it) { $report.Add([pscustomobject]@{Item=$it.Label;Status='PRESENT'}) }
else { $missing.Add($it); $report.Add([pscustomobject]@{Item=$it.Label;Status=$(if($it.Optional){'MISSING(opt)'}else{'MISSING'})}) }
}
# --- heal missing --------------------------------------------------------------
if ($missing.Count -gt 0 -and -not $VerifyOnly) {
Log "$($missing.Count) item(s) missing - mounting share to heal"
for ($a=1;$a -le 5 -and -not $mounted;$a++){ if (Mount-Share){$mounted=$true;Log "Mounted $ShareUnc as $drive"} else {Log "mount attempt $a failed - 10s" 'WARN'; Start-Sleep 10} }
if (-not $mounted) { Log "Could not mount share - cannot heal. $($missing.Count) item(s) remain missing." 'ERROR' }
else {
foreach ($it in $missing) {
$src = Join-Path $drive $it.Src
if (-not (Test-Path -LiteralPath $src)) {
Log "[heal SKIP] $($it.Label): not on share ($src)$(if($it.Optional){' - optional'})" $(if($it.Optional){'INFO'}else{'WARN'})
($report | Where-Object { $_.Item -eq $it.Label })[0].Status = $(if($it.Optional){'ABSENT(opt)'}else{'NO-SOURCE'})
continue
}
if (-not (Test-Path -LiteralPath $it.Dst)) { New-Item -ItemType Directory -Path $it.Dst -Force | Out-Null }
$args=@($src,$it.Dst)
if ($it.Mode -eq 'Dir') { $args+='/E' } else { $args+=$it.Files }
if ($it.Xd) { $args+=@('/XD',(Join-Path $src $it.Xd)) }
$args+=@('/R:3','/W:5','/NFL','/NDL','/NP')
Log "[heal] $($it.Label): robocopy $src -> $($it.Dst)"
& robocopy @args | Out-Null
$rc=$LASTEXITCODE
Start-Sleep 1
$ok = Test-Present $it
($report | Where-Object { $_.Item -eq $it.Label })[0].Status = $(if($ok){'HEALED'}elseif($rc -ge 8){'HEAL-FAIL'}else{'STILL-MISSING'})
Log "[heal $(if($ok){'OK'}else{'FAIL'})] $($it.Label) robocopy exit=$rc present=$ok"
}
& net use $drive /delete /y 2>$null | Out-Null
}
}
# --- report --------------------------------------------------------------------
Log '================ STAGING VERIFY/HEAL REPORT ================'
foreach ($r in $report) { Log (" {0,-26} {1}" -f $r.Item, $r.Status) }
$bad = @($report | Where-Object { $_.Status -in @('MISSING','NO-SOURCE','HEAL-FAIL','STILL-MISSING') })
if ($bad.Count -gt 0) {
Log "RESULT: $($bad.Count) REQUIRED item(s) still missing: $(($bad|ForEach-Object{$_.Item}) -join ', ')" 'ERROR'
Log "Log: $log"
exit 1
} else {
Log 'RESULT: all required payloads present (or healed).'
Log "Log: $log"
exit 0
}

View File

@@ -38,6 +38,12 @@ $ErrorActionPreference = 'Continue'
# logged; manifests tagged with a newer MINOR are fine. # logged; manifests tagged with a newer MINOR are fine.
# #
# Changelog: # Changelog:
# 2.6 - added _CmmVersion filter. Entry tagged _CmmVersion only applies when
# it equals C:\Enrollment\cmm\version.txt (the bay's resolved PC-DMIS
# version, written at imaging from cmm-bay-config.csv). Untagged entries
# always pass; missing/empty version file is a no-op (legacy install-all
# + non-CMM scopes unaffected). Lifted out of 09-Setup-CMM so the gate
# lives in one place both the imaging and enforce paths share.
# 2.5 - Type=EXE handler honors optional WaitTimeoutSec on the manifest # 2.5 - Type=EXE handler honors optional WaitTimeoutSec on the manifest
# entry. WiX Burn bootstrappers (UDC_Setup.exe) install the MSI # entry. WiX Burn bootstrappers (UDC_Setup.exe) install the MSI
# successfully but the wrapper process never exits (waits on a # successfully but the wrapper process never exits (waits on a
@@ -58,7 +64,7 @@ $ErrorActionPreference = 'Continue'
# 2.0 - initial Stage 2a: PS1/BAT/File/Registry/INF action types, # 2.0 - initial Stage 2a: PS1/BAT/File/Registry/INF action types,
# Always/MarkerFile/ValueMatches/pnputil detection, PCTypes filter # Always/MarkerFile/ValueMatches/pnputil detection, PCTypes filter
$LIB_MANIFEST_MAJOR = 2 $LIB_MANIFEST_MAJOR = 2
$LIB_MANIFEST_MINOR = 5 $LIB_MANIFEST_MINOR = 6
$logDir = Split-Path -Parent $LogFile $logDir = Split-Path -Parent $LogFile
if (-not (Test-Path $logDir)) { if (-not (Test-Path $logDir)) {
@@ -575,6 +581,42 @@ function Test-MachineNumberMatches {
return $false return $false
} }
# CMM PC-DMIS version filter. The bay's PC-DMIS version (2016/2019/2026) is
# resolved at imaging by resolve-cmm-bay-config.ps1 from cmm-bay-config.csv (the
# single bay -> version map) and persisted to C:\Enrollment\cmm\version.txt. An
# entry tagged _CmmVersion applies only when it equals that file; untagged
# entries (CLM, goCMM, Protect Viewer, DODA, the PDF converter) always pass.
# When the file is absent/empty - a bay imaged before the picker, or any
# non-CMM PC running a different scope - the filter is a no-op so every tagged
# entry passes. That preserves the legacy "install all versions" behavior for
# pre-picker bays and leaves non-CMM scopes untouched.
#
# This is the SINGLE place the version gate lives. Both the imaging path
# (09-Setup-CMM) and the runtime path (GE-Enforce) call this lib, so the gate
# cannot apply in one path and not the other. The 2016-installed-on-a-2019-bay
# bug was exactly that drift: the imaging path filtered by _CmmVersion but the
# enforce path did not, so enforce reinstalled every version it did not detect.
$script:_cachedCmmVersion = $null
$script:_cmmVersionRead = $false
function Get-CurrentCmmVersion {
if ($script:_cmmVersionRead) { return $script:_cachedCmmVersion }
$script:_cmmVersionRead = $true
$f = 'C:\Enrollment\cmm\version.txt'
if (Test-Path -LiteralPath $f) {
$v = (Get-Content -LiteralPath $f -First 1 -ErrorAction SilentlyContinue)
if ($v) { $script:_cachedCmmVersion = $v.Trim() }
}
return $script:_cachedCmmVersion
}
function Test-CmmVersionMatches {
param($App)
if (-not $App._CmmVersion) { return $true } # untagged entry always applies
$myVer = Get-CurrentCmmVersion
if (-not $myVer) { return $true } # no resolved version -> legacy install-all
return ([string]$App._CmmVersion -ieq $myVer)
}
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
# Main loop # Main loop
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
@@ -614,6 +656,13 @@ foreach ($app in $config.Applications) {
continue continue
} }
if (-not (Test-CmmVersionMatches -App $app)) {
$myVer = Get-CurrentCmmVersion
Write-InstallLog " _CmmVersion filter: entry targets $($app._CmmVersion) but bay version is $(if ($myVer) { $myVer } else { '(none)' }) - skipping"
$pcFiltered++
continue
}
if (Test-AppInstalled -App $app) { if (Test-AppInstalled -App $app) {
Write-InstallLog ' Already installed at expected version - skipping' Write-InstallLog ' Already installed at expected version - skipping'
$skipped++ $skipped++

View File

@@ -124,42 +124,13 @@ else {
if (Test-Path 'C:\Enrollment\pc-type.txt') { $pcType = (Get-Content 'C:\Enrollment\pc-type.txt' -First 1 -EA 0).Trim() } if (Test-Path 'C:\Enrollment\pc-type.txt') { $pcType = (Get-Content 'C:\Enrollment\pc-type.txt' -First 1 -EA 0).Trim() }
if (Test-Path 'C:\Enrollment\pc-subtype.txt') { $pcSubType = (Get-Content 'C:\Enrollment\pc-subtype.txt' -First 1 -EA 0).Trim() } if (Test-Path 'C:\Enrollment\pc-subtype.txt') { $pcSubType = (Get-Content 'C:\Enrollment\pc-subtype.txt' -First 1 -EA 0).Trim() }
# Read resolved PC-DMIS version from bay-config (written by # PC-DMIS version gating (drop entries whose _CmmVersion does not match the
# resolve-cmm-bay-config.ps1 via startnet.cmd). If missing, install all # bay's C:\Enrollment\cmm\version.txt) is now owned by the shared lib
# PC-DMIS versions (legacy behavior for bays imaged before the picker). # Install-FromManifest.ps1 (>= 2.6). Both this imaging path and the runtime
$cmmVersion = '' # GE-Enforce path call that lib, so the gate is applied identically in one
$cmmVersionFile = 'C:\Enrollment\cmm\version.txt' # place and the two paths cannot drift. We pass the full manifest unfiltered
if (Test-Path -LiteralPath $cmmVersionFile) { # and let the lib filter per entry. The bug this prevents: enforce lacked
$cmmVersion = (Get-Content -LiteralPath $cmmVersionFile -First 1 -EA 0).Trim() # the gate and reinstalled the wrong PC-DMIS version on an already-imaged bay.
}
Write-CMMLog "Resolved CMM version: $(if ($cmmVersion) { $cmmVersion } else { '(none - installing all)' })"
# Filter manifest: drop entries whose _CmmVersion doesn't match the
# resolved version. Entries without _CmmVersion always pass (CLM, goCMM,
# Protect Viewer, DODA). Write a temp filtered manifest for the lib.
if ($cmmVersion) {
try {
$cfg = Get-Content $stagingMani -Raw | ConvertFrom-Json
$filtered = @($cfg.Applications | Where-Object {
if (-not $_._CmmVersion) { return $true }
return ($_._CmmVersion -ieq $cmmVersion)
})
$skipped = @($cfg.Applications | Where-Object {
$_._CmmVersion -and ($_._CmmVersion -ine $cmmVersion)
})
foreach ($s in $skipped) {
Write-CMMLog " Skipping $($s.Name) (_CmmVersion=$($_._CmmVersion) != $cmmVersion)"
}
$cfg.Applications = $filtered
$filteredMani = Join-Path $stagingRoot 'cmm-manifest-filtered.json'
$cfg | ConvertTo-Json -Depth 10 | Set-Content -LiteralPath $filteredMani -Encoding UTF8
Write-CMMLog "Filtered manifest: $($filtered.Count) entries (from $($filtered.Count + $skipped.Count))"
$stagingMani = $filteredMani
} catch {
Write-CMMLog "Version filter failed: $_ - using unfiltered manifest" 'WARN'
}
}
Write-CMMLog "Running Install-FromManifest against $stagingRoot (PCType=$pcType, PCSubType=$pcSubType)" Write-CMMLog "Running Install-FromManifest against $stagingRoot (PCType=$pcType, PCSubType=$pcSubType)"
& $libSource -ManifestPath $stagingMani -InstallerRoot $stagingRoot -LogFile $logFile -PCType $pcType -PCSubType $pcSubType & $libSource -ManifestPath $stagingMani -InstallerRoot $stagingRoot -LogFile $logFile -PCType $pcType -PCSubType $pcSubType
$rc = $LASTEXITCODE $rc = $LASTEXITCODE

View File

@@ -38,6 +38,25 @@ param(
$DefaultRewrites = @( $DefaultRewrites = @(
@{ From = 'rd.ds.ge.com'; To = 'wjs.geaerospace.net' } # WJ legacy domain -> new domain @{ 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' $ErrorActionPreference = 'Continue'
$ts = Get-Date -Format 'yyyyMMdd-HHmmss' $ts = Get-Date -Format 'yyyyMMdd-HHmmss'
$logDir = 'C:\Logs\CMM' $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) --- # --- Grant BUILTIN\Users ReadKey+WriteKey on the reg key (goCMM opens it writable:true to read) ---
if (Test-Path $goCmmKey) { if (Test-Path $goCmmKey) {
try { 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'."

View File

@@ -0,0 +1,37 @@
@echo off
REM ==========================================================================
REM Debug-ShopDBReporting.bat - diagnose a PC not updating its ShopDB entry.
REM
REM Usage (run on the problem PC):
REM Debug-ShopDBReporting.bat full triage incl a live test POST
REM Debug-ShopDBReporting.bat /nopost inspect log + registry only (no POST)
REM Debug-ShopDBReporting.bat /mn 2001 force the machine number sent
REM
REM The live POST writes this PC's row to ShopDB (idempotent upsert, same as the
REM scheduled reporter). Use /nopost to avoid writing.
REM ==========================================================================
setlocal EnableDelayedExpansion
REM --- self-elevate ---
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=%~dp0Debug-ShopDBReporting.ps1"
set "ARGS="
:parse
if "%~1"=="" goto run
if /I "%~1"=="/nopost" set "ARGS=!ARGS! -NoPost" & shift & goto parse
if /I "%~1"=="/mn" set "ARGS=!ARGS! -MachineNo '%~2'" & shift & shift & goto parse
shift
goto parse
:run
powershell.exe -NoProfile -ExecutionPolicy Bypass -File "%PS%" !ARGS!
echo.
pause
endlocal

View File

@@ -0,0 +1,199 @@
<#
Debug-ShopDBReporting.ps1
One-shot diagnosis of why a shopfloor PC is not updating its ShopDB entry through
api.asp (action=updateCompleteAsset). Runs the whole triage and prints a verdict:
1. Preconditions - hostname, BIOS serial (api.asp requires it), eDNC MachineNo
(the PC->machine relationship needs it).
2. Client log - the latest C:\Logs\Shopfloor\report-asset-*.log POST/RESPONSE.
3. Live test POST - calls api.asp updateCompleteAsset and parses the JSON.
4. Verdict - likely cause + fix.
NOTE: the live POST WRITES to ShopDB (it is the real reporter action - an idempotent
upsert of this PC's row, same as the scheduled reporter does). Use -NoPost to skip
it and only inspect the local log + registry.
Run as administrator on the problem PC.
Params:
-ApiUrl override the api.asp endpoint
-MachineNo override the machine number sent (default: read from eDNC registry)
-NoPost do not send the live test POST
#>
param(
[string]$ApiUrl = 'https://tsgwp00525.wjs.geaerospace.net/shopdb/api.asp',
[string]$MachineNo,
[int]$TimeoutSec = 30,
[switch]$NoPost
)
$ErrorActionPreference = 'Continue'
function Section($t){ Write-Host ''; Write-Host ("==== {0} ====" -f $t) -ForegroundColor Cyan }
function KV($k,$v){ Write-Host (" {0,-20}: {1}" -f $k, $v) }
Write-Host '########################################################'
Write-Host '# ShopDB Reporting Debug'
Write-Host ("# {0}" -f (Get-Date -Format 'yyyy-MM-dd HH:mm:ss'))
Write-Host '########################################################'
# ---------------------------------------------------------------------------
# 1. Preconditions
# ---------------------------------------------------------------------------
Section '1. Preconditions'
$hostname = $env:COMPUTERNAME
$serial = ''
try { $serial = (Get-CimInstance Win32_BIOS -ErrorAction Stop).SerialNumber } catch {
try { $serial = (Get-WmiObject Win32_BIOS -ErrorAction Stop).SerialNumber } catch {}
}
$serial = ("" + $serial).Trim()
# Resolve machine number the same way the reporter / GE-Enforce lib do:
# eDNC registry (WOW6432Node then native) first, then C:\Enrollment\machine-number.txt.
$regMachineNo = ''
foreach ($rp in @('HKLM:\SOFTWARE\WOW6432Node\GE Aircraft Engines\DNC\General',
'HKLM:\SOFTWARE\GE Aircraft Engines\DNC\General')) {
if ($regMachineNo) { break }
if (Test-Path $rp) {
try { $regMachineNo = ("" + (Get-ItemProperty -Path $rp -Name MachineNo -ErrorAction Stop).MachineNo).Trim() } catch {}
}
}
$txtMachineNo = ''
$mnFile = 'C:\Enrollment\machine-number.txt'
if (Test-Path -LiteralPath $mnFile) {
try { $txtMachineNo = ("" + (Get-Content -LiteralPath $mnFile -First 1 -ErrorAction Stop)).Trim() } catch {}
}
$machineNoSource = ''
if (-not $MachineNo) {
if ($regMachineNo) { $MachineNo = $regMachineNo; $machineNoSource = 'eDNC registry' }
elseif ($txtMachineNo) { $MachineNo = $txtMachineNo; $machineNoSource = 'machine-number.txt (fallback)' }
} else { $machineNoSource = 'override param' }
$corpIp = ''
try {
$corpIp = (Get-NetIPAddress -AddressFamily IPv4 -ErrorAction Stop |
Where-Object { $_.IPAddress -notmatch '^(127\.|169\.254\.)' } |
Select-Object -First 1).IPAddress
} catch {}
KV 'Hostname' $hostname
KV 'BIOS serial' ($(if ($serial) { $serial } else { '(MISSING - api.asp will reject)' }))
KV 'eDNC MachineNo (reg)' ($(if ($regMachineNo) { $regMachineNo } else { '(none)' }))
KV 'machine-number.txt' ($(if ($txtMachineNo) { $txtMachineNo } else { '(none)' }))
KV 'MachineNo to send' ($(if ($MachineNo) { "$MachineNo [from $machineNoSource]" } else { '(none - relationship will be skipped)' }))
KV 'Corp IPv4' ($(if ($corpIp) { $corpIp } else { '(none found)' }))
KV 'API endpoint' $ApiUrl
# ---------------------------------------------------------------------------
# 2. Latest client reporter log
# ---------------------------------------------------------------------------
Section '2. Latest client reporter log'
$logDir = 'C:\Logs\Shopfloor'
$log = $null
if (Test-Path $logDir) {
$log = Get-ChildItem -Path $logDir -Filter 'report-asset-*.log' -ErrorAction SilentlyContinue |
Sort-Object LastWriteTime | Select-Object -Last 1
}
if ($log) {
KV 'Log file' $log.FullName
KV 'Last written' $log.LastWriteTime
Write-Host ' --- last POST / RESPONSE / ERROR lines ---'
Get-Content $log.FullName -ErrorAction SilentlyContinue |
Select-String -Pattern 'POST |RESPONSE |ERROR ' |
Select-Object -Last 6 | ForEach-Object { Write-Host (" " + $_.Line) }
} else {
Write-Host ' No report-asset-*.log found. The reporter may never have run on this PC.'
Write-Host ' -> check the scheduled task / GE-Enforce entry that invokes Report-AssetToShopDB.ps1'
}
# ---------------------------------------------------------------------------
# 3. Live test POST
# ---------------------------------------------------------------------------
Section '3. Live test POST to api.asp'
$resp = $null; $postErr = $null; $rawText = $null
if ($NoPost) {
Write-Host ' -NoPost set: skipping the live POST.'
} elseif (-not $serial) {
Write-Host ' SKIPPED: no BIOS serial, api.asp requires serialNumber. Fix the hardware/WMI read first.'
} else {
# Accept the internal IIS certificate for this run (PS 5.1-safe).
try {
if (-not ([System.Management.Automation.PSTypeName]'TrustAllCerts').Type) {
Add-Type @"
using System.Net; using System.Security.Cryptography.X509Certificates;
public class TrustAllCerts : ICertificatePolicy {
public bool CheckValidationResult(ServicePoint sp, X509Certificate c, WebRequest r, int p) { return true; }
}
"@
}
[System.Net.ServicePointManager]::CertificatePolicy = New-Object TrustAllCerts
[System.Net.ServicePointManager]::SecurityProtocol = [System.Net.SecurityProtocolType]::Tls12
} catch {}
$body = @{
action = 'updateCompleteAsset'
hostname = $hostname
serialNumber = $serial
pcType = 'Shopfloor'
}
if ($MachineNo) { $body['machineNo'] = $MachineNo }
Write-Host (" POST host={0} serial={1} machineNo={2}" -f $hostname, $serial, $(if ($MachineNo) { $MachineNo } else { '(none)' }))
try {
$resp = Invoke-RestMethod -Uri $ApiUrl -Method Post -Body $body -TimeoutSec $TimeoutSec -ErrorAction Stop
Write-Host ' --- response JSON ---'
Write-Host (" " + ($resp | ConvertTo-Json -Compress -Depth 5))
} catch {
$postErr = $_.Exception.Message
Write-Host (" POST FAILED: {0}" -f $postErr) -ForegroundColor Red
try { $rawText = $_.Exception.Response } catch {}
}
}
# ---------------------------------------------------------------------------
# 4. Verdict
# ---------------------------------------------------------------------------
Section '4. VERDICT'
function Truthy($v){ return ($v -eq $true -or "$v" -eq 'True' -or "$v" -eq '1') }
if ($NoPost) {
Write-Host ' (live POST skipped) Inspect section 1-2 above.'
if (-not $MachineNo) { Write-Host ' NOTE: no machine number (eDNC registry and machine-number.txt both empty) -> no relationship until set.' -ForegroundColor Yellow }
}
elseif ($postErr) {
Write-Host ' CANNOT REACH api.asp - network, TLS, DNS, or IIS error.' -ForegroundColor Red
Write-Host (" Detail: {0}" -f $postErr)
Write-Host ' -> ping/curl the host, confirm IIS is up, check the cert and the /shopdb/ path.'
}
elseif (-not $serial) {
Write-Host ' BIOS SERIAL MISSING - api.asp rejects the POST (serialNumber required).' -ForegroundColor Red
Write-Host ' -> fix WMI/Win32_BIOS on this PC; it is a hardware/OS read issue, not ShopDB.'
}
elseif (-not (Truthy $resp.success)) {
Write-Host ' SERVER RETURNED success=false - it aborted at a DB step.' -ForegroundColor Red
Write-Host (" Breadcrumb/message: {0}" -f $resp.message)
Write-Host ' -> the LAST "N-OK," token in that message is where it stopped. Match N to api.asp.'
}
elseif (-not (Truthy $resp.relationshipCreated)) {
if (-not $MachineNo) {
Write-Host ' ASSET OK, but NO RELATIONSHIP because this PC has no machine number.' -ForegroundColor Yellow
Write-Host (" machineid={0}. Neither the eDNC registry nor C:\Enrollment\machine-number.txt" -f $resp.machineid)
Write-Host ' has a value, so api.asp skips the relationship by design.'
Write-Host ' -> set the bay machine number (Set-MachineNumber writes the eDNC registry), then'
Write-Host ' re-run. CMM/keyence/wax-trace bays have no DNC number and register asset-only.'
} else {
Write-Host ' ASSET OK, but RELATIONSHIP NOT CREATED even though MachineNo was sent.' -ForegroundColor Red
Write-Host (" machineid={0}, machineNo={1}." -f $resp.machineid, $MachineNo)
Write-Host ' -> this is the server-side silent abort (LogToFile Err-leak). Deploy the api.asp fix'
Write-Host ' (commit a4051e3) to prod IIS, then re-run. Also confirm the IIS logs dir is'
Write-Host ' writable by the app-pool identity (that is the trigger).'
}
}
else {
Write-Host ' HEALTHY.' -ForegroundColor Green
Write-Host (" Asset updated (machineid={0}) and PC->machine relationship created." -f $resp.machineid)
Write-Host ' If ShopDB still looks wrong, the issue is data/display, not the reporter path.'
}
Write-Host ''
Write-Host 'Done.'

View File

@@ -83,16 +83,38 @@ if (-not $serialNumber) {
exit 0 exit 0
} }
# DNC machine number from eDNC reg (2001, 2002, ...). optional - sent only if present. # DNC machine number (2001, 2002, ...). optional - sent only if found.
# Resolution order mirrors the GE-Enforce lib Get-CurrentMachineNumber so the
# reporter and manifest gating agree:
# 1. eDNC registry (WOW6432Node, then native) - follows bay reassignment, which
# Set-MachineNumber rewrites here.
# 2. C:\Enrollment\machine-number.txt - the imaging-time value written once by
# startnet.cmd. Used when eDNC has not populated the registry yet (fresh
# image, or a bay where eDNC has not run), so the PC still reports its number
# and api.asp can build the relationship.
$machineNo = '' $machineNo = ''
$edncRegPath = 'HKLM:\SOFTWARE\WOW6432Node\GE Aircraft Engines\DNC\General' foreach ($regPath in @(
'HKLM:\SOFTWARE\WOW6432Node\GE Aircraft Engines\DNC\General',
'HKLM:\SOFTWARE\GE Aircraft Engines\DNC\General'
)) {
if ($machineNo) { break }
try { try {
if (Test-Path $edncRegPath) { if (Test-Path $regPath) {
$machineNo = [string](Get-ItemProperty -Path $edncRegPath -Name MachineNo -ErrorAction Stop).MachineNo $v = [string](Get-ItemProperty -Path $regPath -Name MachineNo -ErrorAction Stop).MachineNo
$machineNo = $machineNo.Trim() if ($v) { $machineNo = $v.Trim() }
} }
} catch { } catch {
Log "WARN could not read eDNC MachineNo: $($_.Exception.Message)" Log "WARN could not read MachineNo from ${regPath}: $($_.Exception.Message)"
}
}
if (-not $machineNo) {
$mnFile = 'C:\Enrollment\machine-number.txt'
if (Test-Path -LiteralPath $mnFile) {
try {
$v = Get-Content -LiteralPath $mnFile -First 1 -ErrorAction Stop
if ($v) { $machineNo = ([string]$v).Trim(); Log "machineNo from $mnFile (eDNC registry empty): $machineNo" }
} catch { Log "WARN could not read ${mnFile}: $($_.Exception.Message)" }
}
} }
# OS caption for the operatingsystems lookup. # OS caption for the operatingsystems lookup.