# Restore-UDCData.ps1 - Idempotent UDC data restore for the manifest engine. # # Triggered by the GE Shopfloor Enforce scheduled task (runs as SYSTEM, every # user logon + every 5 min). Standard-machine manifest entry uses # DetectionMethod=Always so this fires every cycle; the script self-decides # whether there's actually any work to do. # # CONTRACT: # - 99% of cycles: no backup waiting -> exit 0 in ~1 second, no side effects # - 1 cycle (the one after Backup-UDCData lands a backup for this PC's bay): # stop UDC, copy CurrentData.json + ArchivedData/ to C:\ProgramData\UDC, # move consumed backup to \migrated\\, write # restore.manifest.json, restart UDC. After this, root is empty so the # check returns "no backup waiting" again on subsequent cycles. # # DESIGNED FOR THE SWAP WORKFLOW: # New PC gets pre-imaged with real machine number + locked down, sits in # storage. Days/weeks later, tech runs Backup-UDCData on old PC -> backup # lands on share. Tech swaps PCs. New PC powers on at the bay -> ShopFloor # autologon -> manifest engine fires this script -> backup detected -> # restored -> UDC opens with prior history intact. # # Replaces the placeholder->real trigger in Update-MachineNumber.ps1 for the # pre-imaged-then-swapped case (where the trigger fired at imaging time, before # the backup existed). Update-MachineNumber.ps1's branch still handles the # secondary case (tech used 9999 placeholder + sets number at bay) - both # triggers safely no-op if the other already consumed the backup. [CmdletBinding()] param( [string]$BackupShareRoot = '\\tsgwp00525.wjs.geaerospace.net\shared\dt\shopfloor\backup\udc', [string]$UdcDataDir = 'C:\ProgramData\UDC', [string]$UdcExePath = 'C:\Program Files\UDC\UDC.exe', [string]$UdcSettingsPath = 'C:\ProgramData\UDC\udc_settings.json', [string]$Site = 'West Jefferson' ) $ErrorActionPreference = 'Stop' $logDir = 'C:\Logs\UDC' if (-not (Test-Path $logDir)) { try { New-Item -Path $logDir -ItemType Directory -Force | Out-Null } catch { $logDir = $env:TEMP } } $logFile = Join-Path $logDir 'Restore-UDCData.log' function Log { param([string]$Msg, [string]$Level = 'INFO') $ts = Get-Date -Format 'yyyy-MM-dd HH:mm:ss' $line = "[$ts][$Level] $Msg" Add-Content -LiteralPath $logFile -Value $line Write-Host $line } # --- Resolve local machine number --- if (-not (Test-Path -LiteralPath $UdcSettingsPath)) { # No UDC installed yet. Manifest engine will catch up later if/when it lands. exit 0 } try { $json = Get-Content -LiteralPath $UdcSettingsPath -Raw | ConvertFrom-Json $mn = $json.GeneralSettings.MachineNumber } catch { Log "Failed to parse $UdcSettingsPath : $_" 'ERROR' exit 0 } if (-not $mn -or $mn -eq '9999' -or $mn -notmatch '^\d+$') { # Placeholder or invalid - the placeholder->real trigger in # Update-MachineNumber.ps1 will catch it when the tech sets a real number. exit 0 } # --- Probe for a waiting backup --- $bayDir = Join-Path $BackupShareRoot $mn $srcCur = Join-Path $bayDir 'CurrentData.json' $srcArc = Join-Path $bayDir 'ArchivedData' if (-not (Test-Path -LiteralPath $srcCur)) { # Most-common path: no backup waiting. Exit silently to keep enforce-cycle # logs clean. (Manifest engine still records that the entry ran.) exit 0 } # We have a backup. From here on, log everything. Log "===============================================" Log "UDC data backup detected at $bayDir - restoring." Log "Hostname: $env:COMPUTERNAME" Log "Machine number: $mn" # --- Stop UDC.exe so CurrentData.json isn't locked --- Get-Process UDC -ErrorAction SilentlyContinue | ForEach-Object { try { $_.Kill() $_.WaitForExit(5000) | Out-Null Log "Stopped existing UDC.exe (PID $($_.Id))" } catch { Log "Could not stop UDC.exe (PID $($_.Id)): $_" 'WARN' } } Start-Sleep -Milliseconds 500 # --- Ensure local UDC data dir exists --- if (-not (Test-Path -LiteralPath $UdcDataDir)) { New-Item -ItemType Directory -Path $UdcDataDir -Force | Out-Null } $localCur = Join-Path $UdcDataDir 'CurrentData.json' $localArc = Join-Path $UdcDataDir 'ArchivedData' # --- Copy CurrentData.json --- $copiedCur = $false try { Copy-Item -LiteralPath $srcCur -Destination $localCur -Force -ErrorAction Stop $copiedCur = $true Log "Copied CurrentData.json ($((Get-Item $localCur).Length) bytes)" } catch { Log "Copy CurrentData.json failed: $_" 'ERROR' } # --- Copy ArchivedData/ --- $copiedArc = $false $arcFiles = 0 $arcBytes = 0 if (Test-Path -LiteralPath $srcArc) { try { if (Test-Path -LiteralPath $localArc) { Remove-Item -LiteralPath $localArc -Recurse -Force -ErrorAction SilentlyContinue } Copy-Item -LiteralPath $srcArc -Destination $localArc -Recurse -Force -ErrorAction Stop $arcItems = Get-ChildItem -LiteralPath $localArc -Recurse -File -ErrorAction SilentlyContinue $arcFiles = $arcItems.Count $arcBytes = ($arcItems | Measure-Object Length -Sum).Sum $copiedArc = $true Log "Copied ArchivedData/ ($arcFiles files, $arcBytes bytes)" } catch { Log "Copy ArchivedData/ failed: $_" 'ERROR' } } # --- One-shot consumption: only move backup -> migrated/ if BOTH copies succeeded --- # (If one failed, leave the live backup in place so the next cycle can retry.) $consumeOk = ($copiedCur -and ($copiedArc -or -not (Test-Path -LiteralPath $srcArc))) if ($consumeOk) { try { $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 } $restoreManifest = [ordered]@{ RestoredAt = (Get-Date -Format 'o') DestinationHostname = $env:COMPUTERNAME DestinationUser = [System.Security.Principal.WindowsIdentity]::GetCurrent().Name MachineNumber = $mn CurrentDataBytes = (Get-Item $localCur).Length ArchivedDataFiles = $arcFiles ArchivedDataBytes = $arcBytes RestoredVia = 'Restore-UDCData.ps1 (manifest engine, on logon)' } $restoreManifest | ConvertTo-Json | Set-Content -Path (Join-Path $migStamp 'restore.manifest.json') -Encoding UTF8 Log "Backup consumed -> migrated\$stamp\" } catch { Log "Move-to-migrated failed (data IS restored locally, but live backup remains - next cycle will retry consumption): $_" 'ERROR' } } else { Log "Restore incomplete - leaving live backup at $bayDir for retry next cycle." 'WARN' } # --- Relaunch UDC with the current machine number args so it picks up the # restored CurrentData.json. UDC's vendor autostart in HKLM\Run will also # fire on the next user logon, so this is belt-and-suspenders for the # same-session case (e.g. tech is at the keyboard during the restore). --- if ((Test-Path -LiteralPath $UdcExePath) -and $copiedCur) { try { Start-Process -FilePath $UdcExePath -ArgumentList @("`"$Site`"", "-$mn") Log "Relaunched UDC.exe with `"$Site`" -$mn" } catch { Log "UDC relaunch failed: $_" 'WARN' } } Log "===============================================" exit 0