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:
@@ -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.
|
||||
|
||||
$enrollDir = "C:\Enrollment"
|
||||
|
||||
@@ -1,33 +1,17 @@
|
||||
# Run-ShopfloorSetup.ps1 - Dispatcher for shopfloor PC type setup
|
||||
# Runs Shopfloor baseline scripts first, then type-specific scripts on top.
|
||||
|
||||
param(
|
||||
# Stage-Dispatcher.ps1 passes -FromDispatcher to bypass the stage-file
|
||||
# gate below. When called by the unattend's FirstLogonCommands (no flag),
|
||||
# the gate defers to the dispatcher if a stage file exists.
|
||||
[switch]$FromDispatcher
|
||||
)
|
||||
|
||||
# --- Stage-file gate ---
|
||||
# If run-enrollment.ps1 wrote a stage file, the imaging chain is managed by
|
||||
# Stage-Dispatcher.ps1 via RunOnce. Exit immediately so the FirstLogonCommands
|
||||
# chain finishes, the PPKG reboot fires, and the dispatcher takes over on
|
||||
# the next boot. Without this gate, the unattend's FirstLogonCommands runs
|
||||
# this script right after run-enrollment in the same session (before the
|
||||
# PPKG reboot), bypassing the entire staged chain.
|
||||
if (-not $FromDispatcher) {
|
||||
$stageFile = 'C:\Enrollment\setup-stage.txt'
|
||||
if (Test-Path -LiteralPath $stageFile) {
|
||||
$stage = (Get-Content -LiteralPath $stageFile -First 1 -ErrorAction SilentlyContinue)
|
||||
Write-Host "Stage file found ($stage) - deferring to Stage-Dispatcher.ps1 on next logon."
|
||||
exit 0
|
||||
}
|
||||
}
|
||||
# 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 ---
|
||||
# Captures everything the dispatcher and all child scripts write to host so
|
||||
# we can diagnose setup failures after the fact. -Append + -Force so repeat
|
||||
# invocations (e.g. after a reboot mid-setup) accumulate instead of clobbering.
|
||||
$logDir = 'C:\Logs\SFLD'
|
||||
if (-not (Test-Path $logDir)) {
|
||||
try { New-Item -ItemType Directory -Path $logDir -Force | Out-Null } catch { $logDir = $env:TEMP }
|
||||
@@ -86,13 +70,13 @@ Write-Host "Shopfloor PC Type: $pcType"
|
||||
# another script:
|
||||
#
|
||||
# 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.
|
||||
# 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 setup reboots). 06's
|
||||
# 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',
|
||||
@@ -176,7 +160,7 @@ foreach ($name in $runAfterTypeSpecific) {
|
||||
|
||||
Write-Host "Shopfloor setup complete for $pcType."
|
||||
|
||||
# Copy utility scripts 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) {
|
||||
@@ -196,6 +180,25 @@ if ($pcType -eq "Standard") {
|
||||
}
|
||||
}
|
||||
|
||||
# --- Run enrollment (PPKG install) ---
|
||||
# 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
|
||||
# @logon scheduled task, then reboots.
|
||||
$enrollScript = Join-Path $enrollDir 'run-enrollment.ps1'
|
||||
if (Test-Path -LiteralPath $enrollScript) {
|
||||
Write-Host ""
|
||||
Write-Host "=== Running enrollment (PPKG install) ==="
|
||||
try {
|
||||
& $enrollScript
|
||||
} catch {
|
||||
Write-Warning "run-enrollment.ps1 failed: $_"
|
||||
}
|
||||
} else {
|
||||
Write-Host "run-enrollment.ps1 not found - skipping enrollment."
|
||||
}
|
||||
|
||||
# Set auto-logon to expire after 2 more logins
|
||||
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."
|
||||
@@ -208,14 +211,9 @@ Write-Host "================================================================"
|
||||
# Flush transcript before shutdown so the log file is complete on next boot
|
||||
try { Stop-Transcript | Out-Null } catch {}
|
||||
|
||||
if ($FromDispatcher) {
|
||||
# Dispatcher owns the reboot — it cancels ours and reboots on its own
|
||||
# terms after advancing the stage and re-registering RunOnce. We still
|
||||
# schedule one as a safety net (dispatcher cancels it immediately).
|
||||
Write-Host "Returning to Stage-Dispatcher for reboot."
|
||||
shutdown /r /t 30
|
||||
} else {
|
||||
# Standalone run (manual or legacy FirstLogonCommands) — reboot directly.
|
||||
# run-enrollment.ps1 already initiated the reboot. If it didn't run
|
||||
# (no PPKG), reboot now.
|
||||
if (-not (Test-Path -LiteralPath $enrollScript)) {
|
||||
Write-Host "Rebooting in 10 seconds..."
|
||||
shutdown /r /t 10
|
||||
}
|
||||
|
||||
@@ -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 ---
|
||||
$logDir = 'C:\Logs\SFLD'
|
||||
|
||||
@@ -336,7 +336,7 @@ function Add-ShopfloorToolsApps {
|
||||
$src = Find-ExistingLnk $app.SourceName
|
||||
if ($src) {
|
||||
# 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").
|
||||
if ((Resolve-Path -LiteralPath $src -ErrorAction SilentlyContinue).Path -eq
|
||||
(Join-Path $shopfloorToolsDir $app.SourceName)) {
|
||||
@@ -390,7 +390,7 @@ function Remove-EmptyCategoryFolders {
|
||||
# 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,
|
||||
# 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
|
||||
# picks them up and updates the Edge policy. Self-healing.
|
||||
# ============================================================================
|
||||
|
||||
@@ -52,12 +52,17 @@
|
||||
param(
|
||||
[int]$PollSecs = 30,
|
||||
[int]$RetriggerMinutes = 5,
|
||||
# When set, the monitor exits with a status code instead of prompting
|
||||
# or rebooting. Used by Stage-Dispatcher.ps1 to keep control of the
|
||||
# imaging chain. Manual sync_intune.bat usage (no flag) stays interactive.
|
||||
# exit 0 = post-reboot install complete, all milestones reached
|
||||
# exit 2 = pre-reboot deployment done, reboot needed
|
||||
[switch]$Unattended
|
||||
# -Unattended: exit with status code instead of prompting/rebooting.
|
||||
# Legacy flag kept for backward compatibility with manual testing.
|
||||
[switch]$Unattended,
|
||||
# -AsTask: runs as a persistent @logon scheduled task. On pre-reboot
|
||||
# complete: reboots directly. On post-reboot complete: unregisters own
|
||||
# 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 "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) {
|
||||
Write-Host "(Unattended mode - returning to dispatcher)"
|
||||
Write-Host "(Unattended mode - exiting)"
|
||||
exit 0
|
||||
}
|
||||
Wait-ForAnyKey
|
||||
@@ -574,11 +593,13 @@ function Invoke-RebootPrompt {
|
||||
Write-Host "Install-UDC, Install-VCRedists, Install-OpenText, and so on."
|
||||
Write-Host ""
|
||||
|
||||
if ($Unattended) {
|
||||
# Dispatcher mode: exit with code 2 so the dispatcher can set
|
||||
# RunOnce before initiating the reboot itself.
|
||||
Write-Host "Pre-reboot complete. Returning to dispatcher (exit 2)." -ForegroundColor Yellow
|
||||
exit 2
|
||||
if ($AsTask -or $Unattended) {
|
||||
# Task/unattended mode: reboot directly, no prompt.
|
||||
# The task persists across the reboot -- it fires again on next
|
||||
# logon and picks up the post-reboot monitoring phase.
|
||||
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.
|
||||
|
||||
@@ -1,6 +1,11 @@
|
||||
# run-enrollment.ps1
|
||||
# Installs GCCH enrollment provisioning package via Install-ProvisioningPackage
|
||||
# Called by FirstLogonCommands as SupportUser (admin) after imaging
|
||||
# Installs GCCH enrollment provisioning package, waits for all PPKG apps
|
||||
# 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'
|
||||
$logFile = "C:\Logs\enrollment.log"
|
||||
@@ -19,8 +24,8 @@ Log "=== GE Aerospace GCCH Enrollment ==="
|
||||
# --- Find the .ppkg ---
|
||||
$ppkgFile = Get-ChildItem "C:\Enrollment\*.ppkg" -ErrorAction SilentlyContinue | Select-Object -First 1
|
||||
if (-not $ppkgFile) {
|
||||
Log "ERROR: No .ppkg found in C:\Enrollment\"
|
||||
exit 1
|
||||
Log "No .ppkg found in C:\Enrollment\ - skipping enrollment."
|
||||
return
|
||||
}
|
||||
Log "Package: $($ppkgFile.Name)"
|
||||
|
||||
@@ -43,28 +48,25 @@ try {
|
||||
Log "Provisioning package added successfully (fallback)."
|
||||
} catch {
|
||||
Log "ERROR: Fallback also failed: $_"
|
||||
exit 1
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
# --- 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,
|
||||
# CyberArk, etc.) run in the background as BPRT steps. Each step writes a
|
||||
# Log.txt under C:\Logs\BPRT\<step name>\. "Remove Staging Locations" is
|
||||
# the LAST step — when its Log.txt exists, all provisioning 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.
|
||||
# CyberArk, etc.) run in the background as BPRT steps. "Remove Staging
|
||||
# Locations" is the LAST step -- when its Log.txt exists, all provisioning
|
||||
# is done.
|
||||
$bprtMarker = 'C:\Logs\BPRT\Remove Staging Locations\Log.txt'
|
||||
$maxWait = 900 # 15 minutes (Office install can be slow)
|
||||
$pollInterval = 10
|
||||
$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) {
|
||||
Start-Sleep -Seconds $pollInterval
|
||||
$elapsed += $pollInterval
|
||||
# Show progress every 30 seconds
|
||||
if ($elapsed % 30 -eq 0) {
|
||||
$completedSteps = @(Get-ChildItem 'C:\Logs\BPRT' -Directory -ErrorAction SilentlyContinue |
|
||||
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) {
|
||||
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 {
|
||||
Log "WARNING: PPKG provisioning timeout after $maxWait s — some apps may not be installed."
|
||||
Log " Proceeding anyway. The SYSTEM logon task will retry incomplete items on next boot."
|
||||
Log "WARNING: PPKG provisioning timeout after $maxWait s."
|
||||
}
|
||||
|
||||
# --- 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 SetupDisplayedEula /t REG_DWORD /d 1 /f | Out-Null
|
||||
|
||||
# --- Stage the imaging chain for next boot ---
|
||||
# The PPKG schedules a reboot (PendingFileRenameOperations for Zscaler
|
||||
# rename, PPKG self-cleanup, etc). Instead of canceling it and cramming
|
||||
# Run-ShopfloorSetup into this same session, we let the reboot happen
|
||||
# and register a RunOnce entry that fires Stage-Dispatcher.ps1 on the
|
||||
# next autologon. The dispatcher reads setup-stage.txt and chains
|
||||
# through: shopfloor-setup -> sync-intune -> configure-pc.
|
||||
$stageFile = 'C:\Enrollment\setup-stage.txt'
|
||||
$dispatcherPath = 'C:\Enrollment\Stage-Dispatcher.ps1'
|
||||
$runOnceKey = 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\RunOnce'
|
||||
# --- Register sync_intune as persistent @logon scheduled task ---
|
||||
# sync_intune monitors the Intune enrollment lifecycle (5-phase status
|
||||
# table). It stays registered as a logon task until:
|
||||
# - Pre-reboot: monitors until Phase 1+2+3 done -> reboots
|
||||
# - Post-reboot: monitors until DSC install complete -> unregisters
|
||||
# itself, launches Configure-PC
|
||||
#
|
||||
# Runs as BUILTIN\Users (logged-in user) so it can show GUI (QR code,
|
||||
# status table). Needs the interactive session, NOT SYSTEM.
|
||||
$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"
|
||||
Set-Content -LiteralPath $stageFile -Value 'shopfloor-setup' -Force
|
||||
if (Test-Path -LiteralPath $monitorScript) {
|
||||
try {
|
||||
$action = New-ScheduledTaskAction `
|
||||
-Execute 'powershell.exe' `
|
||||
-Argument "-NoProfile -NoExit -ExecutionPolicy Bypass -File `"$monitorScript`" -AsTask -ConfigureScript `"$configureScript`""
|
||||
|
||||
if (Test-Path -LiteralPath $dispatcherPath) {
|
||||
Log "Registering RunOnce for Stage-Dispatcher.ps1"
|
||||
Set-ItemProperty -Path $runOnceKey -Name 'ShopfloorSetup' `
|
||||
-Value "powershell.exe -NoProfile -ExecutionPolicy Bypass -File `"$dispatcherPath`"" `
|
||||
-Type String -Force
|
||||
$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
|
||||
|
||||
Log "Registered '$taskName' logon task (persists until sync complete)."
|
||||
} catch {
|
||||
Log "WARNING: Failed to register sync task: $_"
|
||||
}
|
||||
} 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
|
||||
|
||||
@@ -258,8 +258,7 @@ if errorlevel 1 (
|
||||
echo WARNING: Failed to copy enrollment package.
|
||||
goto copy_pctype
|
||||
)
|
||||
copy /Y "Y:\run-enrollment.ps1" "W:\run-enrollment.ps1"
|
||||
copy /Y "Y:\shopfloor-setup\Stage-Dispatcher.ps1" "W:\Enrollment\Stage-Dispatcher.ps1"
|
||||
copy /Y "Y:\run-enrollment.ps1" "W:\Enrollment\run-enrollment.ps1"
|
||||
|
||||
REM --- Create enroll.cmd at drive root as manual fallback ---
|
||||
> W:\enroll.cmd (
|
||||
|
||||
Reference in New Issue
Block a user