Imaging chain: Stage-Dispatcher + PPKG reboot + unattended sync_intune
Replaces the single-session "cancel PPKG reboot and cram everything into
one autologon" flow with a staged chain where each reboot advances to the
next step automatically. The technician touches the keyboard 3 times total
(UNPLUG prompt, Y to reboot, Configure-PC selections).
New Stage-Dispatcher.ps1:
Reads C:\Enrollment\setup-stage.txt and chains through:
shopfloor-setup -> sync-intune -> configure-pc
Each stage re-registers HKLM RunOnce so the dispatcher fires again on
the next logon. Stage file is deleted when the chain completes.
Transcript logged to C:\Logs\SFLD\stage-dispatcher.log.
Stage "shopfloor-setup": runs Run-ShopfloorSetup.ps1 (which reboots via
shutdown /r /t 10). Dispatcher advances stage to sync-intune in the
~10 second window before the machine goes down, re-registers RunOnce.
Stage "sync-intune": launches Monitor-IntuneProgress.ps1 -Unattended.
Exit 2 (pre-reboot done, user confirmed): dispatcher re-registers
RunOnce and initiates shutdown /r /t 5. Stage stays at sync-intune so
the monitor picks up post-reboot state on next boot.
Exit 0 (post-reboot install complete): dispatcher chains directly to
Configure-PC.ps1 in the same session, then deletes the stage file.
Stage "configure-pc": runs Configure-PC.ps1 and deletes the stage file.
Fallback entry point if the post-reboot chain was interrupted.
Modified run-enrollment.ps1:
Removed the shutdown /a that canceled the PPKG reboot. Instead writes
setup-stage.txt = "shopfloor-setup" and registers RunOnce for the
dispatcher. PPKG reboot fires naturally (handles PendingFileRename
operations like Zscaler rename and PPKG self-cleanup). Now tracked in
the git repo at playbook/shopfloor-setup/run-enrollment.ps1.
Modified Monitor-IntuneProgress.ps1:
New -Unattended switch. When set:
Invoke-SetupComplete exits 0 without waiting for keypress.
Invoke-RebootPrompt exits 2 without prompting or rebooting (dispatcher
handles both). Manual sync_intune.bat usage (no flag) unchanged.
RetriggerMinutes bumped from 3 to 5 (user request).
Modified startnet.cmd:
Now also copies Stage-Dispatcher.ps1 from the PXE server to
W:\Enrollment\Stage-Dispatcher.ps1 alongside run-enrollment.ps1.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -51,7 +51,13 @@
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[int]$PollSecs = 30,
|
||||
[int]$RetriggerMinutes = 3
|
||||
[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
|
||||
)
|
||||
|
||||
# ============================================================================
|
||||
@@ -547,6 +553,11 @@ function Invoke-SetupComplete {
|
||||
Write-Host "========================================" -ForegroundColor Green
|
||||
Write-Host ""
|
||||
Write-Host "The post-reboot DSC install phase is finished. The device is ready."
|
||||
|
||||
if ($Unattended) {
|
||||
Write-Host "(Unattended mode - returning to dispatcher)"
|
||||
exit 0
|
||||
}
|
||||
Wait-ForAnyKey
|
||||
exit 0
|
||||
}
|
||||
@@ -562,6 +573,15 @@ function Invoke-RebootPrompt {
|
||||
Write-Host "device-config.yaml and runs the per-app wrappers: Install-eDNC,"
|
||||
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
|
||||
}
|
||||
|
||||
# Interactive mode (manual sync_intune.bat): prompt and reboot directly.
|
||||
Write-Host "Press Y to reboot now, or N to cancel: " -NoNewline -ForegroundColor Cyan
|
||||
$ans = Read-SingleKey -ValidKeys @('Y', 'N')
|
||||
Write-Host $ans
|
||||
|
||||
175
playbook/shopfloor-setup/Stage-Dispatcher.ps1
Normal file
175
playbook/shopfloor-setup/Stage-Dispatcher.ps1
Normal file
@@ -0,0 +1,175 @@
|
||||
# Stage-Dispatcher.ps1 - Chain controller for the shopfloor imaging pipeline.
|
||||
#
|
||||
# Reads C:\Enrollment\setup-stage.txt to know which step of the imaging
|
||||
# process we're in, launches the right script, advances the stage, and
|
||||
# re-registers itself via RunOnce so the next logon picks up the next step.
|
||||
#
|
||||
# STAGE CHAIN:
|
||||
# "shopfloor-setup" -> Run-ShopfloorSetup.ps1 (installs apps, deferred org)
|
||||
# Run-ShopfloorSetup reboots via shutdown /r /t 10;
|
||||
# dispatcher has ~10 seconds to write the next stage
|
||||
# and re-register RunOnce before the machine goes down.
|
||||
#
|
||||
# "sync-intune" -> Monitor-IntuneProgress.ps1 -Unattended
|
||||
# Monitor exits 2 = pre-reboot Y pressed (no reboot yet)
|
||||
# -> dispatcher re-registers RunOnce, initiates reboot
|
||||
# Monitor exits 0 = post-reboot install complete
|
||||
# -> dispatcher advances to configure-pc
|
||||
#
|
||||
# "configure-pc" -> Configure-PC.ps1 (startup items, machine number)
|
||||
# -> dispatcher deletes stage file. Chain complete.
|
||||
#
|
||||
# (file missing) -> exit 0. Setup is done, nothing to do.
|
||||
#
|
||||
# ENTRY POINT:
|
||||
# Registered as a RunOnce value by run-enrollment.ps1 after PPKG install.
|
||||
# Each stage re-registers RunOnce for the next boot. RunOnce entries are
|
||||
# consumed by Windows after execution, so no cleanup needed.
|
||||
#
|
||||
# LOGGING:
|
||||
# Appends to C:\Logs\SFLD\stage-dispatcher.log via Start-Transcript.
|
||||
|
||||
$ErrorActionPreference = 'Continue'
|
||||
|
||||
# --- Transcript ---
|
||||
$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 'stage-dispatcher.log'
|
||||
try { Start-Transcript -Path $transcriptPath -Append -Force | Out-Null } catch {}
|
||||
|
||||
Write-Host ""
|
||||
Write-Host "================================================================"
|
||||
Write-Host "=== Stage-Dispatcher.ps1 $(Get-Date -Format 'yyyy-MM-dd HH:mm:ss') ==="
|
||||
Write-Host "================================================================"
|
||||
Write-Host "Running as: $([Security.Principal.WindowsIdentity]::GetCurrent().Name)"
|
||||
|
||||
# --- Paths ---
|
||||
$stageFile = 'C:\Enrollment\setup-stage.txt'
|
||||
$dispatcherPath = $MyInvocation.MyCommand.Path
|
||||
$enrollDir = 'C:\Enrollment'
|
||||
$shopfloorDir = Join-Path $enrollDir 'shopfloor-setup\Shopfloor'
|
||||
$runOnceKey = 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\RunOnce'
|
||||
|
||||
# --- Stage file ---
|
||||
if (-not (Test-Path -LiteralPath $stageFile)) {
|
||||
Write-Host "No stage file found at $stageFile - setup complete, nothing to do."
|
||||
try { Stop-Transcript | Out-Null } catch {}
|
||||
exit 0
|
||||
}
|
||||
|
||||
$stage = (Get-Content -LiteralPath $stageFile -First 1).Trim()
|
||||
Write-Host "Current stage: $stage"
|
||||
|
||||
# --- Helper: re-register this dispatcher to run at next logon ---
|
||||
function Register-NextRun {
|
||||
try {
|
||||
Set-ItemProperty -Path $runOnceKey -Name 'ShopfloorSetup' `
|
||||
-Value "powershell.exe -NoProfile -ExecutionPolicy Bypass -File `"$dispatcherPath`"" `
|
||||
-Type String -Force -ErrorAction Stop
|
||||
Write-Host "RunOnce registered for next logon."
|
||||
} catch {
|
||||
Write-Warning "Failed to register RunOnce: $_"
|
||||
}
|
||||
}
|
||||
|
||||
# --- Stage dispatch ---
|
||||
switch ($stage) {
|
||||
|
||||
'shopfloor-setup' {
|
||||
Write-Host ""
|
||||
Write-Host "--- Stage: shopfloor-setup ---"
|
||||
$script = Join-Path $enrollDir 'Run-ShopfloorSetup.ps1'
|
||||
if (-not (Test-Path -LiteralPath $script)) {
|
||||
Write-Warning "Run-ShopfloorSetup.ps1 not found at $script"
|
||||
break
|
||||
}
|
||||
|
||||
# Run-ShopfloorSetup.ps1 calls shutdown /r /t 10 at the end, which
|
||||
# gives us a ~10 second window after it returns to advance the stage
|
||||
# and re-register RunOnce before the reboot fires.
|
||||
& $script
|
||||
|
||||
Write-Host "Run-ShopfloorSetup.ps1 finished. Advancing stage to sync-intune."
|
||||
Set-Content -LiteralPath $stageFile -Value 'sync-intune' -Force
|
||||
Register-NextRun
|
||||
Write-Host "Reboot imminent (initiated by Run-ShopfloorSetup.ps1)."
|
||||
}
|
||||
|
||||
'sync-intune' {
|
||||
Write-Host ""
|
||||
Write-Host "--- Stage: sync-intune ---"
|
||||
$monitor = Join-Path $shopfloorDir 'lib\Monitor-IntuneProgress.ps1'
|
||||
if (-not (Test-Path -LiteralPath $monitor)) {
|
||||
Write-Warning "Monitor-IntuneProgress.ps1 not found at $monitor"
|
||||
break
|
||||
}
|
||||
|
||||
# Launch the monitor in THIS console window (interactive - tech sees
|
||||
# QR code + status table). -Unattended makes it exit with a status
|
||||
# code instead of prompting/rebooting, so we keep control here.
|
||||
Write-Host "Launching Intune sync monitor (unattended mode)..."
|
||||
& $monitor -Unattended
|
||||
$monitorExit = $LASTEXITCODE
|
||||
Write-Host "Monitor exited with code: $monitorExit"
|
||||
|
||||
switch ($monitorExit) {
|
||||
0 {
|
||||
# Post-reboot install complete. All milestones reached.
|
||||
# Chain directly to Configure-PC (same session, no reboot).
|
||||
Write-Host "Post-reboot install complete. Launching Configure-PC..."
|
||||
Set-Content -LiteralPath $stageFile -Value 'configure-pc' -Force
|
||||
|
||||
$configScript = Join-Path $shopfloorDir 'Configure-PC.ps1'
|
||||
if (Test-Path -LiteralPath $configScript) {
|
||||
& $configScript
|
||||
} else {
|
||||
Write-Warning "Configure-PC.ps1 not found at $configScript"
|
||||
}
|
||||
|
||||
# Chain complete.
|
||||
Write-Host "Imaging chain complete. Removing stage file."
|
||||
Remove-Item -LiteralPath $stageFile -Force -ErrorAction SilentlyContinue
|
||||
}
|
||||
2 {
|
||||
# Pre-reboot deployment done. User confirmed reboot.
|
||||
# Re-register RunOnce so we come back after the reboot to
|
||||
# monitor the post-reboot install phase.
|
||||
Write-Host "Pre-reboot phase complete. Rebooting..."
|
||||
Register-NextRun
|
||||
# Stage stays at 'sync-intune' - on next boot the monitor
|
||||
# will detect post-reboot state and monitor until done.
|
||||
shutdown /r /t 5
|
||||
}
|
||||
default {
|
||||
Write-Warning "Unexpected monitor exit code: $monitorExit"
|
||||
Write-Host "Re-registering for retry on next logon."
|
||||
Register-NextRun
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
'configure-pc' {
|
||||
Write-Host ""
|
||||
Write-Host "--- Stage: configure-pc ---"
|
||||
$configScript = Join-Path $shopfloorDir 'Configure-PC.ps1'
|
||||
if (Test-Path -LiteralPath $configScript) {
|
||||
& $configScript
|
||||
} else {
|
||||
Write-Warning "Configure-PC.ps1 not found at $configScript"
|
||||
}
|
||||
|
||||
Write-Host "Imaging chain complete. Removing stage file."
|
||||
Remove-Item -LiteralPath $stageFile -Force -ErrorAction SilentlyContinue
|
||||
}
|
||||
|
||||
default {
|
||||
Write-Warning "Unknown stage: '$stage'. Removing stage file."
|
||||
Remove-Item -LiteralPath $stageFile -Force -ErrorAction SilentlyContinue
|
||||
}
|
||||
}
|
||||
|
||||
Write-Host ""
|
||||
Write-Host "=== Stage-Dispatcher.ps1 finished $(Get-Date -Format 'yyyy-MM-dd HH:mm:ss') ==="
|
||||
try { Stop-Transcript | Out-Null } catch {}
|
||||
78
playbook/shopfloor-setup/run-enrollment.ps1
Executable file
78
playbook/shopfloor-setup/run-enrollment.ps1
Executable file
@@ -0,0 +1,78 @@
|
||||
# run-enrollment.ps1
|
||||
# Installs GCCH enrollment provisioning package via Install-ProvisioningPackage
|
||||
# Called by FirstLogonCommands as SupportUser (admin) after imaging
|
||||
|
||||
$ErrorActionPreference = 'Continue'
|
||||
$logFile = "C:\Logs\enrollment.log"
|
||||
New-Item -ItemType Directory -Path "C:\Logs" -Force -ErrorAction SilentlyContinue | Out-Null
|
||||
|
||||
function Log {
|
||||
param([string]$Message)
|
||||
$ts = Get-Date -Format "yyyy-MM-dd HH:mm:ss"
|
||||
$line = "$ts $Message"
|
||||
Write-Host $line
|
||||
Add-Content -Path $logFile -Value $line
|
||||
}
|
||||
|
||||
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 "Package: $($ppkgFile.Name)"
|
||||
|
||||
# --- Set computer name to E<serial> ---
|
||||
$serial = (Get-CimInstance Win32_BIOS).SerialNumber
|
||||
$newName = "E$serial"
|
||||
Log "Setting computer name to $newName"
|
||||
Rename-Computer -NewName $newName -Force -ErrorAction SilentlyContinue
|
||||
|
||||
# --- Install provisioning package ---
|
||||
Log "Installing provisioning package..."
|
||||
try {
|
||||
Install-ProvisioningPackage -PackagePath $ppkgFile.FullName -ForceInstall -QuietInstall
|
||||
Log "Provisioning package installed successfully."
|
||||
} catch {
|
||||
Log "ERROR: Install-ProvisioningPackage failed: $_"
|
||||
Log "Attempting fallback with Add-ProvisioningPackage..."
|
||||
try {
|
||||
Add-ProvisioningPackage -PackagePath $ppkgFile.FullName -ForceInstall -QuietInstall
|
||||
Log "Provisioning package added successfully (fallback)."
|
||||
} catch {
|
||||
Log "ERROR: Fallback also failed: $_"
|
||||
exit 1
|
||||
}
|
||||
}
|
||||
|
||||
# --- Set OOBE complete ---
|
||||
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'
|
||||
|
||||
Log "Writing stage file: shopfloor-setup"
|
||||
Set-Content -LiteralPath $stageFile -Value 'shopfloor-setup' -Force
|
||||
|
||||
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
|
||||
} else {
|
||||
Log "WARNING: Stage-Dispatcher.ps1 not found at $dispatcherPath - RunOnce not set"
|
||||
}
|
||||
|
||||
Log "=== Enrollment complete. PPKG reboot will fire and Stage-Dispatcher picks up on next logon. ==="
|
||||
@@ -224,6 +224,7 @@ if errorlevel 1 (
|
||||
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"
|
||||
|
||||
REM --- Create enroll.cmd at drive root as manual fallback ---
|
||||
> W:\enroll.cmd (
|
||||
|
||||
Reference in New Issue
Block a user