# 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_.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 \\. # One-shot: after successful restore, the live backup at the root # is moved into \migrated\\ 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// (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// $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 --- # -ErrorAction Stop on the WRITE so PermissionDenied / IO errors become # terminating and actually hit the catch block. Without this, the cmdlet # writes a non-terminating error (visible in transcript) but flow # continues + $out.UdcUpdated is set to $true, leading the dialog to # report "UDC updated" when the file write actually failed. if (Test-Path $script:UdcSettingsPath) { try { $json = Get-Content $script:UdcSettingsPath -Raw -ErrorAction Stop | ConvertFrom-Json $json.GeneralSettings.MachineNumber = $NewNumber $json | ConvertTo-Json -Depth 99 | Set-Content -Path $script:UdcSettingsPath -Encoding UTF8 -ErrorAction Stop $out.UdcUpdated = $true } catch { $out.Errors += "UDC update failed: $_" } } # --- Update eDNC registry --- # Same -ErrorAction Stop reasoning as above. Set-ItemProperty's # PermissionDenied is non-terminating by default; without -ErrorAction # Stop, the catch block never fires and $out.EdncUpdated=$true gets set # despite the write failing. This is the bug that made the 13:35:39 # tech run on FGY07FZ3 report "eDNC updated to 3005 / All updates # succeeded" while the actual reg value stayed at 9999. if (Test-Path $script:EdncRegPath) { try { Set-ItemProperty -Path $script:EdncRegPath -Name MachineNo -Value $NewNumber -Type String -Force -ErrorAction Stop $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 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 ']+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." $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 }