# Register-ShopdbShadow.ps1 -- put this bay into shopdb SHADOW mode. # # Shadow mode = fetch the shopdb manifest, diff it against the share manifest, # report the cycle to shopdb, and install FROM THE SHARE exactly as today. Zero # behaviour change. It is the observable step before any cutover. # # Runs as SYSTEM under GE-Enforce, from a manifest entry gated to one hostname. # Idempotent: re-registers the task each cycle so drift self-heals, and writes # BaseUrl only when it differs. # # WHY the share manifest is read through the mounted drive: GE-Enforce.ps1 # mounts the SFLD share with SFLD credentials before invoking the engine, and # this script runs inside that window. SYSTEM has no standing access to the UNC # path, so the mounted drive is the only path that resolves. The drive letter is # not fixed, so it is derived from where this script is running rather than # hardcoded. $ErrorActionPreference = 'Continue' $TaskName = 'ShopDB GE-Enforce (shadow)' $InstallDir = 'C:\Program Files\GE\Shopfloor' $BaseUrl = 'https://tsgwp00525.wjs.geaerospace.net/shopdb' # Scope is NOT hardcoded: this script is shipped by more than one scope # (collections and nocollections today), and a wrong value here would shadow the # wrong manifest silently. It runs from :\\shopdb-client, so the # directory it sits under IS the scope name - the same derivation used below for # the share manifest, so the two cannot disagree. $Scope = Split-Path -Leaf (Split-Path -Parent $PSScriptRoot) function Write-ShadowLog { # Write-Host ALONE is not enough: the engine records only "ps1: " and # the exit code for a PS1 entry, so nothing this script says reaches the # enforce log. Combined with the fail-safe `exit 0` on every path, a silent # early-out was indistinguishable from success - which is exactly how the # never-firing task went unnoticed. Write to a file as well so the next # failure is answerable from disk. param([string]$Message) $line = "[{0}] [shadow-setup] {1}" -f (Get-Date -Format 'yyyy-MM-dd HH:mm:ss'), $Message Write-Host $line try { $dir = 'C:\Logs\Shopfloor' if (-not (Test-Path $dir)) { New-Item -ItemType Directory -Path $dir -Force | Out-Null } Add-Content -LiteralPath (Join-Path $dir ('shadow-setup-{0}.log' -f (Get-Date -Format yyyyMMdd))) -Value $line } catch { } } try { # --- BaseUrl (the client reads this; no token needed on an allowlisted subnet) $regPath = 'HKLM:\SOFTWARE\GE\ShopDB' if (-not (Test-Path $regPath)) { New-Item -Path $regPath -Force | Out-Null } $current = (Get-ItemProperty -Path $regPath -Name BaseUrl -ErrorAction SilentlyContinue).BaseUrl if ($current -ne $BaseUrl) { Set-ItemProperty -Path $regPath -Name BaseUrl -Value $BaseUrl Write-ShadowLog "BaseUrl set to $BaseUrl" } $runner = Join-Path $InstallDir 'Invoke-ShopdbEnforce.ps1' $engine = Join-Path $InstallDir 'lib\Install-FromManifest.ps1' foreach ($p in @($runner, $engine)) { if (-not (Test-Path -LiteralPath $p)) { Write-ShadowLog "MISSING $p - the File entries have not landed yet; will retry next cycle." exit 0 # fail-safe: never break the bay, the entry re-runs } } # --- the share manifest this scope is enforced from, via the mounted drive. # $PSScriptRoot is :\\shopdb-client, so its grandparent is the # scope dir. Deriving it keeps this correct whatever letter GE-Enforce mounted. $scopeDir = Split-Path -Parent $PSScriptRoot $shareManifest = Join-Path $scopeDir 'manifest.json' if (-not (Test-Path -LiteralPath $shareManifest)) { Write-ShadowLog "share manifest not found at $shareManifest - not registering." exit 0 } $arguments = '-NoProfile -ExecutionPolicy Bypass -File "{0}" -Scope "{1}" -EnginePath "{2}" -ShadowMode -ShareManifestPath "{3}"' ` -f $runner, $Scope, $engine, $shareManifest # ONLY register when it is missing or its arguments changed. # # Registering unconditionally is what stopped this working the first time. # A `-Once -At (Get-Date)` trigger does NOT fire immediately: the first run # is start-boundary PLUS the repetition interval, so 15 minutes out. This # entry is DetectionMethod=Always and runs every enforce cycle, 5 minutes # apart, and each Register-ScheduledTask -Force reset the start boundary to # "now" - pushing the first run back to +15 before the previous +15 could # elapse. 5 < 15, so the task sat at Ready with LastTaskResult 267011 # (SCHED_S_TASK_HAS_NOT_RUN) indefinitely. Measured on the win11 VM. $existing = Get-ScheduledTask -TaskName $TaskName -ErrorAction SilentlyContinue if ($existing) { $currentArgs = ($existing.Actions | Select-Object -First 1).Arguments if ($currentArgs -eq $arguments) { # Correct arguments, but a bay provisioned by the BROKEN version of # this script carries a task that was reset every cycle and so has # never run. Leaving it alone would strand exactly the bays that hit # the bug. Kick it once; after that LastRunTime is set and this is a # no-op forever. $info = $existing | Get-ScheduledTaskInfo $neverran = ($null -eq $info.LastRunTime) -or ($info.LastRunTime -lt (Get-Date '2000-01-01')) if ($neverran) { Write-ShadowLog ("task exists but has never run (LastTaskResult $($info.LastTaskResult)) - starting it once.") Start-ScheduledTask -TaskName $TaskName -ErrorAction SilentlyContinue } return # schedule is correct; do not reset the start boundary } Write-ShadowLog 'task arguments changed - re-registering.' } $action = New-ScheduledTaskAction -Execute 'powershell.exe' -Argument $arguments # RepetitionInterval ALONE - passing RepetitionDuration serializes to a # Duration the Task Scheduler schema rejects. $trigger = New-ScheduledTaskTrigger -Once -At (Get-Date) -RepetitionInterval (New-TimeSpan -Minutes 15) $principal = New-ScheduledTaskPrincipal -UserId 'NT AUTHORITY\SYSTEM' -RunLevel Highest $settings = New-ScheduledTaskSettingsSet -AllowStartIfOnBatteries -DontStopIfGoingOnBatteries -StartWhenAvailable Register-ScheduledTask -TaskName $TaskName -Action $action -Trigger $trigger ` -Principal $principal -Settings $settings -Force | Out-Null # Kick it once rather than waiting out the first 15-minute interval, so a # freshly provisioned bay reports on this cycle instead of the next. Start-ScheduledTask -TaskName $TaskName -ErrorAction SilentlyContinue Write-ShadowLog "registered '$TaskName' (every 15 min), shadowing $shareManifest" exit 0 } catch { # Fail-safe: a broken setup script must never stop the bay enforcing. Write-ShadowLog "FAILED: $_" exit 0 }