GE-Enforce mounts the SFLD share on W: and holds it for the whole enforce cycle, passing W:\<pctype> as -InstallerRoot to Install-FromManifest. Two scripts that run inside that cycle, as SYSTEM in the same drive namespace, mapped W: for their own use and deleted it on exit: Restore-UDCData.ps1 manifest PS1 entry, mounts the UDC backup share Update-MachineNumber.ps1 "Apply Machine Number" task, same backup share Once W: is gone, the next manifest entry's Join-Path resolves against a dead drive qualifier. In Windows PowerShell 5.1 that emits nothing rather than throwing, so the null lands in Test-Path -LiteralPath and the entry dies with "Cannot bind argument to parameter 'LiteralPath' because it is null". Observed on a collections bay: the controller-credential entry, which runs immediately after UDC Data Restore, failed this way while the entry one line earlier had resolved a W: path successfully one second before. Restore-UDCData now uses R: and Update-MachineNumber uses N: for the UDC backup share. Neither letter is referenced anywhere else in the tree; V: was not an option because Restore-EDncReg and Update-MachineNumber's NTLARS restore already use it. The re-mount at GE-Enforce.ps1:239-244 does not cover this. It runs after the manifest loop and blames idle SMB timeout, but the whole cycle took one second, so timeout was never the cause.
365 lines
19 KiB
PowerShell
365 lines
19 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 {
|
|
# N:, never W:. GE-Enforce owns W: for its whole cycle, and this
|
|
# runs as SYSTEM in the same drive namespace - taking W: here kills
|
|
# the share out from under an in-flight enforce cycle.
|
|
$mountedUdc = Mount-SFLDShare -SharePath $udcSharePath -DriveLetter 'N:'
|
|
if ($mountedUdc) {
|
|
try {
|
|
$bayDir = Join-Path 'N:\' $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 N: /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 <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
|
|
}
|