OWNERSHIP.md Every expensive bug in this pipeline has had one shape: two systems setting the same thing, last writer winning silently. Four happened on 2026-08-06 alone - computer name (package vs run-enrollment), drive letters (PESetup vs a volume finder), enrollment (package vs an at-logon -ManualFallback task that syspreped finished machines), kiosk URLs (GE-Enforce vs site-config). Each is written down with the evidence so the next person deletes a writer instead of adding one. share-drift.py The share is production and the repo is meant to describe it, but drift runs both ways: live hand-edits nobody committed, and repo fixes never deployed. The unattend outage lived only on the share while the repo copy was fine, and nothing compared them. Each mapped pair is classified. git-owned means the repo wins and the pair must match - those fail the run. unreconciled means the two have genuinely diverged and nobody has decided; reported, not failed. The unattends are unreconciled on purpose: live is ~17 KB against ~12 KB in the repo, so a blind push would regress production. Reads over SSH via base64 so BOM and CRLF survive the hop. First run: 8 git-owned pairs all match, 4 known-unreconciled. Run-ShopfloorSetup.ps1 Corrects a comment that was actively misleading. It claimed shopfloor PCs are "vanilla by design" and that the orchestrator runs -ManualFallback to skip BPRT injection and the package entirely. Shopfloor bays DO enrol - the SFLD package joins Entra with its BPRT token and a human assigns the device category in Intune. -ManualFallback runs sysprep /oobe /reboot, which is why wiring it to an at-logon task destroyed the deployment chain. The absent Entra wait is still correct, for a different reason: at that point the bay is on the isolated PXE LAN with no route to Entra (579C144 held 172.16.9.81 and 172.24.19.142, neither in the production ranges). sync_intune retries until the tech re-cables. "Entra ID Joined: false" right after imaging is normal.
552 lines
27 KiB
PowerShell
552 lines
27 KiB
PowerShell
# Run-ShopfloorSetup.ps1 - Main dispatcher for shopfloor PC type setup.
|
|
#
|
|
# Flow:
|
|
# 1. PreInstall apps (Oracle, VC++, OpenText, UDC) -- from local bundle
|
|
# 2. Type-specific scripts (eDNC, ACLs, CMM share apps, etc.)
|
|
# 3. Deferred baseline (desktop org, taskbar pins)
|
|
# 4. Copy desktop tools (sync_intune, Configure-PC, Set-MachineNumber)
|
|
# 5. Run enrollment (PPKG install + wait for completion)
|
|
# 6. Register sync_intune as @logon scheduled task
|
|
# 7. Reboot -- PPKG file operations complete, sync_intune fires on next logon
|
|
#
|
|
# Called by the unattend FirstLogonCommands as SupportUser (admin).
|
|
|
|
# --- Transcript logging ---
|
|
$logDir = 'C:\Logs\SFLD'
|
|
if (-not (Test-Path $logDir)) {
|
|
try { New-Item -ItemType Directory -Path $logDir -Force | Out-Null } catch { $logDir = $env:TEMP }
|
|
}
|
|
$transcriptPath = Join-Path $logDir 'shopfloor-setup.log'
|
|
try { Start-Transcript -Path $transcriptPath -Append -Force | Out-Null } catch {}
|
|
|
|
Write-Host ""
|
|
Write-Host "================================================================"
|
|
Write-Host "=== Run-ShopfloorSetup.ps1 starting $(Get-Date -Format 'yyyy-MM-dd HH:mm:ss') ==="
|
|
Write-Host " Transcript: $transcriptPath"
|
|
Write-Host "================================================================"
|
|
Write-Host ""
|
|
|
|
# Imaging-progress reporter. Posts coarse stage updates to the PXE webapp
|
|
# at http://172.16.9.1:9009/imaging/status so the operator can watch
|
|
# progress in a browser. Best-effort: failures never block imaging.
|
|
$pxeStatusLib = Join-Path $PSScriptRoot 'shopfloor-setup\Shopfloor\lib\Send-PxeStatus.ps1'
|
|
if (Test-Path $pxeStatusLib) {
|
|
try { . $pxeStatusLib } catch { Write-Warning "Send-PxeStatus load failed: $_" }
|
|
}
|
|
function Report-Stage {
|
|
param([string]$Stage, [int]$Index, [int]$Total = 8, [string]$Status = 'in_progress', [string]$Error_ = '')
|
|
if (Get-Command Send-PxeStatus -ErrorAction SilentlyContinue) {
|
|
Send-PxeStatus -Stage $Stage -StageIndex $Index -StageTotal $Total -Status $Status -Error_ $Error_
|
|
}
|
|
}
|
|
Report-Stage -Stage 'Run-ShopfloorSetup: starting' -Index 2
|
|
|
|
# AutoLogonCount is NOT set here. Previously we bumped it to 99/4, but
|
|
# Windows decrements it per-logon and at 0 clears AutoAdminLogon -- which
|
|
# nukes the lockdown-configured ShopFloor autologon later in the chain.
|
|
# The unattend XML's <AutoLogon><LogonCount> handles SupportUser logons;
|
|
# the lockdown's Autologon.exe handles ShopFloor. We stay out of it.
|
|
|
|
# Cancel any pending reboot so it doesn't interrupt setup
|
|
cmd /c "shutdown /a 2>nul" *>$null
|
|
|
|
# Self-resume: register this script as a RunOnce so a vendor-installer-
|
|
# forced reboot mid-flight (FormTracePak Setup.exe, eDNC MSI, etc) auto-
|
|
# resumes the chain after the next SupportUser auto-login. RunOnce is
|
|
# single-shot - if we complete normally we remove this key at end of
|
|
# script. If we're killed mid-flight by a forced reboot, the key
|
|
# survives and fires after reboot.
|
|
#
|
|
# Idempotent design throughout this script: every step checks detection
|
|
# before installing, so a forced-reboot re-entry just skips the already-
|
|
# done work and continues from where it left off.
|
|
#
|
|
# Also top up AutoLogonCount so the SupportUser autologon budget
|
|
# (LogonCount=12 from unattend XML) survives extra unplanned reboots.
|
|
$selfResumeKey = 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\RunOnce'
|
|
$selfResumeName = 'ResumeRunShopfloorSetup'
|
|
$selfResumeCmd = 'powershell.exe -NoProfile -ExecutionPolicy Bypass -File "' + $PSCommandPath + '"'
|
|
try {
|
|
Set-ItemProperty -Path $selfResumeKey -Name $selfResumeName -Value $selfResumeCmd -Type String -Force -ErrorAction Stop
|
|
Write-Host "Self-resume RunOnce registered: will re-fire $PSCommandPath if interrupted"
|
|
} catch {
|
|
Write-Warning "Failed to register self-resume RunOnce: $_"
|
|
}
|
|
try {
|
|
Set-ItemProperty -Path 'HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Winlogon' -Name 'AutoLogonCount' -Value 10 -Type DWord -Force -ErrorAction Stop
|
|
Write-Host "AutoLogonCount topped up to 10 (vendor-forced reboot resilience)"
|
|
} catch {
|
|
Write-Warning "Failed to top up AutoLogonCount: $_"
|
|
}
|
|
|
|
# Wired NIC state handling moved to sync_intune (Monitor-IntuneProgress.ps1).
|
|
# Previously this script prompted the tech to unplug the PXE cable and
|
|
# then re-enabled wired adapters interactively - that blocked the whole
|
|
# imaging chain on human keypress. The new flow leaves the wired state
|
|
# exactly as Order 5 (migrate-to-wifi.ps1) left it:
|
|
# - Tower (no WiFi): wired stays enabled, Run-ShopfloorSetup runs on wired.
|
|
# - Laptop (WiFi): wired disabled, Run-ShopfloorSetup runs on WiFi.
|
|
# Sync_intune re-enables wired at the start of its monitor loop, by which
|
|
# time the tech has had ample wall-clock time to physically unplug PXE
|
|
# and re-cable into production without blocking the chain on a keypress.
|
|
|
|
$enrollDir = "C:\Enrollment"
|
|
$typeFile = Join-Path $enrollDir "pc-type.txt"
|
|
$setupDir = Join-Path $enrollDir "shopfloor-setup"
|
|
|
|
# NOTE: there is deliberately NO wait for an Entra join here - but NOT because
|
|
# shopfloor bays skip enrollment. They do enrol: the SFLD provisioning package
|
|
# joins Entra using the BPRT token it carries, and a human then assigns the
|
|
# device category in Intune.
|
|
#
|
|
# The wait is absent because the join cannot happen yet. At this point the bay is
|
|
# still on the isolated PXE LAN with no route to Entra - observed on 579C144
|
|
# 2026-08-06, holding 172.16.9.81 and 172.24.19.142, neither in the production
|
|
# ranges. sync_intune retries every 30 seconds until the tech re-cables to
|
|
# production, which is the right place to wait. An earlier version blocked here
|
|
# for 45 minutes and then warned about a failure that had not happened.
|
|
# "Entra ID Joined: false" in C:\Logs\BPRT\criticalChecks.json straight after
|
|
# imaging is therefore NORMAL, not a fault.
|
|
#
|
|
# CORRECTION (2026-08-06): this comment previously claimed shopfloor PCs are
|
|
# "vanilla by design" and that the orchestrator runs with -ManualFallback to skip
|
|
# BPRT injection and the package entirely. That was wrong and dangerous.
|
|
# -ManualFallback runs sysprep /oobe /reboot, so wiring it to an at-logon task
|
|
# syspreped finished machines seconds after autologon and destroyed the
|
|
# deployment chain. See docs/OWNERSHIP.md.
|
|
|
|
if (-not (Test-Path $typeFile)) {
|
|
# A missing pc-type.txt means one of two very different things, and the old
|
|
# blanket "skip + exit 0" hid the bad one for weeks: four Display bays sat
|
|
# at imaging stage 2 with a green exit code and nobody noticed.
|
|
# - no C:\Enrollment at all -> this machine was never staged by WinPE
|
|
# (pre-imaging, or the staging block never ran). That is a FAILURE on a
|
|
# machine that has clearly just been imaged, so say so loudly.
|
|
# - C:\Enrollment exists but no pc-type.txt -> staging ran and the write
|
|
# failed. Also a failure.
|
|
$stagingLog = Join-Path $enrollDir 'winpe-staging.log'
|
|
$detail = if (Test-Path $enrollDir) {
|
|
"C:\Enrollment exists but pc-type.txt is missing - WinPE staging ran but did not write it. Check $stagingLog."
|
|
} else {
|
|
"C:\Enrollment does not exist - WinPE staging never ran. The Windows volume was probably not found in startnet.cmd, so pc-type.txt, the enrollment package and shopfloor-setup were ALL skipped."
|
|
}
|
|
Write-Host ""
|
|
Write-Host "================================================================"
|
|
Write-Host " FAILED: no pc-type.txt at $typeFile"
|
|
Write-Host " $detail"
|
|
Write-Host " Shopfloor setup cannot run. This PC is imaged but NOT configured."
|
|
Write-Host "================================================================"
|
|
Write-Host ""
|
|
Report-Stage -Stage 'Run-ShopfloorSetup: FAILED - no pc-type.txt' -Index 2 -Status 'failed' -Error_ $detail
|
|
exit 1
|
|
}
|
|
|
|
$pcType = (Get-Content $typeFile -First 1).Trim()
|
|
if (-not $pcType) {
|
|
Write-Host "pc-type.txt is empty - skipping shopfloor setup."
|
|
exit 0
|
|
}
|
|
|
|
$subtypeFile = Join-Path $enrollDir "pc-subtype.txt"
|
|
$pcSubType = ''
|
|
if (Test-Path $subtypeFile) {
|
|
$pcSubType = (Get-Content $subtypeFile -First 1).Trim()
|
|
}
|
|
|
|
Write-Host "Shopfloor PC Type: $pcType$(if ($pcSubType) { " / $pcSubType" })"
|
|
|
|
# Scripts to skip in the alphabetical baseline loop. Each is either run
|
|
# explicitly in the finalization phase below, or invoked internally by
|
|
# another script:
|
|
#
|
|
# 05 - Office shortcuts. Invoked by 06 as Phase 0. Office isn't installed
|
|
# until after the PPKG + reboot, so 05 no-ops initially. 06's SYSTEM
|
|
# logon task re-runs it on the next boot.
|
|
# 06 - Desktop org. Phase 2 needs eDNC/NTLARS on disk (installed by
|
|
# type-specific 01-eDNC.ps1). Run in finalization phase.
|
|
# 07 - Taskbar pin layout. Reads 06's output. Run in finalization phase.
|
|
# 08 - Edge default browser + startup tabs. Invoked by 06 as Phase 4.
|
|
# Reads .url files delivered by DSC (after Intune enrollment). 06's
|
|
# SYSTEM logon task re-runs it to pick them up.
|
|
$skipInBaseline = @(
|
|
'05-OfficeShortcuts.ps1',
|
|
'06-OrganizeDesktop.ps1',
|
|
'07-TaskbarLayout.ps1',
|
|
'08-EdgeDefaultBrowser.ps1',
|
|
# Machine number flow: split into two scripts registered as scheduled
|
|
# tasks by Register-CheckMachineNumberTask.ps1. Prompt runs as the
|
|
# logged-in user (GUI), Apply runs as SYSTEM (privileged writes).
|
|
# Neither should run in baseline pass.
|
|
'Prompt-MachineNumber.ps1',
|
|
'Apply-MachineNumber.ps1',
|
|
'Check-MachineNumber.ps1',
|
|
'Configure-PC.ps1'
|
|
)
|
|
|
|
# Scripts run AFTER type-specific scripts complete. 05 and 08 are NOT
|
|
# here because 06 calls them internally as sub-phases.
|
|
$runAfterTypeSpecific = @(
|
|
'06-OrganizeDesktop.ps1',
|
|
'07-TaskbarLayout.ps1'
|
|
)
|
|
|
|
# --- Run Shopfloor baseline scripts first (skipping deferred ones) ---
|
|
$baselineDir = Join-Path $setupDir "Shopfloor"
|
|
if (Test-Path $baselineDir) {
|
|
$scripts = Get-ChildItem -Path $baselineDir -Filter "*.ps1" -File | Sort-Object Name
|
|
foreach ($script in $scripts) {
|
|
if ($skipInBaseline -contains $script.Name) {
|
|
Write-Host "Skipping baseline: $($script.Name) (runs in finalization phase)"
|
|
continue
|
|
}
|
|
cmd /c "shutdown /a 2>nul" *>$null
|
|
Write-Host "Running baseline: $($script.Name)"
|
|
try {
|
|
& $script.FullName
|
|
} catch {
|
|
Write-Warning "Baseline script $($script.Name) failed: $_"
|
|
}
|
|
}
|
|
}
|
|
|
|
# --- 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) ---
|
|
if ($pcType -ne "Shopfloor") {
|
|
$typeDir = Resolve-PCTypeDir -BaseDir $setupDir -Name $pcType
|
|
if ($typeDir -and (Test-Path $typeDir)) {
|
|
# Only run numbered scripts (01-eDNC.ps1, 02-MachineNumberACLs.ps1).
|
|
# Unnumbered .ps1 files (Set-MachineNumber.ps1) are desktop tools,
|
|
# not installer scripts, and must not auto-run during setup.
|
|
$scripts = Get-ChildItem -Path $typeDir -Filter "*.ps1" -File |
|
|
Where-Object { $_.Name -match '^\d' } |
|
|
Sort-Object Name
|
|
foreach ($script in $scripts) {
|
|
cmd /c "shutdown /a 2>nul" *>$null
|
|
Write-Host "Running $pcType setup: $($script.Name)"
|
|
try {
|
|
& $script.FullName
|
|
} catch {
|
|
Write-Warning "Script $($script.Name) failed: $_"
|
|
}
|
|
}
|
|
} else {
|
|
Write-Host "No type-specific scripts found for $pcType."
|
|
}
|
|
}
|
|
|
|
# --- Finalization: run deferred baseline scripts (desktop org, taskbar pins)
|
|
# ---
|
|
# These needed to wait until all apps (eDNC, NTLARS, UDC, OpenText) were
|
|
# installed by the baseline + type-specific phases above. 06 internally
|
|
# calls 05 (Office shortcuts) and 08 (Edge config) as sub-phases, so we
|
|
# only need to invoke 06 and 07 explicitly here.
|
|
foreach ($name in $runAfterTypeSpecific) {
|
|
$script = Join-Path $baselineDir $name
|
|
if (-not (Test-Path $script)) {
|
|
Write-Warning "Deferred script not found: $script"
|
|
continue
|
|
}
|
|
cmd /c "shutdown /a 2>nul" *>$null
|
|
Write-Host "Running deferred baseline: $name"
|
|
try {
|
|
& $script
|
|
} catch {
|
|
Write-Warning "Deferred script $name failed: $_"
|
|
}
|
|
}
|
|
|
|
Write-Host "Shopfloor setup complete for $pcType."
|
|
|
|
# --- Copy utility scripts to SupportUser desktop ---
|
|
foreach ($tool in @('sync_intune.bat', 'Configure-PC.bat', 'Force-Lockdown.bat', 'SetShopfloorAutoLogon.bat')) {
|
|
$src = Join-Path $setupDir "Shopfloor\$tool"
|
|
if (Test-Path $src) {
|
|
Copy-Item -Path $src -Destination "C:\Users\SupportUser\Desktop\$tool" -Force
|
|
Write-Host "$tool copied to desktop."
|
|
}
|
|
}
|
|
|
|
# Machine-number-using PC types (collections + nocollections, plus their
|
|
# legacy Standard-Machine alias) get the Set-MachineNumber helper on the
|
|
# SupportUser desktop. Timeclock / Lab / common variants don't use a
|
|
# machine number, so the helper has nothing to do there.
|
|
$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) {
|
|
Copy-Item -Path $src -Destination "C:\Users\SupportUser\Desktop\$helper" -Force
|
|
Write-Host "$helper copied to SupportUser desktop."
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
# --- Register sync_intune as persistent @logon scheduled task ---
|
|
# Must be registered BEFORE enrollment because Install-ProvisioningPackage
|
|
# triggers an immediate reboot that kills run-enrollment.ps1. The task
|
|
# registration must survive the PPKG reboot, so we do it here while
|
|
# Run-ShopfloorSetup.ps1 is still running.
|
|
#
|
|
# The task fires at every logon until sync_intune detects completion and
|
|
# unregisters itself. It monitors Intune enrollment (Phase 1-5), NOT BPRT
|
|
# app installs -- BPRT finishes on its own in the background after the
|
|
# PPKG reboot, and is irrelevant to the Intune lifecycle.
|
|
$taskName = 'Shopfloor Intune Sync'
|
|
$monitorScript = Join-Path $setupDir 'Shopfloor\lib\Monitor-IntuneProgress.ps1'
|
|
$configureScript = Join-Path $setupDir 'Shopfloor\Configure-PC.ps1'
|
|
|
|
if (Test-Path -LiteralPath $monitorScript) {
|
|
try {
|
|
$action = New-ScheduledTaskAction `
|
|
-Execute 'powershell.exe' `
|
|
-Argument "-NoProfile -NoExit -ExecutionPolicy Bypass -File `"$monitorScript`" -AsTask -ConfigureScript `"$configureScript`""
|
|
|
|
$trigger = New-ScheduledTaskTrigger -AtLogOn
|
|
|
|
$principal = New-ScheduledTaskPrincipal `
|
|
-GroupId 'S-1-5-32-545' `
|
|
-RunLevel Limited
|
|
|
|
$settings = New-ScheduledTaskSettingsSet `
|
|
-AllowStartIfOnBatteries `
|
|
-DontStopIfGoingOnBatteries `
|
|
-StartWhenAvailable `
|
|
-ExecutionTimeLimit (New-TimeSpan -Hours 2)
|
|
|
|
Register-ScheduledTask `
|
|
-TaskName $taskName `
|
|
-Action $action `
|
|
-Trigger $trigger `
|
|
-Principal $principal `
|
|
-Settings $settings `
|
|
-Force `
|
|
-ErrorAction Stop | Out-Null
|
|
|
|
Write-Host "Registered '$taskName' logon task."
|
|
} catch {
|
|
Write-Warning "Failed to register sync task: $_"
|
|
}
|
|
} else {
|
|
Write-Warning "Monitor-IntuneProgress.ps1 not found at $monitorScript"
|
|
}
|
|
|
|
# Set auto-logon to expire after 4 more logins (2 needed for sync_intune
|
|
# pre-reboot + post-reboot, plus 2 margin for unexpected reboots from
|
|
# Windows Update, PPKG file operations, or script crashes).
|
|
# (AutoLogonCount intentionally not set -- see comment at top of script)
|
|
|
|
# --- Register cross-PC-type enforcers (Acrobat, etc.) ---
|
|
# These run on every logon regardless of PC type, mounting the SFLD share
|
|
# for version-pinned app enforcement. Initial install already handled by
|
|
# preinstall flow; enforcers only kick in when detection fails.
|
|
# Wired-disable / re-enable dance retired after PXE LAN renumber to
|
|
# 172.16.9.0/24. GE Report IP filters Get-NetIPAddress on StartsWith("10.")
|
|
# so PXE LAN addresses are no longer caught - wired NIC can stay up
|
|
# through the whole imaging chain without leaking to the GE webhook.
|
|
|
|
$commonSetupDir = Join-Path $setupDir 'common'
|
|
|
|
# --- Register the unified GE-Enforce scheduled task ---
|
|
# Single dispatcher for all PC-type ongoing-update enforcement. Reads
|
|
# per-pctype manifest.json from the tsgwp00525 share and processes
|
|
# common + per-type + per-type-subtype manifests in order.
|
|
#
|
|
# Display PCs are excluded: their kiosk user cannot reach the SFLD
|
|
# share, and everything Display needs (kiosk EXE + Edge policies) is
|
|
# baked at imaging time (preinstall.json Install-KioskApp + 09-Setup-
|
|
# Display.ps1). No ongoing share-dependent enforcement on Displays.
|
|
$noEnforceTypes = @('Display', 'gea-shopfloor-display')
|
|
$registerGE = Join-Path $commonSetupDir 'Register-GEEnforce.ps1'
|
|
if ($noEnforceTypes -contains $pcType) {
|
|
Write-Host ""
|
|
Write-Host "=== Skipping GE-Enforce registration ($pcType is self-contained) ==="
|
|
} elseif (Test-Path -LiteralPath $registerGE) {
|
|
Write-Host ""
|
|
Write-Host "=== Registering unified GE Shopfloor enforcer ==="
|
|
try {
|
|
$enforcerRuntime = Join-Path $commonSetupDir 'GE-Enforce.ps1'
|
|
$libSource = Join-Path $commonSetupDir 'lib\Install-FromManifest.ps1'
|
|
# Stage enforcer runtime so the scheduled task can reach it post-imaging.
|
|
$runtimeDir = 'C:\Program Files\GE\Shopfloor'
|
|
$runtimeLib = Join-Path $runtimeDir 'lib'
|
|
foreach ($d in @($runtimeDir, $runtimeLib)) {
|
|
if (-not (Test-Path $d)) { New-Item -Path $d -ItemType Directory -Force | Out-Null }
|
|
}
|
|
Copy-Item -LiteralPath $enforcerRuntime -Destination (Join-Path $runtimeDir 'GE-Enforce.ps1') -Force
|
|
Copy-Item -LiteralPath $libSource -Destination (Join-Path $runtimeLib 'Install-FromManifest.ps1') -Force
|
|
& $registerGE -EnforcerPath (Join-Path $runtimeDir 'GE-Enforce.ps1')
|
|
} catch {
|
|
Write-Warning "GE-Enforce registration failed: $_"
|
|
}
|
|
} else {
|
|
Write-Warning "Register-GEEnforce.ps1 not found - no ongoing enforcement will run on this PC"
|
|
}
|
|
|
|
# Map S: drive on user logon for every account in BUILTIN\Users. The
|
|
# vendor 'SFLD - Consume Credentials' task is principal-restricted and
|
|
# does not fire for the ShopFloor end-user, so this parallel task fills
|
|
# the gap. Cross-PC-type because every shopfloor account needs S:.
|
|
# Display PCs skipped: kiosk user has no SFLD creds, S: map would fail
|
|
# every logon. Self-contained Display has no share dependency.
|
|
$registerMapShare = Join-Path $setupDir 'Shopfloor\Register-MapSfldShare.ps1'
|
|
if ($noEnforceTypes -contains $pcType) {
|
|
Write-Host ""
|
|
Write-Host "=== Skipping S: drive logon mapper ($pcType is self-contained) ==="
|
|
} elseif (Test-Path -LiteralPath $registerMapShare) {
|
|
Write-Host ""
|
|
Write-Host "=== Registering S: drive logon mapper ==="
|
|
try { & $registerMapShare } catch { Write-Warning "Map-SfldShare registration failed: $_" }
|
|
} else {
|
|
Write-Host "Register-MapSfldShare.ps1 not found (optional) - skipping"
|
|
}
|
|
|
|
# --- Check Machine Number logon prompt ---
|
|
# Auto-register the "Check Machine Number" scheduled task. Bays imaged with
|
|
# the 9999 placeholder will prompt the first ShopFloor end-user logon to
|
|
# enter the real machine number; on success Update-MachineNumber.ps1 pulls
|
|
# the per-machine NTLARS .reg + UDC settings JSON + UDC data backup from
|
|
# SFLD and the task self-unregisters. Self-disables once the number is
|
|
# real, so safe to always register here.
|
|
# Skipped for self-contained types (Display) that have no machine number.
|
|
$registerCheckMN = Join-Path $setupDir 'Shopfloor\Register-CheckMachineNumberTask.ps1'
|
|
if ($noEnforceTypes -contains $pcType) {
|
|
Write-Host ""
|
|
Write-Host "=== Skipping Check Machine Number task ($pcType has no machine number) ==="
|
|
} elseif (Test-Path -LiteralPath $registerCheckMN) {
|
|
Write-Host ""
|
|
Write-Host "=== Registering Check Machine Number logon task ==="
|
|
try { & $registerCheckMN } catch { Write-Warning "Check-MachineNumber registration failed: $_" }
|
|
} else {
|
|
Write-Host "Register-CheckMachineNumberTask.ps1 not found (optional) - skipping"
|
|
}
|
|
|
|
# --- Run enrollment (PPKG install) ---
|
|
# Enrollment is the LAST thing we do. Install-ProvisioningPackage triggers
|
|
# an immediate reboot -- everything after this call is unlikely to execute.
|
|
# The sync_intune task is already registered above, so the PPKG reboot
|
|
# can kill us and the chain continues on the next boot.
|
|
# ---- Network-handoff gate (BEFORE PPKG) ----
|
|
# PXE imaging LAN has no DHCP gateway by design. Laptops with WiFi auto-
|
|
# connect to corp SSID and get a default route via WiFi - PPKG/AAD/Intune
|
|
# work fine. Towers without WiFi have ONLY the wired link to PXE LAN ->
|
|
# no default route -> AAD + Intune endpoints unreachable -> enrollment
|
|
# stalls -> re-image required (recurring failure mode observed
|
|
# 2026-05-05). Block run-enrollment until tech provides a usable internet
|
|
# path. Cheap to verify, prevents wasted imaging cycles.
|
|
$hasWifi = [bool](Get-NetAdapter -ErrorAction SilentlyContinue |
|
|
Where-Object { $_.PhysicalMediaType -eq 'Native 802.11' -or $_.MediaType -like '*802.11*' })
|
|
$hasDefaultRoute = [bool](Get-NetRoute -DestinationPrefix '0.0.0.0/0' -ErrorAction SilentlyContinue)
|
|
if (-not $hasWifi -and -not $hasDefaultRoute) {
|
|
Write-Host ""
|
|
Write-Host "================================================================" -ForegroundColor Red
|
|
Write-Host " STOP - NO USABLE INTERNET PATH" -ForegroundColor Red
|
|
Write-Host "================================================================" -ForegroundColor Red
|
|
Write-Host ""
|
|
Write-Host " This PC has no WiFi adapter and no default route." -ForegroundColor Yellow
|
|
Write-Host " Currently on the PXE imaging LAN, which has no gateway." -ForegroundColor Yellow
|
|
Write-Host " PPKG enrollment WILL fail because AAD + Intune endpoints" -ForegroundColor Yellow
|
|
Write-Host " are unreachable from this network." -ForegroundColor Yellow
|
|
Write-Host ""
|
|
Write-Host " FIX: Plug this PC into a corp wall jack now." -ForegroundColor Cyan
|
|
Write-Host ""
|
|
Write-Host " Verify with: ipconfig" -ForegroundColor Cyan
|
|
Write-Host " A non-blank Default Gateway must show on the wired NIC." -ForegroundColor Cyan
|
|
Write-Host ""
|
|
Write-Host " Press R to retry after moving the cable." -ForegroundColor Cyan
|
|
Write-Host " Press X to abort imaging (no enrollment runs)." -ForegroundColor Cyan
|
|
Write-Host ""
|
|
while ($true) {
|
|
try { $key = ([Console]::ReadKey($true).KeyChar.ToString()).ToUpper() }
|
|
catch { $key = (Read-Host 'Press R or X').Trim().ToUpper() }
|
|
if ($key -eq 'X') {
|
|
Write-Host "Aborted by tech. Imaging stopped before PPKG." -ForegroundColor Yellow
|
|
try { Stop-Transcript | Out-Null } catch {}
|
|
exit 1
|
|
}
|
|
if ($key -eq 'R') {
|
|
$hasDefaultRoute = [bool](Get-NetRoute -DestinationPrefix '0.0.0.0/0' -ErrorAction SilentlyContinue)
|
|
if ($hasDefaultRoute) {
|
|
Write-Host "Default route detected. Continuing to PPKG enrollment." -ForegroundColor Green
|
|
break
|
|
}
|
|
Write-Host "Still no default route. Verify cable + corp jack." -ForegroundColor Red
|
|
}
|
|
}
|
|
}
|
|
|
|
$enrollScript = Join-Path $enrollDir 'run-enrollment.ps1'
|
|
if (Test-Path -LiteralPath $enrollScript) {
|
|
Write-Host ""
|
|
Report-Stage -Stage 'Run-ShopfloorSetup: PPKG enrollment' -Index 5
|
|
Write-Host "=== Running enrollment (PPKG install) ==="
|
|
Write-Host "NOTE: PPKG schedules a near-immediate reboot. We will cancel"
|
|
Write-Host " it and hand off to Monitor-IntuneProgress -PostPpkg, which"
|
|
Write-Host " runs a 120s settle (giving MDM time to push baseline"
|
|
Write-Host " policy) and then performs a clean reboot."
|
|
try { Stop-Transcript | Out-Null } catch {}
|
|
& $enrollScript
|
|
|
|
Write-Host ""
|
|
Report-Stage -Stage 'Run-ShopfloorSetup: handoff to Monitor-IntuneProgress' -Index 6
|
|
Write-Host "=== Handing off to Monitor-IntuneProgress -PostPpkg ==="
|
|
cmd /c "shutdown /a 2>nul" | Out-Null
|
|
# Made it past all the reboot-prone vendor installers. Clear the
|
|
# self-resume RunOnce so a normal completion + reboot does not re-fire
|
|
# this script post-PPKG (PPKG install owns the reboot chain from here).
|
|
try { Remove-ItemProperty -Path $selfResumeKey -Name $selfResumeName -ErrorAction SilentlyContinue } catch {}
|
|
$monitor = Join-Path $setupDir 'Shopfloor\lib\Monitor-IntuneProgress.ps1'
|
|
if (Test-Path -LiteralPath $monitor) {
|
|
& powershell.exe -NoProfile -ExecutionPolicy Bypass -File $monitor -PostPpkg
|
|
} else {
|
|
Write-Warning "Monitor-IntuneProgress.ps1 not found at $monitor - falling back to plain reboot"
|
|
shutdown /r /t 10
|
|
}
|
|
} else {
|
|
Write-Host "run-enrollment.ps1 not found - skipping enrollment."
|
|
Write-Host ""
|
|
Write-Host "================================================================"
|
|
Write-Host "=== Run-ShopfloorSetup.ps1 complete $(Get-Date -Format 'yyyy-MM-dd HH:mm:ss') ==="
|
|
Write-Host "================================================================"
|
|
try { Remove-ItemProperty -Path $selfResumeKey -Name $selfResumeName -ErrorAction SilentlyContinue } catch {}
|
|
try { Stop-Transcript | Out-Null } catch {}
|
|
Write-Host "Rebooting in 10 seconds..."
|
|
shutdown /r /t 10
|
|
}
|