Replace all Unicode characters with ASCII in playbook scripts

Em dashes (U+2014) and arrows (U+2192) break PowerShell 5.1 on
Windows when the file has no UTF-8 BOM -- byte 0x94 gets read as
a right double quote in Windows-1252, silently closing strings
mid-parse. This caused run-enrollment.ps1 to fail on PXE-imaged
machines with "string is missing the terminator" at line 113.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
cproudlock
2026-04-10 13:23:11 -04:00
parent fb5841eb20
commit c06310f5bd
7 changed files with 321 additions and 275 deletions

View File

@@ -1,4 +1,4 @@
# 01-Setup-Display.ps1 Display-specific setup (runs after Shopfloor baseline) # 01-Setup-Display.ps1 -- Display-specific setup (runs after Shopfloor baseline)
# Reads display-type.txt to install either LobbyDisplay or Dashboard kiosk app. # Reads display-type.txt to install either LobbyDisplay or Dashboard kiosk app.
$enrollDir = "C:\Enrollment" $enrollDir = "C:\Enrollment"

View File

@@ -1,221 +1,219 @@
# Run-ShopfloorSetup.ps1 - Dispatcher for shopfloor PC type setup # Run-ShopfloorSetup.ps1 - Main dispatcher for shopfloor PC type setup.
# Runs Shopfloor baseline scripts first, then type-specific scripts on top. #
# Flow:
param( # 1. PreInstall apps (Oracle, VC++, OpenText, UDC) -- from local bundle
# Stage-Dispatcher.ps1 passes -FromDispatcher to bypass the stage-file # 2. Type-specific scripts (eDNC, ACLs, CMM share apps, etc.)
# gate below. When called by the unattend's FirstLogonCommands (no flag), # 3. Deferred baseline (desktop org, taskbar pins)
# the gate defers to the dispatcher if a stage file exists. # 4. Copy desktop tools (sync_intune, Configure-PC, Set-MachineNumber)
[switch]$FromDispatcher # 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
# --- Stage-file gate --- #
# If run-enrollment.ps1 wrote a stage file, the imaging chain is managed by # Called by the unattend FirstLogonCommands as SupportUser (admin).
# Stage-Dispatcher.ps1 via RunOnce. Exit immediately so the FirstLogonCommands
# chain finishes, the PPKG reboot fires, and the dispatcher takes over on # --- Transcript logging ---
# the next boot. Without this gate, the unattend's FirstLogonCommands runs $logDir = 'C:\Logs\SFLD'
# this script right after run-enrollment in the same session (before the if (-not (Test-Path $logDir)) {
# PPKG reboot), bypassing the entire staged chain. try { New-Item -ItemType Directory -Path $logDir -Force | Out-Null } catch { $logDir = $env:TEMP }
if (-not $FromDispatcher) { }
$stageFile = 'C:\Enrollment\setup-stage.txt' $transcriptPath = Join-Path $logDir 'shopfloor-setup.log'
if (Test-Path -LiteralPath $stageFile) { try { Start-Transcript -Path $transcriptPath -Append -Force | Out-Null } catch {}
$stage = (Get-Content -LiteralPath $stageFile -First 1 -ErrorAction SilentlyContinue)
Write-Host "Stage file found ($stage) - deferring to Stage-Dispatcher.ps1 on next logon." Write-Host ""
exit 0 Write-Host "================================================================"
} Write-Host "=== Run-ShopfloorSetup.ps1 starting $(Get-Date -Format 'yyyy-MM-dd HH:mm:ss') ==="
} Write-Host " Transcript: $transcriptPath"
Write-Host "================================================================"
# --- Transcript logging --- Write-Host ""
# Captures everything the dispatcher and all child scripts write to host so
# we can diagnose setup failures after the fact. -Append + -Force so repeat # Bump AutoLogonCount HIGH at the start so reboots during setup (e.g. VC++ 2008
# invocations (e.g. after a reboot mid-setup) accumulate instead of clobbering. # triggering an immediate ExitWindowsEx) don't exhaust autologin attempts before
$logDir = 'C:\Logs\SFLD' # the dispatcher can complete. The end-of-script reset puts it back to 2 once
if (-not (Test-Path $logDir)) { # everything succeeds.
try { New-Item -ItemType Directory -Path $logDir -Force | Out-Null } catch { $logDir = $env:TEMP } reg add "HKLM\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Winlogon" /v AutoLogonCount /t REG_DWORD /d 99 /f | Out-Null
}
$transcriptPath = Join-Path $logDir 'shopfloor-setup.log' # Cancel any pending reboot so it doesn't interrupt setup
try { Start-Transcript -Path $transcriptPath -Append -Force | Out-Null } catch {} cmd /c "shutdown /a 2>nul" *>$null
Write-Host "" # Prompt user to unplug from PXE switch before re-enabling wired adapters
Write-Host "================================================================" Write-Host ""
Write-Host "=== Run-ShopfloorSetup.ps1 starting $(Get-Date -Format 'yyyy-MM-dd HH:mm:ss') ===" Write-Host "========================================" -ForegroundColor Yellow
Write-Host " Transcript: $transcriptPath" Write-Host " UNPLUG the ethernet cable from the" -ForegroundColor Yellow
Write-Host "================================================================" Write-Host " PXE imaging switch NOW." -ForegroundColor Yellow
Write-Host "" Write-Host "========================================" -ForegroundColor Yellow
Write-Host ""
# Bump AutoLogonCount HIGH at the start so reboots during setup (e.g. VC++ 2008 Write-Host "Press any key to continue..." -ForegroundColor Yellow
# triggering an immediate ExitWindowsEx) don't exhaust autologin attempts before $null = $Host.UI.RawUI.ReadKey("NoEcho,IncludeKeyDown")
# the dispatcher can complete. The end-of-script reset puts it back to 2 once
# everything succeeds. # Re-enable wired adapters
reg add "HKLM\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Winlogon" /v AutoLogonCount /t REG_DWORD /d 99 /f | Out-Null Get-NetAdapter -Physical | Where-Object { $_.InterfaceDescription -notmatch 'Wi-Fi|Wireless' } | Enable-NetAdapter -Confirm:$false -ErrorAction SilentlyContinue
# Cancel any pending reboot so it doesn't interrupt setup $enrollDir = "C:\Enrollment"
cmd /c "shutdown /a 2>nul" *>$null $typeFile = Join-Path $enrollDir "pc-type.txt"
$setupDir = Join-Path $enrollDir "shopfloor-setup"
# Prompt user to unplug from PXE switch before re-enabling wired adapters
Write-Host "" if (-not (Test-Path $typeFile)) {
Write-Host "========================================" -ForegroundColor Yellow Write-Host "No pc-type.txt found - skipping shopfloor setup."
Write-Host " UNPLUG the ethernet cable from the" -ForegroundColor Yellow exit 0
Write-Host " PXE imaging switch NOW." -ForegroundColor Yellow }
Write-Host "========================================" -ForegroundColor Yellow
Write-Host "" $pcType = (Get-Content $typeFile -First 1).Trim()
Write-Host "Press any key to continue..." -ForegroundColor Yellow if (-not $pcType) {
$null = $Host.UI.RawUI.ReadKey("NoEcho,IncludeKeyDown") Write-Host "pc-type.txt is empty - skipping shopfloor setup."
exit 0
# Re-enable wired adapters }
Get-NetAdapter -Physical | Where-Object { $_.InterfaceDescription -notmatch 'Wi-Fi|Wireless' } | Enable-NetAdapter -Confirm:$false -ErrorAction SilentlyContinue
Write-Host "Shopfloor PC Type: $pcType"
$enrollDir = "C:\Enrollment"
$typeFile = Join-Path $enrollDir "pc-type.txt" # Scripts to skip in the alphabetical baseline loop. Each is either run
$setupDir = Join-Path $enrollDir "shopfloor-setup" # explicitly in the finalization phase below, or invoked internally by
# another script:
if (-not (Test-Path $typeFile)) { #
Write-Host "No pc-type.txt found - skipping shopfloor setup." # 05 - Office shortcuts. Invoked by 06 as Phase 0. Office isn't installed
exit 0 # 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
$pcType = (Get-Content $typeFile -First 1).Trim() # type-specific 01-eDNC.ps1). Run in finalization phase.
if (-not $pcType) { # 07 - Taskbar pin layout. Reads 06's output. Run in finalization phase.
Write-Host "pc-type.txt is empty - skipping shopfloor setup." # 08 - Edge default browser + startup tabs. Invoked by 06 as Phase 4.
exit 0 # Reads .url files delivered by DSC (after Intune enrollment). 06's
} # SYSTEM logon task re-runs it to pick them up.
$skipInBaseline = @(
Write-Host "Shopfloor PC Type: $pcType" '05-OfficeShortcuts.ps1',
'06-OrganizeDesktop.ps1',
# Scripts to skip in the alphabetical baseline loop. Each is either run '07-TaskbarLayout.ps1',
# explicitly in the finalization phase below, or invoked internally by '08-EdgeDefaultBrowser.ps1',
# another script: 'Check-MachineNumber.ps1',
# 'Configure-PC.ps1'
# 05 - Office shortcuts. Invoked by 06 as Phase 0. Office isn't installed )
# until after the first reboot, so 05 no-ops on the imaging run.
# 06's SYSTEM logon task re-runs it on the second boot. # Scripts run AFTER type-specific scripts complete. 05 and 08 are NOT
# 06 - Desktop org. Phase 2 needs eDNC/NTLARS on disk (installed by # here because 06 calls them internally as sub-phases.
# type-specific 01-eDNC.ps1). Run in finalization phase. $runAfterTypeSpecific = @(
# 07 - Taskbar pin layout. Reads 06's output. Run in finalization phase. '06-OrganizeDesktop.ps1',
# 08 - Edge default browser + startup tabs. Invoked by 06 as Phase 4. '07-TaskbarLayout.ps1'
# Reads .url files delivered by DSC (after setup reboots). 06's )
# SYSTEM logon task re-runs it to pick them up.
$skipInBaseline = @( # --- Run Shopfloor baseline scripts first (skipping deferred ones) ---
'05-OfficeShortcuts.ps1', $baselineDir = Join-Path $setupDir "Shopfloor"
'06-OrganizeDesktop.ps1', if (Test-Path $baselineDir) {
'07-TaskbarLayout.ps1', $scripts = Get-ChildItem -Path $baselineDir -Filter "*.ps1" -File | Sort-Object Name
'08-EdgeDefaultBrowser.ps1', foreach ($script in $scripts) {
'Check-MachineNumber.ps1', if ($skipInBaseline -contains $script.Name) {
'Configure-PC.ps1' Write-Host "Skipping baseline: $($script.Name) (runs in finalization phase)"
) continue
}
# Scripts run AFTER type-specific scripts complete. 05 and 08 are NOT cmd /c "shutdown /a 2>nul" *>$null
# here because 06 calls them internally as sub-phases. Write-Host "Running baseline: $($script.Name)"
$runAfterTypeSpecific = @( try {
'06-OrganizeDesktop.ps1', & $script.FullName
'07-TaskbarLayout.ps1' } catch {
) Write-Warning "Baseline script $($script.Name) failed: $_"
}
# --- 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 # --- Run type-specific scripts (if not just baseline Shopfloor) ---
foreach ($script in $scripts) { if ($pcType -ne "Shopfloor") {
if ($skipInBaseline -contains $script.Name) { $typeDir = Join-Path $setupDir $pcType
Write-Host "Skipping baseline: $($script.Name) (runs in finalization phase)" if (Test-Path $typeDir) {
continue # Only run numbered scripts (01-eDNC.ps1, 02-MachineNumberACLs.ps1).
} # Unnumbered .ps1 files (Set-MachineNumber.ps1) are desktop tools,
cmd /c "shutdown /a 2>nul" *>$null # not installer scripts, and must not auto-run during setup.
Write-Host "Running baseline: $($script.Name)" $scripts = Get-ChildItem -Path $typeDir -Filter "*.ps1" -File |
try { Where-Object { $_.Name -match '^\d' } |
& $script.FullName Sort-Object Name
} catch { foreach ($script in $scripts) {
Write-Warning "Baseline script $($script.Name) failed: $_" cmd /c "shutdown /a 2>nul" *>$null
} Write-Host "Running $pcType setup: $($script.Name)"
} try {
} & $script.FullName
} catch {
# --- Run type-specific scripts (if not just baseline Shopfloor) --- Write-Warning "Script $($script.Name) failed: $_"
if ($pcType -ne "Shopfloor") { }
$typeDir = Join-Path $setupDir $pcType }
if (Test-Path $typeDir) { } else {
# Only run numbered scripts (01-eDNC.ps1, 02-MachineNumberACLs.ps1). Write-Host "No type-specific scripts found for $pcType."
# 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' } | # --- Finalization: run deferred baseline scripts (desktop org, taskbar pins)
Sort-Object Name # ---
foreach ($script in $scripts) { # These needed to wait until all apps (eDNC, NTLARS, UDC, OpenText) were
cmd /c "shutdown /a 2>nul" *>$null # installed by the baseline + type-specific phases above. 06 internally
Write-Host "Running $pcType setup: $($script.Name)" # calls 05 (Office shortcuts) and 08 (Edge config) as sub-phases, so we
try { # only need to invoke 06 and 07 explicitly here.
& $script.FullName foreach ($name in $runAfterTypeSpecific) {
} catch { $script = Join-Path $baselineDir $name
Write-Warning "Script $($script.Name) failed: $_" if (-not (Test-Path $script)) {
} Write-Warning "Deferred script not found: $script"
} continue
} else { }
Write-Host "No type-specific scripts found for $pcType." cmd /c "shutdown /a 2>nul" *>$null
} Write-Host "Running deferred baseline: $name"
} try {
& $script
# --- Finalization: run deferred baseline scripts (desktop org, taskbar pins) } catch {
# --- Write-Warning "Deferred script $name failed: $_"
# 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. Write-Host "Shopfloor setup complete for $pcType."
foreach ($name in $runAfterTypeSpecific) {
$script = Join-Path $baselineDir $name # --- Copy utility scripts to SupportUser desktop ---
if (-not (Test-Path $script)) { foreach ($tool in @('sync_intune.bat', 'Configure-PC.bat')) {
Write-Warning "Deferred script not found: $script" $src = Join-Path $setupDir "Shopfloor\$tool"
continue if (Test-Path $src) {
} Copy-Item -Path $src -Destination "C:\Users\SupportUser\Desktop\$tool" -Force
cmd /c "shutdown /a 2>nul" *>$null Write-Host "$tool copied to desktop."
Write-Host "Running deferred baseline: $name" }
try { }
& $script
} catch { # Standard PCs get the UDC/eDNC machine number helper
Write-Warning "Deferred script $name failed: $_" if ($pcType -eq "Standard") {
} foreach ($helper in @("Set-MachineNumber.bat", "Set-MachineNumber.ps1")) {
} $src = Join-Path $setupDir "Standard\$helper"
if (Test-Path $src) {
Write-Host "Shopfloor setup complete for $pcType." Copy-Item -Path $src -Destination "C:\Users\SupportUser\Desktop\$helper" -Force
Write-Host "$helper copied to SupportUser desktop."
# Copy utility scripts to SupportUser desktop }
foreach ($tool in @('sync_intune.bat', 'Configure-PC.bat')) { }
$src = Join-Path $setupDir "Shopfloor\$tool" }
if (Test-Path $src) {
Copy-Item -Path $src -Destination "C:\Users\SupportUser\Desktop\$tool" -Force # --- Run enrollment (PPKG install) ---
Write-Host "$tool copied to desktop." # Enrollment runs AFTER all our apps are installed. The PPKG installs
} # Chrome, Office, CyberArk, Tanium, etc. and needs a reboot for file
} # operations (Zscaler rename, PPKG cleanup). run-enrollment.ps1 waits
# for all PPKG steps to complete, registers sync_intune as a persistent
# Standard PCs get the UDC/eDNC machine number helper # @logon scheduled task, then reboots.
if ($pcType -eq "Standard") { $enrollScript = Join-Path $enrollDir 'run-enrollment.ps1'
foreach ($helper in @("Set-MachineNumber.bat", "Set-MachineNumber.ps1")) { if (Test-Path -LiteralPath $enrollScript) {
$src = Join-Path $setupDir "Standard\$helper" Write-Host ""
if (Test-Path $src) { Write-Host "=== Running enrollment (PPKG install) ==="
Copy-Item -Path $src -Destination "C:\Users\SupportUser\Desktop\$helper" -Force try {
Write-Host "$helper copied to SupportUser desktop." & $enrollScript
} } catch {
} Write-Warning "run-enrollment.ps1 failed: $_"
} }
} else {
# Set auto-logon to expire after 2 more logins Write-Host "run-enrollment.ps1 not found - skipping enrollment."
reg add "HKLM\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Winlogon" /v AutoLogonCount /t REG_DWORD /d 2 /f | Out-Null }
Write-Host "Auto-logon set to 2 remaining logins."
# Set auto-logon to expire after 2 more logins
Write-Host "" reg add "HKLM\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Winlogon" /v AutoLogonCount /t REG_DWORD /d 2 /f | Out-Null
Write-Host "================================================================" Write-Host "Auto-logon set to 2 remaining logins."
Write-Host "=== Run-ShopfloorSetup.ps1 complete $(Get-Date -Format 'yyyy-MM-dd HH:mm:ss') ==="
Write-Host "================================================================" Write-Host ""
Write-Host "================================================================"
# Flush transcript before shutdown so the log file is complete on next boot Write-Host "=== Run-ShopfloorSetup.ps1 complete $(Get-Date -Format 'yyyy-MM-dd HH:mm:ss') ==="
try { Stop-Transcript | Out-Null } catch {} Write-Host "================================================================"
if ($FromDispatcher) { # Flush transcript before shutdown so the log file is complete on next boot
# Dispatcher owns the reboot — it cancels ours and reboots on its own try { Stop-Transcript | Out-Null } catch {}
# terms after advancing the stage and re-registering RunOnce. We still
# schedule one as a safety net (dispatcher cancels it immediately). # run-enrollment.ps1 already initiated the reboot. If it didn't run
Write-Host "Returning to Stage-Dispatcher for reboot." # (no PPKG), reboot now.
shutdown /r /t 30 if (-not (Test-Path -LiteralPath $enrollScript)) {
} else { Write-Host "Rebooting in 10 seconds..."
# Standalone run (manual or legacy FirstLogonCommands) — reboot directly. shutdown /r /t 10
Write-Host "Rebooting in 10 seconds..." }
shutdown /r /t 10
}

View File

@@ -1,4 +1,4 @@
# 04-NetworkAndWinRM.ps1 Set network profiles to Private and enable WinRM (baseline) # 04-NetworkAndWinRM.ps1 -- Set network profiles to Private and enable WinRM (baseline)
# --- Transcript --- # --- Transcript ---
$logDir = 'C:\Logs\SFLD' $logDir = 'C:\Logs\SFLD'

View File

@@ -336,7 +336,7 @@ function Add-ShopfloorToolsApps {
$src = Find-ExistingLnk $app.SourceName $src = Find-ExistingLnk $app.SourceName
if ($src) { if ($src) {
# Skip if the sweep already moved the source into the # Skip if the sweep already moved the source into the
# destination folder (src == dest "cannot overwrite # destination folder (src == dest -> "cannot overwrite
# the item with itself"). # the item with itself").
if ((Resolve-Path -LiteralPath $src -ErrorAction SilentlyContinue).Path -eq if ((Resolve-Path -LiteralPath $src -ErrorAction SilentlyContinue).Path -eq
(Join-Path $shopfloorToolsDir $app.SourceName)) { (Join-Path $shopfloorToolsDir $app.SourceName)) {
@@ -390,7 +390,7 @@ function Remove-EmptyCategoryFolders {
# are delivered by DSC AFTER this initial shopfloor setup, so the first # are delivered by DSC AFTER this initial shopfloor setup, so the first
# run at imaging time won't find them (falls back to hardcoded URLs, # run at imaging time won't find them (falls back to hardcoded URLs,
# Plant Apps gets skipped). By calling 08 from inside 06, every SYSTEM # Plant Apps gets skipped). By calling 08 from inside 06, every SYSTEM
# scheduled-task logon re-run of 06 also re-runs 08 so after DSC drops # scheduled-task logon re-run of 06 also re-runs 08 -- so after DSC drops
# the .url files and the next sweep files them into Web Links\, 08 # the .url files and the next sweep files them into Web Links\, 08
# picks them up and updates the Edge policy. Self-healing. # picks them up and updates the Edge policy. Self-healing.
# ============================================================================ # ============================================================================

View File

@@ -52,12 +52,17 @@
param( param(
[int]$PollSecs = 30, [int]$PollSecs = 30,
[int]$RetriggerMinutes = 5, [int]$RetriggerMinutes = 5,
# When set, the monitor exits with a status code instead of prompting # -Unattended: exit with status code instead of prompting/rebooting.
# or rebooting. Used by Stage-Dispatcher.ps1 to keep control of the # Legacy flag kept for backward compatibility with manual testing.
# imaging chain. Manual sync_intune.bat usage (no flag) stays interactive. [switch]$Unattended,
# exit 0 = post-reboot install complete, all milestones reached # -AsTask: runs as a persistent @logon scheduled task. On pre-reboot
# exit 2 = pre-reboot deployment done, reboot needed # complete: reboots directly. On post-reboot complete: unregisters own
[switch]$Unattended # task, launches ConfigureScript, exits. This is the primary mode for
# the imaging chain (registered by run-enrollment.ps1).
[switch]$AsTask,
# Path to Configure-PC.ps1, launched after post-reboot completion in
# -AsTask mode. Passed by the scheduled task's -ArgumentList.
[string]$ConfigureScript = ''
) )
# ============================================================================ # ============================================================================
@@ -554,8 +559,22 @@ function Invoke-SetupComplete {
Write-Host "" Write-Host ""
Write-Host "The post-reboot DSC install phase is finished. The device is ready." Write-Host "The post-reboot DSC install phase is finished. The device is ready."
if ($AsTask) {
# Task mode: unregister our own scheduled task, launch Configure-PC
Write-Host "Unregistering sync task..." -ForegroundColor Cyan
try {
Unregister-ScheduledTask -TaskName 'Shopfloor Intune Sync' -Confirm:$false -ErrorAction SilentlyContinue
} catch {}
if ($ConfigureScript -and (Test-Path -LiteralPath $ConfigureScript)) {
Write-Host "Launching Configure-PC..." -ForegroundColor Cyan
try { & $ConfigureScript } catch { Write-Warning "Configure-PC failed: $_" }
}
exit 0
}
if ($Unattended) { if ($Unattended) {
Write-Host "(Unattended mode - returning to dispatcher)" Write-Host "(Unattended mode - exiting)"
exit 0 exit 0
} }
Wait-ForAnyKey Wait-ForAnyKey
@@ -574,11 +593,13 @@ function Invoke-RebootPrompt {
Write-Host "Install-UDC, Install-VCRedists, Install-OpenText, and so on." Write-Host "Install-UDC, Install-VCRedists, Install-OpenText, and so on."
Write-Host "" Write-Host ""
if ($Unattended) { if ($AsTask -or $Unattended) {
# Dispatcher mode: exit with code 2 so the dispatcher can set # Task/unattended mode: reboot directly, no prompt.
# RunOnce before initiating the reboot itself. # The task persists across the reboot -- it fires again on next
Write-Host "Pre-reboot complete. Returning to dispatcher (exit 2)." -ForegroundColor Yellow # logon and picks up the post-reboot monitoring phase.
exit 2 Write-Host "Pre-reboot complete. Rebooting in 5 seconds..." -ForegroundColor Yellow
shutdown /r /t 5
exit 0
} }
# Interactive mode (manual sync_intune.bat): prompt and reboot directly. # Interactive mode (manual sync_intune.bat): prompt and reboot directly.

View File

@@ -1,6 +1,11 @@
# run-enrollment.ps1 # run-enrollment.ps1
# Installs GCCH enrollment provisioning package via Install-ProvisioningPackage # Installs GCCH enrollment provisioning package, waits for all PPKG apps
# Called by FirstLogonCommands as SupportUser (admin) after imaging # to finish installing, registers sync_intune as a persistent @logon task,
# then reboots.
#
# Called by Run-ShopfloorSetup.ps1 AFTER all PreInstall + type-specific
# apps are already installed (not as a FirstLogonCommand -- that was the
# old flow).
$ErrorActionPreference = 'Continue' $ErrorActionPreference = 'Continue'
$logFile = "C:\Logs\enrollment.log" $logFile = "C:\Logs\enrollment.log"
@@ -19,8 +24,8 @@ Log "=== GE Aerospace GCCH Enrollment ==="
# --- Find the .ppkg --- # --- Find the .ppkg ---
$ppkgFile = Get-ChildItem "C:\Enrollment\*.ppkg" -ErrorAction SilentlyContinue | Select-Object -First 1 $ppkgFile = Get-ChildItem "C:\Enrollment\*.ppkg" -ErrorAction SilentlyContinue | Select-Object -First 1
if (-not $ppkgFile) { if (-not $ppkgFile) {
Log "ERROR: No .ppkg found in C:\Enrollment\" Log "No .ppkg found in C:\Enrollment\ - skipping enrollment."
exit 1 return
} }
Log "Package: $($ppkgFile.Name)" Log "Package: $($ppkgFile.Name)"
@@ -43,28 +48,25 @@ try {
Log "Provisioning package added successfully (fallback)." Log "Provisioning package added successfully (fallback)."
} catch { } catch {
Log "ERROR: Fallback also failed: $_" Log "ERROR: Fallback also failed: $_"
exit 1 return
} }
} }
# --- Wait for PPKG provisioning to finish --- # --- Wait for PPKG provisioning to finish ---
# Install-ProvisioningPackage is async it queues the provisioning engine # Install-ProvisioningPackage is async -- it queues the provisioning engine
# and returns immediately. The actual app installs (Chrome, Office, Tanium, # and returns immediately. The actual app installs (Chrome, Office, Tanium,
# CyberArk, etc.) run in the background as BPRT steps. Each step writes a # CyberArk, etc.) run in the background as BPRT steps. "Remove Staging
# Log.txt under C:\Logs\BPRT\<step name>\. "Remove Staging Locations" is # Locations" is the LAST step -- when its Log.txt exists, all provisioning
# the LAST step — when its Log.txt exists, all provisioning is done. # is done.
# We poll for that marker before writing the stage file, so the PPKG reboot
# doesn't fire while installs are still in progress.
$bprtMarker = 'C:\Logs\BPRT\Remove Staging Locations\Log.txt' $bprtMarker = 'C:\Logs\BPRT\Remove Staging Locations\Log.txt'
$maxWait = 900 # 15 minutes (Office install can be slow) $maxWait = 900 # 15 minutes (Office install can be slow)
$pollInterval = 10 $pollInterval = 10
$elapsed = 0 $elapsed = 0
Log "Waiting for PPKG provisioning to complete (polling for $bprtMarker)..." Log "Waiting for PPKG provisioning to complete..."
while (-not (Test-Path -LiteralPath $bprtMarker) -and $elapsed -lt $maxWait) { while (-not (Test-Path -LiteralPath $bprtMarker) -and $elapsed -lt $maxWait) {
Start-Sleep -Seconds $pollInterval Start-Sleep -Seconds $pollInterval
$elapsed += $pollInterval $elapsed += $pollInterval
# Show progress every 30 seconds
if ($elapsed % 30 -eq 0) { if ($elapsed % 30 -eq 0) {
$completedSteps = @(Get-ChildItem 'C:\Logs\BPRT' -Directory -ErrorAction SilentlyContinue | $completedSteps = @(Get-ChildItem 'C:\Logs\BPRT' -Directory -ErrorAction SilentlyContinue |
Where-Object { Test-Path (Join-Path $_.FullName 'Log.txt') } | Where-Object { Test-Path (Join-Path $_.FullName 'Log.txt') } |
@@ -75,11 +77,8 @@ while (-not (Test-Path -LiteralPath $bprtMarker) -and $elapsed -lt $maxWait) {
if (Test-Path -LiteralPath $bprtMarker) { if (Test-Path -LiteralPath $bprtMarker) {
Log "PPKG provisioning complete after $elapsed s." Log "PPKG provisioning complete after $elapsed s."
$allSteps = @(Get-ChildItem 'C:\Logs\BPRT' -Directory -ErrorAction SilentlyContinue | Select-Object -ExpandProperty Name)
Log " Completed steps: $($allSteps -join ', ')"
} else { } else {
Log "WARNING: PPKG provisioning timeout after $maxWait s — some apps may not be installed." Log "WARNING: PPKG provisioning timeout after $maxWait s."
Log " Proceeding anyway. The SYSTEM logon task will retry incomplete items on next boot."
} }
# --- Set OOBE complete --- # --- Set OOBE complete ---
@@ -87,27 +86,56 @@ Log "Setting OOBE as complete..."
reg add "HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\Setup\OOBE" /v OOBEComplete /t REG_DWORD /d 1 /f | Out-Null reg add "HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\Setup\OOBE" /v OOBEComplete /t REG_DWORD /d 1 /f | Out-Null
reg add "HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\Setup\OOBE" /v SetupDisplayedEula /t REG_DWORD /d 1 /f | Out-Null reg add "HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\Setup\OOBE" /v SetupDisplayedEula /t REG_DWORD /d 1 /f | Out-Null
# --- Stage the imaging chain for next boot --- # --- Register sync_intune as persistent @logon scheduled task ---
# The PPKG schedules a reboot (PendingFileRenameOperations for Zscaler # sync_intune monitors the Intune enrollment lifecycle (5-phase status
# rename, PPKG self-cleanup, etc). Instead of canceling it and cramming # table). It stays registered as a logon task until:
# Run-ShopfloorSetup into this same session, we let the reboot happen # - Pre-reboot: monitors until Phase 1+2+3 done -> reboots
# and register a RunOnce entry that fires Stage-Dispatcher.ps1 on the # - Post-reboot: monitors until DSC install complete -> unregisters
# next autologon. The dispatcher reads setup-stage.txt and chains # itself, launches Configure-PC
# through: shopfloor-setup -> sync-intune -> configure-pc. #
$stageFile = 'C:\Enrollment\setup-stage.txt' # Runs as BUILTIN\Users (logged-in user) so it can show GUI (QR code,
$dispatcherPath = 'C:\Enrollment\Stage-Dispatcher.ps1' # status table). Needs the interactive session, NOT SYSTEM.
$runOnceKey = 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\RunOnce' $taskName = 'Shopfloor Intune Sync'
$monitorScript = 'C:\Enrollment\shopfloor-setup\Shopfloor\lib\Monitor-IntuneProgress.ps1'
$configureScript = 'C:\Enrollment\shopfloor-setup\Shopfloor\Configure-PC.ps1'
Log "Writing stage file: shopfloor-setup" if (Test-Path -LiteralPath $monitorScript) {
Set-Content -LiteralPath $stageFile -Value 'shopfloor-setup' -Force try {
$action = New-ScheduledTaskAction `
-Execute 'powershell.exe' `
-Argument "-NoProfile -NoExit -ExecutionPolicy Bypass -File `"$monitorScript`" -AsTask -ConfigureScript `"$configureScript`""
if (Test-Path -LiteralPath $dispatcherPath) { $trigger = New-ScheduledTaskTrigger -AtLogOn
Log "Registering RunOnce for Stage-Dispatcher.ps1"
Set-ItemProperty -Path $runOnceKey -Name 'ShopfloorSetup' ` $principal = New-ScheduledTaskPrincipal `
-Value "powershell.exe -NoProfile -ExecutionPolicy Bypass -File `"$dispatcherPath`"" ` -GroupId 'S-1-5-32-545' `
-Type String -Force -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
Log "Registered '$taskName' logon task (persists until sync complete)."
} catch {
Log "WARNING: Failed to register sync task: $_"
}
} else { } else {
Log "WARNING: Stage-Dispatcher.ps1 not found at $dispatcherPath - RunOnce not set" Log "WARNING: Monitor-IntuneProgress.ps1 not found at $monitorScript"
} }
Log "=== Enrollment complete. PPKG reboot will fire and Stage-Dispatcher picks up on next logon. ===" Log "=== Enrollment complete. Rebooting... ==="
# Reboot -- PPKG file operations (Zscaler rename, cleanup) happen on next boot.
# sync_intune fires at next logon via the scheduled task.
shutdown /r /t 10

View File

@@ -258,8 +258,7 @@ if errorlevel 1 (
echo WARNING: Failed to copy enrollment package. echo WARNING: Failed to copy enrollment package.
goto copy_pctype goto copy_pctype
) )
copy /Y "Y:\run-enrollment.ps1" "W:\run-enrollment.ps1" copy /Y "Y:\run-enrollment.ps1" "W:\Enrollment\run-enrollment.ps1"
copy /Y "Y:\shopfloor-setup\Stage-Dispatcher.ps1" "W:\Enrollment\Stage-Dispatcher.ps1"
REM --- Create enroll.cmd at drive root as manual fallback --- REM --- Create enroll.cmd at drive root as manual fallback ---
> W:\enroll.cmd ( > W:\enroll.cmd (