Tech catches a PC imaged with a wrong machine number. Previously the share restore (NTLARS .reg + UDC settings + UDC live data) only fired on the placeholder->real transition, so a real->real change rewrote only UDC JSON, eDNC reg, and MTConnect Devices.xml - leaving the wrong NTLARS config in place. Update-MachineNumber.ps1: replace the placeholder-only guard with an any-change guard so the share restore block fires on reassign too. The existing one-shot migrated/ consumption keeps live-data restore idempotent. Also writes C:\Enrollment\machine-number.txt to keep imaging-time scripts in sync. Set-MachineNumber.ps1 (both collections + nocollections): show a confirmation dialog when reassigning between two real numbers, naming old/new and listing what gets pulled. Audit each call to C:\Logs\Shopfloor\reassign.log. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
351 lines
18 KiB
PowerShell
351 lines
18 KiB
PowerShell
# Update-MachineNumber.ps1 - Shared helper for reading and updating the
|
|
# machine number in UDC and eDNC. Dot-source from any script that needs
|
|
# machine-number operations:
|
|
#
|
|
# . "$PSScriptRoot\lib\Update-MachineNumber.ps1" (from Shopfloor\ scripts)
|
|
# . "$PSScriptRoot\..\Shopfloor\lib\Update-MachineNumber.ps1" (from Standard\ scripts)
|
|
# . "$PSScriptRoot\Update-MachineNumber.ps1" (from lib\ scripts)
|
|
#
|
|
# Exported functions:
|
|
# Get-CurrentMachineNumber - returns @{ Udc = $string_or_null; Ednc = $string_or_null }
|
|
# Update-MachineNumber - updates both, returns @{ UdcUpdated = $bool; EdncUpdated = $bool; Errors = @() }
|
|
#
|
|
# Both handle missing files/keys gracefully (app not installed = skip, not error).
|
|
|
|
$script:UdcSettingsPath = 'C:\ProgramData\UDC\udc_settings.json'
|
|
$script:UdcExePath = 'C:\Program Files\UDC\UDC.exe'
|
|
$script:EdncRegPath = 'HKLM:\SOFTWARE\WOW6432Node\GE Aircraft Engines\DNC\General'
|
|
|
|
function Get-CurrentMachineNumber {
|
|
<#
|
|
.SYNOPSIS
|
|
Reads the current machine number from UDC settings JSON and eDNC registry.
|
|
.OUTPUTS
|
|
Hashtable with keys Udc ($string or $null) and Ednc ($string or $null).
|
|
#>
|
|
$result = @{ Udc = $null; Ednc = $null }
|
|
|
|
if (Test-Path $script:UdcSettingsPath) {
|
|
try {
|
|
$json = Get-Content $script:UdcSettingsPath -Raw | ConvertFrom-Json
|
|
$result.Udc = $json.GeneralSettings.MachineNumber
|
|
} catch {}
|
|
}
|
|
|
|
if (Test-Path $script:EdncRegPath) {
|
|
try {
|
|
$result.Ednc = (Get-ItemProperty -Path $script:EdncRegPath -Name MachineNo -ErrorAction Stop).MachineNo
|
|
} catch {}
|
|
}
|
|
|
|
return $result
|
|
}
|
|
|
|
function Update-MachineNumber {
|
|
<#
|
|
.SYNOPSIS
|
|
Updates UDC + eDNC machine number in one step. Stops UDC before writing,
|
|
relaunches after.
|
|
.PARAMETER NewNumber
|
|
The new machine number (digits only - caller validates).
|
|
.PARAMETER Site
|
|
Site name passed to UDC.exe -site argument. Defaults to 'West Jefferson'.
|
|
.OUTPUTS
|
|
Hashtable: @{ UdcUpdated = $bool; EdncUpdated = $bool; Errors = @() }
|
|
#>
|
|
param(
|
|
[Parameter(Mandatory)]
|
|
[string]$NewNumber,
|
|
|
|
[string]$Site = 'West Jefferson'
|
|
)
|
|
|
|
$out = @{ UdcUpdated = $false; EdncUpdated = $false; Errors = @(); RegImported = $null; OldUdc = $null; OldEdnc = $null }
|
|
|
|
# If the machine number is changing (placeholder->real OR real->real
|
|
# reassignment after a duplicate-image), pull per-machine state for the
|
|
# new number from the SFLD share: NTLARS .reg, UDC settings, UDC live
|
|
# data. The live-data restore is idempotent via one-shot migrated/
|
|
# consumption, so it stays safe on reassign too.
|
|
$current = Get-CurrentMachineNumber
|
|
$out.OldUdc = $current.Udc
|
|
$out.OldEdnc = $current.Ednc
|
|
$isChanging = ($current.Udc -ne $NewNumber) -or ($current.Ednc -ne $NewNumber)
|
|
|
|
if ($isChanging -and $NewNumber -ne '9999') {
|
|
$sharePath = $null
|
|
$siteCfgPath = 'C:\Enrollment\site-config.json'
|
|
if (Test-Path $siteCfgPath) {
|
|
try {
|
|
$cfg = Get-Content $siteCfgPath -Raw | ConvertFrom-Json
|
|
# Alias-aware lookup: prefer new key, fall back to legacy.
|
|
# PowerShell 5.1 has no null-coalesce operator.
|
|
$sharePath = $cfg.pcProfiles.'gea-shopfloor-collections'.ntlarsBackupSharePath
|
|
if (-not $sharePath) { $sharePath = $cfg.pcProfiles.'Standard-Machine'.ntlarsBackupSharePath }
|
|
} catch {}
|
|
}
|
|
|
|
if ($sharePath) {
|
|
try {
|
|
. (Join-Path $PSScriptRoot 'Restore-EDncReg.ps1')
|
|
$mounted = Mount-SFLDShare -SharePath $sharePath -DriveLetter 'V:'
|
|
if ($mounted) {
|
|
try {
|
|
$out.RegImported = Import-EDncRegBackup -SourceRoot 'V:\' -MachineNumber $NewNumber
|
|
} finally {
|
|
& net use V: /delete /y 2>$null | Out-Null
|
|
}
|
|
} else {
|
|
Write-Host " Update-MachineNumber: SFLD share unreachable - skipping restore."
|
|
}
|
|
} catch {
|
|
$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.'gea-shopfloor-collections'.udcSettingsSharePath
|
|
if (-not $udcSettingsSharePath) { $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.'gea-shopfloor-collections'.udcBackupSharePath
|
|
if (-not $udcSharePath) { $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) ---
|
|
Get-Process UDC -ErrorAction SilentlyContinue | ForEach-Object {
|
|
try { $_.Kill(); $_.WaitForExit(5000) | Out-Null } catch {}
|
|
}
|
|
Start-Sleep -Seconds 1
|
|
|
|
# --- Update UDC settings JSON ---
|
|
if (Test-Path $script:UdcSettingsPath) {
|
|
try {
|
|
$json = Get-Content $script:UdcSettingsPath -Raw | ConvertFrom-Json
|
|
$json.GeneralSettings.MachineNumber = $NewNumber
|
|
$json | ConvertTo-Json -Depth 99 | Set-Content -Path $script:UdcSettingsPath -Encoding UTF8
|
|
$out.UdcUpdated = $true
|
|
} catch {
|
|
$out.Errors += "UDC update failed: $_"
|
|
}
|
|
}
|
|
|
|
# --- Update eDNC registry ---
|
|
if (Test-Path $script:EdncRegPath) {
|
|
try {
|
|
Set-ItemProperty -Path $script:EdncRegPath -Name MachineNo -Value $NewNumber -Type String -Force
|
|
$out.EdncUpdated = $true
|
|
} catch {
|
|
$out.Errors += "eDNC update failed: $_"
|
|
}
|
|
}
|
|
|
|
# --- Relaunch UDC with new args ---
|
|
if ((Test-Path $script:UdcExePath) -and $out.UdcUpdated) {
|
|
try {
|
|
# UDC.exe arg signature: quoted site name (with space), then
|
|
# dash-prefixed machine number. Example: UDC.exe "West Jefferson" -7605
|
|
Start-Process -FilePath $script:UdcExePath -ArgumentList @("`"$Site`"", "-$NewNumber")
|
|
} catch {
|
|
$out.Errors += "UDC relaunch failed: $_"
|
|
}
|
|
}
|
|
|
|
# --- 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): $_"
|
|
}
|
|
}
|
|
|
|
# Keep C:\Enrollment\machine-number.txt in sync. Post-imaging GE-Enforce
|
|
# prefers eDNC reg, but imaging-time scripts (Install-FromManifest
|
|
# TargetMachineNumbers filter, 01-eDNC.ps1, 03-RestoreEDncConfig.ps1)
|
|
# still read this file. Avoid drift on reassign.
|
|
try {
|
|
$mnFile = 'C:\Enrollment\machine-number.txt'
|
|
$mnDir = Split-Path -Parent $mnFile
|
|
if (-not (Test-Path $mnDir)) { New-Item -ItemType Directory -Path $mnDir -Force | Out-Null }
|
|
Set-Content -Path $mnFile -Value $NewNumber -Encoding UTF8 -NoNewline
|
|
$out.MachineNumberTxtUpdated = $true
|
|
} catch {
|
|
$out.Errors += "machine-number.txt update failed: $_"
|
|
}
|
|
|
|
return $out
|
|
}
|