BPRT was stopping after the first RestartRequired=true command (DotNet35). Test image captured 2026-04-15 showed 3 of 21 PPKG commands ran (PPKG Version Check, Lock Screen, DotNet35) before provtool exited 0 leaving Office / Chrome / Tanium / Activate-Windows / Enable-DeviceLockdown / Hide-SupportUser / 12 more scripts unexecuted. Symptom: criticalChecks said EntraID NOT joined (wrong -- it was), sessions.json showed a 'LogonIdleTask' session perpetually 'Not started', and the resulting PC was missing most of its fleet software. BPRT is the OOBE runtime source -- it expects the OOBE engine to own the post-DotNet35 reboot + resume. In our post-autounattend context there is no OOBE engine, so restart-required commands stall the pipeline. PSCmdlet is the source Install-ProvisioningPackage uses internally and has the correct resume semantics for post-OOBE application. The original motivation for BPRT (avoiding the 180s PowerShell timeout) does not apply because we invoke provtool.exe directly, not via the Install-ProvisioningPackage cmdlet. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
159 lines
7.4 KiB
PowerShell
Executable File
159 lines
7.4 KiB
PowerShell
Executable File
# run-enrollment.ps1
|
|
# Installs GCCH enrollment provisioning package. That's it.
|
|
#
|
|
# Install-ProvisioningPackage triggers an immediate reboot -- nothing after
|
|
# that call executes. The sync_intune task and all other post-enrollment
|
|
# setup are registered by Run-ShopfloorSetup.ps1 BEFORE calling this script.
|
|
|
|
$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 "No .ppkg found in C:\Enrollment\ - skipping enrollment."
|
|
return
|
|
}
|
|
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 ---
|
|
# IMPORTANT: The PPKG must be installed BEFORE OOBEComplete is set. Bulk
|
|
# enrollment PPKGs are designed to run during OOBE; on Windows 11 22H2+ they
|
|
# can hang indefinitely if OOBE is already marked complete.
|
|
#
|
|
# We invoke provtool.exe directly instead of Install-ProvisioningPackage.
|
|
# The PowerShell cmdlet enforces a hardcoded 180-second timeout on the
|
|
# underlying provtool call, which a 7-8 GB GCCH PPKG often exceeds on
|
|
# slower disks. When the cmdlet times out it throws, and the Add-
|
|
# ProvisioningPackage fallback has been observed to invoke provtool with
|
|
# an empty packagePathsToAdd (session registered but never started),
|
|
# leaving the PC un-enrolled. provtool.exe directly has no caller-side
|
|
# timeout; Start-Process -Wait waits on the actual child process.
|
|
#
|
|
# The PPKG triggers an IMMEDIATE reboot once fully applied. Nothing below
|
|
# that point executes on the current boot. BPRT app installs (Chrome,
|
|
# Office, Tanium, etc.) happen on the next boot. The sync_intune
|
|
# scheduled task (registered by Run-ShopfloorSetup.ps1 before calling us)
|
|
# fires at the next logon to monitor Intune enrollment.
|
|
$ppkgLogDir = "C:\Logs\PPKG"
|
|
New-Item -ItemType Directory -Path $ppkgLogDir -Force -ErrorAction SilentlyContinue | Out-Null
|
|
$provtool = Join-Path $env:SystemRoot 'System32\provtool.exe'
|
|
# Arg order matches what the Install-ProvisioningPackage cmdlet invokes
|
|
# internally (observed in ProvEventLog.txt): positional path, then /quiet,
|
|
# then /source. No /log: or /ppkg: prefix - those are not valid provtool
|
|
# flags and caused 0x80004005 E_FAIL in the first test.
|
|
#
|
|
# /source PSCmdlet matches what Install-ProvisioningPackage invokes
|
|
# internally and is the correct post-OOBE context. BPRT was tried first
|
|
# and verified to stop after the first RestartRequired command (DotNet35):
|
|
# only 3 of 21 commands ran (PPKG Version Check, Lock Screen, DotNet35),
|
|
# leaving Office/Chrome/Tanium/Activate-Windows etc never executed
|
|
# because BPRT expects the OOBE runtime to own the reboot-and-resume
|
|
# loop, and there is no OOBE runtime here. PSCmdlet registers a
|
|
# RunOnce-style resume handler so the remaining commands continue after
|
|
# the reboot Run-ShopfloorSetup issues. Timeout concerns that previously
|
|
# motivated BPRT don't apply here because we invoke provtool.exe
|
|
# directly, not via the 180s-capped Install-ProvisioningPackage cmdlet.
|
|
$provArgs = @("`"$($ppkgFile.FullName)`"", "/quiet", "/source", "PSCmdlet")
|
|
|
|
# Enable the Provisioning-Diagnostics-Provider Admin channel so events
|
|
# from the BPRT run land somewhere we can export afterward. This is
|
|
# idempotent - running each time is safe.
|
|
wevtutil.exe set-log 'Microsoft-Windows-Provisioning-Diagnostics-Provider/Admin' /enabled:true 2>$null | Out-Null
|
|
Log "Installing provisioning package via provtool.exe (no PowerShell timeout)..."
|
|
Log "Command: $provtool $($provArgs -join ' ')"
|
|
Log "PPKG diagnostic logs -> $ppkgLogDir (provtool writes them automatically)"
|
|
try {
|
|
$p = Start-Process -FilePath $provtool -ArgumentList $provArgs -Wait -PassThru -NoNewWindow -ErrorAction Stop
|
|
Log "provtool.exe exit code: $($p.ExitCode)"
|
|
if ($p.ExitCode -ne 0) {
|
|
$hex = '0x{0:X8}' -f $p.ExitCode
|
|
Log "WARNING: provtool.exe returned non-zero exit code ($hex). Check $ppkgLogDir for diagnostic bundle."
|
|
}
|
|
} catch {
|
|
Log "ERROR: Failed to launch provtool.exe: $_"
|
|
}
|
|
|
|
# --- Harvest Windows' own provisioning diagnostics into $ppkgLogDir ---
|
|
# provtool.exe /quiet does not drop a zip bundle on success, so the real
|
|
# detail lives under C:\ProgramData\Microsoft\Provisioning\, in the
|
|
# registry under HKLM\Software\Microsoft\Provisioning\Sessions\*, and in
|
|
# the Provisioning-Diagnostics-Provider event log. Copy/export all three
|
|
# into our log dir so they ride back with the shopfloor logs bundle.
|
|
Log "Harvesting Windows provisioning diagnostics to $ppkgLogDir..."
|
|
try {
|
|
$provData = 'C:\ProgramData\Microsoft\Provisioning'
|
|
if (Test-Path $provData) {
|
|
Copy-Item -Path (Join-Path $provData '*') -Destination $ppkgLogDir `
|
|
-Recurse -Force -ErrorAction SilentlyContinue
|
|
Log " copied $provData -> $ppkgLogDir"
|
|
} else {
|
|
Log " $provData not present (provtool may not have touched it)"
|
|
}
|
|
} catch {
|
|
Log " WARN: ProgramData copy threw: $_"
|
|
}
|
|
|
|
try {
|
|
$sessions = Get-ChildItem 'HKLM:\Software\Microsoft\Provisioning\Sessions' -ErrorAction SilentlyContinue
|
|
if ($sessions) {
|
|
$snap = $sessions | ForEach-Object {
|
|
$props = Get-ItemProperty $_.PSPath -ErrorAction SilentlyContinue
|
|
if ($props) {
|
|
[pscustomobject]@{
|
|
Session = $_.PSChildName
|
|
BeginTime = $props.BeginTime
|
|
LastRunTime = $props.LastRunTime
|
|
RebootCount = $props.RebootCount
|
|
State = $props.State
|
|
StateValue = $props.StateValue
|
|
}
|
|
}
|
|
}
|
|
$snap | ConvertTo-Json -Depth 3 |
|
|
Out-File -FilePath (Join-Path $ppkgLogDir 'provisioning-sessions.json') -Encoding UTF8
|
|
Log " wrote provisioning-sessions.json ($($snap.Count) session(s))"
|
|
foreach ($s in $snap) {
|
|
Log " session $($s.Session): State=$($s.State) RebootCount=$($s.RebootCount)"
|
|
}
|
|
} else {
|
|
Log " no sessions under HKLM:\Software\Microsoft\Provisioning\Sessions"
|
|
}
|
|
} catch {
|
|
Log " WARN: session snapshot threw: $_"
|
|
}
|
|
|
|
try {
|
|
$evtx = Join-Path $ppkgLogDir 'Provisioning-Diagnostics-Admin.evtx'
|
|
if (Test-Path $evtx) { Remove-Item $evtx -Force -ErrorAction SilentlyContinue }
|
|
$null = & wevtutil.exe epl 'Microsoft-Windows-Provisioning-Diagnostics-Provider/Admin' $evtx 2>&1
|
|
if (Test-Path $evtx) { Log " exported $evtx" }
|
|
} catch {
|
|
Log " WARN: wevtutil export threw: $_"
|
|
}
|
|
|
|
# --- Set OOBE complete (only reached if PPKG didn't trigger immediate reboot) ---
|
|
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
|
|
|
|
# If we get here, the PPKG didn't reboot immediately. Unlikely but handle it.
|
|
Log "PPKG did not trigger immediate reboot. Returning to caller."
|