Phase 3+4 rename reorg: repo dir renames + startnet.cmd menu
Pairs with Phase 1+2 from earlier (alias maps in Install-FromManifest,
GE-Enforce, Get-PCProfile, verify-state). See project-shopfloor-rename-reorg
memory for the plan.
Phase 3 (repo + paths):
- git mv per-PC-type dirs to gea-shopfloor-* names:
Standard -> gea-shopfloor-collections
CMM -> gea-shopfloor-cmm
Keyence -> gea-shopfloor-keyence
Genspect -> gea-shopfloor-genspect
WaxAndTrace -> gea-shopfloor-waxtrace
Display -> gea-shopfloor-display
Lab -> gea-shopfloor-common (folded; Timeclock+Lab merge)
- New gea-shopfloor-nocollections/ (clone of collections sans UDC scripts).
- New gea-shopfloor-heattreat/ (placeholder, README only).
- Move Standard/ntlars-backups/ -> _ntlars-backups/ (per-MN, not per-type).
- Run-ShopfloorSetup.ps1: Resolve-PCTypeDir helper walks alias group when
the on-disk dir for the current pcType is missing. Set-MachineNumber
helper-copy gated on collections|nocollections|legacy Standard-Machine.
- Update-MachineNumber.ps1: pcProfiles lookups try gea-shopfloor-collections
first, fall back to legacy Standard-Machine. PowerShell 5.1 compatible
(no null-coalesce).
Phase 4 (startnet.cmd menu):
- Choice 3 "GEA Shopfloor" now drills into a 9-item sub-menu instead of
going straight to enrollment. Sub-cats:
1. Machine with Collections -> gea-shopfloor-collections
2. Machine without Collections -> gea-shopfloor-nocollections
3. Common (Timeclock, Lab) -> gea-shopfloor-common
4. Keyence -> gea-shopfloor-keyence
5. CMM -> gea-shopfloor-cmm
6. Genspect -> gea-shopfloor-genspect
7. Heattreat -> gea-shopfloor-heattreat
8. Wax and Trace -> gea-shopfloor-waxtrace
9. Display -> gea-shopfloor-display
- Office menu (existing 6-option) follows for every sub-cat.
- Machine number prompt only for collections + nocollections.
- pc-subtype.txt + display-type.txt no longer written. PCTYPE is a
single full string (gea-shopfloor-*); subtype-aware code paths fall
back to empty and resolve via the alias map.
- CMM bootstrap stage gate switched from "%PCTYPE%"=="CMM" to
"%PCTYPE%"=="gea-shopfloor-cmm".
Test harness:
- B-enforce/run.sh PCSUBTYPE default changed from "Machine" to "" so
single-arg invocation matches the new single-string scheme. Two-arg
legacy form ("Standard Machine") still works via aliasing.
- B-enforce/tamper.ps1 alias-aware Test-MatrixEntryMatches mirroring
verify-state.ps1.
Smoke-tested on win11 VM as SYSTEM via qga: B-enforce harness 5-phase
cycle (stage / baseline / tamper / heal / idempotent) passes 10/10
with PCType=gea-shopfloor-collections AND with legacy "Standard Machine"
two-arg form.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -117,10 +117,42 @@ if (Test-Path $baselineDir) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# --- PCType dir alias resolution (2026-05-04 rename reorg) -------------
|
||||||
|
# Fleet PCs may have pc-type.txt = legacy "Standard"/"CMM"/etc OR new
|
||||||
|
# gea-shopfloor-* names. Repo dirs have been renamed to new names; this
|
||||||
|
# helper resolves either form to the actual on-disk dir under $setupDir.
|
||||||
|
$pcTypeDirAliases = @(
|
||||||
|
@('Standard', 'gea-shopfloor-collections', 'gea-shopfloor-nocollections'),
|
||||||
|
@('Standard-Machine', 'gea-shopfloor-collections', 'gea-shopfloor-nocollections'),
|
||||||
|
@('Standard-Timeclock', 'gea-shopfloor-common'),
|
||||||
|
@('CMM', 'gea-shopfloor-cmm'),
|
||||||
|
@('Keyence', 'gea-shopfloor-keyence'),
|
||||||
|
@('Lab', 'gea-shopfloor-common'),
|
||||||
|
@('WaxAndTrace', 'gea-shopfloor-waxtrace'),
|
||||||
|
@('Genspect', 'gea-shopfloor-genspect'),
|
||||||
|
@('Display', 'gea-shopfloor-display'),
|
||||||
|
@('Heattreat', 'gea-shopfloor-heattreat')
|
||||||
|
)
|
||||||
|
function Resolve-PCTypeDir {
|
||||||
|
param([string]$BaseDir, [string]$Name)
|
||||||
|
$primary = Join-Path $BaseDir $Name
|
||||||
|
if (Test-Path $primary) { return $primary }
|
||||||
|
foreach ($g in $pcTypeDirAliases) {
|
||||||
|
if ($g -icontains $Name) {
|
||||||
|
foreach ($alias in $g) {
|
||||||
|
if ($alias -ieq $Name) { continue }
|
||||||
|
$candidate = Join-Path $BaseDir $alias
|
||||||
|
if (Test-Path $candidate) { return $candidate }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return $null
|
||||||
|
}
|
||||||
|
|
||||||
# --- Run type-specific scripts (if not just baseline Shopfloor) ---
|
# --- Run type-specific scripts (if not just baseline Shopfloor) ---
|
||||||
if ($pcType -ne "Shopfloor") {
|
if ($pcType -ne "Shopfloor") {
|
||||||
$typeDir = Join-Path $setupDir $pcType
|
$typeDir = Resolve-PCTypeDir -BaseDir $setupDir -Name $pcType
|
||||||
if (Test-Path $typeDir) {
|
if ($typeDir -and (Test-Path $typeDir)) {
|
||||||
# Only run numbered scripts (01-eDNC.ps1, 02-MachineNumberACLs.ps1).
|
# Only run numbered scripts (01-eDNC.ps1, 02-MachineNumberACLs.ps1).
|
||||||
# Unnumbered .ps1 files (Set-MachineNumber.ps1) are desktop tools,
|
# Unnumbered .ps1 files (Set-MachineNumber.ps1) are desktop tools,
|
||||||
# not installer scripts, and must not auto-run during setup.
|
# not installer scripts, and must not auto-run during setup.
|
||||||
@@ -173,17 +205,26 @@ foreach ($tool in @('sync_intune.bat', 'Configure-PC.bat', 'Force-Lockdown.bat',
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
# Standard-Machine PCs get the UDC/eDNC machine number helper. Timeclock
|
# Machine-number-using PC types (collections + nocollections, plus their
|
||||||
# PCs do not use a machine number, so the helper has nothing to do there.
|
# legacy Standard-Machine alias) get the Set-MachineNumber helper on the
|
||||||
if ($pcType -eq "Standard" -and $pcSubType -ne "Timeclock") {
|
# SupportUser desktop. Timeclock / Lab / common variants don't use a
|
||||||
foreach ($helper in @("Set-MachineNumber.bat", "Set-MachineNumber.ps1")) {
|
# machine number, so the helper has nothing to do there.
|
||||||
$src = Join-Path $setupDir "Standard\$helper"
|
$needsMachineNumberHelper = $false
|
||||||
|
if ($pcType -ieq 'Standard' -and $pcSubType -ne 'Timeclock') { $needsMachineNumberHelper = $true }
|
||||||
|
if ($pcType -ieq 'gea-shopfloor-collections' -or $pcType -ieq 'gea-shopfloor-nocollections') { $needsMachineNumberHelper = $true }
|
||||||
|
if ($needsMachineNumberHelper) {
|
||||||
|
$helperSrc = Resolve-PCTypeDir -BaseDir $setupDir -Name 'gea-shopfloor-collections'
|
||||||
|
if (-not $helperSrc) { $helperSrc = Resolve-PCTypeDir -BaseDir $setupDir -Name 'Standard' }
|
||||||
|
foreach ($helper in @('Set-MachineNumber.bat', 'Set-MachineNumber.ps1')) {
|
||||||
|
if ($helperSrc) {
|
||||||
|
$src = Join-Path $helperSrc $helper
|
||||||
if (Test-Path $src) {
|
if (Test-Path $src) {
|
||||||
Copy-Item -Path $src -Destination "C:\Users\SupportUser\Desktop\$helper" -Force
|
Copy-Item -Path $src -Destination "C:\Users\SupportUser\Desktop\$helper" -Force
|
||||||
Write-Host "$helper copied to SupportUser desktop."
|
Write-Host "$helper copied to SupportUser desktop."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
# --- Register sync_intune as persistent @logon scheduled task ---
|
# --- Register sync_intune as persistent @logon scheduled task ---
|
||||||
# Must be registered BEFORE enrollment because Install-ProvisioningPackage
|
# Must be registered BEFORE enrollment because Install-ProvisioningPackage
|
||||||
|
|||||||
@@ -75,7 +75,10 @@ function Update-MachineNumber {
|
|||||||
if (Test-Path $siteCfgPath) {
|
if (Test-Path $siteCfgPath) {
|
||||||
try {
|
try {
|
||||||
$cfg = Get-Content $siteCfgPath -Raw | ConvertFrom-Json
|
$cfg = Get-Content $siteCfgPath -Raw | ConvertFrom-Json
|
||||||
$sharePath = $cfg.pcProfiles.'Standard-Machine'.ntlarsBackupSharePath
|
# 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 {}
|
} catch {}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -105,7 +108,10 @@ function Update-MachineNumber {
|
|||||||
# creds, so go direct to the canonical share. ---
|
# creds, so go direct to the canonical share. ---
|
||||||
$udcSettingsSharePath = $null
|
$udcSettingsSharePath = $null
|
||||||
if ($cfg) {
|
if ($cfg) {
|
||||||
try { $udcSettingsSharePath = $cfg.pcProfiles.'Standard-Machine'.udcSettingsSharePath } catch {}
|
try {
|
||||||
|
$udcSettingsSharePath = $cfg.pcProfiles.'gea-shopfloor-collections'.udcSettingsSharePath
|
||||||
|
if (-not $udcSettingsSharePath) { $udcSettingsSharePath = $cfg.pcProfiles.'Standard-Machine'.udcSettingsSharePath }
|
||||||
|
} catch {}
|
||||||
}
|
}
|
||||||
if ($udcSettingsSharePath) {
|
if ($udcSettingsSharePath) {
|
||||||
try {
|
try {
|
||||||
@@ -147,7 +153,10 @@ function Update-MachineNumber {
|
|||||||
# transition). ---
|
# transition). ---
|
||||||
$udcSharePath = $null
|
$udcSharePath = $null
|
||||||
if ($cfg) {
|
if ($cfg) {
|
||||||
try { $udcSharePath = $cfg.pcProfiles.'Standard-Machine'.udcBackupSharePath } catch {}
|
try {
|
||||||
|
$udcSharePath = $cfg.pcProfiles.'gea-shopfloor-collections'.udcBackupSharePath
|
||||||
|
if (-not $udcSharePath) { $udcSharePath = $cfg.pcProfiles.'Standard-Machine'.udcBackupSharePath }
|
||||||
|
} catch {}
|
||||||
}
|
}
|
||||||
if ($udcSharePath) {
|
if ($udcSharePath) {
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -1,19 +0,0 @@
|
|||||||
@echo off
|
|
||||||
REM Install-eMxInfo.cmd - copy the site-specific eMxInfo.txt into both
|
|
||||||
REM Program Files eDNC paths. Run by Install-FromManifest.ps1 (Type=CMD)
|
|
||||||
REM when Hash detection on C:\Program Files\eDNC\eMxInfo.txt fails.
|
|
||||||
|
|
||||||
set "SRC=%~dp0eMxInfo.txt"
|
|
||||||
if not exist "%SRC%" (
|
|
||||||
echo Install-eMxInfo: source file not found at %SRC%
|
|
||||||
exit /b 1
|
|
||||||
)
|
|
||||||
|
|
||||||
if not exist "C:\Program Files\eDNC\" mkdir "C:\Program Files\eDNC\" 2>/dev/null
|
|
||||||
if not exist "C:\Program Files (x86)\eDNC\" mkdir "C:\Program Files (x86)\eDNC\" 2>/dev/null
|
|
||||||
|
|
||||||
copy /Y "%SRC%" "C:\Program Files\eDNC\eMxInfo.txt" >/dev/null || exit /b 2
|
|
||||||
copy /Y "%SRC%" "C:\Program Files (x86)\eDNC\eMxInfo.txt" >/dev/null || exit /b 3
|
|
||||||
|
|
||||||
echo Install-eMxInfo: deployed eMxInfo.txt to both eDNC paths
|
|
||||||
exit /b 0
|
|
||||||
@@ -1,345 +0,0 @@
|
|||||||
# 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, ~5 lines of log
|
|
||||||
# - 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 <bay>\migrated\<timestamp>\, 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.
|
|
||||||
#
|
|
||||||
# LOGGING:
|
|
||||||
# Single rotating log at C:\Logs\UDC\Restore-UDCData.log (1 MB cap, rotated
|
|
||||||
# to .old.log on overflow). Every cycle writes a header line so even the
|
|
||||||
# silent no-op path leaves a trace. Errors include full exception type,
|
|
||||||
# position, and stack trace.
|
|
||||||
|
|
||||||
[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',
|
|
||||||
# Share can take 20-60s to become reachable from SYSTEM context after a
|
|
||||||
# cold boot or fresh logon. Retry until then before deciding "no backup".
|
|
||||||
[int]$ShareTimeoutSec = 60,
|
|
||||||
[int]$SharePollSec = 3
|
|
||||||
)
|
|
||||||
|
|
||||||
$ErrorActionPreference = 'Continue'
|
|
||||||
|
|
||||||
# -- Logging setup --------------------------------------------------------
|
|
||||||
$logDir = 'C:\Logs\UDC'
|
|
||||||
try {
|
|
||||||
if (-not (Test-Path $logDir)) { New-Item -Path $logDir -ItemType Directory -Force | Out-Null }
|
|
||||||
} catch { $logDir = $env:TEMP }
|
|
||||||
$logFile = Join-Path $logDir 'Restore-UDCData.log'
|
|
||||||
$logFileMaxBytes = 1MB
|
|
||||||
|
|
||||||
# Rotate log file if oversized (keeps one prior generation)
|
|
||||||
try {
|
|
||||||
if ((Test-Path $logFile) -and ((Get-Item $logFile).Length -gt $logFileMaxBytes)) {
|
|
||||||
$rotated = Join-Path $logDir 'Restore-UDCData.old.log'
|
|
||||||
if (Test-Path $rotated) { Remove-Item $rotated -Force -ErrorAction SilentlyContinue }
|
|
||||||
Rename-Item -Path $logFile -NewName 'Restore-UDCData.old.log' -Force -ErrorAction SilentlyContinue
|
|
||||||
}
|
|
||||||
} catch {}
|
|
||||||
|
|
||||||
function Log {
|
|
||||||
param([string]$Msg, [string]$Level = 'INFO')
|
|
||||||
$ts = Get-Date -Format 'yyyy-MM-dd HH:mm:ss.fff'
|
|
||||||
$line = "[$ts][$Level] $Msg"
|
|
||||||
try { Add-Content -LiteralPath $logFile -Value $line -ErrorAction SilentlyContinue } catch {}
|
|
||||||
Write-Host $line
|
|
||||||
}
|
|
||||||
|
|
||||||
function LogErr {
|
|
||||||
param($Err)
|
|
||||||
if (-not $Err) { return }
|
|
||||||
$exType = if ($Err.Exception) { $Err.Exception.GetType().FullName } else { '<no exception>' }
|
|
||||||
$exMsg = if ($Err.Exception) { $Err.Exception.Message } else { "$Err" }
|
|
||||||
Log " exception: $exType - $exMsg" 'ERROR'
|
|
||||||
if ($Err.InvocationInfo -and $Err.InvocationInfo.PositionMessage) {
|
|
||||||
$pos = ($Err.InvocationInfo.PositionMessage -replace "`r?`n", ' | ')
|
|
||||||
Log " at: $pos" 'ERROR'
|
|
||||||
}
|
|
||||||
if ($Err.ScriptStackTrace) {
|
|
||||||
$st = ($Err.ScriptStackTrace -replace "`r?`n", ' | ')
|
|
||||||
Log " stack: $st" 'ERROR'
|
|
||||||
}
|
|
||||||
if ($Err.Exception -and $Err.Exception.InnerException) {
|
|
||||||
Log " inner: $($Err.Exception.InnerException.Message)" 'ERROR'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Log '==============================================='
|
|
||||||
Log "Restore-UDCData starting (PID $PID)"
|
|
||||||
Log "Hostname: $env:COMPUTERNAME"
|
|
||||||
try {
|
|
||||||
$whoami = [System.Security.Principal.WindowsIdentity]::GetCurrent().Name
|
|
||||||
} catch { $whoami = '<unknown>' }
|
|
||||||
Log "User identity: $whoami"
|
|
||||||
Log "PowerShell version: $($PSVersionTable.PSVersion)"
|
|
||||||
Log "BackupShareRoot: $BackupShareRoot"
|
|
||||||
Log "UdcDataDir: $UdcDataDir"
|
|
||||||
Log "UdcSettingsPath: $UdcSettingsPath"
|
|
||||||
Log "ShareTimeoutSec: $ShareTimeoutSec SharePollSec: $SharePollSec"
|
|
||||||
|
|
||||||
# -- Resolve local machine number ----------------------------------------
|
|
||||||
if (-not (Test-Path -LiteralPath $UdcSettingsPath)) {
|
|
||||||
Log "udc_settings.json not present - UDC not installed yet, no work to do."
|
|
||||||
Log 'Exit 0.'
|
|
||||||
exit 0
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
$json = Get-Content -LiteralPath $UdcSettingsPath -Raw -ErrorAction Stop | ConvertFrom-Json -ErrorAction Stop
|
|
||||||
$mn = $json.GeneralSettings.MachineNumber
|
|
||||||
Log "Resolved MachineNumber from udc_settings: $mn"
|
|
||||||
} catch {
|
|
||||||
Log "Failed to parse $UdcSettingsPath" 'ERROR'
|
|
||||||
LogErr $_
|
|
||||||
Log 'Exit 0.'
|
|
||||||
exit 0
|
|
||||||
}
|
|
||||||
if (-not $mn -or $mn -eq '9999' -or $mn -notmatch '^\d+$') {
|
|
||||||
Log "Machine number is placeholder/empty/non-numeric ('$mn'). Update-MachineNumber.ps1's branch will catch the placeholder->real transition. No work to do."
|
|
||||||
Log 'Exit 0.'
|
|
||||||
exit 0
|
|
||||||
}
|
|
||||||
|
|
||||||
# -- Mount share with SFLD user creds -----------------------------------
|
|
||||||
# This script runs as NT AUTHORITY\SYSTEM (manifest engine on logon, or
|
|
||||||
# scheduled task). SYSTEM authenticates to remote SMB as the COMPUTER
|
|
||||||
# ACCOUNT (DOMAIN\HOSTNAME$), not as a user. The SFLD share's ACLs grant
|
|
||||||
# top-level enumeration to authenticated computers but file-level read
|
|
||||||
# only to a specific SFLD user. Without explicit user creds, Test-Path
|
|
||||||
# on bay-level files returns False (access denied = same return as not-
|
|
||||||
# found), making the script silently log "absent" when the files in fact
|
|
||||||
# exist. Symptom: Restore-UDCData.log shows endless "no work this cycle"
|
|
||||||
# while another PC (or a user-context invocation) successfully consumes
|
|
||||||
# the backup. Fix: mount the share with explicit SFLD creds from
|
|
||||||
# HKLM:\SOFTWARE\GE\SFLD\Credentials and probe via the drive letter.
|
|
||||||
. (Join-Path $PSScriptRoot '..\Shopfloor\lib\Restore-EDncReg.ps1')
|
|
||||||
|
|
||||||
Log "Mounting share with SFLD creds: $BackupShareRoot -> W:"
|
|
||||||
$shareMounted = $false
|
|
||||||
$sw = [Diagnostics.Stopwatch]::StartNew()
|
|
||||||
while ($sw.Elapsed.TotalSeconds -lt $ShareTimeoutSec) {
|
|
||||||
if (Mount-SFLDShare -SharePath $BackupShareRoot -DriveLetter 'W:') {
|
|
||||||
$shareMounted = $true
|
|
||||||
break
|
|
||||||
}
|
|
||||||
Start-Sleep -Seconds $SharePollSec
|
|
||||||
}
|
|
||||||
$sw.Stop()
|
|
||||||
if ($shareMounted) {
|
|
||||||
Log ("Share mounted as W: after {0:N1} s" -f $sw.Elapsed.TotalSeconds)
|
|
||||||
} else {
|
|
||||||
Log "Mount-SFLDShare failed after $ShareTimeoutSec s. SFLD creds may be missing in HKLM:\SOFTWARE\GE\SFLD\Credentials, or the share is unreachable. Exiting non-zero so the dispatcher logs a failure." 'ERROR'
|
|
||||||
Log 'Exit 1.'
|
|
||||||
exit 1
|
|
||||||
}
|
|
||||||
|
|
||||||
# All bay-level paths now go through W: (authenticated as SFLD user) so
|
|
||||||
# Test-Path returns the truth, not access-denied-False.
|
|
||||||
$bayDir = Join-Path 'W:\' $mn
|
|
||||||
$srcCur = Join-Path $bayDir 'CurrentData.json'
|
|
||||||
$srcArc = Join-Path $bayDir 'ArchivedData'
|
|
||||||
Log "Probing backup paths for bay $mn"
|
|
||||||
Log " bayDir: $bayDir"
|
|
||||||
$bayDirExists = Test-Path -LiteralPath $bayDir
|
|
||||||
Log " bayDir exists: $bayDirExists"
|
|
||||||
$srcCurExists = Test-Path -LiteralPath $srcCur
|
|
||||||
Log " CurrentData.json src: $(if ($srcCurExists) { 'present' } else { 'absent' }) - $srcCur"
|
|
||||||
$srcArcExists = Test-Path -LiteralPath $srcArc
|
|
||||||
Log " ArchivedData/ src: $(if ($srcArcExists) { 'present' } else { 'absent' }) - $srcArc"
|
|
||||||
|
|
||||||
if (-not $srcCurExists -and -not $srcArcExists) {
|
|
||||||
Log "No backup waiting for bay $mn (neither CurrentData.json nor ArchivedData\ at bay root) - no work to do this cycle."
|
|
||||||
& net use W: /delete /y 2>$null | Out-Null
|
|
||||||
Log 'Exit 0.'
|
|
||||||
exit 0
|
|
||||||
}
|
|
||||||
if (-not $srcCurExists) {
|
|
||||||
Log "Partial backup waiting (ArchivedData\ present, CurrentData.json absent). Will restore ArchivedData\ only. Source PC may have had no live UDC session at backup time, or backup partially failed." 'WARN'
|
|
||||||
}
|
|
||||||
if (-not $srcArcExists) {
|
|
||||||
Log "Partial backup waiting (CurrentData.json present, ArchivedData\ absent). Will restore CurrentData.json only." 'WARN'
|
|
||||||
}
|
|
||||||
|
|
||||||
# -- We have a backup. Restore. ------------------------------------------
|
|
||||||
Log "Backup waiting at $bayDir - proceeding with restore"
|
|
||||||
|
|
||||||
# Stop UDC.exe so CurrentData.json isn't locked
|
|
||||||
$udcProcs = @(Get-Process UDC -ErrorAction SilentlyContinue)
|
|
||||||
Log "UDC processes currently running: $($udcProcs.Count)"
|
|
||||||
foreach ($p in $udcProcs) {
|
|
||||||
try {
|
|
||||||
Log " stopping UDC.exe PID $($p.Id)"
|
|
||||||
$p.Kill()
|
|
||||||
$p.WaitForExit(5000) | Out-Null
|
|
||||||
Log " stopped"
|
|
||||||
} catch {
|
|
||||||
Log " could not stop UDC.exe PID $($p.Id)" 'WARN'
|
|
||||||
LogErr $_
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Start-Sleep -Milliseconds 500
|
|
||||||
|
|
||||||
# Ensure local UDC data dir exists
|
|
||||||
if (-not (Test-Path -LiteralPath $UdcDataDir)) {
|
|
||||||
Log "Creating local UDC data dir: $UdcDataDir"
|
|
||||||
try {
|
|
||||||
New-Item -Path $UdcDataDir -ItemType Directory -Force | Out-Null
|
|
||||||
} catch {
|
|
||||||
Log "Failed to create $UdcDataDir - cannot continue" 'ERROR'
|
|
||||||
LogErr $_
|
|
||||||
& net use W: /delete /y 2>$null | Out-Null
|
|
||||||
Log 'Exit 1.'
|
|
||||||
exit 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
$localCur = Join-Path $UdcDataDir 'CurrentData.json'
|
|
||||||
$localArc = Join-Path $UdcDataDir 'ArchivedData'
|
|
||||||
|
|
||||||
# Copy CurrentData.json (only if present at source)
|
|
||||||
$copiedCur = $false
|
|
||||||
if ($srcCurExists) {
|
|
||||||
Log "Copying CurrentData.json"
|
|
||||||
Log " src: $srcCur"
|
|
||||||
Log " dst: $localCur"
|
|
||||||
try {
|
|
||||||
Copy-Item -LiteralPath $srcCur -Destination $localCur -Force -ErrorAction Stop
|
|
||||||
$copiedCur = $true
|
|
||||||
$sz = (Get-Item -LiteralPath $localCur).Length
|
|
||||||
Log " OK ($sz bytes)"
|
|
||||||
} catch {
|
|
||||||
Log " FAILED" 'ERROR'
|
|
||||||
LogErr $_
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
Log "CurrentData.json not present in backup - skipping that copy step"
|
|
||||||
}
|
|
||||||
|
|
||||||
# Copy ArchivedData/
|
|
||||||
$copiedArc = $false
|
|
||||||
$arcFiles = 0
|
|
||||||
$arcBytes = 0
|
|
||||||
if ($srcArcExists) {
|
|
||||||
Log "Copying ArchivedData/"
|
|
||||||
Log " src: $srcArc"
|
|
||||||
Log " dst: $localArc"
|
|
||||||
try {
|
|
||||||
if (Test-Path -LiteralPath $localArc) {
|
|
||||||
Log " removing existing $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 " OK ($arcFiles files, $arcBytes bytes)"
|
|
||||||
} catch {
|
|
||||||
Log " FAILED" 'ERROR'
|
|
||||||
LogErr $_
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
Log "ArchivedData/ not present in backup - skipping that copy step"
|
|
||||||
}
|
|
||||||
|
|
||||||
# One-shot consumption: only consume when every present source has been
|
|
||||||
# successfully copied. If a source was absent we don't fault on it; if a
|
|
||||||
# source was present but copy failed, we leave the live backup for retry.
|
|
||||||
# Must have copied at least one thing to consume.
|
|
||||||
$consumeOk = (($copiedCur -or -not $srcCurExists) -and `
|
|
||||||
($copiedArc -or -not $srcArcExists) -and `
|
|
||||||
($copiedCur -or $copiedArc))
|
|
||||||
Log "consumeOk=$consumeOk (copiedCur=$copiedCur, copiedArc=$copiedArc, srcCurExists=$srcCurExists, srcArcExists=$srcArcExists)"
|
|
||||||
|
|
||||||
if ($consumeOk) {
|
|
||||||
try {
|
|
||||||
$stamp = Get-Date -Format 'yyyy-MM-ddTHH-mm-ssZ'
|
|
||||||
$migDir = Join-Path $bayDir 'migrated'
|
|
||||||
$migStamp = Join-Path $migDir $stamp
|
|
||||||
Log "Moving consumed backup to $migStamp"
|
|
||||||
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 }
|
|
||||||
|
|
||||||
if (Test-Path -LiteralPath $srcCur) {
|
|
||||||
Move-Item -LiteralPath $srcCur -Destination (Join-Path $migStamp 'CurrentData.json') -Force -ErrorAction Stop
|
|
||||||
Log " moved CurrentData.json"
|
|
||||||
}
|
|
||||||
if (Test-Path -LiteralPath $srcArc) {
|
|
||||||
Move-Item -LiteralPath $srcArc -Destination (Join-Path $migStamp 'ArchivedData') -Force -ErrorAction Stop
|
|
||||||
Log " moved ArchivedData/"
|
|
||||||
}
|
|
||||||
$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
|
|
||||||
Log " moved backup.manifest.json"
|
|
||||||
}
|
|
||||||
|
|
||||||
$restoreManifest = [ordered]@{
|
|
||||||
RestoredAt = (Get-Date -Format 'o')
|
|
||||||
DestinationHostname = $env:COMPUTERNAME
|
|
||||||
DestinationUser = $whoami
|
|
||||||
MachineNumber = $mn
|
|
||||||
CurrentDataPresent = $copiedCur
|
|
||||||
CurrentDataBytes = if ($copiedCur) { (Get-Item -LiteralPath $localCur).Length } else { 0 }
|
|
||||||
ArchivedDataPresent = $copiedArc
|
|
||||||
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 " wrote restore.manifest.json"
|
|
||||||
|
|
||||||
Log "Backup consumed -> migrated\$stamp\"
|
|
||||||
} catch {
|
|
||||||
Log "Move-to-migrated FAILED (data IS restored locally; live backup remains, next cycle will retry consumption)" 'ERROR'
|
|
||||||
LogErr $_
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
Log "Restore incomplete - leaving live backup at $bayDir for retry next cycle." 'WARN'
|
|
||||||
}
|
|
||||||
|
|
||||||
# Relaunch UDC with the current machine number args. 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 -or $copiedArc)) {
|
|
||||||
Log "Relaunching UDC.exe: `"$Site`" -$mn"
|
|
||||||
try {
|
|
||||||
Start-Process -FilePath $UdcExePath -ArgumentList @("`"$Site`"", "-$mn")
|
|
||||||
Log " relaunched"
|
|
||||||
} catch {
|
|
||||||
Log " UDC relaunch FAILED" 'WARN'
|
|
||||||
LogErr $_
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
# Unmount the SFLD-creds-mounted drive so we don't leave a stale net-use entry
|
|
||||||
& net use W: /delete /y 2>$null | Out-Null
|
|
||||||
|
|
||||||
Log 'Exit 0.'
|
|
||||||
Log '==============================================='
|
|
||||||
exit 0
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user