Files
shopdb-flask/plugins/geenforce/client/Register-ShopdbShadow.ps1
cproudlock 6dc363411d geenforce: the shadow task actually runs, and says so on disk
Registered fine and never fired once. Three faults, all found on the win11 VM.

A `-Once -At (Get-Date)` trigger does NOT run immediately: its first run is the
start boundary PLUS the repetition interval, 15 minutes out. This ships as a
DetectionMethod=Always entry, so it ran 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 Ready at LastTaskResult 267011 (SCHED_S_TASK_HAS_NOT_RUN)
forever. It now registers only when the task is missing or its arguments
changed, and starts it once on first registration rather than waiting out the
first interval.

A bay provisioned by the broken version carries a task with correct arguments
that has never run, so "leave it alone if it matches" would have stranded
exactly the machines that hit the bug. If the task has never run it is kicked
once; after that LastRunTime is set and the check is a no-op.

None of this was visible. The engine records only "ps1: <path>" and an exit
code for a PS1 entry, so Write-Host reached nothing, and with the fail-safe
`exit 0` on every path a silent early-out was indistinguishable from success.
It now also writes C:\Logs\Shopfloor\shadow-setup-<date>.log.

Scope is no longer hardcoded either: this script is shipped by more than one
scope now, and a wrong value would shadow the wrong manifest silently. It is
derived from the script's own directory, the same way the share manifest path
already was, so the two cannot disagree.
2026-08-13 13:19:59 -04:00

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
}