diff --git a/playbook/shopfloor-setup/Shopfloor/lib/Update-MachineNumber.ps1 b/playbook/shopfloor-setup/Shopfloor/lib/Update-MachineNumber.ps1 index d361432..52ae8a1 100644 --- a/playbook/shopfloor-setup/Shopfloor/lib/Update-MachineNumber.ps1 +++ b/playbook/shopfloor-setup/Shopfloor/lib/Update-MachineNumber.ps1 @@ -96,6 +96,133 @@ function Update-MachineNumber { $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.'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.'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) --- @@ -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 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): $_" + } + } + return $out } diff --git a/playbook/shopfloor-setup/site-config.json b/playbook/shopfloor-setup/site-config.json index e011e81..9f90f6c 100644 --- a/playbook/shopfloor-setup/site-config.json +++ b/playbook/shopfloor-setup/site-config.json @@ -25,7 +25,7 @@ }, "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" }, @@ -78,13 +78,15 @@ }, "Standard-Machine": { - "machineappsSharePath": "\\\\tsgwp00525.wjs.geaerospace.net\\shared\\dt\\shopfloor\\main\\machineapps", - "ntlarsBackupSharePath": "\\\\tsgwp00525.wjs.geaerospace.net\\shared\\dt\\shopfloor\\main\\ntlars-backups", + "machineappsSharePath": "\\\\tsgwp00525.wjs.geaerospace.net\\shared\\dt\\shopfloor\\main\\machineapps", + "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": [ { "label": "WJ Shopfloor", "type": "existing", "sourceLnk": "WJ Shopfloor.lnk" }, { "label": "Plant Apps", "type": "url", "urlKey": "plantApps" }, - { "label": "eDNC", "type": "exe", "target": "C:\\Program Files (x86)\\Dnc\\bin\\DncMain.exe" }, - { "label": "UDC", "type": "exe", "target": "C:\\Program Files\\UDC\\UDC.exe" } + { "label": "eDNC", "type": "exe", "target": "C:\\Program Files (x86)\\Dnc\\bin\\DncMain.exe" } ], "taskbarPins": [ { "name": "Microsoft Edge", "lnkPath": "%ALLUSERSPROFILE%\\Microsoft\\Windows\\Start Menu\\Programs\\Microsoft Edge.lnk" }, @@ -104,7 +106,7 @@ }, "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", "startupItems": [], "taskbarPins": [ @@ -153,7 +155,7 @@ }, "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", "startupItems": [ { "label": "WJ Shopfloor", "type": "existing", "sourceLnk": "WJ Shopfloor.lnk" }