Files
shopdb-flask/plugins/geenforce/client/Register-ShopdbShadow.ps1

130 lines
6.8 KiB
PowerShell

# 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 <drive>:\<scope>\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: <path>" 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 <drive>:\<scope>\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
}