Update-MachineNumber: pull per-bay udc_settings.json from SFLD on placeholder->real

When the tech transitions a 9999-placeholder PC to its real machine number,
also restore the per-bay udc_settings_<num>.json from
\\tsgwp00525\shared\spc\udc\settings_backups\. PXE-time preinstall can't reach
this share (no SFLD creds yet), so 00-PreInstall uses the local C:\Enrollment
mirror; post-config the share is reachable, so the renumber path goes direct
to the canonical source.

Adds udcSettingsSharePath to site-config.json under Standard-Machine.

Bundles in prior uncommitted work in the same file: ntlars reg restore,
UDC data restore (CurrentData.json + ArchivedData/), MTConnect Devices.xml
inline rewrite + service restart, and one-shot consume of per-bay UDC
backup -> migrated/<timestamp>/.
This commit is contained in:
cproudlock
2026-04-30 12:34:53 -04:00
parent 6e9053b83c
commit 75b85bfde6
2 changed files with 192 additions and 7 deletions

View File

@@ -96,6 +96,133 @@ function Update-MachineNumber {
$out.Errors += "ntlars restore failed: $_" $out.Errors += "ntlars restore failed: $_"
} }
} }
# --- UDC settings JSON restore: pull udc_settings_<NewNumber>.json
# from the SFLD UDC settings_backups share. At imaging time
# 00-PreInstall-MachineApps.ps1 pulls this from the local
# C:\Enrollment mirror, but a 9999-placeholder PC has no real
# pre-stage. Once the tech sets the real number we have SFLD
# creds, so go direct to the canonical share. ---
$udcSettingsSharePath = $null
if ($cfg) {
try { $udcSettingsSharePath = $cfg.pcProfiles.'Standard-Machine'.udcSettingsSharePath } catch {}
}
if ($udcSettingsSharePath) {
try {
$mountedUdcSet = Mount-SFLDShare -SharePath $udcSettingsSharePath -DriveLetter 'X:'
if ($mountedUdcSet) {
try {
$srcSettings = Join-Path 'X:\' "udc_settings_$NewNumber.json"
if (Test-Path -LiteralPath $srcSettings) {
Get-Process UDC -ErrorAction SilentlyContinue | ForEach-Object {
try { $_.Kill(); $_.WaitForExit(5000) | Out-Null } catch {}
}
Start-Sleep -Milliseconds 500
$localUdcDir = 'C:\ProgramData\UDC'
if (-not (Test-Path $localUdcDir)) { New-Item -ItemType Directory -Path $localUdcDir -Force | Out-Null }
Copy-Item -LiteralPath $srcSettings -Destination $script:UdcSettingsPath -Force -ErrorAction Stop
$out.UdcSettingsRestored = $true
Write-Host " Update-MachineNumber: UDC settings restored from $srcSettings"
} else {
Write-Host " Update-MachineNumber: no udc_settings_$NewNumber.json on settings_backups share"
}
} finally {
& net use X: /delete /y 2>$null | Out-Null
}
} else {
Write-Host " Update-MachineNumber: UDC settings_backups share unreachable - skipping settings restore."
}
} catch {
$out.Errors += "UDC settings restore failed: $_"
}
}
# --- UDC data restore: pull CurrentData.json + ArchivedData/ from
# the per-bay backup at <udcBackupSharePath>\<NewNumber>\.
# One-shot: after successful restore, the live backup at the root
# is moved into <NewNumber>\migrated\<timestamp>\ so it can't be
# replayed on subsequent reboots or reused for a future PC at the
# same bay. The 'isPlaceholder' guard above ensures this whole
# block only ever fires once per PC's lifetime (placeholder->real
# transition). ---
$udcSharePath = $null
if ($cfg) {
try { $udcSharePath = $cfg.pcProfiles.'Standard-Machine'.udcBackupSharePath } catch {}
}
if ($udcSharePath) {
try {
$mountedUdc = Mount-SFLDShare -SharePath $udcSharePath -DriveLetter 'W:'
if ($mountedUdc) {
try {
$bayDir = Join-Path 'W:\' $NewNumber
$srcCur = Join-Path $bayDir 'CurrentData.json'
$srcArc = Join-Path $bayDir 'ArchivedData'
if (Test-Path -LiteralPath $srcCur) {
Write-Host " Update-MachineNumber: UDC backup found at $bayDir - restoring."
# Stop UDC pre-emptively so CurrentData.json isn't locked
Get-Process UDC -ErrorAction SilentlyContinue | ForEach-Object {
try { $_.Kill(); $_.WaitForExit(5000) | Out-Null } catch {}
}
Start-Sleep -Milliseconds 500
$localUdcDir = 'C:\ProgramData\UDC'
if (-not (Test-Path $localUdcDir)) { New-Item -ItemType Directory -Path $localUdcDir -Force | Out-Null }
$localArc = Join-Path $localUdcDir 'ArchivedData'
# Copy
Copy-Item -LiteralPath $srcCur -Destination (Join-Path $localUdcDir 'CurrentData.json') -Force -ErrorAction Stop
if (Test-Path -LiteralPath $srcArc) {
if (Test-Path -LiteralPath $localArc) { Remove-Item -LiteralPath $localArc -Recurse -Force -ErrorAction SilentlyContinue }
Copy-Item -LiteralPath $srcArc -Destination $localArc -Recurse -Force -ErrorAction Stop
}
# Move live backup -> migrated/<timestamp>/ (one-shot consumption)
$stamp = (Get-Date -Format 'yyyy-MM-ddTHH-mm-ssZ')
$migDir = Join-Path $bayDir 'migrated'
$migStamp = Join-Path $migDir $stamp
if (-not (Test-Path -LiteralPath $migDir)) { New-Item -ItemType Directory -Path $migDir -Force | Out-Null }
if (-not (Test-Path -LiteralPath $migStamp)) { New-Item -ItemType Directory -Path $migStamp -Force | Out-Null }
Move-Item -LiteralPath $srcCur -Destination (Join-Path $migStamp 'CurrentData.json') -Force -ErrorAction Stop
if (Test-Path -LiteralPath $srcArc) {
Move-Item -LiteralPath $srcArc -Destination (Join-Path $migStamp 'ArchivedData') -Force -ErrorAction Stop
}
$bakManifest = Join-Path $bayDir 'backup.manifest.json'
if (Test-Path -LiteralPath $bakManifest) {
Move-Item -LiteralPath $bakManifest -Destination (Join-Path $migStamp 'backup.manifest.json') -Force -ErrorAction SilentlyContinue
}
# Audit manifest in migrated/<stamp>/
$localArcInfo = if (Test-Path $localArc) { Get-ChildItem $localArc -Recurse -File -ErrorAction SilentlyContinue } else { @() }
$restoreManifest = [ordered]@{
RestoredAt = (Get-Date -Format 'o')
DestinationHostname = $env:COMPUTERNAME
DestinationUser = [System.Security.Principal.WindowsIdentity]::GetCurrent().Name
MachineNumber = $NewNumber
CurrentDataBytes = (Get-Item (Join-Path $localUdcDir 'CurrentData.json')).Length
ArchivedDataFiles = $localArcInfo.Count
ArchivedDataBytes = ($localArcInfo | Measure-Object Length -Sum).Sum
RestoredVia = 'Update-MachineNumber.ps1'
}
$restoreManifest | ConvertTo-Json | Set-Content -Path (Join-Path $migStamp 'restore.manifest.json') -Encoding UTF8
$out.UdcRestored = $true
Write-Host " Update-MachineNumber: UDC restore OK (consumed -> migrated\$stamp\)"
} else {
Write-Host " Update-MachineNumber: no UDC backup at $bayDir (fresh PC, no prior data)"
}
} finally {
& net use W: /delete /y 2>$null | Out-Null
}
} else {
Write-Host " Update-MachineNumber: UDC backup share unreachable - skipping UDC restore."
}
} catch {
$out.Errors += "UDC restore failed: $_"
}
}
} }
# --- Stop UDC before editing its JSON (avoid stale shutdown write) --- # --- Stop UDC before editing its JSON (avoid stale shutdown write) ---
@@ -137,5 +264,61 @@ function Update-MachineNumber {
} }
} }
# --- Update MTConnect Devices.xml (per-variant) + restart agent services ---
# MTConnect deploys via the GE-Enforce manifest engine + the
# Install_MTConnect_ExisDir_BatConvert.ps1 wrapper. Devices.xml on disk
# has the machine number embedded in the <Device name= uuid= id=> attrs.
# When the tech changes 9999 -> real number, those attrs need to follow,
# OR MTConnect data will keep reporting under the old/wrong identity.
#
# We do an inline substitution rather than re-running the full wrapper
# because the tech may not have credential delegation to the SFLD share
# from their interactive session.
$out.MTConnectUpdated = @()
# Drive this off "is the service installed?" rather than file existence:
# Fanuc and Okuma both deploy to C:\MTConnect\Agent\, distinguishable only
# by the registered service name (NTFS is case-insensitive so the two
# devices.xml / Devices.xml entries collapse to the same file). Without
# this filter, the Okuma branch on an Okuma PC sees the file already
# rewritten by the (no-op) Fanuc branch and skips the service restart.
$mtcVariants = @(
@{ Service='MTConnect Agent Fanuc'; Path='C:\MTConnect\Agent\devices.xml' },
@{ Service='MTConnect Agent Okuma'; Path='C:\MTConnect\Agent\Devices.xml' },
@{ Service='MTConnect eDNC Agent'; Path='C:\MTConnect_eDNC\Agent\Devices.xml' },
@{ Service='Makino MTConnect Agent'; Path='C:\Makino-MTConnect\Agent\Devices.xml' }
)
foreach ($v in $mtcVariants) {
$svc = Get-Service -Name $v.Service -ErrorAction SilentlyContinue
if (-not $svc) { continue }
if (-not (Test-Path -LiteralPath $v.Path)) { continue }
try {
$content = Get-Content -LiteralPath $v.Path -Raw -ErrorAction Stop
# Find the Device root's name= attr to discover the OLD identifier
if ($content -notmatch '<Device[^>]+name="([^"]+)"') { continue }
$oldName = $matches[1]
# Most variants encode machine number as the trailing digit run:
# Fanuc/Makino: name="9999" -> trailing 9999
# OKUMA: name="loc9999" -> trailing 9999, prefix 'loc'
# eDNC: name="eDNC_OKUMA9999" -> trailing 9999, prefix 'eDNC_OKUMA'
if ($oldName -notmatch '^(.*?)(\d+)$') { continue }
$prefix = $matches[1]
$oldDigits = $matches[2]
if ($oldDigits -eq $NewNumber) { continue } # already correct
$oldFull = "$prefix$oldDigits"
$newFull = "$prefix$NewNumber"
$oldEsc = [regex]::Escape($oldFull)
# Quoted attr value: name="X" / uuid="X" / id="X"
$content = $content -replace ('"' + $oldEsc + '"'), ('"' + $newFull + '"')
# OKUMA-style with serial after dot: uuid="locX.<serial>"
$content = $content -replace ('"' + $oldEsc + '\.'), ('"' + $newFull + '.')
Set-Content -LiteralPath $v.Path -Value $content -NoNewline -ErrorAction Stop
$out.MTConnectUpdated += "$($v.Path) ($oldFull -> $newFull)"
try { Restart-Service -Name $v.Service -Force -ErrorAction Stop }
catch { $out.Errors += "Restart $($v.Service) failed: $_" }
} catch {
$out.Errors += "MTConnect Devices.xml update failed for $($v.Path): $_"
}
}
return $out return $out
} }

View File

@@ -25,7 +25,7 @@
}, },
"common": { "common": {
"_comment": "Cross-PC-type share paths used by logon enforcers (Acrobat-Enforce, future analogues). One SFLD share path per app; enforcer mounts the share with SFLD creds from HKLM:\\SOFTWARE\\GE\\SFLD\\Credentials and applies acrobat-manifest.json etc.", "_comment": "Cross-PC-type share paths used by GE-Enforce. The dispatcher mounts the share with SFLD creds from HKLM:\\SOFTWARE\\GE\\SFLD\\Credentials and applies common/manifest.json (and per-pctype manifests below) on every user logon.",
"commonAppsSharePath": "\\\\tsgwp00525.wjs.geaerospace.net\\shared\\dt\\shopfloor\\common\\apps" "commonAppsSharePath": "\\\\tsgwp00525.wjs.geaerospace.net\\shared\\dt\\shopfloor\\common\\apps"
}, },
@@ -78,13 +78,15 @@
}, },
"Standard-Machine": { "Standard-Machine": {
"machineappsSharePath": "\\\\tsgwp00525.wjs.geaerospace.net\\shared\\dt\\shopfloor\\main\\machineapps", "machineappsSharePath": "\\\\tsgwp00525.wjs.geaerospace.net\\shared\\dt\\shopfloor\\main\\machineapps",
"ntlarsBackupSharePath": "\\\\tsgwp00525.wjs.geaerospace.net\\shared\\dt\\shopfloor\\main\\ntlars-backups", "ntlarsBackupSharePath": "\\\\tsgwp00525.wjs.geaerospace.net\\shared\\dt\\shopfloor\\main\\ntlars-backups",
"udcBackupSharePath": "\\\\tsgwp00525.wjs.geaerospace.net\\shared\\dt\\shopfloor\\backup\\udc",
"udcSettingsSharePath": "\\\\tsgwp00525.wjs.geaerospace.net\\shared\\spc\\udc\\settings_backups",
"_startupItems_comment": "UDC removed 2026-04-28 - the UDC vendor installer registers UDC.exe in HKLM\\Software\\Microsoft\\Windows\\CurrentVersion\\Run, which Task Manager surfaces as a Startup item. Our duplicate Startup\\UDC.lnk created a race-condition single-instance conflict (Run key fires from Explorer init, our .lnk fires slightly later, the second launch silently exits) that left UDC running in a not-fully-attached session and invisible to the ShopFloor user. Vendor autostart is the canonical autostart - we no longer add our own.",
"startupItems": [ "startupItems": [
{ "label": "WJ Shopfloor", "type": "existing", "sourceLnk": "WJ Shopfloor.lnk" }, { "label": "WJ Shopfloor", "type": "existing", "sourceLnk": "WJ Shopfloor.lnk" },
{ "label": "Plant Apps", "type": "url", "urlKey": "plantApps" }, { "label": "Plant Apps", "type": "url", "urlKey": "plantApps" },
{ "label": "eDNC", "type": "exe", "target": "C:\\Program Files (x86)\\Dnc\\bin\\DncMain.exe" }, { "label": "eDNC", "type": "exe", "target": "C:\\Program Files (x86)\\Dnc\\bin\\DncMain.exe" }
{ "label": "UDC", "type": "exe", "target": "C:\\Program Files\\UDC\\UDC.exe" }
], ],
"taskbarPins": [ "taskbarPins": [
{ "name": "Microsoft Edge", "lnkPath": "%ALLUSERSPROFILE%\\Microsoft\\Windows\\Start Menu\\Programs\\Microsoft Edge.lnk" }, { "name": "Microsoft Edge", "lnkPath": "%ALLUSERSPROFILE%\\Microsoft\\Windows\\Start Menu\\Programs\\Microsoft Edge.lnk" },
@@ -104,7 +106,7 @@
}, },
"CMM": { "CMM": {
"_comment": "Hexagon CMM apps (CLM 1.8, goCMM, PC-DMIS 2016, PC-DMIS 2019 R2). At imaging time they install from a WinPE-staged local bootstrap at C:\\CMM-Install (put there by startnet.cmd when pc-type=CMM, source is the PXE server enrollment share). Post-imaging, the 'GE CMM Enforce' scheduled task runs CMM-Enforce.ps1 on user logon and enforces versions against the tsgwp00525 share below (the SFLD creds Azure DSC provisions unlock the mount). cmmSharePath is the ongoing-enforcement source, not the imaging-time source.", "_comment": "Hexagon CMM apps (CLM 1.8, goCMM, PC-DMIS 2016, PC-DMIS 2019 R2). At imaging time they install from a WinPE-staged local bootstrap at C:\\CMM-Install (put there by startnet.cmd when pc-type=CMM, source is the PXE server enrollment share). Post-imaging, the unified GE-Enforce dispatcher reads cmm/manifest.json on the tsgwp00525 share below and enforces versions on every user logon (the SFLD creds Azure DSC provisions unlock the mount). cmmSharePath is the ongoing-enforcement source, not the imaging-time source.",
"cmmSharePath": "\\\\tsgwp00525.wjs.geaerospace.net\\shared\\dt\\shopfloor\\cmm\\machineapps", "cmmSharePath": "\\\\tsgwp00525.wjs.geaerospace.net\\shared\\dt\\shopfloor\\cmm\\machineapps",
"startupItems": [], "startupItems": [],
"taskbarPins": [ "taskbarPins": [
@@ -153,7 +155,7 @@
}, },
"Keyence": { "Keyence": {
"_comment": "Keyence VR-6000 microscope/profilometer PCs. At imaging time, 09-Setup-Keyence.ps1 installs VR-6000 Series Software MSI + KEYENCE VR USB driver from the WinPE-staged shopfloor-setup\\Keyence\\ bundle. Post-imaging, the 'GE Keyence Enforce' scheduled task runs Keyence-Enforce.ps1 on user logon and enforces versions against the tsgwp00525 share below (SFLD creds provisioned by Azure DSC unlock the mount). keyenceSharePath is the ongoing-enforcement source; bump the manifest + MSI on the share to push updates fleet-wide.", "_comment": "Keyence VR-6000 microscope/profilometer PCs. At imaging time, 09-Setup-Keyence.ps1 installs VR-6000 Series Software MSI + KEYENCE VR USB driver from the WinPE-staged shopfloor-setup\\Keyence\\ bundle. Post-imaging, the unified GE-Enforce dispatcher reads keyence/manifest.json on the tsgwp00525 share below and enforces versions on every user logon (SFLD creds provisioned by Azure DSC unlock the mount). keyenceSharePath is the ongoing-enforcement source; bump the manifest + MSI on the share to push updates fleet-wide.",
"keyenceSharePath": "\\\\tsgwp00525.wjs.geaerospace.net\\shared\\dt\\shopfloor\\keyence\\machineapps", "keyenceSharePath": "\\\\tsgwp00525.wjs.geaerospace.net\\shared\\dt\\shopfloor\\keyence\\machineapps",
"startupItems": [ "startupItems": [
{ "label": "WJ Shopfloor", "type": "existing", "sourceLnk": "WJ Shopfloor.lnk" } { "label": "WJ Shopfloor", "type": "existing", "sourceLnk": "WJ Shopfloor.lnk" }