<# .SYNOPSIS Run the ShopDB kiosk installer as soon as ShopDB becomes reachable. .DESCRIPTION A PXE-imaged display currently ends up with NO GE-Enforce client at all. Confirmed on 579C144, 2026-08-06: is the client installed? NOT FOUND scheduled tasks that would run it? NONE The client is delivered by Install-ShopdbKiosk.ps1, which downloads itself from {BaseUrl}/installers/kiosk over HTTPS - and ShopDB is only reachable once the bay has joined the AESFMA wifi SSID. So the installer cannot run during imaging, and nothing was arranged to run it afterwards. This closes that gap. Imaging stages the installer and the key; this waiter runs as SYSTEM on a schedule, does nothing until ShopDB answers, then runs the installer once and removes itself. Deliberately does NOT reimplement the installer. It waits, invokes, verifies and cleans up - the vendor script keeps doing the actual work, so it can be replaced wholesale without touching this. .PARAMETER EnrollmentRoot Where imaging staged things. Expects: \display-type.txt Dashboard | Lobby | 3DPrintRoom and, beside this script (see -KioskRoot): kiosk\Install-ShopdbKiosk.ps1 kiosk\shopdb-key.txt (optional) labelled: collector=..., fetch=... .PARAMETER TaskName The scheduled task to remove once installation succeeds. .NOTES The key file is deleted after a successful install. The installer writes the secrets into HKLM:\SOFTWARE\GE\ShopDB, which is ACLed to SYSTEM and Administrators - a better home than a file readable by any local user until lockdown. Bounding that exposure to the imaging window is the point. #> [CmdletBinding()] param( [string]$EnrollmentRoot = 'C:\Enrollment', # Where the staged installer and key live. Defaults to kiosk\ NEXT TO THIS # SCRIPT, not under EnrollmentRoot: startnet copies the whole type-specific # tree to # C:\Enrollment\shopfloor-setup\gea-shopfloor-display\ # so the payload arrives beside this file, not at C:\Enrollment\kiosk. # Getting this wrong is silent - the bootstrap just logs "not staged" every # cycle and never installs anything. [string]$KioskRoot = (Join-Path $PSScriptRoot 'kiosk'), [string]$BaseUrl = 'https://tsgwp00525.wjs.geaerospace.net/shopdb', [string]$TaskName = 'ShopDB Kiosk Bootstrap', [int]$TimeoutSeconds = 30 ) $ErrorActionPreference = 'Continue' $logDir = 'C:\Logs\ShopDB' $log = Join-Path $logDir 'kiosk-bootstrap.log' New-Item -ItemType Directory -Path $logDir -Force -EA SilentlyContinue | Out-Null function Log { param([string]$m) $line = "{0} {1}" -f (Get-Date -Format 'yyyy-MM-dd HH:mm:ss'), $m Write-Host $line Add-Content -Path $log -Value $line -EA SilentlyContinue } function Remove-Self { try { Unregister-ScheduledTask -TaskName $TaskName -Confirm:$false -EA Stop Log "Unregistered '$TaskName' - nothing further to do." } catch { Log "Could not unregister '$TaskName': $_" } } Log "=== kiosk bootstrap ===" # --- already done? ------------------------------------------------------ # Both conditions, not just the registry key: BaseUrl alone can be present from # a partial run, and the enforce task is what actually proves an install. $reg = 'HKLM:\SOFTWARE\GE\ShopDB' $haveCfg = (Test-Path $reg) -and ((Get-ItemProperty $reg -EA SilentlyContinue).BaseUrl) $haveTask = [bool](Get-ScheduledTask -EA SilentlyContinue | Where-Object { $_.TaskName -match '(?i)shopdb.*enforce' }) if ($haveCfg -and $haveTask) { Log "Already installed (BaseUrl set, enforce task present)." Remove-Self return } # --- what kind of display is this? -------------------------------------- $dtFile = Join-Path $EnrollmentRoot 'display-type.txt' if (-not (Test-Path $dtFile)) { Log "No $dtFile - cannot choose a DisplayType. Leaving the task armed." return } $displayType = (Get-Content $dtFile -First 1).Trim() if ($displayType -notin @('Dashboard','Lobby','3DPrintRoom')) { Log "display-type.txt says '$displayType', which the installer will reject. Leaving armed." return } Log "DisplayType: $displayType" $installer = Join-Path $KioskRoot 'Install-ShopdbKiosk.ps1' if (-not (Test-Path $installer)) { Log "Installer not staged at $installer. Leaving armed." return } # --- is ShopDB reachable yet? ------------------------------------------- # Expected to fail until the bay joins AESFMA. That is the whole reason this # script exists, so a failure here is logged quietly and retried, not raised. $probe = "$($BaseUrl.TrimEnd('/'))/api/docs" try { $r = Invoke-WebRequest -Uri $probe -UseBasicParsing -TimeoutSec $TimeoutSeconds -EA Stop Log "ShopDB reachable (HTTP $($r.StatusCode))." } catch { $code = try { $_.Exception.Response.StatusCode.value__ } catch { $null } if ($code) { # Answered at all = reachable. 401/403 just means no token on this probe. Log "ShopDB reachable (HTTP $code)." } else { Log "Not reachable yet - waiting for AESFMA. ($($_.Exception.Message))" return } } # --- keys ---------------------------------------------------------------- $keyFile = Join-Path $KioskRoot 'shopdb-key.txt' $collectorKey = '' $fetchToken = '' if (Test-Path $keyFile) { # LABELLED format, because the two tokens are not interchangeable and a # positional mix-up is silent: a fetch token in the collector slot leaves # asset reporting broken while everything looks configured. # # collector= always needed for asset reporting # fetch= optional when the subnet is allowlisted # # A bare single line is REJECTED rather than guessed at. foreach ($line in (Get-Content $keyFile -EA SilentlyContinue)) { $t = $line.Trim() if (-not $t -or $t.StartsWith('#')) { continue } if ($t -match '^(?i)collector\s*=\s*(.+)$') { $collectorKey = $Matches[1].Trim() } elseif ($t -match '^(?i)fetch\s*=\s*(.+)$') { $fetchToken = $Matches[1].Trim() } else { Log "Key file line is not labelled 'collector=' or 'fetch=' - ignoring it rather than guessing which token it is." } } Log ("Key file present (collector={0}, fetch={1})" -f ` $(if ($collectorKey) { 'yes' } else { 'no' }), $(if ($fetchToken) { 'yes' } else { 'no' })) } else { # Not fatal. The fetch token is unnecessary when the kiosk subnet is # IP-allowlisted; only the asset-report task needs the collector key, and # the installer skips that task rather than failing. Log "No key file - installing without tokens (allowlisted subnets still work; no asset reporting)." } # --- run the vendor installer ------------------------------------------- $args = @{ DisplayType = $displayType; BaseUrl = $BaseUrl } if ($collectorKey) { $args.CollectorKey = $collectorKey } if ($fetchToken) { $args.ShopdbToken = $fetchToken } Log "Running $installer ..." try { & $installer @args 2>&1 | ForEach-Object { Log " $_" } } catch { Log "Installer threw: $_" return } # --- verify, then clean up ---------------------------------------------- $haveCfg = (Test-Path $reg) -and ((Get-ItemProperty $reg -EA SilentlyContinue).BaseUrl) $haveTask = [bool](Get-ScheduledTask -EA SilentlyContinue | Where-Object { $_.TaskName -match '(?i)shopdb.*enforce' }) if ($haveCfg -and $haveTask) { Log "Install verified: BaseUrl set and enforce task registered." if (Test-Path $keyFile) { # The installer has moved the secrets into HKLM (SYSTEM/Admins ACL). # The staged copy is readable by any local user until lockdown, so it # goes now rather than lingering. Remove-Item $keyFile -Force -EA SilentlyContinue Log "Removed staged key file." } Remove-Self } else { Log "Installer ran but verification FAILED (BaseUrl=$haveCfg, enforceTask=$haveTask). Leaving armed to retry." }